Skip to content

fix(app): stop the Team model selector deadlocking on its first pick - #22

Closed
Rwanbt wants to merge 1 commit into
devfrom
fix/team-selector-min-models-deadlock
Closed

fix(app): stop the Team model selector deadlocking on its first pick#22
Rwanbt wants to merge 1 commit into
devfrom
fix/team-selector-min-models-deadlock

Conversation

@Rwanbt

@Rwanbt Rwanbt commented Jul 30, 2026

Copy link
Copy Markdown
Owner

Problem

On a fresh Android install, every tap in the Team models picker raised
Échec de la configuration Team / Failed to save global Team configuration, and no model
could ever be checked. The Team selector was permanently unusable.

This is not the stale-sidecar / SPA-fallback bug fixed previously — verified: the
/team/config route is present both in source and in the embedded mobile CLI bundle.

Root cause

A cardinality contract that the client did not know about:

  1. team-model-selector.tsx persisted every toggle. Guards existed for dropping below 2
    and exceeding 8, but none for the 0 → 1 ascent.
  2. The server schema requires .min(2) (packages/opencode/src/team/selection.ts), enforced
    by validator("json", TeamSelection) on PUT /team/config → one model is rejected 400.
  3. setSelected(next) ran after await save. The save always failed, so selected()
    stayed [] and the next tap replayed the same 400.

Reaching two models required passing through one, so it was structurally unreachable.

Desktop escaped it because the TUI dialog batches the whole selection behind a ready() gate
and only saves on confirm — users who configured there already had a valid stored selection
that onMount reloaded.

Fix

  • New pure planTeamToggle() returning reject | stage | persist. Sub-minimal selections are
    staged locally and never sent; persistence happens on the toggle that reaches the minimum.
    Kept pure deliberately: packages/app has no testing-library, so an inline JSX guard would
    have shipped untested.
  • MIN_TEAM_MODELS replaces the 2 hardcoded on both sides of the network boundary. The
    server schema remains the source of truth.
  • Server error detail is now surfaced on Team and Debate save failures — a validation 400
    and a network failure previously produced the same opaque toast, which is exactly what made
    this bug unreadable on device.
  • validateTeamSelection now requires two available models rather than all of them, and the
    persisted selection is filtered against connected providers on mount (mirrors
    DebateModelSelector). One retired model no longer invalidates a usable selection.
  • The FAVORITES_GROUP sentinel used a raw NUL byte, which made git classify the file as
    binary — no diffs, no review. Replaced with the \0 escape: identical string value, file is
    textual again.

Same-class sweep

All six call sites of team.config / debate.config / .save() were inspected. Every other
one already gated correctly before persisting (dialog-team-setup.tsx batches behind
ready(), debate-model-selector.tsx returns early below one participant, submit.ts gates
on isValid). No other occurrence of "incremental persistence against a minimum-cardinality
schema".

Verification

  • bun test --preload ./happydom.ts ./src695 pass / 0 fail (687 + 8 new).
  • bun run typecheck → PASS. biome check → clean.
  • Device test on a Mi 10 Pro (aarch64 release build, signed, installed over the previous
    build): first pick no longer errors, second pick persists, selection survives a restart, and
    removing below the minimum still shows the expected toast.

The 8 new tests pin the invariant directly: the first pick is staged and never sent, and
persistence only occurs at MIN_TEAM_MODELS.

Not included

The regenerated packages/mobile/**/opencode-cli.js bundles are left out. They are build
artifacts whose working-tree diff is dominated by CRLF churn and unrelated regeneration drift,
so they do not belong in this fix.

The selector persisted every toggle, but the server schema requires at least two
distinct models and answers 400 below that. Since `selected()` only advanced
after a successful save, the first pick of an empty selection failed forever:
reaching two models required passing through one. On a fresh Android install the
Team selector was therefore permanently unusable, showing only "Failed to save
global Team configuration" on every tap.

Extract the toggle decision into a pure `planTeamToggle()` that stages
sub-minimal selections locally and only persists once MIN_TEAM_MODELS is reached.
`MIN_TEAM_MODELS` replaces the `2` that was hardcoded on both sides of the
network boundary; the server schema stays the source of truth. The decision is
pure because packages/app has no testing-library, so an inline guard in the JSX
would have shipped without any test.

Also in this commit:

- Surface the server error detail when saving a Team or Debate configuration
  fails. A validation 400 and a network failure previously raised the same
  opaque message, which is what made this bug unreadable on device.
- Relax `validateTeamSelection` to require two available models rather than all
  of them, and filter the persisted selection against connected providers on
  mount, mirroring DebateModelSelector. A single retired model no longer
  invalidates an otherwise usable selection.
- Replace the raw NUL byte in the FAVORITES_GROUP sentinel with the `\0` escape.
  Same string value, but git no longer classifies the file as binary, so the
  component is diffable and reviewable again.

Verified on a Mi 10 Pro (aarch64 release build): first pick no longer errors,
the second persists, and the selection survives a restart. 695 app tests pass,
including 8 new ones pinning that a sub-minimal selection is never sent.
@github-actions

Copy link
Copy Markdown

This PR doesn't fully meet our contributing guidelines and PR template.

What needs to be fixed:

  • PR description is missing required template sections. Please use the PR template.

Please edit this PR description to address the above within 2 hours, or it will be automatically closed.

If you believe this was flagged incorrectly, please let a maintainer know.

@github-actions

Copy link
Copy Markdown

Thanks for your contribution!

This PR doesn't have a linked issue. All PRs must reference an existing issue.

Please:

  1. Open an issue describing the bug/feature (if one doesn't exist)
  2. Add Fixes #<number> or Closes #<number> to this PR description

See CONTRIBUTING.md for details.

@github-actions

Copy link
Copy Markdown

This pull request has been automatically closed because it was not updated to meet our contributing guidelines within the 2-hour window.

Feel free to open a new pull request that follows our guidelines.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant