mirror of
https://github.com/stablyai/orca.git
synced 2026-09-28 08:02:43 +00:00
b5dec85a4ea91f320df32a92096ff3315cf8e7cd
856
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
b5dec85a4e |
ci: reuse mobile web route analysis and skip unrelated mobile tests (#23329)
* ci: share mobile route analysis and scope mobile test runs * ci: cover mobile web runner process dependencies * test: verify mobile web selectors through the new runner --------- Co-authored-by: m4air <m4air@m4airs-Air.localdomain> |
||
|
|
1882458f44 |
ci: scope orcad smoke and parallelize Linux packages (#23314)
* ci: scope orcad smoke and parallelize Linux package formats * ci: validate packaging when its copy dependency changes --------- Co-authored-by: m4air <m4air@m4airs-Air.localdomain> |
||
|
|
3eb1adec20 |
ci: reuse fixture setup and scope localization extraction (#23291)
Co-authored-by: m4air <m4air@m4airs-Air.localdomain> |
||
|
|
7a4f83336b |
ci: shard SSH Docker coverage and warm Windows native caches (#23286)
Co-authored-by: m4air <m4air@m4airs-Air.localdomain> |
||
|
|
ee6281ff79 |
fix: dispose OpenCode 2 hooks after prompt setup and cleanup failures (#23209)
* fix: dispose OpenCode 2 hooks after prompt setup and cleanup failures * test: pin reviewed OpenCode setup cleanup bytes * fix(i18n): restore diff note draft catalog entries * test: isolate historical hourly build version inputs * test: align historical package input with shared CI repair * test: inject hourly package version without module mocking * test: share the hourly package input contract across CI repairs * test(windows): verify NSIS policy with native PowerShell modules --------- Co-authored-by: m4air <m4air@m4airs-Air.localdomain> Co-authored-by: OrcaWin <alpha-eng@stably.ai> |
||
|
|
4c38cabb4e |
fix(windows): synchronize native terminal table lifetime (#23257)
* fix(i18n): restore diff note draft catalog entries * fix(windows): synchronize native terminal table lifetime * fix(ci): preserve focused Playwright file selection * test(windows): retain PTY stress failure evidence * test: isolate hourly identity inputs from release version changes * test(ci): require focused commands on every platform Assert each golden platform step and its focused arguments before deduplicating Playwright discovery probes. --------- Co-authored-by: m4air <m4air@m4airs-Air.localdomain> Co-authored-by: Neil <neil@stably.ai> |
||
|
|
dffb3498e2 |
fix(ci): preserve focused Playwright file selection (#23270)
* fix(ci): preserve focused Playwright file selection * test: isolate historical hourly build version inputs * test: align historical package input with shared CI repair * test: inject hourly package version without module mocking * test: share the hourly package input contract across CI repairs * test(ci): require focused commands on every platform Assert each golden platform step and its focused arguments before deduplicating Playwright discovery probes. --------- Co-authored-by: m4air <m4air@m4airs-Air.localdomain> Co-authored-by: Neil <neil@stably.ai> |
||
|
|
28d617d0d0 |
fix: close abandoned PDF.js development asset streams (#23054)
* fix: close abandoned PDF.js development asset streams * test: keep casting safety comments attached after formatting * fix(i18n): restore diff note draft catalog entries * fix: guard closed PDF streams and preserve foreign listeners * fix(i18n): make AI the recipient of diff notes * test(browser): report the phase of WebRTC probe timeouts * test: pin the historical hourly package version fixture * test: provide the historical package version through build options --------- Co-authored-by: OrcaWin <alpha-eng@stably.ai> Co-authored-by: m4air <m4air@m4airs-Air.localdomain> |
||
|
|
3ab183d770 |
fix: stop stale runtime event work after renderer cleanup (#23066)
* fix: stop stale runtime event work after renderer cleanup * fix(i18n): restore diff note draft catalog entries * test(browser): report the phase of WebRTC probe timeouts * fix: preserve runtime subscription ownership during nested sync * test: isolate hourly identity inputs from release version changes --------- Co-authored-by: OrcaWin <alpha-eng@stably.ai> Co-authored-by: m4air <m4air@m4airs-Air.localdomain> |
||
|
|
1554f15b6c |
fix: close abandoned static web asset readers (#23062)
* fix: close abandoned static web readers and align AI note labels * test: pin the historical hourly package version fixture * test: provide the historical package version through build options |
||
|
|
4b6fe95943 |
fix(windows): preserve relocated terminals and native process scans (#22872)
* fix(windows): ship the process-table addon to the relocated daemon host The Windows terminal daemon runs from a copy of the app under %LOCALAPPDATA%\Orca\daemon-host\<version>. That copy took node-pty but not @vscode/windows-process-tree, so the daemon's bare require of the addon found nothing and every process-table read (foreground tracking, descendant sweeps) fell back to a powershell.exe Get-CimInstance scan (#16905). - Copy the addon's runtime files (package.json, lib/, the .node binary) into the host; the ~25MB of gyp intermediates beside them are filtered out. - Treat a host missing those files as unmaterialized, so hosts built before this are rebuilt, and skip relocation if the install itself lacks them. - Log the daemon's native/CIM capability at startup and warn once when the process table falls back to CIM. Revives #19525 on current main. * test(windows): locate update-survival loss before relaunch * test(windows): preserve daemon tree before update-survival proof * test(windows): distinguish Electron exit from launcher close timeout * test(windows): verify process exit when inherited pipes delay close * test(windows): trace installer process checks in isolated survival runs * fix(windows): probe process-query capability before installer sweep * fix(windows): match installer probe and process-check profile behavior * fix(windows): use NSIS separators for the process-check include * test(windows): dismiss session-search overlay in survival harness --------- Co-authored-by: m4air <m4air@m4airs-Air.localdomain> |
||
|
|
681f3ca1ba |
perf: stream the packaged browser installer checksum in CI (#23082)
* perf: stream the packaged browser installer checksum in CI * fix(i18n): restore diff note draft catalog entries --------- Co-authored-by: OrcaWin <alpha-eng@stably.ai> Co-authored-by: m4air <m4air@m4airs-Air.localdomain> |
||
|
|
8416e8de10 |
refactor(persistence): retire ordinary JSON profile writes (#23202)
* refactor(persistence): retire ordinary JSON profile writes Require SQLite for writable profiles and keep import, compatibility export, and recovery in a documented legacy-json boundary. * fix(cli): preserve dynamic profile imports in release output * test(persistence): exercise SQL races and verify packaged CLI imports * test(persistence): consolidate shared fixture imports * test(persistence): close SQLite fixtures before cleanup and await launcher output * test(automations): use SQLite fixtures for dispatch fencing and skip coalescing --------- Co-authored-by: m4air <m4air@m4airs-Air.localdomain> |
||
|
|
da6d483ab9 |
fix(vault): read OpenCode SQLite inside WSL and SSH hosts (#23128)
* fix(vault): read OpenCode SQLite on WSL and SSH execution hosts * fix(vault): bound host setup and preserve cancellation across readers * fix(vault): keep WSL discovery visible and isolate probe tests * fix: retry local Vault runtime downloads without reserving remote stages Preserve verified remote cache reuse and latch only unresolved host work. Update WSL source-guard and remote dedup test integration. * fix(queue): discard aborted requests before respawn * fix(vault): preserve host paths and recover setup after reconnect * fix(ssh): fence every runtime platform probe across reconnects * fix(vault): reject setup results from superseded SSH connections --------- Co-authored-by: m4air <m4air@m4airs-Air.localdomain> |
||
|
|
d20cb69c48 |
Optimize CI follow-up workflows (#23190)
Co-authored-by: m4air <m4air@m4airs-Air.localdomain> |
||
|
|
3a081abf71 |
fix(persistence): reclaim Windows profile locks after PID reuse (#23122)
* fix(persistence): identify reused Windows profile-owner processes * fix(persistence): preserve absent-owner recovery without native registry * ci: build Windows registry before native profile identity checks * fix(cli): include native profile-owner dependencies in typecheck * test: register native profile owner test in Windows PR lane * test: use resilient Windows profile-owner cleanup --------- Co-authored-by: m4air <m4air@m4airs-Air.localdomain> |
||
|
|
d17a17684b |
Reduce redundant CI runs, pnpm uploads, and fixture startups (#23145)
* Reduce redundant CI runs, store uploads, and fixture processes * Avoid repeating draft-independent mobile checks on readiness --------- Co-authored-by: m4air <m4air@m4airs-Air.localdomain> |
||
|
|
9b30c7f60a |
ci: verify mobile disposal and balance unit-test costs (#23114)
* ci: verify mobile disposal and reduce unit scheduling costs * docs(ci): clarify timing assignment validation --------- Co-authored-by: m4air <m4air@m4airs-Air.localdomain> |
||
|
|
6fc3cdcad6 |
Bundle Bun for headless Orca and profile persistence (#22635)
Bundle a pinned, verified Bun runtime for headless Orca so existing Node launch commands can hand off before opening a profile. Keep desktop execution on Electron. Add the Bun SQLite adapter and terminal backend, bounded shutdown, process inspection and cross-platform artifact qualification. Keep future managed SSH deployment separate from current production launch paths. |
||
|
|
38bcdf76ac |
perf(ci): reduce queue pressure without paid runners (#23053)
* ci: measure complete unit file costs for shard balancing * perf(ci): reduce repeated PR setup and capture complete shard timings * perf(ci): seed reusable main-branch native and typecheck caches * fix(ci): stop superseded unit workflows from resisting cancellation * perf(ci): reuse bundle fixtures and share the baseline Git build * ci: record hosted gains and refresh main hook parity * ci: retain default workers after performance-budget regression * docs: record hosted mobile timing flake --------- Co-authored-by: m4air <m4air@m4airs-Air.localdomain> |
||
|
|
82412dab8b |
Persist profile state in SQLite with background writes (#22612)
Migrate profile state to SQLite and move writes and backups into a background worker. Acknowledge terminal, SSH and automation changes only after durable saves. Preserve JSON import, recovery, rollback and compatibility exports. Validate migration, worker failures, maintenance, cross-profile moves and terminal lifetime races with unit, integration and end-to-end coverage. |
||
|
|
067975bfd1 |
fix(native-chat): every lease latch has a way to die (#22820)
* fix(native-chat): every lease latch has a way to die A failed exit settlement no longer leaves the lease in recovery: the release writes no stage and keeps the exit in its death evidence, and whatever the dead generation left running is settled from that evidence at the next acquire or read restore. The settlement retry flag, its disposition and every branch that read it are gone. A reservation that recorded no process is released at startup and after a failed start, the never-written conflicted status and the processless proof are deleted, recovery resolution always concludes, and Codex records its child's identity at spawn, before the handshake. * test(native-chat): a re-create needs a release proven by death evidence * test(codex): the child's pid is reported before the handshake * test(native-chat): type the crash and exit fixtures without casts * fix(native-chat): wait out a terminal owner an older build recorded, in recovery rather than manual recovery * test(native-chat): a chat mid-turn at quit reopens idle, and an older build reads an unproven release * test(native-chat): explain the baseline store cast * fix(native-chat): a terminal owner's refusal names the process instead of recursing Opening a chat whose terminal owner an older build recorded threw a stack overflow instead of the refusal that names the process to quit. * fix(native-chat): wait out a terminal owner recovery cannot verify instead of releasing it A terminal agent an older build recorded keeps its PTY across an Orca restart, so a probe that cannot answer (a start-time read that fails on a loaded host) is not evidence its transport is gone. Releasing it let a native child resume the same conversation beside the live terminal agent. Only proof of its exit now ends the claim. * ci(cross-version): run the unproven-release downgrade test The sharded unit job excludes tests/e2e/cross-version-wire, and the cross-version job runs an explicit list that did not name the new test, so it never ran in CI. A change to the record validator now also starts the job. * refactor(native-chat): map the retired manual-recovery stage to recovering at decode Nothing in this build writes manual-recovery, and restart reconciliation already rewrites it. Mapping it where the other retired handoff stages are mapped removes it from the in-memory lease type and deletes the branches that could only see it: the acquisition refusal, the renewer skip, the unproven-release stage check, and the handoff-status 'manual recovery is required' answer. Older builds accept recovering, so a record written back still loads after a downgrade. * docs(native-chat): say what happens to a live child an ownerless reservation leaves The reaper runs once at store open, while the unreconciled lease still claims the child's token, so it does not stop that child on this launch. The comment claimed it did. * test(native-chat): name the each-case label for its role * fix(native-chat): continue a create retried after recovery released its reservation The client retries a create it never heard back from under the same operation id. Recovery had released that create's reservation, so the retry was refused agent_session_ownership_unknown while its row was pending, and agent_session_operation_expired once the row aged out, and the chat never started. A retry whose lease nothing holds now continues as a fresh reservation at the next fence, which also stops the old reservation's spawn from committing. * test(native-chat): name the refusal a replayed create used to get * fix(native-chat): one quit-the-terminal-agent message for a chat a terminal agent holds A chat held by a terminal agent an older build recorded frees only when that agent exits. Sending said to reopen the chat and opening it said two runtimes claimed it; both now say the chat is open in a terminal agent, name its process, and say to quit it. Error codes are unchanged. * ci: run PR checks on the rebased head * fix(native-chat): name a terminal owner's process only when its start time can tell it from a reused pid * test(native-chat): relaunch from the dying host's durable state, so its still-pending attach cannot race the new host |
||
|
|
570f070f6c |
fix(mobile): the page's Live input on Android echoes each letter as typed (OTA phase C follow-up) (#22958)
* test(mobile): the page's live input sends a composed word's letters as they are typed The reported shape, in a browser: `/tui` on an Android keyboard, where the `/` reached the terminal and `tui` did not until Enter. Android keyboards hold a composing region over the Latin word being typed, so on the page every input event mid-word carries `isComposing: true`, and the preedit mirror holds reported preedit with no settle timer. Native Android reports no range, so there each ASCII keystroke is sent as it is typed. Driven through Chromium's own IME path with an Android WebView user agent: the letters after the slash must arrive one by one, and a correction on commit must still erase and retype. The iPhone case is the guard that a composition off Android stays held. The hook case says the same thing without a browser. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): the page on Android reports no composing range, as native Android does `handleLiveInputChange` passed `nativeEvent.isComposing` straight to the preedit mirror. On the page that event is the DOM's, and an Android WebView marks the keyboard's composing region, which Samsung and other Latin keyboards keep over every word. The mirror treats a reported range as preedit that is not text yet, holds all of it with no settle timer, and Chromium fires no input event after compositionend, so the word sat in the field until the next keystroke or Enter. Native Android reports no range at all, and its fallback holds only a trailing non-ASCII run. The page on Android now reports the same: undefined. ASCII echoes on each key, a Hangul or kana run still settles on the timer, and a correction on commit still erases and retypes. iOS, where the range is the text system's marked text, is unchanged. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * docs(mobile): name the Android page among the platforms that report no composing range The mirror's fallback note listed only React Native Android; the page on Android now reports no range too. The reader's docblock becomes the one line it needs. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb |
||
|
|
c9d421f3c1 |
fix(mobile): the page's terminal shows its last rows above the command dock (OTA phase C follow-up) (#22806)
* fix(mobile): mount the page's terminal frame with its onLayout On the page every branch of the session content is a bare View in one slot, so the terminal frame reused the loading View. react-native-web observes onLayout only on a View that mounts with it, so the frame never reported its height: every measure fell back to the page's window.innerHeight and the PTY got rows the frame cannot show (63 vs 47 on a 1280x2856 screen), with the last rows under the command dock. Keying the frame makes it mount fresh. The parity pin moves for the key string. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): size the terminal document to its host, not the window Inside the WebView the window is the terminal frame; on the page it is the whole page, header and dock included. Every fit, pan, scroll and overlay bound in the document read window.innerWidth/innerHeight, so a measure without a container height sized the PTY to the page. The document now reads one seam, viewportSize: the window by default (native unchanged), the host element's box on the page. A census keeps raw window size reads out of document/. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): put the terminal document's coordinates in its host Round one moved the page document's size to its host but left its origin at the window, so a tap below the session header mapped rows low (row 19 for row 11), and the selection overlay and scroll indicator, position: fixed, drew over the page. - viewportRect replaces viewportSize: the host's box on the page, the window at 0,0 in the WebView. viewportPoint is the one place a client point meets that origin; cell mapping, mouse reports, the pinch anchor and edge scroll go through it, and a census holds the rest to deltas. - observeViewport: the fit refits on a host ResizeObserver on the page, a window resize in the WebView. - A hidden host measures 0x0; the fit keeps scale 1 rather than 0. - On the page the host is the overlays' containing block. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): state what native measured in the parity note Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): read the page terminal's overlay and refit off its host The overlay is absolute inside the host now, and the page's refit follows the host's box, so the owed-frame case pulses the host rather than the window. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): hold the terminal fit while its host has no width computeFitScale answered 1 for a 0-wide host, but applyFitScale still committed that fit as soon as the cell width was known, so a display:none screen got a fit for no box. The attempt now leaves the fit pending until the host reports a positive width, and observeViewport starts it again once it does: one commit, never at scale 0. A regression test pins edge scroll to the host's edges: with the host at top 100 and height 600, the down band starts at client Y 660, not 560. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): skip the terminal refit while its host has no box react-native-screens hides the session with display:none when another screen covers it, so the host's observer reports 0x0. The refit still clamped pan and repainted, so coming back to the session lost the pan the user left; native never refits on navigation. The refit now does nothing until the host has a positive width, and the next real box refits once. Also untangles the seam-count sentence in document-host-seams. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): refit the page terminal only when its box changes Coming back to a session takes the host from 0x0 to its old box. The observer fired, the refit committed, and the user's pan and zoom were reset, where native keeps them because navigation never resizes its WebView. The fit now remembers the box it was committed for, and the refit acts only on a positive box different from that one. The frame tests wait on the document's own frames instead of a timer, and the hidden-host case asserts the zero-width read happened. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): refit a shown terminal when a fit was asked for while hidden The zero-width gate drops a fit requested while the host is hidden, and the same-box skip then kept the stale fit when the host came back at its old size: 80 columns at 390 px stayed at the 55-column scale, 0.945 instead of 0.65. Any fit request now forgets the last fitted box, and so does a text-scale change that could not resize a hidden grid. A plain hide and show asks for no fit, so it still keeps pan and zoom. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb |
||
|
|
46079aa20b |
fix(lint): include .mts and .cts in line-limit checks (#22413)
Co-authored-by: DeryFerd <DeryFerd@users.noreply.github.com> |
||
|
|
f9356d491a |
fix(release): run the tag's own skill freshness inventory tests in the release gate (#22775)
The skill-sharing release gate checks out the release tag, then restores main's copy of skill-freshness-inventory.test.ts. That file holds behaviour tests, so a behaviour test added on main runs against a tag that predates the behaviour: #22606 added one and failed the v1.4.211 gate on macOS and Linux. Keep restoring skill-provider-runtime-roots.test.ts from the workflow ref. |
||
|
|
2c7609bf6a |
fix(terminal): serialize only the visible width after a column shrink (#22586)
* fix(terminal): serialize only the visible width after a column shrink xterm does not reflow the alternate buffer (or a normal buffer under pre-21376 ConPTY), so after a shrink each line keeps its old length. SerializeAddon walked every non-final row to line.length, so any snapshot taken after a shrink carried stale right-hand cells that wrapped into extra rows on replay; restores repainted that garbage and a differential TUI such as OpenCode never cleared it. Clamp the row walk and the wrap-boundary lookups to the terminal's columns in Orca's addon-serialize source patch, and regenerate the bundles, maps and lockfile hash per docs/reference/xterm-patch-regeneration.md. * test(terminal): read shrink-snapshot fixtures through public APIs Drops the private-terminal casts the casting gate flags; the normal-buffer case now drives a plain pre-21376 ConPTY terminal and its SerializeAddon directly. * fix(terminal): blank a wide glyph clipped by a column shrink when serializing After a non-reflowing shrink a width-2 glyph can have its lead half in the last column and its trailing half past the grid. Serializing the lead half makes the replay wrap it to the next row and shift every row below, so serialize that cell as a blank and keep the row exactly the grid's width. A glyph ending exactly at the edge is unchanged. * test(mobile): move the session closure pin past the main agent status modules #22452 added src/shared/main-agent-status.ts and src/shared/agent-turn-outcome.ts, which agent-status-types.ts imports, so the session route's closure grew by two local modules (4218 -> 4220). That change was src/shared-only, so its own CI never ran this suite; main has been at 4220 since, and any PR that fires the mobile web app job fails on the stale pin. Measured on |
||
|
|
a05649de91 |
fix(terminal): prove an idle Git Bash prompt through its bin launcher (#22752)
* fix(terminal): prove an idle Git Bash prompt through its bin launcher Git for Windows' bin\bash.exe is a launcher that runs usr\bin\bash.exe as a child and waits, so an idle Git Bash pane's job always holds two pids and the Windows shell proof never confirmed it. Accept exactly the launcher plus its direct bash.exe child, checked against the identity process table. * test(terminal): wait for the Git Bash prompt before asserting the hand-off job * fix(terminal): prove a Git Bash prompt as one unbranched MSYS bash chain Orca launches Git Bash as bin\bash.exe -c "chcp.com ...; exec \"$BASH\" ... -i", and each MSYS exec leaves its pre-exec process alive as a stub, so an idle pane's job is launcher -> stub -> interactive bash. Accept any job that is one parent-to-child chain rooted at the launcher whose every later member is bash.exe, instead of a fixed two-process shape. * ci: register the Git Bash shell-proof win32 test in the package-test list * fix(terminal): read the spawned shell as a path, and keep one shell map A spawned shell path with a space (/Users/John Doe/bin/zsh) was split as a command line, so the POSIX proof compared against "john" and never confirmed. Local panes now keep only the spawned shell path and derive the name from it; the Git Bash chain walk drops guards the member check already covers. |
||
|
|
b419b3183e | test: remove redundant mobile and GitLab checks (#22748) | ||
|
|
7ea01279cd |
feat(search): bundle ripgrep for local, WSL, and SSH search (#22396)
* feat(search): bundle ripgrep for local, WSL, and SSH search Ship @vscode/ripgrep-universal's prebuilt rg for all six relay platforms in every desktop artifact. Local and WSL searches spawn the bundled binary and drop the git ls-files / git grep fallbacks; SSH deploys upload the remote's binary once per ripgrep version and the relay prefers it over PATH rg. * fix(search): address bundled ripgrep review findings - Key the SSH ripgrep cache on the binary's content hash; a package bump is the only update step - glibc verifier: read arch tokens below the slice root and accept static ELFs (arm64 release blocker) - Ship ripgrep/PCRE2/musl license notices; bundle rg with orcad - Packaged builds never spawn a bare rg; report fd pressure as transient - SSH: install rg before sweep/GC, size-validate installs, back off instead of disabling on launch failure - Scope Dependabot to @vscode/ripgrep-universal; revert unrelated lockfile churn * chore(search): drop bundled-ripgrep reference doc; assert full packaging layout parity * refactor(search): one entry point for spawning the bundled ripgrep Local Quick Open, Quick Open path search, the Explorer name filter, and runtime text search each repeated the same three steps: resolve the bundled command, spread in the WSL distro, spread in the WSL shell expression. Fold that into spawnBundledRipgrep so one place owns the rule that a bare 'rg' must never reach spawn, and simplify the resolver's command/packaged checks. Restore the AGENTS.md ripgrep rule dropped alongside its reference doc in |
||
|
|
9f7f406b57 |
test(mobile): repin the RPC recording corpus and session closure after #22392 (#22702)
* test(mobile): repin the RPC recording corpus to main after #22392 #22392 pinned baseline to a branch commit ( |
||
|
|
5613c4fe71 |
fix(i18n): add missing translations for artifacts and browsing (#22697)
Adds translations for artifact publishing, remote browser features, SSH workspace routing, browser identity settings, and skills management across all supported languages (Spanish, French, Japanese, Korean, Chinese). |
||
|
|
80e0bee23b |
fix(floating-workspace): keep agent launches from moving the main window's tab (#22603)
* fix(floating-workspace): keep agent launches from moving the main window's tab
Launching an agent from the floating workspace's "+" menu switched the main
window off whatever chat or editor tab it was showing and onto its terminals.
The main window's selection is supposed to move only for the worktree it is
showing: browser and editor tab creation, splits, moves and drops all check
`activeWorktreeId === worktreeId` before touching it. Two places did not:
- `launchAgentInNewTab` called `setActiveTabType('terminal')` without a
worktree, which targets the active worktree whatever worktree the launch
landed in.
- terminal `createTab` wrote the global `activeTabId` for a tab in any
worktree.
Both now follow the store rule. The launch still selects its tab within its
own worktree, which is what the floating panel renders.
The floating titlebar button had side-stepped this with an `activate: false`
opt-out plus manual selection. That opt-out had no other caller and is removed;
the button now launches and focuses like every other entry point.
* fix(tabs): scope the remaining launch surface writes to the launch's worktree
Three more launch paths create a terminal tab and then call
`setActiveTabType('terminal')` without a worktree, which targets whatever
worktree is active when the call runs rather than the one the tab landed in:
- the paired-host agent launch, after the host's asynchronous create
- Session History resume, which can target a worktree the user is not viewing
and activates it only afterwards
- sleeping-agent resume, which the activation gate runs after asynchronous
readiness checks, by which time the user may have moved to another worktree
Each now names its worktree, like the local agent launch. The new tab still
lands selected when the user switches to that worktree.
* test(tabs): pin the paired-host launch scope in its existing web-runtime test
* test(tabs): type the left-worktree resume fixture instead of casting it
* refactor(tabs): require the worktree that setActiveTabType applies to
`setActiveTabType(type, worktreeId?)` quietly fell back to the active
worktree when the caller left the worktree out. A caller acting on a tab in
another worktree (the floating workspace, a background launch, a reveal that
lands after an async step) therefore retyped whatever the main window was
showing. The launch paths fixed earlier in this branch were instances of that;
54 other callers still relied on the fallback.
The worktree is now a required argument (nullable only for the no-active-
worktree case), so every caller states which worktree it means and a new
unscoped call fails to compile. Each call site passes the worktree of the tab
it acts on; where that is by construction the active worktree (shortcuts,
palette, tab strip), the result is unchanged. `activateTabAndFocusPane`
resolves the tab's owning worktree the same way `setActiveTab` does.
End-to-end helpers that drive the store directly pass the active worktree,
which keeps their previous behaviour.
* fix(floating-workspace): let the floating New Terminal activate its own tab
The floating "+" New Terminal created its tab with `activate: false` and then
selected it with `activateTab`, because creating an active tab used to write
the main window's selected tab even for another worktree. `createTab` now
activates a tab only within its own worktree's group unless that worktree is
the one on screen, so the workaround is no longer needed.
Creating the tab active also moves the floating workspace's remembered tab to
the new one; before, it stayed on the previously selected floating tab, which
auto-acknowledge reads to decide which floating agent the user is looking at.
* refactor(floating-workspace): route every floating New Terminal through one creator
The floating "+" New Terminal had stopped deferring activation, but Cmd+T with the
floating panel focused still went through a separate creator that created the tab
inactive and activated it by hand, which left the floating workspace's remembered tab
on the previous tab. Both now call createFloatingWorkspaceTerminalTab, which creates
the tab active in its own group and focuses it.
* docs(tabs): say why an unowned tab id keeps the on-screen worktree scope
|
||
|
|
060743d813 |
fix(mobile): keep the streamed browser pane flipping on slow phones, and stop double taps (#22392)
* fix(mobile): keep the streamed browser pane flipping on slow phones, and stop double taps
Frame pacing. The pane decodes each frame on a hidden layer and flips to it on
onLoad. While one frame decoded, every newer frame re-pointed that same hidden
layer, which cancels the in-flight load. On a phone that decodes a frame
slower than frames arrive (~10/s during page loads, menus, spinners), onLoad
never fired for any of them and the pane sat on an old frame until the page
went still. A decoding layer is now never re-pointed: only the newest frame is
held, and it takes the layer once the decode settles. A 1.5s watchdog frees a
layer whose decode never reports, and a frame the hidden layer already holds
(a blinking caret alternating two frames) flips at once, since an unchanged
source reloads nothing.
Double taps. When browser.mouseClick failed, the pane replayed the tap as
move/down/up. On a timeout the click is still queued on the host, so the
replay landed a second tap on whatever the first one opened. The replay now
runs only when the click definitely did not reach the host.
* test(mobile): re-record the corpus without the timed-out tap replay
The corpus certified the move/down/up replay after a transport-rejected
browser.mouseClick, which the commit before removes. Scoped like #22179:
baseline bumped by editing that one line, then --record.
788 files. Every changed line classified:
- `baseline`: 787 files (786 goldens + pilot-scenarios.json), nothing else.
- matrix-browser.pointer-click-browser.mouseclick-1.json: the
transport-rejection and transport-rejection-no-message partitions of
browser-pointer-click-fallback now send only browser.mouseClick#1. The
refused partitions still replay, unchanged.
* test(mobile): move the session closure pin past #22452's main-agent-status modules
#22452 changed only src/shared, so its CI never ran the page-closure suite; main
now measures 4220 modules (1034 local) against a pin of 4218. This branch adds
nothing to the closure: its own count matches main's.
* docs(mobile): say a re-pointed decoding layer loses its onLoad, as measured on Android
* refactor(mobile): give the streamed browser pane's double buffer one owner
The frame pacing, decode watchdog, layer flip and reset were spread over three
hooks and a helper module, wired back through the stream hook and the pane.
They now live in one plain pacer (browser-frame-pacer.ts) with one timer, and
the pane binds each layer's View/Image straight to it.
Behaviour fixed on the way, each with a failing test first:
- A slow last frame with nothing newer queued was abandoned by the watchdog and
never shown. The decode deadline now only applies when a newer frame waits.
- Any pane re-render re-pointed both layers at the newest frame behind the
pacer's back, so a blinking caret froze. The Image source prop is now only
the mount-time frame; every later source write is the pacer's.
- A frame that failed to decode left its layer marked as holding it, so an
identical frame flipped to an undecoded layer. Giving up on a decode now
clears the layer's source.
- A native onLoad for a source the layer has since moved off could flip early.
The flip now checks nativeEvent.source.uri, which Android and iOS Fabric both
report as the raw source string; RN Web's own load event has none, so the web
flips only through its decode probe.
The session closure pin drops by the two modules this removes.
* refactor(mobile): send the tap's mouseClick directly instead of through a flag
The delivery-unknown check was a mutable flag set inside the request callback.
The click now calls browser.mouseClick itself in a try/catch: a delivered click
returns, a delivery-unknown failure returns without replaying, and a refusal or
null result still replays as move/down/up. Same wire traffic; the corpus
certifies it unchanged.
* fix(mobile): never cut a streamed frame's decode short
The 1.5 s decode watchdog abandoned a slow decode whenever a newer frame was
queued and re-pointed its layer. On an Android emulator under load that is the
original freeze again: noise frames decode in 2-10 s, every abandon starts a
decode the next abandon cuts, Fresco reports the superseded loads (30 stale
onLoads in one run) and the pane showed 11 of 41 applied frames. Without it,
the same run flips every applied frame (16/16, no stale load), and a slow last
frame is shown in every cycle.
Nothing else needs it: with the layer never re-pointed mid-decode, native
answers every load with onLoad or onError, and the web probe's decode()
always settles. The pacer keeps one timer, for the interval.
* fix(mobile): track what each frame layer's Image holds, so no write goes unanswered
The pacer cleared a layer's source when it gave up on a decode (a reset
mid-decode, or a failed decode) while the native Image still held it. The
next identical frame was then written again, which is a native no-op on
Android (ReactImageView.setSource returns on equal sources) and iOS
(ImageShadowNode skips equal requests): no onLoad, no onError, and the pane
stayed frozen until the stream restarted. Returning from the background to
an unchanged page is enough to trigger it.
Each layer now records the source its Image holds and whether that source
has answered (loading, ready, failed). A layer is written only when it is
not loading and only with a different source, so every write gets exactly
one answer. A reset no longer abandons anything; the load under way still
answers for its layer. A frame the hidden layer already holds flips at once
if it decoded and is skipped if it failed.
The pane test's native model now treats a same-source write as a no-op and
answers each change once with the layer's current source; both new cases
(reset mid-decode then the same frame, failed decode then the same frame)
freeze on the previous head.
Also, per review: the pacer no longer touches busy or metadata. The stream
hook creates it and receives each frame as it goes on screen, so metadata
(and with it touch mapping) now follows the visible frame rather than one
still decoding.
* test(mobile): hold the pane test's AppState listener without a type assertion
* fix(mobile): never replay a tap the host answered
A fulfilled browser.mouseClick ran on the host, but a null result still
replayed it as move/down/up. The native bridge always answers { clicked },
while the external-Chromium provider returns agent-browser's `data` as is,
which can be null, so a right-click there was a double tap. Only a refusal
that is not delivery-unknown now replays.
* test(mobile): re-record the corpus for the answered-tap rule and rename its checkpoint
The commit before stops replaying a tap the host answered with a null
result. The seed's checkpoint was named clicked-by-fallback, which several
partitions no longer do, so it is renamed tap-settled in the same record.
Baseline bumped to
|
||
|
|
7a4f080086 |
revert: #18790 (orchestration incarnation reap fallback and bundled Freebuff agent) (#22601)
This reverts commit
|
||
|
|
f2ac9f29b2 |
fix(browser): let pixel capture hold its own page drawn, without the desktop window (#22534)
* fix(browser): let pixel capture hold its own page drawn, without the desktop window Screenshots were the last browser commands that still borrowed the desktop window: they took the per-page automation-visibility lease, which waits for two desktop-window animation frames (capped at 2 s) and never arrives when the window is minimized or throttled. Only pixel capture actually needs a page drawn — input, scripts, layout, the accessibility tree and PDF all work on a hidden page. Capture now takes a main-owned paint hold: a synchronous, per-page ref-count that tells the renderer one way (no reply awaited) to keep the page drawn and keeps the desktop renderer unthrottled while held. Both Orca's full-page capture and the agent-browser helper's screenshots take it in cdp-screenshot.ts and retry on a bounded schedule until the page answers with a frame; a CDP error fails fast. Deleted: the queue's needsPaint lease, the executeJavaScript acquire path and its two racing 2 s timeouts and late-token cleanup, the renderer's rAF wait and window bridge, the capture commands' own leases, the fixed 300/500 ms settle waits, and the global one-screenshot-at-a-time lock. Rebased onto main after #22528 landed; content identical to the reviewed branch head 7b390ed6a8. * fix(browser): probe for a frame instead of repeating the full capture Retrying a capture resent the caller's full request, so on an already drawn tall page (a full-page capture takes ~0.5 s) the 250 ms retry started a second full beyond-viewport capture while the first was still running. Measured on Electron 43: any later request makes a held page produce a frame, and that frame answers every pending capture with a full, correct image. So the capture is sent once and 1x1 probes follow until it answers; their results are ignored. Also report a detached debugger as detached rather than destroyed, and give the layout-metrics timeout its own "did not respond" message, since that request doesn't need a drawn page. |
||
|
|
37820f9683 |
feat(mobile): the page owns its safe area, like a native screen (OTA phase C follow-up) (#22570)
* feat(mobile): the page owns its safe area, like a native screen The shell reserved both system-bar strips outside the WebView and painted them bgBase, so every page screen showed a flat band above its header, sheet scrims stopped short of the status bar, and the dock floated above the gesture bar. For a page that declares `safe-area-insets` in `ready.accepts`, the shell now draws the WebView edge-to-edge and keeps only the keyboard strip off it. `init` carries the insets the view sits under (bottom 0 while the keyboard ends the view, top 0 under the update banner), and a move is re-sent over the existing route-update `init`. An older page keeps the reserved strips, since it has no reader for the insets. On the page, a root layout (`app/_layout.web.tsx`) feeds those insets to react-native-safe-area-context below ExpoRoot's env()-measuring provider. It also replaces expo-router's DefaultNavigator, an all-edges SafeAreaView that padded a second time. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile-web): re-measure the page pins for the root layout The page's route tree gained `./_layout.tsx` (its web sibling of the native root), so every pin that counts the tree moved: - Script sweep re-measured by building `routes.slice(0, n)` for each n. It reads 69 scripts at 16 routes, which matches the real build. The asset-ceiling crossing moves from 31 routes to 32. - Route closures now enter through both layouts. `entryNames` gains `[dir]` because `app/_layout` and `app/h/_layout` share a name. - Session closure pin 4216 -> 4219. The added modules are bridge-safe-area-insets, page-safe-area-provider and _layout.web. - The web-overrides allowlist names `app/_layout.web.tsx`. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): read safe-area ownership from the page-document state Review fixes on the page-owns-safe-area change. - Ownership is page-document state now. `page-ready` carries `accepts` beside `reports`, the patch sets `pageOwnsSafeArea` from `safe-area-insets`, and the session hook projects it like `backClaimed`. The screen's own per-session copy is gone. - Insets moves re-send `init` only to a page that declared `safe-area-insets`. A page that took route updates but not insets was sent a useless `init` on every keyboard show and hide. - The banner wrapper is gone. The root pads the status bar strip while the banner shows. - The shell session defaults the insets inline, with no predicate that mutated its argument. - The provider is folded into its single caller, `app/_layout.web.tsx`. The session closure pin reads 4218 (local 1032). - The screen tests share their module mocks, and the safe-area cases move to a suite of their own: owned page, banner, iOS and Android keyboard, and an older page that gets no re-init. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb |
||
|
|
63866c1e27 |
fix(mobile-web): page inputs lose the browser focus ring and hairlines draw one device pixel (OTA phase C follow-up) (#22569)
* fix(mobile-web): drop the UA focus ring from page text inputs Chromium rings every focused text field (:focus-visible); no native TextInput paints one. A zero-specificity rule in its own inline block beside the Expo root reset removes it for every page input; buttons keep the browser's ring. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile-web): draw page hairlines one device pixel thick react-native-web pins StyleSheet.hairlineWidth to 1 CSS px, three device pixels on a 480 dpi phone; native draws one. A build shim replaces that one assignment with React Native's own formula (roundToNearestPixel(0.4), else 1/ratio), so every page hairline matches native without touching components. A rendered check at a real device scale (Playwright's emulated scale floors borders to CSS px, which no phone does) measures both parity fixes. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile-web): apply the hairline shim to react-native-web's CommonJS build The page's dependencies require react-native, so esbuild resolves every importer to react-native-web's dist/cjs build, which the previous filter did not match: the shipped bundle still assigned hairlineWidth=1. The filter now matches both builds, the rendered check requires the package the way the page does, and a builder test reads every hairlineWidth assignment in the bundle. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile-web): draw page hairlines at a width WebKit paints too 1/ratio is exactly one device pixel, and WebKit floors that to 0 and paints nothing (0.3333px at a scale of 3), so the iOS shell would have lost every hairline. The shim now uses native's device-pixel count plus half a pixel; both engines floor a border to whole device pixels, so each paints what React Native paints at ratios 1, 2, 3, 3.5 and 4, measured per engine. The rendered parity check now runs in WebKit at a device scale of 3 as well as Chromium. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile-web): check the parity style in the existing root-reset build Drops a second full build that read one HTML string, plus two assertions that tested the constant against itself. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile-web): round the page hairline up to the 1/64 px layout step Half a pixel over native's count kept borders at one device pixel but made the separators drawn as `height: StyleSheet.hairlineWidth` straddle two rows at about half of all offsets. Both engines lay out in 1/64 CSS px, and WebKit stores an exact 1/3 as 21/64 and paints nothing, so the width is now native's device-pixel count over the ratio, rounded up to the next 1/64 (22/64 at 3). The rendered check adds a separator at a 10.1 px offset in Chromium and WebKit. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb |
||
|
|
5b6a857e41 |
fix(mobile): route the bottom drawer's keyboard through the platform seam (OTA phase C follow-up) (#22556)
* fix(mobile): route the bottom drawer's keyboard through the platform seam Fill-mode sheets called Keyboard.metrics() directly, which react-native-web does not implement, so opening one on the page threw and the shell re-downloaded the workspace. The drawer now reads useSoftKeyboard, whose native half seeds from metrics() and carries the event duration, and whose web half answers from the window (duration 0). The fill/content-sized seed rule and resolveBottomDrawerKeyboardInset are unchanged. A census keeps Keyboard.metrics/addListener inside the seam plus the tab-sheet hide wait. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(config): retire the drawer's exemption from the page keyboard census The bottom drawer now reads the keyboard seam, so no module in the source-control or review closures names react-native-web's Keyboard stub. The census also flags Keyboard.metrics, which the stub lacks. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * refactor(mobile): give the drawer an imperative keyboard pair from the seam The seam now exports subscribeSoftKeyboard and currentSoftKeyboardHeight beside its hooks. The drawer's effect is back to its original shape with only its Keyboard calls swapped for the pair, and useSoftKeyboard is back to {height, visible} with no metrics() seed. Seeding every consumer opened an iOS window between willHide and didHide where metrics() still reads open. The web pair answers from visualViewport, so it stays silent inside the shell and lifts sheets in a plain mobile browser. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): start the web keyboard subscription from the current strip A keyboard already covering the page when subscribeSoftKeyboard attached never produced onHide when it closed, so the occlusion hook and a seeded fill sheet stayed lifted. Outside the shell only. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb |
||
|
|
9cdbc0c128 |
fix(mobile): keep the shell's window insets out of the page WebView (OTA phase C follow-up) (#22549)
* fix(mobile-web): stop the page declaring viewport-fit=cover The shell already pads the WebView out of the status and navigation bars. With viewport-fit=cover, Android's edge-to-edge WebView still reports the window's bar insets through env(safe-area-inset-*), which react-native-safe-area-context on web reads, so every page-side SafeAreaView padded a full bar a second time. Without it env() reads 0 and the shell's pad is the only one. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): keep the shell's window insets out of the page WebView WebView M144+ forwards the window's systemBars and displayCutout insets to CSS env(safe-area-inset-*) for every WebView, and Chromium applies them regardless of viewport-fit. The shell already pads the WebView out of both bars, so every page-side SafeAreaView (expo-router's DefaultNavigator and the session header) padded a bar a second time. M139+ likewise resizes the visual viewport for ime(), which the shell has already done by shortening the WebView. The WebView now sees those three types zeroed, per Android's "zeroing" approach (not CONSUMED, so later changes still reach it). A listener replaces the WebView's own onApplyWindowInsets, so the zeroed set is passed back into it. iOS needs nothing: the WKWebView uses contentInsetAdjustmentBehavior = .never inside the padded shell and reports zero insets. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * docs(mobile-web): say why the page declares no viewport-fit The earlier comment claimed dropping viewport-fit=cover makes env() read 0 on Android; Chromium's WebView applies the safe area regardless of viewport-fit. The page simply never asks to extend under the bars, and the shell owns the safe area. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * refactor(mobile): keep the page inset zeroing private to the shell view The transformation has no honest JVM test (the builder runs as SDK 0 there and drops every inset type), so it moves into MobileWebShellView.kt as private members instead of standing alone. The listener comment now covers both the P-R listener and the S+ onApplyWindowInsets path it replaces, and the page document's comment says the env() zeroing is Android's; on iOS the padded WKWebView reports none. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb |
||
|
|
519bde81df |
feat(ipynb): render notebooks like a notebook, with seamless click-to-edit cells (#22519)
* feat(ipynb): parse ANSI SGR sequences in notebook output Tracebacks and stream output carry terminal colour codes that the notebook printed raw. Splits text into styled runs (16/256/truecolor, bold, italic, underline) and drops non-SGR escapes via the shared stripper. * feat(ipynb): render notebooks like a notebook, not a form - Prose inherits the app UI font instead of a bare terminal font name, which Chromium could not resolve and fell back to Times (removes the now-dead resolveEditorFontFamilyOrInherit). - Markdown cells render by default; double-click or Enter edits them in the same Monaco surface code cells use. Code cells activate on press so a collapsing neighbour cannot swallow the click, which also retires the root pointer-capture deactivation (Monaco blur already covers it). - Code sits on its own tinted surface, the active cell gets a ring, and the editor sizes to its content so activating a cell no longer jumps. - The always-on 8-button toolbar and native select become a hover/focus toolbar (move, delete, and a menu for insert and cell type); the Jupyter [n] prompt turns into the run button. - Outputs show only the richest MIME representation, HTML renders in a script-less, no-network sandboxed frame sized to its content, and ANSI colours use the default terminal palettes. Adopts the MarkdownPreviewBody reuse, richest-MIME selection, extra raster MIME ranks, and CSP-sandboxed auto-height HTML frame from #18542. Co-authored-by: maxidiazbattan <maxidiazbattan@gmail.com> * chore(ipynb): drop the nbformat label and BETA badge from the notebook header parseIpynb already rejects notebooks without a v4 cells array, so the label carried no actionable information; the parsed nbformat field goes with it. Removes those catalog keys and the stale lowercase code/markdown ones. * fix(ipynb): keep cells pixel-stable when they switch to editing The preview and the live editor disagreed on four things, measured over CDP: - Font: the excerpt painted the bare "SF Mono" name (or --font-mono via its row class), while Monaco appended its own fallbacks and landed on Menlo. Both now use resolveEditorFontStack, the editor font plus the terminal fallback chain. - Line height: 20px rows vs Monaco's 21px. Both read CODE_EXCERPT_LAYOUT. - Gutter: a 48px line-number column plus 12px inset vs Monaco's 25px gutter. Notebook cells drop line numbers (the Jupyter and VS Code notebook default) and Monaco's decorations lane is the same 12px inset. - Rows: colorized blank lines collapsed to 0px, and a trailing newline had no preview row. Rows are fixed-height and a trailing newline opens an empty last line, matching the Monaco model. The [n] prompt and run icon now share one grid cell, so the hover swap keeps the label's box and centre. The commented-line tint moves to a theme token. * feat(ipynb): VS Code-style run gutter above a fixed execution count Replaces the in-place [n]/play swap from |
||
|
|
293c2508fc |
test(mobile): move the session closure pin past the structured tool-line module (#22430)
#22349 added `src/shared/structured-agent-session-tool-call-block.ts`, which the projection and live turn the session route already reaches import. The PR was src/shared-only, so its CI never ran the closure suite; main's pin stayed at 4215 while the closure measures 4216. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb |
||
|
|
52a1e2875b |
feat(orchestration): accept Muse model and effort for supervised workers (#22383)
* feat(orchestration): accept Muse model and effort for supervised workers `worker-start --agent muse` already launched, but `--model` was refused because Muse had no session-option catalog. Add one that maps worker preferences to `muse --model <id>` and `--reasoning-effort <level>`; it seeds no models, so native-chat surfaces show no picker. opencode stays without `--model`: the opencode 2 TUI (now shipped as `opencode`) rejects the flag, so the refusal now tells callers to rely on the agent's own config. Help, skill guide, and docs list valid `--agent` ids and the agents that accept `--model`. Refs #19823 * test(mobile): repin session route closure for the Muse option catalog |
||
|
|
ebed0964a2 |
feat(agents): add first-class Muse Code harness (#22216)
* feat(agents): add first-class Muse Code harness Add Muse as a supervised Orca agent across desktop, mobile, session history, source control, local hooks, SSH, WSL, and native Windows. Preserve user settings, support Muse 1.3 hook environment allowlists, and recognize versioned foreground processes. Include question, waiting, completion, resume, and readiness coverage. Co-authored-by: homesh-dev <300847526+homesh-dev@users.noreply.github.com> Co-authored-by: jeffhuen <32542276+jeffhuen@users.noreply.github.com> Co-authored-by: John Cusack <johncusackccm@gmail.com> Co-authored-by: Adrien De oliveira <75085839+adriendeoliveira@users.noreply.github.com> * test(agents): cover Muse remote hook registration * test(agents): cover Muse hook and source-control contracts * test(agents): exclude Muse hook metadata from script mode check * test(agents): keep Muse skill picker coverage stable * test(ai-vault): include Muse in every-agent fixture * test(mobile): repin Muse agent icon closure * fix(muse): detect questions and approvals from structured Muse signals Muse 1.3 fires no hook for request_user_input, so a pending question left the pane "working". Its internal reminder subagents also post hooks with their own session ids (even after Stop), which surfaced "tool failed" rows and flipped finished panes back to working. - Read pending questions from Muse's session log (user_input_prompt_requested/settled) via the existing transcript poll, now generalized from Codex subagents to Muse on main and relay. - Drop child-session hooks (SubagentStart ids, or turn_id === session_id). - Treat Notification permission_prompt as the approval wait; PermissionRequest also fires for auto-approved calls, so it only caches the approval card. - Ignore Notification copy as the prompt; poll replays are not new prompts or turn boundaries. - Allowlist USERPROFILE so Windows cmd AutoRun doesn't fail every hook. * perf(muse): parse only question events from the session log Most Muse session-log lines are large model/tool records. Filter raw lines by the user_input_prompt_ marker before JSON.parse via an optional readJsonlCursor line filter. * fix(muse): unwrap batched log records and scope questions to the live turn Review follow-ups: question events inside retained_frame batches were skipped, and a question left open by a crash or interrupt stayed pending for the pane's life. Share the history scanner's retained_frame unwrapper, and only report a pending question whose run_id matches the hook turn_id. * refactor(muse): drop type assertion in retained_frame unwrap * fix(agent-hooks): satisfy exhaustive-switch lint in transcript poll policy --------- Co-authored-by: Adrien De oliveira <75085839+adriendeoliveira@users.noreply.github.com> |
||
|
|
0e6862cbcc |
fix(mobile): the page offers no control whose only effect is a re-dial it cannot make (#22326)
* fix(mobile): a Retry that can only re-dial is not offered where nothing dials Six failed-load screens share one Retry shape: re-dial a host that is not connected, otherwise re-read. On the page the re-dial is inert (`client-context.web.tsx:55`) and each screen's load already re-runs when the shell's client reconnects, so in the disconnected state that Retry did nothing at all. `connectionRetryAction` makes the decision once and answers null when a re-dial is needed and none exists; agent history, the file explorer root, the file preview, git history, the source-control status gate and the diff review render no Retry for null. The explorer's per-folder Retry keeps its control: it queues the folder, and the queue drains on the next `connected` whoever brought it back. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): the page offers no re-dial, so no header offers one `forceReconnect` on the page was `() => Promise.resolve()`: the shell owns the connection and nothing in the document can re-dial it. The host header's Reconnect and the session header's "tap to retry" were wired to it and did nothing there. The context member is now nullable and the page's provider hands out null, so the compiler found every caller: both headers render no reconnect affordance for null, and the session status keeps the verdict label without promising a tap. Native providers and the recording adapters still pass a function, so nothing a phone renders changes. The session route's host-JSX parity hash moves for the header's extra null check; the page test doubles that stubbed the old inert re-dial now stub null. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * docs(mobile): the auth-failed banner cites the page's re-dial as null Three comments and the banner's override reason still said the page's `forceReconnect` was an inert `() => Promise.resolve()`, and cited `client-context.web.tsx` lines the previous commit moved. They now say null and point at the lines that hold it. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): the session closure gains the page's Retry decision `connection-retry-action.ts` is the one module the Retry fix adds to the session route's page closure, reached through the explorer, source control and git history it docks. Measured on this head with all five generators run first, and diffed against the pre-change closure: one local module added, none removed. Session route closure 4207 -> 4208 modules, local 1021 -> 1022. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * docs(mobile): the capability probe belongs on the page, and says why The push fence excluded `runtime-capability-probe.ts` because the session route and the host screen run it. The session half holds, and the probe works there: `status.get` carries no client identity and makes no write, the shell forwards it like any non-`native.` request, and the desktop's mobile allowlist admits it. The host-screen half no longer does: `codex-reset-credit-capability.ts` is reached only from `accounts.tsx`, which the bundle carries and the page hands to the native screen. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): keep the session retry test's cast under its disable line The formatter wrapped the cast onto the line after the disable comment, which left it uncovered. The cast now sits on its own line directly below the SAFETY note. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): the agent-history Retry test mocks the pathname the handoff reads Main's page route handoff now subscribes to `usePathname` (#22300), and the Retry suite this branch added mounts that handoff with an `expo-router` mock that lacked it. Same one-line addition main made to the back-handoff suite. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): pin the reload each hidden page Retry relies on Hiding a Retry on the page rests on the screen's load re-running when the shell's client reconnects, because nothing on the page re-dials. Only the explorer's folder drain pinned that. Each other site now has a case that starts unreachable with no Retry and asserts the load goes out on the client and state the reconnect delivers: agent history (status.get), file preview (the preview read), diff review (the snapshot load), git history (git.history) and source-control status (git.status, in the loaders suite because the panel test mocks the state hook). Each goes red when the `client`/`connState` dependencies it guards are removed; for source control that is both `loadStatus` and the `loadBranchCompare` it depends on. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): one import of the transport types in the source-control loaders test CI's native code-quality audit denies the duplicate-import warning the reload pin added. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb |
||
|
|
7240368726 |
feat(mobile): a failed hybrid-shell update is recorded on the device and shown in Troubleshoot (#22321)
* feat(mobile): name why a bundle fetch refused what arrived
The fetch threw plain errors whose only content was prose naming asset
paths and hashes, so a caller could not keep the cause without keeping
the prose. Each refusal now carries a code beside the unchanged message.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* feat(mobile): record why a hybrid shell update failed, on the device
A release build forwards no console output to logcat, so a refused or
failed page update left the fallback banner and nothing else. Every exit
from a failed update read now emits a record-update-failure effect: the
cause as a closed code (never an error message), the generation offered
and the one on disk, and what went on screen instead. The runner stamps
host id and time and the generation store appends it to a bounded log in
the cache root, five per host and twenty in all, oldest evicted first.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* feat(mobile): forget a removed host's recorded update failures
Removal clears the host's entries from the shell's update-failure log
after the metadata commit, unawaited and best-effort: it is evidence
about a host that is gone and never a reason to hold the removal.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* feat(mobile): show recorded update failures in Troubleshoot
A "Workspace updates" section lists the newest recorded failure of each
paired host: the reason, the generation offered, and what the shell
showed instead. It renders nothing until a failure has been recorded and
mounts only where the hybrid shell can run.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* test(mobile): type the update-failure row doubles without casts
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* fix(mobile): forget a host's update failures once a newer generation commits
The Troubleshoot row reads "Last update from Host N failed", which stops
being true the moment a later update from that host lands. The activated
step for the build this flow downloaded now emits forget-update-failures
for the host. A cache open, an offline open and a same-build hit activate
a build the flow never requested, so they leave the record alone.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* test(mobile): re-pin the session closure for the shared journal producer #22299 added
main at
|
||
|
|
11db2b9a7d |
feat(mobile): the device Back key reaches the page (#22308)
* feat(mobile): the page can claim the device Back key The shell's page had no way to hear Android Back: every sheet inside it early-returned on web, so the key popped the whole session route. Adds the first negotiated shell-to-page frame kind alongside it. - `back-claim`, page to shell, declared in `init.accepts`: the document is holding the key, or has let it go. - `back`, shell to page, declared in `ready.accepts`: one press, dispatched to the newest consumer that takes it. A press nothing takes is handed back as a `navigate-back` rather than dropped. Both are optional fields on frames the other side already reads, so an old shell never hears a claim and an old page is never sent a press; each pops as it does today. No protocol bump and no stream opcode. `bridge-host.ts` was at its line cap, so the notify forwarder moves to `bridge-host-notify.ts` unchanged. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): Android Back closes the sheet on the page, not the screen Inside the shell's page every sheet early-returned on web, so one press left the session route with the sheet still open. The drawer, the right drawer and the file-preview prompt now claim the key through one seam on both platforms: `use-back-claim.ts` is the hardware key, `use-back-claim.web.ts` is a claim on the shell's. All sixteen session sheets render through `MountedBottomDrawer`, so the one claim there covers every one of them, and a census fails if a sheet bypasses it. `route-handoff.web.ts` claims while the page grew a stack of its own, and hands the press back when it did not. The shell takes the key off the navigator only while a claim is live: Android gets a `hardwareBackPress` handler that returns the host's own answer, iOS loses the stack's swipe-back. The claim is cleared on `document-started`, on a remount, on a new `ready`, on anything that takes the generation off screen, on the page's `close` and on dispose. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(config): a Back press closes a sheet on the real bundle The unit suites reach both halves of the lane but never the two together on a document a browser rendered. The render rig can now post a `back` frame, and the drawer check opens the Filter sheet, reads the claim off the notify list, sends one press and pins that the sheet closed with no `navigate-back` behind it. Red without the drawer's claim: the claim never arrives. Also fixes a fragility the rich-markdown rig caught. `MountedBottomDrawer` is shared with the native app and mounts under no page provider in a bare tree, where `usePageBridgeClient` threw; the seam now reads the bridge through `usePageBridgeClientIfPresent` and claims nothing without one. Session route closure 4207 -> 4209: `use-back-claim.web.ts` through the route handoff, `bridge-page-back.ts` through the envelope. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * refactor(mobile): mirror the Back seam's latest values from an effect Three `ref.current = …` writes sat in render, which React replays and discards. Each moves into a dependency-list-free effect declared ahead of the registration that reads it, the shape `use-mobile-web-shell-bridge.ts` already uses for the same reason: the caller rebuilds the value every render, so there is nothing to depend on, and `useRef` seeds the first mount. The registration still keys on the claim alone, so a rebuilt handler re-registers nothing. The web seam's test drops its two type assertions for a named fixture type. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): the page says its Back claim again on every init The claim was edge-triggered and the shell forgets on purpose: it drops the claim answering every `ready`, and a host rebuilt under a live page — a client swap through forceReconnect, which leaves the WebView mounted — starts with none at all. A document still holding a sheet was then unknown to the shell, and the next press popped the screen out from under it. `init` is the shell saying it is here now, so the page answers each one with the state rather than with a transition. Posted after the session has taken the frame, so the gate reads that `init`'s own `accepts` and a shell that never named the claim still hears nothing. Nothing is said while nothing is held. Every `init` answering a `ready` comes from a host that dropped the claim first, so it already holds false; the only other one carries a rewritten route, where a stale true needs a `false` the page posted to have never left, and a port that refused that frame refuses this one too. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): a rebuilt host keeps the session's Back claim A host is rebuilt when the client under it changes, and the page document does not move: the WebView stays mounted, the session id holds, and the page is never told. The rebuilt host started with no claim and no `accepts`, so it refused every press and the navigator popped the screen out from under an open sheet. Two clients on the same generation leave the page nothing to refuse, so nothing made it re-ask and re-assert. What the page declared and what it is holding are facts about the session, the way `sessionEstablished` already is. `createBridgeHostBack` takes them as a seed, `readSessionBack()` hands them on, and the hook holds them stamped with the session so a record left by one never seeds the next. `dispose()` no longer reports the claim gone: a host retiring is not a document ending, and that report was the thing taking the key off a live sheet. Every reset path is unchanged and still has its own case — the page's `ready`, its `close`, and the session's own store for `document-started`, `remounted` and anything that takes the generation off screen. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb |
||
|
|
564f135248 |
fix(mobile): an accessory Enter ends the field's editing session, and the page's Enter survives a composition (#22300)
* test(config): the page's live input never submits under an open composition The emulator's page-only defect, in a browser: an Android soft keyboard keeps a composition open over the word being typed, so the Enter keydown carries `isComposing: true`, which is the condition react-native-web reads to skip `onSubmitEditing` entirely. Nothing reaches the terminal and the field keeps the text. The probe route now mounts `useTerminalLiveInputCommit` and the command dock's own field props, so keys enter through the browser rather than through a handle that calls the hook directly. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * style(config): format the live-input render check Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): the page's live input submits under an open composition react-native-web's keydown handler withholds `onSubmitEditing` whenever the Enter keydown reports a composition — `nativeEvent.isComposing`, or the Android `keyCode` 229 that stands for it — which is a soft keyboard's normal state mid-word. Nothing reached the terminal and the field kept the text. Native Android's editor action has no such suppression, which is why only the page showed it. The field now also claims `beforeinput`/`insertLineBreak`, the browser's own end-of-line signal. react-native-web cancels every keydown it does submit on, so that event exists only in the cases it dropped, never twice; an IME still choosing a candidate reports `insertCompositionText` and is left alone. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): list the live-input submit binding, and repin the session closure The `.web.ts` sibling needed its row in `web-overrides.json`, whose check lists exactly the overrides on disk. The session route's page closure moves with it: the callback ref and the binding it resolves to are both local, and the native sibling stays out, which is what the pair is for. modules 4207 -> 4209 (+2) local modules 1021 -> 1023 (+2) Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): the key bar's Enter chip leaves the sent text in the live field The device trace's variant (a), which is what shots/23 was: the chip emits no DOM key event, so react-native-web's submit handling never runs and the accessory hook is the only thing that could end the field's editing session. With nothing held it takes the send-now branch, which flushes nothing and writes neither the capture state nor the field, so the text the PTY already echoed stays put and the next keystrokes append to it. Not a page defect: the held-text fallback only holds a trailing non-ASCII run, so ASCII leaves nothing held on native either. The unit case is on the shared hook for that reason. The probe route now models what the send actions do with 'allow-raw', so the check can see a control sent twice or not at all. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(config): the buffered command field has the live field's Enter gate too Three cases, one red. A plain Enter sends the draft and empties the field, which is `beginBufferedTerminalDraftSend`'s doing and stays a guard. The key bar's Enter chip in buffered mode is a plain terminal key: the accessory hook declines at its live-handle guard, one carriage return goes out and the draft is untouched, also a guard. The red one is Enter under an open composition. This field reaches its send through `onSubmitEditing` alone, so react-native-web's keydown gate swallows it exactly as it did for the live field, and the draft neither goes out nor leaves the field. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): an accessory Enter ends the field's editing session, on both fields Two defects, one rule: after a control that ends the line, the terminal owns the echoed text and the field's editing session is over. The key bar's Enter chip (device trace, variant (a)) emits no DOM key event, so only the accessory hook could end that session. Its held-text branch does, through the flush; with nothing held it took `send-now`, which flushed nothing and wrote neither the capture state nor the field. Not page-only: the held-text fallback holds a trailing non-ASCII run, so ASCII leaves nothing held on native either. `send-now` now takes the same flush when the bytes end the line, and still defers the send to its caller so exactly one return goes out. The buffered command field had the live field's composition gate, because it also reaches its send through `onSubmitEditing` alone. It binds the page's line-break signal now too, which is why the seam is named for a terminal text field rather than for the live input. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): the submit binding holds the handler its first render was given Found by pullfrog on #22300. The binding refreshed its handler ref only when the callback identity changed, so a caller memoizing on an empty dependency list was bound once and never again. The buffered command field does exactly that: its submit closes over handleSend, a per-render function whose guard reads client and activeHandle, both null until effects supply them, so the page's line-break submit could never pass that guard. The probe route now carries the same two paths the dock has — a fresh per-render function on the field's onSubmitEditing prop, and the memoized closure on the binding — because the working prop path is what hid this. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): the buffered field's page submit reaches a live handleSend Found by pullfrog on #22300. `submitBufferedDraft` was memoized on an empty dependency list, which froze the per-render `handleSend` it calls. That guard reads `client`, `activeHandle` and `canSend`, none of which the first render has, so the page's line-break submit could never pass it. The field's own `onSubmitEditing` prop kept working, which is what hid it. The handler is per-render now, and the binding refreshes its handler ref on every commit rather than when the callback identity changes — the ref exists so the listener always reaches the newest handler, and it should not rest on a caller's memoization. That second half fixes nothing on its own: a `useCallback` with `[]` returns one function object for the life of the component, so no ref can find a newer closure behind it. The source census is what catches that, and it is red against the frozen handler. The probe route is back on the dock's shape, per-render on both submit paths, with a note saying why a route that writes its own submit cannot catch this. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): the page's buffered submit reaches a handleSend that can send The census beside this reads one spelling of a frozen handler. This reads the behaviour: the send-actions hook is rendered first as a session is before its effects land, with no client and no handle, then again as it is after, and the listener the page's binding attached has to reach the second one. Asserted on the params that reach the client, not on a call count. Red with the `useCallback` restored, and red with a `useMemo` in its place, which is the point of testing the behaviour rather than the spelling. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): declare the two mock arrays instead of asserting them `[] as Array<T>` inside the hoisted factory was a type assertion with nothing to explain: the arrays are built here, so a checked declaration says the same thing and the quality gate has nothing to flag. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * refactor(mobile): the submit binding stops claiming a cure it does not have The handler ref refreshed on every commit rather than on the callback identity, and the docblock called that the reason the seam exists. It is not: a caller that freezes its closure hands this hook one function object for the life of the component, so no ref finds a newer one, and a caller that does not freeze it changes identity every render and refreshes the dependency anyway. Measured both ways. The dependency is back, and the prose says only what the code does. The rule that does hold — a bound handler must not be frozen on an empty dependency list — is stated where it is enforced, in the wiring census, with the behavioural check named beside it. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * refactor(mobile): the submit binding's latest-ref drops its dependency list React Doctor flags the dependency twice, once per bound field: both of the dock's field submits are rebuilt every render, because the handleSend they read is, so `[onSubmit]` is a new value every time and there is nothing to compare. The tree's other latest-refs are written without a list for the same reason — use-mobile-web-shell-bridge.ts:148 is the one this follows. The comment says what the ref does, which is mirror the newest closure after each commit so callers may pass per-render handlers. It claims nothing about a caller that freezes one; that rule is still the wiring census's. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb |
||
|
|
a8786c040d |
fix(mobile): the page never removes a host, and stops bundling push (#22283)
* fix(mobile): the page never removes a host The page holds one host profile from `init.host` and no credential, so `removeHost` on web resolved without doing anything and the screen reported success for a host that was still paired. Its `.web` sibling refuses with a typed error instead, and the auth-failed banner's Remove — the one surface that opens the confirm — is absent on the page, because a control that can only refuse should not be there. Refusing is also the fence that keeps `push-registration.ts` out of the page bundle: the native lifecycle file's import was that subsystem's only path into a page route. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): drop the push families the page closure no longer reaches `host-removal-lifecycle.web.ts` was `src/notifications`'s only path into a page route, so the whole directory left the page bundle and the capability probe left the C1 layout closure with it. The derived family set shrank by two; the pin tables and their counts now match what the closure reaches. The expo-notifications fence grows a second claim and loses a precondition that had become false: `push-token.web.ts` and `desktop-notification-channel.web.ts` are no longer in the bundle either, so the fence is stated as the absence of the directory. C1 20 families / 94 goldens, C2 68 / 257, C3 26 / 116, C5 25 / 125. Session route closure 4211 -> 4207 modules. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): the page's auth-failed banner offers no control it can honour The banner is reachable on the page — the shell forwards the native client's state verbatim (`bridge-host.ts:381`) and `auth-failed` is in the wire enum (`bridge/bridge-envelope.ts:43`) — and the page can honour none of its three actions. `forceReconnect` is `() => Promise.resolve()` there (`client-context.web.tsx:55`, read through `host-client-hooks.ts:87`), `/pair-scan` sits outside the page's route root of `app/h` (`mobile-web-app-route-manifest.mjs:6`), and removal refuses. The previous commit hid only Remove and claimed the other two still worked; they do not. The whole action row moves into `AuthFailedBannerActions`, whose `.web` sibling renders no control and one line naming the app. The sentence above it is unchanged: re-pairing from the desktop is still what to do. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb |
||
|
|
3bb9a4e261 |
fix(mobile): keep a painted frame under the page until its first paint (#22264)
* fix(mobile): keep a painted frame under the page until its first paint The shell tore its own frame down the moment a generation was on screen (`MobileWebShellScreen.tsx`, the `ready` branch), and a mounted WebView draws nothing until its document paints. What showed for the whole of the page's boot was the surface behind it with nothing on it: 1.42 s on a cached generation, against a one-frame budget. The page is the only thing that knows when it has a frame, so it says so. It declares `painted` in `ready.reports` and posts the notify after the browser has painted its first commit; the shell holds the same neutral frame it was already painting while it opened the generation, then fades it out. The wait is bounded by the declaration and never by a timer: a generation served by an older desktop declares nothing and is uncovered on `ready`, which is what every shell did before this. iOS painted white rather than nothing: a WKWebView is opaque by default, so the shell's own surface never showed through. It is now transparent, as the Android view already was. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): hold the cover through the compositor handover The page reports the paint its own renderer made; putting that on the app's surface costs another frame or two. A linear fade from the report left two frames of bare surface between the two on an emulator, which is the hole the cover exists to close. Eased in over 220 ms, the cover keeps most of its opacity across that handover: five reopens now show 0-21 ms of bare surface against 102-2043 ms on the build without it. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): negotiate the paint report in both directions The page posted `painted` whatever shell it met, and `notify` is a closed union: every shell installed before this answered it with an error frame, once per mount. The shell now advertises the name in `init.accepts` beside the param clear and the client identity, and the page posts only when it was advertised. The declaration in `ready.reports` stays unconditional, because it is an optional field an older reader strips rather than a new opcode, and because the first `ready` — the only one that matters for the first paint — is sent before any `init` has arrived. The accepts list moves into `bridge-init-frame.ts` beside the grants, which is the module that builds the frame carrying it. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): read the cover's colour instead of asserting its shape Two gate findings on the round-two head. The cover test reached the background through a cast of the style prop; it now reads it through a checked narrowing, so the test proves the shape it depends on rather than declaring it. `use-mobile-web-shell-bridge.test.ts` stopped typechecking when the bridge args gained `onPagePainted`: its harness is a literal, so a new required handler is a missing property. The probe now counts paints and one case spends the counter, which is what a handler wired only to satisfy a type would not do. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * refactor(mobile): move the cached-generation opening out of the reducer `mobile-web-shell-session.ts` crossed `max-lines` after the merge: the refused- update work and the paint handling both grew it. What comes out is one thing — putting a generation already on disk on screen, and deciding whether this route is one that bundle carries. It is the reducer's cache path and its refused- update path both, and it was already three functions sitting together. `step` goes into a module of its own because the two now share it; a copy in each would be two spellings of one transition, and exporting it from either would point the dependency the wrong way. No behaviour moves: the reducer's table tests are unchanged and the page closure is unchanged at 4,211, since neither new module is reachable from a page route. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): drop the previous document's paint when a new one starts A document that replaced a painted one inside the same mount inherited its `pagePainted`, so the cover lifted before the replacement had drawn anything. The native view already reports `loading`; the screen dropped it. It now reaches the reducer as `document-started` and clears the page document state. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): make the declaration case call the frame policy The case compared the name to itself and never called `shellPageFrame`, so it passed for a policy that ignored the declaration entirely. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): report the page's frame from the route screen, not the router Every route screen is behind `import()`, so the wrapper above expo-router commits with a suspense fallback while the chunk is still arriving. The paint report hung there, which uncovered the shell's view over an empty body on a cold chunk. It now hangs on the screen the manifest resolves, layouts excluded. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): retire the readiness wait a replaced document armed `document-started` cleared the page document state and left the flow alone, so the previous document's readiness deadline passed the flow check, read `pageReady` as false and failed a session whose replacement was still loading. The flow moves with the document, for the reason `remounted` already moves it. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): let a departing route screen take its paint report back The report waits two frames, and nothing cancelled the second one, so a screen unmounted in between still told the shell to uncover. The reporter now answers with a take-back the wrapper returns as its cleanup, and the once-per-document latch frees only when a report was cancelled before it landed. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): let the screen that arrived take over a frame still owed A screen committing inside the two frames an earlier one was owed found the latch taken and reported nothing; the earlier screen then freed that latch on its way out and nobody was left to lift the cover. The newest commit now supersedes the pending report, and only a posted one spends the latch. Covers the redirect window with a render check against the pr route, whose target chunk is held open while the document sits on the hub's fallback. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): make the take-over case turn on the take-over The case cancelled the first screen's frame through the cleanup path, so it passed with the take-over deleted. It now leaves that screen mounted and reads the clock: the frame after the replacement commits is the replacement's first, not the one the screen behind it was still owed. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb |