Skip to content

sync: absorb upstream v1.4.215 - #98

Merged
zpyoung merged 469 commits into
mainfrom
zpyoung/auto-sync-fork-with-upstream-stablyai-orca-ru-20260928T1553
Sep 28, 2026
Merged

zpyoung merged 469 commits into
mainfrom
zpyoung/auto-sync-fork-with-upstream-stablyai-orca-ru-20260928T1553

Conversation

@zpyoung

@zpyoung zpyoung commented Sep 28, 2026 •

Copy link
Copy Markdown
Owner
Files Added Deleted Net
Test 873 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​88704 $\color{#cf222e}{\Huge{\mathbf{−}}}$​10340 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​78364
Prod 2127 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​78892 $\color{#cf222e}{\Huge{\mathbf{−}}}$​29641 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​49251

What this does

Absorbs upstream's latest stable release, v1.4.215 (083f583a53e4c74a65acf420eee4ca2e0efa9df1), into the fork. main is on v1.4.207. The earlier v1.4.209 attempt (#92) was closed without merging, so this one sync covers eight releases: 441 upstream commits and 5,923 changed files. This was an unattended sync-upstream run.

  • Resolution: auto-ours+tree. The plain merge hit 42 conflicts, so it was retried with -X ours. Five tree conflicts remained. All five were "deleted by us, modified by them", and the fork's deletion stands (git rm):
    • .github/workflows/track-community-prs.yaml
    • config/scripts/electron-builder-mac-channel-config.test.mjs
    • config/scripts/skill-sharing-release-workflow.test.mjs
    • config/scripts/verify-dev-channel-packaging.test.mjs
    • src/renderer/src/components/native-chat/use-native-chat-live-session-visibility.test.ts
  • Fork commits: 474 (the --no-merges origin/main --not upstream/main v1.4.215 range). All 474 are still reachable at their original SHAs. After this PR the range counts 484: the ownership commit plus nine fix commits.
  • Backup: origin/backup/main-20260928-155359Z points at 9710c6b44b (the old origin/main), confirmed with ls-remote.
  • The previous tag, v1.4.207, is still an ancestor of main, so the merge base is correct.

Ownership resolution (config/scripts/sync-upstream-file-ownership.mjs)

7,803 paths differed: checkout 5,583, remove 203, ours 187, merge-review 1,830.

  • checkout.txt (reset to the tag). The undeclared-edit sweep found two generated files. Both were regenerated rather than restored by hand:
    • src/shared/rpc-contract/rpc-params-catalog.generated.ts: rebuilt with generate-rpc-params-catalog.mjs.
    • pnpm-lock.yaml: pnpm install --lockfile-only changes only packageManagerDependencies. That reflects the fork's pnpm@12.5.1 pin from Add Heimdall watchers: PR sitter and objective runner #97; upstream is still on 12.0.0.
  • remove.txt was applied. The deleted-module import sweep over the merged tree found nothing.
  • ours.txt audit. No exceptions were stale, and none had a four-digit removal (no module split). 35 exception paths changed upstream between v1.4.207 and v1.4.215. Each was three-way merged with v1.4.207 as the base:
    • Merged cleanly (17): release-mac-build.yml, reliability-gates.jsonc, ci-dependency-download-cache.test.mjs, create-draft-release.test.mjs, electron-builder-config.test.mjs, install-electron-package-binary.mjs, remote-wire-compatibility.md, mobile-browser-frameless-stream.test.tsx, filesystem-git-status-staging.test.ts, the four orca-runtime-* OMP files, structured-agent-session-integration.test.ts, terminal-wait-detection.ts, use-native-chat-retained-session.test.ts, and remote-runtime-pty-transport.ts. en-runtime-required.json also merged cleanly and was then regenerated, as its exception reason requires.
    • pr.yml: took upstream's CI rework (perf(ci): reduce queue pressure without paid runners stablyai/orca#23053 and follow-ups): the ready-for-review readiness reuse, the masked outputs, and folding the root-directory and README-link checks into code_paths. Kept the fork's fork_ownership_guard job, its verify wiring, and the changed_count output. The fork's removal of package_windows and wsl_source_changed stands. pr-code-change-scope.test.mjs and pr-workflow-parallelism.test.mjs follow the same resolution.
    • release-cut.yml: the fork keeps its removal of the skill-sharing release gates. Took upstream's -c.publish.releaseType=draft on both Linux commands.
    • electron-builder.config.cjs: kept the fork's owner: 'zpyoung' / repo: 'orca'. Took upstream's releaseType: 'draft' and the three new asarUnpack entries. The fork's mac build already publishes as a draft and asserts that it stays one, so this matches the fork's flow.
    • create-draft-release.mjs: kept both imports.
    • package.json: kept the fork version, test wiring and pnpm@12.5.1. Took upstream's test:bun:profile and the new React Doctor rule levels.
    • Tier-4 files: filesystem-handler-context.ts, orca-runtime-start-tui-idle-visible-read-probe.ts, runtime-terminal-wait.ts, main.css, NativeChatResolvedView.tsx and NativeChatComposer.tsx. Each is covered under the tier-4 review below.
    • Kept the fork's version (fork records): readme-downloads.svg and release-mapping.json. current-manifest.json and snapshot-registry.json were also kept and then regenerated with generate-skill-bundle-manifest.mjs --write, after generate-bundled-skill-guides.mjs --write refreshed the fork-only orca-ask and orca-ledger guides for upstream's new runtime-access-denied stub text.
    • Release-workflow toolchain scan: no pnpm/action-setup or pin changes need replaying.
  • merge-review.txt (seams and features). --verify-residuals flagged 16 seam files. A clean three-way re-merge (no -X ours) of every seam file found conflicts in exactly those files, plus two whose budgets already matched (pr-e2e-gate-contract.test.mjs and NativeChatToolLine.tsx, fixed in c05eb6c011 and e0a3c44cbe). Every conflict was resolved to keep both sides:
    • Split, re-homed: ArtifactListRows.tsx went from 196 to 58 lines (Virtualize artifacts list with reusable component stablyai/orca#22061). The barrel was restored from the tag, and the artifact-passwords seam moved to ArtifactListRow.tsx at the same +2/-3.
    • Split, re-homed: visible-worktrees.ts lost buildVisibleWorktreeOptionsFromState to the new visible-worktree-options-from-state.ts. The workspace activity and review filter context moved there as a new seam (+4/-0), and visible-worktrees.ts drops to +14/-0.
    • -X ours hunk damage, repaired to both sides: check-max-lines-ratchet.mjs (the fork's 1000 threshold plus upstream's mts|cts), cli/specs/index.ts, orca-runtime.ts (ledger mixin plus upstream's constructor invalidators), dispatcher.ts, pty-bridge-session-control.ts (the fork's writeInputAccepted with upstream's simpler onWriteUnavailable), AppBackgroundServices.tsx, NativeChatToolRun.tsx (the fork's category dots with upstream's run sentence), use-native-chat-live-session-pending.test.ts, use-visible-workspace-kanban-worktree-ids.ts, use-terminal-pane-mobile-actions.ts (upstream's mouse-tracking guard plus the dock focus guard), ui-slice-hydration-actions.ts, notification-settings-types.ts, project-groups.ts, and loading-store/store.ts.
    • Budgets re-baselined after re-reading the seam: orca-runtime.ts (+8/-3) and dispatcher.ts (+1/-0).
  • Cross-version tag sweep: upstream's new agent-session-unproven-release-downgrade.unit.test.ts pins v1.4.211, which this remote does not have. No fork release contains v1.4.211 and nothing newer, so the constant is repointed to v1.4.208-rc.0.zy01 (built on v1.4.207) and declared as a passthrough seam. agent-session-record-store.ts, the only module the test imports at that ref, is byte-identical across the two. Its import closure differs only additively (new thread-goal and turn-completion exports). This needs a reviewer's eye.
  • Locale bundles: all 19 fork bundles have all six locales.

Per-sync checklists

Fix commits

  • 47f8521cc1 Adapt the terminal-dock provably-live write to upstream's removal of the agent-session write gate, the hidden-resize clear, and the mainWindow argument.
  • 1bf4b124d3 Drop the OMP test case "rechecks the admitted session fence before the atomic write". refactor(native-chat): remove the unused terminal handoff stablyai/orca#22783 deleted terminal-owned agent sessions and the write gate, so there is no fence left to assert. The only test removed; flagged for review.
  • f84ee51806 Re-import isTerminalWaitWhitespace and readFileSync, which upstream stopped importing but fork lines still call.
  • e975182c10 Heimdall adapter: pass callerSession: undefined and build coordinator with upstream's orchestrationCallerIdentity, per refactor(orchestration): resolve every caller and target to one orchestration party, keyed by the Orca session id stablyai/orca#22555's unified caller.
  • 8d0913ca17 Renderer callers: setActiveTabType(type, worktreeId), the Antigravity trust preset, and the live-session contract in a fork test.
  • 5668b56895 Upstream's new filesystem-download-lifetime.test.ts passes the fork's fifth (diff) cancellation registry. Declared as a seam.
  • e0a3c44cbe The fork's uncategorized tool name follows upstream's group/tool-line hover-group rename, so its hover highlight still fires.
  • c05eb6c011 Drop the retired root_directory_guard from the e2e gate contract's ungated-job check. The fork_ownership_guard exemption stays.
  • 74b89ed416 Lint: route two fork crypto.randomUUID calls through createBrowserUuid, mirroring upstream's adoption in 7d2c399 for the new no-restricted-properties rule.

Local verification gate

Tests are left to this PR's own CI; vitest does not run on the sync host.

PR CI

The first run failed 15 test files across six shards. Every failure reproduced on the sandbox. All but one are fixed, each in its own commit:

  • e1a562cdf4: upstream's new max-lines .mts/.cts cases now use the fork's budgets.
  • 39c0a45105: the fork's project-group store test uses upstream's SQLite test harness. A writable Store now requires a profile-state authority.
  • c0cf3b60b9: the diff-cancellation tests use EventEmitter senders. Upstream's sender-scoped cancellation subscribes to sender events.
  • 2fafdb9864: the dock focus seam no longer throws on a leaf without a UUID. Upstream's new middle-click repro ([Bug]: Middle-click pastes the PRIMARY selection twice in mouse-tracking TUIs (Linux) — #8993's suppression is never armed when mouseTrackingMode !== 'none' stablyai/orca#21762) uses a plain-named leaf.
  • 936445c985: the fork's changelog draft-release cases pass targetCommitish, which is now required.
  • 0634795e84: dropped merged assertions on track-community-prs.yaml and package_windows. The fork removes both.
  • 0e40e4d515: the coloring test now expects the tool result folded into its call row. Upstream no longer renders a separate Result row.
  • 55fc06b6f3: the fork's hosted-review fix-checks launch is profiled in upstream's new agent-launch caller census. The census now counts 13 callers.
  • df71798fa7: the Ledger-tab test reads persisted state through readPersistedStateJson, since state now lives in SQLite.
  • 971ab030b8: the Heimdall adapter test mocks orchestrationCallerIdentity.
  • 50390b8789: upstream's new readiness-reuse tests (pr-ready-check-gate and pr-ready-check-reuse) are adapted to the fork's PR job graph.
  • 234a108e82: the new feed-lifetime test uses the fork's release feed URL.
  • 9f56335436: the live-session test calls loadEarlier with void, as upstream's own test does. await-ing the new promise inside act() hung the test, and the hang cascaded into 23 failures.
  • 1d60092a51: the test sandbox image now ships src/shared/zip-extractor-command.ts. Without it every sandbox image build failed at v1.4.215.

The second run was green except for one failure, which needs a human decision.

Needs attention: serialize-grid-transcript-replay.test.ts × claude-code-ready-cold-start

v1.4.215 adds src/main/daemon/serialize-grid-transcript-replay.test.ts (stablyai#22586). It replays every captured transcript in src/main/runtime/__fixtures__ through the serializer round trip. That directory also holds the fork's Claude cold-start readiness fixture (an exception-owned path). The fixture produces 8 new-fail (I2) checks against an expected 0. The test pins a per-transcript table of known upstream limitations: KNOWN_PREEXISTING_I2_FAILURES = { less: 6, nano: 2, opencode: 5 }.

This is deterministic. It failed in shard 5/8 on both CI runs and on the sandbox. Every other touched test passes (685/686).

It was left for a human because each available fix changes what the gate checks:

  1. Record 'claude-code-ready-cold-start': 8 in upstream's known-failure table. The table's own contract requires verifying against the previous build's addon first, and this adds a fork entry to an upstream exact pin.
  2. Move the fork fixture out of the replay corpus directory, which removes a real transcript from the test's input.
  3. Fix the serializer's I2 behavior on cursor-drawn Claude screens, which is upstream code.

Third CI round

The second run was green except two checks. Both are fixed:

  • f607ab15fe: transcript replay. Upstream's new serialize-grid-transcript-replay.test.ts replays the fork's claude-code-ready-cold-start fixture, and 8 of that fixture's checkpoints fail I2. I checked them the way the table's own comment asks: I built the previous serializer from origin/main with build-serialize-addon-at-ref.mjs and re-ran with ORCA_OLD_SERIALIZE_ADDON. The result was 88 checks: 8 both-fail, 29 fixed, 0 regressions, 0 I1 byte diffs. The previous build fails all 8 the same way, so they are pre-existing serializer limits. I added the fixture to KNOWN_PREEXISTING_I2_FAILURES as a one-line registration seam and left upstream's table line alone.
  • 67fb6a58a9: real IME / Linux X11. Playwright 1.63, which came in with this release, reads the -- forwarded by pnpm run test:e2e -- <spec> as the end of file selection. The X11 step therefore ran all 841 e2e tests instead of 4 and hit the 25-minute timeout; main's scheduled run on 1.59 ran 4. Upstream fixed this on main in fix(ci): preserve focused Playwright file selection stablyai/orca#23270 (dffb3498e2), but neither v1.4.215 nor v1.4.216 includes it. I cherry-picked that commit whole. Its five files are declared as pending-upstream exceptions (see docs/fork-upstreaming.md#focused-playwright-file-selection), to be dropped once a stable tag carries the fix.

Both passed on the sandbox, including upstream's new focused-selection test against the installed Playwright. The manifest checks and the ownership guard also pass.

Jinwoo-H and others added 30 commits September 23, 2026 02:34
…ine module (stablyai#22430)

stablyai#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
…omoted (stablyai#22439)

C30 was promoted to general on 2026-09-23 (selector generation 286). The
same-cap wave now rolls it as a general cell instead of handing it back
isolated, and the shadow gate reads its pool beside C27-C29. Follow-up to
stablyai#22375.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
…ds (stablyai#22443)

The source cell runs the rehome commit. An Asia source pays a cross-ocean
round trip per statement while holding relay_cells row locks every cell
needs, which convoys the fleet database. Selection now drops source cells
outside the director's region before building the decision window, so the
incumbent_region filter shrinks while Asia cells stay valid targets. The
preview counts the same hosts as source-outside-director-region and the
poll summary reports skippedOffRegionSourceCells. Temporary stopgap.

Claude-Session: ced32ebb-7155-4413-adad-1eccd14c2010
* feat(relay): alert on relay cell table lock convoys

Adds a log-based metric and alert for cell-inventory lock holds of at
least 1,000 ms, and a Cloud SQL log metric and alert for relay-only lock
timeout cancels at 20 or more per minute. NOWAIT refusals are excluded:
background sweeps produce about 160 per minute even with rehome paused.

Replayed over 2026-09-20 14:00 to 2026-09-22 15:00 UTC: the hold filter
matches all 93 asia-east2 rehome holds plus 9 director holds, and every
one of the 88 cancel burst minutes overlaps an asia-east2 hold.

Claude-Session: ced32ebb-7155-4413-adad-1eccd14c2010

* fix(relay): page only on cell lock holds; director holds stay visible

Director holds of 1-2.5 s recur several times a day with rehoming paused,
and pausing rehome does not stop them. The paging hold policy now selects
role=cell samples only; a separate policy with no notification channel
keeps director holds visible. The burst documentation no longer claims no
burst happens while paused, and the runbook points a burst with no cell
hold at the director policy.

Replayed cell-only: 93 of 93 asia-east2 holds, 0 director holds over
2026-09-20 14:00 to 2026-09-22 15:00 UTC; 0 from then to 2026-09-23 07:30.

Claude-Session: ced32ebb-7155-4413-adad-1eccd14c2010
…home commit (stablyai#22449)

* fix(relay): lock only the target cell row, last and NOWAIT, in the rehome commit

The idle-rehome commit runs on the source cell. From an Asia cell each
statement is a cross-region round trip, and the transaction locked every
relay_cells row plus every runtime, capability and safety row before about
twenty more statements, so each Asia-source rehome held the whole fleet's
cell rows for ~3.6 s and every reconnect, renewal and placement queued or
timed out behind it.

The commit now reads the cell inventory and the runtime, capability and
safety tables unlocked, keeps the control and worker rows locked (now
NOWAIT), and takes one cell lock: the target row, in a single statement
that locks it NOWAIT, re-checks enabled, general admission and capacity,
and reserves the units, issued as the last statement before COMMIT. A
target that changed admission, filled up, or is locked by another writer
rolls the whole commit back and answers deferred (candidate-ineligible).

The hold is sampled under a site label, so cellInventoryHoldMsMax still
sees rehome holds and rehomeTargetRowHoldMsMax reports them apart.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* fix(relay): fail closed on the rehome target-row lock clause

The target-row statement now carries FOR UPDATE ... NOWAIT unless the
dialect is explicitly SQLite, so a wrapper that omits the optional dialect
can no longer run the reservation unlocked. Test wrappers and the fault
injection entry forward the dialect they wrap.

The latency test also probes the admission and region tables at every
round trip; only the target's admission row may be locked, and only
before COMMIT. The runbook notes that an Asia-sourced commit holds the
rehome control row for about 6.5 s, so a pause that fails once is retried.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
…tablyai#14287)

Fixes stablyai#22049

Co-authored-by: Nurdaulet Bolat <204565446+nvimq@users.noreply.github.com>
…tablyai#22457)

* fix(cloud-verify): scan HEAD instead of all history

- Gitleaks now verifies only the checked-out revision
- Reduces scan scope and improves verification workflow performance

* ci(cloud-verify): clarify that Gitleaks scans HEAD-reachable history
… creates (stablyai#22291)

* feat(agent-launch): let a caller reserve the pane its terminal launch creates

* fix(agent-launch): refuse a launch whose reserved pane is already live

* fix(agent-launch): refuse a live reserved pane before it is revealed

The live-pane refusal used to fire in the executor, after createTerminal
had already issued a handle, published the mobile snapshot and revealed
the tab. The reveal re-registered a fresh launch config over the running
agent's. agent.launch now passes requireFreshPane with a reserved pane,
and createTerminal throws AgentLaunchPaneAlreadyLiveError as soon as
spawn reports it attached to a live pane. That is before any handle,
snapshot or reveal. The spawn reattach itself is the one terminal.create
already uses, so the live PTY is never killed, and the stable-pane
create claim is still released in finally. The isReattach plumbing
added to the launch factory for the old check is gone.

A replay-safe launch refused this way on an existing workspace now
records a failed ledger row, the same way a name collision does.
Before, the row stayed claimed, so every retry got
agent_session_operation_unknown. agent.launchReplay passes the code
through. On create-worktree the workspace already exists when the
terminal is refused, so the row stays unknown. The code is added to
the runtime passthrough list so callers can branch on it.

The pane key is now in the replay fingerprint, deliberately. It is not
placement: group, anchor and focus still stay out of the request and
out of the ledger. It is identity. It is written into the pane's PTY
environment and names the tab the caller has placed. A retry that
reserved a different pane is therefore a different request. Replaying
the first answer would return a key the new reservation can never
find. This matches terminal.createAgentSession, which also fingerprints
its tab and leaf ids. The key is only folded in when present, so every
existing digest is unchanged, and a test pins that.

The wire schema now refuses a tab id the runtime would not adopt as
sent: one with surrounding whitespace, which the runtime trims, and one
longer than 512 characters, which the spawn reservation does not key
on. It reuses the tab-id schema that Placement uses.

* test(agent-launch): pin that a refused live pane issues no handle

The refusal test named handle issuance but only asserted the reveal, so a
throw moved to just before the reveal would still pass. Assert no terminal
is registered, with the attach test as the positive control.
…et it from goal mode (stablyai#22377)

* feat(native-chat): show a Codex chat's goal above the composer and set it from goal mode

Structured Codex chat now treats the thread goal as session state: a banner above the
composer shows the current goal (pursuing / paused) with clear, pause/resume and expand;
/goal enters a goal mode whose send calls thread/goal/set; the objective is journaled as a
user message marked as sent as a goal. The banner is derived from the journaled goal rows,
which Codex's resume snapshot refreshes, so a reopened or adopted chat shows its goal.

Fixes STA-8159

* fix(native-chat): replace a recorded goal by clearing first, and recover a lost goal-change response

- A set while the journal records a goal (any status) clears it before setting,
  so the new goal starts with its own time and token counters instead of
  rewriting the old goal's objective in place.
- The threadGoal plan answers an unknown outcome from the goal the journal
  records and reruns otherwise, so one request timeout no longer refuses every
  later Clear/Pause/Resume as unknown for the mounted session.
- The goal-mode chip says "Exit goal mode"; "Clear goal" stays the banner's
  action on the provider goal.
- A typed bare /goal on Enter enters goal mode, the same as picking it.
- The renderer reads the goal off the tail of its ordered snapshot; the host's
  unordered map keeps the by-sequence reader.
- Drop the composer's duplicate in-flight guard; the goal controller already
  serializes changes.
- Pin that a counter-only revision reaches a subscriber's live page under its
  original sequence.

* fix(native-chat): keep a bare /goal inside goal mode as the entrance, and pin goal delivery and serialization

- A bare `/goal` submitted while already in goal mode re-enters the mode instead
  of setting a goal whose objective is the literal text "/goal".
- The counter-only revision pin now drives the host's own event sink bound to a
  real journal, so it goes red when the publish after a lifecycle transition is
  dropped; the previous fake sink never published.
- Pin that a set which threw after journaling its objective puts that objective
  back exactly once when the ledger reruns the same operation id.
- Cover the goal controller hook: absent without host support, the loaded window
  wins over the host's answer, a second change while one is unsettled answers
  false without a request, and a refused change frees the next one.

* fix(native-chat): resume a blocked or usage-limited goal, and keep goal-mode drafts honest

- The goal bar offers Resume on a blocked or usage-limited goal, which the
  provider resumes exactly as it resumes a paused one; a goal whose token budget
  is spent still offers only Clear. The rule lives beside the other goal facts
  in shared code so every reader answers it the same way.
- A `/goal <text>` typed inside goal mode sets the objective `<text>`, as it
  does outside goal mode, instead of a goal whose objective is the literal
  command.
- Setting a goal is a host round trip; a draft edited while it was in flight is
  no longer wiped when the goal lands, matching every other host command.
- Pin that a lost status-change response is read as applied only when the
  recorded goal is in that status, that a cleared row in the loaded window
  outranks the host's earlier answer, and that the PTY lane is untouched.

* fix(native-chat): keep the load-older anchor on the loaded window when a live revision lands below it

A live revision of a row keeps that row's original sequence. When the row is
older than the client's loaded window, the shared reducer merged it in and it
became the load-older anchor, so paging `before` it skipped every row between.
A goal's counter-only revisions during a long goal turn reach any client that
attached after the goal row left its window, so a reopened chat lost rows on
scroll-back.

The reducer now admits live rows only at or above the window's oldest row
while older rows remain on the host; the journal keeps the revision and the
page reader serves it once the window reaches the row. With nothing older on
the host the window is the whole journal, so a row below the head is admitted
as before.

Also drain accepted provider events before a goal set reads the journal to
decide whether it replaces a recorded goal.
…o the turn's end (stablyai#22432)

* fix(native-chat): keep one live tool-run header from a call's start to the turn's end

The collapsed tool run's header was two elements, one for "a call is running"
and one for "nothing is", chosen call by call. Every call start and end
remounted it, the count disappeared while a call ran and came back one
higher, and a call that finished inside a frame still bought the whole swap.
That is the 42→43 flicker in the report.

The header is now one element whose live state belongs to the turn, not to
any call: it stays live from the run's first call until the agent moves past
it (prose, a further run, or the turn's end), and settles in place. While
live the sentence speaks in the present tense and counts the call in flight
("Running 3 commands"), with the latest call's command beside it as a muted
preview; once settled it reads as before ("Ran 3 commands ✓"). The category
glyph is the run's in both states, and the completion mark only appears once
settled, so nothing pops between calls.

Which run is live is derived where the transcript is sliced into rows: the
last row that speaks or acts is the trailing one. A reasoning aside after it
leaves it live; an answer or a further run settles it.

Present-tense forms for the ten sentence categories are added to the shared
copy and the English catalog. The transcript-file lane, which renders with
the structured activity UI off, is unchanged.

* fix(native-chat): settle a run blocked on the reader, keep it live past an approval

- A run whose question is awaiting the reader's answer no longer pulses
  "Reading 1 file" while the agent is blocked; it falls back to its calls.
- An approval's receipt no longer moves past the run above it, so the call
  it just approved reads as running while it runs.
- The header button is the live region, so the count is announced too.
- Drop the unused live option and record from the shared English sentence;
  nothing renders it yet.

* fix(native-chat): stop the settled run's check from fading in on every mount

Windowing remounts settled rows as the reader scrolls, and a restored transcript
mounts them all at once, so the fade replayed where nothing had changed. Also
pin that the live header counts the next call on the same element.
* feat(sidebar): copy workspace name from context menu

Add Copy Name directly below Copy Path in the workspace context menu. It
copies the name through resolveWorktreeDisplayName, the renderer mirror of
main's mergeWorktree fallback (custom name, then branch, then folder), so the
copied text is what `name:` worktree selectors resolve against and a cleared
custom name no longer copies `undefined`.

The context-menu model now spreads the command handlers instead of listing
each one twice, which keeps it under the file-length limit.

Fixes stablyai#21980
Linear: STA-8068

* refactor(sidebar): rename Copy Name to Copy Worktree Name

- Clarify that this copies the worktree display name, not the path
- Update all locale strings and i18n keys
- Rename test file to match

* test(sidebar): cover folder workspace copy worktree name

---------

Co-authored-by: Seongho Bae <me@seonghobae.me>
…perator (stablyai#22518)

The selector membership check capped cell ids at c29, so enable failed
closed with "selector membership is invalid" once c30 went general.
Accept c1-c99 with no leading zero.

Claude-Session: ced32ebb-7155-4413-adad-1eccd14c2010
…-edit cells (stablyai#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 stablyai#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 172b4f7. The [n] count is always
shown and never moves; the run button sits above it in the same gutter
column, revealed on hover or focus from an invisible slot that keeps its
box. While a cell runs, the slot shows a spinner and the count reads [*].

* refactor(ipynb): give notebook code its own preview, keep MonacoCodeExcerpt unchanged

172b4f7 bent the shared MonacoCodeExcerpt (used by PR comment code
context) to the notebook's layout, changing that surface's font, tracking
and highlight tint. The excerpt's rendering is now byte-identical to main;
only its colorizer is extracted as useMonacoColorizedLines. The notebook
renders its own rows from CODE_LAYOUT, the same metrics its Monaco editor
uses, so activation stays pixel-stable.

* fix(ipynb): open the cell editor with the caret where the user pressed

Monaco mounted with its caret at 1:1 and focus ran occurrence highlighting
on that first token, so clicking any line lit up every 'import'. The press
on the preview now maps to a model position (its rows mirror model lines)
and Monaco opens there before focusing; keyboard and markdown activation
open at the end. Occurrence highlighting stays on, as in VS Code notebooks.

* feat(ipynb): put the execution count on top and the run button below it

The [n] count now shares the first code line's box (the gutter mirrors the
code surface's border and padding, and the count is one line tall); the
run button slot sits beneath it and stays reserved while hidden.

* fix(editor): re-colorize code excerpts when the theme changes

Monaco's colorizer emits theme-specific token classes, so excerpts colorized
under one theme kept that theme's token colours after a switch (dark-theme
keywords rendered green on the light theme). Affects notebook previews and
PR comment code context.

* fix(ipynb): mount cell editors before paint so a click swaps in seamlessly

@monaco-editor/react waits on an async loader, shows "Loading..." and builds
the editor inside a display:none container. Every click on a cell therefore
painted a placeholder, then an editor laid out at 0x0 (one uncoloured line),
and the caret guess ran against the static preview, falling back to the end
of the cell whenever the press missed a text row (padding, gaps).

The cell editor is now created with monaco.editor.create in a layout effect:
sized, tokenized (restoring its view state marks the visible lines stable)
and placed before the first paint. Monaco hit-tests the press itself with
getTargetAtClientPoint, so the caret lands on the nearest position for text,
trailing whitespace, blank lines and padding; keyboard and markdown
activation open at the start. Indent guides stay off to match the preview.

Word and selection occurrence highlighting are off in cell editors, since
clicking a word lit up every other occurrence like a search.

Also drops wiring the rewrite made dead: the per-editor save shortcut (the
notebook's capture handler already owns Cmd+S), the callback refs, the
onSaveRequest prop, and the toolbar's positioning wrapper.

* fix(editor): follow OS theme flips when colorizing code excerpts

useMonacoColorizedLines read the theme once from settings, so a 'system'
theme flipping with the OS never re-colorized. It now uses
useDocumentDarkTheme, like the rest of the notebook; rendering is unchanged.

* fix(ipynb): let Escape close an open widget before it leaves the cell

The cell's Escape command had no context, so Escape with suggest, find or
parameter hints open closed the whole cell editor. It now only fires when
none of those widgets is visible.

* fix(ipynb): keep HTML output frames at an opaque origin

The output frame had allow-same-origin only so the viewer could measure its
content height. Notebook HTML is untrusted, so the frame is now sandbox=""
(opaque origin, no scripts) at a fixed height that fits a pandas head() or
describe() table and scrolls anything larger.

* refactor(fonts): move buildFontFamily into lib/monospace-font-family

lib/editor-font-zoom imported it from a terminal-pane component module. The
monospace fallback chain now lives in lib, and every importer, including the
terminal, points there. Pure move; the tests move with it.

* fix(editor): pass comma-separated editor font stacks through unchanged

resolveEditorFontStack quoted a value like "JetBrains Mono, monospace" as
one family name, which no font matches. A comma list is now used as-is;
only a single name gets the quoted fallback chain.

* chore(i18n): drop the Korean override for a removed notebook key

---------

Co-authored-by: maxidiazbattan <maxidiazbattan@gmail.com>
Co-authored-by: m4air <m4air@m4airs-Air.localdomain>
Co-authored-by: Andre Ambrósio <56239028+sirambrosio@users.noreply.github.com>
…yai#22370)

* fix(native-chat): underline only file links a click can act on

A chat message could underline a bare file name such as `deck.md` that
resolved nowhere, and clicking it did nothing, so it read as a broken link.

- Inline code and quoted text become file links only when they name a path
  (contain a `/` or `\`), matching plain prose; a bare file name stays plain code.
- Every file link click now answers: it opens, or says the file was not found,
  that the host could not be checked, or that the path could not be resolved.
- Explicit links like [x](README.md:5) route as files, and linked text keeps
  `#`, `?` and `%XX` literally instead of re-parsing them as URL syntax.

* fix(native-chat): wrap the parsed file location so file URIs in chat text still open

Linkified prose, quoted text and inline code wrapped their display text, which the
literal wrapped-href route no longer URL-parses, so file:///... resolved as a relative
path under the worktree. Wrap pathText[:line[:col]] from the parsed link instead.
…shape (stablyai#22312)

* test(file-search): pin request-key listings against the real runtime shape

* test(file-search): pin intermediate renders and late remote answers

Strengthen the stale-answer guard to assert every intermediate render reads as loading (null), add a late-answer drop case, make the tab-entry loading pin non-vacuous, and correct the e2e comment for local listings.

* test(file-search): keep only the non-duplicate runtime-listing pins

Drop the remote projection cases already covered at the hook level, collapse
the classifier integration to the loading pin, drop the local-only rapid-edit
e2e, and fix the brittle README absent-file assertion that failed CI.
…rn (stablyai#22552)

* fix: explain Xcode-blocked Git once in the sidebar and rescan on return

* refactor: simplify Xcode toolchain banner to one classifier and a stateless rescan

* refactor: fold banner copy into one table and cover SSH/runtime gating
…22548)

stablyai#22399 admitted an OpenCode 2 form.created as a pane blocker only when
metadata.kind === "question". On v2.0.15 that is an allow-list on a field
with no contract: packages/schema/src/form.ts declares Metadata as an open
Schema.Record and metadata itself as optional, and the public
POST /api/session/:sessionID/form endpoint lets any client raise a real
blocking form on a real session with no metadata. Orca dropped those, so
the pane painted no blocker while OpenCode waited forever.

Invert the default. Every form whose owner is a real session blocks;
only a form owned by the "global" MCP-elicitation sentinel is dropped,
because that owner is not a session and never goes idle, so its blocker
could not be retired. That also restores websearch.provider as a blocker:
it carries the real context.sessionID, session idle retires it, and while
it is pending the agent is genuinely stalled on the user.

Resolution is unchanged: clearAttentionForResolution keys on the exact
form id plus source session, so a resolution for a dropped form matches
nothing and cannot retire a live blocker.
…OTA phase C follow-up) (stablyai#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
… chat (stablyai#22387)

* feat(native-chat): one shell-environment setting for every structured chat

Structured Codex chats started from the login-shell environment, while
structured Claude chats started from Orca's own process environment, so a
variable exported in .zshrc reached one and not the other. Both now start
from the same base, chosen by a new setting:

- on (default): the whole login-shell environment, as a terminal gets
- off: Orca's environment plus PATH, locale, SSH_AUTH_SOCK, and the
  variable names the user lists

The setting is re-read each time a chat starts or resumes. It is shown
only when Chat UI, the Chat UI default view, and structured native chat
are all on. Terminal-backed chat is unchanged.

* fix(native-chat): normalize the shell-environment settings when a profile loads

A hand-edited settings file could store the variable list as something other
than an array, and the structured runtime called `.filter` on it per launch, so
a malformed value failed every structured chat create and resume, and the
settings pane render. Normalize both keys where the profile loads, the same way
the other array settings are, through one shared normalizer the runtime policy
also uses. Also pin that an uncommitted name draft survives an unrelated
settings re-render.

* fix(native-chat): keep the pinned account as the only source of a structured chat's Claude home

The session record owns which Claude home a structured chat uses, and the
acquisition pin (claudeConfigDirEnvPatch) is the only emitter of
CLAUDE_CONFIG_DIR, compared against what the child would otherwise inherit.
With the login-shell snapshot as the inherited base, a CLAUDE_CONFIG_DIR
exported only in a shell rc flipped that comparison and produced an explicit
pin to the CLI default home, which moves the CLI off its default Keychain item.

Drop the inherited CLAUDE_CONFIG_DIR in the Claude launch resolver before the
pin runs, as Codex already does for an inherited CODEX_HOME. A configured
per-agent overlay still passes through, since the record already honors it.

* fix(native-chat): drop Orca's own CLAUDE_CONFIG_DIR from a structured Claude child too

The process spawner merges Orca's process env under the launch env, so a
CLAUDE_CONFIG_DIR exported to Orca itself reached the child around the launch
resolver's drop and unseen by the account pin. One helper now strips it from
both inherited bases. Also declare the two shell-environment settings on the
runtime store contract and add the six new strings to every locale catalog.

* feat(native-chat): add shell variables one at a time with a removable list

* fix(native-chat): return focus to the name input after removing a shell variable

* fix(native-chat): use a neutral placeholder for the shell variable input

The empty input showed a grey HTTPS_PROXY as its placeholder, which reads as a
saved value, especially right after that exact entry is removed from the list.
Use "Variable name" instead, in every locale catalog.
…rawn (stablyai#22528)

* WIP

* WIP2

* fix(browser): only pixel-capturing commands wait for the page to be drawn

Every targeted browser command used to take an automation-visibility lease,
which waits for two desktop-window animation frames (capped at 2 s). When the
desktop window is minimized or throttled, that wait always hits the cap, so
each phone tap or agent command stalled for up to 2 s (STA-8024).

Input, page JavaScript, layout and accessibility snapshots all work on a
hidden page; only pixel capture needs it drawn. The lease is now opt-in via
`needsPaint` on the two commands that can capture pixels through this path
(`exec` passthrough and `pdf`); `ensureVisible` is removed. Screenshots keep
managing their own lease.

(Commits 24821ae and 07b4a09 carry this change under WIP messages.)

* refactor(browser): drop the session-recreate option only no-lease commands used

Only exec and pdf take the automation-visibility lease now, and both create an
agent-browser session, so the lease's re-registration path always recreates
the session. Remove the `recreate` option from restartSessionForTarget and the
options passthrough that fed it. Also fix the keypress test comment that still
counted the lease's extra page lookup, and note that screenshots lease inside
the screenshot lock.

* fix(browser): resolve a command's page when it runs, not when it is queued

Enqueue resolved the page's guest webContents and created its session up
front. Before this PR every command re-resolved after its lease, which hid
that; now a command queued behind exec/pdf, whose lease can re-register the
page with a new guest webContents, would run against the old one.

Enqueue now only picks the page (and so the queue). The webContents lookup
and session setup move into the queued job and run when the command starts,
after the lease for exec/pdf.

The lease path's refresh collapses into that step. Resolving after the lease
already sees a re-registration that happens during the lease, so the only
thing left from the refresh is the case where the page's existing session is
bound to an older guest: that session is restarted on the new one (keeping
intercept routes) and the active-tab pointers move with it. That check now
covers every session-backed command, not only leased ones.

Admission is still checked at enqueue and again when the job starts.

* refactor(browser): drop the session-restart reconcile no page can reach

Every renderer re-registration of a page with a different guest goes through
browser:registerGuest, which calls onProcessSwap in the same tick. That
destroys the page's session and rejects its queued commands, so by the time a
queued command runs, no session can still be bound to an older guest. The
reconcile in the queue and restartSessionForTarget, its only caller, are
removed along with the two tests that called it directly.

A queued command now resolves the page's current guest, ensures its session,
and runs. The re-registration tests now report the swap through
onProcessSwap, as production does: commands queued behind the swapping lease
are rejected with browser_tab_closed, the leased pdf runs on the new guest,
and intercept routes survive the swap.
…e parent (stablyai#22563)

Capped search rows are slices of the transcript. Own each row with the shared copier before buffering it, and keep the heap regression from stablyai#22378.

The sidebar test harness now includes the worktree listing map the toolchain banner reads, so that rerender no longer crashes.

Co-authored-by: fancivez <fancivez@gmail.com>
…r tests (stablyai#22567)

The legacy worker terminal recovery retry re-arms itself on a 1s..30s backoff
for as long as a dispatch stays deferred. Nothing in the runtime suite ever
resolves one, so a single test armed a loop that kept re-running recovery --
and the worktree scans it issues -- through the shared `listWorktrees` stub for
the rest of the file's run, landing inside whichever test was executing when the
timer fired. That is what made `lineage-and-scan-cache-part-03`'s per-repo TTL
test see 4 scans instead of 3 only under CI load.

Give the controller a `cancelAllRetries`, track controllers while they have a
timer armed, and cancel them from the shared runtime test lifecycle reset.

Measured over the whole 1299-test file with an afterEach probe: 25 stray
post-test `listWorktrees` calls before, 0 after.
…terminal launches with the session picks (stablyai#22523)

* feat(agent-launch): let a caller reserve the chat session and carry session picks to a terminal launch

* fix(agent-launch): keep a caller-minted session id named for its agent, and mint the fallback the same way

* test(mobile): model the older host from the launch fields, not the refined schema

* docs(agent-launch): describe the reserved session id as conversation identity, not placement

The caller mints the session id so it knows which conversation it
started; tab placement is not keyed on it. Also puts the terminal
surface's doc comment back on createTerminalSurface.

* docs(agent-launch): say a terminal launch reads the session picks on the wire contract

The `sessionOptions` field doc still said a terminal launch ignores them, which this branch changed.

* fix(agent-launch): check a reserved session id's token after the agent name, not the whole id

A hyphenated agent name failed the one-token check, so any session id for such an agent was
refused at the wire, while every other agent without a chat has its id ignored on the terminal.
…seam (OTA phase C follow-up) (stablyai#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
…s draw one device pixel (OTA phase C follow-up) (stablyai#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
* fix(native-chat): dock the task strip on the goal tab

The background-task strip was the full width of the message box, so it
did not share an edge with the narrower goal tab underneath it. When a
goal is showing, the strip now uses that tab's width and keeps a square
bottom, and the goal tab's top stays square so the two sit on each other.

* fix(native-chat): derive the task strip and goal tab seam from adjacency

The strip and goal tab were each told by the chat session whether the
other was showing, through two flags that had to match what actually
rendered. The session also re-derived the goal tab's own visibility rule
to compute one of them.

Now each bar styles its side of the seam from the DOM: the strip takes the
goal tab's width and drops its bottom corners and shadow when the goal tab
is its next sibling, and the goal tab drops its top border and corners
when the strip comes right before it. The flags and the duplicated goal
visibility check are gone, so the seam cannot disagree with what renders,
and anything placed between the two bars falls back to the separate look.
…phase C follow-up) (stablyai#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
v1.4.215 removed the agent-session PTY write gate (d443320), the hidden-resize
output clear (8fa5883) and the mainWindow argument to the write-event check
(9700030). The terminal-dock acknowledged write mirrors writePtyInput, so it
takes the same shape upstream's writePtyInput now has; behavior is unchanged.
…oved

d443320 deleted terminal-owned agent sessions and the agent-session PTY write
gate with them, so 'rechecks the admitted session fence before the atomic write'
asserts a check the runtime no longer performs and cannot pass without restoring
upstream's removed subsystem. The OMP atomic-submit cases are unchanged.
v1.4.215 moved isTerminalWaitWhitespace's last upstream use into
antigravity-terminal-readiness and stopped importing readFileSync in
persistence-ui-state.test; the fork's Claude prompt detection and Ledger-tab
persistence test still use both.
stablyai#22555 made callerSession a required key on run-scope and mailbox checks and
replaced startLocalWorker's coordinatorPane with a coordinator identity. The
watcher is a terminal-handle caller, so it passes no session and builds the
identity with upstream's orchestrationCallerIdentity from the same handle/pane.
setActiveTabType now takes the worktree id, Antigravity joined the trust-preflight
presets, and NativeChatLiveSession gained olderHistoryGeneration with a
promise-returning loadEarlier.
…oad test

filesystem-download-lifetime.test.ts is new in v1.4.215 and builds the handler
context with four registries; the fork's pending-upstream diff cancellation adds
a fifth. Declared as a passthrough seam, and write-input/persistence-ui-state
budgets re-baselined for the two fixes above.
v1.4.215 restricts crypto.randomUUID in the renderer (no-restricted-properties),
since it is missing over plain-HTTP Remote Web. Mirrors upstream's adoption in
7d2c399: createBrowserUuid calls randomUUID wherever it exists.
… tool name

v1.4.215 renamed the tool row's hover group to group/tool-line, so the fork's
uncategorized tool name kept a bare group-hover that no longer fires. Taken
from upstream's own NativeChatToolLine change in the same release.
…ntract

v1.4.215 folded root_directory_guard into code_paths; -X ours kept the fork's
three-way condition naming it. The fork_ownership_guard exemption stays, and
its seam line and budget move to the resolved condition.
@greptile-apps

greptile-apps Bot commented Sep 28, 2026 •

Copy link
Copy Markdown

RetriggerConfidence Score: 4/5

[High risk] Upstream sync absorbs infrastructure, CI, and cloud deployment changes.

The PR should not merge until the fork's structured-session composer connects the new goal-mode behavior.

Summary

The PR absorbs upstream v1.4.215, including changes to cloud services, packaging, CI, and native chat, while replaying fork-owned integrations.

  • The fork composer adopts the new goal and context display props without connecting them to its live structured-session path.

Reviews (1) · Last reviewed commit: "fix(sync): drop the retired root_directo..."

…s budgets

v1.4.215 added .mts/.cts and installed-oxlint cases pinned to upstream's
300/400/600/800 budgets; the fork's .oxlintrc.json already applies its
500/600/800/1000 budgets to those extensions, as the file's existing seam does.
…rness

v1.4.215 made a writable Store require a profile-state authority; the fork's
test now constructs through createSqliteTestStore and closes stores after each
case, like upstream's own persistence tests.
…ders

Upstream's sender-scoped cancellation now subscribes to sender lifecycle events,
so a bare { id } sender throws; the fork cases use the EventEmitter senders
upstream's own cases in the same file already use.
Upstream's new middle-click repro test drives the mobile-actions seam with a
plain-named leaf, and makePaneKey throws on non-UUID leaves. The seam now asks a
leaf-keyed bridge helper that degrades to not-docked instead of throwing.
… cases

v1.4.215 made targetCommitish required (stablyai#21835); the fork's three changelog-body
cases predate it.
The ours.txt three-way merge carried upstream's new track-community-prs and
package_windows cache assertions into two fork-owned contract tests; the fork
deletes that workflow and has no Windows PR lane.
v1.4.215 stopped rendering a separate Result row; the coloring test keeps its
one-glyph assertion and checks the result folds in.
…tream's caller census

v1.4.215's census requires every launchAgentInNewTab call site to carry a
profile the behaviour suites replay. The fork's hosted-review copy is profiled
like upstream's fix-checks caller and spread into the table as a seam.
The store now persists to the profile-state database, so the fork case reads
through upstream's readPersistedStateJson instead of the JSON file.
pr-ready-check-gate/reuse are new in v1.4.215 and assume upstream's job set; the
fork's verify also requires fork_ownership_guard and has no package_windows or
wsl_source_changed. Declared as seams.
The replayed stablyai#22541 contract makes loadEarlier return the page promise; awaiting
it inside act() hung on reads the test resolves later, and the hang cascaded
into 22 later cases. Uses upstream's own void-call form.
…ow imports

v1.4.215's install-electron-package-binary re-exports from src/shared, which the
image context and COPY list omitted, so every sandbox image build failed.
…alizer diffs

Upstream's new transcript-replay test (stablyai#22586) replays every fixture in
src/main/runtime/__fixtures__, including the fork's claude-code-ready-cold-start
capture, and pins each one's I2 new-fail count to KNOWN_PREEXISTING_I2_FAILURES.

Verified the table's own way: with ORCA_OLD_SERIALIZE_ADDON built from origin/main
(v1.4.207), the fixture scores 88 checks, 8 both-fail, 29 fixed, 0 regression,
0 i1-bytes-differ. All 8 are pre-existing serializer limitations, not regressions.
…yai#23270)

Playwright 1.63 (taken with v1.4.215) reads the forwarded `--` in
`pnpm run test:e2e -- <spec>` as ending file selection, so the real-IME X11
step ran all 841 e2e tests and timed out at 25 minutes. Cherry-picks upstream's
dffb349, which is on upstream main but in neither v1.4.215 nor v1.4.216.

Declared as pending-upstream exceptions so the next sync does not reset these
files to a tag that still has the bug; drop them once a stable tag carries it.
Also declares the transcript-replay registration seam.
@zpyoung
zpyoung merged commit ffe35fc into main Sep 28, 2026
100 of 101 checks passed
@zpyoung
zpyoung deleted the zpyoung/auto-sync-fork-with-upstream-stablyai-orca-ru-20260928T1553 branch September 28, 2026 17:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.