Commit Graph
8764 Commits
Author SHA1 Message Date
Jinjing b60df2e3d6 Revert "feat(workspace): set project location from the create-worktree host p…" (#14912)
This reverts commit e4e54a17d0.
2026-08-16 10:41:39 -07:00
Jinjing 763b1febeb Revert "feat(skills): add private bundle sharing (#14401)" (#14913)
This reverts commit 757fae28d7.
2026-08-16 10:39:57 -07:00
Jinjing 1da1bdc01c Preserve OpenCode session across command completion, control SessionStart emission (#14866)
* Preserve OpenCode session across command completion

- Add session start events and launch token tracking to establish session boundaries
- Defer retiring launch authority until OpenCode process actually exits, not just when a command finishes
- Fence previous tokens after restarts to prevent status updates from stale sessions
- Maps SessionStart as a session boundary for proper turn/state management

* Emit SessionStart only from OpenCode, not mimo-code

Restrict SessionStart lifecycle events to OpenCode exclusively. Mimo-code no longer emits SessionStart, as it should rely on OpenCode for session boundary signals. This prevents duplicate lifecycle events that could interfere with pane authority tracking and session state management. Also tighten foreground process result validation to reject stale results after title observation changes, fixing a race where a delayed foreground read from a previous cycle would incorrectly retire authority.
2026-08-16 10:01:27 -07:00
Neil e4e54a17d0 feat(workspace): set project location from the create-worktree host picker (#14868)
* feat(workspace): set project location from the create-worktree host picker

Hosts that still say "Project location not set" now get an inline Set location action. It opens a nested dialog over Create worktree so the in-progress form stays put.

* fix(workspace): replace unset-location status copy with a button

Drop the redundant "Project location not set" caption and show a Set project location action with a hover tooltip instead.

* refactor(workspace): tighten the set-project-location dialog

- reuse CreateProjectParentBrowser instead of a second host-filesystem browse view
- drop ProjectLocationBrowseTarget; parseExecutionHostId already models it
- single setLocation path in RunTargetCombobox (row, button, Enter)
- memoize the default clone URL instead of scanning on every store update
- fix missing required props in the new composer-card test

* fix(workspace): close the correctness gaps in set-project-location

- Escape in the host browser now backs out to the form instead of dismissing
  the dialog and discarding the half-filled path/clone URL. Radix dismisses
  from a document-capture listener, so only preventDefault can stop it.
- Drop the stopPropagation guards: window-capture (the composer's Escape
  handler) already ran by then, so they never protected it — the nestedDialogOpen
  gate does. They did silently kill RemoteFileBrowser's own key handling.
- Hide Set project location for host-local repo:<id> projects (folder projects,
  git repos with no remote). Linking on another host matches by project identity,
  which those have none of, so the call could only ever toast an error.
- Drop a standalone placeholder setup once a repo projection covers the same
  project+host, restoring the (projectId, hostId) uniqueness invariant. Setting a
  location on a host with a pending setup was leaving a ghost that sorts first and
  reads back as 'not set up'.
- Existing-folder submit label matched a catalog string reading 'Importing...'

* test(composer): follow the renamed local in the host-retarget source assertion
2026-08-16 03:30:47 -07:00
Jinwoo HongandE2E Test 757fae28d7 feat(skills): add private bundle sharing (#14401)
Co-authored-by: E2E Test <e2e@test.local>
2026-08-16 02:36:18 -07:00
Jinjing 1b6d2403cb ci: run full e2e against the daily cut commit (#14870)
After a live daily publish, dispatch e2e.yml at the cut SHA. Detached on
purpose so a red suite cannot fail or delay the signed daily.
2026-08-16 01:57:01 -07:00
Jinjing 5c56bfb28b ci: run the daily macOS build 4 hours later (#14869)
The 14:15 UTC cut is too early (6:15am PST / 7:15am PDT). Move it to
18:15 UTC so dailies land late morning Pacific instead.
2026-08-16 01:52:58 -07:00
Neil fd1dba9db9 fix(daemon): validate spawn cwd asynchronously so one dead share cannot freeze every terminal (#14848)
* fix(daemon): validate spawn cwd asynchronously so one dead share cannot freeze every terminal

createOrAttach validated the working directory synchronously on the daemon's
only thread. Measured on Windows 11 + Ubuntu-24.04:

  existsSync on an unreachable UNC share   21,022 ms
  wsl.exe probe, cold distro                1,266 ms
  wsl.exe probe, warm distro                   59 ms
  existsSync/statSync on healthy \\wsl.localhost  4 ms / 1 ms

A single unreachable share therefore blocks the whole RPC loop past the
client's 30s request ceiling, so every other terminal stalls behind it and
reports `DaemonProtocolError: Request createOrAttach timed out after 30000ms`.
The main process already validates asynchronously and passes prevalidatedCwd
(ipc/pty.ts); the daemon never got the same treatment.

Add validateWorkingDirectoryAsync (one stat, not exists-then-stat, so an
unreachable share is not paid for twice) and await it from the daemon spawn
preflights. spawnSubprocess now returns SubprocessHandle | Promise<...>, which
existing sync stubs still satisfy.

Deliberately not bounding the stat with a timeout: the 30s ceiling comes from
blocking the shared loop, not from the duration. A timeout cannot tell "slow
share" from "gone share", so it would fail spawns that succeed today at 3-8s
on a cold VPN mount, and trade an accurate "working directory does not exist"
for a guess.

The new await opened a race: it sits between the "already exists?" check and
the sessions.set that publishes the session, so two concurrent creates for one
session id both spawned. Gate creation per session id; distinct ids still spawn
in parallel.

STA-4470

* fix(daemon): fence async spawn lifecycle
2026-08-16 01:16:11 -07:00
Jinjing 2f4e9aed70 fix(worktrees): address review feedback on the worktrees slice split (#14669)
* fix(worktrees): address review feedback on the worktrees slice split

Follow-ups to #14643. All of these predate the split (the constructs moved
verbatim from worktrees.ts), so they are behavior fixes, not refactor fallout.

- fetchAllWorktrees: route runtime-scope-forbidden errors through the toast in
  the hydration path, matching the fast path
- updateWorktreeMeta / ensureHostedReviewPushTarget: use
  trySettingsForWorktreeOwner so an ambiguous owner skips push-target resolution
  instead of throwing past the { ok, error } contract
- updateWorktreeMeta: compare linkedPR with ?? null so an unknown worktree does
  not trigger a spurious hosted-review refresh (matches the four sibling providers)
- persistWorktreeMeta: localize the two runtime-capability messages
- buildWorktreeRenameState: fix a doc comment that contradicted the code
- setWorktreesPinnedAndReveal: skip updateWorktreesMeta on an empty map
- pruneLastVisitedTimestamps: treat an empty worktreesByRepo list as unhydrated,
  and clear activeWorkspaceKey alongside a stale activeWorktreeId
- forceDeletePreservedBranch: fail closed when several retained cleanups match
  and no host was given, rather than routing to the active runtime
- purgeOrphanedRuntimeSshProjects: drop blank target ids so a repo with no
  connectionId cannot match
- markWorktreesDeleting: make the skip guard phase-aware so queued rows are
  promoted to deleting
- worktree purge: validate right-sidebar tabs with normalizeRightSidebarRoute so
  pr-checks and plugin panels are not silently dropped

* test(worktrees): add edge case tests from review feedback

Add defensive tests covering phase-aware deletion state transitions, tab
preservation across worktree purges, graceful handling of ambiguous owner
scenarios, and stale workspace cleanup. These catch edge cases that could
lead to silent failures or invalid state transitions.

* fix(worktrees): preserve workspace key pointing at live worktree

Replace overly broad scope check with direct equality check, so pruning
only drops the stale worktree's own workspace key while preserving keys
pointing to other live worktrees or folder workspaces.

* fix(worktrees): clear a legacy unprefixed active workspace key on prune

pruneLastVisitedTimestamps only dropped the `worktree:`-prefixed derived key,
so sessions predating the prefix kept a phantom workspace selected. Match the
purge path, which already treats a bare worktree id as a workspace key.

* chore(i18n): re-key the two runtime-capability strings to the canonical scheme

The keys added when persistWorktreeMeta was localized were hand-written. Derive
them the way localize-renderer-strings.mjs does — module path plus
sha1(path:text) — so a drift audit against the generator does not flag them.

* fix(worktrees): stop updateWorktreeLineage rejecting past its never-rejects contract

The sidebar's "Remove parent link" (WorktreeContextMenu.handleRemoveParentLink)
awaits updateWorktreeLineage in a bare `void Promise.all(...)` with no catch, so
the action must always resolve. Two paths broke that:

- settingsForWorktreeOwner ran outside the try and throws on an ambiguous owner.
  Skip with warnAmbiguousOwnerOnce instead, matching the ensure-push-target fix.
- the recovery refresh inside the catch has no internal try/catch, so a failing
  lineage RPC rejected out of the handler meant to absorb the failure.

assignWorktreeParent keeps rethrowing (both its callers catch and toast), but its
recovery refresh is now best-effort too so it can't mask the original cause.

Also: localize the new ambiguous-host preserved-branch throw (it reaches a toast
description; the throw above it stays verbatim because it mirrors a main-process
message), and drop blank SSH target ids once so both purge lookups agree.

* fix(worktrees): surface un-nest failures instead of silencing them

Supersedes the approach in 789edeaa7f. That commit fixed the unhandled rejection
in WorktreeContextMenu.handleRemoveParentLink by making updateWorktreeLineage
never reject, which had two costs:

- it made the .catch in use-lineage-drop-commit unreachable, so a drag-unnest
  failure went from an error toast to a silent no-op; and
- trySettingsForWorktreeOwner returns null for BOTH 'ambiguous' and 'missing'
  route resolutions (worktree-operation-route.ts collapses the three-way result),
  so the skip also swallowed unresolvable-route cases while logging "identity is
  ambiguous", which is the wrong cause for a legacy row on a multi-runtime setup.

Fix the caller instead: updateWorktreeLineage keeps rejecting, and both call
sites go through a shared unnestWorktrees() that catches and toasts. The
best-effort recovery refresh from 789edeaa7f is kept — that part was correct and
stops a failing refresh masking the original error on the assign path.

unnestWorktrees also de-duplicates the identical console.error/toast pair the two
callers had, and reuses the existing failedUnnestWorkspace key (already
translated in all five locales), so no new string ships.
2026-08-16 01:00:36 -07:00
Jinjing c066a557ea fix(persistence): address review feedback on extracted modules (#14862)
Follow-ups to the module extraction (#14252):

- Guard the lineage companion maps in worktree meta GC, and clear them
  alongside a corrupt worktreeMeta so stale rows can't re-attach.
- Repair a renamed worktree's stale lineage.worktreeId and flag the save.
- Key the git-username cache by execution host + path so the same checkout
  path on local/SSH/runtime hosts can't cross-hydrate usernames.
- Drop undefined keys before the automation update spread; a Partial with
  an explicit undefined blanked stored values.
- Normalize pane identities in every workspaceSessionsByHostId partition,
  not just the legacy blob, and use the merged leaf maps for lease and
  acknowledgement remapping.
- Reject duplicate preferred leaf ids so two panes can't collapse onto one
  pty/buffer/scrollback key.
- Let an explicit rightSidebarExplorerView outrank the legacy search-tab
  fallback; the legacy migration now runs on the raw payload at load.
- Record why the mobile pairing migration leaves its sources in place.
- Trim persisted folderPath, normalize synthesized worktree visibility
  preferences, validate notification settings field-by-field, strip
  undefined optional keys before the SSH lease merge, reuse `now` for an
  automation's updatedAt, and rename the pane alias registrar.
2026-08-16 00:36:21 -07:00
Jinjing 470ef65bd7 fix(editor): address review feedback on the split editor slice (#14850)
* fix(editor): address review feedback on the split editor slice

- Localize the conflict-placeholder guidance string (en/es/ja/ko/zh).
- Filter target-worktree tabs by migrated tab id so an owner transition
  cannot leave two tabs sharing one id.
- Resolve a pending editor reveal by fileId first; the oldFilePath scan
  could pick another worktree's rekey.
- Return before opening a workspace editor item when conflict metadata
  is missing, so no tab is created for a file that never entered openFiles.
- Skip a persisted open file whose resolved id is already used; the
  session schema allows repeated (path, worktree, runtime) tuples.

* fix(editor): remove migrated tab ids from sibling groups

When tabs migrate to a target group during editor owner transition, the
same tab IDs can be left in sibling groups, causing state corruption.
Strip these IDs from all sibling groups to ensure each tab ID exists
only once across the editor layout.
2026-08-16 00:27:01 -07:00
Neil 6cf6a7faff feat(crash-reporting): capture Crashpad minidumps and name the failing CHECK (#14823)
* feat(crash-reporting): capture Crashpad minidumps and name the failing CHECK

40% of renderer deaths report exit 0x80000003 (STATUS_BREAKPOINT) — a Chromium
CHECK/DCHECK — and we captured only the exit code, so the cause was structurally
unknowable. Nothing in the tree wired crashReporter at all.

Start Crashpad pre-whenReady and lift the text signature out of the dump:
Chromium stores the fatal log line in the LOG_FATAL annotation, so the check
name, file and line are recoverable with no symbols and no minidump_stackwalk.

Upload stays off. The existing transport is a user-initiated 4 MiB text bundle;
raw dumps are multi-MB binary carrying process memory. Dumps stay on disk and
only the signature rides the existing crash-report flow.

- minidump-stream-reader: bounds-checked view; a truncated dump degrades
- minidump-crashpad-annotations: allowlisted keys (switch-N carries command lines)
- minidump-crash-signature: LOG_FATAL -> file/line, exception, faulting module
- crashpad-capture: polls for the dump, which races process-gone delivery

STA-4469

* refactor(crash-reporting): claim dumps once and match them to the dead process

A newer dump from a different process could be paired to the wrong report, and
two reports in one crash burst could both claim the same file. Match the dump's
own ptype against the process Electron said died, and claim each dump once.

Also let the fatal line use the stack-length budget: a CHECK message truncated
at 240 chars can lose the condition, which is the diagnosis.

Fix the child-type test to match the classifier: GPU exits are recoverable
churn and never become reports, so they must not burn a dump poll either.

STA-4469

* fix(crash-reporting): recover real Electron CHECK logs

Electron 43 Windows dumps carry the Chromium CHECK line in captured memory but omit the claimed LOG_FATAL annotation. Recover only bounded Chromium-formatted fatal/CHECK lines, and prune raw dumps after crash events so suppressed child crash storms stay within the 128 MiB budget.

* fix(crash-reporting): satisfy not-found lint
2026-08-15 23:45:32 -07:00
Neil eb3f6838af perf: coalesce git upstream status reads (#11697)
* perf: coalesce git upstream status reads

* fix(git): repair upstream lease key imports and guard its field list

The read owner imported the shared/types barrel deleted by #14447, and its
push-target key hand-enumerated fields, so a new GitPushTarget field would
silently share a lease between two different targets. The destructure now
fails to compile if a field is added. Lease tests moved into their own file
after #14728 split ssh-git-provider.test.ts.

* test(git): enforce native upstream coalescing in CI

The 10-caller benchmark only runs under ORCA_GIT_UPSTREAM_COALESCING_BENCH_JSON,
so nothing in CI failed when the native/WSL lease was bypassed. Route status.ts
through invalidateGitUpstreamStatusReads so the export has a production caller.
2026-08-15 23:24:05 -07:00
Neil 8dc29a5be6 ci: render LoC signs Huge bold (#14855) 2026-08-15 23:23:47 -07:00
Neil ac48d753a7 ci: color added/deleted LoC counts in PR summary (#14839)
* ci: color added/deleted LoC counts in PR summary

* ci: use GitHub color-swatch dots for added/deleted LoC counts

* ci: color LoC counts with LaTeX textsf

* ci: bold LoC counts; render zero in white

* ci: render LoC counts large bold sans-serif

* ci: use bold math font for LoC counts

* ci: color only the + and - signs on LoC counts
2026-08-15 23:18:20 -07:00
Jinjing 31e9f4af30 Refactor: split editor.ts into modular state actions (#14847)
* refactor: split editor.ts under 400 lines

Move editor slice types, file-id/tab helpers, and action factories under
src/renderer/src/store/slices/editor/. The source file is now a public
barrel. No intentional behavior change.

* refactor: split editor-chrome-slice into state modules

- Move EditorDraftState, ExplorerDirState, and RightSidebarState type definitions into their respective action files
- Simplify state creator return types from Pick<EditorSlice, ...> to specific state types
- Improve modularity by colocating types with implementations
2026-08-15 22:14:37 -07:00
Jinjing 8b04e060fa refactor(persistence): extract modules to half persistence.ts (#14252)
* refactor(persistence): extract modules to half persistence.ts

* refactor(persistence): tighten the extracted operations seam

Review follow-ups on the module extraction, all behavior-neutral.

The extracted operations read and mutate the Store's state object in place, but
every seam typed it as a bare PersistedState, so nothing at the boundary said a
caller must pass the live reference — a future caller handing over a clone would
have its writes silently dropped. Name that contract: StoreOwnedPersistedState
carries it to every operations interface and every mutating free function.
normalizePersistedPaneIdentityState and backfillFolderScopeConnectionIds stay on
PersistedState; they build a fresh state rather than mutating the Store's.

The six *PersistenceOperations wrappers were constructed per delegate call. They
are stateless today, so this was inert, but any future instance state would be
lost between calls. Memoize them, and mark state and gitUsernameCache readonly
so the compiler enforces the single-assignment invariant memoizing them relies
on.

Also: restore flushSshPtyConsumerRecovery, whose inlining left its rationale
duplicated at both call sites; document that migrateWorktreeIdentity's boolean
gates the caller's save, since the extracted function kept no docs of its own;
and merge a duplicate shared/types import that was failing lint under
--deny-warnings.

* delete plan doc

* refactor(persistence): add error recovery and improve field cleanup

- Rollback failed migrations to prevent corrupted state that blocks retry
- Gracefully skip malformed entries in normalization instead of aborting
- Strip retired fields to prevent orphaned state and sync issues

* refactor(persistence): drop the redundant persistence- filename prefix

The extracted modules already live in src/main/persistence/, so name
them after the domain they own. Point leftover shared/types imports
at the real type modules while touching those files.

* refactor(persistence): optimize lookups and fix unsanitized updates

- Use Maps instead of repeated array searches for O(1) lookups
- Apply sanitized updates instead of raw input in ui-state-update
- Compare fields directly rather than JSON strings to avoid false dirty states from persisted key ordering differences

* refactor(persistence): group modules into lifecycle folders

Move the 42 flat persistence modules into six folders named for what the
module does, and lift the Store class out of the barrel so persistence.ts
becomes an 8-line public surface.

Bodies are unchanged: every moved file diffs clean against HEAD once import
blocks are excluded. Only import specifiers were rewritten, by resolving each
one to an absolute path and mapping it through the move map.

Store keeps its existing max-lines suppression; its baseline entry is repathed
rather than re-added. Its 119-method public API sets a ~525-line floor, so it
cannot meet the 400-line cap without breaking the API for 153 importers.

* Sanitize worktree visibility sources and preferences on hydration

Ensure invalid or corrupted data from disk (untracked whitespace,
relative paths, bogus preference values) is cleaned during load
rather than corrupting the in-memory store.
2026-08-15 21:41:01 -07:00
Neil 1f483c3e4e fix(tab-bar): exclude disabled agents from new-tab search (#14824) 2026-08-15 20:28:21 -07:00
Neil 306c5d545f refactor(source-control): split the dropdown action resolver under the max-lines budget (#14835)
`source-control-dropdown-items.ts` was 524 counted lines behind an
`eslint-disable max-lines`. It splits along the seams the resolver already had:

- `source-control-dropdown-item-types` — the row union, consumed by CommitArea,
  the composer and the action dispatcher without pulling in the state machine.
- `source-control-dropdown-labels` — count/label/title wording.
- `source-control-dropdown-action-context` — the branch, upstream and review
  facts every row reads, derived once so rows cannot disagree about them.
- `source-control-dropdown-commit-items` / `-remote-items` / `-review-items` —
  the three row groups, each keeping its own disabled-reason ladder intact.

`resolveDropdownItems` is now just the entry order plus the conflict-abort and
hosted-review-busy passes.

Verified output-identical to the pre-split resolver: a differential harness ran
both implementations over 16,380 generated input combinations (every upstream
shape × PR state × conflict operation × blocked reason × staged count ×
provider) and compared entries deeply. The harness was scaffolding and is not
committed.
2026-08-15 19:44:02 -07:00
Neil 73aa5d0ca7 refactor(daemon,runtime): split daemon, pty and rpc modules under the max-lines budget (#14834)
Splits the nine oversized modules in the daemon/provider/runtime domain into
focused per-concern files and drops their max-lines baseline entries.

- daemon: `Session` decomposes into an output plane (emulator, pending-output
  buffer, client fan-out), a producer-pause controller, a shell-ready barrier and
  a termination controller; `DaemonClient` into socket connect, hello handshake,
  ndjson readers, pending-request settlement, listener registry and notify
  settlement; `daemon-health` into pid-file parsing, process identity,
  stale-kill, TCC attribution and bundle staleness; `shell-ready` into the marker
  constant and the bash/zsh rcfile generators.
- providers: local-pty shell-ready wrapper generation, wrapper root, startup
  command and bash rcfile split out of local-pty-shell-ready.
- runtime: `Coordinator` sheds DAG convergence, decision gates, escalation
  triage, the runtime contract, the stale-base flag and task dispatch; the
  files/git/github rpc modules split into per-domain method groups.

Behavior-preserving: the extracted units keep their original construction order,
guards and timer lifetimes, and every RPC method name is still registered.
Test `vi.mock` surfaces were re-partitioned to follow the moved symbols.
2026-08-15 19:36:03 -07:00
Neil 6854cceb90 refactor(panes,tabs): split pane manager and tab-group modules under the max-lines budget (#14760)
The two tab-group hooks, the pane manager, worktree activation, and the terminal
pane context menu each carried a file-level `eslint-disable max-lines` and ran
461-745 counted lines against a 300-line budget. AGENTS.md calls for splitting
rather than suppressing, and config/max-lines-baseline.txt is a shrink-only
ratchet, so this removes all five suppressions and prunes their entries
(341 -> 335).

Pure move, no behavior change. useTabDragSplit is cut into gesture lifecycle,
hover preview and drop commit; useTabGroupWorkspaceModel into item projections
plus the tab-close, close-scope, activation and creation command sets; the pane
manager into host, tree mutations, pane creation, drag wiring, reparent frame
tracking, layout sweeps and rendering diagnostics.

react-hooks exhaustive-deps stays at zero warnings, matching HEAD. Dependency
additions are only stable identifiers -- refs and callbacks that became
parameters -- and no `.current` dereference was added to any dependency array.

Verified: oxlint clean, ratchet passes, typecheck clean, full unit suite green
(the three remaining failures are pre-existing load flakes in untouched files,
each green when re-run serially), no new runtime import cycles among 1020
modules, no barrel files, and no lint suppression added anywhere.
2026-08-15 19:10:36 -07:00
Neil cc19692f93 refactor(editor): split Monaco, autosave and notebook modules under the max-lines budget (#14748)
The four editor modules, the diff-comment decorator and the file-type icon table
each carried a file-level `eslint-disable max-lines` and ran 319-806 counted
lines against 300/400-line budgets. AGENTS.md calls for splitting rather than
suppressing, and config/max-lines-baseline.txt is a shrink-only ratchet, so this
removes all six suppressions and prunes their entries (341 -> 334).

Pure move, no behavior change. MonacoEditor is cut along its own seams -- mount,
input bindings, markdown annotations, decorations, content sync, view-state
persistence and reveal scheduling -- with the markdown overlay becoming its own
component. useEditorPanelContentState splits into file and diff content loaders
plus the active-tab load and reload triggers.

When the mount module came in at 370 counted lines, over the 300 ceiling, it was
split again into its parameter types and its input bindings rather than carrying
a suppression.

Hook usage is identical to HEAD across all three React split families: the same
counts of every hook type between each original and its extracted modules, so no
hook was added, dropped, or converted to a plain function. react-hooks
exhaustive-deps stays at zero warnings, matching HEAD.

Verified: oxlint clean, ratchet passes, typecheck clean, full unit suite green
(the four remaining failures are pre-existing load flakes in untouched files,
each green when re-run serially), no new runtime import cycles among 884
modules, and no lint suppression added anywhere.
2026-08-15 19:02:11 -07:00
Neil 98450718f7 refactor(right-sidebar): split file explorer and sidebar under the max-lines budget (#14741)
The six right-sidebar modules and the remote file browser each carried a
file-level `eslint-disable max-lines` and ran 347-797 counted lines against
300/400-line budgets. AGENTS.md calls for splitting rather than suppressing, and
config/max-lines-baseline.txt is a shrink-only ratchet, so this removes all seven
suppressions and prunes their entries (341 -> 333).

Pure move, no behavior change.

Two renderer-specific hazards were found and fixed rather than shipped.

First, effect and ref LIFETIME. FileExplorer's `if (!worktreePath) return` sits
above the files pane, so moving the worktree-reset effect into that pane made its
guard ref `lastResetWorktreePathRef` die on any render where worktreePath was
transiently null (workspace-list refresh, store rehydrate, remote worktree
reload). On remount the guard read null, so the reset fired even when returning
to the SAME worktree -- wiping dirCache, collapsing every expanded directory,
clearing the name filter and undo history, and forcing a full re-read over SSH.
The tree-load effects now live in a hook called from FileExplorer above the early
return, and the pane is purely presentational with zero hooks. That also restores
the original parent-effect ordering, which had shifted because React flushes
child effects before parent effects.

Second, extracting a hook silently degrades dependency analysis: `setX` setters
that the linter knew were stable when created locally become opaque parameters,
producing 8 new react-hooks/exhaustive-deps warnings where src/renderer had zero.
Those are fixed by listing the genuinely stable identifiers (useState setters and
ref OBJECTS). No `.current` dereference was added to any dependency array, since
that would change callback identity as the ref mutates.

Verified: oxlint clean with exhaustive-deps back to zero, ratchet passes,
typecheck clean, full unit suite green on the first pass, no new runtime import
cycles, no lint suppression added, and hook usage identical to HEAD across both
split families.
2026-08-15 18:48:23 -07:00
Neil 97b71c2285 refactor(usage): split AI-usage scanners and stores under the max-lines budget (#14668)
The three usage scanners and their stores, plus the renderer usage-overview
model, each carried a file-level `eslint-disable max-lines` and had grown to
338-769 counted lines against a 300-line budget. AGENTS.md calls for splitting
rather than suppressing, and config/max-lines-baseline.txt is a shrink-only
ratchet, so this removes all seven suppressions and prunes their entries
(341 -> 334).

Each file is cut along the seams it already had -- and that several of the
suppression comments named out loud: filesystem discovery / record parsing /
attribution / aggregation for the scanners, and pricing policy / scope filters /
rollups / session rows / automation attribution for the stores.

Pure move, no behavior change. Code is relocated verbatim; the only edits are
import plumbing and, where a private class method became a free function, the
mechanical `this.state` -> `state` parameter threading. Every converted call
site passes `this.state` at call time and the automation path takes a live
`getState: () => this.state` getter, so no state is snapshotted. No barrel
exports: each new module owns real logic and importers point at the owner.

Verified: oxlint clean, ratchet passes, typecheck clean, full unit suite green
(remaining failures are pre-existing load flakes in untouched files, each green
when re-run serially), no import cycles among the 64 affected modules, and a
statement-level diff of every split confirms the moves are verbatim.
2026-08-15 18:33:33 -07:00
Neil bc28107864 refactor(hooks,relay): split agent hook services and relay under the max-lines budget (#14725)
The four agent hook services, the main hooks module, and the two relay modules
each carried a file-level `eslint-disable max-lines` and ran 365-628 counted
lines against a 300-line budget. AGENTS.md calls for splitting rather than
suppressing, and config/max-lines-baseline.txt is a shrink-only ratchet, so this
removes all seven suppressions and prunes their entries (341 -> 334).

Pure move, no behavior change. Each hook service splits into its managed script
source, its config/bundle serialization, and its remote-install path, keeping the
per-agent integrations independent: copilot, amp, antigravity and hermes each
retain their own getManagedScript rather than sharing one, because each emits a
different script body for a different agent. Merging them by name would have
been a behavior change, not a refactor.

For antigravity the suppression's stated rationale -- that local install, Windows
wrapper generation, status cleanup, and SSH remote install must share one event
list and managed-command matcher so stale-hook cleanup cannot drift by platform
-- is now enforced structurally instead: both install paths call
buildInstalledConfig + createAntigravityManagedCommandMatcher over the single
ANTIGRAVITY_EVENTS catalog, with the graph a strict DAG.

Also registers the six new antigravity/ and copilot/ modules in
config/tsconfig.cli.json. That project uses a curated `include` list rather than
a glob, so an unlisted module fails `tsc -p config/tsconfig.tc.cli.json` with
TS6307 even though the entire unit suite passes.

Verified: oxlint clean, ratchet passes, typecheck clean, full unit suite green
(remaining failures are pre-existing load flakes in untouched files, green when
re-run serially), no new runtime import cycles, and no lint suppression added.
2026-08-15 18:25:37 -07:00
Neil 83117f2860 refactor(integrations): split issue-tracker clients under the max-lines budget (#14704)
The GitLab, GitHub, Jira and Linear integration modules, their two IPC
registrars, and the shared GitHub project types each carried a file-level
`eslint-disable max-lines` and ran 351-614 counted lines against a 300-line
budget. AGENTS.md calls for splitting rather than suppressing, and
config/max-lines-baseline.txt is a shrink-only ratchet, so this removes all
eight suppressions and prunes their entries (341 -> 333).

Pure move, no behavior change. Each client is cut along the seam it already
had: per-operation modules for the issue APIs (create / update / comment /
field options), and for Jira the request queue, site credential store,
authenticated request, and site identity. The two IPC registrars keep their own
handlers and delegate the rest to per-domain sub-registrars, so they remain
real entry points rather than re-export shims.

The IPC surface is proved intact rather than assumed: comparing (method,
channel) multisets between HEAD and the split gives 52 registrations across 52
distinct channels on both sides.

Provider-neutrality is preserved -- GitLab and GitHub keep separate, parallel
module layouts rather than being merged behind a shared abstraction.

Verified: oxlint clean, ratchet passes, typecheck clean, full unit suite green
(the one remaining failure is a pre-existing load flake in an untouched file,
green when re-run serially), no new runtime import cycles among 744 modules,
and no lint suppression added anywhere.
2026-08-15 18:17:20 -07:00
Neil 15e1ba3f84 refactor(ipc): split main-process IPC modules under the max-lines budget (#14703)
The six oversized src/main/ipc modules each carried a file-level
`eslint-disable max-lines` and ran 427-671 counted lines against a 300-line
budget. AGENTS.md calls for splitting rather than suppressing, and
config/max-lines-baseline.txt is a shrink-only ratchet, so this removes all six
suppressions and prunes their entries (341 -> 335).

Pure move, no behavior change. Each file is cut along the seams it already had:
pet splits into format allowlist / storage paths / symlink-safe copy / bundle
manifest + import; filesystem-auth into path-containment primitives, the
config-derived allow-list, and the git-registered root cache; notifications into
sound selection, native lifecycle, permission probe, and burst cooldown;
crash-reporting into renderer error reports, breadcrumbs, and sender.

The IPC surface is proved intact rather than assumed: comparing (method,
channel) multisets between HEAD and the split gives 49 registrations across 49
distinct channels on both sides. filesystem-auth's security boundary keeps its
acyclic layering -- containment primitives, then allow-list, then root cache,
then path-resolution orchestration -- with no layer gaining a back-edge.

Also keeps clipboard-ipc-handlers.test.ts under the 800-line test budget. The
split had briefly added a redundant vi.mock for isENOENT (byte-identical to the
real implementation) that pushed it to 801; the mock is dropped in favor of the
real function, with realpath added to the existing node:fs/promises mock.

Verified: oxlint clean, ratchet passes, typecheck clean, full unit suite green
(the one remaining failure is a pre-existing load flake in an untouched file,
green when re-run serially), no new runtime import cycles among 617 modules,
and no lint suppression added anywhere.
2026-08-15 18:08:54 -07:00
Neil c8fe5fc8c1 refactor(browser): split browser and browser-IPC modules under the max-lines budget (#14697)
The five oversized src/main/browser modules and src/main/ipc/browser.ts each
carried a file-level `eslint-disable max-lines` and ran 377-654 counted lines
against a 300-line budget. AGENTS.md calls for splitting rather than
suppressing, and config/max-lines-baseline.txt is a shrink-only ratchet, so
this removes all six suppressions and prunes their entries (341 -> 335).

Pure move, no behavior change. cdp-ws-proxy is decomposed into collaborating
objects rather than free functions because its state is genuinely
per-connection: every collaborator is a private readonly instance field built
in the constructor with live closures over `this`, so per-connection state
stays per-connection. Likewise the screencast pacer's isClosed/isStopping and
snapshot capture's getSeq are live thunks, not values captured at wiring time,
so guards inside already-armed timers still observe a later stop().

browser-guest-ui.ts is renamed to browser-guest-shortcut-forwarding.ts: after
the split it exports exactly one function, setupGuestShortcutForwarding, so the
old name no longer described its contents.

Also restores a single `webContents.debugger` read in the screencast path. The
extraction had left three reads where the original had one; the accessor is
stable today, so this is not a behavior fix but it removes a latent divergence.

Verified: oxlint clean, ratchet passes, typecheck clean, full unit suite green
(remaining failures are pre-existing load flakes in untouched files, each green
when re-run serially), no new runtime import cycles, and the IPC channel set
diffed identical before/after with all 23 handlers still trust-gated.
2026-08-15 17:59:29 -07:00
Jinjing 93ab6e142e refactor(source-control): extract modules to half SourceControl.tsx (#14396)
* docs(source-control): plan half-size extraction

* refactor(source-control): extract modules to half SourceControl.tsx

* move git decoration token comment to correct component

* delete plan doc

* test: add useSourceControlBranchCompare and git-history hook tests

Comprehensive unit tests covering the scheduling, stale response filtering,
and visibility logic of the extracted branch-compare and git-history hooks.

* refactor(source-control): internationalize UI strings

Add translate() support for all hardcoded strings throughout source control UI,
extract reusable SourceControlTreeDirectoryHeader component, improve error
handling in bulk operations with logging and user-facing toasts, and add proper
return type annotations to hooks.

* fix(source-control): satisfy react-doctor rules in extracted modules

Reset worktree-scoped hook state during render instead of in an effect,
and give dropdown separators stable ids so the changed-code quality gate
stops flagging the extracted SourceControl modules.

* docs: add JSDoc comments to source-control hooks and components

Clarify the purpose, behavior, and constraints of test-harness functions,
directory-row components, and the git-history hook to help maintainers
understand the extracted and refactored source-control module.

* refactor: organize source-control into lifecycle dest folders

* fix(source-control): clear remaining react-doctor findings

Reset worktree- and history-scoped state during render, keep Cmd/Ctrl
selection updates free of setter side effects, and key graph paths by
swimlane/parent id. Also add the PR LoC helper scripts the quality
workflow fetches from the branch head.

* fix(source-control): refetch git history when owner host changes

Track activeRuntimeEnvironmentId as a stable key in useSourceControlGitHistory so that when the owner host changes but the worktree and path remain the same, the git history panel correctly refetches from the new host instead of keeping stale commits from the previous one. Add ownerHostKey to the useEffect dependency array to trigger refetch on host changes. Include JSDoc documentation for related components and expand test coverage to verify the host-change scenario.

* fix(source-control): refetch git history when owner host changes

Track activeRuntimeEnvironmentId as a stable key in useSourceControlGitHistory so that when the owner host changes but the worktree and path remain the same, the git history panel correctly refetches from the new host instead of keeping stale commits from the previous one. Add ownerHostKey to the useEffect dependency array to trigger refetch on host changes. Include JSDoc documentation for related components and expand test coverage to verify the host-change scenario.

* refactor(source-control): consolidate bulk mutation error handling

Extracts repeated error reporting into a dedicated helper function and applies
it consistently across all bulk stage/unstage handlers, including two that were
previously missing error handling.
2026-08-15 16:47:03 -07:00
Jinjing b8dc393c18 Revert "fix(mobile-native-chat): retire pending bubbles glued into one transc…" (#14819)
This reverts commit 68ca17e46c.
2026-08-15 16:34:57 -07:00
Jinwoo Hong d2ffe1f362 fix(terminal): settle CLI prompts for Claude and Codex (#14608) 2026-08-15 15:45:17 -07:00
Jinjing 7aaa7c6f5b refactor(sidebar): group worktree-list files by domain (#14486)
* refactor(sidebar): group worktree-list files by domain

Follow-up to #14465 / #14467. Keep the landed extract and reorganize the
flat worktree-list dump into drag/, headers/, reveal/, rows/, scroll/,
and viewport/. Fold tiny modules into their owners, move leftover
sidebar-root files into the module, and retarget imports and source-path
tests. Layout-only; no behavior change.

* fix(sidebar): merge duplicate virtual-rows imports

Inlining virtual-row-dom-attributes left a second import from the same
module, which fails audit:code-quality:native --deny-warnings.

* refactor(sidebar): condense indentation comments

Shorten explanations to focus on the essential why, removing redundant
detail and improving readability without changing functionality.

* refactor: organize worktree-list into lifecycle dest folders

* fix react doctor

* fix: update reliability-gates path after worktree-list reorg

host-filtering.test.ts moved from viewport/ to listing/; keep the
runtime-routing.active-server-preference gate pointing at the real file.

* Extract workspace status colors to design tokens

Define theme-aware color tokens for workspace PR-state indicators (done, in-review, in-progress) to ensure consistent identity across theme switches. Update references to use the new tokens and refactor EmptyState button to use the Button component.

* fix(sidebar): stop mutating refs during worktree-list render

React Doctor fails static analysis when refs are written in render.
Commit reused array identity and the Smart live-signal latch after
paint, and return the attention map from the sort memo instead of
stashing it on a render-time ref.
2026-08-15 13:40:09 -07:00
Jinjing 35b308f46e Remove worktree deletion success toasts (#14724) 2026-08-15 12:38:14 -07:00
github-actions[bot] a253a74e62 Update README downloads badge 2026-08-15 18:26:26 +00:00
Neil c0a775454a fix(terminal): stop the hidden-output restore re-arming itself after dispose (#14749)
`requestHiddenOutputRestoreIfNeeded` tracked its in-flight task with a `.finally`
handler that re-armed the restore. The task body is `while (!disposed)`, so once
the pane is disposed it exits immediately, the handler runs at once and re-arms
again — an unbounded self-feeding promise chain that consumed ~4GB in ~12s and
starved the microtask queue.

The pane is gone at that point and there is nothing to restore, so the handler
now returns early when disposed.

This was only invisible because the 25k-line pty-connection suite happened to run
a later test that tore the loop down; splitting that file into focused suites left
the arming tests at the end of a file and turned it into a reproducible OOM.

The regression test drives the restore into its armed state, disposes the pane and
counts how many times the chain re-reads `isVisibleRef`. Before this change it
cycled 196 times and OOM-killed the worker; after, it settles immediately.
2026-08-15 06:06:51 -07:00
a234123751 feat(shortcuts): warn when macOS Mission Control captures digit chords (#14734)
* feat(shortcuts): warn when macOS Mission Control captures digit chords

Mission Control's Switch to Desktop shortcuts (Ctrl+digit by default,
present whenever the user has multiple Spaces) are consumed by
WindowServer before the app receives the event, so Orca's digit-range
shortcuts silently do nothing and the app can never observe the press.
Detect the conflict instead: probe com.apple.symbolichotkeys through
the live-prefs pipeline on Shortcuts pane mount and surface a standing
conflict warning on the affected rows, counted into the Conflicts stat.

Non-Darwin and the web client return no chords, and any probe failure
yields an empty result so a missing signal can never show a false
warning.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(shortcuts): harden Mission Control conflict warnings

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com>
2026-08-15 05:34:02 -07:00
Neil 92fb276040 test(daemon): stop mutating process-global cwd in the daemon cwd-repair tests (#14750)
The two cwd-repair tests really did `process.chdir()` into a temp directory and
delete it, so for that window every other test file sharing the worker process saw
a missing cwd. That was already a hazard; splitting pty-subprocess.test.ts into ten
files made the window overlap far more work, and the tests started failing
intermittently in full-suite runs while passing in isolation.

Stub the boundary instead: report a non-existent daemon cwd via `process.cwd()` and
spy on `process.chdir()` to capture the repair target. The assertion gets stricter
rather than weaker — it now names the directory the repair chose instead of
observing where the process happened to land — and no process-global state moves.

Both cwd-repair tests had failed at least once in prior full-suite runs; the suite
is green across repeated runs with this change.
2026-08-15 04:47:24 -07:00
Neil c4e397bcdc perf(renderer): index web session editor tab reconciliation (#8098)
Replace the per-editor-tab Array.find over the worktree's unified tabs with a
lazily-built id/fileId map, and swap two O(n^2) includes-in-a-loop scans for
Set membership. The map is only materialized when a snapshot actually carries
a mirrored editor tab, so terminal-only snapshots pay nothing.
2026-08-15 04:27:58 -07:00
Neil e919f4843d perf: coalesce SSH git status reads (#11696)
* perf: coalesce SSH git status reads

* fix(ssh): key status leases by branch-line-total fork point

The request payload carries branchLineTotalMergeBase but the lease key did
not, so a strict refresh asking for the total could join an in-flight poll
that omitted it (blanking the branch-header chip) or reuse a pre-commit
fork point. Mirrors the guard already on the local path in git/status.ts.
2026-08-15 04:16:00 -07:00
Neil 931cb037c5 fix(ci): satisfy restrict-template-expressions in pr-test-loc-summary test (#14755)
#14738 landed a template literal interpolating an untyped fetch url, which
fails audit:code-quality:type-aware. That audit runs in the static analysis
job, so main is currently red and every open PR inherits the failure.
2026-08-15 03:13:08 -07:00
Brennan Benson 5b7f44278a fix(workspace-cleanup): refuse removal when the owning host is not certain (STA-4343) (#14731)
* fix(workspace-cleanup): refuse removal when the owning host is not certain (STA-4343)

* fix(workspace-cleanup): distinguish host collisions

* fix(workspace-cleanup): recheck host at removal boundary
2026-08-15 01:40:59 -07:00
Neil 393c8764e0 ci: post test vs non-test LoC on pull requests (#14738) 2026-08-15 01:34:44 -07:00
Jinwoo Hong 2eb3e11327 fix(terminal): make close and handles incarnation-stable (STA-4327) (#14590) 2026-08-15 01:25:09 -07:00
Jinjing 2b10767d9d fix(e2e): unblock golden file-link hover and Windows worktree activate (#14720)
* fix(e2e): unblock golden file-link hover and Windows worktree activate

Mac/Windows tmp paths wrap across xterm rows, so locateLink never
found the full absolute path. Print ./package.json instead.

createGoldenWorktree used os.tmpdir() (Windows 8.3 RUNNER~1) while
Git listed the long path, so activateGoldenWorktree never matched.
Realpath after worktree add and compare on the Node side.

* fix(e2e): handle realpath failures in golden worktree creation

Ensure half-built worktrees and branches are rolled back when
realpathSync fails, preventing leaks into later test runs. Extract
error handling into rollbackGoldenWorktree() for consistent cleanup.
2026-08-15 01:02:06 -07:00
Neil 9367169888 refactor(tests): split every oversized test file off the max-lines suppression list (#14728)
* refactor(tests): split oversized test files off the max-lines suppression list

Every `*.test.ts`/`*.spec.ts` that carried an `eslint/oxlint-disable max-lines`
directive is now split into focused, behavior-scoped suites that fit the 800-line
test budget, with shared setup extracted into co-located `*-test-harness.ts` /
`*-test-fixtures.ts` modules (300-line budget). 83 files became ~930; the largest
output is 797 effective lines. `orca-runtime.test.ts` is intentionally untouched.

Test bodies were moved by scripted line-range slicing rather than retyped, so
assertions are byte-identical. The only permitted body edits were mechanical
rebinding where a shared value moved into a harness (e.g. `tmpHome` ->
`homes.tmpHome`).

Registries that enumerate test files were updated in lockstep:
- config/max-lines-baseline.txt: pruned 341 -> 258 entries (all 83 removed).
- config/reliability-gates.jsonc: 33 gates repointed at the split files, with
  assertionRefs split per file where a gate's coverage now spans several.
- .github/workflows/pr.yml: the real-zsh lane now lists the 4 split files that
  actually exercise zsh, so they keep running in the dedicated shell lane.

Also renamed agent-hooks `server-test-fixtures.ts` to `server.test-fixtures.ts`
so the global-fetch call-site audit keeps skipping it, and added `.js` extensions
to the CLI suites' dynamic harness imports (node16 resolution) to unbreak
`build:cli`.

Verification: full suite 52,449 passing vs 52,448 at baseline with zero
assertions lost; `pnpm lint`, `pnpm typecheck`, and `pnpm build:cli` all exit 0;
the terminal-pane e2e spec runs 31/31 headless.

* refactor(tests): split hook-idle arbitration suite that oxfmt pushed over budget

The pre-commit oxfmt pass reflowed pty-connection-hook-idle-arbitration.test.ts
to 811 effective lines, 11 over the test budget. Split the hook-completion side
effect and replacement-agent veto cases into their own suite; both files now sit
well under the cap and the 15 tests are unchanged.

* test: port upstream test changes into the split files after rebase

Rebasing onto main surfaced 27 tests that main had added to files this branch
deleted, plus edits to tests that had already moved. Taking the deletion side of
those modify/delete conflicts would have dropped that coverage silently, so each
upstream change is ported into the split file that now owns the behavior — for
example main's six orchestration mailbox tests land across orchestration-runs,
-send, and -check.

Also repoints `orchestration.notification-mailbox-consistency`, a gate main added
after this branch's gate remap, at those same three split files, and re-prunes
the max-lines baseline against main's (257 entries).

Verified: all 27 upstream test titles present; full suite 52,761 passing with the
only diff vs baseline being 12 tests main itself removed and 3 that moved from
skipped to passing; lint and typecheck exit 0.

* fix(test): flush pending continuations before tearing down terminal test globals

CI shard 5/16 failed on both Node 24 and 26 with `ReferenceError: window is not
defined` from pty-connection.ts, surfacing through
pty-connection-daemon-snapshot-replay.test.ts.

The reattach/settle chains `await` a real promise and then touch `window.api`.
Under fake timers those continuations cannot run, so they only become schedulable
once restoreTerminalTestGlobals() switches back to real timers — which previously
happened immediately before `delete globalThis.window`, so a late continuation
threw and failed the whole file. Flush async ticks in that window instead.

This is latent in the source rather than new: the pre-split 25k-line file kept
running other tests after these, which gave the chains time to settle before
teardown. Splitting the file moved teardown directly behind them.

* fix(test): keep an inert window after terminal test teardown instead of deleting it

The async-tick flush was not enough: the reattach/settle chain can resolve after
teardown regardless of how long we drain, so CI shard 5/16 still failed with
`ReferenceError: window is not defined` from pty-connection.ts.

A real renderer never loses `window`, so deleting it was the artificial part.
Swap in an inert proxy whose properties resolve to callables and whose calls
resolve to undefined, making a late `window.api.pty.*` call a harmless no-op.
The next test replaces it wholesale via installTerminalTestGlobals(), and no test
asserts that `window` is absent.
2026-08-15 00:54:20 -07:00
Brennan Benson 66dfdc456f feat(computer-use): support macOS middle click and stop the silent left-click fallback (#14721)
* feat(computer-use): support macOS middle click and gate the AX click path

`--mouse-button middle` already validated end-to-end through the CLI, the
zod schema, and the provider validator, and both the Windows and Linux
providers honored it. Only the macOS provider rejected it outright with
"middle-click is not yet supported", so the flag was a dead end on the one
platform that has no fallback.

Two changes:

- Add `.middle` to the macOS button mapping. macOS has no dedicated middle
  event family, so it rides `otherMouseDown`/`otherMouseUp` with the button
  number carried by `mouseButton: .center`; that constructor argument is
  honored for exactly the `otherMouse*` types, so no extra field write is
  needed.
- Validate the requested button before the accessibility fast path, and skip
  that path for buttons it cannot express. Previously the raw string was read
  unvalidated, and `performClickAction` only special-cased `right`, so
  `click --mouse-button middle --element-index N` (no modifiers, count 1) fell
  through to `AXPress` — a left click — and reported success with
  `path: "accessibility"`. Any unrecognized button string did the same. This
  matches guards the Windows and Linux providers already had.

The button enum moves into `OrcaComputerUseMacOSCore` so it is unit-testable;
`main.swift` keeps only the CoreGraphics mapping.

Also documents `--mouse-button` in the computer-use skill guide, which never
mentioned the flag, so agents on Windows and Linux had no way to discover it.

* test(computer-use): cover macOS middle click in the real-desktop e2e suite

* test(computer-use): prove macOS middle-click delivery
2026-08-15 00:41:45 -07:00
Neil cceaea1296 perf(usage): index repos for worktree metadata (#8077) 2026-08-15 00:38:13 -07:00
Neil a663f1bd00 fix(terminal): hold a cursor chord until the composing syllable commits (#14730)
* fix(terminal): hold a cursor chord until the composing syllable commits

The composed glyph reaches the pty from the composition session-end handler,
which runs after the chord's keydown. Only Enter was held for that, so every
other chord went straight out on the transport and overtook the text it was
typed after: with 가나 on the line, typing 가나다 and pressing Cmd+Left left
다가나, the composing 다 landed at the cursor's destination.

Defer any sendInput chord while a composition is live or its session has not
yet flushed. Korean 2-Set shows the shape most clearly — the platform replays
the chord unmarked after keyup, so isComposing is already false while the
session is still pending.

No fallback timer on this path. A newline arriving late still arrives, which is
what that timer is for; a chord arriving mid-preedit is the corruption the wait
exists to prevent, and a conversion can hold its candidate window open for
seconds. Dropping the chord costs one keypress, firing early costs a line.

Pane commands are unaffected: they are not sendInput actions.

Fixes #12871

* test(e2e): pin the composing-chord order at the pty

The unit coverage asserts the handler's ordering against a synthetic transport.
This asserts it where it is actually observable: the committed glyph and the
chord reach the pty by two different routes, and only their merged order is
visible to the shell.

Verified to discriminate — against keyboard-handlers.ts from main the same spec
reads 01 eb8ba4 0a, the chord ahead of the syllable, which is the reported
corruption byte-for-byte.

* refactor(terminal): add the composing-chord deferral without touching the Enter path

Nesting the new branch inside the Enter condition re-indented the whole Enter
block, which is the kind of diff that can silently change it. Keeping them as
sibling conditions leaves the Enter path out of the diff entirely.

* test(e2e): pin the renderer to macOS for the Cmd+Left chord

Cmd+Left resolves to \x01 only under the macOS branch of the shortcut policy, so
on a Linux shard the chord produced no byte and the spec passed by measuring
nothing — it failed in CI for that reason, not for the behaviour under test.
Pinning the platform is the established pattern for these specs, and
expectImePlatformPolicy fails loudly if the override does not take.
2026-08-15 00:13:27 -07:00
Neil b849099045 fix(deps): bump transitive nanoid to 3.3.18 (GHSA-2v37-7h3g-55p8) (#14723) 2026-08-14 23:58:20 -07:00
Brennan Benson 54645250e1 fix: keep remote visibility hydration off local startup path (#14674)
* fix: defer remote visibility defaults after startup

* fix: preserve deferred visibility hydration

* fix: retain hydration on no-op runtime selection
2026-08-14 23:18:16 -07:00