Repository navigation
feat(board): show PR progress for finished sessions - #4916
Conversation
There was a problem hiding this comment.
🟡 Changes recommended
A newly added inline comment in SessionCardView is inaccurate relative to the card’s actual PR rendering behavior and should be reworded for correctness.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
Adds an additional UI affordance on the Sessions Board to make “finished” sessions (Merged/Terminated) show exact pull request completion progress without changing the daemon-derived lifecycle status label, helping clarify cases like “terminated but still has an open PR”.
Changes:
- Add a computed PR progress footer line on merged/terminated session cards (e.g., “1 of 2 PRs merged · 1 open”).
- Add test coverage for the new progress line behavior in both the product-ui package and the desktop renderer integration tests.
- Introduce localized strings for the “X of Y PRs merged” phrase across all supported desktop locales.
File summaries
| File | Description |
|---|---|
| packages/product-ui/src/SessionsBoardView.tsx | Adds progress counting + a full-width footer line on merged/terminated cards. |
| packages/product-ui/src/SessionsBoardView.test.tsx | Adds unit test coverage for progress footer rendering and non-rendering cases. |
| frontend/src/renderer/components/SessionsBoardAdapters.tsx | Wires localized progress label generation into desktop card labels. |
| frontend/src/renderer/components/SessionsBoard.test.tsx | Asserts the progress footer appears in the desktop board for a terminated session. |
| frontend/src/renderer/i18n/en.json | Adds pluralized “merged progress” translation keys. |
| frontend/src/renderer/i18n/de.json | Adds pluralized “merged progress” translation keys. |
| frontend/src/renderer/i18n/es.json | Adds pluralized “merged progress” translation keys. |
| frontend/src/renderer/i18n/fr.json | Adds pluralized “merged progress” translation keys. |
| frontend/src/renderer/i18n/ja.json | Adds pluralized “merged progress” translation keys. |
| frontend/src/renderer/i18n/ko.json | Adds pluralized “merged progress” translation keys. |
| frontend/src/renderer/i18n/pt-BR.json | Adds pluralized “merged progress” translation keys. |
| frontend/src/renderer/i18n/zh-CN.json | Adds pluralized “merged progress” translation keys. |
Review details
- Files reviewed: 12/12 changed files
- Comments generated: 1
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
|
Reviewed the change. On the code side this is clean, and the motivation holds up. Why it is worth doing: it is a follow-up to #2390. That fix made a terminated session that still owns an open PR correctly show "Terminated" instead of "Merged" - which also removed the signal that some of its PRs did merge. This progress line ("1 of 2 PRs merged, 1 open") restores that lost visibility on finished cards, without touching status derivation or Kanban placement. Presentation-only, so the risk is contained. Code and i18n look correct:
One thing before this can be approved: since it is a visual change, could you attach a screenshot of the progress line on a real finished card - ideally a terminated session with one merged and one open PR ("Terminated" plus "1 of 2 PRs merged, 1 open"), and an all-merged one? I want to confirm the rendered line matches the intended copy and lays out correctly on the card before signing off. The frontend tests can't be run in my environment, so the screenshot is the confirmation I'm missing. No inline comments - nothing to fix in the code itself. |
illegalcall
left a comment
There was a problem hiding this comment.
Requesting changes for two user-visible correctness issues noted inline. Also, commit cf90467 (Update comments in SessionsBoardView.tsx) does not follow the Conventional Commit rule in AGENTS.md; please amend it to a conventional subject such as docs(board): clarify PR progress comments.
e565d00 to
38ddb37
Compare
Both inline correctness issues fixed and pushed in earlier commits (isTerminated gate for merged sessions; localized count-aware progress suffixes). Commit cf90467 reworded to docs(board): clarify PR progress comments via targeted history rewrite (content unchanged, verified byte-identical trees) and force-pushed.
|
@illegalcall fixed the live-session gate and localized plurals, added regression tests, and corrected the commit subject. All CI checks pass. Could you take another look? |
Co-authored-by: Copilot Autofix powered by AI <[email protected]>
…ixes A session's status can already read "merged" before the daemon marks it terminated, so the finished-card PR progress footer now also requires isTerminated for merged sessions (not just for the terminated status). Reword the stale inline comment about when the footer renders, and replace the English-only "N open/draft/closed" suffixes with count-aware locale keys across all eight supported locales.
31b7d19 to
92793ca
Compare
illegalcall
left a comment
There was a problem hiding this comment.
Approved after the follow-up fixes. Live merged sessions are gated on real termination, counted PR states are localized, and the English draft plural is corrected in c2343a8. All current checks pass.
* fix(frontend): restore terminal focus after fullscreen and restart (#4988) * fix(frontend): restore terminal focus after controls * fix(frontend): guard deferred fullscreen focus and keep restart focus target Cancel and re-check pending fullscreen focus-restoration frames so a retained terminal cannot steal focus back after it's hidden or unmounted mid-transition. Keep the restart control focused during the pending orchestrator restart, blurring it only right before successful navigation, so keyboard users retain a focus target for the whole operation and the failure dialog can take focus normally on error. Addresses review comments from PR #4988. * fix(frontend): preserve focus during deferred fullscreen completion * fix(frontend): retain retry dialog focus while restart is pending --------- Co-authored-by: AO Tests <[email protected]> * feat(desktop): refresh Ruto app icons across platforms (#5079) * feat(desktop): refresh Ruto macOS app icon * feat(desktop): use approved Ruto icon on Windows --------- Co-authored-by: itrytoohard <[email protected]> * fix(workspace): keep a backslash in a filename on Linux and macOS (#5055) `cleanWorkspaceRelativePath` folded every backslash into "/" unconditionally. Backslash is a path separator only on Windows; on Linux and macOS it is an ordinary filename character, so a workspace file named `weird\name.txt` could not be opened through the workspace-files API — and worse, the request silently resolved to `weird/name.txt`, a different path that may exist and hold something else. The file had no `runtime.GOOS` reference at all, so the normalization applied everywhere rather than only where it is correct. The traversal guards are untouched: absolute paths, empty input, `..` and `../`-prefixed paths are still rejected, and the second test pins that so the change cannot be read as loosening validation. Fails without the fix with: workspace_files_path_test.go:22: got "dir/weird/name.txt", want "dir/weird\\name.txt"; a backslash must stay part of the filename on this platform * fix: finalize project and workspace import flows (#4945) * feat(import): add project import validation api * feat(import): add workspace import onboarding flow (#4788) * feat(import): add workspace import onboarding * fix(import): address workspace onboarding CI failures * fix(import): regenerate API schema drift * feat(ui): add project creation progress * feat(import): validate folders through daemon * fix(import): use validation result for setup state * feat(import): add project import onboarding flow * fix: simplify project import setup modal * test: align sidebar import flow with validation api * fix: show import setup progress and reuse orchestrator spawn * fix: reroute duplicate project imports * fix: preserve local branch on project import * fix: allow local default branches for imported repos * fix: default import remote suggestion * fix: clarify remote import requirement * fix: shorten remote import warning * fix: warn about child git repos on import * fix: align renderer tests with import flow * fix: sync e2e bridge with import preload * fix: localize project import onboarding copy * fix: sync import onboarding locale catalogs * feat: add harness login actions in settings (#4523) * feat: add agent authentication plans * feat: open trusted command terminals * fix: respect canceled initial input waits * feat: launch native agent login flows * feat: expose agent authentication API * feat: add harness login actions to settings * fix: document agent auth actions * fix: harden harness login flow * fix: address harness auth review * fix: harden harness auth review gaps * fix: isolate harness authentication terminals * feat: keep harness authentication inline * fix: correct harness authentication flows * fix: harden harness auth terminal isolation * docs: link kimchi authentication guidance * fix: align harness auth with readiness * fix: satisfy Go CI checks * fix: address harness auth review feedback * fix: require explicit interactive auth action * fix: make interactive login delivery reliable * fix: support both qwen input modes * fix: align harness auth flows with official docs * fix: route aider setup to official docs * fix: hide diagnostics after successful install * fix: prefer official harness installers * fix: require interactive Kiro install on macOS * test: update conpty session resolver call * fix: tighten Help report form submit button layout (#4786) * fix: tighten Help report form submit button layout * fix: polish Help report form per review feedback Match browser profile input radius, make the details field scroll-only, and replace the destination segment with GitHub/Discord/Email icon actions. * fix: restore Recent projects subtitle on home page (#4855) * fix(ui): put the caret where you have to type (#4847) * fix(ui): put the caret in the New task composer when it opens Opening New task from the sidebar project menu (dropdown or right-click) left the prompt without a caret: the user had to click the field before typing. Two separate focus steals from the closing Radix menu: 1. The menu still traps focus during the commit in which it closes, so it pulls focus straight back out of whatever the dialog just autofocused. The prompt now claims focus from an effect and re-asserts for two frames while focus sits outside its overlay, instead of relying on the `autoFocus` attribute that loses that race. 2. FocusScope restores focus to the menu trigger a tick after the menu unmounts, landing on top of the open dialog. The shared dropdown and context menu content now skip that restore when another surface already holds focus. A menu that closes without opening anything leaves focus on `document.body`, so Escape and plain selection keep Radix's normal return-to-trigger behaviour for keyboard users. Skipping the restore is not enough on its own: the dialog cannot hand focus back afterwards, because Radix captured its return target when the dialog mounted and that was the menu item, which no longer exists. So the menu records where focus should return to while it is open, the trigger for a dropdown and the previously focused element for a context menu, which are the same targets Radix would have used, and hands it back once the surface that borrowed the caret releases it. The menu fix also keeps focus inside any other dialog opened from a menu item; the Switch agent dialog used to leave focus on the Session actions button behind it, so the next Tab escaped the dialog. * fix(terminal): focus a worker terminal when its session is on screen Opening a TUI session or switching between two of them left the caret nowhere: focus stayed on the sidebar button that was clicked, and keystrokes were dropped until the user clicked into the terminal. A worker terminal only asked for focus while an agent switch was in flight, because `presentation` in CenterPane is the agent-switch presentation and is undefined the rest of the time: focusRequested={target.kind === "shell" || (target.kind === "worker" && (Boolean(presentation?.allowSourceInput) || Boolean(displayedSuccessNotice)))} So the only paths that focused a terminal were a standalone shell, a switch that allows source input, and the transient success notice. A terminal you can type into now holds the caret as soon as it is on screen, the same as the chat composer. Worker input is still left alone while it is genuinely disabled, during an interface transition, a locked agent switch, or an open handoff dialog, which is the condition that already marks the surface inert. Known consequence off macOS: the palette deliberately hands plain Ctrl+K to a focused terminal so readline's kill-to-end-of-line keeps working. That rule now applies from the moment a session opens rather than only after the user clicks into the terminal. Covered by a test that pins both halves of the behaviour. * fix(ui): let the surface under a closed dialog claim the caret first The Linux CI gate caught the two focus fixes racing each other. Closing the Switch agent dialog clears handoffDialogOpen, so the worker terminal re-enables input and focuses itself, while the menu hand-back was also trying to focus the Session actions trigger one tick after the dialog unmounted. macOS gave the trigger, Linux gave the terminal. The hand-back exists only so focus is never stranded on document.body, so it is now the fallback rather than a competitor: it waits two frames, long enough for whatever the dialog was covering to claim the caret through its own effects, and steps in only if nothing did. A terminal you can type into is a better landing spot than the button that opened the dialog anyway. The switch-agent test asserted the trigger specifically, which is the platform-dependent half. It now pins the invariant that actually matters: focus lands somewhere usable, never on the page body. A chat session, which has no terminal underneath to claim focus, covers the hand-back end to end. * fix(ui): drop a pending focus hand-back once the caret finds a home Review catch on #4847. handleFocusRelease returned early whenever relatedTarget was non-null and never cleared pendingReturnTarget, and the document focusout listener was never removed. A dialog that handed focus straight to another element left the menu trigger armed indefinitely, so an unrelated focus loss to document.body minutes later pulled the caret back to that stale trigger. The pending entry now records the surface that borrowed the caret, not just the element inside it, which separates the two cases the old check conflated. Focus moving within that surface is the user working inside the dialog and changes nothing. Focus moving anywhere outside it means the caret has a home, so the hand-back is abandoned and the listener goes away. The fallback path clears the entry too, whichever way it decides, and a drop while the surface is still open keeps waiting instead of spending the hand-back on it. * fix(ui): capture a context menu's return target before it takes the caret Second review catch on #4847. The return target was read from a microtask after the menu content attached. A dropdown was fine, since it is found through the trigger's aria-controls link, but a context menu has no such trigger and fell back to document.activeElement, and by the time that microtask ran Radix had already moved focus into the menu. So the target captured for every context menu was the menu element itself, which is detached by the time there is anything to hand back, leaving focus on document.body instead. The fallback is now read synchronously as the content attaches, while the element the user was actually on still holds focus, and anything inside the menu is refused outright. The trigger lookup stays in the microtask, because aria-controls is only there once the commit that opened the menu has landed, and it overrides the fallback only when a trigger is found. That makes the intended behaviour explicit: a context menu hands focus back to whatever held it when the menu opened, which is the same element Radix restores to. Covered by a unit test that reproduces Radix taking the caret during the open, and by an e2e that opens and closes the sidebar context menu rather than only opening it. * fix(import): align workspace onboarding with project flow * chore(frontend): refresh lockfile for ci * fix(renderer): avoid workspace session routing race * fix(ui): guide workspace remote setup on spawn errors * fix(ui): use valid details open prop * fix(renderer): avoid workspace session routing race * fix(ui): guide workspace remote setup on spawn errors * fix(e2e): sync fake bridge github login * fix(frontend): restore import flow check coverage * feat(ui): retain project creation progress * fix(ui): wire project progress dialog import * fix(frontend): align shell with latest main * feat: validate duplicate git project imports * feat: validate git repositories before cloning * fix: update e2e bridge for repository checks * test: wait for git repository validation * fix(frontend): restore bridge and rename translations * refactor(ui): simplify project import flow * fix(i18n): keep folder missing message * fix(import): trust validation git readiness * refactor(ui): share project path rows and modal backdrops * fix(ui): render one create-project backdrop * fix(ui): hide parent modal during project setup * fix(ui): toast project import errors and shake modal * fix(ui): reset project import flow on close * fix(ui): keep modal backdrop behind content * fix(ui): make project errors accessible and animated * fix: prepare empty clone repositories during onboarding * fix: clean up abandoned clone preparations * fix: keep import transitions and errors user friendly * fix: keep import kind choices out of git setup * fix: align onboarding validation with import scenarios * fix: route workspace imports through review * fix: restore compact workspace import list * fix: restore expandable workspace repository setup * fix: auto apply workspace repository setup * fix: animate workspace setup collapse * fix: show workspace creation progress * fix: simplify workspace creation progress * fix: default child repo imports to project * fix: keep workspace import shortcut * fix: harden project import git preparation * fix: harden clone preparation cleanup * fix: harden clone repository dialog validation * fix: harden clone import transitions * fix: reject credentials in import remotes * fix: deduplicate clone validation errors * fix: gate project import before agent setup * fix: reject userinfo in import remotes * fix: defer clone input validation feedback * fix: open registered imports before setup * fix: show truthful project setup progress * test: align project import fixtures * fix: simplify workspace import decisions * fix: simplify empty workspace state * fix: simplify empty workspace guidance * fix: clarify empty workspace recovery * fix: simplify project import guidance * fix: sequence workspace project handoff * fix: remove empty folder review state * fix: clarify clone destination selection * fix: require workspace child setup before continue * fix: classify workspace git setup actions * fix: defer workspace setup until continue * fix: trust workspace scan git state * fix: prepare optional workspace folders * fix: enforce workspace import for project collections * fix: allow seeding workspace repository * style: soften import setup checkboxes * style: use shared checkbox for import setup * style: sharpen import setup checkbox * refactor: streamline project import flow * fix: restore onboarding CI checks * fix: align onboarding checks with current flow * test: preserve sidebar rename coverage * fix(import): address workspace review feedback * fix(import): force git roots into project flow * test(import): model workspace root as container * refactor: harden project onboarding lifecycle * fix: simplify project git setup approval --------- Co-authored-by: Aditi Chauhan <[email protected]> Co-authored-by: Aditi <[email protected]> Co-authored-by: NIKHIL ACHALE <[email protected]> Co-authored-by: Just_Anniee <[email protected]> Co-authored-by: Pulkit Saraf <[email protected]> * fix(chat): allow untouched sessions to switch to terminal (#5053) * fix(chat): allow untouched sessions to switch to terminal * test(chat): isolate handoff regressions from installed providers * fix(chat): preserve fresh Claude interface round trips * fix(chat): release unused provider binding during fresh handoff * style: follow local Go import grouping * fix: wait for chat controller before loading provider controls * fix: explain the outward interface handoff in the composer * fix(mobile): re-render the board on a change, not on the poll tick (#4854) * fix(mobile): re-render the board on a change, not on the poll tick getSessions re-maps the wire response into fresh objects on every tick, so the board's session list changed identity every 8s — 2s over a tunnel — even when the daemon returned exactly what it returned last time. That re-bucketed and re-sorted the fleet, rebuilt the section list and re-rendered every card for nothing. Keep the previous value when the payload matches, the same idiom reloadConfig already uses for the config object. Relative timestamps were riding on that churn, so the board and the notifications list now own a one-minute clock instead of redrawing on someone else's schedule. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01W4APgCQCABthJ62x1CmDrg * fix(mobile): stop the board clock while the app is backgrounded React Native keeps JS timers firing in the background: iOS hands pending timers to an NSTimer on the way out (RCTTiming's sleep timer) and Android's JS thread never stops. So the minute clock behind the relative timestamps kept re-rendering the board, the section list and every card once a minute with nothing on screen, for as long as the app lived. The comment above it claimed the opposite, which is how it got through. Gate the clock on the same shouldPoll(AppState) predicate as the board poll: the interval is cleared on background and restarted on active, with one catch-up read on the way back so the labels do not sit a full tick stale. The handle lives in the effect rather than in state, so leaving costs no render at all and returning costs exactly one. Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_01UF3x7fHJ3XVM4RMGFYWuqT --------- Co-authored-by: Claude Opus 5 (1M context) <[email protected]> * feat(board): show PR progress for finished sessions (#4916) * feat(board): show PR progress for finished sessions * docs(board): clarify PR progress comments Co-authored-by: Copilot Autofix powered by AI <[email protected]> * fix(board): gate PR progress on real termination, localize count suffixes A session's status can already read "merged" before the daemon marks it terminated, so the finished-card PR progress footer now also requires isTerminated for merged sessions (not just for the terminated status). Reword the stale inline comment about when the footer renders, and replace the English-only "N open/draft/closed" suffixes with count-aware locale keys across all eight supported locales. * test(board): cover localized PR progress plurals * fix(board): pluralize English draft progress --------- Co-authored-by: AO Tests <[email protected]> Co-authored-by: Copilot Autofix powered by AI <[email protected]> Co-authored-by: AO Test <[email protected]> Co-authored-by: Dhruv Sharma <[email protected]> * fix(updates): keep failures out of sidebar (#5066) * feat: create GitHub repository during project import (#5092) * feat: create github repo during project import * test: update renderer fake github bridge * fix: resolve import repo owner from git config * fix: clarify github repository availability state * fix: repair origin after github repo creation * fix: retry github import push after partial setup * fix: clarify child repository import guidance * fix(chat): render provider error detail and web links (#4485) * fix(chat): show actionable provider error detail * fix(chat): require Codex provenance for billing action * fix(chat): normalize trusted recovery actions * fix(chat): harden Codex recovery provenance * fix(chat): reject ambiguous Codex error JSON * fix(chat): preserve provider error envelope identity * fix(chat): reject ambiguous provider notifications * revert: narrow provider error fix to renderer * fix(chat): link provider error URLs * fix: serialize mobile bridge state transitions (#5084) * fix: close active LAN connections when mobile access stops (#5087) * fix(board): stop spinning a loader on settled session cards (#5082) The board card read its status label from `displayStatus` but its loader from `status`. The daemon derives those from opposite ends of a session's PR set -- `displayStatus` from the best PR, `status` from the worst open one -- so a session with one mergeable PR and one still awaiting review rendered a green "Mergeable" label next to a loader that span forever, even with the agent idle. The predicate had no activity term, so nothing could switch it off. Key the loader off the label the card actually renders: keep it while the agent is genuinely working, otherwise show it only for display statuses that mean work is still turning. "Review pending" keeps its loader and "Mergeable" loses it, which is what #4725 asked for. `status` remains the fallback only when the daemon sent no `displayStatus`. The multi-PR aggregation in the daemon is deliberate and is unchanged. Fixes #5081 Co-authored-by: itrytoohard <[email protected]> Co-authored-by: Claude Opus 5 (1M context) <[email protected]> * fix: refresh mobile status after disable errors (#5091) * fix: simplify GitHub repository import setup (#5118) * fix: simplify GitHub repository import setup * fix: align repository name availability feedback * fix: keep import setup folder name stable * fix: animate repository availability progress * feat: select GitHub repository owner * feat: cache GitHub repository owners * fix: show selected GitHub owner avatar * test: add cached GitHub owners to e2e bridge * fix: allow custom GitHub repository owners * fix: clarify custom GitHub owner selection * fix: localize custom GitHub owner labels * fix(desktop): refresh new session workspace readiness (#4532) * fix(desktop): refresh new session workspace readiness Use spawn completion and durable session update events to invalidate editor handoff state, with bounded retries for fresh sessions. Fixes #4531 * test(desktop): cover stale workspace event recovery * test(desktop): forbid transient workspace error * test(desktop): adapt readiness coverage to current event streams --------- Co-authored-by: itrytoohard <[email protected]> * fix(desktop): stop linking Star Us button to auth-gated /stargazers page (#4824) GitHub's /stargazers (and /watchers) sub-page now returns 404 for unauthenticated requests on every repo, not just this one - confirmed against torvalds/linux and microsoft/vscode. Anyone clicking "Star Us" from a fresh system browser with no active GitHub session hits a 404. Link to the repo's main page instead, which works anonymously and has a visible Star control. Fixes #4822 Co-authored-by: ronish <[email protected]> * fix(a11y): expose selected chat settings choices (#5120) * fix(a11y): expose selected chat settings choices * fix(a11y): scope radio menu roles to chat settings * fix: require child git repos for workspace import (#5122) * fix: require child git repos for workspace import * fix: only redirect remote workspace roots * fix: clarify workspace child setup requirements * fix: require manual workspace remote setup * fix: block incomplete workspace repositories * fix: align workspace root validation * fix(onboarding): automatically start GitHub login PTY (#5009) * fix(onboarding): automatically start GitHub login Fixes #5008 * fix: stabilize GitHub login terminal * fix: finish GitHub onboarding promptly * fix: preserve full GitHub auth flow * fix: keep auth polling out of refresh state * fix: harden GitHub auth terminal lifecycle * fix: handle split terminal position requests * fix: address GitHub onboarding review * fix: address GitHub onboarding review regressions * fix: preserve login protocol replies and adopt existing auth terminals * test: declare terminal write method in protocol mock * fix: remove unused replay handling and preserve failed login cleanup * fix: discard cursor replies from terminal replay * fix: suppress historical cursor replies on fresh pane mounts * feat(brand): add Ruto Twitter profile artwork (#5136) * feat(brand): add Ruto Twitter profile artwork * feat(brand): refine avatar framing and enamel contrast * feat(brand): use matte black avatar background --------- Co-authored-by: itrytoohard <[email protected]> * fix(landing): fill favicon canvas and align header logo (#5135) * fix(landing): fill favicon canvas and align header logo * fix: make landing favicon generation reproducible * fix: tighten landing wordmark and include AO name * fix: use colon in landing brand label --------- Co-authored-by: itrytoohard <[email protected]> * fix(settings): send support email to prasad with prateek on cc (#5138) The Report a problem flow in global settings addressed feedback only to [email protected]. Make [email protected] the primary recipient and copy prateek so both receive support mail. The copied draft now carries a Cc line, and the mailto URL sets a cc param alongside the subject and body. * fix(landing): migrate public site to useao.dev (#5133) * fix(landing): migrate public site URLs to useao.dev * fix(landing): configure Cloudflare domain redirects * docs(landing): clarify domain rollback and public validation * refactor(landing): derive participant pass URL from marketing origin --------- Co-authored-by: itrytoohard <[email protected]> * docs: list OMP among supported coding agents (#5140) Co-authored-by: itrytoohard <[email protected]> * fix(desktop): stop leaking the shell renderer when the window is closed (#5150) * fix(desktop): stop leaking the shell renderer when the window is closed WindowComposition.dispose() read `options.mainWindow.contentView` outside the try/catch. On window close (Cmd+W or the close button) the `closed` event fires after the native BaseWindow is destroyed, so that property access throws `TypeError: Object has been destroyed` before `shellView.webContents.close()` runs. The shell WebContents is never closed, so its renderer process is orphaned and keeps polling the daemon; every close leaks another one. The throw also escaped the `.finally()` in main.ts's `closed` handler as an unhandled rejection. Reopening the app (app "activate") then builds a fresh window alongside the orphans, which wedges the startup loader. Cmd+Q was unaffected because it exits the process entirely. Move the `removeListener`/`removeChildView` pair inside the existing try/catch so `dispose()` can no longer throw, keeping the shell WebContents teardown reachable, and add a `.catch()` to the teardown chain in main.ts as a backstop. Co-Authored-By: Claude Sonnet 5 <[email protected]> Claude-Session: https://claude.ai/code/session_012qCvuNBkshNRPYNkzR8pB6 * test(desktop): use a stable contentView mock in the destroyed-window test Hoist the mocked contentView object and its spies out of the getter so they are not throwaway, and assert removeListener/removeChildView are not reached once the getter throws. Co-Authored-By: Claude Sonnet 5 <[email protected]> Claude-Session: https://claude.ai/code/session_012qCvuNBkshNRPYNkzR8pB6 --------- Co-authored-by: Claude Sonnet 5 <[email protected]> * fix: improve clone destinations and startup before first push * fix: improve clone destinations and startup before first push * fix: preserve initial branch across onboarding preparation paths * fix: align clone destination picker * test: align clone coverage with destination picker * fix: retain editable clone destinations alongside picker * fix: align clone destination action styling * fix: keep clone destination focus quiet --------- Co-authored-by: itrytoohard <[email protected]> Co-authored-by: ronish <[email protected]> * fix(mobile): poll native-session readiness so Switch to Chat enables itself (#4852) * fix(mobile): poll native-session readiness so Switch to Chat enables itself A freshly spawned Claude Code TUI session reports NATIVE_SESSION_UNVERIFIED until its session-start hook lands, about a second after spawn. Mobile fetched the interface-transition status once on mount and only polled while a transition was already active, so the chat icon stayed unavailable until the screen was left and reopened, and the alert showed the daemon's raw Go error. Recheck the two transient readiness codes once a second, matching desktop's nativeSessionReadinessPoll in useSessionInterfaceTransition.ts, refresh once on tap before declaring the switch unavailable, and give the readiness states plain-language copy. Refs #4841 Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01W4APgCQCABthJ62x1CmDrg * fix(mobile): stop the readiness recheck on a rejected status The readiness poll runs at 1s and a failed recheck set `error` but left `status`, so the interval kept firing at full rate. The LAN bridge locks a device out for a minute after five failed auths, so a rotated password armed that lockout in five seconds and held it for as long as the session screen stayed open. Gate the interval on shouldKeepPolling, the rule the board poll already uses: a transient failure keeps retrying, a 401 or 429 stops. A successful recheck, or the tap-refresh this PR adds, resumes it. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01W4APgCQCABthJ62x1CmDrg * fix(mobile): bound the readiness poll and make the tap path honest Review follow-up on #4852. - Gate the readiness poll on shouldPoll(AppState) and cap it per readiness state, so a session whose code never clears (#4122) stops instead of polling forever, and a backgrounded phone stops ticking the presence roster and holding the desktop's live dot on. - Replace setInterval with a self-scheduling loop that awaits each request before queueing the next, and drop superseded responses so a slow answer cannot revert a supported:true status or fire onSettled. - Give the tap its own busy state, a re-entrancy guard and an unmount bail. The busy flag is owned by the screen, not the hook: the poll shares refresh(), so a hook-wide flag let a poll tick disable the user's own button. disabled now matches what the button announces as busy, since a tap during the start POST fired a duplicate 409. - Return a discriminated recheck result carrying the HTTP status, so a connectivity failure is no longer reported as an agent that cannot do Chat, and 401/429/other-answered/unreachable each get their own copy via classifyConnectionFailure. Failed requests get a separate capped budget with backoff. A failed request never advances status, so rescheduling on the last known one re-armed the 300ms transition poll indefinitely after a mid-switch 404 — worse than the unbounded readiness poll this started from. The same budget is what retries a first fetch that never landed, which otherwise left the screen with no poll at all. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01W4APgCQCABthJ62x1CmDrg * fix(mobile): show the switch spinner while the start POST is in flight Widening `disabled` to cover `starting` left that POST disabled but silent for up to REQUEST_TIMEOUT_MS — the same dead-button shape the recheck spinner was added to remove, one path over. The spinner now covers both, so every state the button announces as busy also looks busy. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01W4APgCQCABthJ62x1CmDrg * fix(mobile): retry transient poll failures, stop only when the session is gone The failure cap on the interface-transition poll outlived the outage it was meant to survive. A live handoff plus a network drop spent five requests over about 75s and then stopped for good: with no timer left, connectivity returning changed nothing, the "Switching to ..." banner froze, and the only way out was to leave the screen and come back, the shape #4841 was filed about. The cap was aimed at one real case, a session deleted mid-transition 404ing at 300ms forever. But a 404 or 410 is a definitive answer about the session and a timeout is not, so the scheduler now stops at once on those two and retries everything else at a backoff that holds at 8s instead of giving up. 401/403/429 still stop the loop before the scheduler sees them; the app foregrounding now gives that stop one fresh request, which is what the board poll already does. The failure count moves out of the poll effect into a ref, the same shape as the readiness budget and for the same reason: `appActive` re-runs the effect, and a count it owned restarted the backoff (and refilled the budget) every time the app was switched. It is cleared by any request that lands, including an accepted start POST, and by a session or config change. Both transition surfaces show a Retry while the last request failed, next to Cancel where the phase still allows one. The chat banner had no way to re-ask the hook at all, and the terminal header button is disabled for the whole transition, so this is the only tap that rechecks while one is live. Also from reviewing the change: the terminal tap path now treats a superseded recheck answer the way the hook's loop does (the readiness tick can overtake a slow tap request and the tap would alert off the older payload), and it bails if the fresh answer already carries a transition someone else started, rather than firing a second start that the daemon 409s. Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_01UF3x7fHJ3XVM4RMGFYWuqT * fix(mobile): bound the waits that cannot finish themselves Review follow-up on #4852 round 3. Only an answer spends the readiness window, which is right — a timeout says nothing about whether the native session became ready — but once failures stopped terminating anything, a readiness wait against a daemon that never answers had nothing left to end it: an hour of timeouts was 183 requests with the budget still untouched. The two waits no longer share a retry policy. A live handoff is a real operation with a banner up, so it still retries for as long as the screen is open; that is what round 2 fixed. A readiness wait is speculative — #4122 says it may never clear even on a healthy link — and so is a mount fetch that never landed, so those stop after five consecutive failures and leave the header recheck as the escape. This also settles the persistent-5xx case deliberately: a daemon failing this one handler is a fact about the session, so it ends a speculative wait, while a restarting daemon under a live handoff is still ridden out. Also from the review: an already-active transition found by the tap's own recheck confirms with a haptic instead of returning in silence after a visible spinner; InlineBanner slots are no longer pressable when their handler is withheld, so "Retrying…" and "Cancelling…" stop firing a tap haptic for nothing (the terminal card already did this); and the poll effect's dep array records why `fetchFailed` is there while unread in the body. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01QgJGmQ2wmikHFH7Vmwuv15 * fix(mobile): do not spend the failure budget on a request that landed Self-audit of the previous commit. Bounding a speculative wait made the failure count interact with the 401 stop, which it must not. A rejection increments the count today, so five app switches under a rotated password would spend the speculative budget and the sixth foreground would schedule nothing — removing the one request per app switch that notices the password was fixed. The comment beside the constant claimed the opposite. A rejection is a request that landed: the daemon answered, so the link is not what is wrong and the backoff has nothing to measure. `refresh` now clears the count for anything `shouldKeepPolling` rejects and leaves `pollable` to own that stop, so the two stops cannot compete. Under a persistent 429 that is still one request per foreground, well clear of the five-failure lockout. Also aligned two doc comments with the code: the scheduler's note on retrying a cold start now says that retry is bounded, and the note beside the constant says what actually separates the two stops. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01QgJGmQ2wmikHFH7Vmwuv15 --------- Co-authored-by: Claude Opus 5 (1M context) <[email protected]> * fix(updater): add macOS repair installer and reliable restart progress (#5002) * fix(updater): recover from macOS staging missing-file failures Discard failed ready-to-install state so the same release can be retried. Keep native cache files intact, order staged provenance writes, and prevent late escalation from resurrecting a rejected copy. Refs #4175. * fix(updater): prepare remembered macOS updates before restart Re-establish the native updater handoff after reopening AO and keep preparation failures visible before quitting. Explain the installation wait so an early manual relaunch does not interrupt the swap. * fix(updater): keep progress visible through macOS restart * fix(updater): add standalone macOS repair installer packaging * fix(updater): finish staging recovery before queued retries * fix(updater): deliver update status to the shell window, not BrowserWindow Under BaseWindow composition BrowserWindow.getAllWindows() is empty, so updates:status and updates:telemetry reached no renderer until Settings was reopened. Route both through a shell recipient sink wired from main.ts to trustedShellWebContents; keep the BrowserWindow loop for test harnesses. * fix(updater): keep the macOS update helper hidden unless recovery is needed The helper launches as an accessory (no Dock icon, no window) and presents its window only if the update stalls or fails. A normal update closes and reopens AO with no second window. READY, the parent's quit handshake, is a stdout line independent of any visible UI, so the handoff is unchanged; the watchdog and recovery window are retained. * docs(updater): correct stale comment about the helper's quit handshake The helper acknowledges readiness via its READY stdout line, not a visible window; after the hidden-unless-recovery change it shows no window on the normal path. Update the comment to match. * fix(updater): drop the restart progress bar for a minimal preparing state The Restart & install dialog no longer renders a progress bar. On a normal update it just closes and AO restarts; while preparing, the confirm button relabels to "Preparing update…" and both buttons disable; a failure still shows the actionable message with retry. No progress component either way. * fix(updater): reconfirm changed restart targets * test(updater): preserve platform literals in install regression * fix(desktop): keep interface-switch status off the session tab title (#5159) * fix(desktop): keep interface-switch status off the session tab title Replace the wide “Resuming… Chat UI” pill with a compact spinner beside the tab ⋮ menu, drop switch copy from the composer, and only reserve extra tab width while a switch is in progress so idle titles stay untruncated. Co-authored-by: Cursor <[email protected]> * fix: hide session ⋮ and cancel while an interface switch runs Replace the tab actions menu with a non-interactive spinner for the duration of a Chat/Terminal switch, and omit mid-switch cancel until that path is reliable. --------- Co-authored-by: Cursor <[email protected]> * fix(updates): parse nightly build stamps as UTC, not local wall time (#5115) The nightly stamp (e.g. 202609071035) encodes the build time in UTC, but parseNightlyVersion fed the components to the local Date constructor, so the parsed instant shifted by the machine's UTC offset. In Asia/Kolkata the stamp parsed as 05:05Z instead of 10:35Z and rendered the build time as 10:35 AM instead of 4:05 PM; in America/Los_Angeles it rendered as 17:35Z. Near UTC midnight the date-only surfaces (sidebar row, restart dialog) showed the wrong calendar day. The existing parser tests asserted local getters, so they passed in every timezone and encoded the wrong interpretation. Parse with Date.UTC and validate the round-trip with getUTCMonth. All consumers (Sidebar, UpdatesSection, RestartToUpdateDialog) format through Intl.DateTimeFormat, so once the Date is the correct instant they render the equivalent device-local time for free. Tests: assert builtAt.toISOString()/getTime() against Date.UTC (no local getters), add positive/negative offset and UTC day-boundary coverage in the parser, extend the Sidebar and RestartToUpdateDialog component tests with a day-boundary nightly, and add an UpdatesSection test covering the full device-local Built timestamp. Fixes #5059 * Optimistic project import: load board first, provision in background (#5127) * feat(frontend): load project board immediately, spawn orchestrator in background Drop the blocking getRepositoryBranch lookup before submit; the daemon resolves the base branch at spawn. Navigate to the project board right after registration and run the orchestrator spawn (fetch + worktree + runtime) in the background behind a provisioning gate with banner, timeout escape, and retry. * perf(backend): probe repository concurrently during project registration Run isGitRepo, repoHasCommit, and resolveGitOriginURL in one WaitGroup wave instead of three serial git spawns. Admission error precedence (NOT_A_GIT_REPO before PROJECT_UNBORN) is unchanged. * test(frontend): cover background provisioning and import without branch lookup Assert imports submit without a blocking branch lookup and that the board shows the provisioning banner with gated actions until the spawn settles. * feat(frontend): add provisioning locale strings Add shell.provisioning and shell.provisioningDots to en plus all seven locales to satisfy the locale parity gate. * fix(frontend): close setup modal before orchestrator provisioning settles completeProjectCreation resolved only after the spawn finished, so the setup modal stayed open with its spinner over the freshly navigated board and the provisioning timeout unlocked a board the user could not touch. Split out a detached provisionOrchestrator flow that owns provisioning state, timeout, refresh, and retry error; creation now resolves right after register-and-navigate. * fix(frontend): preserve checked-out root branch for workspace imports The branch-lookup removal also dropped the workspace root defaultBranch that POST /projects relied on (pinned by e2e/workspace-root-import.spec.ts). Restore getRepositoryBranch for workspace imports only; single-repo imports keep the fast path since the daemon resolves their base branch at spawn. * fix(frontend): gate sidebar orchestrator launcher while provisioning The sidebar hover launcher could fire a second spawn while the background provisioning spawn was still in flight. Guard the early return and the disabled condition on isProvisioning, matching the board and topbar. * fix(lan): stop blocking Codex model routes on the LAN listener (#5152) * fix(lan): stop blocking Codex model routes on the LAN listener The LAN control block listed the whole /api/v1/agents/codex prefix, so mobile clients got an intentional 404 for GET /api/v1/agents/codex/models and POST /api/v1/agents/codex/models/refresh — the harmless read-only model routes mobile needs to pick a Codex model. Desktop was unaffected because it goes over loopback. Narrow the block to the two credential surfaces it was meant to cover: /api/v1/agents/codex/accounts and /api/v1/agents/codex/account-switches. Security is unchanged (login/logout/verify/delete/switch stay loopback-only) while /models, /models/refresh and /probe become reachable over LAN. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01PCgrqCHgFv8F83gXwHd43o * test(lan): drive Codex model routes through the real router The existing LAN block test uses a stub inner handler that answers 200 for any path, so it proves only that the filter no longer swallows the request — it would still pass if the model routes were unmounted. Add a test that mounts the real AgentsController behind the real LAN listener over a real socket, asserts GET /models, POST /models/refresh and POST /probe return 200 and that the handler actually ran, and re-pins that the credential routes still 404 with valid auth and a spoofed loopback Host. Verified failing (ROUTE_NOT_FOUND) with the old /api/v1/agents/codex prefix. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01PCgrqCHgFv8F83gXwHd43o --------- Co-authored-by: Claude Opus 5 (1M context) <[email protected]> * fix(mobile): apply a saved project filter only when the daemon still lists it (#5058) * fix(mobile): apply a saved project filter only when the daemon still lists it The saved ao.activeProject was restored from storage and never checked against the daemon's project list. After `ao project rm` on the desktop, or pairing with a machine whose projects differ, the board filtered every session out, the spawn sheet posted the dead id and showed the 404, and nothing said the board was filtered: the switcher hides below two projects and Settings labelled the stale id "All projects". The filter consumers read is now derived from the saved choice and the list: the id when the daemon lists it, all projects when it does not. Derived rather than reset and written back, so storage stays tap-only, a late response from a machine the user just left cannot discard a filter picked on the new one, and the choice returns if the project does. An empty list is treated as unknown because getSessions substitutes [] for a failed /projects. Settings and the switcher name the raw id while the list cannot name it, the spawn sheet drops a seeded project the list does not have, and the board's empty state says it is filtered and offers Show all projects. Fixes #4843 Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_01UF3x7fHJ3XVM4RMGFYWuqT * fix(mobile): tell a failed project list apart from an empty one Review follow-up on #5058. getSessions folded a failed /projects into [], so an empty list meant both "this daemon has no projects" and "we could not ask". resolveActiveProject had to treat it as no evidence, which made the filter flicker: a failing tick resolved a rejected filter back to the saved id, the next success rejected it again. It now reports null for a failure, and the store keeps the last list a machine answered with — retainProjects owns that rule and is unit-tested, so the success/failure/recovery sequence from the review is a test rather than a comment. A known-empty list is now evidence, because sessions outlive their project. Kept per machine via machineIdentity, extracted from eventCursorKey so the chat cursor and the project list cannot disagree about what one machine is. The spawn sheet's automatic re-seed went through setProjectId directly and so skipped the resets the picker path already did, letting an explicit model and a touched agent follow the sheet onto another project and be posted with the spawn. Both paths now go through one changeProject. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01QgJGmQ2wmikHFH7Vmwuv15 * test(mobile): pin the event-cursor key against the literal it ships with Self-audit of the review follow-up. The new test asserted the key equals `ao.chat.events.${machineIdentity(cfg)}`, which cannot fail when machineIdentity changes shape — both sides move together — so it did not actually hold the claim that extracting the helper left the cursor keys byte-identical. Pinned as literals instead; verified by renaming the identity prefix, which the literal test catches and the old one did not. A silently changed key orphans every stored cursor, which costs a full replay on every machine the app has talked to. Two smaller things from the same pass: retainProjects tests against null rather than truthiness, because a successful [] taking that branch is the distinction the type exists for and truthiness invites the wrong reading; and the review's sequence test now asserts the visible-worker count alongside the applied filter, so the table in the reply is the test rather than a claim beside it. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01QgJGmQ2wmikHFH7Vmwuv15 * fix(mobile): drop a project list from a machine the app has left Review round 2 on #5058. Recording which machine answered was not enough. fetchAll has no staleness guard, so a request already in flight when the user re-pairs still lands, and folding it in displaced the list the current machine had just given us; that machine's next failed /projects then found nothing retained for itself, went unknown, and the saved filter came back and hid its workers. Reported as expected "all", received "removed" — which the new sequence test reproduces exactly when either guard is removed. Two guards, because they cover different windows. retainProjects now drops an answer whose machine is not the one the app is on, which keeps the guarantee to "the last list THIS machine gave us, until this machine gives us another". And projectsForMachine gates the read, because the write guard cannot help before the new machine has answered at all — the state still holds the old machine's list, and the spawn sheet re-checks its seed against the same value, so exposing it there would invalidate a legitimate pick and clear the model chosen under it. Only the project write is guarded in fetchAll, deliberately: guarding every write there would also swallow the connection and loading state on a re-pair. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01QgJGmQ2wmikHFH7Vmwuv15 --------- Co-authored-by: Claude Fable 5.1 <[email protected]> * fix(daemon): bound change log retention (#5103) * fix(daemon): bound change log retention * fix(storage): avoid migration version collision * fix(daemon): satisfy retention lint * test(storage): register change log migration --------- Co-authored-by: Kilo <[email protected]> * feat(chat): keep ACP providers alive across daemon restarts (#4766) * feat(chat): preserve ACP providers across daemon restarts * fix(chat): reattach scratch sessions after restart * fix(chat): harden persistent ACP adoption * fix(chat): close persistent ACP recovery gaps * fix(chat): preserve causal ACP replay order * feat(chat): preserve every ACP provider across daemon replacement * fix(chat): release unpublished hosts on branch commit failure * test: align restart dialog with persistent ownership The updater now trusts the daemon's chatProviderPreserved fact instead of a Codex allowlist. Update the stale safe-session fixture and cover persistent ACP sessions plus false or unknown Codex ownership without weakening the production guard. * fix(terminal): back-pressure PTY output instead of killing the WebSocket (#4552) A high-output command in an attached pane (a build, `yes`, a verbose test run) filled the per-connection 1024-frame write buffer faster than JSON + base64 WebSocket writes drained it. On overflow `enqueue` called `c.cancel()`, tearing down the whole connection and every pane on it; the client reattached, replayed, flooded again, and died — a crash loop. Replace the buffered channel with a per-connection queue that treats the two frame classes differently. PTY output waits for room above a 1MB watermark, so the attachment's read loop stalls and tmux throttles at the source. Control frames (opened, exited, error, resize, pong, session snapshots) never wait and are never dropped, because the CDC fan-out enqueues on one goroutine shared by every connection — blocking there would stall unrelated connections, and dropping there would lose a pane's exit notice. Ordering is unchanged: one queue, appended in production order, drained in order. A waiting producer is released by a drain or by the connection's context, so shutdown cannot deadlock. Fixes #4499 Claude-Session: https://claude.ai/code/session_015jQW4B3BgpKxaSfXB7mYXV * feat(chat): show Cursor's Ask and Agent as execution modes (#4978) * feat(chat): show Cursor's Ask and Agent as execution modes Cursor advertises Agent, Plan and Ask in one ACP `mode` option. AO classified execution postures with a global plan|agent word list, so Ask fell through to the approval menu and the mode control was left with a single choice it could not render as a switch. Decide per option instead. Plan and Agent name a posture wherever they appear; Ask only does when the same option also advertises Agent, so a harness whose "Ask for approval" is a permission policy is unaffected. Classification runs on the provider's advertised choices before AO synthesises Claude's Agent Mode, so that synthesis cannot retroactively promote an approval choice. ExecutionModePicker now names the choice in force and lists every advertised posture, sending the provider's own opaque value. The two-choice Plan/Agent pair keeps its switch inside the model menu; a provider with more postures than that gets its own trigger rather than being buried under the model picker. Cursor's approval policy stays in Project Settings and out of this control. Verified against a live `cursor-agent acp` session: the mode option round-trips agent/plan/ask through the config-options endpoint unchanged. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01VGTBavc4qethQ5CSWFxtbA * fix(chat): fall back to the option name, not a claimed posture The execution option carried a hardcoded "Agent Mode" name, which is exactly what the trigger's label falls through to when no posture is in force — either the provider reported no current value, or reported a permission value that the right-hand picker owns. The trigger then asserted Agent Mode for a posture it could not see. Keep the provider's own option name and resolve the label from the current choice explicitly. The synthesized "Agent Mode" choice, which is a real posture mapped to Claude's Manual value, is unchanged. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01VGTBavc4qethQ5CSWFxtbA * docs(chat): record the cost of label-based mode classification A permission choice that merely mentions the word ("Agent asks before edits") would read as a posture and pull the option's ask choices in with it. No real harness does this and the plan/agent matching always carried the same exposure, so the behaviour is unchanged — the note says where the real fix lives if such a choice ever surfaces: the provider's category, not a longer word list. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01VGTBavc4qethQ5CSWFxtbA * fix(chat): keep extras reachable beside a standalone mode picker Moving non-binary execution modes out to their own trigger took them out of clubbedLeft, so a provider advertising a multi-posture mode option plus an unclassified option — and no model, effort or toggle options — rendered no ClubbedConfigPicker at all and silently dropped the extras. That walks back the guarantee pinned by `keeps unclassified provider options accessible`. Count extras toward clubbedLeft on their own. ClubbedConfigPicker already handles an undefined executionMode alongside extras, so the extra gets its own picker beside the mode trigger. This also closes the pre-existing case of a lone unclassified option with nothing else on the left. Both new tests fail without the clubbedLeft change. Also pins the disabled state of the standalone trigger and the lone extra under configPending, and exercises the bracketed Cursor model id the wire-value test claimed to cover. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01VGTBavc4qethQ5CSWFxtbA * docs(chat): trim redundant Cursor mode comments --------- Co-authored-by: Claude Opus 5 (1M context) <[email protected]> Co-authored-by: Dhruv Sharma <[email protected]> * fix(cli): reject missing required daemon response bodies (#5157) * fix(cli): reject missing required daemon response bodies Empty 200/201 responses with a required decoded result no longer report false success. The shared CLI transport returns a decode error for empty or literal-null bodies, while legitimate bodyless calls (nil out) and 204 responses keep succeeding with a bounded drain for keep-alive reuse. Spawn and review submit additionally validate the returned session/review identity so uninitialized results can never print a success line. * fix(cli): validate every batch review entry before reporting success Batch review submit skipped ID/verdict validation whenever the returned reviews array was nonempty, so entries like {} or null still printed 'recorded 1 review(s)'. Every returned entry must now carry its run ID and verdict; an empty array still falls back to requiring a fully-populated single review. * fix(cli): use errors.As for envelope assertion in transport test Satisfies the errorlint check (type assertion on error fails on wrapped errors). No behavior change. * fix(chat): refresh provider catalogs after agent switch (#4439) * fix(chat): refresh provider catalogs after agent switch Clear model/config/skill caches on switch admission and pause catalog queries until the durable handoff settles. Chat takeover uses structured controller readiness instead of terminalHandleId, reusing the observed switch lifecycle seam. Config-option mutations are guarded by controller epoch so a pre-admission PATCH cannot republish the outgoing catalog. Fixes #4437 Co-authored-by: Cursor <[email protected]> * refactor(chat): centralize agent switch lifecycle * fix(chat): preserve source fence through agent switch * fix(chat): preserve unified switch actions after rebase * fix(chat): reconcile catalogs after fast agent switch --------- Co-authored-by: Cursor <[email protected]> Co-authored-by: Dhruv Sharma <[email protected]> Co-authored-by: Claude Opus 5 (1M context) <[email protected]> * fix: preserve GitLab response and cache consistency (#5148) * fix: update Twitter handle to ao_build (#5158) * fix: simplify bug report form (#5153) * fix: simplify bug report form * fix: update bug report Discord invite * fix: replace bug report Discord invite * feat(cli): enrich session list status (#4028) * feat(cli): enrich session list status * fix(api): keep review thread count optional * fix(api): publish unresolved thread count only when exact Address review findings on the unresolved-thread count: - Make UnresolvedThreadCount a pointer through the contract, DTO, and CLI so an unknown count stays absent on the wire while an observed zero serializes explicitly. A pre-change daemon's fieldless success no longer decodes as a known zero (CLI renders unknown as '-'). - Persist the review observation's partial flag (pr.review_partial, migration 0123). The summary omits the count when the latest observation hit the provider thread-window cap or when reviews were never fetched, so partial rows are never presented as an exact total. - Deduplicate provider thread identities across grouped PR URL aliases before counting; the fresher alias record wins a disagreement and an unresolved record wins a tie. Co-Authored-By: Claude Code <[email protected]> * fix(rebase): renumber pr_review_partial migration to 0130, regen sqlc origin/main claimed 0123-0129 while this branch sat on 0123; move to the next free number, restore the review_partial VALUES placeholder lost in the queries automerge, and regenerate gen/ so both author_avatar_url (0128) and review_partial (0130) are present. Co-Authored-By: Claude Code <[email protected]> * fix(review): completeness must come only from a successful thread fetch Review follow-up: the summary gate trusted producers that never fetched review threads. - observer: a metadata/CI-only pass (or a failed review fetch in preserve mode) no longer fills a zero ReviewObservedAt from the observation time; only a successful review fetch establishes a review observation. - observer: a completeness flip (partial <-> full) counts as a review change, so the certainty repair persists even when content hashes match and the poll would otherwise skip the write. - claim: claimRowsFromSCM now carries the review completeness pair — a partial fetch claims partial certainty, and a failed review fetch passes the zero pair instead of publishing claim-time certainty. - storage: UpsertPR/UpsertLegacyPR treat (review_observed_at, review_partial) as one pair — a NULL timestamp preserves the stored certainty instead of overwriting it. - migration 0130: existing rows initialize to partial=TRUE (unknown certainty stays hidden until a full fetch repairs it); a repairRenumbered* entry remaps dev databases that recorded the column under its pre-renumber 0123, and a schemaRepairs entry covers burned 0130 profiles. Tests cover the failed first fetch, partial/failed claims, the preserve contract at the SQL layer, and the unchanged-content completeness repair. Co-Authored-By: Claude Code <[email protected]> * fix(storage): repair intermediate review-count upgrade state --------- Co-authored-by: axisrow <[email protected]> Co-authored-by: Claude Code <[email protected]> Co-authored-by: Dhruv Sharma <[email protected]> * fix(ui): show closed sessions without merge in red (#5165) * fix(ui): show merged session status in red * fix(ui): show closed sessions without merge in red * docs: simplify human bug reports and evidence triage (#5130) Co-authored-by: itrytoohard <[email protected]> * feat(mobile-telemetry): tag chat vs tui on spawn, conductor, and handoff (#5179) The mobile app picks a chat or tui interface per session via `mode`, but no telemetry recorded which one, so chat-vs-tui usage was unanswerable. Add a `mode` (chat|tui) property to feature_used and set it on the two spawn paths (spawn, conductor) and on the live interface handoff. Also add a `handoff` feature so an in-session chat<->tui switch is measurable, with its `mode` naming the target (and, in a two-mode world, the direction). `mode` is a closed enum in the allowlist; no free text is added, preserving the mobile privacy contract. trackFeature gains an optional context arg carried onto both the success and failure event. * test(storage): guard pinned sqlc regeneration (#5190) * fix(chat): hold queued turns when a turn fails instead of draining (#5188) A terminal provider failure on a busy chat conversation dispatched the next durable queued turn immediately. On a systemic outage every queued turn failed in turn and the whole queue drained into failed history (#4861). afterProject called drainLocked after every primary ChatEventTurnCompleted with no check on terminal state, so a failed or recovered completion released queued work into the same broken provider. drainLocked now takes an allowDispatch decision: turn completion passes it true only for a proven completed turn. A failed, recovered, or status-less completion holds the queue at queued, durable and retryable. The Stop cutoff still re-enables dispatch so post-Stop messages send, and the other explicit drain callers keep dispatching. Regression tests cover the failed and recovered holds; the existing suite already covers completed draining and post-Stop delivery. * fix(chat): preserve OpenCode Plan Mode selection (#5015) * fix(chat): keep OpenCode plan mode toggle reversible * fix(chat): preserve OpenCode mode across agent resumes * style: group Plan mode test imports * fix(reviewer): resume Claude after reviewer relaunch (#4659) * fix(reviewer): preserve Claude restore identity Fixes #4658 * fix(reviewer): require explicit native restore signal * fix(reviewer): resume Claude on dead-pane relaunch * fix(reviewer): probe fallback native session id before trusting resume restoreSessionID short-circuited with ok=true whenever inv.AgentSessionID was empty, without probing anything. agentrestore.Command's caller falls back to deriving the same deterministic id from inv.ReviewerID unconditionally and reports NativeResumed=true regardless of whether that transcript was ever written (e.g. a daemon restart before the hook captured a native id on a first-ever pass). Probe that fallback id the same way the legacy-migration path already does, so a resume is only reported when the conversation actually exists. Also close the…
…4916) * feat(board): show PR progress for finished sessions * docs(board): clarify PR progress comments Co-authored-by: Copilot Autofix powered by AI <[email protected]> * fix(board): gate PR progress on real termination, localize count suffixes A session's status can already read "merged" before the daemon marks it terminated, so the finished-card PR progress footer now also requires isTerminated for merged sessions (not just for the terminated status). Reword the stale inline comment about when the footer renders, and replace the English-only "N open/draft/closed" suffixes with count-aware locale keys across all eight supported locales. * test(board): cover localized PR progress plurals * fix(board): pluralize English draft progress --------- Co-authored-by: AO Tests <[email protected]> Co-authored-by: Copilot Autofix powered by AI <[email protected]> Co-authored-by: AO Test <[email protected]> Co-authored-by: Dhruv Sharma <[email protected]>

Code changes: +62 -3
Tests: +139 -0
Others: +76 -0
Follow-up to #2390.
Summary
Behavior
Verification
Risk
Presentation-only. This does not change backend status derivation, Kanban placement, PR lifecycle facts, or session termination behavior.