* fix(keybindings): show the ⇧ glyph for Shift on Windows and Linux
Spelled-out Shift made non-mac shortcut labels long (Ctrl+Shift+Backspace).
* test(menu): expect the ⇧ glyph in non-mac app menu hints; align double-tap hint
* fix(windows): keep Orca's std handles out of every spawned child
libuv spawns with bInheritHandles=TRUE, so an inheritable std handle reached
long-lived children such as the WSL holder and the daemon and held a
launcher's pipe open after Orca exited. Clear HANDLE_FLAG_INHERIT on the std
handles at the top of main startup; stdio 'inherit' still works because
libuv hands the child its own inheritable duplicate.
* test(windows): model Orca's Electron main, which keeps std handles inheritable
Plain Node clears its std handles' inherit flag at startup, so a Node child
never reproduced the leak. Electron main opts out of that, which is why Orca
leaks and why the control arm now runs Electron; a plain-Node arm pins the
difference.
* test(windows): say why the std-handle tests need longer timeouts
The cached policy gains an optional movedProjects: keep | bring-back, read
only with a block. An unknown value reads as keep, so it never drops the
block, and older builds ignore the field. The gate records the choice for
the run (wslHostsBringBackMovedProjects) and telemetry reports it as
wsl_hosts_moved_projects. Nothing acts on it yet; bringing projects back
follows.
Co-authored-by: Claude <noreply@anthropic.com>
* feat(wsl): offer the drive-path conversion in project settings, and convert the window's stores (D1c-4d)
* fix(wsl): tell the user why saved state blocked the drive-path conversion
A refusal over saved state now names the cause: a folder with different details under both its WSL
path and its drive path, or a project that also holds a repository that isn't converting. An older
host's refusal without a cause keeps the general message.
* fix(wsl): explain a conversion refused because the distro's /mnt/<letter> is not that Windows drive
Co-Authored-By: Claude <noreply@anthropic.com>
* test(wsl): give the renderer-rewrite journal fixture the profile the journal now records
* test(wsl): give the e2e conversion journal the profile field the journal now requires
* test(wsl): carry the #33135 startup tests onto the window-store journal: it waits for the window, and finishes on any platform once the gate is open
* test(wsl): off Windows the conversion IPC still reports startup's outcomes, as the journal can finish on any platform
---------
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
* fix(cli): never register the bundled launcher as a link to itself
orca serve registers the CLI at startup. On macOS the installer walks
PATH for an existing orca command, and Orca's own terminals put the
bundled Resources/bin first on PATH. The bundled launcher reads as a
managed Orca launcher, so it was picked as the command slot, moved
aside, and replaced with a symlink to itself. The app's CLI then broke
until reinstalled.
Skip the launcher when scanning PATH, and refuse any symlink install
whose command path is the launcher.
Fixes#33872
* fix(cli): compare the launcher by directory entry, not path spelling
The self-link guards compared path strings, so a PATH entry or
ORCA_CLI_INSTALL_PATH that reached Resources/bin through a symlinked or
differently cased directory still replaced the bundled launcher with a
link to itself. Compare entry and parent-directory identity instead.
Co-authored-by: Hotragn <103170876+Hotragn@users.noreply.github.com>
* fix(cli): keep differently named hard links to the launcher replaceable
* fix(cli): treat a case-variant name as the launcher only when it is an alias, not a second hard link
* refactor(cli): compare the launcher by inode via existing entry snapshots
Replace the speculative isSameDirectoryEntry helper with isSameEntry beside
readEntrySnapshot/hasSameIdentity, and drop the hard-link test that only
pinned the removed behavior.
Co-authored-by: Hotragn <103170876+Hotragn@users.noreply.github.com>
* Revert "refactor(cli): compare the launcher by inode via existing entry snapshots"
This reverts commit f02f8c2a2ba0263fd611170af7c3f76d22e48ea7.
* test(cli): fold the bundled-launcher cases into the macOS command-path suite
* refactor(cli): move the launcher entry check beside the entry snapshots
Reuse readEntrySnapshot/hasSameIdentity for the entries, keep the parent
identity check (a hard link in another dir such as /usr/local/bin must stay
replaceable, which is why f02f8c2a2ba was reverted), and drop the readdir
case-folding branch. Pin the cross-directory hard link instead of the
speculative sibling one.
Co-authored-by: Hotragn <103170876+Hotragn@users.noreply.github.com>
* fix(cli): keep a case-variant hard link beside the launcher replaceable
---------
Co-authored-by: Neil <neil@stably.ai>
* fix(i18n): code AI Vault scan issues and chat auto-name failures for the renderer
* fix(i18n): read scan issue params through the wire type
---------
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
* fix: copy local Windows paths with native separators
Copy Path for a proven-local absolute Windows path now writes backslash
separators, the form native Open/Attach dialogs accept (#26204). Applied at
the clipboard boundary of the editor tab menu and header, File Explorer Copy
Path(s) and its shortcut, source control Copy Path, Monaco Copy Path to Line,
HTML document preview Copy file path, workspace sidebar and activity Copy
Path, skills Copy path, and AI Vault Copy Log Path, each with the owner its
Reveal/Open in action already resolves. Remote, unresolved, web-client, URL,
relative and non-Windows output is unchanged.
* fix: sync Copy Path owner refs after commit
---------
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
* test(ssh): let Linux finish releasing upload fds before asserting they closed
* test(ssh): pin abort teardown confirmation and reword the fd-release note
The holder's code 1 means its distro stopped. Noting it, independent of bridge activity, closes
the window the bridge exit's burst cooldown or a still-open socket can leave: later background
reads must re-read the running list instead of trusting a cached "running". The holder's process
classification moves to its own module to keep the file under max-lines.
* fix(dictation): drop audio on cancel so a re-tap starts at once
A cancelled or abandoned dictation was transcribed anyway, holding the
speech session until the upload or local decode finished.
Fixes#35681
* test(dictation): fake worker answers discard like the real one
* refactor(dictation): one session reset for worker stop and discard
* fix(dictation): exit a worker that cannot open a fresh stream
* perf(orcad): keep the Codex PATH scan off WSL readiness
orcad's startup Codex hook lookup and reconcile now wait for readiness to
publish (a launch still releases them at once), and the WSL Windows-copy
check reuses a recent verdict for the same PATH instead of re-statting
every Windows-drive PATH entry on each call.
* refactor(orcad): hand readiness to managed agent hooks through a returned handle
Register a cmake Monaco language for CMakeLists.txt backed by Shiki's
CMake grammar, mapping its storage/entity/argument scopes to token
types the stock Monaco themes style. .cmake files stay with plugin
syntax packs.
Fixes#35452
* feat(orcad): gate agent launches on accounts and cancel feature waits for departed callers
requires: may read the parsed params, so terminal.create and
session.tabs.createTerminal wait on accounts only when they start an
agent; plain shells stay ungated. createAgentSession, ensureAgentSession,
agentSession.create, agent.launch (unless it reuses a terminal),
git.generateCommitMessage and the worker-launching orchestration methods
also wait on accounts. The feature wait races the request signal and the
connection close and refuses without running the handler.
* feat(orcad): gate terminal.ensureAgentSession only for explicit launches
An attach to an existing session is restore latency and needs no fresh
account state; only kind 'explicit' may start an agent.
* test: reset orcad feature readiness after every unit test
A startOrcad test that fails partway left latches closed, so later tests
in the same worker could wait 10 s on a gated method.
The share RPC's fallback now reads stopped roots only on the distros its first pass left unread, the directory matcher refuses an empty root, and the lowercasing is documented as widen-only.
GitHub serves a renamed repository under its old name, so branch discovery
finds the PR through a stale origin URL, then rejects it because the PR
reports the new head repository name. Accept the PR when the queried
repository is origin itself and GitHub reports it as a same-repository PR,
and share that same-repository check with the cached-number lookup path.
Fixes#35687
Co-authored-by: Neil <neil@stably.ai>
* perf(wsl): take one activation fence on a same-version connect
A same-version WSL connect fenced twice: the deploy took the activation
fence only to read that the version was already active, released it, and
the wake took it again to start the stopped slot. Now the deploy skips its
fence for that case (WSL-only, when the connect snapshot saw the fence
open), and one wake fence re-reads the record, checks liveness and starts
the slot under its owner guard. Another version active by then, or a busy
fence, runs the full deploy and wake as before. SSH is unchanged.
* fix(wsl): leave an interrupted wake's fence to the wake that can reclaim it
The one-fence wake recorded its own token over an earlier interrupted
wake's, so the full wake could no longer prove that fence its own and the
host stayed fenced until stale recovery. With an interrupted wake on
record, the one-fence path now answers 'fenced' and the full wake runs.
* fix(wsl): WSL host bridge dials, ready clients and holder re-ensures never boot a stopped distro (E9)
With a chat open on a WSL host, terminating the distro let the control connection's quick
redial reach the still-open loopback listener, whose per-socket `orca_node_bridge --exec`
spawned wsl.exe and booted it again.
1. The WSL bridge listener admits a dial only on a settled running read (the cached one, under
the usual lease), refusing a stopped or unverifiable distro. Every wsl.exe launch from the
WSL host transport notes the distro as launched, so a user act's connect is never refused its
own bridges by a "stopped" read taken before it started the distro.
2. A bridge the guest ends first notes the distro may have stopped, so the next dial re-reads.
3. A ready runtime client skips the cold reachability check only while the last settled running
list names the distro.
4. Background holder re-ensures (liveness) re-read the running list right before spawning; a user
ensure never inherits a background one's refusal. The holder's types move to their own module.
The shared SSH bridge listener and the shared-control reconnect are unchanged.
* fix(wsl): note a possible stop only on the filtered bridge exit, not every guest-first close
A guest-first end is also how one-shot RPC sockets and WebSockets close normally; noting a stop
on each made the next dial wait out the settle and probe, flapped reachability, and fed false
stop notes to the session scanner. The note now rides WslOrcadBridgeActivity's exit signal: the
guest ending the last open bridge, outside the burst cooldown.
A timed-out worktree add surfaced only "git timed out.". Rewrite the
GitCommandTimeoutError message on the add path to state the limit used and
point to ORCA_WORKTREE_ADD_TIMEOUT_MS, set where Git runs (the remote host
for SSH or orca serve workspaces). The advice is dropped once the limit is
already at its maximum. The error's class and timeout field are preserved.
Fixes#35174
Enable Vitest's persistent module cache outside CI under node_modules/.cache/orca-vitest-modules,
keyed by the existing CI module-cache policy hash (lockfile, tsconfig/package manifests, runner
scripts) plus the Node version. reclaim-space removes caches unused for 7 days.
Track the fake electron-vite CLI pids before the fallible web-build wait, give that wait 20s
on loaded hosts, and have the fake CLI exit once its parent wrapper is gone.
- the command extension registry: workers registered every command into it,
but nothing ever looked one up (commands run through the service directly)
- PluginListRow, a list row no screen renders anymore
- hasPluginSyntaxFailure, called only by tests
- six exported types nothing imports, and a re-export nothing uses
* fix(plugins): keep binary editor edits across renames, reloads and twin tabs
- Renaming a binary file with unsaved plugin-editor edits gave its tab a new
id, and the bytes store took the old id's disappearance as a close and
dropped the edit. The bytes now move with the tab as the rename lands.
- Tabs of one file share one dirty flag, so they now share one bytes draft;
before, saving one tab marked both clean and dropped the other's edit.
- A reload that reads the same bytes keeps the document, so the editor is not
sent a new version that turns an edit in flight into a conflict.
* fix(plugins): finish a twin-tab bytes save through any tab still open
A tab that closed mid-save left its twin with the old hash, and a twin
adopting a shared draft no longer lets its own older read replace it.
* fix(plugins): hand a closed tab's bytes draft to its open twin; drop stale reads
A twin that had not shown the file yet lost the draft when the tab holding it
closed, and a read that started before an edit and save could replace the
newer saved bytes. Boxes now count edits and saves; a read only lands if none
happened since it started.
Cwd-less gh/glab calls whose host CLI is missing fell back to the default
WSL distro with no running check, so background work (rate limits, auth
status, Settings, task-source probes) booted a stopped distro. The
fallback now asks the background exec gate first: stopped or
unverifiable refuses with the distro-stopped error, running and user
acts fall back as before.
* fix(i18n): format renderer dates and numbers in the UI language
About 80 renderer sites formatted with the OS locale: Intl constructors
given undefined (or nothing) and argless toLocale*String calls, while
nearby text followed the UI language.
- toLocale*String calls pass getIntlLocale().
- Module-level Intl formatters and collators use new ui-intl-formatters
helpers, cached per options object and rebuilt on language switches;
per-call constructors pass getIntlLocale().
- ReleaseBuildPicker's memoized labels re-run on language changes.
- An ast-grep rule (renderer-intl-ui-locale) rejects undefined/omitted
locales in src/renderer; machine-facing code passes a fixed locale.
* fix(i18n): collators check their own locale support; re-sort memoized lists on language changes; widen the Intl guard
* fix(i18n): name the formatter options type
* fix(i18n): reuse the CSV viewer's existing language subscription
---------
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
A launch made while the Codex history source is a \\wsl path records no
rollout dates, so after switching back to a plain path the next migration
pass only scanned the old pending dates and missed those sessions. Such a
launch now sets the local backfill marker's full-scan flag (Orca user data,
no 9P access), and the next eligible pass walks all history.
Also checks the resolved source home (setting, else the system home) when
deciding whether the history source is in WSL.
* refactor(plugins): one liveness check and list hook for plugin contributions
Panels, commands, custom editors and new-file types each repeated the same
running/restarting/idle filter, the same plugin tagging, and the same
load-then-memo hook. They now share isLivePlugin, fromPlugin,
keyedPluginContribution and usePluginListDerived.
* perf(plugins): keep the panel-error map's identity when health is unchanged
Every panel load reported its health, and each report rebuilt the map the
sidebar badges subscribe to. Also drops an unchecked cast.
* perf(i18n): load built-in catalogs once, without copies or a runtime plural fill
Each catalog load parsed the JSON, rebuilt it to fill CLDR plural forms
from _other, then deep-copied and deep-merged it into the store (about
26 ms after parsing per 1.3 MB catalog), and concurrent loads of one
locale fetched it twice.
- sync:localization-catalog now commits the plural forms a locale's
CLDR rules need beside each _other (fr/es/pt many, tr one, a few
de/ru), and verify:localization-catalog fails when one is missing.
Plural forms may include {{count}} where English omits it.
- Renderer and main load built-in catalogs as-is; plugin packs keep the
runtime fill.
- Renderer catalog requests are shared per locale; a timed-out request
is dropped so a retry fetches again. Bundles are added with skipCopy.
* fix(i18n): require {{count}} in plural forms when English shows it
* refactor(i18n): share one i18next setup between main and renderer
- createOrcaI18n (src/shared/orca-i18n-instance.ts) owns the backend,
init options, plugin-pack registration, resource-language resolution
and the translate wrapper; main and renderer inject only how a
built-in catalog loads (dynamic import vs. fetch) and extra modules.
- Remove setRendererUiLanguage (tests only); tests load then switch.
- resolveUiLocale maps built-ins by lookup instead of an if-chain, and
catalog URLs/loaders are direct map lookups.
- Drop <I18nextProvider>: initReactI18next already binds the instance.
* refactor(i18n): native i18next plurals at call sites, plural-aware extraction
Call sites branched on count === 1 and passed literal _one/_other keys,
which resolvePluralFamilyKey stripped again so CLDR could pick the form.
They now call translate('x', otherDefault, { count, defaultValue_one })
and i18next selects the form natively; catalog keys stay x_one/x_other.
- Converted 284 families with an AST codemod plus a few by hand; added
defaultValue_one to 21 base-key families that relied on en.json.
- Extraction runs with plurals enabled, so both English defaults are
checked for drift. A key that only fills {{count}} (no defaultValue_one)
stays singular in en.json.
- verify/sync:localization-catalog expand a native plural call into its
x_one/x_other English entries.
- Removed 22 base keys that a sibling plural family shadowed in every
catalog.
* fix(i18n): pick English plural defaults by English rules; require count for plural refs
* refactor(i18n): convert plural sites merged from main; treat key-as-default plural extraction as dynamic
* fix(i18n): keep an undefined pass-through count absent; only dotted key-as-default is dynamic
* refactor(i18n): native plurals for exact-1 families; drop desktop suffix stripping
Twelve families kept special exact-1 copy ("Used the browser") beside an
_other-only family, so CLDR one (ru 21, 31) reused _other wording. They
now call the base key with an English _one translator source, the
exact-1 branch stays a separate key, and ru one forms get the right
noun ("21 раз"). With no suffixed call sites left, desktop translate
no longer strips plural suffixes; mobile keeps resolvePluralFamilyKey.
* test(i18n): update plural call expectations in two renderer tests
* refactor(i18n): convert a plural site merged from main
---------
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
* fix(i18n): code AI text-generation failures for commit, PR, and Jira title generation
* fix(i18n): merge duplicate type import
* fix(i18n): code Source Control AI settings and generation plan failures
---------
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
In a hidden Linux test window Playwright's mouse click on a button inside the
sandboxed editor frame never reaches it: the frame takes focus, but no
pointerdown or click fires, so the edit was never sent and the tab never went
dirty. Activate the button from the keyboard instead, which still counts as a
real user gesture.
* fix(orcad): keep orcad's terminal models at the daemon's live depth
orcad's per-PTY headless terminal model kept 5,000 rows of scrollback, but
orcad has no renderer to reveal from: requested snapshots read the daemon
first and subscribe snapshots read at most 1,000 rows. The model now
mirrors the daemon's live window, from the same resolver the daemon uses.
The execution-context reseed also awaited its provider snapshot with no
bound, so a provider that never answered held every live chunk queued
behind it. It now uses the authoritative snapshot timeout.
Refs #34410
Refs #33870
* fix(orcad): keep provider preference when the reseed snapshot times out
Refs #34410
Refs #33870
* fix(runtime): bound every provider snapshot wait in one place
serializeProviderTerminalBuffer now applies the authoritative snapshot
timeout when a caller passes none, so no reader can wait on a provider that
never answers. The execution-context reseed relies on that bound; when its
capture times out it keeps the PTY provider-preferred and reseeds again once
the capture lands, instead of staying on the provider path.
Refs #34410
Refs #33870
* fix(runtime): reseed once after a late provider snapshot, trust the model on none
awaitProviderTerminalBuffer reports the capture still running when the
bounded wait gives up, instead of callers inferring it from the acquisition
map. The execution-context reseed retries once when that capture lands with
a snapshot, and stops preferring the provider when it lands with none, so a
slow provider can neither loop reseeds nor pin the PTY to the provider path.
Refs #34410
Refs #33870
* fix(runtime): never trust a late-reseed model that found no snapshot
The once-only retry disposes a model that had parsed the live bytes since
the timeout. If its own capture then fails, the replacement holds less than
its predecessor, so the PTY keeps preferring the provider instead of
treating that model as complete.
Refs #34410
Refs #33870
* perf(i18n): load built-in catalogs once, without copies or a runtime plural fill
Each catalog load parsed the JSON, rebuilt it to fill CLDR plural forms
from _other, then deep-copied and deep-merged it into the store (about
26 ms after parsing per 1.3 MB catalog), and concurrent loads of one
locale fetched it twice.
- sync:localization-catalog now commits the plural forms a locale's
CLDR rules need beside each _other (fr/es/pt many, tr one, a few
de/ru), and verify:localization-catalog fails when one is missing.
Plural forms may include {{count}} where English omits it.
- Renderer and main load built-in catalogs as-is; plugin packs keep the
runtime fill.
- Renderer catalog requests are shared per locale; a timed-out request
is dropped so a retry fetches again. Bundles are added with skipCopy.
* fix(i18n): require {{count}} in plural forms when English shows it
* refactor(i18n): share one i18next setup between main and renderer
- createOrcaI18n (src/shared/orca-i18n-instance.ts) owns the backend,
init options, plugin-pack registration, resource-language resolution
and the translate wrapper; main and renderer inject only how a
built-in catalog loads (dynamic import vs. fetch) and extra modules.
- Remove setRendererUiLanguage (tests only); tests load then switch.
- resolveUiLocale maps built-ins by lookup instead of an if-chain, and
catalog URLs/loaders are direct map lookups.
- Drop <I18nextProvider>: initReactI18next already binds the instance.
* refactor(i18n): native i18next plurals at call sites, plural-aware extraction
Call sites branched on count === 1 and passed literal _one/_other keys,
which resolvePluralFamilyKey stripped again so CLDR could pick the form.
They now call translate('x', otherDefault, { count, defaultValue_one })
and i18next selects the form natively; catalog keys stay x_one/x_other.
- Converted 284 families with an AST codemod plus a few by hand; added
defaultValue_one to 21 base-key families that relied on en.json.
- Extraction runs with plurals enabled, so both English defaults are
checked for drift. A key that only fills {{count}} (no defaultValue_one)
stays singular in en.json.
- verify/sync:localization-catalog expand a native plural call into its
x_one/x_other English entries.
- Removed 22 base keys that a sibling plural family shadowed in every
catalog.
* fix(i18n): pick English plural defaults by English rules; require count for plural refs
* refactor(i18n): convert plural sites merged from main; treat key-as-default plural extraction as dynamic
* fix(i18n): keep an undefined pass-through count absent; only dotted key-as-default is dynamic
* test(i18n): update plural call expectations in two renderer tests
* refactor(i18n): convert a plural site merged from main
---------
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
* fix(i18n): code AI text-generation failures for commit, PR, and Jira title generation
* fix(i18n): merge duplicate type import
---------
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
* fix(codex): never read a WSL distro's history over 9P in the per-account and shared-mirror lanes
When codexSessionSourceHome points inside a distro (\\wsl.localhost\…), the
per-account lane used it as a history source and the shared-mirror lane scanned
it, both through 9P from the Windows side, which boots a stopped distro (E9) and
yields links codex resume ignores. Both now leave such a source out; the WSL
lane, which runs only at launch, owns those homes. Ordinary custom sources are
unchanged.
* fix(codex): no system-default history migration scan from a WSL source home
The host system-default session migration (backfill and index heal) also read
resolveHostCodexSessionSourceHome as its sessions root; a \\wsl.localhost
source would be scanned over 9P and boot a stopped distro (E9). Such a source
now makes the migration ineligible, sharing one check with the mirror lane.
* fix(codex): one WSL-source predicate for every history lane, and no backfill prep from it
Every lane that reads the host history source now uses one protected predicate,
isHostHistorySourceInWsl(): the per-account, shared-mirror and ~/.codex lanes,
migration eligibility, backfill invalidation after a managed system-default
launch, and preparing a migration pass, which now returns false without marking
the backfill pending.
#34632 let Share read every stopped WSL distro's skill folders. The Share
dialog now sends the chosen skills' folders, and discovery reads only the
stopped roots (and workspace plugin folder) that hold them; every other
stopped root stays unread and unavailable. The skills.share RPC names skills,
not folders, so it scans gated first and reads stopped roots only when a
selector is missing and a folder went unread.
Panels and custom editors each had their own copy of the read-entry,
newest-read-wins, reload-on-change loop. The panel copy also cleared its
failure dot on the next plugin-list change while it still showed the
suspended message. One hook now loads both; a suspended frame stays suspended
(and reported failed) until its HTML actually changes, and panel health is
derived from the entry state.
* feat(wsl): bridge sh/dash/ash panes report their folder (OSC 7) at every prompt
bash, zsh, fish and nushell panes already tell the terminal daemon their
folder; a bridge pane whose login shell is sh, dash or busybox ash did
not, so restores fell back to the starting folder.
dash has no rcfile flag or prompt hook, but an interactive sh reads $ENV
after ~/.profile, and dash 0.5.8+ (plus ash and bash-as-sh) expand $(...)
in PS1. The bridge now points ENV at a new sh/env file in the published
wrapper tree, saving the user's own ENV. That file hands ENV back to the
user's value (so children see theirs), sources it, and inside WSL only
prefixes PS1 with the report. A root containing $, ` or \ is skipped,
since the shell expands ENV's value.
A non-interactive sh -c never reads ENV, and a missing file (Orca
updated away or uninstalled) is silently ignored by dash, ash and bash.
* fix(wsl): sh ENV hook survives quotes in the user's ENV and keeps prompt width
- The user's ENV is expanded through a here-document, so a value with a
quote, $ or space no longer breaks startup with a syntax error.
- bash-as-sh and busybox ash (fancy prompt) get the report inside \[ \]
so its invisible bytes don't count toward the prompt width; dash has no
line editor to miscount and would print \[ literally, so it stays bare.
- Tests: a user ENV of `$HOME/u "q" sp/env`, a user PS1 with $, a quote
and a backslash, and reports read through the daemon's WSL parser.
- CI: edits to the sh hook and the bridge launch trigger the shell
contracts job (its prefix list moves to its own module, keeping the
scope script under its line limit), and the sh and fish WSL pty tests
leave the unit shards.
* ci: check out the shell-contract prefix module in the change detector
The detector job sparse-checks-out a fixed file list; the new
pr-shell-contract-prefixes.mjs that pr-code-change-scope.mjs imports was
missing from it, so the job failed with ERR_MODULE_NOT_FOUND. A contract
test now walks the scope script's local imports and requires each in
that list.
* ci: list the shell-contract prefix module wherever the scope script's modules are listed
Two config-script suites copy pr-code-change-scope.mjs and its sibling
modules into a fixture repo; without the new pr-shell-contract-prefixes.mjs
the copied script failed to load, and the detector fell back to running
everything. Also lists it with the scope script's other modules in the
mobile workflow trigger and the mobile non-test file set.
* test(terminal): run the renderer's OSC 7 agreement cases through xterm
The renderer's OSC 7 parser only ever sees what xterm's OSC parser hands
its handler, and xterm drops raw C0 bytes first: a raw tab, LF or CR
becomes /tmp/ab, and a raw NUL is dropped rather than refused. The
agreement table now records those rows as a known renderer gap (viaXterm)
and the renderer side runs every case through @xterm/headless.
* test(terminal): pin raw ESC, CAN and SUB, and run the daemon side through its PTY scanner
- A raw ESC ends xterm's OSC at /tmp/a, and the daemon's scanner rejects
the report (ESC not followed by \), so the pane keeps its folder.
- CAN and SUB cancel the sequence in xterm, so the renderer's handler
never runs (no-report); the daemon reads the byte as part of the path.
- The daemon side now also runs every case through
TerminalOscCwdTitleScanner after a prior good report, as PTY traffic
reaches it, alongside the direct-parse check.