Skip to content

2026-08-26 -- Phase 3 batch M: the panel explains itself

This content is not available in your language yet.

Five UX-operability items (UXO-4/5/6/7/8) plus one authorization-surface label fix (UXA-6). The through-line: states the panel already had but never narrated — a hung connect, a submitted login code, a disabled picker, a Send button that silently became Stop — each now says what is happening and what the user can do about it. Every decision is a pure helper with a table test; the views only render the answer.

UXO-8: the composer narrates its own primary action

Section titled “UXO-8: the composer narrates its own primary action”

ResolvePrimaryAction (deliberate, test-pinned) flips the primary button Send<->Stop with the field’s emptiness while a turn runs — but the flip itself was silent: nothing explained that Enter would now append to the RUNNING turn. DescribeActionHint(turnActive, fieldHasText) drives the hint line under the field (“Enter sends into the running turn; Esc stops it” when text is present, the existing “Esc to stop” when not), and the button carries a per-state tooltip. Four-way table in ComposerPrimaryActionTests.

UXO-7: a slow connect escalates instead of spinning forever

Section titled “UXO-7: a slow connect escalates instead of spinning forever”

“Connecting…” looked identical at 2 seconds and at 2 minutes. AgentHub now stamps StartingSinceUtcTicks on every state change (non-zero only while Starting); StatusBarView re-renders once a second (schedule.Execute — an unchanged hub state never re-raises Changed, so the 10s threshold needs wall-clock ticks) and past SlowConnectThresholdMs (10s, the UX spec’s escalation rule) swaps in “Still connecting…” plus an inline Reconnect button. The decision is ShouldShowSlowConnect(state, elapsedMs, thresholdMs) — pure, and a zero/unset start tick computes elapsed as -1, which fails toward the calm text.

UXO-5: the model picker works before the first connect

Section titled “UXO-5: the model picker works before the first connect”

The header picker was a DEAD BUTTON until the first initialize response (and stayed one when the CLI was down) — clicks silently ignored, no explanation. ResolveModelPickerState(hasLiveOptions, hasCachedCatalog, streaming) now decides {Enabled, Live/Cache/Empty}: live options win (disabled only mid-stream, where set_model would race the turn); with no live options the persisted catalog cache (PanelSettings.modelCatalog, refreshed on every live connection) backs the menu, the checkmark marks the persisted panel default, and a selection goes through AgentHub.SetDefaultModel — the Settings dropdown’s next-spawn-only write, NEVER SwitchSessionModel (a silent no-op with no client); with neither, the button stays enabled and the click shows one disabled item (“Models can be chosen after connecting”) instead of swallowing the click. The default-suffix is skipped in cache mode: with no running model, “default” and “current” collapse into one fact and the checkmark already states it.

UXO-6: the login code flow acknowledges the code

Section titled “UXO-6: the login code flow acknowledges the code”

Submitting a code changed NOTHING on screen (the CLI gives no per-code acknowledgement), and a rejected code ended as a silently vanished sub-card — indistinguishable from success until the status line disappointed. Two pieces:

  • ResolveAccountLoginFeedback(loginInFlight, waitingForCode, codeSubmitted) — pure; a submitted code means Verifying (field and Submit lock, instruction says “Verifying the code…”) until the process exits.
  • The failure verdict CANNOT be decided in flight: AgentHub clears CurrentLoginSession the moment the process exits and re-queries auth status, so RefreshAccountSection remembers the AuthStatus INSTANCE visible at exit time and judges on the first tick that shows a different one (reference identity, the same exact-key trick the model memoization uses). Not logged in -> a red failed-login notice under the card, cleared by the next Log in press or a successful login. A deliberate Cancel suppresses the verdict — cancelling is not failing.

UXO-4: the first-run login card leads with the one-click path

Section titled “UXO-4: the first-run login card leads with the one-click path”

The not-logged-in card OPENED with a terminal walkthrough (run claude, type /login) and buried its own in-panel login button below, next to a same-weight Check-again. Inverted: the card leads with the primary Log in button and a lead sentence saying exactly what pressing it does (the Settings Account card opens with the browser link and code field; the panel reconnects itself on success). The terminal path survives, demoted into a collapsed “Another way” foldout together with its Check-again button. Tests pin exactly one uap-card-btn—primary per setup card and the foldout’s collapsed default.

UXA-6: the Always menu speaks human, persistence speaks wire

Section titled “UXA-6: the Always menu speaks human, persistence speaks wire”

“Always allow mcp__unity-ops__uap_scene_create_object” — protocol plumbing on the one label whose whole job is telling the user what they are granting. FormatRuleForDisplay reformats the menu label only: the tool name goes through ToolCardDescriber.FormatMcpNameWithServer (server kept, per that method’s authorisation-surface contract), a “(ruleContent)” scope rides along verbatim, and an MCP rule with NO scope — the synthesized every-argument grant — now says “(all calls to this tool)” instead of looking narrower than it is. The persisted rule string is untouched wire grammar; a test pins that the pretty label never leaks into TryExtractAddRuleToolName.

  • UXO-6 cannot detect the CLI re-prompting for a SECOND code after a wrong one (AuthLoginSession raises WaitingForCode once); the locked field stays locked until exit, so a re-prompting CLI requires Cancel + Log in again. Measured behavior so far is exit-on-bad-code, which this handles.
  • UXO-5’s cached catalog can be stale (models added/removed server-side since the last connection); the first live initialize replaces it, and a stale pick still round-trips through the CLI’s own validation at spawn.
  • UXO-7’s 1s scheduler tick runs while the status bar is attached regardless of state — one Refresh per second was measured as noise- level (the same Refresh already runs per Changed event, far hotter during streaming).