* fix(agent-status): track Codex rollout subagents
* fix(agent-status): resolve cross-day Codex child rollouts and unblock CI gate
Codex files each rollout under its own local start date, so a session that
runs past midnight spawns children into a sibling day directory. Scanning
only the parent's directory left 13% of real subagent spawns (48/371 across
local rollouts) permanently unresolved, which pinned a phantom "working" row
and re-ran readdirSync every poll tick forever. Resolve the child's own day
directory from occurred_at_ms, and time-box a child whose rollout stays
unreadable so a deleted or never-written file can't leak a working row.
Also make the hook HTTP handler return void: the changed-code quality gate
keys findings by span overlap, so this PR's added line inside the pre-existing
async createServer callback resurfaced no-misused-promises as a new finding.
Tests cover cross-day resolution, grace-period retirement, and that the poll
re-arms across successive roster changes (the prior tests passed even when
the poll died after its first change).
* fix(agent-status): keep the Codex subagent poll alive across nested hooks
A nested non-codex CLI inherits its parent's ORCA_PANE_KEY, so its hook
POST reached scheduleCodexSubagentPoll and tore the timer down before the
source guard, silently ending polling while a rollout child was still live.
* fix(workspace-space): serialize local disk and cap traversal memory
Prevent resource exhaustion during large workspace scans by limiting
local disk access to one concurrent `du` call and capping portable
traversal memory to 100k entries or 64 MiB per worktree. Fixes July 27
incident with 298 worktrees causing host stalls and renderer OOM.
Portable traversals now use fixed-worker iterative frames instead of
recursive promises. Capacity failures become unavailable rows. Behavior
below limits is unchanged.
* fix(workspace-space): bound concurrent SSH fallback traversals
Desktop-side SSH fallback traversals run in the main process with independent admission
budgets. Without limiting, up to six concurrent traversals could stack six 64 MiB budgets.
Cap remote fallback traversals to 2 concurrent, keeping aggregate admission at 2 × 64 MiB.
Also make capacity error messages reflect configured limits instead of hardcoded defaults.
* perf(git): overlap getBranchCompare's head-of-chain reads
Four git spawns ran strictly in series before any compare work began:
branch --show-current, the base-ref probe, rev-parse HEAD, and rev-parse <base>.
Three are independent -- compareRef is display-only metadata and HEAD's oid does
not depend on the base ref -- so they now run concurrently. The fourth was
redundant outright: the probe already runs `rev-parse --verify --quiet
<ref>^{commit}` and discarded the oid it printed, which was then re-resolved by a
second spawn. resolveWorktreeBaseCommitOid returns that oid so it can be reused;
hasWorktreeBaseCommitRef now delegates to it, leaving its other 4 callers
untouched.
3.6-3.7x on a short remote base label (192ms -> 52ms), 1.44x on an
already-qualified refs/... base, which skips the probe by design.
Reuse is keyed by ref: resolveWorktreeAddBaseRef returns at its first successful
candidate, so only that ref's oid is ever read back. Peeling is safe because only
refs/heads and refs/remotes candidates reach the probe, where ^{commit} is a
no-op.
No new git features: this removes a spawn rather than adopting an option.
Co-authored-by: Orca <help@stably.ai>
* fix(git): preserve compare semantics across providers
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Orca <help@stably.ai>
* perf(relay): keep the PTY replay window as chunks, not a re-sliced string
appendReplayBuffer did `buffered += data` then `buffered.slice(-REPLAY_BUFFER_MAX)`
once over the 100KB cap. It runs per raw node-pty emission -- before batching --
so once a PTY saturates the window (which a long-lived shell does almost
immediately) every subsequent chunk copied the whole 100KB.
Reuse RecentPtyOutputBuffer, which already solved this shape in the main process:
keep chunks, drop from the head, defer the join to read(). The relay's three
readers are attach, adopt, and revive only.
66-205x on the append path, per PTY, on the user's SSH host.
RecentPtyOutputBuffer's limit is now configurable, because the relay retains
100KB where the main process retains 64KB. One arithmetic branch still used the
hardcoded constant after that change and silently under-retained (100,800 of
102,400 code units); the equivalence tests caught it before it shipped, and the
suite now pins the configured limit directly.
Co-authored-by: Orca <help@stably.ai>
* test(relay): exercise a real surrogate split; drop eval from the benchmark
Review feedback, both valid:
- The surrogate test never split a pair. The cap is even and a pair is two code
units, so an emoji run alone always cuts on a pair boundary. A trailing single
unit shifts the cut mid-pair, leaving a dangling low surrogate (0xDE00) --
asserted directly now, with the boundary-aligned case kept as its own test.
- Parse REPLAY_BUFFER_MAX as a product instead of eval(). The regex already
admits only digits, spaces and `*`, and eval tripped Biome's noGlobalEval
regardless of the eslint suppression.
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Orca <help@stably.ai>
* feat(plugins): Orca plugin system — kernel, content packs, panels, workers, marketplace v0 (experimental)
Adds Orca's experimental plugin system behind a settings flag: a
supervised kernel, declarative content packs (VM recipes, commands and
keybindings, language packs), sandboxed iframe panels, forked worker
hosts, and a Git-backed marketplace v0 with consent, provenance and
kill-list enforcement.
Theme, icon-theme and terminal-theme contributions are deferred to a
follow-up pass.
* fix(plugins): make unsupported marketplace listings unreachable by key
findPlugin() backs preview/install/previewInstalledUpdate via
requireListing(), so filtering only listPlugins() hid the catalog card
while leaving the dead install path reachable one click later.
* fix(plugins): fan Pi session-only status out to plugin subscribers
The providerSessionOnly early-return in applyNormalizedStatus emitted to
onAgentStatus (main-window fanout) but skipped enrichedStatusListeners, so
plugins subscribed to agent.status.changed silently missed every Pi
session_start event. Route both emit sites through one helper so a future
early return cannot drop the plugin tap again.
Co-authored-by: Orca <help@stably.ai>
* plugins: drop dead code and hoist duplicated trust-boundary patterns
Cleanup pass over the P1 diff, no behavior change:
- Delete `readPluginTreeSnapshot`/`readSnapshotFile` and their types, plus
the now-vestigial `directories`/`signal` plumbing in `collectFiles`.
- Delete `resolveContainedPluginDirectory` (no callers).
- Delete `plugin-content-load-pool.ts`; it reimplemented the existing
`mapWithConcurrency`, whose index arg also removes the pairing wrapper
in `buildPluginList`.
- Hoist `PLUGIN_CONTENT_HASH_PATTERN` and `PLUGIN_COMMIT_PATTERN` into
the install-lockfile module; 11 sites hand-rolled these identically.
- Point the new reliability gate at the PR instead of gitignored docs
paths, matching every other gate's link form.
* fix(plugins): retry plugin state renames on Windows AV/EPERM locks
Six plugin write paths (lockfile, provenance, current pointer, kill
list, marketplace cache, staged install dir) did a plain rename, so an
antivirus or indexer holding the target open surfaced as a failed
install. The repo already retries this hazard for issue #1507, but only
through a sync helper; these paths are all async.
Adds one bounded async retry + atomic write used by all six, and trims a
consent-provenance header that restated its own JSX.
* test(plugins): cover the Windows rename retry path
The retry loop shipped untested: both existing cases hit the non-retry path,
and the temp-cleanup test passed identically with the `finally` removed.
Mock `rename` to queue errno codes so CI can exercise locks it cannot provoke.
Co-authored-by: Orca <help@stably.ai>
* fix(plugins): pin bundled plugin resources to LF
Windows CI checks out with autocrlf, so the byte-hashed launch tree arrived
as CRLF and verify-packaged-plugin-resources rejected it — the packaged build
could never pass on Windows. Reproduced locally: CRLF yields the exact CI
error, LF verifies clean. Files are already LF, so nothing renormalizes.
Co-authored-by: Orca <help@stably.ai>
* test: guard the bundled-plugin LF pin against a CRLF checkout
The byte-hash mismatch only surfaced in Windows packaging CI. Assert the
.gitattributes pin and that a CRLF tree is rejected, so a regression fails
on any platform instead of waiting for a packaged Windows build.
Co-authored-by: Orca <help@stably.ai>
* ci: trigger packaged-build check on bundled plugin resource changes
The launch tree is byte-hashed during packaging, but no trigger path covered
it — so the CRLF fix for that check would not have re-run the check. Add the
resources, verifier and .gitattributes paths that can break packaging.
Co-authored-by: Orca <help@stably.ai>
* perf(plugins): rebuild the panel frame only when its baked theme values change
The revision keys the panel iframe, so every bump destroys the sandboxed
frame and its in-panel state. It counted root attribute mutations, but
--workspace-sidebar-live-width is written every rAF of a sidebar drag, so
dragging with a panel open blanked it ~60x/sec. Compare the two values the
shell actually bakes in instead.
Co-authored-by: Orca <help@stably.ai>
* test: stop pinning a plugin name in the CRLF guard
The CRLF case rewrites every launch file, so the reported mismatch is
whichever plugin sorts first. P2 adds theme plugins that sort ahead of
orca-navigation-shortcuts, which broke the assertion there.
Co-authored-by: Orca <help@stably.ai>
* style: drop stray blank lines left by the rebase resolutions
Both sides of the agent-hooks and orca-runtime conflicts contributed a
trailing blank, which oxfmt rejects. Whitespace only.
Co-authored-by: Orca <help@stably.ai>
* test(plugins): stop the startup budget failing on machine load
P95 runs 16-34ms idle but exceeds the 50ms bound under full-suite
parallelism, so the gate flaked. Widen it to catch an order-of-magnitude
regression instead; the no-worker/no-plugin-code assertions are the real
guarantee. Verified a 400ms regression still fails.
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Orca <help@stably.ai>
* fix(ssh): connect to Linux hosts that cannot compile node-pty
node-pty ships no Linux prebuilt at any architecture, so it is compiled on
the remote. On a host without a C/C++ toolchain that build fails, and because
both native deps install in one npm command it also took down
@parcel/watcher — which does have a working Linux prebuilt — and failed the
whole connection. Every Linux image without build tools was unusable.
node-pty only backs remote terminals; files, git, and the editor do not need
it, and a missing native dep is already non-fatal further down the deploy. So
when the existing toolchain probe confirms the compiler is missing, reinstall
without node-pty instead of aborting. The manifest has to drop it too — npm
reconciles every dependency in package.json, not just the ones named on the
command line, so naming only @parcel/watcher still rebuilds node-pty.
If that reinstall also fails the actionable build-tools error is rethrown, so
a host broken for some other reason still reports the toolchain gap.
The relay's PTY error now names the fix rather than saying only that node-pty
is unavailable.
Verified on a stock Rocky Linux 10.2 aarch64 container (openssh-server, git,
nodejs, npm, no compiler): connect succeeds, /etc lists over SSH, node-pty is
absent while @parcel/watcher installs its linux-arm64-glibc prebuilt, and
spawning a terminal reports the install hint.
* fix(ssh): keep the node-pty skip path honest about platform and watcher
The PTY unavailable message named build tools unconditionally, but only Linux
compiles node-pty — the deploy-side skip is gated on linux and the toolchain
probe returns null on Windows. A Windows or macOS remote, where node-pty ships
prebuilds, was told to install make/g++/python3. Pick the remedy by the relay's
own platform.
The skip path returned before the install probe, so a @parcel/watcher that
installs but cannot require() (glibc below the floor) connected with dead file
watching and nothing logged. Probe before returning and warn; no rebuild, since
node-pty provably cannot compile on that host, and never fatal.
Also log the pty-less reinstall's own failure and attach it as cause — the
rethrown toolchain message is built from the original npm error, so an
unrelated retry failure (registry, ENOSPC, EACCES) was lost. The reinstall now
keeps the caller's resetDeps as well, so a repair reconnect still clears every
dep the probe found broken.
Tests: the skip-success fixture queued a chmod/probe/rebuild sequence
production never runs, and the surplus slots were absorbed by launchRelay's
readiness poll (1817ms vs 3-9ms for its peers). It now emits exactly the 12
execs production performs, and pins that no rebuild is issued. Adds the missing
negative case: a gyp-shaped failure on a host whose probe reports a complete
toolchain must still hard-fail rather than silently degrade.
* fix(ssh): hedge the node-pty remedy and keep repair resets on the skip path
* fix(agent-hooks): install OpenCode status plugin into the WSL guest so status works over WSL
OpenCode reports agent status via a JS plugin dropped into OPENCODE_CONFIG_DIR
(unlike Claude/Codex, which use managed hooks.json scripts). Over the WSL
runtime that plugin was never materialized inside the guest and
OPENCODE_CONFIG_DIR never crossed into the guest, so OpenCode status never
reached Orca's sidebar (the workspace stayed green). Codex already worked; this
was OpenCode-specific.
Mirror the SSH plugin-overlay path for WSL:
- Guest relay registers AGENT_HOOK_INSTALL_PLUGINS_METHOD, byte-caps the source,
and materializes an OpenCode config overlay via PluginOverlayManager (the same
electron-free path the SSH relay uses), returning overlayDirs.opencode.
- Host manager ships the plugin source over the existing stdio channel after
installers run (and on mid-session reinstall) and records the guest overlay
dir; -32601 / CONNECTION_LOST / DISPOSED are swallowed like ssh-relay-session.
- PTY env points OPENCODE_CONFIG_DIR/ORCA_OPENCODE_CONFIG_DIR at the guest
overlay; until the relay reports it (first spawn / older guest bundle) it drops
those vars rather than crossing the Windows overlay path into WSL — so
in-guest OpenCode falls back to its own config (pre-fix behavior, no
regression).
- WSLENV passes OPENCODE_CONFIG_DIR/ORCA_OPENCODE_CONFIG_DIR through (/u for
guest paths).
SSH is untouched: the same JSON-RPC constant is reused and the guest response
merely gains an optional overlayDirs field the SSH host ignores.
Runtime repro: native opencode launched in a WSL Orca terminal had
ORCA_AGENT_HOOK_PORT/ORCA_PANE_KEY but no OPENCODE_CONFIG_DIR and no Orca plugin
in ~/.config/opencode, so agentStatusByPaneKey stayed empty.
* fix(agent-hooks): stop the WSL OpenCode overlay leaking Windows paths and churning under running agents
Review fixes on top of the WSL OpenCode plugin install:
- Never cross a Windows OPENCODE_CONFIG_DIR into the guest. The /p flag was
not a defensive default but WSLENV's translate-and-deliver flag, and
buildWslRelaySpawnEnv spreads process.env while the daemon merge resurrects
keys buildPtyHostEnv only deleted -- so a Windows value reached the guest as
/mnt/c/... and was adopted as its OpenCode config root. Register the two
vars only when the value is already a guest POSIX path.
- Make guest materialization idempotent. The overlay id is instance-scoped,
and the host re-ships on every reinstall (60s after connect, and on later
pane spawns), so materializeOpenCode's remove-and-rebuild wiped the config
root under running agents and raced panes spawning against the path just
handed to them. Rebuild only when the shipped source changed or the overlay
went missing.
- Mirror the guest's default ~/.config/opencode (honouring XDG_CONFIG_HOME)
when no explicit dir is discoverable, so pointing OPENCODE_CONFIG_DIR at the
overlay no longer silently drops the user's models/agents/skills/mcp.
- Carry opencodeOverlayDir across relay relaunch; it is instance-keyed and on
the distro's persistent filesystem, so dropping it only blanked status on
panes spawned mid-relaunch.
* fix(agent-hooks): stop advertising a WSL OpenCode overlay the guest failed to rebuild
Round-2 review fixes:
- materializeOpenCode wipes before rebuilding, and every failure path after the
wipe returns null leaving the dir present but plugin-less. The host treated
that null the same as "no handler / teardown" and silently kept the previous
value, so a pane could be pointed at an empty config root -- worse than the
documented fallback of dropping the var. requestGuestOpenCodeOverlayDir now
distinguishes 'none' (guest answered, no dir) from 'unavailable', and the
manager clears the recorded dir on 'none'.
- The handler cache keyed only on plugin source, so a ~/.config/opencode created
after the relay connected was never mirrored for the relay's lifetime. Key on
the resolved source dir too, and validate the cache by the plugin file rather
than the directory -- the directory is exactly what a failed rebuild leaves
behind, so checking it alone made the bad state stick.
* fix(agent-hooks): don't mirror the XDG default OpenCode config into the WSL overlay
OPENCODE_CONFIG_DIR is APPENDED to OpenCode's config-dir list, not a
replacement for it. Verified against the shipped binary: the list is built as
[Path.config, ...project .opencode dirs, ...OPENCODE_CONFIG_DIR ? [it] : []],
and Path.config is derived independently from XDG_CONFIG_HOME/$HOME/.config.
So ~/.config/opencode is read whether or not Orca overrides the var, and the
earlier fallback that mirrored it into the overlay made OpenCode load the
user's config -- and their plugins -- twice. Resolve only an explicitly-set
dir, which is the one case that genuinely leaves the list when Orca overwrites
the variable. This also restores parity with the SSH and local paths.
* docs(agent-hooks): correct the WSL install-plugins cache comment and test framing
The per-call source-dir re-resolution comment still described the XDG default
branch that 6745eba90 removed. The relay's env is fixed for its lifetime and
the rc scan behind it is memoized, so the sourceDir cache key is defensive
rather than live -- say so, and retitle the test that simulates it by mutating
the captured env, so neither reads as coverage of a production scenario.
* Fix fork PR/MR worktree creation race via durable review-head refs
When creating a fork PR/MR worktree, concurrent `git fetch origin` operations
clobber the shared FETCH_HEAD, causing the wrong commit to be checked out.
Fetch PR/MR heads into dedicated per-review refs (`refs/orca/pull/<N>`,
`refs/orca/merge-requests/<N>`) that persist and isolate each head from other
fetches. Gracefully keep the compare-base when the fetch fails but the local
ref already exists, avoiding silent fallback to the wrong branch on transient
network errors.
* Bound PR/MR head fetches with 60s timeout
Prevent PR/MR creation from hanging when a remote is stalled or
unreachable. Both GitHub and GitLab head fetches now enforce a
60-second timeout, matching the bound used in the create-path
fetch. Durable refs (refs/orca/pull/*, refs/orca/merge-requests/*)
decouple the ref from FETCH_HEAD, preserving legacy client semantics.
* test: align CI expectations with main PowerShell/sparse regressions
PR checks merge into main, which recently changed PowerShell launch args
(cwd restore after profiles) and sparse-checkout detection (require
core.sparseCheckout). Derive PowerShell spawn args from the production
resolver, mock the sparse config flag, reset shared worktree list scan
cache between tests, and stop requiring floating polls to avoid getRepos
hydration.
* Address review follow-ups on durable review-head refs
- Unify PR review-head remote selection: local and SSH GitHub paths share
resolveGitHubReviewHeadRemote, which prefers the remote mapping to the
hosting GitHub project (upstream before origin, matching work-item/API
candidate order) so contributor clones fetch refs/pull from the repo
that actually hosts the PR.
- Soft-keep durable review heads: when the PR/MR head fetch fails but
refs/orca/pull/<N> / refs/orca/merge-requests/<iid> still resolves,
keep the pinned SHA (warn) instead of failing resolve, mirroring the
compare-base fallback. Extracted shared compare-base soft-keep into
compare-base-ref-fetch.ts.
- Extract fetchGitLabMergeRequestHeadRef (local + SSH) parallel to the
GitHub helper; bound its local fetch with the shared 60s timeout.
- Share relay-style fetch validation (positive safe-integer id, remote
not starting with "-") between relay and local helpers via
review-head-tracking-ref.ts; move REVIEW_HEAD_FETCH_TIMEOUT_MS there.
- Drop the githubPullRequestHeadLocalRef re-export; resolve head SHAs via
rev-parse --verify <ref>^{commit}.
- Add GitLab anti-FETCH_HEAD regression test plus durable-head soft-keep
and remote-selection unit tests.
Co-authored-by: Orca <help@stably.ai>
* test: supply live getRepos for terminal-retirement hydrates
Main's headless tab hydrate (#9343) skips worktree keys whose repo is not
in getRepos. Retirement tests that rebuild mobile tabs from a persisted
session now advertise the fixture repo as live so PR Checks merge stays green.
* fix(editor): extract RichMarkdownEditor props to stay under max-lines
Main's SSH external-image wiring (#10323) pushed RichMarkdownEditor.tsx over
the 400-line tsx budget, failing PR Checks lint on every merge into main.
Move the props type into a sibling module so the component stays under the
limit without disabling max-lines.
* Make durable review-head refs remote-identity scoped
Embed remote name + URL hash into refs/orca/pull|merge-requests refs to prevent soft-keep from serving wrong project's PR/MR when FETCH_HEAD is clobbered by concurrent fetch. Fetch functions now return the written ref path (writer-authoritative) so callers rev-parse exactly what was fetched, not re-derive identity. Soft-keep only applies to transient errors (timeout, network); fails hard on missing refs, auth failures, and stale relay. Relay returns localRef so client avoids re-hashing (URL normalization can disagree).
---------
Co-authored-by: Orca <help@stably.ai>
* fix(relay): honor the remote OpenSSH login shell for Windows SSH terminals (#9327)
* Prove OpenSSH DefaultShell fallback stays safe on bad registry reads
* fix(quick-open): guide rg install after fallback budget errors
* fix(quick-open): show local host wording for local install guidance
The install-rg guidance component hardcoded 'on the remote', so the new
local fallback path told local users to install ripgrep 'on the remote'
— wrong for the exact case #9627 targets. Parse the location out of the
message and render the matching wording; add the local locale string and
a render test that guards against the 'on the remote' regression. Also
harden the reason capture against a stray ')' in the error text.
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com>
Co-authored-by: Orca <help@stably.ai>
Orca spread its whole process.env into every spawned terminal, so a dev build leaked NODE_ENV=development into integrated terminals (breaking next build, Vitest, etc.). Strips the inherited value at all three PTY-spawn sites (daemon, local provider, relay); a value the user sets themselves still wins.
Supersedes #9058, which patched only the relay path and so missed the reported (local daemon) repro.
Closes#9057
Co-authored-by: John Debay <john@debay.net>
* fix(pi): resolve OMP tab identity from the outer wrapper, not the wrapped pi (#6364)
OMP runs as a `shell → omp → pi` process tree and Orca recognizes both `omp`
and `pi` as distinct agents. The foreground-process readers scored the deepest
`+` foreground descendant, so they returned the wrapped `pi` engine instead of
the `omp` the user actually launched — and because the sampled `+` frame moves
between omp and pi across command boundaries, the read oscillated and the tab
icon/label flickered OMP↔Pi. It surfaced on Remote Host / SSH (only in a
worktree, only while working) where a mirrored/restored pane has no client-side
launchAgent to anchor identity, so the (host-read) foreground was the only
pi/omp signal — and it was wrong.
Fix the detection at its source so foregroundAgent is trustworthy everywhere
(local tab icon, exit detection, and the remote mirror):
- Add `resolveOuterWrapperForegroundProcess` (src/shared/foreground-wrapper-agent.ts):
when the recognized foreground winner shares a `titleIdentityGroup` with a
shallower recognized ancestor, return the shallowest — the outer wrapper.
Stable regardless of which of omp/pi holds the foreground at the sample; bare
Pi and cross-group reads (Codex, etc.) are unchanged.
- Wire it into both readers: local `agent-foreground-process.ts` and the SSH
relay `pty-shell-utils.ts`. Also harden the relay's fallback short-circuit so a
pi-compatible process reported by node-pty rescans for the omp wrapper instead
of trusting `pi` raw (never downgrading below the fallback).
Also unify the host-side publish path onto the shared owner resolver: the mobile
session snapshot builders now resolve pane ownership via `resolvePaneAgentOwner`
({launchAgent, hookAgent}) instead of a bespoke `launchAgent ?? foregroundAgent`,
so the host stops being a divergent fourth identity path and the hook's
host-stamped identity survives when launchAgent is dropped on a mirrored pane.
Tests: unit coverage for the wrapper resolver, `shell→omp→pi` ps trees for both
readers (outer omp, oscillating-frame, bare pi, node-pty pi fallback), and a
host-snapshot regression that keeps an OMP hook labeled OMP when the wrapped pi
child owns the foreground.
* fix(pi): harden wrapper foreground ownership
* fix(pi): preserve OMP ownership in daemon foreground reads
* fix(pi): retain OMP owner when process scans fail
* fix(source-control): keep huge change sets responsive
* Fix cancellation and retry handling for capped status
* Harden capped status for conflict-heavy repositories
* Harden capped status recovery and cancellation
* fix(source-control): preserve capped status correctness
* fix(source-control): translate submodule status at render time
---------
Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
Move managed agent-hook filesystem work behind one relay RPC so high-latency SSH connects pay one WAN round trip instead of hundreds. Keep installers serial, lock shared account config across relay processes, and fence cancelled connection generations from replacement state.
Co-authored-by: nasagong <zinho2000@gachon.ac.kr>
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
Collapse multi-line explanatory comment blocks into single-line "why" statements
per AGENTS.md ("Document the Why, Briefly"): drop restatements of the code and
mechanism narration; keep the non-obvious reason, external refs, and directives.
Comments-only — verified no code changed via a Babel/esbuild comment-strip
token-equality gate against origin/main; typecheck and oxlint clean.
Area: shared, cli, relay, preload. 26 files changed, 1039 insertions(+), 3300 deletions(-).
Co-authored-by: Orca <help@stably.ai>
* feat(agents): pi session resume support
* fix(pi): require persisted session files for resume
* test(sleeping-agent): use non-resumable sentinel in malformed-record fixture
The 'drops malformed sleeping agent resume records' test used agent:'pi' as
its example of an unknown/non-resumable agent, expecting the record to be
dropped. This PR added 'pi' to RESUMABLE_TUI_AGENTS, making that fixture
valid and retained, so the toBeUndefined assertion broke. Switch the
malformed-case fixture to a genuinely non-resumable sentinel
('definitely-not-an-agent') so the drop-malformed path is still exercised;
no other assertions changed.
* Add durable resume identity for Pi sessions without fabricating turn sta
Pi's `session_start` hook now carries the session file needed to resume
a sleeping pane, but until now Orca either discarded it or treated it
as a fake status transition. Thread a `providerSessionOnly` envelope
through the hook listener, relay, main-process server, and renderer
store so resume identity (and its session-file-scoped equality/claim
key) can be persisted and replayed without emitting prompt telemetry
or a visible working/done row.
* Add durable resume identity for completed Pi sessions
Pi's agent_end hook marks a turn done, but the underlying TUI session
stays alive and resumable. Previously a `done` status wiped sleeping
records and launch config as if the session ended, so hibernation,
manual worktree sleep, and quit-capture all lost Pi's resume identity.
- Track a "live recovery" record for done-but-still-resumable Pi
sessions, exempting it from the usual done-state cleanup paths in
agent-status.ts and agent-hibernation-planner.ts
- Gate providerSessionOnly rows and sleeping-agent schema records on
actual resumability (getAgentResumeArgv) instead of trusting the
presence of a provider session
- Wait for Pi to persist its session file before advertising resume
metadata, and treat `/reload` as a non-terminal event so it doesn't
clobber visible status
- Extend SSH relay envelopes to carry providerSessionOnly so remote
hosts get the same behavior
* Add explicit periodic/quit mode to sleeping-agent session capture
Split captureAllSleepingAgentSessions into 'periodic' and 'quit' modes
so a background checkpoint can no longer downgrade a confirmed-quit
record or promote a completed Pi session without an authoritative
transcript path. Updates all call sites and tests accordingly.
* Use normalizeAgentStatusPayload for default pi status
Remove unnecessary JSON.stringify wrapper and call the appropriate normalization function directly.
---------
Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com>
* Add mobile image-diff previews via shared data-URI builder
- Extracts a `buildImageDataUri` helper (src/shared/image-data-uri.ts) shared by
the desktop ImageViewer and mobile, so both trim whitespace-wrapped base64 and
skip non-previewable mimes (e.g. application/pdf) the same way.
- Adds mobile-diff-image-preview.ts to render binary git.diff results (add/modify/
delete) as images instead of falling back to "Binary preview unavailable".
- Extracts resolveMobileFileTabDoc to consolidate the session file-tab loading
logic (diff/image/html/text) out of the route file for testability.
* Fix stale binary image fallback for empty modified diffs and relay reads
- mobileDiffImageDataUri now distinguishes a true deletion (modified
side absent) from a modify whose binary bytes arrived empty
(relay/size-cap cases), returning null instead of the stale
pre-change image
- readWorkingDiffFile passes the file path to bufferToBlob so relay
working-tree reads can detect previewable image extensions instead
of always reporting empty binary content
- add mobile-file-tab-doc.test.ts covering diff/image/binary/text
resolution paths
* Regenerate skill bundle manifest for 1.4.144-rc.2
Co-authored-by: Orca <help@stably.ai>
* fix(review): trim comments to AGENTS.md's one/two-line why-only rule
Comments in mobile-diff-image-preview.ts and mobile-file-tab-doc.ts ran
3-6 lines and narrated mechanism instead of stating only the non-obvious
reason, per AGENTS.md's "Code Comments: Document the Why, Briefly" rule.
Co-authored-by: Orca <help@stably.ai>
* Distinguish read failures from true deletions in binary diff results
- Working-tree stat/readFile errors and relay reads previously collapsed
onto the same empty-content signal as a genuine deletion, letting
previewers fall back to stale original bytes on a failed read.
- Add modifiedDeleted/missing flags through status.ts, git-handler-ops,
and git-working-file-read so only proven deletions trigger the
original-bytes fallback; failed reads now return null.
- Tighten buildImageDataUri to accept only image/* mimes instead of
special-casing application/pdf.
* fix(relay): expect missing:false on index blob maxBuffer overflow
readBlobAtIndex now returns a missing flag so staged deletions are
distinct from size-capped binary reads; update the overflow test.
* Allow opening deleted files to show pre-delete text or image diffs
Deleted files can now be opened to view their pre-delete content via
git.diff (including images via modifiedDeleted). Only unresolved conflicts
remain unopenable. Centralizes the canOpen rule in canOpenMobileGitStatusEntry()
to keep opener guards consistent across the mobile source control UI.
---------
Co-authored-by: Orca <help@stably.ai>
* feat(native-chat): add native chat view across mobile
* fix(native-chat): address review findings and CodeRabbit threads
Correctness:
- Restore an independent initial readSession seed and surface initial-drain
errors as snapshot frames so the chat view can never strand on 'loading'
- Pair mobile tool results to calls by ordinal FIFO (parallel calls no longer
misgraft results); clear a pending ask only when its own call resolves
- Show a new streaming reply immediately (same-turn suppression, not length)
- Delegate mobile noise filtering to the shared harness-injected classifier
- Admit soft-leaving mobile clients in beginMobileInputFloor (parity with
mobileTookFloor) so grace-window writes aren't dropped
- Self-heal a stale 'working' status once this turn's reply lands
- Catch RPC rejections in mobile file-open helpers; guard sanitizeToolInput
key collisions; settle web/runtime transports on unrecognized first frames
and forward snapshot errors
Perf:
- Throttle the mobile streaming bubble (50ms) so per-part status frames stop
re-parsing the whole accumulated markdown
- Short-circuit markdown path detection on dot-less or oversized runs
(quadratic backtracking guard)
UX/minor:
- Wire hold-mode dictation through the native chat composer
- Allow scoped-package (@) paths in file-path detection
- Move caret after mid-text autocomplete insertion; index-prefixed ask option
keys; single scroll-to-end effect; bounded wait + toast when image attach
races a resubscribe; count-based pending reconciliation; cache-hit search
cancels stale debounce; chat-tab toggle wins over in-flight preference load
- Share shouldStepNativeChatAskAnswer between desktop and mobile; import
block guards/source priority from shared instead of local copies
- Defensive non-positive transcript limits; test strengthening (TTL expiry,
post-unsubscribe stale frame, lease readiness, filtered console.error)
* refactor(native-chat): share desktop/mobile chat logic in src/shared
Extract the parity-mirrored native-chat modules into shared implementations
both surfaces re-export: ask parsing (registry, parseAskFromStatus,
extractPendingAsk, formatAskAnswer), answer stepping offsets/scheduler, diff
detection/parsing, harness-noise filtering, tool fold/pair/split, and tool
summaries. Removes the hand-synced copies and their stale Metro comments.
Divergence reconciliations take the safer side of each: diffs truncate at
120 lines/32KB everywhere (desktop previously unbounded), tool-run summaries
cap at 3 parts with bounded-depth previews, nameless tool calls are skipped,
and basenames split on both separators.
Also: settle and kill every sibling quick-open pass when one reaches
maxResults (main rg/git and relay git; relay rg already did) so a capped
search cannot leave a scan walking a huge tree; fold window-bounding into
the shared merger's applyAppend; localize the web 'Pair a host' snapshot
error.
* fix(native-chat): address CodeRabbit follow-ups on shared modules
- Attachment lease gate re-checks connection/target/tab after the bounded
wait, so a tab/host switch or disconnect mid-wait can't send into a stale
terminal; a moved-away target drops silently like the pre-wait guard and
only an unrecovered lease surfaces the toast. Adds hook tests.
- extractPendingAsk parses transcript tool-calls through the same
registered-parser + canonical-shape fallback as live status, so a custom
question tool that rendered live survives reconnect/replay.
- Direct unit tests for the shared ask parser (FIFO ordering, fallback,
malformed payloads) and tool-summary bounded preview (depth/collection
caps, circular refs, basename/command branches).
* fix(native-chat): treat initialLimit 0 as a valid empty window
Both engine guards used truthiness, so an explicit zero limit skipped the
bounded tail reader and fell back to an unbounded incremental read. Latent
only (every caller clamps positive), hardened for consistency with the
tail reader's non-positive-limit handling.
* fix(mobile): native-chat composer lock UX + send-failure feedback
- Distinguish input-lock reasons: transport 'disconnected' shows Reconnecting…
instead of mislabeling a reconnect as locked-by-another-client
- Guard the composer lock behind a 600ms hold so connState blips / lease
hand-offs don't flicker the placeholder; unlock stays instant
- Surface a rejected send inline above the composer (a bottom toast hides
behind the keyboard); auto-dismisses after 4s
- waiting-session hint invites the first message instead of implying the
agent is still starting
* test(mobile): sync answer-send pacing test to the 500ms advance buffer
Missed in merge 8fe3c391c, which carried main's NATIVE_CHAT_ADVANCE_BUFFER_MS
300->500 (#8568) into the shared stepping module that mobile derives from.
* fix(mobile): restore terminal stream after chat cold start
* fix(native-chat): harden retries, optimistic sends, and file scans
* fix(mobile): deliver AskUserQuestion answers by option number (STA-1860)
Port #8840's fix to the mobile native chat: the Ask card now tracks
per-question option INDICES (+ free text) and the answer-send hook drives
Claude's arrow-navigate selector with buildAskAnswerKeys keystroke groups —
option numbers, next-tab arrows, Enter — paced one selector step apart, instead
of pasting label text that the selector ignores (which silently committed the
default option). Non-Claude agents keep the pasted-label path via the
selection-based formatAskAnswer.
Backcompat: keystrokes are built client-side and written through the EXISTING
terminal.send passthrough with enter:false — the same contract the permission
card already uses — so an older desktop runtime (SSH/relay included) replays
them verbatim; no RPC/contract change in either update order. Free text is
newline-sanitized because terminal.send has no paste framing.
Drops the now-unused formatCompleteAskAnswer from the shared module.
* fix native chat send and runtime races
* fix mobile native chat formatting
* fix(native-chat): mobile empty state matches desktop copy
Mobile showed a single generic line ('Send a message to get started') where
desktop shows a titled two-line empty state naming the agent ('Start a chat with
Claude' + 'Ask Claude to inspect code, explain output, or make a change.'). Align
them from one source of truth so they can't drift again:
- Extract the agent-type label map + formatAgentTypeLabel to
src/shared/agent-type-label.ts (desktop re-exports; mobile imports).
- Add src/shared/native-chat-empty-state.ts with the canonical English copy;
desktop uses it as its i18n fallbacks (localization unchanged — en/es/ja/ko/zh
keys still win), mobile substitutes the agent label and renders it directly
(mobile ships English only).
- Mobile: render title + subtitle for waiting-session AND ready-but-empty (both
are 'start a chat'), error copy for errors; keep the loading spinner.
Live-verified on the iOS sim against a pn-dev of this branch. typecheck node/web
+ mobile tsc clean; 30 mobile + 428 desktop/shared native-chat tests green.
* style: oxfmt the empty-state parity test (line wrap)
---------
Co-authored-by: Brennan Benson <brennanbenson@Brennans-MacBook-Pro.local>
Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
* feat(git-status): batch, cancel, and cache git status polling to cut idl
- Add a single duty-cycled refresh scheduler (activity debounce + 60s
safety timer) replacing multiple overlapping intervals, so status
polling no longer runs near-continuously on large repos (#7983).
- Let safety refreshes reuse cached numstat line counts instead of
re-running diff --numstat every cycle, invalidated by head change,
known mutations, and a bounded TTL.
- Thread AbortSignal/request-token cancellation through IPC, RPC, and
relay layers so a superseded or backgrounded git:status call is
killed instead of finishing wastefully.
- Fix automatic upstream/status apply ordering so a slow, older
refresh can no longer clobber a newer result, and so an earlier
refresh still applies when a later one fails.
* Fix aborted git status scans being mistaken for completed empty results
- An aborted scan/numstat pass now always rejects instead of silently
resolving, so a cancelled request can't look like a valid empty status.
- Stop clearing the line-stats cache key on abort, since an aborted pass
never wrote to it — clearing was evicting a concurrent scan's healthy
snapshot and forcing a redundant numstat recompute.
* Fix aborted git status scans resolving as completed results
Cancelled scans could still resolve with partial or stale data instead of
rejecting, letting callers treat an aborted refresh as a valid status. Also
stop counting aborted scan duration toward catch-up refresh pacing, which
was stretching the next refresh interval by the full length of a cancelled
(often slow) scan.
* Add cancellable, generation-aware git status polling to cut stale scans
- Route git.status through an abortable subscription per requestToken so
cancelStatus can actually abort the remote scan instead of being a
no-op, preventing wasted work and stale responses overwriting fresher
state.
- Bump the git status polling generation on push-target changes so an
in-flight refresh against the old remote/branch can't apply stale
upstream data to the new one.
- Guard the stale-conflict poller against writes after unmount.
- Retire pre-purge line-stat scans in the cache so an older in-flight
scan can't repopulate a key after a token-scoped purge.
* fix(worktrees): stop surfacing prunable git worktrees as live workspaces
A worktree still registered in git but whose directory was deleted
(git's `prunable` state) was enumerated as a normal workspace,
producing repeated pty:spawn DaemonProtocolError / fs:readDir ENOENT
loops and a blank pane.
- Parse the `prunable` porcelain field (Git >= 2.36) in both the main
and relay worktree-list parsers.
- For Git < 2.36 (no `prunable` field), probe each linked worktree
path for existence on the fallback line-block path, skipping locked
registrations to mirror git's own prunable rules.
- Omit prunable worktrees from the detected-workspace enumeration only;
removal/cleanup flows keep seeing them.
- Extend the real-binary compatibility contract with the 2.36
`prunable` boundary.
Fixes#8389
Claude-Session: https://claude.ai/code/session_018Rg1Bpq4GGwmz613hq6RSD
* fix(worktrees): pin the prunable/locked porcelain annotations to their real Git 2.31 boundary
The prunable and locked annotations landed in Git 2.31, five releases
before `worktree list -z` (2.36); only -z defines the capability
fallback boundary. Correct the compatibility contract so a future
matrix entry in the 2.31-2.35 range passes, and reword the fallback
comments: on 2.31-2.35 the annotations still parse and the existence
probe is a backstop; only Git <2.31 relies on it outright.
* fix(worktrees): omit prunable registrations from the Space scan
A prunable registration has no directory to size or reclaim, so Space
rendered it as a dead "Missing" row whose checkbox stayed disabled with
no prune/remove affordance (reported on macOS after a reboot cleared
/private/tmp under 16 registrations). Skip prunable entries in the scan,
matching the workspace enumeration; removal flows list worktrees
separately and still see them.
---------
Co-authored-by: kaynan <kaynan.camargo@terceiro-sky.com.br>
Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
* fix(runtime): retain watcher and PTY teardown ownership
* fix(runtime): restore watchers after interrupted cleanup
* fix(runtime): prevent stale watcher revival
* test(runtime): cover watcher shutdown ownership
* test(daemon): model physical PTY exit
* fix(daemon): keep shutdown terminating when disposal cannot prove exit
A rejecting host.dispose() (unreapable child past its exit deadline) left
the shutdown RPC without its process.nextTick(shutdown) and skipped socket
cleanup in shutdown(), stranding the daemon as an unreachable orphan after
the stale-daemon replacement flow unlinks its socket. Log and continue:
daemon exit reparents the child to init instead of blocking on it.
* fix(runtime): keep local watching alive after an idle-kill deadline miss
An idle child that outlived the exit deadline set shutdownRequested on the
shared desktop supervisor, which has no retire-and-replace path — every
later subscribe rejected supervisor_disposed and the roots were cached
unwatchable, silently ending local file watching for the session. The idle
path owns zero records, so there is no double-watch hazard; the zombie
keeps its capacity reservation until physical exit and the next subscribe
gets a fresh child.
* fix(renderer): resync replayed paired-web file watches
Transparent replay removed the implicit resync the old close-and-rebuild
path provided: a replayed files.watch only reports changes from its own
native setup, so changes during the reconnect gap were silently lost.
Deliver a conservative overflow to consumers once the replayed watch is
ready, matching the overflow-after-interruption contract everywhere else.
* fix(runtime): address teardown review findings
* fix(runtime): retry watches after teardown deadlines
* Fix PTY descendant leaks on forced teardown
* Fix jitter-sensitive terminal lifecycle test
* Suppress Git Credential Manager OAuth popup on git clone (fixes#7652)
Orca's git runner disables the interactive credential prompt on every git
call that goes through gitExecFileAsync/gitStreamStdout, but the two raw
'git clone' spawns (desktop repos:clone and the runtime clone path) passed
no env, so they inherited process.env with no guard. On Windows a clone
that needs GitHub auth then makes Git Credential Manager pop its
'Connect to GitHub' OAuth window, and in a network-restricted intranet the
browser/device flow never completes while git's credential retry re-pops it.
Apply nonInteractiveGitEnv() to both clone spawns so the prompt is
suppressed (GCM_INTERACTIVE=never, credential.interactive=false,
GIT_TERMINAL_PROMPT=0). The credential *helper* is kept, so cached-token
clones for private repos still work; only the interactive fallback popup is
disabled and the clone fails fast with a clear error instead.
* Suppress GCM OAuth popup in agent terminals and setup hooks too (#7652)
The clone-spawn fix stopped Orca's own managed git from popping Git
Credential Manager, but git run in terminals and setup scripts inherited
process.env with no guard. That is the more likely source of the reported
loop: agents are told to run 'git pull --rebase'/'git fetch'/retry 'git
push' (preamble + conflict/push-failure prompts), and each retry re-pops
GCM's 'Connect to GitHub' window in a network-restricted intranet.
Apply the credential-prompt guard to:
- setup/archive/hook scripts (hooks.ts non-WSL exec env), which run
unattended on worktree create/archive.
- the shared PTY host env (buildPtyHostEnv), via a small
applyTerminalGitCredentialPromptGuard helper. Agent terminals are
guarded unconditionally (they cannot dismiss a GUI popup); user
terminals are guarded by default via the new
terminalSuppressGitCredentialPrompt setting so power users can opt out.
The credential helper is kept, so cached gh auth still works; only the
interactive fallback prompt is disabled. Verified end-to-end in a real
Orca terminal (GIT_TERMINAL_PROMPT=0 + GCM_INTERACTIVE=never by default;
absent when the opt-out is set).
* Scope user-terminal credential guard to Windows, add settings toggle, forward guard into WSL (#7652)
* Retrigger PR checks (Actions dropped the synchronize dispatch for 57e7ce249)
* Keep shell locale out of the terminal/hook credential guard (#7652 review fix)
* Fix Fable review findings: guard WSL hook branch, wire settings search, catalog keyword keys, sparse-env askpass, one-shot agent classification (#7652)
* fix(terminal): harden Git credential popup guard
* test(pty): cover SSH credential guard setting
* fix(git): guard remote clones and setup runners
* fix(git): scope credential guards to unattended work
---------
Co-authored-by: Brennan Benson <brennanbenson@Brennans-MacBook-Pro.local>
* Fall back to a merge when a divergent pull has no reconciliation strateg
- Git 2.27+ refuses `git pull` on divergent branches unless pull.rebase or
pull.ff is configured. Retry with `--no-rebase` (Git's historical default)
so pulls succeed out of the box on fresh hosts.
- Skip the fallback whenever the caller already specified a reconciliation
strategy (e.g. --ff-only, --rebase) so explicit policies still fail as
expected on divergence.
- Applied identically in the local git pull path and the relay/SSH git
handler so both surfaces behave the same way.
* Refactor divergent-pull merge fallback into shared helper
Extracts the retry-as-merge logic (duplicated between local git and
relay SSH pull paths) into `runPullWithDivergenceFallback` in
git-remote-error.ts, so both callers share one implementation and
test coverage.
* Fix PTY shutdown crash on Windows by dropping unsupported signals
- node-pty's Windows agent throws when kill() is called with a signal
argument; route all pty.kill() calls through killPtyProcess(), which
drops the signal on win32 and forwards it on POSIX
* Expand Windows PTY kill-signal-drop test coverage to all call sites
Replace the single graceful-shutdown test with a suite covering every
place killPtyProcess is invoked (graceful/immediate shutdown, stale-spawn
cleanup, SIGKILL fallback timers, and dispose), so a future regression
that reintroduces a signal argument on any Windows code path is caught.
* fix(ssh): isolate relay filesystem watchers
* Fix relay watcher fault-harness pid file and in-process fallback isolati
- Use exclusive ('wx') creation for the fault-harness pid file so a leaked
ORCA_WATCHER_CHILD_PID_FILE env var can't clobber an existing file, and
have the harness remove the file after reading a replacement pid.
- Force useInProcessVitestFallback to false in the relay watcher pool so a
leaked VITEST env var can never load the native watcher addon in-process
on the relay; fail closed instead when the isolated child is missing.
- Thread an injectable RelayWatcherProcessPool into FsHandler/
RelayFilesystemWatchRegistry for tests, and add coverage for both fixes.
The no-ripgrep git fallback ran both ls-files passes under Promise.all
with killSurvivors on first rejection, so any ignored-pass failure
(timeout, kill, non-zero exit) discarded the successful primary listing
and Quick Open showed an error with zero files — the all-or-nothing
failure called out in #7719.
Directory collapse (#7842) bounds the enumeration so timeouts are far
less likely, but a single-pass failure still nuked every result. Now,
in the local main process and the SSH relay alike, only a primary-pass
failure is fatal: an ignored-pass failure logs a warning and the
listing resolves with the primary results plus any ignored entries
streamed before the failure. Cancellation semantics are unchanged —
an aborted scan still rejects via the primary pass or the expansion's
cancellation check.
Fixes#7719
Co-authored-by: Brennan Benson <brennanbenson@Brennans-MacBook-Pro.local>
* fix(grok): restore clipboard and native-chat parity
Grok CLI already supports argv prompts, OSC 52 copy, and image paste chips.
Orca was blocking those paths: stdin-after-start keystroke injection, OSC 52
writes default-off, image-attachment denylist, and native-chat allowlist.
- Launch Grok with positional argv prompts
- Default OSC 52 TUI clipboard writes on (still user-toggleable)
- Treat Grok as image-attachment capable
- Parse ~/.grok/.../chat_history.jsonl for native chat
OSC 52 clipboard *query* remains ignored by design (host clipboard exfil risk);
xAI docs only require OSC 52 write for remote copy.
* fix(grok): sync OSC 52 docs and locale catalog with default-on
Update terminalAllowOsc52Clipboard type docs for the true default, and
refresh locale strings so settings UI mentions Grok alongside other TUIs.
* fix(grok): tool hook matcher, StopFailure, previews, AskUser waiting
Grok tool-event matchers are real regexes; bare `*` failed as match-all.
Install `.*` for Pre/Post tool hooks, add StopFailure for API-error ends,
recognize Grok-native tool input keys, and map ask_user_question PreToolUse
to waiting with interactivePrompt (Kimi-style live card path).
* fix(grok): resolve chat_history under GROK_HOME and long-cwd layouts
Centralize Grok session path helpers so hooks and native-chat honor
GROK_HOME and find chat_history.jsonl by session id when the cwd group
is slug-encoded (encoded name > 255 bytes) instead of only
encodeURIComponent(cwd).
* fix(terminal): keep Kitty keyboard for Grok on Windows ConPTY
Local Windows ConPTY withholds KKP so CSI-u-blind CLIs (e.g. Antigravity)
keep Enter/nav working (#2434). Grok needs KKP for Ctrl+Enter interject and
modified-Enter newline chords; blanking the advertisement for Orca-launched
Grok left those actions broken.
- Prefer KKP when tuiAgent is grok despite ConPTY withhold
- Wire launchAgent from tab/startup into keyboard protocol options
* fix(grok): restore OSC52 default-off, split decoders, honor GROK_HOME hooks
- Keep terminalAllowOsc52Clipboard default false (clipboard exfil risk)
- Split transcript-line-decoders under max-lines without suppressions
- Install local Grok hooks under resolveGrokHomeDir() / GROK_HOME
* refactor(grok): share CLI home resolution
* fix(grok): harden terminal and native chat integration
* test(grok): align CI coverage with native chat support
---------
Co-authored-by: Jinwoo Hong <73622457+Jinwoo-H@users.noreply.github.com>