mirror of
https://github.com/stablyai/orca.git
synced 2026-10-31 09:07:40 +00:00
a0a98a4e692bbd658115fdcebade96ad41d5ef2d
1104
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
6b5cd3246a | fix(mobile): unbreak mobile typecheck after shared socket-liveness change (#29496) | ||
|
|
0c8303c210 |
fix(mobile): Agent Session History resumes sessions in SSH workspaces that can read them (#29461)
* fix(mobile): resume Agent Session History rows in an SSH workspace that can read them * fix(mobile): keep transcript-referencing resumes on the transcript's own host |
||
|
|
6ae1386f98 |
fix(native-chat): never run a chat on an older host with more access than Ask for approval allows (#29442)
* fix(native-chat): refuse to start or send on a host that ignores Ask for approval * fix(native-chat): hide Continue on a host held for its permission mode * fix(native-chat): hold queued cards and goal turns on a held host; re-check on reconnect * fix(native-chat): hold goal objectives and start retries on a held host; re-ask while held * test(native-chat): give the remote Continue test a host that applies permission modes |
||
|
|
13084ba347 |
feat(mobile): long-press a workspace to see its linked PR, issues and notes (#29225)
* fix(mobile): phone server rows follow the desktop's other-client and sleeping filters A phone created through a desktop now records whom it acted for, so the desktop's Hide other-client workspaces filter keeps the user's own phone workspaces (locally and on servers) and hides only other people's. The desktop relay marks each server row its sidebar would hide, and the rows its own browser or chat tabs keep awake; the phone applies both. * test(e2e): dedupe sidebar row ids in the visibility spec * fix(mobile): address review: owner naming, old-server relayed phones, scope trim * refactor: move the sidebar's project section grouping into shared The project sections the desktop sidebar draws under Group by Project are built from the project/setup projection, the per-host catalog fold, the endpoint host stamp, and the section labelling and ordering rules. Move the pure parts of each into src/shared (no behaviour change) so the paired phone can call the same code instead of guessing the grouping. * fix(mobile): group by Project with the desktop sidebar's own sections The phone grouped Project sections by repo display name, so it merged checkouts the desktop keeps apart (several local stablyai/orca checkouts, or a server's copy) and split what the desktop merges. The phone now reads each host's repo.list, project.list and projectHostSetup.list (the desktop's own and each server's through the relay), folds them with the renderer's shared catalog fold and projection merge, and builds sections with the desktop's shared grouping, labelling and ordering code. Keys, labels and Recent order equal the sidebar's; a host that predates the project lists gets the repo-derived projection, as the desktop does. Header colour and icon come from the section's anchor repo and rows from their own repo by id, as on the desktop. The repo filter lists every catalog repo by id per host instead of merging same-named repos. Group-by labels read Project and None. project.list and projectHostSetup.list are routed for paired phones (read-only, workspace permission). * refactor(mobile): record a phone's owner as creator and fold desktop tab activity into the host flag A phone now records its owner as the creator (this host for a directly paired phone, the relaying desktop for a delegated one), so the shared rule is unchanged and onBehalfOf is gone. The relay ORs the desktop's saved browser/chat tabs into hasHostSidebarActivity, so the phone's status dot follows too, and local worktree.ps counts the same signals. The old-server fallback is removed. * test(e2e): poll the phone and the sidebar together in the visibility spec * fix(mobile): one Project-section builder for desktop and phone, with the desktop's repo order Review fixes. The sidebar's buildOrderedGroups now calls the shared buildProjectSections for Group by Project (notice and pending repos are optional inputs), so desktop and phone run one function. The phone ranks manual header order by its folded catalog order, which is each host's repo.list order with the desktop's own repos first, as the renderer loads them; only the desktop's cross-host manualRepoOrder (pairing-local) is unknown to it. The projection selector is now a memo over the shared resolver. A transient project-list failure keeps that host's last catalog; only forbidden or method_not_found falls back to the repo-derived projection. Catalog rows are checked on every member grouping reads, and the parity test answers the renderer's real catalog fetch, includes a server's own project rows, and fixes Manual and Recent orders that differ. * fix(mobile): use the repo-derived catalog until a host's project lists first answer * fix(mobile): the desktop serves its sidebar's Project catalog instead of the phone fetching it The phone fetched repo.list, project.list and projectHostSetup.list from the desktop and every relay-ready server each minute and folded them itself, so rows from offline or older servers fell back to repo sections and keys moved on cold start. The desktop renderer now publishes the inputs its own Project headers use (state.repos in sidebar order, the projection selector's projects and setups, projectOrderBy) on the runtime window graph it already sends to main, only when they change. Main keeps the last one and answers it on a new paired-desktop read, mobileRelay.projectSections. The phone reads it with its repo metadata and groups with the shared builder; a desktop that predates the read (forbidden or method_not_found) leaves the phone grouping rows by repo from the desktop's repo.list, and a transient failure keeps the last catalog. The payload carries only grouping and label fields, no hooks, credentials or uploaded icons. The phone's own catalog fetching and its project.list/projectHostSetup.list routes are removed. * fix(mobile): keep uploaded repo icons on the phone, and isolate the snapshot tests * test(e2e): the desktop window serves its sidebar's Project catalog to the phone * chore(i18n): regenerate the runtime English catalog (drift already on main) * fix(mobile): read the Project catalog as its own request, outside the recorded repo-metadata flow The extra read inside the repo-metadata refresh stalled its recorded RPC sequence. It now runs beside it with its own throttle. The repo-metadata goldens are re-recorded for the intended state change: the name-keyed colour, icon and id maps became desktopRepos. * refactor(sidebar): one shared Workspace options filter and sort evaluator The sidebar, the jump palette and the options badge each applied the Workspace options filters with their own copy of the rules. They now call one pure evaluator in src/shared/workspace-view, which reads each row through a small fact reader so the phone can call it next with the facts its host serves. The sort comparator moves there too, with each surface supplying its own Agent activity rank. No behaviour change: a parity test runs the old sidebar chain and the new evaluator over every toggle combination, and the old comparator against the new one for each sort mode. * wip: workspace view descriptors * refactor(sidebar): address review of the shared Workspace options evaluator One predicate builder instead of a per-row scope argument; one exemption-narrowing function taking the hide flag; consumers import the shared predicates directly; Agent activity ties fall to the label in the shared comparator. The activation filter lift reuses the shared predicates. The parity test now covers other-client rows, live agent attention classes and the sort-label map. * fix(mobile): fetch the Project catalog at once when the screen moves to another host The refresh window is now kept per client, so a host screen reused for another desktop doesn't wait out the previous host's 60s window before showing its Project headers. * wip: desktop option items from shared choices * refactor(sidebar): the Workspace options menu renders from one shared option list src/shared/workspace-view/workspace-view-options.ts lists every Workspace options control (Hosts, Projects, Group by, Sort by, Project order, Card layout, Show properties or Card display, and each filter) with its section, its English copy, its choices and when it shows. The desktop menu walks that list and renders each option through a table keyed on the option key, so a new option fails to compile until the desktop has a control for it; the phone sheet will do the same. The choice lists (sort modes, project order, card layout, agent activity layout, card properties) are derived from the shared ids, and a test checks the desktop's English copy matches the shared copy. The unused classic branch of the card property list is removed. No visible change. * wip: row facts * refactor(sidebar): address review of the shared Workspace options list The show rules get their own tests with fixed expectations (the cases the old filter-section test covered, plus the rest), and the menu test checks each option's own control rather than any matching text. Card display rows toggle one property each, with the folder-path copy in shared. The unused Status and Tasks copy left by the removed classic card list is deleted from the catalogs. Filter rows write through the store directly; files are named for what they hold. * feat(runtime): worktree.ps carries the Workspace options row facts Each worktree.ps row now says whether it is the project's default-branch workspace, created by an automation, created by the CLI, or on a detached HEAD, answered by the same shared predicates the desktop sidebar filters with. The phone has none of these facts today, so its copies of the filters either guess (default branch) or are missing. Additive and inert: optional fields that nothing reads yet. An older host omits them; readers must treat absence as 'not that kind', so the row stays visible. Server rows relayed through the desktop pass them through unchanged. * feat(mobile): the desktop's own phone reads and writes its other-client filter ui.get and ui.set strip the pairing-local UI fields for every paired client. That rule exists for clients with their own host catalog (a desktop paired to a server, a web client). A phone has none: it shows its desktop's view, so 'Hide other-client workspaces' must be one shared value. For the host's own phone (clientKind mobile, no relaying parent) ui.get now always answers the field and ui.set persists it; delegated phones, paired desktops and web clients keep the stripping rule. The phone's view-settings schema reads the rest of the Workspace options values it does not read yet (the automation, CLI and detached-HEAD filters, project order, card properties, agent activity layout), each salvaged to absent so an older desktop keeps the phone's local value. * feat(runtime): address review of the worktree.ps row facts Name the fact defaultBranch, and say what absence means for it: a reader keeps its own guess, because 'hide default branch' must not start hiding or showing rows an older host never described. Folder workspace rows get the facts too, so every row from a newer host carries them. The desktopOtherClient comment now says it is a verdict, not a fact. The test covers the rows where the host disagrees with the phone's current guess. * wip: phone uses the shared evaluator * feat(mobile): address review of the phone's other-client filter mirror One isHostOwnPhone predicate decides both who shares the filter value (ui.get/ui.set) and who gets the other-client row marks, so the two cannot drift. The pairing-local field list notes the exception. Tests cover a paired desktop still being stripped, and a phone write that leaves the field out not touching the host value. * feat(mobile): the phone filters and sorts with the desktop's shared Workspace options rules The phone's own filter chain and comparator are replaced by the shared evaluator, reading the facts the host stamps on each row. Phone filter state takes the desktop's setting names and carries every filter; the badge and Clear filters match the desktop's. The desktop's other-client marks become facts the phone filters with the shared value. A parity test runs the same workspaces through the desktop's reading and the phone's (facts stamped as the host stamps them) over every filter combination, folder projects included. * wip: phone view state carries project order and card props * fix(mobile): host scope in shared filters, patch-key writes, live view push Folds the phone's host filter into the shared filters so Clear and the badge agree, writes back only the touched filter keys, and pushes workspaceViewChanged so an open phone list re-reads desktop Workspace options. * fix(mobile): Clear sends only changed filters; skip a view re-read older than a local edit An older host's strict ui.set refuses a key it lacks, so Clear names only what differs. A ui.get that started before a local edit no longer overwrites it; the edit's own change event re-reads. The change publisher's field list now lives with it and derives the filter fields. * feat(mobile): the phone's Workspace options sheet, rendered from the desktop's shared option list The Filter drawer becomes the desktop's Workspace options: hosts, projects with search, group, sort, project order, card layout and properties, card display, and every filter. Each option renders through a table keyed on the shared option keys, so a new desktop option fails to compile until the phone has a control. Sort and group pickers use the desktop's words; Repo reads Project. * fix(mobile): Workspace options sheet review fixes Hosts read as on the desktop (none checked while all show; the last shown host is disabled). Project order falls back to the served value until the shared one is read. Card layout and card style changes now push to an open phone. Pick-one rows read as radio buttons. Simpler wiring: flat card state, the preset flag rides the patch, shared choices used directly. * fix(mobile): reset project order on host switch; simpler preset-flag handling * fix(workspace-options): all hosts and clear reach the desktop; one filter key list drives every surface Host visibility: the legacy host scope fallback moves to shared code; the phone reads it, and the host resets the scope whenever a client writes the host list, so All hosts and Clear stick. An older desktop that keeps the other-client filter to itself has its marks treated as hide. Badge counts, is-anything-filtered, Clear and reveal derive from the shared filter keys on the desktop sidebar and the kanban filter menu. The phone uses the desktop's group and sort values, holds its view in one state value, and reads its UI fields from one shared list. * feat(workspace-options): Status and PR grouping read the shared tables The phone groups by status with the desktop's status helpers and default catalog (its own copy had the defaults in reverse order) and by PR with a shared lane table, so its labels now read In review / In progress as the desktop's do. The desktop's PR lane decision ends in the same shared state-to-lane step. * feat(mobile): phone rows follow Card layout, Show properties, Card display and agent activity One shared helper (getWorktreeCardDetails) decides which card details show for the chosen properties and layout; the desktop card's flags and the phone row both read it. The phone row hides the PR badge, note/issue/Linear glyphs, host badge and agent rows the desktop turns off, and the Full list agent layout shows every agent row. * test(sidebar): folder rows survive restrictive filters; create grace ignores non-finite times * fix(mobile): write the host scope with the host list, and wait for the first read before forcing the other-client filter An older desktop narrows back to its legacy single-host scope when the phone writes only a null host list, so the phone now also writes the scope for a null or one-host list. The other-client fallback waits for the first ui.get so a new desktop's marks don't hide rows before its filter value arrives. Inline the clear-filters key list and note the PR state asymmetry. * test(mobile): re-record view-settings goldens for the unknown other-client state * fix(mobile): let a tall bottom drawer scroll on the page The drag wrapper around the drawer's ScrollView had no flex limit, so it grew to its content and the sheet clipped it instead of scrolling. The full Workspace options sheet is the first drawer tall enough to hit this. * fix(sidebar): reuse the palette's default host instead of reading the focus setting again * fix(sidebar): read the host options once in Workspace options and pass them to the Hosts item * fix: parse the ui.get reply into a named type, fix the row memo test's filters, prune the focus-read baseline * test(mobile): give the project catalog fixture its project order * feat(mobile): name the options chip Workspace options, as the desktop does The chip keeps the active-filter count and reads to screen readers with the desktop trigger's label. The pluralized filter count moves to shared code so both surfaces word it the same way. * fix(mobile): show the Workspace options count as a corner badge so the chip row fits "Workspace options (2)" squeezed the Sort and Group chips to ellipses at phone width. The count moves to a corner badge, as on the desktop trigger, and the chip drops its icon, which the desktop trigger does not show either. * refactor(sidebar): one switch-to-kind table, and fold the leftover helper files back - The four kind switches come from WORKSPACE_KIND_SWITCH in workspace-kinds; the shared evaluator and main's phone-push check both apply it, and the filter type derives its switch fields from it. - git-identity.ts folds back into git-identity-display.ts, as on main. - SidebarFilterState moves into sidebar-filter-actions.ts; visible-worktree-kinds.ts is gone. - buildWorkspaceRowPredicate is module-private. * feat(mobile): Workspace options sheet in the desktop menu's shape The sheet showed every choice of every option, so the filters sat screens down. It now follows the desktop menu: Hosts and Projects rows with their value, Group by as a segmented control, Sort by, Project order, Card layout and Show properties as rows that open a page of choices, and the filters as switches. Each option's control (an inline row, or a value plus a page) still comes from one table keyed by the shared option list. The segmented control is the voice settings one, extracted so both screens share it. * fix(mobile): Workspace options review fixes - A page whose option stops showing is forgotten, so it cannot reopen by itself. - Only the switch takes the tap on a filter row, so the web page writes once. - Sections split their rows in one pass; unused heading styles go. * fix(mobile): neutral filter switches on the page react-native-web paints an on-switch thumb teal unless given activeThumbColor, which React Native's types omit. MobileSwitch applies the app's switch colors, and its web sibling also sets that prop. * refactor(mobile): one MobileSwitch file, adding the web-only thumb color on web * fix(mobile): Show properties reads Hover under Compact, as the desktop does The row's value now comes from getWorkspaceCardPropertiesValue in the shared options, which the desktop trigger uses too: Hover under the compact layout, otherwise how many properties show. Card display shows no value, as on the desktop. * test(mobile): find the renamed Workspace options chip in the page drawer render check * feat(mobile): drop the Sort and Group chips; the Workspace options chip covers both The desktop has only its options button, and the sheet now holds Sort by and Group by. The chips' pickers, their screen state and the sort label go with them. * feat(mobile): option pages slide in from the right and keep the sheet's height The main sheet stays laid out under an open page, so the drawer keeps its height going in and out. The page slides in over 200 ms and back out on back (the row, or Android's back key), instantly under reduced motion. The reduced-motion hook moves to platform/ now that two screens use it. * feat(mobile): Compact rows fold into one title line like the desktop compact card The phone's Compact layout now moves repo identity into a title-row icon chip and drops the identity meta row (repo, host, branch or folder path, child badge), matching the desktop's compact card. The rule lives next to getWorktreeCardDetails as getWorktreeCardLayout, and the desktop presentation reads it too so both surfaces share one decision. * fix(mobile): keep a remote host glyph on compact rows; pass the card layout from the list Review follow-ups: compact rows show an icon-only host glyph for remote projects as the desktop compact card does, the list computes the layout once next to the card details, and a new-card-style row test. * fix(mobile): option pages fill the sheet and open at its top - The page layer grows to the pager's height, so no part of the main sheet shows below a short page. - The drawer takes a scrollResetKey; the sheet passes the open page, so a page opened from a row low on a scrolled sheet starts at the top. - The main sheet's height is measured under a page too, and the pager state lives in the sheet so it resets when the drawer closes. - A misplaced doc comment goes back to its function. * fix(mobile): phone rows show the Jira, automation and CLI glyphs; the options pager forgets a gone page in an effect * fix(mobile): Jira glyph sits before Linear as on the desktop; Jira reader tests beside it * feat(mobile): long-press a workspace to see its details The phone's long-press sheet now lists the workspace's linked PR and issues (tap to open), who created it, its notes and live ports (Copy), the phone's take on the desktop sidebar card's hover card. Folder workspaces get the sheet too, with details only. The desktop lets phones run workspacePorts.scanHost (read only). * fix(mobile): details read links the desktop's way and stay one-off Links come through the shared getWorkspaceAttachments, so references the desktop keeps apart stay apart, in the host's order with reviews first. The branch review read no longer takes the host's active-branch slot, each read re-runs only for its own inputs, a reopened sheet reads afresh, and Copy's confirmation never outlives the sheet. * fix(mobile): details clear on close without an effect; lint findings * fix(mobile): the phone leaves Ports out of Show properties and Card display * refactor(mobile): clearer names and tests for the phone's hidden card properties * feat(mobile): details sheet heading carries the origin; no ports As Jinwoo approved: the sheet heads with the workspace (primary text), its short branch, and a quiet origin line (automation name, or Orca CLI). The group below holds only linked reviews and tasks, then notes. Pin uses the pin icon. Ports are left out of the phone, so the desktop change that let phones scan them is reverted. * test(mobile): details section render helper matches its props * fix(mobile): details review read asks by branch; review merge by number; comments * fix(mobile): details rows share the action rows' text column * refactor(mobile): details sheet reads only the row The sheet no longer fetches. The host stamps two optional row fields: automationName beside automationCreated, and the branch PR's title, url and checks on linkedPR (from the GitHub cache it already reads). Older hosts omit them and the sheet shows what they send. Links match by provider, type and number; the reference label is one shared helper the desktop card uses too. Folder rows keep no long-press. One sheet file plus the pure model. * fix: review nits on the row-only details sheet * refactor(mobile): one view state and one settings read for the phone's Workspace options; tighter card glyph wiring - The phone's Card layout and card style now live in its view state; one parallel settings.get reads them with pinned placement beside ui.get. - The Card layout preset flag is a write option, no longer a view field or a change-event field. - getWorktreeCardDetails reads the raw desktop settings and resolves the layout once; the desktop card computes it once in its foundation. - The row computes its glyph presence in one place; Ports is hidden through one set. - Drops the vanished-page effect, the native-only a11y props and the web-only switch branch. * fix(mobile): the list settings read applies each half on its own, with tests; pinned placement changes announce themselves * fix(mobile): compact host glyph tests for older rows and the new card style; truer comments * fix(mobile): Card layout writes the setting before its preset; Card display names folder paths when the desktop has folder workspaces * fix(mobile): match the branch PR by repo and link, not number alone |
||
|
|
144aac00cc |
chore: update Vite, Tailwind, and build tooling (#29345)
* chore: update build and test toolchain * fix(build): isolate shared code from CLI entry overwrites * fix(build): preserve main startup asset paths * test(build): override resolved Vite 8 fixture inputs --------- Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com> |
||
|
|
ec9a0af3e8 |
Show phone subagents and open the correct parent chat (#28686)
The phone's worktree list now shows the running subagents the desktop sidebar shows, under their parent agent with their own ages, read from the host's existing child records. Tapping one opens its parent's existing chat or terminal. The feature was reviewed in #26423 and merged into this branch. Links from the phone to native chats whose saved tab ID differs from the session-derived one (restored, reserved and headless chats) now resolve to the chat's real tab instead of missing it. |
||
|
|
f152101010 |
ci: reuse prepared Windows security addon in PR packages (#29352)
* ci: reuse prepared Windows security addon in PR packages * ci: prepare bounded Windows security reuse qualification * ci: ignore owned Windows benchmark receipts during source checks * test: inject Windows security qualification dependencies * test(mobile): register chat view mocks in test roots |
||
|
|
330fd55376 |
test(mobile): split MobileNativeChatView.test.ts under the max-lines limit (#29368)
Pure move: the 36 test cases and their assertions are unchanged, now in six feature files. Shared mocks and render helpers live in MobileNativeChatView.harness.test-fixture.ts. The only non-move edits are typing and formatting: the harness adds getComposerEditGeneration to its default props (a required prop the fixture never passed), tests use hostType(node) for tag comparisons, and the typecheck baseline and reliability gate paths drop the removed file. |
||
|
|
7538fa9466 |
feat(mobile): the phone's Workspace options sheet, with every desktop option (#28997)
* fix(mobile): phone server rows follow the desktop's other-client and sleeping filters A phone created through a desktop now records whom it acted for, so the desktop's Hide other-client workspaces filter keeps the user's own phone workspaces (locally and on servers) and hides only other people's. The desktop relay marks each server row its sidebar would hide, and the rows its own browser or chat tabs keep awake; the phone applies both. * test(e2e): dedupe sidebar row ids in the visibility spec * fix(mobile): address review: owner naming, old-server relayed phones, scope trim * refactor: move the sidebar's project section grouping into shared The project sections the desktop sidebar draws under Group by Project are built from the project/setup projection, the per-host catalog fold, the endpoint host stamp, and the section labelling and ordering rules. Move the pure parts of each into src/shared (no behaviour change) so the paired phone can call the same code instead of guessing the grouping. * fix(mobile): group by Project with the desktop sidebar's own sections The phone grouped Project sections by repo display name, so it merged checkouts the desktop keeps apart (several local stablyai/orca checkouts, or a server's copy) and split what the desktop merges. The phone now reads each host's repo.list, project.list and projectHostSetup.list (the desktop's own and each server's through the relay), folds them with the renderer's shared catalog fold and projection merge, and builds sections with the desktop's shared grouping, labelling and ordering code. Keys, labels and Recent order equal the sidebar's; a host that predates the project lists gets the repo-derived projection, as the desktop does. Header colour and icon come from the section's anchor repo and rows from their own repo by id, as on the desktop. The repo filter lists every catalog repo by id per host instead of merging same-named repos. Group-by labels read Project and None. project.list and projectHostSetup.list are routed for paired phones (read-only, workspace permission). * refactor(mobile): record a phone's owner as creator and fold desktop tab activity into the host flag A phone now records its owner as the creator (this host for a directly paired phone, the relaying desktop for a delegated one), so the shared rule is unchanged and onBehalfOf is gone. The relay ORs the desktop's saved browser/chat tabs into hasHostSidebarActivity, so the phone's status dot follows too, and local worktree.ps counts the same signals. The old-server fallback is removed. * test(e2e): poll the phone and the sidebar together in the visibility spec * fix(mobile): one Project-section builder for desktop and phone, with the desktop's repo order Review fixes. The sidebar's buildOrderedGroups now calls the shared buildProjectSections for Group by Project (notice and pending repos are optional inputs), so desktop and phone run one function. The phone ranks manual header order by its folded catalog order, which is each host's repo.list order with the desktop's own repos first, as the renderer loads them; only the desktop's cross-host manualRepoOrder (pairing-local) is unknown to it. The projection selector is now a memo over the shared resolver. A transient project-list failure keeps that host's last catalog; only forbidden or method_not_found falls back to the repo-derived projection. Catalog rows are checked on every member grouping reads, and the parity test answers the renderer's real catalog fetch, includes a server's own project rows, and fixes Manual and Recent orders that differ. * fix(mobile): use the repo-derived catalog until a host's project lists first answer * fix(mobile): the desktop serves its sidebar's Project catalog instead of the phone fetching it The phone fetched repo.list, project.list and projectHostSetup.list from the desktop and every relay-ready server each minute and folded them itself, so rows from offline or older servers fell back to repo sections and keys moved on cold start. The desktop renderer now publishes the inputs its own Project headers use (state.repos in sidebar order, the projection selector's projects and setups, projectOrderBy) on the runtime window graph it already sends to main, only when they change. Main keeps the last one and answers it on a new paired-desktop read, mobileRelay.projectSections. The phone reads it with its repo metadata and groups with the shared builder; a desktop that predates the read (forbidden or method_not_found) leaves the phone grouping rows by repo from the desktop's repo.list, and a transient failure keeps the last catalog. The payload carries only grouping and label fields, no hooks, credentials or uploaded icons. The phone's own catalog fetching and its project.list/projectHostSetup.list routes are removed. * fix(mobile): keep uploaded repo icons on the phone, and isolate the snapshot tests * test(e2e): the desktop window serves its sidebar's Project catalog to the phone * chore(i18n): regenerate the runtime English catalog (drift already on main) * fix(mobile): read the Project catalog as its own request, outside the recorded repo-metadata flow The extra read inside the repo-metadata refresh stalled its recorded RPC sequence. It now runs beside it with its own throttle. The repo-metadata goldens are re-recorded for the intended state change: the name-keyed colour, icon and id maps became desktopRepos. * refactor(sidebar): one shared Workspace options filter and sort evaluator The sidebar, the jump palette and the options badge each applied the Workspace options filters with their own copy of the rules. They now call one pure evaluator in src/shared/workspace-view, which reads each row through a small fact reader so the phone can call it next with the facts its host serves. The sort comparator moves there too, with each surface supplying its own Agent activity rank. No behaviour change: a parity test runs the old sidebar chain and the new evaluator over every toggle combination, and the old comparator against the new one for each sort mode. * wip: workspace view descriptors * refactor(sidebar): address review of the shared Workspace options evaluator One predicate builder instead of a per-row scope argument; one exemption-narrowing function taking the hide flag; consumers import the shared predicates directly; Agent activity ties fall to the label in the shared comparator. The activation filter lift reuses the shared predicates. The parity test now covers other-client rows, live agent attention classes and the sort-label map. * fix(mobile): fetch the Project catalog at once when the screen moves to another host The refresh window is now kept per client, so a host screen reused for another desktop doesn't wait out the previous host's 60s window before showing its Project headers. * wip: desktop option items from shared choices * refactor(sidebar): the Workspace options menu renders from one shared option list src/shared/workspace-view/workspace-view-options.ts lists every Workspace options control (Hosts, Projects, Group by, Sort by, Project order, Card layout, Show properties or Card display, and each filter) with its section, its English copy, its choices and when it shows. The desktop menu walks that list and renders each option through a table keyed on the option key, so a new option fails to compile until the desktop has a control for it; the phone sheet will do the same. The choice lists (sort modes, project order, card layout, agent activity layout, card properties) are derived from the shared ids, and a test checks the desktop's English copy matches the shared copy. The unused classic branch of the card property list is removed. No visible change. * wip: row facts * refactor(sidebar): address review of the shared Workspace options list The show rules get their own tests with fixed expectations (the cases the old filter-section test covered, plus the rest), and the menu test checks each option's own control rather than any matching text. Card display rows toggle one property each, with the folder-path copy in shared. The unused Status and Tasks copy left by the removed classic card list is deleted from the catalogs. Filter rows write through the store directly; files are named for what they hold. * feat(runtime): worktree.ps carries the Workspace options row facts Each worktree.ps row now says whether it is the project's default-branch workspace, created by an automation, created by the CLI, or on a detached HEAD, answered by the same shared predicates the desktop sidebar filters with. The phone has none of these facts today, so its copies of the filters either guess (default branch) or are missing. Additive and inert: optional fields that nothing reads yet. An older host omits them; readers must treat absence as 'not that kind', so the row stays visible. Server rows relayed through the desktop pass them through unchanged. * feat(mobile): the desktop's own phone reads and writes its other-client filter ui.get and ui.set strip the pairing-local UI fields for every paired client. That rule exists for clients with their own host catalog (a desktop paired to a server, a web client). A phone has none: it shows its desktop's view, so 'Hide other-client workspaces' must be one shared value. For the host's own phone (clientKind mobile, no relaying parent) ui.get now always answers the field and ui.set persists it; delegated phones, paired desktops and web clients keep the stripping rule. The phone's view-settings schema reads the rest of the Workspace options values it does not read yet (the automation, CLI and detached-HEAD filters, project order, card properties, agent activity layout), each salvaged to absent so an older desktop keeps the phone's local value. * feat(runtime): address review of the worktree.ps row facts Name the fact defaultBranch, and say what absence means for it: a reader keeps its own guess, because 'hide default branch' must not start hiding or showing rows an older host never described. Folder workspace rows get the facts too, so every row from a newer host carries them. The desktopOtherClient comment now says it is a verdict, not a fact. The test covers the rows where the host disagrees with the phone's current guess. * wip: phone uses the shared evaluator * feat(mobile): address review of the phone's other-client filter mirror One isHostOwnPhone predicate decides both who shares the filter value (ui.get/ui.set) and who gets the other-client row marks, so the two cannot drift. The pairing-local field list notes the exception. Tests cover a paired desktop still being stripped, and a phone write that leaves the field out not touching the host value. * feat(mobile): the phone filters and sorts with the desktop's shared Workspace options rules The phone's own filter chain and comparator are replaced by the shared evaluator, reading the facts the host stamps on each row. Phone filter state takes the desktop's setting names and carries every filter; the badge and Clear filters match the desktop's. The desktop's other-client marks become facts the phone filters with the shared value. A parity test runs the same workspaces through the desktop's reading and the phone's (facts stamped as the host stamps them) over every filter combination, folder projects included. * wip: phone view state carries project order and card props * fix(mobile): host scope in shared filters, patch-key writes, live view push Folds the phone's host filter into the shared filters so Clear and the badge agree, writes back only the touched filter keys, and pushes workspaceViewChanged so an open phone list re-reads desktop Workspace options. * fix(mobile): Clear sends only changed filters; skip a view re-read older than a local edit An older host's strict ui.set refuses a key it lacks, so Clear names only what differs. A ui.get that started before a local edit no longer overwrites it; the edit's own change event re-reads. The change publisher's field list now lives with it and derives the filter fields. * feat(mobile): the phone's Workspace options sheet, rendered from the desktop's shared option list The Filter drawer becomes the desktop's Workspace options: hosts, projects with search, group, sort, project order, card layout and properties, card display, and every filter. Each option renders through a table keyed on the shared option keys, so a new desktop option fails to compile until the phone has a control. Sort and group pickers use the desktop's words; Repo reads Project. * fix(mobile): Workspace options sheet review fixes Hosts read as on the desktop (none checked while all show; the last shown host is disabled). Project order falls back to the served value until the shared one is read. Card layout and card style changes now push to an open phone. Pick-one rows read as radio buttons. Simpler wiring: flat card state, the preset flag rides the patch, shared choices used directly. * fix(mobile): reset project order on host switch; simpler preset-flag handling * fix(workspace-options): all hosts and clear reach the desktop; one filter key list drives every surface Host visibility: the legacy host scope fallback moves to shared code; the phone reads it, and the host resets the scope whenever a client writes the host list, so All hosts and Clear stick. An older desktop that keeps the other-client filter to itself has its marks treated as hide. Badge counts, is-anything-filtered, Clear and reveal derive from the shared filter keys on the desktop sidebar and the kanban filter menu. The phone uses the desktop's group and sort values, holds its view in one state value, and reads its UI fields from one shared list. * feat(workspace-options): Status and PR grouping read the shared tables The phone groups by status with the desktop's status helpers and default catalog (its own copy had the defaults in reverse order) and by PR with a shared lane table, so its labels now read In review / In progress as the desktop's do. The desktop's PR lane decision ends in the same shared state-to-lane step. * feat(mobile): phone rows follow Card layout, Show properties, Card display and agent activity One shared helper (getWorktreeCardDetails) decides which card details show for the chosen properties and layout; the desktop card's flags and the phone row both read it. The phone row hides the PR badge, note/issue/Linear glyphs, host badge and agent rows the desktop turns off, and the Full list agent layout shows every agent row. * test(sidebar): folder rows survive restrictive filters; create grace ignores non-finite times * fix(mobile): write the host scope with the host list, and wait for the first read before forcing the other-client filter An older desktop narrows back to its legacy single-host scope when the phone writes only a null host list, so the phone now also writes the scope for a null or one-host list. The other-client fallback waits for the first ui.get so a new desktop's marks don't hide rows before its filter value arrives. Inline the clear-filters key list and note the PR state asymmetry. * test(mobile): re-record view-settings goldens for the unknown other-client state * fix(mobile): let a tall bottom drawer scroll on the page The drag wrapper around the drawer's ScrollView had no flex limit, so it grew to its content and the sheet clipped it instead of scrolling. The full Workspace options sheet is the first drawer tall enough to hit this. * fix(sidebar): reuse the palette's default host instead of reading the focus setting again * fix(sidebar): read the host options once in Workspace options and pass them to the Hosts item * fix: parse the ui.get reply into a named type, fix the row memo test's filters, prune the focus-read baseline * test(mobile): give the project catalog fixture its project order * feat(mobile): name the options chip Workspace options, as the desktop does The chip keeps the active-filter count and reads to screen readers with the desktop trigger's label. The pluralized filter count moves to shared code so both surfaces word it the same way. * fix(mobile): show the Workspace options count as a corner badge so the chip row fits "Workspace options (2)" squeezed the Sort and Group chips to ellipses at phone width. The count moves to a corner badge, as on the desktop trigger, and the chip drops its icon, which the desktop trigger does not show either. * refactor(sidebar): one switch-to-kind table, and fold the leftover helper files back - The four kind switches come from WORKSPACE_KIND_SWITCH in workspace-kinds; the shared evaluator and main's phone-push check both apply it, and the filter type derives its switch fields from it. - git-identity.ts folds back into git-identity-display.ts, as on main. - SidebarFilterState moves into sidebar-filter-actions.ts; visible-worktree-kinds.ts is gone. - buildWorkspaceRowPredicate is module-private. * feat(mobile): Workspace options sheet in the desktop menu's shape The sheet showed every choice of every option, so the filters sat screens down. It now follows the desktop menu: Hosts and Projects rows with their value, Group by as a segmented control, Sort by, Project order, Card layout and Show properties as rows that open a page of choices, and the filters as switches. Each option's control (an inline row, or a value plus a page) still comes from one table keyed by the shared option list. The segmented control is the voice settings one, extracted so both screens share it. * fix(mobile): Workspace options review fixes - A page whose option stops showing is forgotten, so it cannot reopen by itself. - Only the switch takes the tap on a filter row, so the web page writes once. - Sections split their rows in one pass; unused heading styles go. * fix(mobile): neutral filter switches on the page react-native-web paints an on-switch thumb teal unless given activeThumbColor, which React Native's types omit. MobileSwitch applies the app's switch colors, and its web sibling also sets that prop. * refactor(mobile): one MobileSwitch file, adding the web-only thumb color on web * fix(mobile): Show properties reads Hover under Compact, as the desktop does The row's value now comes from getWorkspaceCardPropertiesValue in the shared options, which the desktop trigger uses too: Hover under the compact layout, otherwise how many properties show. Card display shows no value, as on the desktop. * test(mobile): find the renamed Workspace options chip in the page drawer render check * feat(mobile): drop the Sort and Group chips; the Workspace options chip covers both The desktop has only its options button, and the sheet now holds Sort by and Group by. The chips' pickers, their screen state and the sort label go with them. * feat(mobile): option pages slide in from the right and keep the sheet's height The main sheet stays laid out under an open page, so the drawer keeps its height going in and out. The page slides in over 200 ms and back out on back (the row, or Android's back key), instantly under reduced motion. The reduced-motion hook moves to platform/ now that two screens use it. * fix(mobile): option pages fill the sheet and open at its top - The page layer grows to the pager's height, so no part of the main sheet shows below a short page. - The drawer takes a scrollResetKey; the sheet passes the open page, so a page opened from a row low on a scrolled sheet starts at the top. - The main sheet's height is measured under a page too, and the pager state lives in the sheet so it resets when the drawer closes. - A misplaced doc comment goes back to its function. * fix(mobile): phone rows show the Jira, automation and CLI glyphs; the options pager forgets a gone page in an effect * fix(mobile): Jira glyph sits before Linear as on the desktop; Jira reader tests beside it * fix(mobile): the phone leaves Ports out of Show properties and Card display * refactor(mobile): clearer names and tests for the phone's hidden card properties * refactor(mobile): one view state and one settings read for the phone's Workspace options; tighter card glyph wiring - The phone's Card layout and card style now live in its view state; one parallel settings.get reads them with pinned placement beside ui.get. - The Card layout preset flag is a write option, no longer a view field or a change-event field. - getWorktreeCardDetails reads the raw desktop settings and resolves the layout once; the desktop card computes it once in its foundation. - The row computes its glyph presence in one place; Ports is hidden through one set. - Drops the vanished-page effect, the native-only a11y props and the web-only switch branch. * fix(mobile): the list settings read applies each half on its own, with tests; pinned placement changes announce themselves * fix(mobile): Card layout writes the setting before its preset; Card display names folder paths when the desktop has folder workspaces |
||
|
|
7c23f79622 |
fix(quick-commands): save agent prompts whole, bounded by what a chat message carries (#24488)
* fix(quick-commands): save agent prompts whole, bounded by what a chat message carries Agent quick-command prompts were silently cut to 6,000 characters on save: the editor accepted a long paste, settings normalization trimmed it, and reopening showed the shortened copy. The 6,000 was a Windows launch speed threshold, not a limit on prompts. - Storage no longer trims. A save is refused, never trimmed, when a prompt's chat text block is over 256 KiB of JSON (measured on its real escaped size, the measure the chat send schema applies) or the list is too large for one paired reply. Host and desktop store both check before writing. - The editor shows the size against the limit and blocks Save past it, and stays open with the text when the host refuses the save. - terminal.quick-commands.long-prompts.v1: hosts withhold prompts over 6,000 characters from clients that do not advertise it (older clients reject a whole list holding one); editors keep an older host's 6,000 cap. - The phone editor no longer drops pasted text past 6,000 via maxLength. Launch delivery of long prompts is left to the launch-prompt series (#24257, #23962, #24080); this branch's earlier paste-after-ready delivery duplicated it and is removed. * fix(quick-commands): drop a stale duplicate doc comment * fix(quick-commands): advertise long prompts to this build's own host too * fix(quick-commands): keep submitted editor immutable until save settles * test(quick-commands): keep storage and save lifecycle coverage focused * fix(quick-commands): keep portalled choices within pending form ownership * Preserve retained credentials when settings validation refuses a save --------- Co-authored-by: OrcaWin <alpha-eng@stably.ai> |
||
|
|
e9c77360f5 |
fix(mobile): open each chat at its newest message when switching chats (#29099)
* fix(mobile): open each chat at its newest message on a chat switch The chat view stays mounted across chat tabs, so a reader's scroll in one chat carried into the next: it opened mid-transcript and not following. Reset follow intent and the page's history head when the chat changes, before paint, and keep view-wide state (Tools toggle, text size). * fix(mobile): pin a switched-to chat to its own end on the page The page pin scrolled to the last reported content height, which after a chat switch is still the previous chat's, so a taller chat could paint one frame mid-history before its own height arrived. Pin to the scroller's live height instead. |
||
|
|
a622f9efd3 |
fix(mobile): stop "Opening workspace" spinning forever when the page never loads (#29274)
* fix(mobile): fail a shell document the view never finishes loading instead of spinning forever A load the WebView refuses or never commits reports nothing, which is how iOS 18 left the OTA workspace page on "Opening workspace" forever (#28626). Arm a document-load deadline whenever the view is given a document; expiry takes the existing document-load-failed recovery (drop the cache, refetch once, then the failure screen with Retry). * refactor(mobile): arm both shell document deadlines from one case |
||
|
|
d6f9df9b74 |
fix(native-chat): settle acknowledged messages after a paged reconnect (#28993)
* fix(native-chat): retire acknowledged send echoes after catch-up * test: follow phone lint rules in reconnect regression * test: format reconnect message expectation |
||
|
|
01800a092a |
chore: update Oxlint and Oxfmt to latest releases (#29291)
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com> |
||
|
|
ef98eff38b |
Allow worktree parents across projects on the same machine (#28942)
* Allow worktree parentage across projects on one host * Keep sidebar placement and project ownership consistent * Keep project labels on phone rows outside their own section * Derive sidebar placement and reveal from shared occurrences * Preserve project text across workspace card styles * Resolve saved lineage peers without rebuilding host projects * fix: preserve independent project deletion progress * fix: satisfy mobile formatting and test lint checks * test: declare project fixture arrays without assertions * Align reveal fixtures with rendered workspace occurrences * fix: reuse project name for cross-repo child cards * fix: satisfy mobile section lint after main merge * fix: retain project label for nested mobile rows * test: keep nested card owner assertion resilient * test: assert project badge visibility across card styles * test: drop lineage tests already covered by the whole-flow suites The host-peer projection is exercised by the runtime and SSH listing integration tests, and the simple phone nesting case is covered by the deep-chain, Pinned and hidden-project flow test. |
||
|
|
e2f2277d51 |
feat(native-chat): one queue header that becomes the pause row; drop the composer Resume and the send dialog (#28654)
* feat(native-chat): one queue header that becomes the pause row; no composer Resume or send dialog The queued cards now sit under one header row on desktop and phone: "Queued" and the count normally, and "Paused" with the count, Clear all and Resume while the host holds the queue after a Stop. A chevron folds only the cards; the choice is local to the view and resets when the queue empties. The composer's Send no longer turns into a second Resume, and a message sent into a paused queue no longer asks "Send message?": it sends, its accepted turn lifts the pause, and the cards follow it. Clear all deletes every card through the existing per-card delete. No wire or host change. * test(mobile): find the background-tasks strip's toggle inside the strip, not the queue header's * Make queued Clear all atomic across clients * Show queued message failures in collapsed headers * Use per-card delete for paused queue Clear all * Join repeated paused queue Clear actions |
||
|
|
11352d4c0c |
fix(mobile): open the OTA workspace page on iOS 18 (#29197)
WebKit reports no source frame for an app-initiated load() before iOS 18.5 (WebKit c28b242), so the iOS shell read its own document load as subframe-initiated and cancelled it, leaving the phone on "Opening workspace" forever. The navigation policy no longer takes the initiating frame as an input: the shell's own document loads when its one-shot load flag is up, the URL is the exact document URL, the target is the main frame, and it is not a download, matching the Android copy. Verified on an iOS 18.1 simulator (hang before, workspace opens after) and iOS 26.5 (unchanged). Refs #28626, STA-10545 |
||
|
|
0d4eb242d7 |
Recover failed-job log retrieval onto main (#29180)
* feat(checks): add optional job log outcome * fix(checks): preserve host capture lifetimes and safe outcomes * fix(checks): retrieve and retry bounded failed-job logs * test(checks): cover queued snapshots and captured error safety * test(checks): verify sanitized host excerpts and paired failures * test(checks): identify measurements and preserve paired cleanup * Use the generated check-log fixture in host retrieval tests * Keep real paired-server shutdown outside the injected stall deadline |
||
|
|
507d0a9122 |
fix(mobile): show answered approvals and questions as receipts, and a waiting question as "Awaiting user input" (#28906)
* fix(mobile): draw answered approvals and questions as receipts, and a pending question as its ask row
The phone projected a structured chat's journal with the host's plain
projection: an answered approval read "Allow Bash? {json} resolved", an
answered question listed its choices without the answer, and every
AskUserQuestion call stayed a raw tool line, also while its card was open
above the composer.
Desktop's question projection and receipt rules move to src/shared
(structured-agent-question-projection, native-chat-resolution-receipt,
with the receipt subject and answer heading and their words). The phone's
structured session now projects through them and hands each row its
receipt; new MobileNativeChatResolutionReceipt and
MobileNativeChatAwaitingInputRow draw them as desktop does. In a terminal
chat, a question call draws as "Awaiting user input:" until answered and
"Asked:" after.
* fix(mobile): say a question was asked, not awaited, once its agent has ended
A structured chat's question stays pending in the journal after the agent
that asked it ends (a restart, a crash, a new generation). The host stops
listing it as actionable, so the answer card goes away, but the phone's
row still read "Awaiting user input" and announced itself as live.
The session hook now returns the host's actionable prompt ids next to the
receipts, and each row asks the same shared
isActionableStructuredAgentSessionPrompt desktop's transcript row calls.
A question nobody waits on reads "Asked: …", as on desktop. The row
callback moves to its own hook so the chat view stays within its size.
* fix(mobile): decide a native chat's open question with desktop's selector
The phone decided whether a question call was still awaited with its own
rule (no answer yet and the turn working). Desktop asks the shared
selectActiveToolCall, which also reads the call's state, so a call marked
completed with no output reads "Asked" there and "Awaiting" on the phone.
Native chats now call the same selector. Terminal chats keep their rule.
* fix(mobile): zoom a receipt and its question row with the chat
Before receipts, an answered question or approval was a faint system line
that grew with the chat's pinch zoom. The receipt and the question row drew
at fixed sizes, so zooming no longer enlarged them. Both now take the
chat's zoom, through the same scaledType the notice row uses, which moves
next to the other row styles.
* fix(mobile): list a group's questions once nobody can answer them, and zoom the row's icon
A grouped prompt's row names only a count ("Asked: 2 questions"). Once it
is cancelled, or the agent that asked has ended, the answer card is gone
and the receipt lists no answers, so the phone showed the questions
nowhere. The row now lists them under its line, following desktop's
listsQuestions rule: the receipt asks for the list only when it shows no
answers, and the transcript's own question rows keep it off, as desktop's
tool run does. While the host waits, the card shows them.
The row's icon also grows with the chat's zoom, as the notice row's icon
does, so it stays level with its line.
* refactor(mobile): trim comments and simplify the question row and receipt
Comments that restated the code go. The answer heading takes the subject
its caller already has, dropping two conditions that never changed the
result, on desktop and the phone. The question row lists a group's
questions whenever its caller asks, as desktop's does, and the receipt
asks only while nobody waits. fontScale is required on both new rows.
|
||
|
|
da8de743eb |
feat(mobile): the phone filters and sorts with the desktop's shared Workspace options rules (#28986)
* fix(mobile): phone server rows follow the desktop's other-client and sleeping filters A phone created through a desktop now records whom it acted for, so the desktop's Hide other-client workspaces filter keeps the user's own phone workspaces (locally and on servers) and hides only other people's. The desktop relay marks each server row its sidebar would hide, and the rows its own browser or chat tabs keep awake; the phone applies both. * test(e2e): dedupe sidebar row ids in the visibility spec * fix(mobile): address review: owner naming, old-server relayed phones, scope trim * refactor: move the sidebar's project section grouping into shared The project sections the desktop sidebar draws under Group by Project are built from the project/setup projection, the per-host catalog fold, the endpoint host stamp, and the section labelling and ordering rules. Move the pure parts of each into src/shared (no behaviour change) so the paired phone can call the same code instead of guessing the grouping. * fix(mobile): group by Project with the desktop sidebar's own sections The phone grouped Project sections by repo display name, so it merged checkouts the desktop keeps apart (several local stablyai/orca checkouts, or a server's copy) and split what the desktop merges. The phone now reads each host's repo.list, project.list and projectHostSetup.list (the desktop's own and each server's through the relay), folds them with the renderer's shared catalog fold and projection merge, and builds sections with the desktop's shared grouping, labelling and ordering code. Keys, labels and Recent order equal the sidebar's; a host that predates the project lists gets the repo-derived projection, as the desktop does. Header colour and icon come from the section's anchor repo and rows from their own repo by id, as on the desktop. The repo filter lists every catalog repo by id per host instead of merging same-named repos. Group-by labels read Project and None. project.list and projectHostSetup.list are routed for paired phones (read-only, workspace permission). * refactor(mobile): record a phone's owner as creator and fold desktop tab activity into the host flag A phone now records its owner as the creator (this host for a directly paired phone, the relaying desktop for a delegated one), so the shared rule is unchanged and onBehalfOf is gone. The relay ORs the desktop's saved browser/chat tabs into hasHostSidebarActivity, so the phone's status dot follows too, and local worktree.ps counts the same signals. The old-server fallback is removed. * test(e2e): poll the phone and the sidebar together in the visibility spec * fix(mobile): one Project-section builder for desktop and phone, with the desktop's repo order Review fixes. The sidebar's buildOrderedGroups now calls the shared buildProjectSections for Group by Project (notice and pending repos are optional inputs), so desktop and phone run one function. The phone ranks manual header order by its folded catalog order, which is each host's repo.list order with the desktop's own repos first, as the renderer loads them; only the desktop's cross-host manualRepoOrder (pairing-local) is unknown to it. The projection selector is now a memo over the shared resolver. A transient project-list failure keeps that host's last catalog; only forbidden or method_not_found falls back to the repo-derived projection. Catalog rows are checked on every member grouping reads, and the parity test answers the renderer's real catalog fetch, includes a server's own project rows, and fixes Manual and Recent orders that differ. * fix(mobile): use the repo-derived catalog until a host's project lists first answer * fix(mobile): the desktop serves its sidebar's Project catalog instead of the phone fetching it The phone fetched repo.list, project.list and projectHostSetup.list from the desktop and every relay-ready server each minute and folded them itself, so rows from offline or older servers fell back to repo sections and keys moved on cold start. The desktop renderer now publishes the inputs its own Project headers use (state.repos in sidebar order, the projection selector's projects and setups, projectOrderBy) on the runtime window graph it already sends to main, only when they change. Main keeps the last one and answers it on a new paired-desktop read, mobileRelay.projectSections. The phone reads it with its repo metadata and groups with the shared builder; a desktop that predates the read (forbidden or method_not_found) leaves the phone grouping rows by repo from the desktop's repo.list, and a transient failure keeps the last catalog. The payload carries only grouping and label fields, no hooks, credentials or uploaded icons. The phone's own catalog fetching and its project.list/projectHostSetup.list routes are removed. * fix(mobile): keep uploaded repo icons on the phone, and isolate the snapshot tests * test(e2e): the desktop window serves its sidebar's Project catalog to the phone * chore(i18n): regenerate the runtime English catalog (drift already on main) * fix(mobile): read the Project catalog as its own request, outside the recorded repo-metadata flow The extra read inside the repo-metadata refresh stalled its recorded RPC sequence. It now runs beside it with its own throttle. The repo-metadata goldens are re-recorded for the intended state change: the name-keyed colour, icon and id maps became desktopRepos. * refactor(sidebar): one shared Workspace options filter and sort evaluator The sidebar, the jump palette and the options badge each applied the Workspace options filters with their own copy of the rules. They now call one pure evaluator in src/shared/workspace-view, which reads each row through a small fact reader so the phone can call it next with the facts its host serves. The sort comparator moves there too, with each surface supplying its own Agent activity rank. No behaviour change: a parity test runs the old sidebar chain and the new evaluator over every toggle combination, and the old comparator against the new one for each sort mode. * wip: workspace view descriptors * refactor(sidebar): address review of the shared Workspace options evaluator One predicate builder instead of a per-row scope argument; one exemption-narrowing function taking the hide flag; consumers import the shared predicates directly; Agent activity ties fall to the label in the shared comparator. The activation filter lift reuses the shared predicates. The parity test now covers other-client rows, live agent attention classes and the sort-label map. * fix(mobile): fetch the Project catalog at once when the screen moves to another host The refresh window is now kept per client, so a host screen reused for another desktop doesn't wait out the previous host's 60s window before showing its Project headers. * wip: desktop option items from shared choices * refactor(sidebar): the Workspace options menu renders from one shared option list src/shared/workspace-view/workspace-view-options.ts lists every Workspace options control (Hosts, Projects, Group by, Sort by, Project order, Card layout, Show properties or Card display, and each filter) with its section, its English copy, its choices and when it shows. The desktop menu walks that list and renders each option through a table keyed on the option key, so a new option fails to compile until the desktop has a control for it; the phone sheet will do the same. The choice lists (sort modes, project order, card layout, agent activity layout, card properties) are derived from the shared ids, and a test checks the desktop's English copy matches the shared copy. The unused classic branch of the card property list is removed. No visible change. * wip: row facts * refactor(sidebar): address review of the shared Workspace options list The show rules get their own tests with fixed expectations (the cases the old filter-section test covered, plus the rest), and the menu test checks each option's own control rather than any matching text. Card display rows toggle one property each, with the folder-path copy in shared. The unused Status and Tasks copy left by the removed classic card list is deleted from the catalogs. Filter rows write through the store directly; files are named for what they hold. * feat(runtime): worktree.ps carries the Workspace options row facts Each worktree.ps row now says whether it is the project's default-branch workspace, created by an automation, created by the CLI, or on a detached HEAD, answered by the same shared predicates the desktop sidebar filters with. The phone has none of these facts today, so its copies of the filters either guess (default branch) or are missing. Additive and inert: optional fields that nothing reads yet. An older host omits them; readers must treat absence as 'not that kind', so the row stays visible. Server rows relayed through the desktop pass them through unchanged. * feat(mobile): the desktop's own phone reads and writes its other-client filter ui.get and ui.set strip the pairing-local UI fields for every paired client. That rule exists for clients with their own host catalog (a desktop paired to a server, a web client). A phone has none: it shows its desktop's view, so 'Hide other-client workspaces' must be one shared value. For the host's own phone (clientKind mobile, no relaying parent) ui.get now always answers the field and ui.set persists it; delegated phones, paired desktops and web clients keep the stripping rule. The phone's view-settings schema reads the rest of the Workspace options values it does not read yet (the automation, CLI and detached-HEAD filters, project order, card properties, agent activity layout), each salvaged to absent so an older desktop keeps the phone's local value. * feat(runtime): address review of the worktree.ps row facts Name the fact defaultBranch, and say what absence means for it: a reader keeps its own guess, because 'hide default branch' must not start hiding or showing rows an older host never described. Folder workspace rows get the facts too, so every row from a newer host carries them. The desktopOtherClient comment now says it is a verdict, not a fact. The test covers the rows where the host disagrees with the phone's current guess. * wip: phone uses the shared evaluator * feat(mobile): address review of the phone's other-client filter mirror One isHostOwnPhone predicate decides both who shares the filter value (ui.get/ui.set) and who gets the other-client row marks, so the two cannot drift. The pairing-local field list notes the exception. Tests cover a paired desktop still being stripped, and a phone write that leaves the field out not touching the host value. * feat(mobile): the phone filters and sorts with the desktop's shared Workspace options rules The phone's own filter chain and comparator are replaced by the shared evaluator, reading the facts the host stamps on each row. Phone filter state takes the desktop's setting names and carries every filter; the badge and Clear filters match the desktop's. The desktop's other-client marks become facts the phone filters with the shared value. A parity test runs the same workspaces through the desktop's reading and the phone's (facts stamped as the host stamps them) over every filter combination, folder projects included. * fix(mobile): host scope in shared filters, patch-key writes, live view push Folds the phone's host filter into the shared filters so Clear and the badge agree, writes back only the touched filter keys, and pushes workspaceViewChanged so an open phone list re-reads desktop Workspace options. * fix(mobile): Clear sends only changed filters; skip a view re-read older than a local edit An older host's strict ui.set refuses a key it lacks, so Clear names only what differs. A ui.get that started before a local edit no longer overwrites it; the edit's own change event re-reads. The change publisher's field list now lives with it and derives the filter fields. * fix(workspace-options): all hosts and clear reach the desktop; one filter key list drives every surface Host visibility: the legacy host scope fallback moves to shared code; the phone reads it, and the host resets the scope whenever a client writes the host list, so All hosts and Clear stick. An older desktop that keeps the other-client filter to itself has its marks treated as hide. Badge counts, is-anything-filtered, Clear and reveal derive from the shared filter keys on the desktop sidebar and the kanban filter menu. The phone uses the desktop's group and sort values, holds its view in one state value, and reads its UI fields from one shared list. * feat(workspace-options): Status and PR grouping read the shared tables The phone groups by status with the desktop's status helpers and default catalog (its own copy had the defaults in reverse order) and by PR with a shared lane table, so its labels now read In review / In progress as the desktop's do. The desktop's PR lane decision ends in the same shared state-to-lane step. * test(sidebar): folder rows survive restrictive filters; create grace ignores non-finite times * fix(mobile): write the host scope with the host list, and wait for the first read before forcing the other-client filter An older desktop narrows back to its legacy single-host scope when the phone writes only a null host list, so the phone now also writes the scope for a null or one-host list. The other-client fallback waits for the first ui.get so a new desktop's marks don't hide rows before its filter value arrives. Inline the clear-filters key list and note the PR state asymmetry. * test(mobile): re-record view-settings goldens for the unknown other-client state * fix(sidebar): reuse the palette's default host instead of reading the focus setting again * fix(sidebar): read the host options once in Workspace options and pass them to the Hosts item * fix: parse the ui.get reply into a named type, fix the row memo test's filters, prune the focus-read baseline * refactor(sidebar): one switch-to-kind table, and fold the leftover helper files back - The four kind switches come from WORKSPACE_KIND_SWITCH in workspace-kinds; the shared evaluator and main's phone-push check both apply it, and the filter type derives its switch fields from it. - git-identity.ts folds back into git-identity-display.ts, as on main. - SidebarFilterState moves into sidebar-filter-actions.ts; visible-worktree-kinds.ts is gone. - buildWorkspaceRowPredicate is module-private. |
||
|
|
f3551dba57 |
fix(mobile): show skill commands you ran and draw warnings, errors and status lines as notices (#28899)
* fix(mobile): show skill commands you ran in the chat Claude records a skill run (/simplify, /review-pr 123) as a command envelope, which the noise filter hides. Desktop surfaces it as the typed `/name args` before folding; the phone only normalised image markers, so the turn vanished and the reply answered nothing. Both apps now prepare a terminal agent's transcript through one shared prepareNativeChatMessages: skill envelopes, reporting-command output and image marker turns. * fix(native-chat): keep a streamed sentence from coming back after a tool or thought row Both chats hide the live preview once the transcript shows it, but each only compared the preview with the last row. When a tool call or thought landed below the sentence, the last row had no words and the earlier sentence came back at the bottom as a live message. latestNativeChatReplyText reads back past those rows to the newest assistant words in the turn; desktop's preview check and the phone's streaming gate both use it. * fix(mobile): draw errors, warnings and status notices as notices, not agent replies The phone drew every system row through the agent's markdown, so an API retry warning, a sign-in failure, "Context compacted" or "Stopped manually" read as the agent's own words. Only the clear boundary and two host notices were special-cased. Desktop's notice rules move to shared native-chat-notice: which block of a system row is its notice, and the look it gets (divider, command output, host status, plan, aside, or a toned line). Desktop's notice row switches on it with no change in what it draws; the phone's new notice row draws the same looks, with words in faint type when a row has no notice. * fix(native-chat): show a new reply that follows a tool call on desktop The earlier fix read back past a tool call or thought to the newest assistant words, then kept desktop's length rule. Commentary written before the tool is often longer than the reply after it, so a new, shorter reply stayed hidden until the transcript caught up. The last-row rule is back as it was. Reading back now only hides the preview when the earlier words already contain it, which is the sentence still on screen while the tool runs. The phone's gate reads only rows that landed after the current reply began, so it was not affected. * fix(mobile): open the actions sheet from a long press on a plan's link A plan card's Markdown was drawn without the row's long-press handler, so on Android holding a link or file path inside a plan no longer opened Copy message / Select text, as it did before plans were drawn as notices. The notice row now passes the handler to the plan's Markdown. |
||
|
|
d9de1fad1d |
Native chat: desktop and mobile read the host's working answer; plain refusal for a stopped agent's prompt (3/5, rebuild of #26867) (#28104)
* feat(native-chat): record which agent run wrote each journal item Each journal item row, subagent roster entry and background-task entry now records ownerFence: the lease fence of the agent run whose output produced it. Optional, persisted, no row version bump and no migration. The journal's itemFence() returns it, so the turn-verdict rules and per-run provider activity read the producing run rather than the writer's fence. * feat(native-chat): one current-work projection for every host reader; record death first The host decides what work is current in one place (structured-agent-session-current-work.ts) from the lease, the exit this host saw, the runtimes it replaced, its live child and each item's provenance. Every host reader (queue, Stop, prompt answers, commands, status feed, idle sweep, teardown snapshot, history pages and frames, orchestration mailbox) reads it, so a dead agent's leftovers stop reading Working or holding the queue even when their cleanup write failed. The journal's activeTurnId()/liveTurnScope() become the raw runningTurn(). On exit the host records the child's end first and signals generationEnded, which republishes frames, the status feed and settlement waits and wakes the queue drain with no journal row. Pages and frames carry optional working and actionablePromptIds fields. * feat(native-chat): clients read the host's working answer; refuse answers to a stopped agent plainly Desktop and mobile prefer the host's working and actionablePromptIds fields when present (merged across batches like latestTurn) and fall back to today's row derivation against an older host. A prompt an ended agent raised stops looking active; answering it is refused before any provider sees it, with "<Agent> has stopped, so your answer was not sent. Send a message to continue." Cancelling it only dismisses it in the journal: it no longer triggers the chat's Stop, pauses the queue or withdraws queued cards. * refactor(native-chat): keep structuredAgentSessionHandsOver file-local until a caller needs it Nothing outside the projection calls it in this part; Part 5 exports it for the start-failure row. * test(native-chat): fewer provenance tests: fold reopen and unstated-owner checks into the main case Drops the epoch-replacement case (Part 2 rewind test reads the same values through the same rebuild) and the corrupt-value case (no version writes one). * test(native-chat): drop current-work mirrors: the viewKey unit case and the still-live rewind case The flow cases keep every outcome: dead leftovers read as nothing, Stop has nothing to stop, a waiting card drains, a live owner publishes as before, and a rewind never promotes dead work. * test(native-chat): fewer client tests: one flow per client, one fallback each Keeps desktop and phone Working from the host (with the older-host fallback), the reducer merge contract, the ended question Cancel that only dismisses, the agent-named refusal and the resolution receipt. Drops per-helper prompt tests, the transcript-placement test, the stop-note client reduction, the Steer cards that match main, and per-reason refusal rows. * test(native-chat): an unstamped roster entry from an older build reads as its row provenance Moved here from Part 4 so it sits beside stampJournalEntryProvenance. * test(native-chat): build the older-build roster row inline, after main removed its body helper Main #27947 writes one row per subagent and deleted journal-subagent-group-body; a roster row from an older build is still what this case reads. * refactor(native-chat): provenance is per item only; drop per-entry stamping Main's #27947 writes one saved row per subagent, and each background task already has its own row, so an item's own ownerFence names the run of every entry a producer still writes. Per-entry stamps inside a row mattered only for the shared roster rows saved before #27947, and no run writes into those any more, so the row's own provenance settles them correctly. Removes the entry stamping, the optional ownerFence on roster entries and background tasks, and their tests. * fix(native-chat): an item's provenance is set when it is created; revisions keep it A newer run's provider can revise an item an older run created: Pi's start and an ACP session/load both report context usage onto the newest saved turn. Restamping that turn with the newer run let the newer run's exit settle the older run's unverifiable turn as interrupted. Only creation states an owner now, as main's itemFences rule did. * test(native-chat): keep the provenance test below the production lines it covers * fix(native-chat): an ended agent's card Cancel commits with its receipt, through dismissPrompt The ended-generation dismissal wrote the cancelled resolution without the Cancel's operation receipt and without the dismissal's failure handling, so a receipt write failing after it would answer "outcome unknown" over a card already closed. It now goes through dismissPrompt, which carries the uncommitted receipt in the same append and answers dismissalNotSaved on failure; a null `answer` skips the adapter, since no live provider holds an ended generation's card (Pi's adapter refuses a fence with no live session outright). |
||
|
|
7cf09809f6 |
fix(mobile): group by Project with the desktop sidebar's own code (#28850)
* fix(mobile): phone server rows follow the desktop's other-client and sleeping filters A phone created through a desktop now records whom it acted for, so the desktop's Hide other-client workspaces filter keeps the user's own phone workspaces (locally and on servers) and hides only other people's. The desktop relay marks each server row its sidebar would hide, and the rows its own browser or chat tabs keep awake; the phone applies both. * test(e2e): dedupe sidebar row ids in the visibility spec * fix(mobile): address review: owner naming, old-server relayed phones, scope trim * refactor: move the sidebar's project section grouping into shared The project sections the desktop sidebar draws under Group by Project are built from the project/setup projection, the per-host catalog fold, the endpoint host stamp, and the section labelling and ordering rules. Move the pure parts of each into src/shared (no behaviour change) so the paired phone can call the same code instead of guessing the grouping. * fix(mobile): group by Project with the desktop sidebar's own sections The phone grouped Project sections by repo display name, so it merged checkouts the desktop keeps apart (several local stablyai/orca checkouts, or a server's copy) and split what the desktop merges. The phone now reads each host's repo.list, project.list and projectHostSetup.list (the desktop's own and each server's through the relay), folds them with the renderer's shared catalog fold and projection merge, and builds sections with the desktop's shared grouping, labelling and ordering code. Keys, labels and Recent order equal the sidebar's; a host that predates the project lists gets the repo-derived projection, as the desktop does. Header colour and icon come from the section's anchor repo and rows from their own repo by id, as on the desktop. The repo filter lists every catalog repo by id per host instead of merging same-named repos. Group-by labels read Project and None. project.list and projectHostSetup.list are routed for paired phones (read-only, workspace permission). * refactor(mobile): record a phone's owner as creator and fold desktop tab activity into the host flag A phone now records its owner as the creator (this host for a directly paired phone, the relaying desktop for a delegated one), so the shared rule is unchanged and onBehalfOf is gone. The relay ORs the desktop's saved browser/chat tabs into hasHostSidebarActivity, so the phone's status dot follows too, and local worktree.ps counts the same signals. The old-server fallback is removed. * test(e2e): poll the phone and the sidebar together in the visibility spec * fix(mobile): one Project-section builder for desktop and phone, with the desktop's repo order Review fixes. The sidebar's buildOrderedGroups now calls the shared buildProjectSections for Group by Project (notice and pending repos are optional inputs), so desktop and phone run one function. The phone ranks manual header order by its folded catalog order, which is each host's repo.list order with the desktop's own repos first, as the renderer loads them; only the desktop's cross-host manualRepoOrder (pairing-local) is unknown to it. The projection selector is now a memo over the shared resolver. A transient project-list failure keeps that host's last catalog; only forbidden or method_not_found falls back to the repo-derived projection. Catalog rows are checked on every member grouping reads, and the parity test answers the renderer's real catalog fetch, includes a server's own project rows, and fixes Manual and Recent orders that differ. * fix(mobile): use the repo-derived catalog until a host's project lists first answer * fix(mobile): the desktop serves its sidebar's Project catalog instead of the phone fetching it The phone fetched repo.list, project.list and projectHostSetup.list from the desktop and every relay-ready server each minute and folded them itself, so rows from offline or older servers fell back to repo sections and keys moved on cold start. The desktop renderer now publishes the inputs its own Project headers use (state.repos in sidebar order, the projection selector's projects and setups, projectOrderBy) on the runtime window graph it already sends to main, only when they change. Main keeps the last one and answers it on a new paired-desktop read, mobileRelay.projectSections. The phone reads it with its repo metadata and groups with the shared builder; a desktop that predates the read (forbidden or method_not_found) leaves the phone grouping rows by repo from the desktop's repo.list, and a transient failure keeps the last catalog. The payload carries only grouping and label fields, no hooks, credentials or uploaded icons. The phone's own catalog fetching and its project.list/projectHostSetup.list routes are removed. * fix(mobile): keep uploaded repo icons on the phone, and isolate the snapshot tests * test(e2e): the desktop window serves its sidebar's Project catalog to the phone * chore(i18n): regenerate the runtime English catalog (drift already on main) * fix(mobile): read the Project catalog as its own request, outside the recorded repo-metadata flow The extra read inside the repo-metadata refresh stalled its recorded RPC sequence. It now runs beside it with its own throttle. The repo-metadata goldens are re-recorded for the intended state change: the name-keyed colour, icon and id maps became desktopRepos. * fix(mobile): fetch the Project catalog at once when the screen moves to another host The refresh window is now kept per client, so a host screen reused for another desktop doesn't wait out the previous host's 60s window before showing its Project headers. |
||
|
|
095399aa4d |
fix(mobile): render markdown with markdown-it instead of a hand-written reader (#28858)
* fix(mobile): size markdown table columns to their content * fix(mobile): always show the copy button under chat messages Revealing it on tap shifted the transcript. * fix(mobile): read markdown with a CommonMark parser markdown-it rather than desktop's micromark: about 8x faster on the phone's JavaScript engine. |
||
|
|
3b11b7ac94 |
fix(mobile): phone server rows follow the desktop's other-client and sleeping filters (#28819)
* fix(mobile): phone server rows follow the desktop's other-client and sleeping filters A phone created through a desktop now records whom it acted for, so the desktop's Hide other-client workspaces filter keeps the user's own phone workspaces (locally and on servers) and hides only other people's. The desktop relay marks each server row its sidebar would hide, and the rows its own browser or chat tabs keep awake; the phone applies both. * test(e2e): dedupe sidebar row ids in the visibility spec * fix(mobile): address review: owner naming, old-server relayed phones, scope trim * refactor(mobile): record a phone's owner as creator and fold desktop tab activity into the host flag A phone now records its owner as the creator (this host for a directly paired phone, the relaying desktop for a delegated one), so the shared rule is unchanged and onBehalfOf is gone. The relay ORs the desktop's saved browser/chat tabs into hasHostSidebarActivity, so the phone's status dot follows too, and local worktree.ps counts the same signals. The old-server fallback is removed. * test(e2e): poll the phone and the sidebar together in the visibility spec |
||
|
|
c7cb5d154a |
fix(mobile): open Create Workspace and Close issue from Tasks on iOS (#28855)
Each Tasks sheet was its own native modal, and iOS refuses to present one over another that is still showing. They now share one modal and stack by zIndex. Fixes #21003 |
||
|
|
6d519f7835 |
fix(mobile): keep content clear of the side insets in landscape (#28749)
In landscape the iPhone sensor housing and Android's side nav bar are left/right insets, which no screen or over-the-air page padded for. Fixes #21190 |
||
|
|
6607c54984 |
perf(ci): cut redundant tests and reuse expensive setup (#28669)
* perf(ci): reduce test overhead and reuse expensive build work * test(ci): keep worker startup outside failure-case deadlines * test: isolate push fixture advisory locks * test(ci): preserve fixed drain retry allowance * Align CI contracts with current triggers and guide content * Stabilize consolidated reveal scenarios and retain behavioral CI contracts * Fix upstream lint blockers while retaining behavior and assertions * Classify global Jira preferences in the WSL migration inventory * Align Jira test fixtures with startup and build safety checks * Include package-test routing dependency in CI checkouts and fixtures * Keep native stream classes intact in transcript dependency tests |
||
|
|
27c0275e3a |
feat(mobile): log OTA shell lifecycle to the connection diagnostics (#28840)
* feat(mobile): log OTA shell lifecycle to the connection diagnostics Exported connection logs showed nothing about the web shell, so a phone stuck on "Opening workspace" could not say whether it stalled activating the download, loading the page, or waiting for its first paint. Record sparse shell milestones, page signals, one-shot 15s stall warnings and bridge diagnostics into the per-host connection log. Diagnostics only. Refs STA-10545, #28626 * fix(mobile): cut shell log volume and pinpoint each waiting screen Every page route mounts its own shell, so four lines per healthy open evicted connection history from the 200-entry log. A healthy open now writes one line, on paint, carrying source, build and elapsed. The 15s stall line now also covers "Checking host" (gate verdict and whether the cache read answered) and a download with no progress. The "page has not drawn" line counts the page's ready asks and whether the document loaded, telling a page that keeps refusing init from one that took it and died. Frames dropped with no bridge host, an unreadable host profile, post-failed error names and route-refused issues are now logged; path masking catches quoted, bracketed and Windows paths. The exported report header names the shell build and bridge protocol. Diagnostics only: no behaviour, UI or wire change. Refs STA-10545 * fix(mobile): dedupe flapping shell lines and mask spaced paths A host with no cache flapping between unreachable and dialling wrote "checking host" and "host unreachable" every cycle per mounted shell; each is now written once per host until a different shell line lands. Path masking keeps going across a space while the next word still holds a separator, and accepts a comma before the path. The paint line's elapsed time is renamed activatedMs, which is what it measures. A route-only session rebuild re-arms the stall timer. Refs STA-10545 * refactor(mobile): split the shell log module by responsibility shell-lifecycle-log.ts passed the 300-line lint cap. The line type, recorder and flapping dedupe move to shell-lifecycle-line-recorder.ts, bridge diagnostic lines to shell-bridge-log-lines.ts, and user-path masking to shell-log-path-masking.ts, with tests split to match. No behaviour change. Refs STA-10545 |
||
|
|
6f929849eb |
fix(mobile): workspace list host badge, "workspaces" wording, and search keyboard on the page (#28824)
* fix(mobile): workspace list host badge, wording, and search keyboard on the page - Host badge no longer shrinks to "m…"; the branch/path text gives up the space. - Search placeholder, accessibility label, and empty states say "workspaces". - The page's workspace list no longer closes the keyboard on each narrowing keystroke: react-native-web dismisses on-drag on every scroll event, including the offset clamp. * fix(mobile): let the row path take only the leftover space instead of pinning the host badge A non-shrinking badge overflowed narrow, indented rows; a zero-basis path keeps a short host name whole and still lets the badge shrink when the row is genuinely too narrow. * fix(mobile): reword the row path's layout comment * refactor(mobile): inline the page's keyboard dismiss choice and use flex: 1 for the row path Drop the platform split and its react-native-web test: an inline Platform.OS check states the same choice where it is used. flex: 1 renders identically to flexBasis 0 + flexGrow 1. |
||
|
|
e5e6189f96 |
Split Jira issue workspace hooks and IPC param checks (#28759)
* refactor(jira): split issue workspace hooks and IPC param checks - Move IPC argument normalization into jira-ipc-params.ts so the jira:* handlers and its tests share one validator. Issue edits are now also checked for labelOperations, which the host reads, so malformed label edits are rejected before they reach Jira. - Move issue hydration and remote detail state out of JiraIssueWorkspace.tsx into use-jira-issue-workspace-data.ts, and comment submission into use-jira-issue-comment-submission.ts, so the component stays under the file-size limit. - Split the reopened-issue and rollback optimistic-edit tests into their own files to keep each test file focused. - Move runtime capability constants into runtime-capability-constants.ts and re-export them from protocol-version.ts, so the values keep the same names and exports for existing importers. * refactor(protocol): use named imports for runtime capability constants Import the runtime capability constants by name instead of destructuring them from a namespace object. The bindings and values are unchanged. Named imports make the dependency explicit to readers and tooling, and the protocol version comment now sits next to the constants it describes. |
||
|
|
a0c46b405f |
feat(jira): load attachment images outside issue Markdown (#28631)
Load Jira attachment images separately from issue Markdown so descriptions and comments stay small while screenshots load lazily and open in the existing lightbox. Downloads begin when an image mounts, capture its owning host, require negotiated image support, and use bounded authenticated byte streams and caches. Older clients retain their inline-image handlers. Keep legacy and cancellable downloads in separate in-flight domains so cancelling a new image reader cannot abort a legacy reader; completed bytes share the bounded cache. Preserve the landed assignee, board, transition/comment queue and request lifetimes. Move unchanged project metadata IPC handlers into their domain module and tighten two inherited focus-check ceilings without changing behavior. Validation:410 tests/54 suites; all changed-source quality stages, lint, format, routing and contract checks; exact final node/web types; independent source review;48/48 fresh hidden Electron checks with226 stable source hashes and nine inspected screenshots. Synthetic transports exercise production clients and UI. Current CI static checks, mobile bundle, wire compatibility, Linux/Windows packaging and Windows serve pass; five test shards were pending at review. Extends the existing attachment work credited to Frank (@lifefloating) and Brennan Benson in PR #8938; existing contribution credits and media remain in this PR. |
||
|
|
8e6848174c |
refactor: clear Jira CI lint failures without behavior changes (#28717)
Restore existing Jira CI lint checks without runtime behavior changes. Existing generation types are moved verbatim and re-exported; the mobile fixture retains object identity coverage with a named client type. Validation: renderer typecheck, 10 generation tests, 3 mobile identity tests, changed-file quality, full-file lint, formatting, and independent source review passed. |
||
|
|
cacce1790d |
feat(tasks): browse Jira and start workspaces on mobile (#28616)
Browse Jira on mobile and create linked workspaces through the paired desktop, retaining shared task queries and protecting account/session changes. Co-authored-by: Minho Yi <tisohjung@gmail.com> |
||
|
|
64538a680c |
fix(tasks): make mobile and desktop searches agree (#28580)
Use one token-based task search implementation on desktop and mobile. Preserve explicit issue/PR qualifiers, distinguish quoted search text, and follow shared reexports in mounted mobile recordings. Refs #10950 and #11121. Co-authored-by: MumuTW <42820974+MumuTW@users.noreply.github.com> |
||
|
|
b84d371a9f | refactor(runtime): compose typed browser command owners (#27576) | ||
|
|
cd1ff506e1 |
feat(mobile): list the desktop's server projects on the phone's Tasks screen (#27741)
* test(e2e): add a paired phone client helper and phone-mirror baseline specs Later phone-mirror slices need one oracle that drives a real mobile-scoped device with the phone's own capabilities against a serve host or a paired desktop. Pins #26022 across a serve restart and today's gap: a desktop's phone does not list the desktop's server workspaces. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * feat(mobile): show SSH host health on the workspace row host badge Rows on an SSH host the desktop cannot reach looked healthy on the phone. The badge now adds the desktop sidebar's own word ("devbox · Disconnected"), derived by the same shared function from the connection status ssh.listTargetSummaries already sends; old desktops keep today's badge. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(e2e): compose the phone-mirror topology and check the cold snapshot Slices need one composer for host, desktop and phone with a single teardown, so specs stop copying setup. The serve-restart spec now reads the first tab-stream snapshot after the cold start, which is what the phone builds its tab strip from. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * feat(mobile): name an unhealthy SSH host even in a single-host list The desktop card shows SSH status on any host count, so the phone now reads target summaries whenever a repo is on SSH and badges an unhealthy host alone too. Labels and health share one target-to-host keying, and a hook-level test pins the reply-to-row wiring. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(e2e): keep phone-mirror teardown errors from masking test failures Teardown failures become a test attachment instead of replacing the real failure, and the cold-snapshot poll reports stream errors together with the PTY list. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): clear stale SSH host badges and announce them without the dot A catalog that no longer spans hosts or holds an SSH repo now clears the health and label maps, so a host that left cannot keep badging rows. The badge gets a screen-reader label ("devbox, Disconnected") instead of reading the middle dot. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * feat(runtime): let a paired desktop register its phones as delegated mobile devices A desktop paired to this runtime can now call pairing.delegatedMobileDevice.sync to make each of its phones an ordinary mobile-scope device it owns, so the phone mirrors that host through the desktop. Children are never pending, inherit the parent's reach, and die with the parent's grant. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * refactor(runtime): move the mobile device revoke primitive below dispatch Dispatch binds the caller-bound delegated-device context, so the revoke path it needs now lives in a layer under it as a pure move; this drops the abstract method seam. No behaviour change. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(runtime): keep a runtime grant until its delegated phones are revoked A failed child cleanup now leaves the parent in place so a retry can still find the children, and revokeRuntimeAccess lives beside the revoke it cascades to. Adds restart, rename and bounds coverage, shares credential minting, and drops the timing-dependent census. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(runtime): fail a delegated sync whose dropped phone cannot be revoked Matches the parent revoke: a child whose cleanup could not be saved is kept and the sync errors before minting anything, so the desktop retries instead of believing the server matches its list. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * feat(mobile): relay a paired phone's server-workspace requests through the desktop A phone mirrors its desktop, including workspaces on the desktop's configured servers. The desktop now forwards a request that names `executionHost: runtime:<env>` to that server over one socket per (phone, server), signed in as the phone's delegated device; replies pass through untouched except stream ids. Hidden until the phone client ships: the relay capability is not advertised yet. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): type the single-host health fixtures inline so the test tsc passes Fixtures hoisted into consts widened their health maps to string, which the now-required hostHealthById rejects under the tests-typecheck ratchet; passing them inline keeps the union. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * docs(mobile): state the desktop relay contract in its entry module main dropped docs/reference, so the relay's contract lives beside the code. Also pins that a server whose delegated sync fails (e.g. delegated_device_revoke_failed) is unavailable until a later request syncs it again. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): pin the relay's configured-server adapter The production hosts adapter was only typechecked; this pins that it resolves configured servers by exact id only (never a name or a removed server), fences by pairing revision and runtime identity, and stops reporting retirements once unsubscribed. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): re-record the repo-metadata goldens for SSH host health The settled metadata state now carries hostHealthById, and an SSH-only catalog now reads the target summaries so a lone unhealthy host can be badged. Scoped re-record via rpc-recording.mts. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * refactor(mobile): tighten the desktop relay after elegance review Reuses the shared E2EE socket opener, folds relay state into fewer fields, never sends a request whose link ended while opening, passes failure codes through, relays only this desktop's own phones, and drops the unused host-state reader until S2 needs one. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): bound relay frames and stop sticky relay verdicts Restores the 8 MiB frame cap on every remote runtime socket, re-checks an update-needed server on each new link so an in-place upgrade relays, answers a phone the server just declined without syncing again, and ends a link whose mirrored capability update cannot be queued. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): run targeted desktop-owned calls on the desktop The phone tags every call on a workspace screen with its execution host, so a paired-desktop method carrying `runtime:<env>` now runs locally instead of failing 'forbidden'. The Mac stays the only reader of the routing table. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * feat(mobile): list the desktop's configured servers and their last workspace rows for the phone The phone needs the same host list the desktop shows, including servers that are offline or too old to relay to. mobileRelay.hosts.list/worktrees answer from the desktop's own status and its own worktree.ps fetches, cached per server and fenced by pairing, never from relayed replies. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * refactor(mobile): list server rows through one desktop fetch, with no side effect on listing The phone now always lists a server's rows through mobileRelay.hosts.worktrees, so the desktop's own fetch is both the live source and the cache, and stamping stays in main. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): fetch a server's full workspace list within a bound, and forget removed servers The desktop now asks for the phone's full-catalog limit, answers within 5s with the last rows when a server hangs, drops a server's rows when it is removed or re-paired, and checks the server's identity after the fetch so a reply from before a re-pair is never served. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * feat(mobile): name a server workspace's host on the phone's calls A phone reaches a server's workspace through its desktop only if each call names the host; the direct, cloud-relay and shell-bridge transports now carry `executionHost`, streams keep it for replay and unsubscribe, and an older shell (no `executionHost` grant) refuses rather than strips it. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): list managed servers as the desktop sidebar does, and hide rows mid-delete A server Orca manages on an SSH host now lists once under that host's name, or not at all while the host is reached over the SSH relay, using the sidebar's own folding. Rows the server is deleting are left out at reply time, as a phone asking the server itself would see. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): keep a disconnected server's last rows, and never join a re-paired server's fetch Retirement also fires on a manual disconnect or managed-server update, which wrongly emptied the phone's list; rows are now pruned only once a server is no longer configured. Fetches are shared per server identity, so a poll after a re-pair asks the new server right away. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * feat(mobile): list the paired desktop's server workspaces on the phone Fixes the phone missing a desktop's `orca serve` workspaces: rows come from the desktop's mobileRelay.hosts.* with each server's health ("Update needed", "Disconnected"), open through the relay, follow the desktop's hidden hosts, and the desktop now advertises the relay capability. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): pin that a desktop failing the hosts list leaves only its own rows The page asks a relaying desktop for its servers; a refusal such as method_not_found must leave today's list with no server rows and no catalog error. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): mirror the desktop's shown hosts exactly, one host included Jinwoo's call: the phone follows visibleWorkspaceHostIds as the desktop sidebar does, so a one-host list narrows the phone too instead of being read as a single-host focus. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): scope a server workspace at its route, never the desktop Every screen under a server workspace's route now reads one context and gets that server's client and a router that keeps pushes on it, refusing to fall back to the desktop; pins write to the row's owner like the Mac. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): never open an unreachable server workspace, and prove the relay in e2e A server workspace the phone can't reach now gets one explicit screen at its route and no row tap or action-sheet entry opens it; native routes read the route's host, a census pins every workspace route, and the e2e reads files and git on the server. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * feat(mobile): gate a server workspace on that server's own features A targeted status.get now relays to the server, so a server workspace's route provides that server's status to every screen under it and its action sheet, while whether the phone can reach the server stays the desktop's answer. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(ci): run the remote-runtime WebSocket opener test on Node Under the bun vitest project, `ws` resolves to Bun's built-in client, which ignores maxPayload, so the frame-cap test hung. The opener only ships on Node/Electron, so run its test in the Node runtime project like its siblings. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): run a server workspace's SSH calls on that server A phone in a server workspace asks for SSH state before a file mutation, but the Mac answered about its own targets, so server-owned SSH repos failed. The desktop asks the server, so the ssh.* family now routes by target; an e2e pins files, uploads, commits and review on the server. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): keep the server files/git e2e offline and baseline-git safe Review eligibility reached real gh/glab over the network and proved nothing beyond repoSlug; setup git now ignores the runner's config and avoids `init -b` (Git 2.25). The relay integration test is the routing oracle, so the census re-listing goes. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * feat(mobile): sleep, wake and new workspaces on a desktop's servers from the phone The desktop renderer alone sleeps a server's workspace and holds its slept agents, so the phone now asks the desktop (two paired-desktop methods) instead of the headless server; the new-workspace sheet lists and creates on reachable servers the way the desktop's composer does. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): sleep and wake only a server the desktop has configured A phone's sleep or wake names a server by host id; the desktop now refuses one that is not in its own configured servers, read from its settings rather than any fetched workspace list. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): keep server workspaces' repos, picks and sleeps on their own host A hung server no longer holds back the desktop's repos, a repo id shared by two hosts no longer swaps the pick, and a server row's optimistic sleep waits for the server's own list to confirm it. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): give the server-workspaces test rows every Worktree field The tests-typecheck ratchet checks mobile test files; the fixture row was missing required Worktree members, so the file no longer typechecked. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): give the file preview the workspace router, back to 128 page assets The preview was the one workspace screen on the plain router, so the scoped router was reached by a route set of its own and esbuild split it into an extra chunk (129 assets); every workspace screen now uses it and it shares the route's chunk. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * revert(mobile): drop server sleep and wake from the phone for now The workspace-layout refactor deletes the notifier path these used; sleep and wake on a server come back later as server-side layout.sleep / layout.wake. A server row's Sleep behaves exactly as on feat/mobile-remote-workspaces. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * refactor(mobile-relay): derive the phone allowlist from the routing table Adding a phone method now takes one edit (its route). A server-targeted call on a legacy non-E2EE socket is refused instead of running locally, and only execution-host methods enter the relay. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * refactor(mobile-relay): drop unread freshness fields from server workspace lists The phone reads only the rows, so stale, fetchedAt, totalCount and truncated were dead wire weight. The desktop still serves a server's last rows while the server is unreachable. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * refactor(mobile): read only the rows of a server's workspace list The desktop no longer sends a freshness flag, so the phone's schema drops it. The relay capability list names the delegated-devices constant instead of repeating its literal, which also retires a now-circular type check. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): open server-workspace diffs and file taps on the phone's own screens A headless orca serve has no renderer, so files.openDiff / files.open answer renderer_unavailable and the Changes tap showed that raw code. On that refusal the phone now opens its review screen (git.diff) or file preview (files.read) instead. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): show a server workspace as disconnected while its server is down The phone read the desktop's connection state for a server workspace, so a stopped server kept a green dot and "Loading tabs" spun forever. The workspace route now narrows that state by the server's health from mobileRelay.hosts.list, reusing the existing disconnected session UI and refetching tabs when the server returns. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): name run targets and the desktop's own row as the host list does The Run on picker showed a server's raw environment id behind "Remote ·" and always a monitor glyph; it now takes the host list's badge labels and icon, as the desktop's picker does. Server rows also made the desktop row read "This computer" because its platform was only read for a mixed desktop catalog. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): drop Open in Session and preview new notes in server workspaces A server workspace's host has no editor tabs, so the review screen no longer offers Open in Session there, and a new markdown note whose files.open answers renderer_unavailable opens in the file preview instead of a raw error toast. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * refactor(mobile): share one desktop server-list feed between the host list and workspaces A server workspace polled mobileRelay.hosts.list beside the host list's own read, so the phone could ask the desktop twice per tick and the row badge and the screen could disagree. One feed per desktop client now owns that read; both readers join it. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(e2e): read a server's reachability from the hosts list, not a dropped stale flag |
||
|
|
75513668fb |
fix(mobile): check a server workspace's SSH state on that server (#26519)
* test(e2e): add a paired phone client helper and phone-mirror baseline specs Later phone-mirror slices need one oracle that drives a real mobile-scoped device with the phone's own capabilities against a serve host or a paired desktop. Pins #26022 across a serve restart and today's gap: a desktop's phone does not list the desktop's server workspaces. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * feat(mobile): show SSH host health on the workspace row host badge Rows on an SSH host the desktop cannot reach looked healthy on the phone. The badge now adds the desktop sidebar's own word ("devbox · Disconnected"), derived by the same shared function from the connection status ssh.listTargetSummaries already sends; old desktops keep today's badge. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(e2e): compose the phone-mirror topology and check the cold snapshot Slices need one composer for host, desktop and phone with a single teardown, so specs stop copying setup. The serve-restart spec now reads the first tab-stream snapshot after the cold start, which is what the phone builds its tab strip from. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * feat(mobile): name an unhealthy SSH host even in a single-host list The desktop card shows SSH status on any host count, so the phone now reads target summaries whenever a repo is on SSH and badges an unhealthy host alone too. Labels and health share one target-to-host keying, and a hook-level test pins the reply-to-row wiring. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(e2e): keep phone-mirror teardown errors from masking test failures Teardown failures become a test attachment instead of replacing the real failure, and the cold-snapshot poll reports stream errors together with the PTY list. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): clear stale SSH host badges and announce them without the dot A catalog that no longer spans hosts or holds an SSH repo now clears the health and label maps, so a host that left cannot keep badging rows. The badge gets a screen-reader label ("devbox, Disconnected") instead of reading the middle dot. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * feat(runtime): let a paired desktop register its phones as delegated mobile devices A desktop paired to this runtime can now call pairing.delegatedMobileDevice.sync to make each of its phones an ordinary mobile-scope device it owns, so the phone mirrors that host through the desktop. Children are never pending, inherit the parent's reach, and die with the parent's grant. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * refactor(runtime): move the mobile device revoke primitive below dispatch Dispatch binds the caller-bound delegated-device context, so the revoke path it needs now lives in a layer under it as a pure move; this drops the abstract method seam. No behaviour change. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(runtime): keep a runtime grant until its delegated phones are revoked A failed child cleanup now leaves the parent in place so a retry can still find the children, and revokeRuntimeAccess lives beside the revoke it cascades to. Adds restart, rename and bounds coverage, shares credential minting, and drops the timing-dependent census. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(runtime): fail a delegated sync whose dropped phone cannot be revoked Matches the parent revoke: a child whose cleanup could not be saved is kept and the sync errors before minting anything, so the desktop retries instead of believing the server matches its list. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * feat(mobile): relay a paired phone's server-workspace requests through the desktop A phone mirrors its desktop, including workspaces on the desktop's configured servers. The desktop now forwards a request that names `executionHost: runtime:<env>` to that server over one socket per (phone, server), signed in as the phone's delegated device; replies pass through untouched except stream ids. Hidden until the phone client ships: the relay capability is not advertised yet. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): type the single-host health fixtures inline so the test tsc passes Fixtures hoisted into consts widened their health maps to string, which the now-required hostHealthById rejects under the tests-typecheck ratchet; passing them inline keeps the union. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * docs(mobile): state the desktop relay contract in its entry module main dropped docs/reference, so the relay's contract lives beside the code. Also pins that a server whose delegated sync fails (e.g. delegated_device_revoke_failed) is unavailable until a later request syncs it again. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): pin the relay's configured-server adapter The production hosts adapter was only typechecked; this pins that it resolves configured servers by exact id only (never a name or a removed server), fences by pairing revision and runtime identity, and stops reporting retirements once unsubscribed. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): re-record the repo-metadata goldens for SSH host health The settled metadata state now carries hostHealthById, and an SSH-only catalog now reads the target summaries so a lone unhealthy host can be badged. Scoped re-record via rpc-recording.mts. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * refactor(mobile): tighten the desktop relay after elegance review Reuses the shared E2EE socket opener, folds relay state into fewer fields, never sends a request whose link ended while opening, passes failure codes through, relays only this desktop's own phones, and drops the unused host-state reader until S2 needs one. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): bound relay frames and stop sticky relay verdicts Restores the 8 MiB frame cap on every remote runtime socket, re-checks an update-needed server on each new link so an in-place upgrade relays, answers a phone the server just declined without syncing again, and ends a link whose mirrored capability update cannot be queued. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): run targeted desktop-owned calls on the desktop The phone tags every call on a workspace screen with its execution host, so a paired-desktop method carrying `runtime:<env>` now runs locally instead of failing 'forbidden'. The Mac stays the only reader of the routing table. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * feat(mobile): list the desktop's configured servers and their last workspace rows for the phone The phone needs the same host list the desktop shows, including servers that are offline or too old to relay to. mobileRelay.hosts.list/worktrees answer from the desktop's own status and its own worktree.ps fetches, cached per server and fenced by pairing, never from relayed replies. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * refactor(mobile): list server rows through one desktop fetch, with no side effect on listing The phone now always lists a server's rows through mobileRelay.hosts.worktrees, so the desktop's own fetch is both the live source and the cache, and stamping stays in main. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): fetch a server's full workspace list within a bound, and forget removed servers The desktop now asks for the phone's full-catalog limit, answers within 5s with the last rows when a server hangs, drops a server's rows when it is removed or re-paired, and checks the server's identity after the fetch so a reply from before a re-pair is never served. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * feat(mobile): name a server workspace's host on the phone's calls A phone reaches a server's workspace through its desktop only if each call names the host; the direct, cloud-relay and shell-bridge transports now carry `executionHost`, streams keep it for replay and unsubscribe, and an older shell (no `executionHost` grant) refuses rather than strips it. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): list managed servers as the desktop sidebar does, and hide rows mid-delete A server Orca manages on an SSH host now lists once under that host's name, or not at all while the host is reached over the SSH relay, using the sidebar's own folding. Rows the server is deleting are left out at reply time, as a phone asking the server itself would see. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): keep a disconnected server's last rows, and never join a re-paired server's fetch Retirement also fires on a manual disconnect or managed-server update, which wrongly emptied the phone's list; rows are now pruned only once a server is no longer configured. Fetches are shared per server identity, so a poll after a re-pair asks the new server right away. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * feat(mobile): list the paired desktop's server workspaces on the phone Fixes the phone missing a desktop's `orca serve` workspaces: rows come from the desktop's mobileRelay.hosts.* with each server's health ("Update needed", "Disconnected"), open through the relay, follow the desktop's hidden hosts, and the desktop now advertises the relay capability. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): pin that a desktop failing the hosts list leaves only its own rows The page asks a relaying desktop for its servers; a refusal such as method_not_found must leave today's list with no server rows and no catalog error. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): mirror the desktop's shown hosts exactly, one host included Jinwoo's call: the phone follows visibleWorkspaceHostIds as the desktop sidebar does, so a one-host list narrows the phone too instead of being read as a single-host focus. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): scope a server workspace at its route, never the desktop Every screen under a server workspace's route now reads one context and gets that server's client and a router that keeps pushes on it, refusing to fall back to the desktop; pins write to the row's owner like the Mac. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): never open an unreachable server workspace, and prove the relay in e2e A server workspace the phone can't reach now gets one explicit screen at its route and no row tap or action-sheet entry opens it; native routes read the route's host, a census pins every workspace route, and the e2e reads files and git on the server. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * feat(mobile): gate a server workspace on that server's own features A targeted status.get now relays to the server, so a server workspace's route provides that server's status to every screen under it and its action sheet, while whether the phone can reach the server stays the desktop's answer. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(ci): run the remote-runtime WebSocket opener test on Node Under the bun vitest project, `ws` resolves to Bun's built-in client, which ignores maxPayload, so the frame-cap test hung. The opener only ships on Node/Electron, so run its test in the Node runtime project like its siblings. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): run a server workspace's SSH calls on that server A phone in a server workspace asks for SSH state before a file mutation, but the Mac answered about its own targets, so server-owned SSH repos failed. The desktop asks the server, so the ssh.* family now routes by target; an e2e pins files, uploads, commits and review on the server. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): keep the server files/git e2e offline and baseline-git safe Review eligibility reached real gh/glab over the network and proved nothing beyond repoSlug; setup git now ignores the runner's config and avoids `init -b` (Git 2.25). The relay integration test is the routing oracle, so the census re-listing goes. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): give the file preview the workspace router, back to 128 page assets The preview was the one workspace screen on the plain router, so the scoped router was reached by a route set of its own and esbuild split it into an extra chunk (129 assets); every workspace screen now uses it and it shares the route's chunk. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * refactor(mobile-relay): derive the phone allowlist from the routing table Adding a phone method now takes one edit (its route). A server-targeted call on a legacy non-E2EE socket is refused instead of running locally, and only execution-host methods enter the relay. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * refactor(mobile-relay): drop unread freshness fields from server workspace lists The phone reads only the rows, so stale, fetchedAt, totalCount and truncated were dead wire weight. The desktop still serves a server's last rows while the server is unreachable. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * refactor(mobile): read only the rows of a server's workspace list The desktop no longer sends a freshness flag, so the phone's schema drops it. The relay capability list names the delegated-devices constant instead of repeating its literal, which also retires a now-circular type check. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): open server-workspace diffs and file taps on the phone's own screens A headless orca serve has no renderer, so files.openDiff / files.open answer renderer_unavailable and the Changes tap showed that raw code. On that refusal the phone now opens its review screen (git.diff) or file preview (files.read) instead. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): show a server workspace as disconnected while its server is down The phone read the desktop's connection state for a server workspace, so a stopped server kept a green dot and "Loading tabs" spun forever. The workspace route now narrows that state by the server's health from mobileRelay.hosts.list, reusing the existing disconnected session UI and refetching tabs when the server returns. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): drop Open in Session and preview new notes in server workspaces A server workspace's host has no editor tabs, so the review screen no longer offers Open in Session there, and a new markdown note whose files.open answers renderer_unavailable opens in the file preview instead of a raw error toast. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * refactor(mobile): share one desktop server-list feed between the host list and workspaces A server workspace polled mobileRelay.hosts.list beside the host list's own read, so the phone could ask the desktop twice per tick and the row badge and the screen could disagree. One feed per desktop client now owns that read; both readers join it. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(e2e): read a server's reachability from the hosts list, not a dropped stale flag |
||
|
|
12ba3bc594 |
fix(mobile): keep the chat where the reader scrolled to (#28413)
* fix(mobile): keep the chat where the reader scrolled to RN Web reports no drag events, so the page never stopped following a live turn. Native lost its place when older history loaded. * fix(mobile): keep chat history the phone already shows A busy turn outran the 40-message window and dropped the row being read. * fix(mobile): keep the page's place when history loads RN Web's list draws rows by index, so rows added at the front replaced the ones on screen. * fix(mobile): hold the page's place by a row on screen Total height also grows when live output lands below the reader in the same update. --------- Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> |
||
|
|
3d8907178e |
feat(notifications): open a server agent's notification on its server (#26518)
* test(e2e): add a paired phone client helper and phone-mirror baseline specs
Later phone-mirror slices need one oracle that drives a real mobile-scoped device with the
phone's own capabilities against a serve host or a paired desktop. Pins #26022 across a serve
restart and today's gap: a desktop's phone does not list the desktop's server workspaces.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* feat(mobile): show SSH host health on the workspace row host badge
Rows on an SSH host the desktop cannot reach looked healthy on the phone. The badge now
adds the desktop sidebar's own word ("devbox · Disconnected"), derived by the same shared
function from the connection status ssh.listTargetSummaries already sends; old desktops keep today's badge.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* test(e2e): compose the phone-mirror topology and check the cold snapshot
Slices need one composer for host, desktop and phone with a single teardown, so specs stop
copying setup. The serve-restart spec now reads the first tab-stream snapshot after the cold
start, which is what the phone builds its tab strip from.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* feat(mobile): name an unhealthy SSH host even in a single-host list
The desktop card shows SSH status on any host count, so the phone now reads target summaries
whenever a repo is on SSH and badges an unhealthy host alone too. Labels and health share one
target-to-host keying, and a hook-level test pins the reply-to-row wiring.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* test(e2e): keep phone-mirror teardown errors from masking test failures
Teardown failures become a test attachment instead of replacing the real failure, and the
cold-snapshot poll reports stream errors together with the PTY list.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* fix(mobile): clear stale SSH host badges and announce them without the dot
A catalog that no longer spans hosts or holds an SSH repo now clears the health and label maps,
so a host that left cannot keep badging rows. The badge gets a screen-reader label ("devbox,
Disconnected") instead of reading the middle dot.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* feat(runtime): let a paired desktop register its phones as delegated mobile devices
A desktop paired to this runtime can now call pairing.delegatedMobileDevice.sync to make each of
its phones an ordinary mobile-scope device it owns, so the phone mirrors that host through the
desktop. Children are never pending, inherit the parent's reach, and die with the parent's grant.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* refactor(runtime): move the mobile device revoke primitive below dispatch
Dispatch binds the caller-bound delegated-device context, so the revoke path it needs now lives in
a layer under it as a pure move; this drops the abstract method seam. No behaviour change.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* fix(runtime): keep a runtime grant until its delegated phones are revoked
A failed child cleanup now leaves the parent in place so a retry can still find the children, and
revokeRuntimeAccess lives beside the revoke it cascades to. Adds restart, rename and bounds coverage,
shares credential minting, and drops the timing-dependent census.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* fix(runtime): fail a delegated sync whose dropped phone cannot be revoked
Matches the parent revoke: a child whose cleanup could not be saved is kept and the sync errors
before minting anything, so the desktop retries instead of believing the server matches its list.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* feat(mobile): relay a paired phone's server-workspace requests through the desktop
A phone mirrors its desktop, including workspaces on the desktop's configured servers. The desktop
now forwards a request that names `executionHost: runtime:<env>` to that server over one socket per
(phone, server), signed in as the phone's delegated device; replies pass through untouched except
stream ids. Hidden until the phone client ships: the relay capability is not advertised yet.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* test(mobile): type the single-host health fixtures inline so the test tsc passes
Fixtures hoisted into consts widened their health maps to string, which the now-required
hostHealthById rejects under the tests-typecheck ratchet; passing them inline keeps the union.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* docs(mobile): state the desktop relay contract in its entry module
main dropped docs/reference, so the relay's contract lives beside the code. Also pins that a
server whose delegated sync fails (e.g. delegated_device_revoke_failed) is unavailable until a
later request syncs it again.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* test(mobile): pin the relay's configured-server adapter
The production hosts adapter was only typechecked; this pins that it resolves configured servers
by exact id only (never a name or a removed server), fences by pairing revision and runtime
identity, and stops reporting retirements once unsubscribed.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* test(mobile): re-record the repo-metadata goldens for SSH host health
The settled metadata state now carries hostHealthById, and an SSH-only catalog now reads the
target summaries so a lone unhealthy host can be badged. Scoped re-record via rpc-recording.mts.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* refactor(mobile): tighten the desktop relay after elegance review
Reuses the shared E2EE socket opener, folds relay state into fewer fields, never sends a request
whose link ended while opening, passes failure codes through, relays only this desktop's own
phones, and drops the unused host-state reader until S2 needs one.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* fix(mobile): bound relay frames and stop sticky relay verdicts
Restores the 8 MiB frame cap on every remote runtime socket, re-checks an update-needed server on
each new link so an in-place upgrade relays, answers a phone the server just declined without
syncing again, and ends a link whose mirrored capability update cannot be queued.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* fix(mobile): run targeted desktop-owned calls on the desktop
The phone tags every call on a workspace screen with its execution host, so a
paired-desktop method carrying `runtime:<env>` now runs locally instead of
failing 'forbidden'. The Mac stays the only reader of the routing table.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* feat(mobile): list the desktop's configured servers and their last workspace rows for the phone
The phone needs the same host list the desktop shows, including servers that are offline or
too old to relay to. mobileRelay.hosts.list/worktrees answer from the desktop's own status and
its own worktree.ps fetches, cached per server and fenced by pairing, never from relayed replies.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* refactor(mobile): list server rows through one desktop fetch, with no side effect on listing
The phone now always lists a server's rows through mobileRelay.hosts.worktrees, so the
desktop's own fetch is both the live source and the cache, and stamping stays in main.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* fix(mobile): fetch a server's full workspace list within a bound, and forget removed servers
The desktop now asks for the phone's full-catalog limit, answers within 5s with the last rows
when a server hangs, drops a server's rows when it is removed or re-paired, and checks the
server's identity after the fetch so a reply from before a re-pair is never served.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* feat(mobile): name a server workspace's host on the phone's calls
A phone reaches a server's workspace through its desktop only if each call names the host; the
direct, cloud-relay and shell-bridge transports now carry `executionHost`, streams keep it for
replay and unsubscribe, and an older shell (no `executionHost` grant) refuses rather than strips it.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* fix(mobile): list managed servers as the desktop sidebar does, and hide rows mid-delete
A server Orca manages on an SSH host now lists once under that host's name, or not at all while
the host is reached over the SSH relay, using the sidebar's own folding. Rows the server is
deleting are left out at reply time, as a phone asking the server itself would see.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* fix(mobile): keep a disconnected server's last rows, and never join a re-paired server's fetch
Retirement also fires on a manual disconnect or managed-server update, which wrongly emptied
the phone's list; rows are now pruned only once a server is no longer configured. Fetches are
shared per server identity, so a poll after a re-pair asks the new server right away.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* feat(mobile): list the paired desktop's server workspaces on the phone
Fixes the phone missing a desktop's `orca serve` workspaces: rows come from the desktop's
mobileRelay.hosts.* with each server's health ("Update needed", "Disconnected"), open through the
relay, follow the desktop's hidden hosts, and the desktop now advertises the relay capability.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* test(mobile): pin that a desktop failing the hosts list leaves only its own rows
The page asks a relaying desktop for its servers; a refusal such as method_not_found must leave
today's list with no server rows and no catalog error.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* fix(mobile): mirror the desktop's shown hosts exactly, one host included
Jinwoo's call: the phone follows visibleWorkspaceHostIds as the desktop sidebar does, so a
one-host list narrows the phone too instead of being read as a single-host focus.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* fix(mobile): scope a server workspace at its route, never the desktop
Every screen under a server workspace's route now reads one context and
gets that server's client and a router that keeps pushes on it, refusing
to fall back to the desktop; pins write to the row's owner like the Mac.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* fix(mobile): never open an unreachable server workspace, and prove the relay in e2e
A server workspace the phone can't reach now gets one explicit screen at its route
and no row tap or action-sheet entry opens it; native routes read the route's host,
a census pins every workspace route, and the e2e reads files and git on the server.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* feat(mobile): gate a server workspace on that server's own features
A targeted status.get now relays to the server, so a server workspace's route
provides that server's status to every screen under it and its action sheet,
while whether the phone can reach the server stays the desktop's answer.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* fix(ci): run the remote-runtime WebSocket opener test on Node
Under the bun vitest project, `ws` resolves to Bun's built-in client, which
ignores maxPayload, so the frame-cap test hung. The opener only ships on
Node/Electron, so run its test in the Node runtime project like its siblings.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* feat(push): carry a notification's workspace server to the phone
A desktop relays alerts for agents on its paired servers, but the push named
only the server's worktree, so a tap opened it on the desktop and bounced.
The gateway accepts an optional runtime execution host and forwards it.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* feat(notifications): tell the phone which server runs a notified workspace
A server agent's alert reached the phone with the server's worktree id and no
host, so tapping it opened the desktop's workspace list and bounced. The
producers now name the runtime host, and main carries it into the push.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* feat(mobile): open a notified server workspace on its server
A push that names a runtime host now routes its tap through that server, like
any other server workspace. Absent or non-runtime hosts open as before.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* fix(mobile): keep viewing suppression to the viewed workspace's own host
A desktop workspace and a server workspace can share an id, so viewing one
must not silence the other's alerts. Also pins a runtime-stamped tab owner.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* test(mobile): name the push provider payload builder for its role
The anti-slop gate rejects "shape" as a symbol name; the loop variable builds
an APNs or FCM payload, so it is named for that.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* fix(mobile): give the file preview the workspace router, back to 128 page assets
The preview was the one workspace screen on the plain router, so the scoped router
was reached by a route set of its own and esbuild split it into an extra chunk
(129 assets); every workspace screen now uses it and it shares the route's chunk.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* refactor(mobile-relay): derive the phone allowlist from the routing table
Adding a phone method now takes one edit (its route). A server-targeted call on a
legacy non-E2EE socket is refused instead of running locally, and only
execution-host methods enter the relay.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* refactor(mobile-relay): drop unread freshness fields from server workspace lists
The phone reads only the rows, so stale, fetchedAt, totalCount and truncated
were dead wire weight. The desktop still serves a server's last rows while
the server is unreachable.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* refactor(mobile): read only the rows of a server's workspace list
The desktop no longer sends a freshness flag, so the phone's schema drops it.
The relay capability list names the delegated-devices constant instead of
repeating its literal, which also retires a now-circular type check.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* fix(mobile): show a server workspace as disconnected while its server is down
The phone read the desktop's connection state for a server workspace, so a stopped
server kept a green dot and "Loading tabs" spun forever. The workspace route now
narrows that state by the server's health from mobileRelay.hosts.list, reusing the
existing disconnected session UI and refetching tabs when the server returns.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* refactor(mobile): share one desktop server-list feed between the host list and workspaces
A server workspace polled mobileRelay.hosts.list beside the host list's own read, so
the phone could ask the desktop twice per tick and the row badge and the screen could
disagree. One feed per desktop client now owns that read; both readers join it.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* test(e2e): read a server's reachability from the hosts list, not a dropped stale flag
|
||
|
|
3eaba4e01b |
feat(mobile): create workspaces on the desktop's servers from the phone (#26533)
* test(e2e): add a paired phone client helper and phone-mirror baseline specs
Later phone-mirror slices need one oracle that drives a real mobile-scoped device with the
phone's own capabilities against a serve host or a paired desktop. Pins #26022 across a serve
restart and today's gap: a desktop's phone does not list the desktop's server workspaces.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* feat(mobile): show SSH host health on the workspace row host badge
Rows on an SSH host the desktop cannot reach looked healthy on the phone. The badge now
adds the desktop sidebar's own word ("devbox · Disconnected"), derived by the same shared
function from the connection status ssh.listTargetSummaries already sends; old desktops keep today's badge.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* test(e2e): compose the phone-mirror topology and check the cold snapshot
Slices need one composer for host, desktop and phone with a single teardown, so specs stop
copying setup. The serve-restart spec now reads the first tab-stream snapshot after the cold
start, which is what the phone builds its tab strip from.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* feat(mobile): name an unhealthy SSH host even in a single-host list
The desktop card shows SSH status on any host count, so the phone now reads target summaries
whenever a repo is on SSH and badges an unhealthy host alone too. Labels and health share one
target-to-host keying, and a hook-level test pins the reply-to-row wiring.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* test(e2e): keep phone-mirror teardown errors from masking test failures
Teardown failures become a test attachment instead of replacing the real failure, and the
cold-snapshot poll reports stream errors together with the PTY list.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* fix(mobile): clear stale SSH host badges and announce them without the dot
A catalog that no longer spans hosts or holds an SSH repo now clears the health and label maps,
so a host that left cannot keep badging rows. The badge gets a screen-reader label ("devbox,
Disconnected") instead of reading the middle dot.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* feat(runtime): let a paired desktop register its phones as delegated mobile devices
A desktop paired to this runtime can now call pairing.delegatedMobileDevice.sync to make each of
its phones an ordinary mobile-scope device it owns, so the phone mirrors that host through the
desktop. Children are never pending, inherit the parent's reach, and die with the parent's grant.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* refactor(runtime): move the mobile device revoke primitive below dispatch
Dispatch binds the caller-bound delegated-device context, so the revoke path it needs now lives in
a layer under it as a pure move; this drops the abstract method seam. No behaviour change.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* fix(runtime): keep a runtime grant until its delegated phones are revoked
A failed child cleanup now leaves the parent in place so a retry can still find the children, and
revokeRuntimeAccess lives beside the revoke it cascades to. Adds restart, rename and bounds coverage,
shares credential minting, and drops the timing-dependent census.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* fix(runtime): fail a delegated sync whose dropped phone cannot be revoked
Matches the parent revoke: a child whose cleanup could not be saved is kept and the sync errors
before minting anything, so the desktop retries instead of believing the server matches its list.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* feat(mobile): relay a paired phone's server-workspace requests through the desktop
A phone mirrors its desktop, including workspaces on the desktop's configured servers. The desktop
now forwards a request that names `executionHost: runtime:<env>` to that server over one socket per
(phone, server), signed in as the phone's delegated device; replies pass through untouched except
stream ids. Hidden until the phone client ships: the relay capability is not advertised yet.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* test(mobile): type the single-host health fixtures inline so the test tsc passes
Fixtures hoisted into consts widened their health maps to string, which the now-required
hostHealthById rejects under the tests-typecheck ratchet; passing them inline keeps the union.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* docs(mobile): state the desktop relay contract in its entry module
main dropped docs/reference, so the relay's contract lives beside the code. Also pins that a
server whose delegated sync fails (e.g. delegated_device_revoke_failed) is unavailable until a
later request syncs it again.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* test(mobile): pin the relay's configured-server adapter
The production hosts adapter was only typechecked; this pins that it resolves configured servers
by exact id only (never a name or a removed server), fences by pairing revision and runtime
identity, and stops reporting retirements once unsubscribed.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* test(mobile): re-record the repo-metadata goldens for SSH host health
The settled metadata state now carries hostHealthById, and an SSH-only catalog now reads the
target summaries so a lone unhealthy host can be badged. Scoped re-record via rpc-recording.mts.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* refactor(mobile): tighten the desktop relay after elegance review
Reuses the shared E2EE socket opener, folds relay state into fewer fields, never sends a request
whose link ended while opening, passes failure codes through, relays only this desktop's own
phones, and drops the unused host-state reader until S2 needs one.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* fix(mobile): bound relay frames and stop sticky relay verdicts
Restores the 8 MiB frame cap on every remote runtime socket, re-checks an update-needed server on
each new link so an in-place upgrade relays, answers a phone the server just declined without
syncing again, and ends a link whose mirrored capability update cannot be queued.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* fix(mobile): run targeted desktop-owned calls on the desktop
The phone tags every call on a workspace screen with its execution host, so a
paired-desktop method carrying `runtime:<env>` now runs locally instead of
failing 'forbidden'. The Mac stays the only reader of the routing table.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* feat(mobile): list the desktop's configured servers and their last workspace rows for the phone
The phone needs the same host list the desktop shows, including servers that are offline or
too old to relay to. mobileRelay.hosts.list/worktrees answer from the desktop's own status and
its own worktree.ps fetches, cached per server and fenced by pairing, never from relayed replies.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* refactor(mobile): list server rows through one desktop fetch, with no side effect on listing
The phone now always lists a server's rows through mobileRelay.hosts.worktrees, so the
desktop's own fetch is both the live source and the cache, and stamping stays in main.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* fix(mobile): fetch a server's full workspace list within a bound, and forget removed servers
The desktop now asks for the phone's full-catalog limit, answers within 5s with the last rows
when a server hangs, drops a server's rows when it is removed or re-paired, and checks the
server's identity after the fetch so a reply from before a re-pair is never served.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* feat(mobile): name a server workspace's host on the phone's calls
A phone reaches a server's workspace through its desktop only if each call names the host; the
direct, cloud-relay and shell-bridge transports now carry `executionHost`, streams keep it for
replay and unsubscribe, and an older shell (no `executionHost` grant) refuses rather than strips it.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* fix(mobile): list managed servers as the desktop sidebar does, and hide rows mid-delete
A server Orca manages on an SSH host now lists once under that host's name, or not at all while
the host is reached over the SSH relay, using the sidebar's own folding. Rows the server is
deleting are left out at reply time, as a phone asking the server itself would see.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* fix(mobile): keep a disconnected server's last rows, and never join a re-paired server's fetch
Retirement also fires on a manual disconnect or managed-server update, which wrongly emptied
the phone's list; rows are now pruned only once a server is no longer configured. Fetches are
shared per server identity, so a poll after a re-pair asks the new server right away.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* feat(mobile): list the paired desktop's server workspaces on the phone
Fixes the phone missing a desktop's `orca serve` workspaces: rows come from the desktop's
mobileRelay.hosts.* with each server's health ("Update needed", "Disconnected"), open through the
relay, follow the desktop's hidden hosts, and the desktop now advertises the relay capability.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* test(mobile): pin that a desktop failing the hosts list leaves only its own rows
The page asks a relaying desktop for its servers; a refusal such as method_not_found must leave
today's list with no server rows and no catalog error.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* fix(mobile): mirror the desktop's shown hosts exactly, one host included
Jinwoo's call: the phone follows visibleWorkspaceHostIds as the desktop sidebar does, so a
one-host list narrows the phone too instead of being read as a single-host focus.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* fix(mobile): scope a server workspace at its route, never the desktop
Every screen under a server workspace's route now reads one context and
gets that server's client and a router that keeps pushes on it, refusing
to fall back to the desktop; pins write to the row's owner like the Mac.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* fix(mobile): never open an unreachable server workspace, and prove the relay in e2e
A server workspace the phone can't reach now gets one explicit screen at its route
and no row tap or action-sheet entry opens it; native routes read the route's host,
a census pins every workspace route, and the e2e reads files and git on the server.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* feat(mobile): gate a server workspace on that server's own features
A targeted status.get now relays to the server, so a server workspace's route
provides that server's status to every screen under it and its action sheet,
while whether the phone can reach the server stays the desktop's answer.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* fix(ci): run the remote-runtime WebSocket opener test on Node
Under the bun vitest project, `ws` resolves to Bun's built-in client, which
ignores maxPayload, so the frame-cap test hung. The opener only ships on
Node/Electron, so run its test in the Node runtime project like its siblings.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* feat(mobile): sleep, wake and new workspaces on a desktop's servers from the phone
The desktop renderer alone sleeps a server's workspace and holds its slept agents, so the phone
now asks the desktop (two paired-desktop methods) instead of the headless server; the
new-workspace sheet lists and creates on reachable servers the way the desktop's composer does.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* fix(mobile): sleep and wake only a server the desktop has configured
A phone's sleep or wake names a server by host id; the desktop now refuses one that is not in
its own configured servers, read from its settings rather than any fetched workspace list.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* fix(mobile): keep server workspaces' repos, picks and sleeps on their own host
A hung server no longer holds back the desktop's repos, a repo id shared by two hosts no longer
swaps the pick, and a server row's optimistic sleep waits for the server's own list to confirm it.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* test(mobile): give the server-workspaces test rows every Worktree field
The tests-typecheck ratchet checks mobile test files; the fixture row was missing required
Worktree members, so the file no longer typechecked.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* fix(mobile): give the file preview the workspace router, back to 128 page assets
The preview was the one workspace screen on the plain router, so the scoped router
was reached by a route set of its own and esbuild split it into an extra chunk
(129 assets); every workspace screen now uses it and it shares the route's chunk.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* revert(mobile): drop server sleep and wake from the phone for now
The workspace-layout refactor deletes the notifier path these used; sleep and
wake on a server come back later as server-side layout.sleep / layout.wake.
A server row's Sleep behaves exactly as on feat/mobile-remote-workspaces.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* refactor(mobile-relay): derive the phone allowlist from the routing table
Adding a phone method now takes one edit (its route). A server-targeted call on a
legacy non-E2EE socket is refused instead of running locally, and only
execution-host methods enter the relay.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* refactor(mobile-relay): drop unread freshness fields from server workspace lists
The phone reads only the rows, so stale, fetchedAt, totalCount and truncated
were dead wire weight. The desktop still serves a server's last rows while
the server is unreachable.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* refactor(mobile): read only the rows of a server's workspace list
The desktop no longer sends a freshness flag, so the phone's schema drops it.
The relay capability list names the delegated-devices constant instead of
repeating its literal, which also retires a now-circular type check.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* fix(mobile): show a server workspace as disconnected while its server is down
The phone read the desktop's connection state for a server workspace, so a stopped
server kept a green dot and "Loading tabs" spun forever. The workspace route now
narrows that state by the server's health from mobileRelay.hosts.list, reusing the
existing disconnected session UI and refetching tabs when the server returns.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* fix(mobile): name run targets and the desktop's own row as the host list does
The Run on picker showed a server's raw environment id behind "Remote ·" and
always a monitor glyph; it now takes the host list's badge labels and icon, as
the desktop's picker does. Server rows also made the desktop row read "This
computer" because its platform was only read for a mixed desktop catalog.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* refactor(mobile): share one desktop server-list feed between the host list and workspaces
A server workspace polled mobileRelay.hosts.list beside the host list's own read, so
the phone could ask the desktop twice per tick and the row badge and the screen could
disagree. One feed per desktop client now owns that read; both readers join it.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* test(e2e): read a server's reachability from the hosts list, not a dropped stale flag
|
||
|
|
df88f890c8 |
Avoid repeated mobile recording checks and slow download assertions (#28468)
* Avoid redundant mobile recording comparisons and byte array assertions * Use four Bun workers for the mobile test group * Avoid repeated bridge decoding and costly binary fixture comparisons * Resolve route closure entry paths against their source tree |
||
|
|
04b4e962a7 |
fix: a Claude model that's gone switches to the list's default when you send (#27826)
* fix: start a new Claude chat on the listed default when its remembered model is gone The remembered new-chat model was launched as-is even after Claude stopped offering it, so every new chat failed its first message. The host create and the desktop new-chat picker now derive the same replacement from the host's saved model list: the listed default, effort moved to the nearest level it offers. Explicit models (--model, held picks) run as given; with no saved list the remembered model is kept. * fix: read the host's model list only for a Claude create that needs it The check read host.deps before ruling out other agents and explicit models, so creates whose host carries no deps (and every non-Claude create) paid for a lookup they never use. * fix: picker derives the fallback only where the host would A held model runs as picked on the host, so the picker no longer derives an effort beside it. A cached list whose default marker was dropped for this workspace keeps the placeholder instead of naming its first row. * fix: the new-chat picker picks the replacement for a gone Claude model The host-side check read the saved list separately from the picker, so the two disagreed and a cold start never fired it. The desktop new-chat picker now picks the replacement it shows, once, through the existing pick path, so the first send runs it and the remembered selection heals. The host is back to main. * fix: decide a gone Claude model's replacement at send, from the list in hand The open-time auto-pick raced the chat's create and the list's arrival, so the first send could still launch the gone model. Now, like the common pattern, the picker shows the replacement for a model the host's loaded list lacks, and the send step applies it through the existing pick call, awaited, right before the message goes out. A failed pick never holds the send back. * fix: one verified list decides a gone model's replacement The picker, the send step and the option setter now all read the list the picker shows, less the row a session adds for its own unlisted model, so what is shown is what applies and an explicit pick from that list stays. A listing that is still running, failed, or absent keeps the model. The send step re-derives from the chat's state when it runs, so a second send behind one that already moved the model applies nothing. * fix: only rows a finished host listing names can replace a gone model A single verified flag outlived the catalog it described: live options built from the built-in fallback list, or a malformed or empty host answer, still authorized a replacement the host would refuse. The state now keeps the ids a finished, well-formed host listing names, and a replacement must be both shown in the picker and named there. * fix(native-chat): the send carries the composer's Claude model and effort Every composer send (desktop and phone) carries the model and effort the picker shows, as optional agentSession.send options, only to a host that advertises agent-session.send-options.v1. The host writes a carried value that differs from the chat's own through the existing pick path before accepting the message, so the turn starts on it and the chat records it. A queued card keeps its options and applies them when it goes out; a launch's create call carries the replacement for its first message. Replaces the separate awaited pick before each send. * chore: drop an unrelated lockfile change * refactor(native-chat): one helper names a send's control fields for client and host * ci: rerun against current main * fix(native-chat): a message runs on the model and effort it was sent with The carried selection is now stored with the message (its submission row, and a queued card's row) and applied through the shared option writer as the delivery loop hands the message over, or before the start it needs. A pick made after the message was accepted no longer changes the model it runs on. Direct sends, queue drains and Send now share that one apply point; the early apply before acceptance and the queue-only apply are gone. The picker and the send read one effective selection: the replacement for a model the host's finished listing lacks shows only for a host that takes a send's selection, the phone validates picks against what it shows, a send carries the default the composer shows, a resumed launch's first message gets the same resolution, and only the listing proves a row (no label heuristic). * docs(native-chat): a submission's options are the host's alone * fix(native-chat): an opening message keeps its model; a pending pick resolves like the picker The opening message a create sends now stores the model and effort the chat was created with, so a pick made before its handover no longer changes what it runs on, and a card kept after a restart keeps them. A pick on its way to the host resolves through the same option view the picker shows before a send carries it, so a gone model it names is replaced too; the send selection reads only the shown snapshot. The option writer builds its own record-and-publish context, so the message path no longer duplicates it, and the client and host share one function for a send's fingerprint fields. * ci: rerun against current main |
||
|
|
5f134d8f01 |
feat(mobile): show and use the desktop's server workspaces on the phone (#26501)
* test(e2e): add a paired phone client helper and phone-mirror baseline specs
Later phone-mirror slices need one oracle that drives a real mobile-scoped device with the
phone's own capabilities against a serve host or a paired desktop. Pins #26022 across a serve
restart and today's gap: a desktop's phone does not list the desktop's server workspaces.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* feat(mobile): show SSH host health on the workspace row host badge
Rows on an SSH host the desktop cannot reach looked healthy on the phone. The badge now
adds the desktop sidebar's own word ("devbox · Disconnected"), derived by the same shared
function from the connection status ssh.listTargetSummaries already sends; old desktops keep today's badge.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* test(e2e): compose the phone-mirror topology and check the cold snapshot
Slices need one composer for host, desktop and phone with a single teardown, so specs stop
copying setup. The serve-restart spec now reads the first tab-stream snapshot after the cold
start, which is what the phone builds its tab strip from.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* feat(mobile): name an unhealthy SSH host even in a single-host list
The desktop card shows SSH status on any host count, so the phone now reads target summaries
whenever a repo is on SSH and badges an unhealthy host alone too. Labels and health share one
target-to-host keying, and a hook-level test pins the reply-to-row wiring.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* test(e2e): keep phone-mirror teardown errors from masking test failures
Teardown failures become a test attachment instead of replacing the real failure, and the
cold-snapshot poll reports stream errors together with the PTY list.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* fix(mobile): clear stale SSH host badges and announce them without the dot
A catalog that no longer spans hosts or holds an SSH repo now clears the health and label maps,
so a host that left cannot keep badging rows. The badge gets a screen-reader label ("devbox,
Disconnected") instead of reading the middle dot.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* feat(runtime): let a paired desktop register its phones as delegated mobile devices
A desktop paired to this runtime can now call pairing.delegatedMobileDevice.sync to make each of
its phones an ordinary mobile-scope device it owns, so the phone mirrors that host through the
desktop. Children are never pending, inherit the parent's reach, and die with the parent's grant.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* refactor(runtime): move the mobile device revoke primitive below dispatch
Dispatch binds the caller-bound delegated-device context, so the revoke path it needs now lives in
a layer under it as a pure move; this drops the abstract method seam. No behaviour change.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* fix(runtime): keep a runtime grant until its delegated phones are revoked
A failed child cleanup now leaves the parent in place so a retry can still find the children, and
revokeRuntimeAccess lives beside the revoke it cascades to. Adds restart, rename and bounds coverage,
shares credential minting, and drops the timing-dependent census.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* fix(runtime): fail a delegated sync whose dropped phone cannot be revoked
Matches the parent revoke: a child whose cleanup could not be saved is kept and the sync errors
before minting anything, so the desktop retries instead of believing the server matches its list.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* feat(mobile): relay a paired phone's server-workspace requests through the desktop
A phone mirrors its desktop, including workspaces on the desktop's configured servers. The desktop
now forwards a request that names `executionHost: runtime:<env>` to that server over one socket per
(phone, server), signed in as the phone's delegated device; replies pass through untouched except
stream ids. Hidden until the phone client ships: the relay capability is not advertised yet.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* test(mobile): type the single-host health fixtures inline so the test tsc passes
Fixtures hoisted into consts widened their health maps to string, which the now-required
hostHealthById rejects under the tests-typecheck ratchet; passing them inline keeps the union.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* docs(mobile): state the desktop relay contract in its entry module
main dropped docs/reference, so the relay's contract lives beside the code. Also pins that a
server whose delegated sync fails (e.g. delegated_device_revoke_failed) is unavailable until a
later request syncs it again.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* test(mobile): pin the relay's configured-server adapter
The production hosts adapter was only typechecked; this pins that it resolves configured servers
by exact id only (never a name or a removed server), fences by pairing revision and runtime
identity, and stops reporting retirements once unsubscribed.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* test(mobile): re-record the repo-metadata goldens for SSH host health
The settled metadata state now carries hostHealthById, and an SSH-only catalog now reads the
target summaries so a lone unhealthy host can be badged. Scoped re-record via rpc-recording.mts.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* refactor(mobile): tighten the desktop relay after elegance review
Reuses the shared E2EE socket opener, folds relay state into fewer fields, never sends a request
whose link ended while opening, passes failure codes through, relays only this desktop's own
phones, and drops the unused host-state reader until S2 needs one.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* fix(mobile): bound relay frames and stop sticky relay verdicts
Restores the 8 MiB frame cap on every remote runtime socket, re-checks an update-needed server on
each new link so an in-place upgrade relays, answers a phone the server just declined without
syncing again, and ends a link whose mirrored capability update cannot be queued.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* fix(mobile): run targeted desktop-owned calls on the desktop
The phone tags every call on a workspace screen with its execution host, so a
paired-desktop method carrying `runtime:<env>` now runs locally instead of
failing 'forbidden'. The Mac stays the only reader of the routing table.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* feat(mobile): list the desktop's configured servers and their last workspace rows for the phone
The phone needs the same host list the desktop shows, including servers that are offline or
too old to relay to. mobileRelay.hosts.list/worktrees answer from the desktop's own status and
its own worktree.ps fetches, cached per server and fenced by pairing, never from relayed replies.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* refactor(mobile): list server rows through one desktop fetch, with no side effect on listing
The phone now always lists a server's rows through mobileRelay.hosts.worktrees, so the
desktop's own fetch is both the live source and the cache, and stamping stays in main.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* fix(mobile): fetch a server's full workspace list within a bound, and forget removed servers
The desktop now asks for the phone's full-catalog limit, answers within 5s with the last rows
when a server hangs, drops a server's rows when it is removed or re-paired, and checks the
server's identity after the fetch so a reply from before a re-pair is never served.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* feat(mobile): name a server workspace's host on the phone's calls
A phone reaches a server's workspace through its desktop only if each call names the host; the
direct, cloud-relay and shell-bridge transports now carry `executionHost`, streams keep it for
replay and unsubscribe, and an older shell (no `executionHost` grant) refuses rather than strips it.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* fix(mobile): list managed servers as the desktop sidebar does, and hide rows mid-delete
A server Orca manages on an SSH host now lists once under that host's name, or not at all while
the host is reached over the SSH relay, using the sidebar's own folding. Rows the server is
deleting are left out at reply time, as a phone asking the server itself would see.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* fix(mobile): keep a disconnected server's last rows, and never join a re-paired server's fetch
Retirement also fires on a manual disconnect or managed-server update, which wrongly emptied
the phone's list; rows are now pruned only once a server is no longer configured. Fetches are
shared per server identity, so a poll after a re-pair asks the new server right away.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* feat(mobile): list the paired desktop's server workspaces on the phone
Fixes the phone missing a desktop's `orca serve` workspaces: rows come from the desktop's
mobileRelay.hosts.* with each server's health ("Update needed", "Disconnected"), open through the
relay, follow the desktop's hidden hosts, and the desktop now advertises the relay capability.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* test(mobile): pin that a desktop failing the hosts list leaves only its own rows
The page asks a relaying desktop for its servers; a refusal such as method_not_found must leave
today's list with no server rows and no catalog error.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* fix(mobile): mirror the desktop's shown hosts exactly, one host included
Jinwoo's call: the phone follows visibleWorkspaceHostIds as the desktop sidebar does, so a
one-host list narrows the phone too instead of being read as a single-host focus.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* fix(mobile): scope a server workspace at its route, never the desktop
Every screen under a server workspace's route now reads one context and
gets that server's client and a router that keeps pushes on it, refusing
to fall back to the desktop; pins write to the row's owner like the Mac.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* fix(mobile): never open an unreachable server workspace, and prove the relay in e2e
A server workspace the phone can't reach now gets one explicit screen at its route
and no row tap or action-sheet entry opens it; native routes read the route's host,
a census pins every workspace route, and the e2e reads files and git on the server.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* feat(mobile): gate a server workspace on that server's own features
A targeted status.get now relays to the server, so a server workspace's route
provides that server's status to every screen under it and its action sheet,
while whether the phone can reach the server stays the desktop's answer.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* fix(ci): run the remote-runtime WebSocket opener test on Node
Under the bun vitest project, `ws` resolves to Bun's built-in client, which
ignores maxPayload, so the frame-cap test hung. The opener only ships on
Node/Electron, so run its test in the Node runtime project like its siblings.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* fix(mobile): give the file preview the workspace router, back to 128 page assets
The preview was the one workspace screen on the plain router, so the scoped router
was reached by a route set of its own and esbuild split it into an extra chunk
(129 assets); every workspace screen now uses it and it shares the route's chunk.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* refactor(mobile-relay): derive the phone allowlist from the routing table
Adding a phone method now takes one edit (its route). A server-targeted call on a
legacy non-E2EE socket is refused instead of running locally, and only
execution-host methods enter the relay.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* refactor(mobile-relay): drop unread freshness fields from server workspace lists
The phone reads only the rows, so stale, fetchedAt, totalCount and truncated
were dead wire weight. The desktop still serves a server's last rows while
the server is unreachable.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* refactor(mobile): read only the rows of a server's workspace list
The desktop no longer sends a freshness flag, so the phone's schema drops it.
The relay capability list names the delegated-devices constant instead of
repeating its literal, which also retires a now-circular type check.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* fix(mobile): show a server workspace as disconnected while its server is down
The phone read the desktop's connection state for a server workspace, so a stopped
server kept a green dot and "Loading tabs" spun forever. The workspace route now
narrows that state by the server's health from mobileRelay.hosts.list, reusing the
existing disconnected session UI and refetching tabs when the server returns.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* refactor(mobile): share one desktop server-list feed between the host list and workspaces
A server workspace polled mobileRelay.hosts.list beside the host list's own read, so
the phone could ask the desktop twice per tick and the row badge and the screen could
disagree. One feed per desktop client now owns that read; both readers join it.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* test(e2e): read a server's reachability from the hosts list, not a dropped stale flag
|
||
|
|
13a75d3939 |
[CI] Reduce redundant test work and import existing helpers directly (#28365)
* [CI] Reduce redundant test work and import existing helpers directly * [CI] Preserve provider guards and pin the mobile browser test core * [CI] Fix CI test setup and verify retained contracts * [CI] Compare native media chunks without boxed byte arrays * [CI] Detect shifted native media reads with position-sensitive bytes |
||
|
|
a622bb0f4b |
fix(native-chat): phone Stop reaches a message the agent hasn't answered; unreadable task list reports unconfirmed (#28243)
* fix(native-chat): phone Stop reaches unanswered sends; unreadable task list is unconfirmed The phone named the running turn on every Stop, so a Stop pressed before the agent echoed the message said "Stop not sent" and the agent ran on. Against a host advertising agent-session.conversation-stop.v1 the phone now sends the Stop that names no turn, as the desktop already does; the host stops whatever the chat has in flight. A card's Cancel still names its turn, and older hosts keep the turn-named Stop. A background-task Stop whose host could not read the session's task list read the failure as "no tasks" and answered cancelled:false. It now refuses as outcome unknown, the refusal a timed-out background stop already returns. Adds host tests pinning the Stop-before-echo races on the unnamed Stop: Codex turns admitted or started before the echo, a later send just before the Stop, a retried Stop, and a Stop after a failed one. All pass on main unchanged. * fix(mobile): show Stop before the turn opens; a card's Cancel keeps its turn The phone offered Stop only once a turn existed, so the Stop that names no turn was unreachable while a send waited for the agent's echo. Against a host that advertises conversation stop, a working chat now offers Stop. A card's Cancel chose the untargeted form from the prompt after the prompt-cancel capability filtered it out, so against a host with conversation stop but no prompt cancel it sent a Stop naming nothing. It now always names the card's turn. The race test for an admitted turn that cannot take an interrupt accepts success only once the host has let go of the agent, and otherwise requires the unconfirmed refusal. * ci: rerun against current main * ci: rerun against current main |
||
|
|
c827ed5324 | ci: run full native mobile typechecks without compiler caches (#28409) | ||
|
|
975c27b780 |
fix(usage): cap Cursor's Retry-After at 24h and honor its HTTP-date form (#28406)
Cursor parsed Retry-After with a bare Number(), so a huge header could block usage refreshes for years and an HTTP-date was ignored. Claude and Cursor now both call parseUsageRetryAfterMs, which applies the 24h usage cap to the shared Retry-After parser, replacing Claude's private copy of the same algorithm. The shared parser is renamed from relay-retry-after-header/parseRelayRetryAfterMs to retry-after-header/parseRetryAfterHeaderMs: relay, the updater, mobile and usage all use it. |
||
|
|
3a011e48d1 |
fix(native-chat): load long chats on the phone by keeping every update within its limit (#28203)
* fix(native-chat): fit chat stream pages in one mobile bridge frame A long native chat failed to open on a phone with "the shell ended this stream (overflow)". The host sized each history page to ~1.94 MiB (half the relay cap), but the mobile app ends any stream whose event frame is over 640 KiB, so the opening snapshot of a long chat never arrived. Pages now fit that frame; older rows load through Load earlier as before. The page also words a stream the app ended in plain language instead of the protocol's reason. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * refactor(native-chat): keep the outline on the relay page share; plainer end words Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): read the bridge frame cap from its shared definition Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * feat(native-chat): bounded frames for the phone's chat stream A phone page now asks the host (when it advertises agent-session.bounded-frames.v1) to keep every agentSession.subscribe frame within the bridge's 640 KiB frame: - each row is cut to what the phone draws (tool output to its detail length unless it draws as a diff, tool inputs to the diff view's length) and then to one row's share of a page, its long strings cut to a common length; the row is marked `truncated` with its full size - history frames carry rows and metadata held to a fixed ceiling; the draft queue and the background-task roster go in frames of their own; the command list is not sent, since the phone reads it from options - the queue's admission bound counts whole cards and is sized to one frame, so a phone always gets every card whole Other subscribers get the frames they always did. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * refactor(native-chat): bounded frames review round 1 - the phone waits for the host's capabilities before opening the stream, so its opening frame is never sent unbounded - rows are bounded once, by the reader that sizes the page; the item form is an options field and always a function - one cut rule (`cutLongStrings`) serves the display trims and the fit - a side list rides an empty history batch rather than a second frame; commands changes send a bounded subscriber nothing - queue admission measures the candidate list once; a legacy queue of more cards than fit lists the ones it sends first Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * refactor(native-chat): bounded frames review round 2 - an over-budget legacy queue cuts its text to one common length and marks the cards it cut, and no longer names a next card it left out - one predicate says whether a subscriber is owed the `/` list - a lighter invariant fixture Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(native-chat): type the shortening guards against named shapes Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * refactor(native-chat): bound every phone's chat stream at the host The host now bounds frames for every phone (clientKind 'mobile') at the RPC boundary, so the subscribe param, the host capability and the phone's wait for the capability probe are gone. Phones of every version already ignore the stream's slash commands, and a phone reached through the desktop relay signs in as a phone too. - A live gap of more than one page reaches a phone as the newest page, so no publish outruns the bridge's unacked window; older rows load on scroll. - Queue admission sizes each card as wide as a failed send can make it. - Rows are bounded only as a page takes them; the unread row marker is gone and a shortened queue card is marked `truncated: true`. - A bounded subscriber no longer reads or tracks the slash commands. - The stream-end words are back to main's: `overflow` also covers a page that stopped acking, which "too large" would misstate. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(native-chat): count every card in line and never shorten one Queue admission now counts every card the queue can publish again without passing admission: waiting and returned cards, and sent cards whose delivery is not yet accepted, which a refusal or a Stop puts back. The fixed per-card reserve for a failed send's words is gone. When a phone's list is over its frame room anyway (an older build's larger queue, or a card that grew after admission), the phone gets the cards it sends first, whole, and the rest once those leave. Cards are never shortened, so no app version can Edit a partial copy over the full draft, and the `truncated` card flag is gone. Bounded frames strip only the roster from an event; no event carries the other side lists. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * refactor(native-chat): bound only phone frames; keep main's queue and page sizes Queue admission is main's again. The phone's list is bounded where it is sent: the cards it sends first, whole, so every frame fits for any queue size and no card is shortened. History pages keep main's size for every reader but phone subscribe frames. A reader is now its item form plus its page budget, so desktop pages, the outline and phone history replies (chunked by the app's bridge) are as on main, and only bounded frames use the bridge frame's share. Drops the queue's `nextQueuedMessageId` rewrite, which no phone reads, and a test that duplicated the prefix check. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * refactor(native-chat): bound phone rows by size alone; final tidy Rows the journal already caps at a 16 KiB head gain little from a second, display-shaped cut, and the diff check behind it passed a character count as a line count. A row is now held to its frame share by shortening alone, and native-chat-diff is back to main's. Unexports file-local bounds, fixes a capability-era test comment, and adds tests for string shortening and a phone's stop-note refresh. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(native-chat): assert a phone's stop-note refresh pages Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(native-chat): run the bounded-frames suite in the Node runtime project It opens a real SQLite journal. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(native-chat): bound legacy dispatch reasons on read Rows written before the write-side bound carry a provider's full error text, which every page and phone frame attaches with its submission. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb |
||
|
|
fba7b753ec |
Cache mobile typechecks and overlap CI setup (#28192)
* ci: run mobile runtime test groups on separate runners * ci: use a lightweight runner for the mobile result gate * ci: distribute Bun tests across both mobile runners * ci: reuse mobile compiler state without adding runner queues * test: verify eviction of source variants in one cache bucket * test: baseline existing serializer limits in the capacity capture |