Skip to content

feat(board): show PR progress for finished sessions - #4916

Merged
Rishet11 merged 5 commits into
mainfrom
ao/agent-orchestrator-341/pr-progress-summary
Sep 8, 2026
Merged

Rishet11 merged 5 commits into
mainfrom
ao/agent-orchestrator-341/pr-progress-summary

Conversation

@Rishet11

@Rishet11 Rishet11 commented Sep 4, 2026 •

Copy link
Copy Markdown
Collaborator

Code changes: +62 -3
Tests: +139 -0
Others: +76 -0

Follow-up to #2390.

Summary

  • Keep the daemon-derived Merged or Terminated session status unchanged.
  • Add a full-width card footer line with exact PR completion, such as 1 of 2 PRs merged · 1 open.
  • Count open, draft, merged, and closed PRs from the same deduplicated summaries rendered on the card.
  • Localize the merged-progress phrase for every supported desktop locale.

Behavior

  • terminated with merged + open: Terminated, plus 1 of 2 PRs merged · 1 open
  • all merged: Merged, plus 2 of 2 PRs merged
  • draft or closed PRs: listed separately in the progress line
  • live sessions and sessions with no PRs: no progress line

Verification

  • npm test -- SessionsBoardView.test.tsx, 21 passed
  • frontend board and i18n tests, 55 passed
  • product UI full typecheck, 121 tests, and production build passed
  • frontend TypeScript typecheck passed
  • production renderer build passed
  • git diff --check passed

Risk

Presentation-only. This does not change backend status derivation, Kanban placement, PR lifecycle facts, or session termination behavior.

Copilot AI lite review requested due to automatic review settings September 4, 2026 19:22

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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.

@OrcInc OrcInc added this to the Desktop & UI milestone Sep 5, 2026
@OrcInc OrcInc added comp/desktop Electron main process and React renderer. enhancement New feature or request labels Sep 5, 2026
@Rishet11

Rishet11 commented Sep 6, 2026 •

Copy link
Copy Markdown
Collaborator Author

Screenshots / demo

The archived all-merged session shows 2 of 2 PRs merged; the archived mixed session shows 1 of 2 PRs merged · 1 open.

Native AO Electron Sessions Board showing two archived PR progress cases

@Pulkit7070

Copy link
Copy Markdown
Contributor

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:

  • The line only renders for terminated/merged status with at least one PR; live and no-PR sessions get nothing.
  • countBoardPullRequests tallies open/draft/merged/closed from the same deduplicated summaries the card already renders.
  • Pluralization is handled properly - pr.progress.merged_one ("PR") vs merged_other ("PRs") - so "1 of 1 PR merged" reads correctly, and all eight locales carry the keys with real translations.

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 illegalcall left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@Rishet11
Rishet11 force-pushed the ao/agent-orchestrator-341/pr-progress-summary branch from e565d00 to 38ddb37 Compare September 6, 2026 14:03
@Rishet11
Rishet11 dismissed illegalcall’s stale review September 6, 2026 14:03

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.

@Rishet11

Rishet11 commented Sep 6, 2026

Copy link
Copy Markdown
Collaborator Author

@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?

AO Tests and others added 4 commits September 7, 2026 20:43
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.
@Rishet11
Rishet11 force-pushed the ao/agent-orchestrator-341/pr-progress-summary branch from 31b7d19 to 92793ca Compare September 7, 2026 15:18
@Rishet11
Rishet11 requested a review from illegalcall September 7, 2026 18:10

@illegalcall illegalcall left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@Rishet11
Rishet11 merged commit 24e1019 into main Sep 8, 2026
3 checks passed
YingkeSu added a commit to YingkeSu/agent-orchestrator that referenced this pull request Sep 11, 2026
* 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…
alerios pushed a commit to AIMagicFlow/agent-orchestrator that referenced this pull request Oct 14, 2026
…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]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/desktop Electron main process and React renderer. enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants