Commit Graph
322 Commits
Author SHA1 Message Date
edc0373fcd plugins: Plugin Alpha (#35470)
* i18n(plugins): drop retired plugin strings from the Portuguese catalog

* i18n: keep the empty project group in the Spanish and French catalogs

* i18n: retire translated entries for plugin strings this change rewords or removes

* plugins: syntax contributions

A plugin can contribute up to 16 syntaxes: a TextMate grammar, optional
language configuration, and the file extensions and names it claims.
Main validates every grammar when the plugin is approved (shape, scope,
size, nesting) and lists only the file associations. A language's
grammars cross IPC only when a matching file opens, and includes must
resolve inside the same plugin. Catalog releases can declare syntax.

* plugins: highlight files with plugin syntaxes

Files Orca shows as plain text use a matching plugin syntax. Its grammar
compiles once in a worker that stays warm between edits and runs under
core's per-line limits; a worker that stops reporting progress or crashes
is terminated, the language stays plain, and the plugin card says so.
Tokens reach Monaco as semantic tokens, so Monaco asks only for models
shown in an editor, keeps one request per model, and keeps painting the
previous colors, shifted by edits, until the new pass lands. Plugin
languages borrow ids from a pool of fixed Monaco languages, counted by
the editors using them; a language no editor uses moves its models back
to plain text, and the least recently used idle slots are reused first.

* plugins: Gleam syntax example pack

An official-style data plugin with the upstream Gleam TextMate grammar
(Apache-2.0, provenance and license alongside) and language
configuration. A test installs it through the production inspection and
loads it through the syntax registry.

* plugins: list themes, icon themes and syntaxes in the marketplace preview

The preview's Includes line named only language packs, commands,
shortcuts, VM recipes, panels and events, so a theme or syntax pack read
as "Manifest metadata only".

* fix(plugins): a syntax failure belongs to one revision, not the syntax id

A grammar that failed in revision A kept the plugin card flagged after revision B
replaced it. Failures are now keyed by id@revision, matching the runtime slots,
and dropped when the installed list no longer contains that revision.

* chore(plugins): format the Gleam example grammar

* plugins: import the Monaco editor API, not the full bundle, from the syntax runtime

* plugins: type the syntax engine against the Monaco API it uses

* plugins: suggest a catalog plugin for files Orca can't open

When a file shows as plain text or as a binary Orca can't display, and a
cached catalog lists a release whose fileTypes include its extension, the
editor offers it: View plugin opens the marketplace preview (never an
install), Not now hides it for the session, and Don't suggest remembers
the extension. Suggestions come only from cached, verified catalogs, skip
kill-listed releases and anything an installed (even disabled) plugin
already handles, and can be turned off in Plugins settings. A release's
fileTypes must match its manifest's syntax extensions.

* fix(plugins): scope the file-type suggestion's Not now to the file type

The editor stays mounted when its file changes, so a single hidden flag
suppressed the suggestion for the next file type too. Track which file
type was hidden instead.

* plugins: type the plugin-owned settings normalizer inputs

* plugins: update the preview-request callback ref after render, not during it

* test(plugins): smoke specs for themes, syntaxes and file-type suggestions

Three end-to-end specs join the demo and marketplace journeys:
- an app theme and icon theme apply from Settings and fall back when the
  plugin is disabled;
- a plugin syntax highlights an already-open file, re-highlights it after a
  grammar update, and lets go when disabled;
- a v2 catalog release with fileTypes is suggested for an unknown file,
  View plugin opens its preview without installing, and Don't suggest is
  remembered.
The marketplace spec's Git fixture helpers move to a shared helper.

* test(plugins): say why the file-type suggestion e2e needs a longer timeout

* test(editor): install Monaco's cancellation guard in the owner view-state test

The test mocks monaco-setup, which skips the guard production installs, so
a disposed editor's pending word highlight surfaced as two unhandled
"Canceled" rejections once plugin syntaxes added a render. The run
passed but reported errors.

* plugins: plugin app themes recolor the code editor

When a plugin app theme is active, every Monaco surface (editor, diffs,
notebook cells, code excerpts, automation prompt) uses a theme derived
from the colors the document is actually painted with: editor background,
text, line numbers, cursor, selection, current line and widgets. Syntax
colors stay the built-in light or dark ones; if text would fall below
4.5:1 or a syntax color below 3:1 on the plugin's editor background, the
editor keeps its built-in theme. Colors are resolved to hex through the
browser's own parser, so color-mix() and other CSS color forms work. The
theme is redefined when the plugin updates or the mode changes, and the
editor returns to the built-in theme when the plugin theme is deselected,
disabled or removed.

* refactor(plugins): one module per contribution point and a single points table

The manifest's contributes object, the execution-profile derivation and the
catalog type list now read one PLUGIN_CONTRIBUTION_POINTS table instead of
three hand-kept lists.

* refactor(plugins): express id and path rules as patterns

Plugin ids and relative paths now validate with a regex first, so a generated
JSON Schema can carry them as `pattern`. The predicate only adds what a
pattern cannot (reserved names, Windows device names), and a bad value
reports one issue instead of two.

* plugins: document every manifest and catalog field in the schema

Each field now carries a description, examples and the Orca release that
first reads it, and each contribution item has a named fragment id. Manifest
v2 accepts an optional $schema key for editor completion, and the catalog
gains an authoring schema built from the same field definitions.

* plugins: generate published JSON Schemas and author references from zod

pnpm generate:plugin-schemas writes schemas/plugins/orca-plugin.schema.json,
schemas/plugins/orca-marketplace.v2.schema.json and the matching reference
pages under docs/reference from the runtime schemas. A test fails when the
committed files drift, when a field lacks a description or since-version,
when a documented example does not validate, and when the schema and the
runtime disagree on bundled manifests or known-bad inputs.

* plugins: report every manifest and catalog mistake with its path

Manifest and catalog validation now lists up to eight distinct problems as
`contributes.themes[0].label: must not be empty` instead of zod's first raw
message, and rewrites parser wording (required, unknown keys, allowed
values, limits) into what the author should change.

* plugins: say which Orca a newer plugin or catalog release needs

A manifest the strict schema rejects is pre-read for engines.orca and
manifestVersion first, so a plugin built for a newer Orca reports
"requires Orca >=X (this is Y)" instead of unknown-key noise. A catalog
release whose minimumOrca is above this build stays listed but is refused
before anything is downloaded.

* plugins: version the published schemas and pin v2 fields to a real release

Schemas move under schemas/plugins/v2/ so a later breaking format can
publish beside them without changing what a pinned URL means, and the
example plugin authors copy carries its own copy for offline completion.
Fields new with manifest v2 cite one constant, PLUGIN_MANIFEST_V2_SINCE
(1.4.232, the release after the latest tag), and a test fails once a tag at
or above it ships without manifest v2. The reference documents format 1 as
legacy.

* plugins: open review when enabling a plugin that was never approved

Dismissing the review dialog records keep-disabled without consent. Turning
the plugin's switch back on then only cleared the disable, so it returned
to "Needs review" with no dialog. Enabling now opens the review whenever
the result is still pending.

* plugins: release details for the plugin detail page

Catalog releases may carry a license, repository and homepage (https only),
an updatedAt date and a short plain-text changelog; an invalid value is
dropped so it never hides the plugin. Manifests may declare license and
homepage. Marketplace listings flatten name, version and those details, and
installed plugins report license, homepage, repository and the version a
rollback would restore.

The JSON lint-staged step no longer fails a commit whose only staged JSON
files are generated schemas the formatter ignores.

* plugins: fold in the schema review

- Parity runs both ways: documented examples also pass their zod field,
  bundled manifests pass both the schema and the runtime, and manifest and
  catalog negatives (traversal, oversize, wrong types, duplicates) fail in
  both. A table lists every rule only the runtime can enforce, and each is
  named in the description of the field it constrains.
- The published files carry the format in their names
  (orca-plugin.v2.schema.json, orca-marketplace.v2.schema.json); $id stays
  on main until the v2 release, and a test demands the release tag once it
  exists.
- The v1 since-value is 1.4.160, the first release that read plugins, and a
  test pins it to the tags. engines.orca documents that format 2 needs at
  least the release that ships it.
- v2 catalog sources strip unknown keys like the rest of the entry, and the
  v2 runtime schema is derived from the one authoring definition, whose
  plugins field documents the duplicate-id rule. Contribution types and
  categories declare uniqueItems; fileTypes items come from the file-type
  schema itself.
- Issue messages no longer depend on zod's English: a missing field is told
  from the input, and every pattern carries its own message. Unknown-key
  lists show five keys plus a count, and each line is capped at 200
  characters.
- A prerelease host (1.5.0-rc.1) now ranks below its release, as in semver.

* plugins: read the rollback version on demand, not while listing

The plugin list is rebuilt at startup and on every plugin change, and each
installed plugin cost a directory read to find the version a rollback would
restore. That moves to plugins:getRollbackVersion, which the detail page
calls when it shows the action, and a test pins the list build to zero
filesystem calls.

* plugins: v1.4.232 shipped without manifest v2, so v2 fields cite 1.4.233

The release-tag check caught it: v1.4.232 was cut from main before this
stack landed.

* plugins: list installed plugins while the plugin system is off

With the system off the service discovers nothing, so plugins:list returned
an empty list and the settings page could not show what is installed. The
client list now reads the install tree on demand in that state and projects
every plugin as disabled; nothing is activated or started, and startup with
the system off still does no plugin I/O. Older clients that hid the list
while off are unaffected.

Also for the plugin page: plugins:getInstalledManifest returns one installed
plugin's parsed manifest on demand, marketplace installs report the catalog
ref beside its URL, and the listing type declares the release record it
already carried.

* plugins: final schema decisions for catalogs and versioning

Catalog
- The catalog is orca-catalog.json with a required catalogVersion: 2; a
  client that sees a higher version refuses with "update Orca to browse
  this catalog". New clients read it first and fall back to the frozen
  orca-marketplace.json, which export-legacy-plugin-catalog.mjs generates.
- Parsing is tolerant at every level: unknown keys are stripped, unreadable
  entries and releases are skipped, over-limit lists are trimmed, a repeated
  id keeps the first copy, invalid display-only fields are dropped.
  pnpm validate:plugin reports each of those as an error for authors and CI.
- Entries list releases newest first; Orca offers the first whose
  minimumOrca it meets, and shows "Needs Orca X" only when none fits.

Versioning
- engines.orca is the only gate and must cover the newest since-version of
  every field and enum value used (capabilities, events and built-in actions
  now carry per-value since tables). The check is part of the v2 schema, so
  validation, catalog checks and installs all enforce it.
- pluginApi is optional in format 2 (defaults to 1) and refused on data and
  instructional plugins. Format 2 rejects unknown keys at every level,
  including engines, author and every contribution item.
- contributes.agents is reserved for coding-agent definitions and must be
  empty in format 2 until their format exists.
- Plugins are gated against the newer of the app version and the newest
  since this build defines, so development and local builds, whose package
  version only moves at release, read the fields they introduce.

The policy is written into the generated references and pinned by tests.

* plugins: one-click install for content-only plugins, and the verified profile on the list

installMarketplacePlugin takes an optional enableIfData. Main then turns the
plugin on in the same step only when the installed, verified manifest is
execution: data, approving exactly the fingerprint it just installed through
the normal consent path; anything else comes back needsReview: true, whatever
the renderer sends. Older hosts ignore the field and leave the plugin pending.

Installed plugins now report their verified execution profile, so the plugin
page never infers "content only" from the absence of a worker.

* plugins: images and contribution summaries for plugin pages

Manifest format 2 gains cover and screenshots (alt text required), and its
icon must be a square PNG or WebP. Images are checked at install from their
headers only: static PNG, JPEG or WebP, per-kind pixel and byte limits, at
most 4096 pixels a side and 8 MB in total, so nothing is ever decoded to
decide trust.

Catalog releases may carry images stored in the catalog repository and
pinned by SHA-256, a summary of what they add, and the capabilities they ask
for, so a listing can show the plugin completely before anything is
downloaded. All three are dropped when unreadable; the summary and
capabilities must match the manifest exactly at install. pnpm validate:plugin
checks each image's hash, size and format against the repository files.

* plugins: serve plugin and catalog images from verified bytes

Images reach the renderer only as orca-plugin-image URLs, which main serves
with nosniff and the type it read from the header:
- catalog images are read from the catalog repository at the fetched
  commit, kept only when their hash, size and format match, and cached by
  SHA-256; a listing references only those;
- preview images are cached by hash before the staging tree is deleted;
- installed plugins' images are named by plugin, install hash and slot and
  re-checked on every read, so listing plugins still reads no files.

Listings, previews and installed plugins carry an images field the plugin
page can render without fetching anything else.

* plugins: catalog theme palettes, and one-click never approves a dev override

Catalog releases may list up to four theme palettes (hex colors only) so a
listing can draw theme cards before install; they are display-only and
dropped when invalid. One-click enable now also refuses when a development
override shadows the installed plugin, since those are not the bytes the
user just chose.

* plugins: one-click approves only the fingerprint the user saw

installMarketplacePlugin takes an optional expectedFingerprint, the
preview's consentFingerprint. With enableIfData, main now approves only
when the installed fingerprint equals it, so bytes that changed between
preview and install stay pending for review instead of being enabled. The
release commit is already pinned by resolvedCommit. Older hosts ignore the
field.

* plugins: gate manifest v2 on 1.4.242 and publish schemas under docs/schemas

v1.4.240 shipped without manifest v2, so the first release that reads it is 1.4.242.
The root-directory guard refuses a new top-level schemas/ folder.

* plugins: read unvalidated schema input through a typed object guard and break the image validation import cycle

* plugins: one place decides an installed plugin's status word

Pull the status ranking, label and enabled/needs-review checks out of the settings row so the
coming list rows and detail page read the same words. No visual change.

* plugins: one list entry shape for installed and catalog plugins

PluginListEntry joins an installed plugin to its catalog listing (or stands for a listing alone),
and matchesPluginSearch is the one search both lists use. No visual change.

* plugins: usePluginCatalog owns catalog loading, refresh and preview

Sources, listings, refresh, the install/update preview and the outside preview request move out
of PluginMarketplaceBrowser into one hook the coming list and detail page share. The browser
keeps only its view state. No visual change.

* plugins: one plugin list with fixed slots, a plugin page, and inline states

- Installed plugins first, then one group per catalog (verified/unverified · owner or URL), capped
  at 6 rows with Show N more; every plugin appears once
- Rows: the name opens the plugin page and covers the row; status, action, switch and ⋯ are
  fixed-width sibling slots in Tab order. Update {version} is the action when a catalog has a newer
  release; Stopped offers Restart with a red mark on the switch; Blocked offers Remove…
- Plugin page in the same pane: what it adds (commands with platform shortcut chips, panels,
  recipes, contribution summary), what it can access, logs, and info. Back link, Alt+← or ⌘[
  return focus to the row
- States inside the list: skeletons, a catalog that fails only affects its own group, no results
  with Clear search, nothing installed. Plugins off keeps the installed list visible, dimmed
- Add plugin ▾ (folder, Git URL) replaces Install plugin; Plugin system stays at the top
- Catalog listings now expose their release (name, version, minimum Orca, types) to the renderer

* plugins: filter by category and catalog, one review for Update all, Needs newer Orca

- Filter popover (category, catalog) next to search; search and filters show every match
- Update all opens one review: updates that ask for new access are expanded with what's new, the
  rest are one line, any can be unticked; installs run one at a time
- A catalog release that needs a newer Orca says so and disables Install, in the row and the page
- Installing from a plugin page shows a review with only access and source, since the page just
  showed what it adds

* plugins: plugin page uses the manifest name; Off status word; official isn't a category

* plugins: catalog sources live in the Add plugin menu

* plugins: search takes its own row when the list is narrow

* plugins: release notes, license and links on the plugin page; roll back names its version

* plugins: fix list review findings

Plugin page owns the pane (back link and plugin header only); Adds to Orca names
every contribution; access never reads as 'nothing' when the check failed; filter
kinds have readable names; Update all is reachable, fetches once per open and
routes updates needing review; installed plugins join their own catalog;
attention-first order; paused list while plugins are off.

* plugins: readable category names on the plugin page

* plugins: Update all keeps its set; reviews queue; updates only from the plugin's own catalog

* plugins: name what an off or unreviewed plugin adds from its manifest; join catalogs by url and ref

* plugins: Needs Orca checks use the version plugins are gated against

Main gates plugins on pluginHostVersion, the newer of the app version and the
newest since-version the build reads. The plugin page compared releases to the
raw app version, so a development or local build showed Needs Orca for plugins
it would install.

* test(plugins): e2e journeys follow the redesigned plugin list

The page offers "Turn on plugins" while the system is off, catalog rows share
the installed rows' data-plugin-key, the review action is "Review", and an
enabled plugin reads "On".

* plugins: describe access from the verified execution profile, never from "no worker"

A commands-and-shortcuts plugin's page said "Content only. It has no code that runs."
because the page treated "no worker" as "no code". A worker plugin without capabilities
read "runs in a sandbox". The page now classifies from the catalog release's execution
profile (install refuses bytes whose manifest differs), the fetched manifest, or the
installed manifest's profile from the plugin list's `execution` field. Unknown stays
"Reading…" or "Couldn't read", never content only. The review dialog uses the same
profile, and an older host that omits it is reviewed as instructional.

* fix(plugins): read revision-scoped syntax failures on detail pages

* plugins: use the Blocks glyph for icon themes in the plugin page

* plugins: update latest-value refs after render in the update dialog and page navigation

* i18n: retire translated entries for plugin strings this change rewords or removes

* plugins: one action to install, inline consent, and a picture-first catalog

Settings > Plugins no longer opens a confirm dialog and then a review dialog.
- Content-only catalog plugins install from their card or page in one click; main
  turns them on only when the verified manifest is data. Undo stays in place for 10s.
- Plugins that run code or instructions show "If you install <Name>, it can:" on the
  host security surface above one Install & enable, which stays off until the access
  rows are read, and approves exactly the fingerprint shown. Review of an installed
  plugin happens on its page too (Enable / Keep off); Remove stays in its menu.
- The catalog is a grid of cards drawn from what each plugin is (its theme colours once
  installed, a code sample, keycaps), with the name as a stretched link and one action.
- The page pins the listing it opened, says "Newer version available" instead of
  swapping rows, and previews are prefetched only on hover intent, capped per visit.
- The plugin system switch, file suggestions, catalog sources and development folders
  move into a header menu; amber text uses a contrast-safe token.

* plugins: give the reworded no-access line its own key so English shows the new copy

* plugins: pin installs to the release shown, warn by what a plugin carries, disclose replaced shortcuts

- The page pins the listing it opened; a preview of any other release (commit, catalog
  commit or execution profile) leaves Install & enable off and shows "Newer version
  available". One-click installs from a card refuse the same mismatch.
- Warnings are the union of the verified profile and the contributions: a panel that
  also adds commands gets the instructions warning, never "bounded" or "own files only";
  unknown contributions over-warn. Access rows follow the same rule.
- Shortcuts that replace one of yours are named in the consent before install, from the
  manifest or the catalog summary; the shortcut list itself stays in Adds to Orca.
- Pages paint from the catalog summaries, capabilities and theme palettes, and show the
  cover (or the card's art) as a hero, with screenshots in a strip and lightbox.
- Installing fills a reserved status line and keeps its action row (Undo, Use now), so
  the page doesn't move; the settings scroller keeps a stable gutter.
- Row art names the plugin type instead of initials, rows drop the "Needs review" word,
  the content hash moves into the menu, and card art is tinted and coloured.

* plugins: catalog covers on cards

* plugins: Review reopens its page after Back; hero art and stable status line follow what an installed plugin carries

* test(plugins): e2e journeys review and install on the plugin page, with no dialogs

* plugins: install exactly the release shown, classify from the full manifest, polish the page

Code review:
- A page or card arms Install only when the preview matches everything the listing showed
  (catalog commit, release commit, content hash, execution); any change, including a
  catalog-only refresh, reads "Newer version available". One-click installs send the
  preview's fingerprint as expectedFingerprint, so main turns on only those bytes.
- Install stays off while a catalog refresh is in flight.
- Trust uses the full verified preview manifest when present; a capped catalog summary
  counts as unknown and gets the stronger warning.
- The installed-manifest read is keyed by plugin and installed revision, so a same-key
  update never pairs old contents with the new fingerprint.
- A failed Undo shows its error on the card or page and keeps Undo to retry.
- The security-surface test now covers the inline consent instead of the removed dialog.

Visual:
- Lightbox: close button clear of the image, "1 of N" with previous/next and arrow keys,
  content scrolls on short windows, focus returns to the opening thumbnail.
- Keycap art shows the plugin's own first shortcut, larger, with the platform's keys.
- The hero fills the content column; a screenshot identical to the cover isn't repeated.
- "Change in Shortcuts" is a link; the chip says "Adds commands" (or "Runs a worker",
  "Adds a panel", "Content only"); the disabled button says "Reading what it can access…".
- A content-only plugin's action row keeps fixed slots, so its note doesn't move on install.

* plugins: keep the decision above the fold, route suggestions to the page, final review fixes

- The page shows the decision (Install, the consent, Enable) before the picture, and the
  picture is capped and cropped from its centre, so the button is in view at 1280x800 and
  900x700. Nothing grows after first paint.
- The consent gives no access verdict while the plugin is still being read.
- A file-type suggestion opens the plugin's page instead of the old install dialog; that
  dialog now serves updates only.
- A theme plugin reserves its "Use now" line from first paint; the action slot and its
  button share one width.
- The file-suggestion toggle reports a failed save; a failed Undo is reported once, on
  the card or page that offered it.
- Cards show a plain trust chip ("Content only", "Adds commands"); the local and bundled
  group reads "Bundled & local".
- Install error copy moves into the protected plugin namespace, and the new consent
  components join the language-pack security pins.
- Dead code goes: the keybinding consent preview (its conflict helper stays), its strings,
  trustRunsNothing and the access-only preview plumbing.

* test(plugins): the file-type suggestion opens the plugin page, not a dialog

* plugins: the install decision sits top-right beside the name; no plugin on/off switch in the UI

- Plugin page: Install (content only), Install & enable (disabled until its access is read),
  Enable / Keep off (installed, waiting for review) and the installed switch share one slot
  top-right of the title. The consent sits directly under the header with no button of its
  own; the "Can't reach anything outside its own files" note sits on the line under the
  publisher, which every state reserves, so nothing moves between states.
- Removed "Turn off plugins" from the ⋯ menu and the "Turn on plugins" button from the off
  state. The setting and all gating stay: off now comes only from policy, and the page says
  so without a switch.
- e2e journeys turn plugins on through the setting and use the page's one button.

* plugins: restore Turn on plugins, drop Undo, add Remove to installed card menu

Plugins stay opt-in, so the off state keeps its Turn on button; only the
menu's Turn off item stays removed. Installed catalog cards get a sibling
more-actions menu with Remove (immediate, inline error) and Turn off/on,
inside a fixed-width action slot so the footer doesn't shift.

* test(plugins): installing a theme plugin never changes the active theme

* fix(plugins): confirm card removal before deleting data

* fix(plugins): pause review queue when leaving plugin page

* fix(plugins): disclose execution changes before updating

* refactor(plugins): share single and bulk update review

* refactor(plugins): share installed card and row actions

* test(plugins): cover confirmed removal and shared update review

* feat(plugins): offer icon and terminal themes after install

* test(plugins): capture settled review dialog screenshots

* fix(plugins): make installed contributions discoverable and actionable

* fix(plugins): label card-install toast Try it and mark terminal-theme Use now buttons

The toast action opens the plugin page rather than applying a theme, so it
always says Try it. Terminal themes often share their app themes' names, so
their Use now buttons carry a terminal icon. Update the update-review e2e for
the Use now accessible names.

* fix(plugins): apply a theme plugin's themes when the user installs or enables it

Installing (card or page) or enabling a plugin that adds themes now applies its
app theme (matching the current light/dark mode), icon theme and terminal theme
at once, with a 'Now using' toast whose Switch back restores the previous
selections. Updates, startup discovery and catalog refreshes never apply.

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

* plugins: call toast actions through a typed button in tests; key card art by stable names

* i18n(plugins): drop retired plugin strings from the Portuguese catalog

* test(plugins): say why the update review e2e needs a longer timeout

* i18n: retire translated entries for plugin strings this change rewords or removes

* refactor(plugins): organize main plugin files by responsibility

* refactor(plugins): group shared plugin schemas and contracts

* refactor(plugins): organize renderer plugin surfaces

* plugins: keep the plugin service under the line limit after the folder moves

* refactor(plugins): move the JSON object guard beside the manifest schemas

* test(plugins): move the flaky-test baseline entries with the worker tests they count

* test(plugins): point the panel lifetime test at the reorganized plugin folders

* plugins: reserve skills, mcpServers, taskSources, connections and settings contribution keys

Format 2 accepts each key only as an empty list, so the points can ship later as additive
minors without a new manifest format.

* plugins: freeze host API v0 with a generated reference and refuse unknown pluginApi majors

The method table now carries a summary and an optional deprecatedSince, and renders
docs/reference/plugin-host-api.md with the stability and deprecation policy. A manifest
naming a newer host API major fails with "update Orca" instead of "must be 1".

* plugins: name the bad release in catalog validation and re-ask on any execution profile change

validate:plugin now reports each unreadable release with its reason (for example a tag
instead of a full commit SHA) rather than a count. The consent fingerprint includes a
declared non-data execution profile, so a data pack that becomes instructional no longer
keeps its one-click approval. Tests pin that catalogs drop non-SHA releases, that a
repository cannot load or approve a plugin it carries, and that added capabilities or a
changed profile always need re-approval.

* plugins: add the author guide, theme token list, v2 examples and a printed catalog release entry

docs/reference/plugin-authoring.md walks write, load a development folder, validate and
publish. The theme token table is generated from the public token set and its fallbacks.
hello-orca and hostile-panel move to manifest format 2. validate:plugin on a plugin
repository root materializes HEAD the way install does and prints the release entry to
paste into orca-catalog.json.

* plugins: name the host API type renderer and reserved contribution fields for their role

* plugins: gate the bundled example plugins on 1.4.240

* plugins: reserve orca, orca-*, official, anthropic, openai and github publishers

* plugins: rename storage/secrets to storage:own/secrets:own and gate events by domain capability

events:subscribe and the events.subscribe host method are gone; workspace events need
workspace:read and agent status needs the new agents:read. Format 1 manifests keep their old
names, rewritten on parse.

* plugins: move samples and fixtures off the now-reserved orca-samples publisher

* plugins: name the public surface workspace, not worktree

Events become workspace.created/removed with workspaceId and a kind field, agent status
carries workspaceId, and command context/keybinding when use workspace. Format 1 names are
rewritten on parse, v1 workers still receive worktree.* handlers, and clients treat any
non-global context from older hosts as workspace-scoped.

* plugins: rename the instructional execution profile to declarative

* plugins: move agents into the reserved contribution keys

Format 2 keeps contributes.agents empty like every reserved point; format 1 entries, which
Orca never read, are dropped on parse so they stay inert and bind no content.

* plugins: skip unknown theme tokens and icon slots at runtime, refuse them in validate:plugin

A token or slot added in a later release no longer fails the whole theme on older Orca.
validate:plugin now parses each theme, terminal theme and icon theme strictly. App themes
accept an optional schemaVersion 1.

* plugins: inject panel design tokens as --orca-* custom properties

* plugins: accept a closed, documented list of panel icons

Format 1 panels keep any icon name: a known one in either Lucide spelling is canonicalized,
an unknown one is dropped and shows plug, as before.

* plugins: make catalog release types optional and derive them

Every required release field is permanent for old clients, and types repeat what contributes
and execution already say. Printed release entries no longer include it.

* plugins: strip unknown kill-list and consent fields instead of refusing them

A strict kill list froze old clients on their cached list as soon as a later list added a
field, silently stopping revocation; a strict consent request would refuse a newer client's
optional review field.

* plugins: check only the shape of a panel action in the renderer

The host's capability gate already refuses methods panels may not call. Filtering against the
client's own method list blocked every panel method a newer host added on older clients,
including remote and SSH ones.

* plugins: read a leading >=x.y.z from engines.orca leniently in the compatibility pre-read

* plugins: derive content-bound approval from the contribution policy table

Each point now declares bindsContent, replacing the hand-kept list in the consent
fingerprint, so a new point such as mcpServers cannot swap bytes under an old approval by
being forgotten.

* plugins: name the host API "Host API 1" and add a stability section to the author guide

The guide now lists what is stable and what is experimental: the agent status payload, the
worker process model, language-pack keys, validate:plugin output beyond its exit codes,
settings:own and manifest format 1. Secrets copy now says encrypted file, not OS keychain.

* plugins: make the hello-orca sample speak in plain words

The worker command returns a sentence instead of a JSON object, and the panel labels
terminals by position instead of their opaque ids.

* plugins: give plugins short branch names, not refs/heads/...

* plugins: panels can no longer type into terminals; workers only (D3)

terminal.sendText with Enter runs shell commands locally or over SSH, which
the sandboxed-panel tier's "limited" consent copy did not convey. The
capability copy now says so, and the sample panel no longer types.

* plugins: put full-access worker plugins behind a second setting, off by default (D2)

Trusted workers are unsandboxed Node, so the plugin system switch alone no
longer lets them run. With "Allow plugins that run code with full access"
off, main refuses to install a full-access catalog release, approve or enable
one, and keeps installed workers stopped with an error naming the setting.
The catalog card says "Full access · turned off in settings" and links to
the switch; the worker chip now reads "Full access".

* plugins: bind dev-folder approval to the folder it was given for

A dev plugin's consent fingerprint now includes the folder's canonical path,
so approving one dev folder no longer approves another folder whose
manifest claims the same key and capabilities. The development dialog now
says edits inside an approved folder run without another review.

* plugins: close WebRTC as an outbound channel from panels

CSP has no WebRTC directive, so a panel could reach a STUN/TURN server it
controls despite connect-src 'none'. The panel shell now locks
RTCPeerConnection to undefined before plugin code runs, and the main window
that hosts panels gets the same WebRTC IP-handling policy as doc preview
guests. DNS-prefetch hints remain the residual doc preview already accepts.

* plugins: stop language packs relabelling trust copy outside plugin settings

Protection covered only plugin copy under settings, so a content-only pack
could rename the catalog's Official badge, the file-type suggestion banner,
or swap Allow and Deny on agent approvals. Those components are now
protected whole, the same way the plugin security dialogs are.

* plugins: refuse Git URLs with a dash-led user or host, or a '..' host

ssh reads a leading '-' as an option, and the scp form is not normalized, so
'stablyai/../evil' read as the official owner. The shared URL check used by
the catalog schema, validate:plugin and every install path now refuses these,
and provenance parsing refuses '..' path segments.

* plugins: flag an update whose catalog moved it to another repository

An update whose catalog now points at a different repository showed the new
URL without saying it changed. The update review now names the old and new
repository and leaves that update unticked, so it installs only once the
user opts in.

* plugins: sample worker returns a readable toast; sample panel shows no internal ids

Hello Orca's ping returned raw JSON, which the command toast printed as-is,
and its panel showed the full ref name. It now returns a sentence and shows
the plain branch name.

* plugins: move runtime policy checks and catalog types out of files over the line limit

* plugins: refresh the plugin safety list every 6 hours while plugins are on

Revocations previously reached a session only at launch or when plugins were
turned on, so a long-running Orca kept running a revoked plugin.

* plugins: main sends an update from a moved repository to review

The update dialog left such updates unticked, but main still accepted a
one-click approval for them. Main now compares the installed and new
repository itself and always answers needsReview.

* plugins: label the plugin system Alpha instead of Experimental

Settings badge, nav entry badge, search/description copy in all locales,
and plugin docs' stability sections. Setting keys are unchanged.

* plugins: trim the Plugin settings menu to titles and a short full-access hint

* plugins: reserve statusBarItems and sidebarCards and list planned contribution points

Both keys are rejected when non-empty, like the other reserved points. The
manifest and authoring references gain a Planned contribution points list
built from one table. Also moves the Alpha note on the host API page into its
generator so the schema/doc parity test passes again.

* plugins: an update that asks for nothing new keeps its approval

Before, any catalog update to a declarative, sandboxed-ui or trusted-worker
plugin changed its consent fingerprint (it binds the package bytes), so the
plugin turned off and waited for review after every Update. Only content-only
(data) plugins kept running.

Main now reads the approved version before installing and, when the update
comes from the same repository and adds no capability, no higher execution
profile, and no new command, shortcut, panel, event or VM recipe, records the
new fingerprint so the plugin stays on. Anything that grows access, a moved
repository, a development override or a plugin that was never approved still
waits for review, and the full-access switch still gates trusted workers.
The update preview now carries what grew, so the Update dialog lists new
shortcuts, commands, panels and events as well as capabilities. The consent
copy says updates that don't ask for more access install without asking.

* plugins: match the full-access test to the shorter setting name

* plugins: update catalog plugins automatically when an update asks for nothing new

Adds "Update plugins automatically" to Plugin settings, on by default. The
setting is pluginUpdatePolicy ('auto' | 'manual') so later policies can be
added without a migration.

At startup (after a short delay) and every 12 hours, packaged Orca refreshes
the catalogs and, for each enabled, approved catalog plugin with a newer
release, installs it in place when it comes from the same repository and
grows no access; the approval carries over and the worker reloads. A release
from a non-official catalog installs only after it has been listed for 12
hours, timed from when this Orca first saw it (stored locally because
catalogs carry no listed-at time), so takedowns can land first. An update
that grows access or moved repository is not installed: the current version
keeps running and the plugin shows "Update needs review", whose Review
update button opens the existing update review with what is new. Plugins
updated this way show a quiet "Updated to vX" chip, and each pass shows at
most one toast. Manual Update and Update all are unchanged.

* plugins: clear code-quality findings in the update approval and auto-update code

* plugins: give an app theme that leaves --sidebar unset a visible sidebar edge

Orca draws no line between the left sidebar and the main area; the sidebar's
slightly different surface is the edge. A theme that omitted --sidebar (like
the Dusk sample) had it filled from --background, so the sidebar matched the
main area and the edge vanished. An unset --sidebar is now mixed 8% toward
the theme's foreground. The in-repo sample theme sets --sidebar explicitly
and the theme smoke test checks it reaches the sidebar.

* plugins: treat an unknown update policy as manual, not auto

* plugins: auto-update only pinned releases, and only the release that was held

* plugins: start the auto-update hold clock before the catalog refresh, and check every 6 hours

* plugins: refresh the kill list right before auto-installing, and skip the install if that fails

* plugins: refuse an unpinned full-access install while full access is off

* plugins: an automatic update re-checks inside the install lock that the plugin is still installed

* plugins: compare the full repository path, so a GitLab subgroup move counts as moved

* plugins: time out the kill-list fetch and retry 15 minutes after a failure

* plugins: drop one unreadable kill-list entry instead of the whole list

* plugins: keep language packs off the orca.yaml and notebook trust prompts, certificate warning and site permission prompt

* plugins: compare whole capability declarations on update, and cover swapped command actions and shortcuts

* plugins: log automatic updates so headless and remote hosts record them

* plugins: say that automatic updates can change what a full-access plugin does

* plugins: pin and document the nested-frame WebRTC residual in plugin panels

* plugins: delete the unused PluginReviewBody

* plugins: drop the dead repositoryMoved parameter and the predecessor wrapper

* plugins: declare the plugin policy settings type once

* plugins: show what main found an update changes instead of recomputing it in the renderer

* plugins: move update approval and update policy into the catalog folders

* test(plugins): expect the workspace.removed event name in the lifecycle test

* plugins: rename structural helpers for their role, type the update identity read, and give plugin system settings their own group

* i18n(plugins): drop retired plugin strings from the Portuguese catalog

* plugins: build the hello-orca status line with a template string

* plugins: install catalog plugins from a directory of a shared repository

A catalog source may name a 'path' inside its repository. Orca downloads the
pinned commit as before but materializes only that subtree, so contentHash
covers the plugin's own files and sibling commits never change it. A changed
path counts as a moved repository and needs review. validate:plugin accepts a
subdirectory and prints its source path; the frozen v1 export skips such
entries.

* plugins: offer the full access setting on a full-access plugin's page

The page offered Install & enable even with full access off, then failed with a generic error.
It now shows the card's 'Turn on in settings' action, names the real reason when main refuses,
and drops a stale install error once the full access setting changes.

* plugins: say a content-only plugin couldn't load its files, not that it crashed

Language packs and other data plugins have no code or logs, so a load failure now reads
"Couldn't load its files" on the page and the row instead of "Its code kept crashing".

* plugins: leave Settings before running a built-in action from a plugin page

Built-in actions such as Open Board and Go Back are disabled while Settings covers the app,
so Run on the plugin page always failed. Run now closes Settings, commits that synchronously
so the dispatcher sees the app view, then runs the action.

* plugins: refuse terminal:send on panel plugins and never list it on panel approvals

Panels get no host method that types into a terminal, yet a panel's approval listed
"Type and run commands in your terminals". Format 2 manifests now fail validation when a
profile below trusted-worker asks for terminal:send, and approvals, the access section and
update review drop that line for format 1 panels that still declare it.

* plugins: say "catalog" in user-facing copy, not "marketplace"

The menu said "Catalog sources…" but the dialog it opens said "Marketplace sources" and
asked for orca-marketplace.json. Plugin copy now says catalog in every locale, names
orca-catalog.json (still accepting the older file), and the author guide matches the UI.
Internal identifiers and settings keys are unchanged.

* test(plugins): run a built-in action from its plugin page end to end

* plugins: concise copy pass across plugin UI

Update review shows one line per plugin and only what's new for updates
that need approval, with the rest behind a See all access disclosure.
Consent, Can access, dialogs, settings menu and empty states drop restated
titles and filler; safety facts (new access, full access = files/network)
stay visible where the user decides.

* plugins: keep See all access on Button and Collapsible primitives' own spacing

* plugins: separate light and dark theme slots so packs pair and mix

Plugin app themes now fill a Light theme or Dark theme slot by their base. Orca paints the
slot for its current mode, so under System it swaps live with the OS. Appearance shows both
pickers grouped by plugin; install fills the matching slot(s) and Switch back restores them.
A legacy single selection migrates into its base's slot.

* plugins: installed carousel and card grid, category badges, In use state; load language packs that cover protected copy

* fix(theme): let plugin themes colour menus and dialogs

Menus, popovers, selects, hover cards, sheets and dialogs painted a fixed
glass tint (dialogs rgb 23,23,23 in dark mode), so a plugin theme's
--popover was ignored and a true-black theme still showed grey dialogs.
The tint now lives in --floating-surface / --dialog-surface, which a
plugin theme replaces with its --popover.

* plugins: name the app-theme slot a pack fills, not its terminal colours

* plugins: page the installed row by whole cards; e2e follows the card menu

* plugins: hide the installed row's scrollbar and draw kind art instead of initials

The installed carousel pages with its arrows and snap points; the native
scrollbar under it only added a stray bar. Format 1 catalog listings (the
official catalog until catalog v2 publishes) carry no release types or
images, so their cards fell back to a two-letter monogram. Art now reads
the kind from catalog categories, and the last fallback is a plugin glyph.

* plugins: reserve official publishers to Orca's catalog and pin installs to their catalog

Reserved publishers (stablyai, official, anthropic, openai, github, orca*; case-insensitive)
and orca-* ids now install only from Orca's official catalog: a Git URL, local folder or any
other catalog is refused, and a third-party catalog listing one is refused whole. The rule lives
in one import-free module the directory indexer can vendor; validate:plugin uses it too and
warns when publisher differs from the origin remote's owner.

A marketplace install never replaces one recorded from a different catalog; switching means
remove and reinstall. Docs state the publisher == repository owner policy (enforced by the
official and community catalog CI, not a new runtime field).

* plugins: label trust by catalog source and note other catalogs listing an install

The chip beside a plugin now says where trust comes from: the Official mark for Orca's catalog,
otherwise the catalog name and the owner parsed from its Git URL (github.com/acme,
gitlab.com/acme/tools), never manifest text; Dev · unverified for development folders and
Unverified for Git URL installs. An installed plugin's page says which other catalogs list it.

* test(plugins): give update review's Git-backed steps a loaded-runner timeout

Install and update review each fetch a plugin over Git. On a loaded machine that
took 25-30s with or without the publisher checks, past the 10s expect default,
so the test read as stalled on Installing…

* test(plugins): give the row-art fixture the entry's catalog source fields

* test(plugins): say why the panel WebRTC test needs a longer timeout

* feat(plugins): dismissible Alpha notice on the Plugins settings page

Shows "Alpha: The plugin interface may change. Expect nuances." under the
Plugins title until dismissed; dismissal persists in UI state
(pluginsAlphaNoticeDismissed). Localized in all locales, with unit and e2e
coverage for show-by-default and dismiss-across-reload.

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

* plugins: stop shipping Orca Navigation Shortcuts and retire stale bundled installs

Removes the bundled copy and its fallback-catalog listing. On startup, bundled
bootstrap now uninstalls any previously bundled plugin this release no longer
ships, so existing users don't keep a protected entry they cannot remove.
Removal code moves to plugin-removal.ts to keep plugin-install.ts under the
line limit.

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

* fix(plugins): concurrent saves of one image no longer fail the update review

Each save now writes its own uniquely named temp file and treats a rename
failure as success when a concurrent writer already produced the same
content-addressed file. A cache failure now only drops the image from the
preview or catalog refresh instead of failing it.

* fix(plugins): an update keeps unchanged machine recipes off the review's New list

Recipes always counted as new because their commands live in files the
manifest diff cannot see. Main now reads what each recipe can run (id,
checkout mode, commands) from the installed tree and the preview's staging
tree, and only a recipe that runs something new asks for review. An
unreadable file still counts as new.

* plugins: app themes color code in the editor via VS Code-shaped tokenColors

Theme files gain optional tokenColors (TextMate scope -> foreground/fontStyle).
The derived Monaco theme maps them onto Monaco's token names, the plugin-syntax
legend and raw TextMate scopes; unreadable colors are dropped.

* test(plugins): theme smoke checks tokenColors color the editor and revert

* i18n: retire translated entries for plugin strings this change rewords or removes

* plugins: add the custom editors contribution point

A plugin can now declare contributes.customEditors to open files of given
types in the editor tab, beside Source. The editor runs in the same
sandboxed, no-network panel frame and only exchanges the document's text
over a scoped postMessage channel; Orca keeps the buffer, dirty state,
saving, external-change banner, read-only tabs and SSH files.

The Editor | Source toggle and the 'first scene after load is the
baseline' approach follow community PR #27413.

Co-authored-by: Roughriver74 <115543510+Roughriver74@users.noreply.github.com>

* plugins: list custom-editor plugins with panels in the plugin list

* plugins: name an editor-only plugin's access 'Adds an editor'

* plugins: keep the Editors category label for custom-editor plugins

* plugins: restyle plugin frames in place and keep file paths out of editor frame names

A theme change used to remount panel and custom editor frames, which reloaded
the plugin and lost its state (Excalidraw undo, unsent edits). The host now posts
orca-panel-theme and the shell updates the --orca-* tokens and the html class in
place. Custom editor frames are named with a random id: window.name is readable
inside the frame, and the old name carried the file's absolute path.

* plugins: let custom editors open binary files as bytes (contentKind)

A custom editor can now declare "contentKind": "bytes" (with "readonly": true)
to receive the file's raw bytes instead of its text, for formats such as
spreadsheets. Orca pages the bytes from the host that loaded the tab (local,
SSH or a paired runtime, through the existing stat + readChunk calls, so no
wire change), refuses files over 16 MiB with a "too large to open" message,
re-sends on outside changes, and never applies a bytes editor's edits. The frame
gets a transferred ArrayBuffer and the matched suffix, never a path.

Documents are now versioned: { version, readOnly, content: { kind, ... } }.
Text edits may name a baseVersion; an edit against a replaced document is
refused with an orca-editor-edit-rejected conflict. Text documents keep the
legacy text field and unversioned edits still apply, so existing editors work.

"readonly": true on any editor makes the install review say "Opens .x files in
Orca" instead of "Opens and edits".

* plugins: suggest catalog custom editors for files Orca already opens

The file-type suggestion only offered a plugin when a file opened as plain text
or as the binary placeholder. It now also offers a catalog custom editor for a
file Orca shows with a language (an .excalidraw file opens as JSON): the editor
asks main with the `editor` purpose, which matches only release file types the
listing's contributions name as custom editors, and only when no installed
plugin's editor handles the type. Binary files such as .xlsx keep the existing
path, since release fileTypes already include custom-editor types. The banner
reads "A plugin adds an editor for .x files: Name"; Not now and Don't suggest
work per type as before.

* fix(plugins): an update that lets a custom editor edit, take over opening, or change content kind asks again

* fix(plugins): one unreadable catalog summary item no longer hides the whole summary

* fix(plugins): plugin editors never open Orca-core file types by default, and join Markdown, CSV and notebook view toggles as options

* fix(plugins): catalog releases list editor file types as data instead of parsing the display summary

* fix(plugins): custom editor edits must name their base version; drop the pre-release text field

* fix(plugins): a custom editor edit Orca refuses keeps the tab dirty and blocks the save instead of vanishing

Rate-limited edits are held and applied before the next save; oversized ones are reported to
the frame and the user. Every save, from any trigger, first flushes plugin editor frames with a
bounded wait and refuses to write while a frame holds edits the buffer lacks.

* fix(plugins): run custom editors as network-isolated guests and save only on real key presses

Editors see file contents, so they now run as <webview> guests in their own in-memory partition
instead of iframes of Orca's renderer. The session routes everything through a dead SOCKS proxy
(loopback included), refuses every non-editor request and permission, and each guest gets
disable_non_proxied_udp, no popups and no navigation. An Electron test proves fetch, WebSocket,
images and TURN-over-TCP all fail from the editor and from its srcdoc and javascript: child
frames, while the same page in a plain session reaches every listener. Electron 43 still resolves
<link rel=dns-prefetch> names outside the session; the test pins that residual and the docs say so.

Save chords now come from main's before-input-event on the guest, which page code cannot
synthesize; the shell's keydown forwarder and the orca-editor-keydown message are gone. Guests
also run in their own renderer process.

* fix(plugins): enforce a read-only custom editor in main, not just the tab

Main reads readonly from the installed manifest (bytes editors always count) and drops every
edit such a guest posts before it reaches the renderer. The tab host also refuses them and tells
the editor its document is read-only, even on a writable tab.

* test(plugins): the editor-suggestion catalog fixture lists editorFileTypes

* plugins: custom editors edit and save binary files; drop readonly and the isolated guest

Bytes editors now post `{ baseVersion, content: { kind: 'bytes', bytes } }` full
replacements (a transferable ArrayBuffer). The tab keeps the unsaved bytes beside
the text drafts, marks itself dirty, and the normal save pipeline (frame flush,
Cmd+S, close prompt) writes them: locally through a new `fs:writeFileBytes`
atomic replace guarded by the loaded bytes' SHA-256, and on SSH-served or paired
hosts by uploading chunks with `files.writeBase64Chunk` and swapping them in with
the new capability-negotiated `files.replaceWithUpload`. Older hosts are refused
before anything is written. A stale baseline is a conflict: the edit stays
unsaved and the external-change banner offers Reload or Overwrite. Source stays
the binary placeholder.

Per the product decision, every custom editor may edit: the `readonly` manifest
field, its main-side enforcement and re-approval rule are gone, and an editor
that only views simply never posts edits. Editors run in the normal sandboxed
plugin frame again instead of a network-isolated webview guest; the frame's save
chord is honored only while it carries real user activation. Consent now reads
"Opens and edits .ext files in Orca. It can read the files it opens."

* plugins: reserve agentHarnesses and hooks instead of agents; rename agents:read to agent:read

* plugins: syntaxes and custom editors share one file matcher (extensions with the dot, plus filenames)

* plugins: title for themes and syntaxes, when for commands (format 1 names rewritten on parse)

* plugins: add the editor catalog type for custom editors

* plugins: official means listed by the official catalog, not an orca- id

* plugins: one envelope protocol and window.orca for workers, panels and editors

Workers, panels and custom editors now share one internal envelope
({orca: 1, kind: req|res|evt|cancel}) instead of three message sets.
Frames get a frozen window.orca from the shell (api, host.call with
AbortSignal cancel, host.has, on, and document helpers in editors);
ready/hello carries the api, the surface's methods and the theme.
Editors gain host calls through their own session.

The host API table is keyed by method with typed params/results (no
handler casts), per-method surfaces and a numeric since minor; every
params schema is strict. Errors are {code, message, retryable,
retryAfterMs?} with cancelled/timeout/conflict/not_permitted/too_large
reserved and unknown codes read as action_failed. Event payloads are
typed with a closed agent-status enum, and the list entry type is
shared by main and preload.

* plugins: orca.document.onDidChange hands a late subscriber the current document

Editors that subscribe after their bundle loads (Excalidraw) missed the
document that arrived first and stayed blank.

* plugins: custom editor documents up to 64 MiB, with a large-file note past 16 MiB

The cap covers text and bytes on every route; reads and SSH or paired-host
writes are already chunked, so only the shared cap moves. Past 16 MiB the
tab shows a small, non-blocking note that the editor may take a moment.

* plugins: delete dead props and branches left by removed rows, logs toggle and access retry

* plugins: one status word per state (a crashed plugin reads Stopped everywhere) and drop unused plugin locale keys

* plugins: contribution rows use the manifest point names, so the policy table is the one source of catalog types

* test(plugins): assert the Use now line and page behavior, not Tailwind classes

* i18n: add the custom editor large-file note to the English catalog

* plugins: fold the loose shared plugin modules into contributions, frames, images, capabilities, events, languages, installation and catalog folders

* plugins: custom editor tab icons and contributes.newFiles templates

A custom editor may declare an icon; the tab and its hover card show it and the
editor title instead of "Editor", falling back to the plugin icon. contributes.newFiles
declares untitled file types (title, extension, optional UTF-8 template and icon) offered
in the tab bar + menu, its search, the command palette and the plugin page, created at the
workspace root through the Markdown untitled-file flow. The install toast's Try it creates
one for plugins that declare them.

* test(plugins): hidden e2e for a plugin new file from the + menu and its editor tab icon

* plugins: turn the plugin system on by default with a one-time migration

Saved false from the old default flips on once (pluginSystemDefaultedOn guard);
a later deliberate false is respected. The Turn on plugins off-state is gone,
and the official catalog seed is deferred off the startup path (and skipped
offline), seeding on demand when the marketplace is opened.

* plugins: move the card menu Disable/Enable directly above Remove, without an icon

* plugins: show the Alpha notice with the warning banner tokens

* plugins: show each category's icon in the filter dropdown

* plugins: seed drawn fallback art per plugin so two listings never look identical

* plugins: show manifest covers for catalog releases published without images

The official catalog's newest Midnight, Nord, OLED and Dusk & Ember releases
were regenerated without an images block (release-entry only kept images the
author pasted), so their cards fell back to drawn art. A catalog that keeps its
plugins in its own repository now borrows the manifest's images from the
catalog commit, checked like declared images, and the validator's printed
release entry includes them so regenerated catalogs keep their covers.

* plugins: drop the unused Turn on plugins strings

* settings: find the Plugins page by plugin, mod and extension searches

* plugins: remove the Allow full-access plugins setting

Full-access (trusted-worker) plugins now need only their own per-plugin
approval: main no longer gates install, consent, enablement or runtime on a
global switch, and the settings menu item, the Turn on in settings card and
page states, and the stored key are gone. The consent now says plainly what
full access means. Kill list, official review and publisher checks are kept;
docs note an enterprise block policy may come later.

* i18n: retire translated entries for plugin strings this change rewords or removes

* test(plugins): prune low-value plugin tests

Drop tests that asserted CSS classes, DOM structure, internal call counts,
copy, or duplicated coverage; merge overlapping e2e file-type suggestion
specs. Keep security, consent, data-integrity, compatibility and main
journey coverage.

* test(plugins): stop git auto-maintenance racing marketplace fetch teardown

* test(plugins): import the panel session token schema from the frame bridge

* feat(plugins): open orca://plugins install links at the plugin's page

orca://plugins/<publisher.id>?catalog=<official|community|git URL> opens
Settings -> Plugins at that plugin; it never installs. Plugins off shows
Turn on plugins first, an unknown catalog asks before it is fetched, and a
malformed link only toasts. Reuses the skill-share deep-link path (open-url,
second-instance argv, sidecar handoff) with repeat/burst dedupe, and
registers orca:// on packaged Windows, which NSIS leaves unregistered.

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

* fix(plugins): show the linked catalog URL on its own line

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

* fix(plugins): follow main's moved plugin files and language-pack coverage field

* fix(plugins): keep a plugin disabled through its update and give format 1 event handlers worktreeId

* fix(plugins): keep held editor edits across view switches, ignore stale editor entry reads, share bytes across split panes, and meet the low-evidence lint

* refactor(editor): keep the file content loader under the line limit

* fix(plugins): keep a bytes draft's baseline across reloads, cover plugin new-file menu rows, and move render-time ref writes into layout effects

* refactor(tab-bar): keep the create-menu controller under the line limit

* refactor(tab-bar): keep the create-menu controller under the line limit with the plugin new-file case

* test(plugins): turn plugins back on through settings in the install-link journey

* fix(wsl): import the atomic plugin file writer from its new folder

* fix(plugins): save bytes custom-editor edits from Save-and-close and the overwrite banner

* test(plugins): park the Linux-only bytes editor e2e failure as a follow-up

* fix(plugins): follow main's new language-pack tests to the moved artifact module

* test(i18n): give main's new plugin language-pack fixture its coverage

* plugins: manifest v2 ships in 1.4.245, since 1.4.244 released without it

* fix(i18n): import the plugin language-pack artifact from its folder in the shared i18n instance

* fix(plugins): drop syntax loads that land after dispose and restore terminal overrides on Switch back

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Roughriver74 <115543510+Roughriver74@users.noreply.github.com>
2026-10-28 20:58:25 -07:00
OrcaWinandOrcaWin 3094a4ae7b fix(i18n): code Create project failures and keybinding diagnostics (#35626)
* fix(i18n): code Create project failures and keybinding diagnostics

* fix(i18n): localize conflict titles and interpolate create-project copy in one pass

---------

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-10-28 20:36:00 -07:00
NeilandClaude 0976872b9e feat(wsl): let a cached remote policy close the WSL hosts gate (kill switch, remote half 2/2) (#35529)
* feat(wsl): let a cached remote policy close the WSL hosts gate (kill switch 2/2)

A cached remote policy of "block" now closes an internal gate the launch
environment opened, for the whole run. It reuses the closed-gate path that
already exists: with saved hosts or moves the gate becomes serve-only, so
moved projects keep working while nothing new is added or moved. With
nothing saved it is closed. A missing, unreadable or "allow" policy changes
nothing. Main stamps windows so preload agrees, the download runs only on
Windows with the internal gate open, and telemetry reports the block.

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

* feat(wsl): report the applied WSL hosts policy and keep the way back under a block

Telemetry reports the cached policy applied at launch as allow, block or
missing (omitted when the internal gate left it unread). Tests pin that a
block keeps Remove host, the way back and unfinished-move recovery, refuses
new hosts and moves, and that a mid-run download applies only next launch.

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-28 19:16:42 -07:00
Neil 3a68805eee fix(wsl): Settings CLI status, CLI repair, interop health and link checks never boot a stopped distro (E9) (#35412)
* fix(wsl): Settings CLI status, startup CLI repair, interop health and link checks never boot a stopped distro (E9)

- cli:getWslInstallStatus: opening Settings takes a settled running read and shows a refreshable
  "not running" status instead of starting the distro; the refresh button passes wakeWslDistro.
- Startup CLI repair re-reads each listed distro (settled) right before its repair spawns and
  defers it otherwise, so a distro stopping mid fan-out is not restarted.
- Interop health: stop-triggered checks no longer trust the stop event's list; every check takes a
  settled read, and again before the root repair.
- shell:pathsExist (chat link and terminal hover existence) refuses a stopped distro's UNC path
  instead of statting it; callers already retry an unverifiable answer.

* docs(wsl): note the missing-kernel availability probe as the known E9 exception

* fix(wsl): shell:pathsExist refuses only the stopped distro's path, not the whole batch

One UNC path in a stopped distro no longer fails the host paths checked with it; the renderer
treats that path alone as unverifiable and retries it.
2026-10-28 18:24:26 -07:00
Neil 8c3551bde3 fix(wsl): background Git, GitHub and GitLab reads never start a stopped distro; explicit opens may (E9) (#35410)
* fix(wsl): keep background git and GitHub reads from booting a stopped distro

Git history, diff and status-follower reads, GitHub issue/work-item/check
polls, chat link existence checks, the composer's create-base prefetch,
the worktree-root fan-out and the Settings/Automations orca.yaml previews
now refuse or skip a stopped WSL distro instead of starting it (E9).

* fix(wsl): explicit opens may start a stopped distro; more polls gated (E9)

Folds the parallel G1-G6 work into this branch and splits background
reads from user acts:

- An explicit open runs as a user act for its distro and may boot it:
  showing a diff tab or its Retry, showing Commits or its Refresh,
  opening the checks panel or entering its tab, checks Refresh, a check
  action, and linking or creating a PR. Main takes a wakeWslDistro flag
  on git:diff, git:branchDiff, git:commitDiff, git:history and
  gh:prChecks and runs those reads under runAsWslUserAct.
- Polls, timers, refreshes after Git actions, prefetches and fan-outs
  stay background and are refused while the distro is stopped.
- Also gated: the hosted-review card poll for every provider, the GitLab
  checks-panel poll (background flag), and the composer's open-time
  orca.yaml check (Create re-checks in the foreground).
- Pollers keep the data they already show on a refusal: the issue on a
  card, the checks list, an open diff and loaded submodule children, and
  stop logging it.

* docs(wsl): note that a hosted-review poll's stopped-distro refusal is still logged (E9)

* fix(wsl): Settings re-asks orca.yaml once a stopped distro runs; drop link-check gating (E9)

- Link existence checks (shell:pathsExist / pathExists) are no longer
  gated here: answering "missing" cached chat and terminal links as
  absent and toasted "File not found" on markdown link clicks. The
  pathsExist refusal lives in its own PR, which throws instead.
- Settings no longer sits on "checking" forever: a repo whose distro
  refused the background check is re-asked on window focus, on
  visibility, and after 10 s, until it answers. An unverifiable distro is
  now marked (wsl-distro-unverifiable) and treated as unread too, instead
  of being stored as "no orca.yaml".
- The sidebar issue poll and the Tasks list and count refuse with "Open a
  terminal in it to start it", since their Retry is itself a background
  read; the checks panel keeps "Retry", whose Refresh is a user act.

* fix(wsl): Tasks Refresh, interactive review refresh and manual PR lookups may start a stopped distro (E9)

- Tasks: a Refresh, Retry or search, or Tasks opened on a single repo,
  passes wakeWslDistro through gh:listWorkItems and gh:countWorkItems
  (IPC only; the RPC twins and web routes drop it). A multi-repo open
  stays background. Their refusal says "Retry" again; only the sidebar
  issue poll says "Open a terminal".
- hostedReview:forBranch wakes for an interactive request (the checks
  panel's manual Refresh), so GitLab, Bitbucket, Azure DevOps and Gitea
  reviews with nothing cached can load.
- gh:prForBranch, the renderer's fallback when refreshPRNow is missing,
  is gated like the other card polls; a manual lookup wakes, matching
  refreshPRNow's manual-only rule. The fallback call moves into its own
  module, built from the refresh candidate.

* fix(wsl): only an explicit Refresh starts a stopped distro for review status (E9)

hostedReview:forBranch woke on admissionTier 'interactive', but the
visible-review scheduler sends 'interactive' for the selected card on a
timer, so that poll restarted a stopped distro every interval. The wake
is now an explicit wakeWslDistro flag (desktop IPC only). The card
refresh helper sets it, since every caller follows a user act (the
checks panel's manual Refresh, or a review created, linked or edited),
and the manual Refresh also wakes its pre-refresh git status.
admissionTier is back to priority only.

* fix(wsl): an explicit review Refresh never joins a refused poll (E9)

fetchHostedReviewForBranch let a forced card Refresh join an in-flight
forced scheduler poll, since the join only compared force. On a stopped
WSL distro that poll is refused, so the click got the refusal. The
in-flight entry now records the wake, and a waking request joins only a
waking flight; a poll may still join an explicit Refresh.
2026-10-28 18:16:49 -07:00
Neil 9babef2866 fix(wsl): background preflight and agent detection never boot a stopped WSL distro (E9) (#35411)
* fix(wsl): background preflight and agent detection never boot a stopped WSL distro (E9)

Integration checks (git/gh/glab --version and auth status) and agent detection ran inside the
focused workspace's WSL distro on every mount, poll and status-bar render, and wsl.exe starts a
stopped distro. They now take a settled running read first and refuse (WSL_DISTRO_STOPPED or
WSL_DISTRO_UNVERIFIABLE) when it is not running. Window focus/visibility, retry buttons, agent
refresh buttons, a runtime choice in Settings and first-run onboarding pass wakeWslDistro, which
runs the check as a user act on that distro only. Running distros behave exactly as before.

* fix(wsl): a focus wakes only a focused WSL workspace's distro; end-to-end preflight gate test

A focus with a native workspace active sends no wake. Ports the real running-list preflight case
from #35395 and adds user-act scoping cases.

* test(wsl): model the running gate in the preflight RPC suite and cover a stopped distro

Windows CI runs the real gate, so the WSL detection case must state that the distro runs.

* fix(wsl): an agent refresh click never joins a background refresh it would be refused with

Store the wake flag with each in-flight refresh, as preflight.ts does, so a status-bar or Agents
refresh click during a mount refresh reaches main as a user act. Note why the RPC project branch
stays ungated.

* fix(wsl): a refused background check waits for the distro, and an agent picker wakes it

A preflight or agent detection refused for a stopped WSL distro was final: once the distro came
up, nothing re-ran it, so the tab-bar + menu offered no WSL agents (real-WSL golden). Now a refusal
is deferred until the distro is next confirmed running (deferUntilWslDistroRunning), and main tells
renderers to re-ask. Opening an agent-launch picker (tab-bar +, composer, continuation dialog,
source-control action dialog) whose list was refused retries it as a wake, never joining a
background probe.

* fix(wsl): an open agent picker wakes a refused list once, and never retries real failures

The picker wake fired whenever the list read 'failed', so a woken probe that failed again
(pending -> failed) re-fired it: an endless wsl.exe loop while the picker stayed open, and repeated
probes for native failures too. It now fires at most once per open per target, and only when the
last probe was an E9 refusal (distro not running/unverifiable), matched by the gate's message.

* fix(wsl): forget agent-inventory refusals when the inventory or its project contexts are cleared
2026-10-28 18:01:42 -07:00
bfbdcd7e80 feat(wsl): run Git for Windows for a WSL project on a Windows drive (D1c-2) (#33090)
* feat(wsl): run Git for Windows for a WSL project on a Windows drive

A project on C:\ whose runtime is WSL ran every Orca Git command through wsl.exe on
/mnt/c: ~300x slower than Windows Git, and blind to Windows-side edits. The project's
WSL runtime preference gains an optional `git: 'windows-host'`. With it, Orca's Git
(and where new worktrees go) is Windows; terminals, agents, accounts and hooks still
resolve to WSL, so nothing else changes. Older builds drop the field and go back to WSL
Git.

The conversion checks first, then the caller saves the returned preference through the
normal project update:
- Git for Windows must exist.
- New worktrees must not be set to go inside the distro.
- No linked worktree may have a Linux-path link. WSL Git made those; Windows Git sees
  them as prunable, and Orca's prune after a worktree removal would delete their
  metadata.
- Optionally writes core.autocrlf=true into the repo when Windows Git uses it, so WSL
  Git in the project's terminals stops seeing CRLF changes. Never core.checkStat.

IPC is refused off Windows and with the internal WSL hosts gate closed.

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

* test(wsl): mock the drive-project IPC in the core handler registration test

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

* test(wsl): pin WSL Git for a drive project that never chose Windows Git, and record which Git ran

* fix(wsl): base-ref lookups and the remote-identity sweep run Git where the project's runtime does

* fix(wsl): route base-ref and remote-identity reads by runtime only with WSL hosts on

These reads ran host Git on main. Routing them into a WSL-runtime project's
distro is part of D1c, so with the WSL hosts gate closed they run host Git
exactly as before.

(cherry picked from commit 2ed82987dc5b82c3a86ea7e03ce05e9efc3e77fb)

* refactor(ipc): register the filesystem handlers in one call, keeping core registration under its line cap

The optional commit-message resolvers pass straight through, so the call is the same with or
without them. #34059 left this file at the 300-line cap and the WSL drive-project wiring adds to it.

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

* fix(wsl): skip folder repos in the Windows Git check, and read worktree links with the prune guard's reader

Folder repos run no Git, so rev-parse on them always failed the preview. The link reader and
the WSL-authored test now come from worktree-prune-guard.ts; the copy's startsWith('/') wrongly
refused //wsl.localhost links, which Windows Git writes itself.

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

* fix(wsl): the remote-identity sweep checks the distro a drive project's Git runs in, so it never boots a stopped one

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-10-28 15:15:26 -07:00
Neil 5018241866 feat(plugins): app and terminal themes in one Theme list, painted safely (#30719)
* plugins: app and terminal theme contributions

Manifests can contribute themes (public CSS tokens over a light or dark
base) and terminal themes (an ANSI palette), each at most 16 KiB. App
themes must set --background and --foreground; omitted tokens are derived
from them (surfaces copy the background, muted text, borders and accents
mix the foreground into it), and values must be colors (no keywords,
lengths, calc, var or url). Both load through the shared artifact-pack
registry with the plugin's display name and a content revision, reach the
renderer over IPC and are re-validated there. Catalog releases can declare
app-theme and terminal-theme, and marketplaces stop hiding theme listings.
docs/reference/plugin-themes.md documents the format.

* plugins: resolve a selected plugin terminal palette on every host

Selecting a plugin terminal theme saves a copy of its palette in settings
(terminalPluginPalettes). The shared lookup reads it, so main, headless
serve and relays answer terminal color queries and native chat matches the
terminal without the plugin loaded; a malformed snapshot falls back to the
default theme.

* plugins: one Theme list with plugin themes, painted safely

Appearance keeps the System/Dark/Light control until a plugin offers a
theme; then Theme becomes one list with plugin themes after a divider,
labelled with the publisher when names collide. Choosing one stores its
base in theme and remembers the earlier built-in choice, which comes back
while the plugin is off or after it is uninstalled; a plugin update that
changes the base re-syncs theme. It also offers the plugin's terminal
colors in one click.

The renderer paints only values CSS parses as colors, checks every
text/surface pair, ring and border against the effective set (plugin
values over this mode's built-ins) and repeats until nothing changes, so
dropping one pair cannot expose another. The applied theme is cached with
its id and revision, painted before first render, and dropped as soon as
settings show it no longer applies. Main bounds the new settings on every
write and load, and uninstalling a plugin clears its theme choices and
palette copies. Terminal themes appear under Plugins in the picker with the
plugin's name and are passed to lookups instead of read from a store.

* plugins: type the theme settings normalizer input; let the store own its normalization

* fix(plugins): check text contrast on the editor surface and clear terminal overrides when adopting a theme's terminal colors

* fix(plugins): reject theme colors the canvas probe silently ignores
2026-10-28 15:13:10 -07:00
Brennan Benson bb1b05e3bd Clear Google cookies from a browser profile, as often as you need (#34158)
* Add repeatable Google cookie clearing for browser profiles

* Bind Google cookie clearing to its successful import target

* Repair cookie-clear contract test boundaries and positive flows

* Correct cookie-clear test fixture typing

* test: use checked runtime bindings in browser profile RPC fixture

* test: provide page registry to sidecar forwarding fixture

* fix: use selected browser profile owner for cookie clear menus

* test: provide confirmation context to link materialization fixture

* test: classify cookie clear session channels

* refactor: consolidate browser command property guards

* test: update cookie clear fixture for merged registry API

* feat: move Google sign-in recovery to browser settings

* Freeze Settings Google cookie clearing intent and refresh availability

* Restore baseline browser import disclosure coverage

* Pass the selected browser profile into cookie settings

* Localize Google cookie settings search keywords

* Update cookie restart fixture policy exports

* Keep localized browser search results visible

* fix: make Google cookie clear a red right-aligned setting that explains when to use it

* Explain why Google cookies are handled separately in the clear row caption
2026-10-28 14:40:57 -07:00
Pr1pandJinjing 8e9a08b543 fix(preflight): skip probing agents disabled in Settings (#34832)
* fix(preflight): skip probing agents disabled in Settings

Background worktree-startup detection now passes settings.disabledTuiAgents
(which it already held for agent selection) into detectInstalledAgents, so
disabled agents are neither probed nor reported. The preflight:detectAgents
and preflight:refreshAgents handlers keep probing every agent so Settings →
Agents still reports real install status.

Fixes #34635

* test(preflight): cover disabled agent detection forwarding

* fix(preflight): skip disabled agents in background inventory

* fix(preflight): restore native inventory after settings changes

---------

Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com>
2026-10-28 13:32:08 -07:00
Neil 045f8c8266 feat(files): show images and PDFs from the byte-range preview path, local and remote (#35165)
* feat(files): show images and PDFs from the byte-range preview path, local and remote

ImageViewer and PdfViewer read an orca-media:// URL instead of base64 when
the editor opens a file: images load by URL, and pdf.js reads PDFs by
range, so a large PDF no longer arrives whole through files.readPreview or
pages into memory first. The grant mints only for the editor's viewers;
diffs, git blobs, markdown images and the paired web client keep base64.

- Minting an image checks its header with the base64 path's rule, so an
  image proven too large to decode safely never reaches the decoder.
- orca-media gains fetch and CORS privileges, and range responses expose
  Accept-Ranges and Content-Range, so pdf.js can read by range.
- Grants report the file size the viewer used to estimate from base64.

* fix(files): read PDFs by real range, check served images, keep old hosts' previews

- pdf.js fetches ranges only over http(s), so an orca-media URL was read
  whole; a small range transport now hands it bytes=a-b reads from the
  grant, and a failed range fails the preview instead of hanging it.
- A file can change after mint, so the raster header is checked again on
  the bytes each image request serves (the same handle locally).
- A paired host without files.readChunk falls back to files.readPreview
  for images and PDFs, as before; only audio and video need the update.

* fix(files): mint no local document grant for a renderer gone mid-mint

The local image/PDF mint is async now (authorize, open, header check), so
a renderer that closed meanwhile could leave a grant no teardown drops. It
binds the renderer lifetime first and refuses to mint once it's gone, as
the remote mint does.

* fix(files): keep the 50 MB cap for URL images, and probe remote headers incrementally

An <img> decodes the whole file, so image grants keep the base64 path's
50 MB cap; PDFs, now read by range, don't need one. A remote header probe
reads only each wider step's new bytes instead of re-reading from the
start. A replaced image of a different size resets its zoom.

* fix(files): tear down a PDF load whose range failed, and read 256 KiB ranges

A failed range only showed the error and left the pdf.js task, its worker
and pages alive; it now destroys that load first. pdf.js asks for one
readChunk-sized range at a time instead of 64 KiB, and a Range preflight
gets its CORS answer instead of 405.

* fix(files): read PDF ranges over IPC so orca-media needs no fetch or CORS

orca-media had gained supportFetchAPI and corsEnabled, and its responses
sent Access-Control-Allow-Origin: *, so pdf.js could fetch ranges. That let
any default-session frame holding a token read the bytes. pdf.js now reads
a PDF grant's ranges through fs:readPreviewRange, which main answers only
for the renderer that minted the token. The scheme is back to element-load
privileges (standard, secure, stream), responses carry no CORS headers, and
preflights get 405.

Grant bookkeeping moves to preview-grant-registry.ts. An e2e spec proves an
orca-media URL can't be fetched from the app page or read from another
session.

* ci(e2e): route the byte-range preview sources to the viewer specs that exercise them

Changes to the orca-media grant path, the image/PDF viewers and the PDF
range transport now run the PDF reload, rebuild and find specs, the remote
large-file open, media playback and the partition-isolation spec.

* fix(files): stop PDF range reads on abort, abort remote reads with the window, fix the CI route

- A closed or reloaded viewer schedules no further pieces of a merged range,
  and a remote range read stops when its window goes away.
- A URL without a size is an error rather than a fetch the scheme refuses.
- The PR guard job's sparse checkout and its admission test include the new
  route module, and the route now also covers the mint handler, preload
  bridge, editor surfaces, scheme registration and read client.

* fix(files): classify linked previews by their target, keep range reads off stopped WSL distros

A local image or PDF reached through a link is classified by the file it
resolves to, at mint and on every read, and a re-mint after the link is
retargeted gets a fresh grant of the new type. A paired host's document
whose first bytes are another kind defers to files.readPreview, which
classifies the target there. Lazy local reads go through the non-waking
WSL guard, as fs:readFile and fs:readFileChunk do.
2026-10-28 09:31:22 -07:00
Neil 808aaee3f6 fix(wsl): hide the sidebar setup-script card while the repo's distro is stopped (#35163)
A background hooks:check refused for a stopped WSL distro now says so (unavailable:
'wsl-distro-stopped'). The setup-script prompt maps it to a distro-stopped state: the card hides
instead of showing 'Couldn't verify…' with a Retry that cannot succeed, and focus re-inspects, so
the card returns once the distro runs. An unverifiable distro stays a retryable error.
2026-10-28 07:59:54 -07:00
Neil c58591e56d fix(wsl): refuse render-driven hook checks on a stopped distro instead of booting it (#35155)
hooks:check takes an optional background flag. The sidebar setup-script prompt and the setup
guide's progress probe, which run on render, pass it: on a stopped or unverifiable WSL distro
the check answers status 'error' without opening orca.yaml over \\\\wsl.localhost, which would boot
the distro. Launch preflight and settings, user acts that start the distro anyway, are unchanged.
2026-10-28 07:33:15 -07:00
f231d1cde6 feat(files): play remote audio and video through one byte-range preview path (#35038)
* feat(media): read remote media ranges through the paired runtime

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(media): stream remote media through orca-media grants

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(editor): play video and audio from remote runtime workspaces

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(media): surface update-required for remote media and cancel in-flight chunks

Probe files.readChunk once per environment and pairing revision before minting a
grant so older servers show the update message. Closing a media stream now aborts
the in-flight remote chunk. Also validate the IPC address, fix extension-only
names, and tighten the preview return type.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* refactor(files): one classifier decides which viewer opens a file

Local reads, files.readPreview on hosts, git blob previews and the desktop's
remote binary handling each kept their own image/PDF extension table, and
media had another. file-preview-kind.ts is now the one table: image, PDF,
audio or video, with the MIME type. AVIF, which only local reads knew,
previews on hosts and in git diffs too.

* perf(runtime): give byte-range reads their own small call lane

files.readChunk (media streams, paged files, downloads) now runs in a
two-call lane per environment, so a playing video or a download never holds
the slots terminal input and workspace calls use.

* feat(files): stream previews from paired runtimes by byte range

Generalises the remote media grant into one byte-range preview path: an
orca-media:// grant keyed by environment, worktree, path and pairing
revision, streamed over files.stat/readChunk. Audio and video use it now;
the shared classifier decides which viewers do.

- One files.stat at mint replaces the readChunk probe: it proves the host
  serves range reads and that the path is a file, so an old host or a
  missing file fails in the tab, not as a silent player error.
- A burst of range requests shares one stat (reused for 1s), so seeking
  costs one round trip less on a slow link; later requests re-stat, so a
  growing recording keeps playing.
- Removing a paired runtime revokes its grants; a re-paired one answers
  410 and drops the grant.
- Only readers that render bytes mint a stream; baseline checks don't.

Co-authored-by: Sahil Sharma <128581847+SahilSharma0810@users.noreply.github.com>

* fix(files): prove chunk reads at mint, and let a reload report the new size

Hosts from v1.4.5 answer files.stat but only v1.4.111 and later answer
files.readChunk, and no capability names it, so minting reads one byte,
cached per pairing, and an older host fails in the tab with the update
message. A re-mint seeds the grant with its fresh stat, so a reload never
reuses an older size. A renderer gone mid-mint mints nothing. Preview
chunks time out after 20s so a stalled one frees its lane sooner.

Co-authored-by: Sahil Sharma <128581847+SahilSharma0810@users.noreply.github.com>

* fix(files): resolve preview hosts in the pairing registry's own directory

The remote preview reader looked environments up under the canonical
user-data path, while the pairing IPC writes the registry under
app.getPath('userData'), which can differ in casing on case-sensitive
installs. Both now use one getter. A chunk-read verdict is cached only
when a pairing revision pins it, so it can't outlive a re-pair.

Co-authored-by: Sahil Sharma <128581847+SahilSharma0810@users.noreply.github.com>

* fix(files): read the pairing registry from one directory everywhere

Doc previews resolved paired servers under the canonical user-data path,
the same casing mismatch the media reader had. They and every other main
caller that names the registry directory inline now use
getRuntimeEnvironmentRegistryRoot(), so it has one source.

Co-authored-by: Sahil Sharma <128581847+SahilSharma0810@users.noreply.github.com>

* fix(files): pin an unpinned preview grant to the pairing it was minted on

A renderer that hadn't loaded the environment's pairing revision yet sent
none, and every later chunk read went unguarded, so a re-pair under the
same environment id would let the old URL read from the new host. Main now
pins the registered revision at mint.

Co-authored-by: Sahil Sharma <128581847+SahilSharma0810@users.noreply.github.com>

* refactor(files): route the remaining pairing-registry readers through the one getter

AI-vault host scans, the settings runtime-environment preference, skill
capability checks and the mobile relay host list named the registry
directory inline. They now use getRuntimeEnvironmentRegistryRoot() too;
callers that also use the directory for other storage keep their own path.

Co-authored-by: Sahil Sharma <128581847+SahilSharma0810@users.noreply.github.com>

* refactor(files): write new pairings through the same registry root

The ephemeral VM pairing writer named the registry directory inline; it now
uses getRuntimeEnvironmentRegistryRoot() like every reader.

Co-authored-by: Sahil Sharma <128581847+SahilSharma0810@users.noreply.github.com>

---------

Co-authored-by: Sahil Sharma <128581847+SahilSharma0810@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 05:08:55 -07:00
NeilandClaude 2747079f94 fix(wsl-mover): ask before quitting while a WSL project move runs (#34860)
* fix(wsl-mover): ask before quitting while a WSL project move runs

A plain quit (Cmd/Ctrl+Q, closing the window) during a move gave no warning: the next launch
recovers safely, but the move is lost. While main reports a move running, the window's close
guards now ask "A project is moving to its WSL host. Quit anyway? The move will be undone at next
launch." Behind the internal WSL hosts gate: the guard registers only with it open, and main
answers false with it closed. An intentional restart and an unanswered question never prompt.

* fix(wsl-mover): the quit prompt's running check gives up after 2 s

The close request is acknowledged before the guards run, so a hung wslMove:isRunning would block
quitting for good. The guard now treats no answer within 2 s as no move running, like a failed
ask.

* test(wsl-mover): give the flag-off host test the confirmation dialog the quit guard reads

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-28 03:39:22 -07:00
Neil ad8d35eb59 feat(wsl): serve saved WSL work behind a closed hosts gate (gate states, kill switch 1/2) (#34683)
* feat(wsl): serve saved WSL work behind a closed hosts gate (serve-only)

The WSL hosts gate gains three states. A closed gate with a saved WSL host or a
committed move becomes serve-only: saved hosts stay listed and reachable,
committed rewrites, retention, interrupted-move recovery and the way back keep
running, and the preload API is exposed through a stamped window argument.
Adding hosts and starting moves still need the open gate. With nothing saved
the gate stays closed exactly as before.

* fix(wsl): stamp serve-only into the dashboard popout and Space windows too

They load the same preload, so without the argument they hid saved WSL hosts'
status and Connect in serve-only. One helper now supplies the argument for every
window on the shared preload.

* fix(wsl): reviewer follow-ups for the serve-only gate

- Refuse wslHosts:add unless the internal gate is open here, so this PR is safe
  on its own.
- Promote on any move journal entry, so recovery can reset an in-flight move
  whose host record is gone.
- List unfinished moves in serve-only.
- An unreadable WSL deployment store fails closed in serve-only too, through a
  shared serving flag main sets at startup.
- Report the gate state in wsl_usage_snapshot as an optional field.
2026-10-28 00:20:39 -07:00
Neil f173998203 feat(wsl-host): a chat on a stopped WSL distro says so and offers Start (#34681)
* feat(wsl-host): a chat on a stopped WSL distro says so and offers Start

* fix(wsl-host): a chat's Start never cancels a pending WSL host Remove
2026-10-28 00:03:58 -07:00
NeilandClaude 1eaaf85e9f Land the WSL project mover dark (merges only after verify-15b, the flag-off audit and green CI) (#34059)
* test(orcad-migration): pin the SSH manifest through the real dormant collectors

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

* refactor(orcad-migration): read the dormant-state export through a source-kind scope

A local-WSL scope lives on host local and owns only its projects, so host-wide
matches stay an SSH whole-host rule. SSH bytes are pinned by the collector golden.

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

* feat(wsl-mover): export a local WSL move's signed manifest with its dormant state (D1b-3c)

Collects the scoped projects, their groups and every dormant-state kind through the
local-WSL export scope, rewrites UNC paths to the distro's, and returns blockers for
anything the collectors can't carry instead of dropping it. Automations move disabled,
as the wire requires, and come back as a list to re-enable on the host after commit.

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

* fix(wsl-mover): reset an interrupted move to zero, or finish it once the host proves a commit

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

* fix(wsl-mover): abort a move by its identity when its manifest is too large for a request

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

* fix(wsl-mover): cap a new move's manifest below the request limit, and move forward on a by-reference commit only once the host flushed it

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

* fix(wsl-mover): remove only the newest move of a distro, so its chain stays readable

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

* feat(runtime): clients read a migrated pane's scrollback from its host (D1b-6d-1)

* fix(runtime): hold a remote pane only when its host is known to serve scrollback, and never read bytes in render (D1b-6d-1)

* fix(runtime): route the scrollback read by the tab's own host, never the focused one (D1b-6d-1)

* fix(runtime): resolve the scrollback host through resolveOwner and use the shared RPC error check, after main moved both (D1b-6d-1)

* feat(wsl-mover): a moved project's existing chats stay listed and resumable on this computer (D1b-7a)

* fix(wsl-mover): moved-project chats go through the layout owner instead of writing layout directly (D1b-7a)

* test(wsl-mover): run the moved-project chat runtime test in the Node runtime (D1b-7a)

* test(wsl-mover): read the hidden local row through main's per-host hidden-repo API (D1b-7a)

* feat(wsl-mover): rewrite the desktop's routing and seed the host's session after a move (D1b-7c)

Registers the main-side desktop rewrite stores ui-routing, session-globals and host-session-seed. Adds a +2 main layout-writer baseline line for wsl-move-main-rewrite-stores.ts: these are one-shot, ledgered move rewrites, and no LayoutCommand can set focus or seed a runtime host's partition.

* fix(wsl-mover): finish the main rewrite before the window loads, keep one copy of each moved tab, and copy instead of move (D1b-7c)

* feat(wsl-move): rewrite renderer stores for moved projects and keep their UI through catalog prunes (D1b-7d)

Registers the renderer-side desktop rewrite stores quick-open-history, file-view-preferences and notebook-environments. The renderer copies each moved workspace's entry to its WSL host key, marks it applied in the same storage, and records the ledger entry; main requests a storage flush first and accepts only the main window. Committed moves are re-listed for re-application at every startup. Catalog prunes keep filters, hook trust and setup-script dismissals of moved repos.

* fix(wsl-move): import the migration manifest types from main's orcad-migration folder (D1b-7d)

* fix(wsl-move): import wslUncPathToLinux from shared wsl-paths in the manifest exporter, where 7d moved it (D1b-7d)

* feat(wsl-mover): rewrite a moved project's orchestration history on the desktop (D1b-7e)

Settled local worker dispatches and their worker-terminal rows in a moved
project take the WSL host's worktree id, in one ledgered, idempotent store.
Chat workers, live rows, federated attachments and JSON receipts stay as
written; automations keep their unchanged local copy (the scheduler only
advances it). Per-table dispositions for orchestration.db.

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

* fix(wsl-mover): hold the orchestration store while a worker in scope is live; skip federated rows (D1b-7e review)

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

* feat(wsl-mover): copy in-app browser logins into the WSL host on a move (D1b-7f)

A committed WSL move now copies each in-app browser profile's cookies once into
the WSL host's stable route partition (owner decision 3a), merging without
overwriting what the host jar holds and never touching the source. Where that
is infeasible (no deployment link so no stable partition, the destination is
not a WSL host or is gone, or the partition name is held by another identity)
the host starts fresh and the move journal records why (3b). Registered as the
ledgered browser-cookies rewrite store; it runs only once browser sessions and
an app window exist. WSL_MOVE_DESKTOP_REWRITE_REGISTRY_COMPLETE stays false.

* feat(wsl-mover): retain moved source rows, reconcile them on start, and plan delta moves (D1b-8)

* fix(wsl-mover): keep the retained mark out of the WSL move manifest (D1b-8)

* feat(wsl-mover): the move driver runs a WSL move end to end, with production gate facts and journaled restore folders

One entry point (and preview/start/cancel/listUnfinished IPC for D1b-9) plans the move, gates
terminals on the hook server's agent status, the close-tab process probe, running automation runs
and live dispatches, pauses and proves exit, stages, sends blobs, commits only from
blobs-transferred (#29825), and finishes on the desktop. Any failure resets to zero or goes forward
when the host committed. Restore folders and the automations to turn back on are kept in the
journal; the host turns those automations back on after commit, retried until it confirms.
A real-PTY test pauses daemon shells, and runs on a real distro in the WSL orcad lane.

* feat(wsl-mover): move dialog with preview, blockers, progress and result behind the internal WSL hosts gate (D1b-9)

* fix(wsl-mover): move dialog hides instead of blocking, follows runs after a reload, and lets a start settle an unfinished move (D1b-9 review)

* fix(wsl-mover): word an unfinished earlier move as still finishing (D1b-9)

* test(wsl-mover): type the e2e's fake move state instead of reading it through Reflect (D1b-9)

* WIP(wsl-mover): 7f-2 registry complete, coverage proof, startup re-apply (D1b-7f-2)

* test(wsl-mover): registry-complete lifecycle, stale-focus restart, unload re-apply (D1b-7f-2)

* fix(wsl-mover): share the session focus write so the re-apply adds no layout writer (D1b-7f-2)

* fix(wsl-mover): redo additive rewrites once at a stale window's unload; behavioural coverage proof (D1b-7f-2 review)

* test(wsl-mover): the IPC record test now finishes the rewrite with the registry complete (D1b-7f-2)

* fix(wsl-mover): fence worktree-less spawns by cwd and re-prove terminal exit before commit (R46, WIP)

* test(wsl-mover): pin the commit re-proof, browser jar identity and repeated move generations (R45, R46, R47)

* fix(wsl-mover): fence any spawn by its folder and name the re-proof's reason (R46 review)

* fix(wsl-mover): a terminal in a known folder outside the move never blocks it (R46 follow-up)

* test(wsl-mover): kill the real move driver at every point against a real host store; abort a reset move's leftover stage on reconnect

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

* fix(wsl-mover): carry unsaved drafts into the moved project; round-trip every fixture through the real export, driver and host import

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

* fix(wsl-mover): reset a move waiting on a distro that is proven unregistered; test updates mid-move (R41), lost distros (R38) and two desktops (R39)

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

* test(wsl-mover): list the D1b-10 real-SQLite mover tests

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

* test(wsl-mover): type the fault test's journal hook without a cast

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

* fix(wsl-mover): reset for a lost distro even when its kept host fails to connect, never for a renamed one; compare filled-in repo keys only where the source lacked them

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

* fix(wsl-mover): carry markdown frontmatter visibility and closed-terminal-tab tombstones into the moved project

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

* test(wsl-mover): read the persisted stage through a typed record, not Reflect

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

* feat(telemetry): report how WSL moves end, by phase and reason code (R43)

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

* fix(telemetry): report a pause refusal's reason, an outcome for a thrown run, and only real transport errors as unreachable

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

* fix(agent-hooks): a WSL host's orcad never removes the hook entries the WSL bridge shares (R42)

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

* fix(agent-hooks): a WSL host's Codex account-home launch with hooks off skips too; mark the R42 guard to drop with the bridge

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

* feat(wsl-mover): "Use on this computer again" brings a moved project back as it was before the move

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

* fix(wsl-mover): judge a bridge WSL pane's foreground inside its distro

On Windows every live bridge pane's own process is wsl.exe, so the busy-terminal gate read every
open WSL terminal as running a program and refused the move. Read the pane's terminal foreground
process group inside the distro (processes carry ORCA_PANE_KEY): only shells means idle, anything
else blocks, and an unreadable guest stays unverifiable.

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

* fix(wsl-mover): only the selected projects' session rows can refuse a WSL move

The session eligibility check treated host local's whole partition as the move's, as the SSH
conversion does. So an agent session or unsaved draft in any other project on this desktop, or in a
project an earlier move retained, refused every later WSL move from that distro.

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

* fix(wsl-mover): sign the manifest as the journal and the host read it

The digest was computed over in-memory rows. A value with toJSON (a Date on a row added this
session) canonicalized to {} when signing but to its string once written to the journal or sent to
the host, so the first move after adding a project reset with orcad_migration_manifest_digest_mismatch
until a restart reloaded the row. Normalize through JSON before signing.

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

* fix(wsl-mover): carry a slept pane's scrollback from its daemon checkpoint

The move sleeps every pane first, and a slept pane's history lives only in the daemon's checkpoint,
never in a renderer snapshot. The export carried renderer snapshots only, so moved terminals opened
empty. After Sleep the driver reads each paused pane's checkpoint (as cold restore would replay it)
and the export carries it under the pane's own snapshot ref; the snapshot channel serves those bytes,
checked against the signed hash. SSH conversions pass no reader and are unchanged.

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

* fix(wsl-mover): say why when a move can't be prepared, instead of a bare reset

An export or journal failure after the pause (a digest mismatch, on awin) was rethrown and shown as
"everything was put back" with no reason. Nothing is journaled yet at that point, so refuse with the
error named instead.

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

* fix(wsl-mover): retry a renderer store main refused to record

The renderer applied a store and wrote its marker, but ignored a refused ledger record (gated-off,
failed, not-main-window), so a committed move stayed unfinished and refused every later move of the
distro until the next app start. Log the reason and retry with backoff until it lands.

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

* fix(wsl-mover): a WSL pane is idle only when its shell has nothing else

Review of #32343: reading only the terminal's foreground group let background jobs, nohup and
disowned work, setsid daemons and a script waiting at read pass as idle, and the move's Sleep would
end them. Scan the pane shell's subtree, its session and every process carrying its key; any
non-shell process, or a shell running a script, blocks.

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

* fix(wsl-mover): never leave behind an agent whose key may be the selected project

Review of #32343: scoping the session check to the selected projects let an unqualified key for a
repo id another host also registers fall out of both the transfer and the refusal. Such a key now
refuses. Adds tests that fail on the old code: another project's live agent with its tab open and
its draft don't refuse, the same kinds of state in the selected project still do.

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

* fix(wsl-mover): refuse an unreadable slept checkpoint, prefer it, and spool it to disk

Review of #32343:
- A checkpoint behind an unreadable log (or an unreadable one) refuses with the pane named instead
  of carrying stale history.
- For a pane the move just slept the checkpoint is the newest copy, so it wins over an older
  renderer buffer or snapshot.
- Buffers spool to a per-move folder and only their hashes and sizes stay in memory; the snapshot
  channel reads them by offset. The folder is removed whatever the outcome.
- A journal write that landed before it threw is reset, so no orphan record blocks the next move.

awin's checkpoints were 310-615 KB, well inside the 5 MB per-pane cap, so nothing there is trimmed.

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

* fix(wsl-mover): retry a failed listing, never a record another window was refused

Review of #32343: not-main-window can't clear by waiting, while a listing failure is transient.

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

* fix(wsl-mover): sign the manifest as the host parses it

The likelier cause of awin's digest mismatch: the signature covered the exported objects, but the
manifest parser keeps only the fields it knows, so a session field it drops (one a sleep can
write) left the journal and the host hashing something else. Sign the parsed form, and warn when the
parser dropped anything, since such a field does not move.

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

* chore(wsl-mover): split the journal step out of the driver, and format

The review fixes took wsl-move-driver.ts past the line limit; the export-and-journal step moves to
its own module unchanged.

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

* fix(wsl-mover): a slept pane passes the busy check; say what each move dropped or lacked

Re-run on awin (c5d263c77c):
- Every slept pane read unverifiable: the distro was asked about panes whose wsl.exe had already
  exited, and found no shell. Only a pane the host still sees running is asked now.
- A live pane read unverifiable when the WSL relay above its shell carried the pane key too; the
  pane's shell is now the topmost tagged shell.
- The parser-dropped-fields warning names the paths, and the slept-history reader logs how many
  panes it carried, could not read, or found no checkpoint for, so the next run says why a moved
  terminal opened empty.

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

* fix(wsl-mover): read the history of panes slept before the move too

verify-3 on awin: panes the user slept earlier in the session have no live terminal, so the move
listed 0 of 0 panes and moved them empty. Their layout still names the session whose checkpoint
holds the history; read those too, live panes first.

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

* fix(wsl-mover): moved panes paint their history, slept panes are found without a reload, and an older undo names the newer move

- A moved tab's first mount reads its history from the host even before this window has the host's status; it used to mount empty and never ask again.
- The window reports the panes it slept (main retires their binding on Sleep), checked against the moved workspaces before their history is read.
- A tab's Windows shell choice is dropped on purpose, not by the parser.
- "Use on this computer again" on an older move names the projects of the newer one to bring back first.

* fix(wsl-mover): moved panes read their history, an idle agent passes, refusals name tabs and show

- The manifest carries no desktop PTY binding: the host kept them, so the client took each moved
  pane for a live terminal, skipped the scrollback read and opened a fresh prompt.
- Progress counts the terminal history it sends while staging, not only the blobs.
- "Use on this computer again" shows a refusal in a dialog; the toast sat behind the update notice.
- Unreadable slept history names the tab by its title and is refused in the preview too.
- A bridge pane whose one job is an agent CLI lets the hook status decide: idle passes, working blocks.

* fix(wsl-mover): a split agent tab's shell panes restart as shells, not fresh agents

A moved (or any headless-restored) agent tab with a plain shell split beside the agent
pane brought every split back as a new agent: the start path used the tab-wide
launchAgent for each pane that had no recorded session. Tab-wide launch evidence now
applies only to a tab's sole pane, as the renderer already treats it.

Found in the D1b-10 real-host run on awin (report bug 6).

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

* fix(wsl-mover): restore folders of panes slept before the move; keep browser tabs

D1b-10 real-host run on awin (report bug 7, with bug 8 pinned), and verify-3:

- A pane slept before the move had no live terminal, so its last folder never reached
  the restore hints and it reopened at the project root (verify-3 lost `deep/`). The
  pause step now adds the panes the saved session or the window still binds, through
  the slept-pane listing that already feeds their history, so hints and history read
  the same panes.
- The move pauses projects through Sleep, which closes a workspace's browser tabs, so
  a moved project lost its in-app browser tab. A move's Sleep request now asks the
  renderer to keep them; an older renderer ignores the field.
- A moving project's unsaved draft carries (the host-session seed lays it back); a
  test pins that it never refuses the move.

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

* fix(wsl-mover): sweep journal writes that died before their rename

A killed journal write left <id>.json.<pid>.<ts>.tmp in the move journal forever
(D1b-10 real-host run, report bug 10). Once per directory and process, the journal now
removes another process's temporaries older than a minute; its own writes and a
write another instance may still be finishing are kept.

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

* fix(wsl-mover): move dialog and add-host fixes from the real-host run

D1b-10 real-host run on awin, report bug 9:
- A host that will not connect now refuses as host-unavailable, in the preview and
  at start, instead of "Orca could not check these projects" / an unexpected error.
- A run that ends without committing reports its result on the progress channel, so
  a window that only follows the run (reloaded mid-move, or started elsewhere) leaves
  "Moving… Preparing" for the result instead of sticking there until a reload.
- The preview counts terminal panes, as the terminal blockers do, not tabs.
- Small transfers read in B or KB, not "0.0 MB of 0.0 MB".
- A distro that stopped after the add-host list was read refreshes the list, so the
  button becomes Start and add, with a message that says so.
- The add-host copy no longer says bridge projects keep working "as they are" with
  no mention that they can move to the host.

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

* fix(wsl-mover): a sleeping WSL pane keeps its shell's folder

Bash on the bridge reports no folder, so a slept pane's checkpoint kept
its spawn folder and the move reopened every pane at the project root.
Before the final checkpoint of a Sleep, read the shell's folder inside
the distro (the busy check's probe, now with /proc/<pid>/cwd) and store
it. Internal WSL hosts gate and Windows only.

* fix(wsl-mover): moved browser tabs and drafts survive the moving window

The window open across a move saved its mirror of the host's tabs over
the host partition, and the unload's redo skipped any workspace that
mirror named, so the seed's browser tab and draft never came back. The
redo now adds the carried browser tabs and this desktop's drafts beside
that mirror, never replacing what it holds. A seeded draft is bound to
the host, as the SSH move's draft rename binds it.

* fix(wsl-mover): a pane whose only history is a stored snapshot cold-restores it for every client

A moved (or imported) pane carries its history only as a stored scrollback snapshot,
which no paired client's mirrored layout names, so the host started a fresh shell and
every desktop showed an empty prompt. Before starting such a pane, the host now seeds
a terminal session from the snapshot, so the start cold-restores it like any
interrupted session. Reproduced end to end in wsl-move-host-pane-history.spec.ts.

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

* fix(wsl-mover): seed history only for WSL-moved local panes, once per pane

Review of the first version: it seeded any pending pane with a saved snapshot,
including SSH panes (which would be asked to reattach a session the remote never
had) and ordinary panes. Now only panes of a project a committed WSL move brought
here (its import receipt names it) and never an SSH workspace. The session id is
the pane's own, so a later start reuses the folder the pane has since written,
or its clean exit, and never replays the stale snapshot; meta.json is written
last, by rename. Tests pin the gate, the reuse, and the legacy history restore
the seed relies on; the e2e adds the not-moved control.

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

* fix(wsl-mover): seed only panes the move carried; show history once; redo a cut-short seed

Second review: narrow the seed from per-repo to per-pane (a tab created after the
move's import is the host's own), redo a seed a crash left without meta.json, and
prove on a windowless host (as the distro's orcad is) that the desktop paints the
moved history exactly once - its mirrored layout never names the snapshot, so it
never reads it too - and that the moved browser tab is listed.

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

* fix(wsl-mover): refuse "Use on this computer again" while files are unsaved

An unsaved file's beforeunload silently vetoed main's reload after a release, so the
window kept the moved layout with panes bound to dead terminals (awin verify-5).
The renderer now asks for those files to be saved first; main logs a vetoed reload.

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

* chore(wsl-mover): log where an open pane's history, folder and busy verdict come from

awin verify-5 saw an open pane carry 140 B after 3,000 lines, reopen at its root, and
stay "cannot check" after its job was killed elsewhere. None reproduces off Windows,
since all three run through wsl.exe; these lines name the gap on the next run.

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

* fix(wsl-mover): an open pane keeps its shell folder through a move

Bash on the bridge reports no folder, so an open pane's checkpoint kept only its
spawn folder and `cd deep/er` reopened at the project root (awin verify-5). The
pause now reads each live pane's shell folder inside its distro before Sleep ends
the shell, and the restore hint prefers it.

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

* fix(wsl-mover): carry a slept pane's history once, not twice

Cold restore fills scrollbackAnsi from snapshotAnsi for a normal screen, so both
hold the whole buffer; the move concatenated them, carrying every line twice, and
the per-pane cap then kept only the newest ~1,040 of 2,000 lines (awin verify-6).
The move now carries the normal buffer once, as cold restore replays it.

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

* fix(wsl-mover): ask the distro when Windows can't observe a pane after a relaunch

After an app relaunch, Windows reads a reattached bridge pane's wsl.exe children as
unverifiable, so every live bridge pane stayed "cannot check" and blocked every
move until it was slept (awin verify-6). The gate now asks the distro about panes
Windows cannot observe too; only a pane whose shell the distro also can't find
stays unverifiable. Also formats the earlier commits' tests.

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

* fix(wsl-mover): name a terminal's pane after a relaunch before judging it

A terminal the runtime rediscovers after a relaunch carries no pane key until a
layout names it again, and the gate read every paneless terminal as unverifiable,
so no move could start after a restart (awin verify-6). The gate and the pause now
take the pane main's PTY bookkeeping binds the terminal to.

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

* fix(wsl-mover): read manifest diff paths through a typed record, not Reflect.get

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

* test(wsl-mover): type the slept-pane modes fixture instead of asserting it

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

* fix(wsl-mover): the move gate reads panes a relaunched window has not reopened

After an app relaunch, every live WSL bridge pane read "cannot check" and blocked every
move (awin verify-6, item 5). Root cause: the daemon adapter's hasPty only knows sessions
this app process spawned or reattached. A pane the relaunched window had not reopened is
still running in the daemon and listed by the runtime, but the inspection pre-check read
hasPty=false as terminal_gone, so the probe was unverifiable. Reproduced on macOS with a
two-launch e2e: an idle pane in an unopened workspace inspected as
{verdict:'unverifiable', reason:'terminal_gone'} after the relaunch. The same run showed the
runtime still holds the pane's original paneKey, so the paneKey theory is refuted.

The gate now passes includeUnattached, which skips that pre-check and asks the daemon,
which still refuses a session it no longer runs. Other callers keep the old behaviour.

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

* test(wsl-mover): an idle stub agent passes the move gate once its Stop arrives

awin verify-6 item 5d was inconclusive: p4's stub agent answered, but the preview stayed
"agent-working". Replaying the D1b-10 stub's hook payloads through the real hook server and
the gate's fact collection shows Stop settles the pane to done and the gate reads ready, so
the Stop path is not at fault. The run's screenshot (ev/p4-agent2.png) shows the likely cause:
the harness launched the stub twice in p4, and one instance sat at its prompt having fired
only SessionStart. The stub fires SessionStart at launch, which Orca's Codex normalizer treats
as a turn start, so a stub never given a prompt reads as working. No product change.

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

* test(wsl-mover): an unreachable daemon reads as unverifiable for the move gate

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

* test(wsl-mover): browserless host e2e and stale-window unit test for moved browser tabs and drafts

The distro's server has no browser of its own; an e2e-only switch (never in a packaged app)
starts the headless server the same way. With the desktop's per-host copy as a move leaves it,
the moved browser tab and pre-move draft show as tabs. The unload redo after a move rewritten
under an open window keeps both, whether the host published first or the rewrite seeded first.

* fix(wsl-mover): moved browser tabs and drafts survive the prepared reload after a move

"Reload now" backs up unsaved files by dispatching a synthetic unload first. That first unload
staged the window's mirror of the host (no browser tab, no draft) and drained the redo mark, so
the window, still open, saved its mirror again and the real reload had nothing to redo. The redo
now runs at every unload of the stale window and a last time when its document is replaced
(did-navigate), and only then is the mark drained.

* test(wsl-mover): the undo-path test's store answers the unsaved-file check

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

* fix(wsl-mover): read workspace records through the selectors

Main's new workspace-record read ratchet flags the mover's direct reads; the move dialog, the moved
projects section and its store helpers now read through workspace-record-selectors, and the scope
rows' catalog names its own fields. Prunes the layout-writer baseline to its actual count.

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

* test(wsl-mover): the dialog e2e expects the slept panes the start request now names

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

* fix(wsl-mover): a moved terminal's pane key comes from paneOfPty

Main's L80 census allows only paneOfPty and the switch-guarded writers to read a PTY record's pane
copies; the WSL move's live-terminal list now reads through it.

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

* style: format the imports the main merge repointed

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

* feat(wsl-mover): carry each moved pane's daemon checkpoint as a terminal-history blob the host restores once

Replaces the host-side stored-scrollback seed (#32789) with an installed interrupted session the pane's first start claims.

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

* feat(wsl-mover): the desktop sends each paused pane's checkpoint as its terminal-history blob

Drops the snapshot-ref spooling and the restore-hints blob for moved panes: one source (the pane's daemon checkpoint) carries history and folder.

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

* refactor(wsl-mover): register WSL move wiring from its own module

Keeps register-core-handlers under the max-lines limit.

* fix(wsl-mover): one import from ./ui in the move wiring

* refactor(wsl-mover): build the manifest's session before the payload

* style(wsl-mover): one import from stable-pane-id after the main merge

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

* test(wsl-mover): drop the e2e for the stored-snapshot seed this redesign deletes

The moved pane's history now comes from its own checkpoint; wsl-move-restore-lifecycle.spec.ts
covers it once, across reloads.

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

* chore(wsl-mover): keep #32668's two layout writers in the baseline until 2/3 deletes them

Main's layout-writer ratchet now counts main-process writers; wsl-move-echoed-browser-tabs.ts, the
file the hold-and-reload in 2/3 deletes, writes two. Restore redesign 2/3 removes the entry.

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

* fix(wsl-mover): move the project you have open into a host that has its own focus

The carried session named this desktop's focus, and a host that already focused another project
refused it (orcad_migration_dormant_focus_conflict:activeRepoId), so moving the project you had
open reset with "Nothing was moved" (awin verify-7 N1). The desktop's focus names nothing on the
host and the desktop rewrites its own focus after the move, so the export now carries none and
the host keeps its own. The reset result also says why the move stopped.

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

* fix(wsl-mover): startup removes the slept-history spools of moves the journal dropped

A run killed mid-pause is reset at the next startup without its process, so its export documents
(25 KB per pane on awin verify-7) stayed under wsl-move-slept-scrollback for good.

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

* fix(wsl-mover): a woken WSL pane keeps its folder unless the probe proves it gone

The cold restore checked a WSL pane's folder with stat over \\wsl.localhost, which waits for a
stopped distro and fails for reasons other than absence. After a rollback the pane stayed blank
about 15 s, then opened at the project root without its folder (awin verify-7, 5e). It now uses
the bounded wsl.exe probe every other restore uses, which keeps the folder unless proven missing.

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

* fix(wsl-mover): a moved pane's new shell prompt starts on its own line

The carried screen ends on the old shell's prompt with the cursor after it, so the host's new
shell printed its prompt beside it (`$ $`, awin verify-7). The exported checkpoint now ends the
normal screen with a line break; a TUI's screen and an unfinished escape are left as they are.

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

* test(wsl-mover): wait on conditions, a fixed clock and stated reasons instead of sleeps

Main's flaky-test ratchet flags the mover's tests:
- The browser-draft and pane-history e2e slept fixed seconds. They now wait on what they slept
  for: the host's tabs mirrored, the host terminal's last line, the moved tabs shown, the pane
  painted.
- The journal sweep test read the wall clock; it runs on a fixed fake time.
- Each longer per-test timeout says why it needs the time.

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

* refactor(wsl-mover): start the WSL mover from its own startup module

The mover's startup (rewrite stores, interrupted-move recovery, retention, retry) lived inline in
main-process-ready-foundation.ts. With main, the host reconnect and D1c branches each adding a
few lines on top, the file passed the max-lines limit in the combined build. It moves as one
unit to wsl-move-startup.ts; the foundation keeps the one call and its ordering.

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

* feat(wsl-mover): hold the window's saves from commit until main reloads it (restore redesign 2/3)

The window open across a move hydrated before the desktop rewrite, so every save it made
(session, UI, the unload checkpoint behind Reload now) wrote stale state back, and three files
re-applied the rewrite after it: echoed browser tabs, the seeded view state's redo and the
stale-window reapply. Now main holds that window's saves from the commit request until its
reloaded page has loaded, and reloads it itself once the rewrite is on disk. Use on this computer
again takes the same path, replacing its per-project release hold.

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

* fix(wsl-mover): only a settled commit reloads the window it held

A rewrite pass that ends while a commit request is out (another move's retry) must not reload
the window early: the reloaded page could save its view of projects the host is publishing.

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

* chore(wsl-mover): prune the deleted re-apply writers from the layout baseline

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

* fix(wsl-mover): the move's pause sleeps a workspace again before refusing

A workspace whose pane was still restoring, or had just spawned, could miss its final checkpoint
on the first Sleep and stay awake; the move then refused with "Orca could not pause the terminals"
although a second Sleep stopped it (awin verify-7, 6e). The pause now sleeps the failed workspaces
up to twice more, a second apart, and refuses only when they keep failing.

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

* fix(wsl-mover): a move retries a workspace whose pause failed instead of refusing at once

When one workspace of a move failed to pause, the move was refused right away with
host_terminal_stop_failed, and the retry added for awin verify-7 never ran (awin verify-8, 6e).
The window rolls a failed workspace's panes back and reports that rollback as an error. The
pause's persist step waited on every workspace and failed on that error, so the whole pause
call threw instead of returning the failed workspace. The retry only ran on a returned failure,
and the move's first pause had no retry at all.

The persist step now fails only on errors from workspaces main reported as slept; main already
reports the failed ones. Both of the move's pauses sleep a failed workspace up to twice more
before refusing. Each attempt is a fresh pause with its own id, so a re-sleep is not counted
as a duplicate.

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

* test(wsl-mover): e2e for the restart-style restore and undo (restore redesign 3/3, WIP)

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

* fix(wsl-mover): a moved pane's first subscriber gets its whole replayed history

The host replays a moved pane's checkpoint once, but every remote subscribe snapshot carried at
most 1000 rows, so the desktop's first view of a 2,000-line pane lost its first half. Only for
panes whose session is a moved one, the subscribe snapshot now tries 5000 rows first, then 2000,
then the usual ladder, within the same byte budget.

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

* test(wsl-mover): the restore lifecycle e2e waits for the printer instead of sleeping

Main's flaky-test ratchet flags it: it now waits until the host terminal shows the printer's
last line before the pane sleeps, and each longer timeout says why.

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

* test(wsl-mover): undo after reloads and a restart wakes split panes live, each with its history once

The undo e2e now follows a real move: main's own Sleep pauses a split tab, the committed move runs
its desktop rewrite and retention at startup, the window reloads three times and the app restarts,
then "Use on this computer again". The project comes back slept (no live terminal until opened),
and opening it wakes both panes from their checkpoints, history once, taking input.

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

* test(wsl-mover): carried and retained agents resume once across move, reloads and undo

The move e2e now carries the pane's idle Codex session to the host (its session file where the
distro keeps it): the agent resumes in the moved pane's folder on the first open and no reload
resumes it again. The undo e2e gives pane A an idle agent: after the undo it resumes there, once,
and pane B starts none.

Verify-6 item 5d (an idle agent read as working): the awin stub posts Codex's SessionStart at
launch, which Orca reads as a turn start; Codex posts it with the first prompt. The gate test now
pins that a resumed agent passes until its next prompt starts a turn.

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

* test(wsl-mover): read the undo e2e's agent record without a cast

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

* test(wsl-mover): parse the undo e2e's agent records instead of reflecting into them

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

* test(wsl-mover): the lifecycle e2e shows the moved pane when its agent does not start

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

* test(wsl-mover): start the carried agent by its recorded absolute command

A login shell may reset PATH (Ubuntu CI did), so the stub is named by the launch config the move
carries, as a recorded agent command is.

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

* test(wsl-mover): say where a resumed moved pane started when it is not its folder

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

* test(wsl-mover): print login profiles that move a shell when the resumed pane is outside its folder

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

* test(wsl-mover): keep the CI runner's login-profile cd out of the moved pane's folder check

Blacksmith's /etc/profile.d/blacksmith.sh ends with cd "$GITHUB_WORKSPACE", so every login
shell on CI, restored or new, plain or agent, starts there. Reproduced in Ubuntu 24.04 with the
same profile line: a plain restored pane lands in GITHUB_WORKSPACE too, and with it unset the
moved pane and its resumed agent start in the pane's folder. Orca spawns the shell in the right
folder; the runner's profile moves it.

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

* test(wsl-mover): judge the resumed pane's folder by where its shell started

Blacksmith's /etc/profile.d/blacksmith.sh sources the job env and cds into GITHUB_WORKSPACE, so on
CI every login shell, plain or agent, restored or new, moves there after Orca starts it in the
pane's folder (reproduced in Ubuntu 24.04 with that profile line; a plain restored pane moves too).
The check now accepts the pane's folder as PWD or as the OLDPWD a profile cd left behind.

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

* fix(wsl-mover): undo stops the host's copy of the project before its agents resume here

"Use on this computer again" resumed the project's agents here while the host's copies kept
running, so one agent session ran on both sides and two writers shared its transcript (awin
verify-7, 6c). Undo now sleeps the project's workspaces on the host and waits until none of their
terminals runs. A host that does not answer, or whose terminals do not exit, keeps the move and
says so; a host the user removed runs nothing, so the project comes back.

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

* test(wsl-mover): e2e: undo stops the host's running copy before the project comes back

A paired host holds the moved project under the move's ids with its terminal still running; after
"Use on this computer again" none of the host's terminals for it runs. Fails with the host stop
removed (the host's terminal stays at 1).

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

* fix(wsl-mover): "Use on this computer again" refuses only for the move's own unsaved files

An unsaved file in any project refused bringing a moved project back, so p6's draft README
blocked undoing p9 (awin verify-8). The window reload no longer stops on an unsaved file, and
only the move's own workspaces come back from their pre-move state. The window now tells main
which workspaces have unsaved files; main refuses only if the move holds one of them, before it
stops anything on the host, and the window names just those files.

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

* fix(wsl-mover): a refused "Use on this computer again" leaves the host's terminals running

Undo stopped the host's copies of the move's terminals before it checked whether the move could
be released, so an undo refused because a later move builds on it still killed the host's shells
and agents; the agents then resumed again (awin verify-9, X1). Undo now runs every refusal first:
unsaved files, a running move, a newer move in the chain, and whether the host answers for every
workspace. Only then does it stop anything, and a host that cannot answer for one workspace has
none of them stopped.

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

* fix(wsl-mover): the move gate finds a pane the window has not reopened since a relaunch

A pane the window has not shown since an app relaunch has no spawn-time pane key in main, so
the move gate's guest busy check could not find its shell. The gate now falls back to main's
own pane record. The Sleep half of the relaunch gap (awin verify-9, X2) moved to main with
#33398 and #33425.

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

* chore(wsl-mover): log who spawns each WSL pane and whether its cold restore came with it

After an undo reload a restored, inactive terminal tab showed only a prompt: something spawned
its session 2 s after the reload, before the window showed it, and its checkpoint never replayed
(awin verify-10). No log named the spawner. Each WSL pane spawn or reattach now logs its
session, spawn intent, whether it was new, whether a cold restore came with it, and the four
caller frames above the daemon client, for verify-11 to capture.

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

* test(wsl-mover): the undo host-copy e2e says why it needs its longer timeout

Main's flaky-test ratchet flags an unexplained per-test timeout.

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

* chore(wsl-mover): record the WSL spawn caller where it enters the daemon router

verify-11's `[wsl-spawn-origin]` lines named only the daemon client's own frames
(createOrAttachSpawn ← doSpawn ← withDaemonRetry): the async spawn path drops the caller.
The caller's frames are now captured synchronously at the router's `spawn` entry and logged
with the spawn, so verify-12 can name what starts a restored pane before its window shows it.

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

* chore(wsl-mover): capture the WSL spawn caller where every daemon spawn enters

verify-12 still logged "caller":[]. The router only wraps the daemon client when
legacy daemons exist; otherwise the bare client is the installed provider, so the
router capture never ran. Capture at the client's spawn entry (keyed by the
minted id too) and raise the stack limit so the async caller frames survive.

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

* chore(wsl-mover): log the seed and the daemon's screen for WSL cold restores

verify-13's second-cycle `$ $` checkpoint shows the daemon's own screen
without the fresh-shell blanking (old prompt still in the viewport,
scrollbackLines 0), which the macOS daemon harness never reproduces.
Log the seed's shape and tail with the spawn, and the daemon's screen
3 s after a WSL cold restore, so the next run shows where it is lost.

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

* fix(wsl-mover): keep an open host pane from restarting its agent during undo

"Use on this computer again" slept the host workspace and waited for its
terminals to go. A host pane open in the window saw its terminal exit and
resumed the agent on the host 1.6 s later, after the stop had already
counted zero, so the reloaded window resumed a second copy here (awin
verify-14, 6c). Refuse this window's terminal and agent starts on that host
from before the stop until the reloaded page has loaded (capped at 2 min),
and lift the fence when undo is refused.

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

* chore(wsl-mover): pre-landing cleanup for the dark landing

- Strip the verify-only spawn-origin and cold-restore-screen logging and the
  per-pane process warning.
- Read a pane's stored scrollback from its host only for WSL hosts; SSH and
  paired-server tabs no longer hold for a placeholder or ask their host.
- Limit the ambiguous-owner refusal to WSL moves; SSH conversions match as
  before.
- Drop the duplicate split-tab launch check (main fixed it in the projection,
  #32418).
- Reword comments that cited internal verify runs.

* fix(terminal): save a woken terminal's first history as a full snapshot

A wake that reused the Sleep-time cold-restore cache reopened the history
writer without asking for a full checkpoint, so the new process's output was
appended to the pre-wake checkpoint, which lacks the fresh-shell padding the
seed gave the daemon. The next save that replayed checkpoint + log (quit's
final save, or a relaunch's adoption) printed the new shell's prompt on the
restored one's row (`$ $`). Mark a full checkpoint for a new process in that
path, as the uncached cold-restore path already does.

* fix(terminal): keep ConPTY's viewport top through a width change, checked against a captured transcript

A captured ConPTY 1.23 resize (wsl.exe) shows it keeps the line at the top
of its viewport through every resize and repaints only that viewport. xterm
keeps the cursor's rows instead: a wider grid that rejoins wrapped rows pulls
rows out of scrollback (ConPTY's repaint then overwrites them), and a
narrower one that rewraps pushes rows into it (the repaint then repeats
them). The windowsPty row rule covered neither. ConPTY-backed emulators now
put the top line back on its row after a resize.

* feat(wsl-mover): D3a move orchestrator skeleton (dark) (#33802)

* feat(wsl-mover): D3a move orchestrator skeleton, dark behind the internal WSL hosts gate

* fix(wsl-mover): exhaustive orchestrator telemetry transition

* feat(wsl-mover): D3a T2 trigger, confirmed-running host readiness and T3 guard

* fix(wsl-mover): D3a review fixes (stuck moving, user release parks, A1 timer, deferred backoff, chain restore, GUID-only R38, uncounted window failures, backoff docs, restore settle, coalesced wakes)

* fix(wsl-mover): restore chain order without mutating

* fix(wsl-mover): D3a never touches a WSL host whose Remove is pending (E11 pendingStop)

* fix(wsl-mover): D3a survives an unreadable journal, honours a release mid-step, replays wakes after Restore, re-checks pendingStop before readying a host

* refactor(wsl-mover): orchestrator ports in their own module

* test(wsl-mover): warm the orchestrator wiring import before its first test

* fix(wsl-mover): a user Remove of a WSL host declines its move until the user adds it or retries

* test(wsl-mover): one checked cast in the host user-action test

* fix(wsl-mover): D3a review 3 — removal-pending clears, start re-checks after the window answers, non-blocking replies, replay in finally; split orchestrator modules under max-lines

* test(wsl-mover): index check style

* test(wsl-mover): core handler test mocks the move orchestrator wiring

* fix(wsl-mover): every user Remove of a WSL host declines its move; readying a host re-checks the decline before connect or add

* fix(wsl-mover): a failed sidecar save or step error never rejects a fire-and-forget trigger

* test(wsl-mover): split orchestrator tests under max-lines with shared fake ports

* fix(wsl-mover): report a host Remove before its cleanup, so a failed cleanup keeps the decline

* fix(wsl-mover): keep the moved-pane history wire surface off outside a WSL host

Register session.readTerminalScrollback, advertise its capability and the
terminal-history migration blob, and install that blob only on a WSL move's
destination (the WSL host's orcad, or an e2e host standing in for one).
Restoring a pane only probes for a moved pane's history there too. The
journal's orphaned-write sweep is limited to the WSL move journal, so SSH
migration journals behave as on main. Pin the advertised blob kinds again.

* fix(wsl-mover): user moves connect the host, automatic ones never boot; gate D3a's selector; tolerate a missing environment list

- The dialog's preview and start reach the host through service.connect (may boot, lifts a park);
  the orchestrator and its startup recovery keep the no-boot path (A1, E9). A host with a Remove
  pending is never connected either way.
- WslMoveDialogHost's active-distro selector does no worktree lookup with the flag off.
- hostScrollbackEnvironmentId returns null before the environment list loads; the mount test
  fixture supplies a WSL host.

* fix(wsl-mover): D3a waits out a move still being retained; orchestrator-over-driver tests (#34087)

* fix(wsl-mover): D3a waits out a move still being retained instead of failing attempts; orchestrator-over-driver integration tests

* fix(wsl-mover): D3a counts an unverifiable in-flight refusal, caps uncounted waits at a day (parks as stuck) and re-checks a done distro waiting on retention

* fix(wsl-mover): cap D3a's uncounted waits by checks, not wall clock; any in-flight detail counts

* fix(wsl-mover): a user's move leaves an already-ready host connected as is

service.connect bumps the liveness generation and re-ensures the server; the orchestrator readies
the host before the driver runs, so the driver connects only a host that is not ready.

* Revert "fix(terminal): keep ConPTY's viewport top through a width change, checked against a captured transcript"

This reverts commit ecd8f07396.

* test(wsl-mover): the orchestrator's automatic path never reconnects a serving host, and adds a bridge host once (E9)

* fix(wsl-mover): keep session.readTerminalScrollback in the RPC manifest; gate its registration

Gating the method out of ALL_RPC_METHODS dropped it from the generated params catalog. It is
static again, so the catalog types it, and the dispatcher leaves it unregistered on any host that
does not serve moved panes' history (method-not-found there, as before).

* fix(wsl-mover): undo returns focus from the host copy, so the reload does not reopen it

verify-15b 6c: undo stopped the host's copy of p9, but the window's focus still named the host
copy (the move pointed it there), so the reloaded window opened that workspace, the host
re-created its pane as a visible user-resume, and, since #34054 resumes a restored host pane's
agent from the hook store, Codex resumed on the host beside the one restored here. With writes
held and before the reload, undo now points focus at the project's copy here, reusing the move's
focus writer with the inverse patch.

* fix(wsl-mover): undo turns the host's copies of the move's automations off before releasing

After "Use on this computer again" the desktop's retained copy of a moved automation runs again,
while the host's copy stayed on: both ran against the same files. Before each move is released,
undo now turns off, on the host, that move's automations whose copy here is on (one off here keeps
the user's choice there). A host that cannot be reached refuses as host-unreachable and one that
refuses as failed, releasing nothing, like the terminal stop.

* fix(wsl-mover): undo turns host automations off before stopping terminals, and back on if it refuses

d3-gate-audit: with the automation step after the terminal stop, a refusal there left the host's
terminals and agents killed while undo refused, breaking X1. Per move, undo now turns the
automations off first (a step that undoes cleanly), then stops the terminals, then releases. A
terminal stop that fails turns back on what was turned off, and a disable that fails partway
turns back on what it had turned off before it throws.

* fix(wsl-mover): roll back undo's host automation step on any later refusal; review lows

- Any refusal after the automations went off (terminal stop, release failure or supersede) turns
  back on exactly what this attempt turned off. One it cannot is recorded on the move
  (hostAutomationsLeftOff), shown in the unfinished-move notice and retried by the rewrite retry.
- The spawn fence is released in a finally, and the port answers failed instead of throwing on an
  unreadable environment store.
- A host with a pending Remove (E11) is left alone with its own refusal copy; the
  host-automations refusal tells the user to connect the host.

* fix(wsl-mover): undo carries the host's automation states back and leaves each running in one place

Owner decision on d3a's item 5: undo reads every one of the move's automations on the host, turns
off each host copy that is on, and, with the release, gives the copy here the host's state (the
user's latest choice; one deleted there reads as off). An automation turned off on the host after
the move no longer comes back on here, and one turned on there no longer keeps running against a
project the host no longer owns. A refused undo restores the host's flags and changes nothing
here.

* fix(wsl-mover): the left-off automation retry re-reads the move after the host round trip

d3-gate-audit: the retry wrote back the snapshot it read before awaiting the host, so an undo that
released the move meanwhile could be undone, or a phase advance rolled back. It now re-reads the
record after the await, writes nothing for a move that is gone, and removes only the ids it
turned on, keeping everything else current.

* fix(wsl-mover): undo records the user's automation choice before turning any off, and takes turns with the retry

d3a re-review and d3-gate-audit:
- Undo reads every moved automation on the host, records those states on the move before turning
  anything off, and applies them here as part of the release step (undone if the release then
  refuses). An earlier attempt's record wins, so one left off by a failed rollback, or by a crash,
  keeps its choice (on) here instead of reading as off.
- A disable the host drops mid-loop carries what it could not turn back on on its error; those
  are recorded as left off, shown in the unfinished-move notice and retried.
- Undo's per-move steps and the left-off retry take turns, and the retry re-reads the move inside
  its turn, so a release during a retry neither resurrects the move nor leaves the host copy on.
- At startup, an undo a crash interrupted sends what it read as on back on through the retry.

* fix(wsl-mover): a left-off retry that finds the move released turns its host copies back off

If a move is released while the retry is turning a left-off automation back on, the host copy
would run for a project that is local again. After the round trip the retry re-reads the move;
when it is gone it turns what it just turned on back off (best effort, logged).

* fix(wsl-mover): a restart keeps a left-off automation's choice; undo's rollback never skips the host

d3a re-review:
- Startup settling keeps the recorded choice (on) for what it sends to the retry, and undo also
  reads any id on the left-off list as on, so a later undo after a restart and failed retries keeps
  it on here.
- A failed revert of the copies here no longer skips turning the host's copies back on; each
  revert write is tried on its own.
- Turning automations back on stops at a host that stops answering, leaving the rest as left off,
  instead of waiting on each in turn while undo's turn is held.

* fix(wsl-mover): undo always asks the host for the move's terminals, refusing a host that does not answer

verify-15c: with the host's orcad stopped, "Use on this computer again" for p7 proceeded, and after
a normal undo p6's host shell kept running. Undo's host-copy stop took the host workspaces to stop
from this computer's session, which after a move keeps only a moved project's chats: a project
with none named no workspace, so undo asked the host nothing (no reachability check, nothing
stopped) and released it. The stop now always lists the host's terminals first, adds every
workspace of the move's scope that runs one, and refuses as host-unreachable when the host does
not answer, answers with an error, or cuts the list short.

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-28 00:02:37 -07:00
Brennan Benson 8d908764dc Revert "Keep same-machine folders with matching Git remotes in separate projects (#32272)" (#34662)
* Revert "Keep same-machine folders with matching Git remotes in separate projects (#32272)"

This reverts commit 29a5f89b8a.

On a machine with several clones of one repo, #32272 moved the oldest clone
into the shared cross-host project. Shared Windows QA machines have several
orca clones, so everyone's sidebar "orca" project suddenly gained that
machine's main checkout and its worktrees. Revert until the intended
grouping is decided.

Conflicts resolved against later main: Settings files moved into
settings/repository (imports adjusted), and two test files later deleted on
main stay deleted.

* Use checked types in restored test fixtures

* Combine restored fixture type imports
2026-10-27 20:38:35 -07:00
Kelvin Amoaba 917ef9c737 feat(browser): install Chrome extensions from the Web Store into Orca's browser (#30940) 2026-10-27 22:04:12 +00:00
Neil f57d00f045 refactor: group shared native-chat upgrade-tip audience (#34509) 2026-10-27 13:17:14 -07:00
Sojiro.OandBrennan Benson 9802866ed5 fix(editor): Copy Remote URL links to the code the workspace is on, with the selected line range (#33597)
* fix(editor): Copy Remote URL links to the code the workspace is on, with the selected line range

The gutter menu's Copy Remote URL always built the link on the default
branch and dropped any multi-line selection. Prefer the HEAD commit when
origin already has it, then the origin branch the current branch tracks,
then the default branch; carry an optional end line through IPC, RPC and
the git provider so a selection becomes a line range.

* fix: pair remote file links with published repository refs

* fix: prepare exact remote file fallback before publication scan

* test: consolidate remote file URL integration coverage

* test: document real-clock cancellation deadline proof

* test: handle optional cancellation signal in assertion

* test: reuse spawned child fixture across runtimes

---------

Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
2026-10-27 13:00:23 -07:00
Seiji KoharaandNeil 2327180d12 feat(quit): add Quit and Stop All Sessions to the app menu and tray (#32153)
* feat(quit): add Quit and Stop All Sessions to the app menu and tray

Quit keeps daemon sessions alive for warm reattach. The new item, placed
directly above Quit in the macOS app menu and menu bar icon, the Windows
File menu and tray, and the Linux File menu, asks for confirmation, runs
Settings' Kill all sessions in the renderer, and quits only when every
session stopped. Closing the tabs first means nothing reopens or resumes
on the next launch. Serve hosts do not offer the item.

* fix(quit): keep Orca open when Kill all cannot prove every session stopped

- Kill all no longer reads a daemon generation that failed to list as
  "no sessions": a failed poll keeps the last confirmed remaining set,
  and a failed initial listing sets listingIncomplete.
- Quit and Stop All Sessions stays open on an incomplete listing or a
  terminal tab opened during the sweep, logs why it stayed open, and
  reveals the window before the final quit.
- Share the main-window message box helper, stub the request on the web
  client, and add Portuguese strings.
- Cover the windowless path, request expiry, the in-flight guard, and
  failed listings in tests; bound the E2E exit wait.

* fix(terminal): count Kill all per daemon generation within a time window

- A poll updates only the daemon generations that answered, so one
  failing generation no longer hides the kills of the others.
- Poll for 6.5 s of wall-clock time instead of 65 attempts, because a
  wedged generation fails each listing only at the request timeout.
- Flag a sweep with no daemon provider as listingIncomplete.
- Log the leftover-tab exit, share the one-shot window intent with
  Open Settings, and cover the sender check, the reveal before the final
  quit, and retries in tests.

* fix(quit): stay open while a newer build's daemon is held

- Set listingIncomplete while this build holds a newer protocol's
  daemon, whose sessions it can neither list nor stop.
- Reuse getDaemonAdapters in Kill all, type the shared one-shot helper
  by its flag factory, and move the app-menu quit handlers next to the
  tray options so both serve/desktop splits are tested.
- Cover a quit that outlasts the request expiry and a failed request
  pull, and fix the docs lead-in for the new quit-time case.

* fix(quit): explain, without stopping anything, while a newer version runs terminals

- Check the newer-version daemon hold before the confirmation and show
  an explanation instead, so Kill all no longer closes every tab and
  then refuses. listingIncomplete stays as the backstop.
- Translate the explanation, describe listingIncomplete generically,
  and assert the single timed-out poll in the Kill all test.

* fix(quit): explain before asking whenever Kill all cannot list every session

- Show the explanation instead of the confirmation while the daemon
  provider is missing (still starting or failed) as well as while a
  newer version's daemon is held.
- Name only the steps that work (wait for terminals to open, reopen a
  terminal opened in a newer version), avoid stating a hold as fact, and
  label the dialog's button through the catalog.

* fix(quit): add the relaunch step to the explanation and align the ja wording

* test(pty): keep a confirmed stop when its generation fails a later poll

* test(quit): give the E2E timeout bump its reason

* fix(quit): stay open when the daemon still lists a session after Kill all

Kill all counts only the sessions of its first listing, so a session
started since then, for example from the CLI or an automation before its
tab reaches the renderer, would outlive the quit. Main now lists every
daemon generation again before quitting, and quits only after a listing
in which every generation answers and none lists a session. It polls
within Kill all's 6.5 s window so the tab sweep's PTY shutdowns can
finish; otherwise the renderer shows the existing error and Orca stays
open.

ui:quitAfterStoppingAllSessions becomes an invoke that answers whether
main quits.

* feat(quit): rename to Quit Completely with a macOS hover hint, and explain plain Quit in the confirmation

* fix(quit): show progress while Quit Completely runs, and refuse at once when the daemon can't be fully listed

---------

Co-authored-by: Neil <neil@stably.ai>
2026-10-27 03:52:06 -07:00
a7c9d19048 fix(file-explorer): offer permanent delete when moving to Trash fails (#33956)
Offer confirmed permanent deletion when moving an item to Trash fails.
Preserve authorization and workspace ownership checks, cancellation behavior,
text-snapshot undo, remote compatibility, and all supported translations.

Fixes #33944

Co-authored-by: nori <dosukoicrypto@gmail.com>
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-10-26 22:25:01 -07:00
Neil 362104ca18 fix(wsl): background Git and the roots catalog never start a stopped WSL distro (E9) (#34231)
* fix(wsl): background Git and the roots catalog never start a stopped WSL distro

git status ran on the UNC project root 0.45 s after a terminate, next to the
explorer read; either boots the distro. Background Git (status, conflict
operation, upstream status, branch compare, the upstream-ref watch) now asks the
shared running gate first and refuses for a stopped distro, including host
paths whose Git runs through wsl.exe. The authorized-roots rebuild skips a
stopped distro's repo and leaves it dirty, and the detected-worktree scan fails
as unavailable, so rows are kept and the next scan after the distro runs lists it.

* fix(wsl): the active workspace's first status and a wake read's roots rebuild may start WSL

- Drive-path projects whose Git runs through WSL had no wake path: the active
  workspace's first git:status after it becomes active now passes
  wakeWslDistro, so Source Control starts the distro; later polls stay gated.
- A user act runs in an async scope naming its distro; gated work inside it
  (the authorized-roots rebuild an authorization miss triggers, the detected
  scan, the Git) may start that distro and no other. External worktrees on a
  stopped distro no longer fail a wake read with access denied.
- After a user act wakes a distro, background reads stop trusting the older
  'stopped' read and probe again.

* fix(wsl): explicit WSL launches refresh the running read; wake only on tab change; gate the home probe

- Any wsl.exe Git/CLI run (commit, push, fetch) or WSL terminal spawn for a
  distro last read as stopped records that it started, so background reads probe
  again instead of refusing for up to 60 s.
- The active editor tab wakes only when the active tab changes; effect re-runs
  driven by git status or overview views read without waking.
- The authorization roots' WSL home probe no longer runs wsl.exe in a stopped
  distro when the home is uncached.
- The settle window no longer imports the orcad liveness module (it pulled main
  code into the web typecheck); a test pins it to EXIT_SETTLE_MS.

* fix(wsl): deferred work and running-list listeners never inherit a user act

A user act's async scope could leak into work it triggers indirectly: a drain of
deferred tasks it caused, or listeners of a running-list probe started inside
it. Both now run outside the scope, so only the act's own work may start its
distro.

* fix(wsl): keep shared modules free of main-only gate imports

worktree-logic.ts and wsl-running-distro-cache.ts are also compiled by the web
project, which cannot see main-only modules (AsyncLocalStorage, wsl.exe).
getAllowedRoots now injects the gated home lookup into computeWorkspaceRootWithin,
and the background gate's own cache subscriptions run outside the user-act scope
instead of the cache knowing about it.
2026-10-26 22:23:07 -07:00
Neil fe82078909 fix(wsl): background file reads and watches never start a stopped WSL distro (E9) (#34222)
* fix(wsl): background file reads and watches never start a stopped WSL distro

Any Windows touch of a \\wsl.localhost path boots a stopped distro. After a
terminate, the dying explorer watcher's overflow refresh ran realpath on the
project root 0.23 s later and booted the distro again. Background reads, stats
and watch installs now ask a fresh running list first (shared, with a 5 s lease
and a stop signal from the watcher exit and the hook relay link) and refuse when
the distro is stopped. Opening the workspace and Retry still start it. A stopped
watch waits for the distro and resyncs the explorer when it runs again.

* fix(wsl): trust a confirmed running read for 60 s without a stop signal

* fix(wsl): user acts wake; unverifiable is not stopped; settle after a stop

- Opening a workspace, expanding a folder, opening a path (explorer, quick-open,
  links), the active editor tab's load and every Retry pass wakeWslDistro on
  readDir, readFile, stat and readFileChunk. Background reloads, restored-tab
  scans and watchers stay gated.
- A failed running-list read is 'unverifiable', not 'stopped': the gate re-reads
  once past the failure backoff (once per failure window) and otherwise refuses
  with 'WSL didn't answer', never touching the path.
- After any stop signal, a running read that started within EXIT_SETTLE_MS of it
  never counts, in isWslDistroRunning itself, so neither the watcher's own
  overflow refresh nor the hook relay's restart check can read a stale listing.
- A confirmed read that sees a distro stop records a stop signal, resetting the
  60 s lease.

* test: user-opened stats now carry the WSL wake flag
2026-10-26 21:17:07 -07:00
NeilandClaude 284411d893 fix(wsl-host): uninstall and startup release WSL holders; a partial Remove keeps the host pending (#33843)
* fix(wsl-host): uninstall and startup release the WSL distro holders Orca left running

The detached wsl.exe holder outlives the app, so an uninstall left its distro,
orcad and shells running. The uninstaller now runs Orca with
--orca-uninstall-release-wsl-holders after killing the app and its daemon; it
ends only the holders this profile recorded, each verified by pid, start time
and command line, and keeps any it cannot verify.

At startup the WSL host service now releases holders whose distro is no longer
a host (a failed add or remove), and every holder while WSL hosts are off; a
closed gate releases them too.

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

* fix(wsl-host): a Remove that can't prove the server stopped keeps the host, pending

A partial Remove dropped the host anyway, leaving a server whose live
terminals block the next add's activation. Remove now drops the host only on a
proven stop; otherwise it records a pending stop on the WSL record, keeps the
host listed and says why (live or unverifiable). Remove again retries without
ever booting a stopped distro (it says to start it); Connect or Add on the same
distro keeps the host; a distro that is no longer registered finishes. Nothing
reconnects a pending host on its own.

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

* fix(wsl-host): E11 review — keep holders when records are unreadable; finish removals a stopped or renamed distro proves

- Startup keeps every holder when the WSL host records can't all be read
  (unverifiable), and no longer re-adopts a pending host's holder.
- Remove matches a renamed distro by GUID (isWslDistroRegistered), and a
  stopped distro finishes a pending Remove: the cold probe proves the stop.
- Only the user's Connect or Add withdraws a pending Remove; other connects
  are refused, and re-adding respects startIfStopped.
- Settings' Remove carries the kept result, so it warns instead of 'Removed'.
- A holder record without a start time is never killed on its command line
  alone, except the one this process just spawned.

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

* fix(wsl-host): E11 review 2 — a closed gate keeps live hosts' holders; stop a renamed distro by its current name

- With the gate closed, a launch releases only orphan holders (no host record,
  or a pending Remove), under the same keep rule as startup; a user with no
  holder records costs one folder check, nothing else.
- The keep rule re-reads host records per holder and keeps an Add in flight.
- Remove resolves the distro's current name from its GUID and stops it there.
- The uninstall command arms its exit timer first and exits 1 if setup throws;
  NSIS launches it only when a userData holder folder exists.

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

* fix(wsl-host): never boot a host whose Remove is pending at the next launch

Early wake built its host list from every record, so a host focused when its
Remove ended pending was booted at each launch, undoing the stop's proof. It
now skips pending records, and Remove forgets the host's early wake whatever
the outcome.

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

* fix(wsl-host): no wake for a pending host; the e2e fake stops an unbridged server like the real one

- mayWake skips a host whose Remove is pending, so a due automation can't
  boot it (focus already couldn't).
- The e2e fake connector read a host with no bridge as stopped. Since E9 a
  re-enable connects nothing, so Remove after off/on left the host's shells
  running. Like the real connector, it now reconnects the always-running fake
  distro to stop its server.
- Status bookkeeping moves to WslHostStatusBoard, and the pending-host connect
  rule to admitConnectToPendingHost, to keep the service under max-lines.

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-26 20:17:53 -07:00
Brennan Benson 29a5f89b8a Keep same-machine folders with matching Git remotes in separate projects (#32272)
* WIP: same-host same-remote checkouts keep their own project id

* WIP: claim the shared project id per checkout surface; drop the display-only clone split

* WIP: index derived ids by repo in the hydration normalizer

* Resolve workspace project affiliation from current checkout owners

* fix: preserve concrete project ownership across catalogs and operations

* fix: combine recent project selector imports

* fix: enforce canonical catalog reads and preserve search continuity

* fix: scope catalog lint exceptions to renderer files

* Resolve project search ownership before native candidate limits

* Combine duplicate session scope imports

* fix: derive returned history row ownership on the host

* Reconcile full session history replies by content

* Require authoritative project catalogs for terminal snapshots

* Preserve registered project claims and partial history results

* test: check orphaned setup cleanup state without assertions

* fix: align catalog callers and native search fixture contracts

* test: check full repo shapes in runtime routing fixtures

* Fix checked catalog inputs in compiler regression fixtures

* Keep checked PTY fixture repos mutable

* test: align catalog fixtures with canonical readers

* test: check catalog and runtime fixture inputs

* test: check catalog fixtures without outer assertions

* test: follow persistence harness relocation

* Adapt catalog test inputs to integrated main checks

* Test History hashing across cached depth revalidation

* test(settings): expect host-qualified setup removal

* test: capture host split listings and scope relay recovery

* ci: regenerate merge ref

* test: type host-split listing diagnostics
2026-10-26 19:07:06 -07:00
Brennan Benson 6e3c10c835 Honor browser preferences and modifiers for HTML preview links (#33622)
* fix(browser): honor preview link destinations and modifiers

* Clarify the source browser provider contract

* test(browser): check preview guest before reporting modifiers

* fix(browser): report system link failures and cover preview routes

* test(browser): combine HTTP link destination imports

* refactor(browser): require source ownership for preview links

* test(browser): type the forbidden Active Server getter

* fix(i18n): retain required preview error catalog entry
2026-10-26 18:43:50 -07:00
NeilandClaude 2e67ba5a64 perf(wsl-host): start booting only the focused WSL host early at launch (E9, replaces #33141) (#33924)
* perf(wsl-host): start booting the distros the last run had open work on as the app launches

The WSL host connect can only start once the store has loaded, about 1.5-2 s into
startup on a slow PC, and its first command then waits for the distro to boot.
The service now remembers which distros had open work, and the next launch starts
booting just those right after taking the single-instance lock.

* perf(wsl-host): wake only the focused WSL host early at launch (replaces #33141's list)

open work on, which conflicts with E9 (a launch boots only the focused host). Each
focus entry now records just that host's distro for the next launch's early boot,
and focusing a workspace off WSL (reported as a null focus entry), turning WSL hosts
off, or a host that can't run clears it.

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

* fix(wsl-host): the early-wake list is authoritative: deleted when off, forgotten on Remove

Review of #33924: the early wake runs before settings load, so it can't read the WSL
hosts setting. Turning WSL hosts off now deletes the list synchronously (emptying it
if the delete fails; a missing or unreadable list wakes nothing), and removing a host
drops it from the list. There is no pendingStop field on host records yet (E11); the
existing registered-host filter covers removed records until then.

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-26 14:35:11 -07:00
NeilandClaude 2dd8207369 fix(wsl-host): boot only the focused WSL host, and keep a stopped one down (E9 A) (#33838)
* fix(wsl-host): boot only the focused WSL host, and keep a stopped one down

E9 connect policy, part 1. A stopped distro now boots only for an explicit user action
or when its host becomes focused:
- Visible work is the active workspace plus workspaces focused earlier in this window
  that still have tabs. Saved tabs alone no longer count, so a launch with two hosts
  boots only the focused one.
- After a confirmed stop (wsl --shutdown, WSL update) the host is parked. Liveness no
  longer reboots it even when it is visible, and polls get "<distro> was stopped"
  instead of a boot. Re-focusing the host, Connect, or the distro running again lifts it.
- Liveness recovery always needs a confirmed-running distro, so a distro that stops
  mid-recovery is never booted again.

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

* test(wsl-host): launch boots one of two hosts; polls never boot; lost contact never parks

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

* fix(wsl-host): only a focus entry or a user action boots a WSL host (E9 review)

Review of #33838:
- Re-focus never lifted a park: a host focused earlier stayed in the visible set, and
  the renderer skips unchanged reports. The renderer now sends a separate focus entry
  (focusHost) each time the user switches to a workspace on a WSL host, including the
  launch restore; only that, or Connect, lifts a park and may boot.
- Runtime calls could boot: ensureForRuntimeCall connected any visible, unparked host.
  It now reads the host's reachability first: a confirmed-stopped distro parks at once,
  an unreadable running list refuses, and only a distro confirmed running is
  reconnected (for visible work). Visible work itself never connects anything.
- Turning WSL hosts on no longer boots every visible host, and focus isn't recorded
  while they're off; the window re-reports its focused host instead.
- Each window's visible work is kept apart and unioned, so one window can't drop
  another's.
- wakeForAutomation/isConnected move to the automation PR, which uses them.

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

* fix(wsl-host): a poll waits for liveness's verdict right after an exit (E9 re-review)

Right after `wsl --shutdown` WSL can still list the stopping distro as running (the
supervisor waits and re-reads for exactly this). A poll's cold probe then read
'unreachable', reconnected, superseded the pending distro-stopped verdict and booted
the distro again. The supervisor now reports when it is settling (a verdict in
progress, or a watch/holder exit within the last few seconds), and a runtime call
refuses instead of reconnecting then; the verdict parks the host as before.

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

* test(wsl-host): a new supervised entry starts with no recorded exit

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-26 12:52:16 -07:00
den4ikandNeil 0afedab10d fix(lifecycle): keep UI listener failures from vetoing the shutdown checkpoint (#21029)
A throwing ui-changed listener (e.g. webContents.send on a destroyed
secondary window) propagated out of store.updateUI() during
app:stage-before-unload-sync and cancelled the shutdown checkpoint,
leaving the user unable to quit. notifyUIChanged now isolates each
listener, the ui:stateChanged broadcast skips destroyed webContents, and
the staging failure cause is returned to preload so the error toast
names it.

Fixes #20897

Co-authored-by: Neil <neil@stably.ai>
2026-10-26 09:15:45 -07:00
Neil a93f73c032 fix(terminal): skip an escape tail an abandoned hidden-output restore would print (#33905)
* fix(terminal): skip an escape tail an abandoned hidden-output restore would print (#33766)

Main drops a hidden pane's renderer delivery after its model ingests the
bytes, so the first chunk sent on reveal can begin inside an escape whose
start was dropped. When the restore is abandoned, the renderer resets its
parser and writes the queued chunks, and xterm printed the tail (';242m',
the rest of a Codex OSC title).

Main tracks each PTY's escape state over every accepted payload and labels
the first delivery after a hidden or pending-cap drop with the escape it
resumes inside (gapEscapeState). Only the abandon path strips that
continuation; a successful restore keeps every byte so the snapshot's
pending escape tail can complete. No label means no change.

* fix(terminal): make the escape-continuation scan's switch exhaustive (#33766)
2026-10-26 06:33:23 -07:00
NeilandTian-yi Liang c9eee6b54c fix(runtime-environments): every window sees a server saved from the CLI (#33930)
`orca environment add` writes the saved-server file directly. Main already
watched that file but only repaired the Active Server preference, so every
renderer surface listing saved servers (the Agent Session History host
picker, host menus, the status bar) kept the old list until something
else refreshed it. Main now tells every window when the file changes, and
the renderer refreshes through the existing coalesced catalog refresher.

Co-authored-by: Tian-yi Liang <tianyilt@qq.com>
2026-10-26 06:00:15 -07:00
Jinwoo Hong a66f6bf011 fix(agent-resume): keep a background agent launch's flags when it is resumed (#33776)
* fix(agent-resume): record a background agent launch's recipe so resume keeps its flags

* test: narrow the reveal's notifier so the test needs no cast

* refactor(agent-resume): one recipe registration path; skip adopted panes

* test: add the launch-recipe listener to the remaining window mocks
2026-10-26 01:49:54 -04:00
Brennan Benson bfc633af8f Offer to resume chats after managed server updates (#28978)
* Guard managed server restarts with host work reservations

* Protect legacy server update requests with host admission

* Reserve chat-safe restarts in SSH and WSL server updates

* Keep activation host fixtures within the module line limit

* fix(orcad): preserve session queue timing during admission

* Explain chat-safe update deferrals and superseded restarts

* Clarify managed server Force upgrades and retry instructions

* Separate chat entry delegates from conversation creation

* Report child work through main's child evidence in the restart drain test

* Drive main's snapshot-room tests through the restart reservation

Main's room tests deployed over a serving incumbent with a census that has no
chat work count and no restart admission, so the update deferred before the
room probe ran. Give them the same idle census and slot admission as the other
activation tests, and pin the probe before the reservation instead of before the
recount it replaced.

* fix(native-chat): an update keeps waiting on a chat start after its conversation is closed

The restart check counted a start only while its conversation stayed open,
so an idle close during a stalled start let a managed update through.
Every tracked attach, create, start and open now counts as restart work
in the same tracker that counts the session queue.

* fix(native-chat): keep the structured session view within its line limit once formatted

Main's copy sat at 400 lines only because one line was left unformatted; the
merge's formatter pushed it to 401. Inlining the one-use question list keeps it
at 400 with identical behavior.

* fix(orcad): a recovery stop request stops the server; only an older client's empty request waits for idle

Stopping a slot and restarting it for an older client's update were both an empty
.orcad-stop-request file, so routing that file through restart admission let a
crash recovery, stranded-setup release or rejected-candidate rollback leave the
server running. New clients now write "stop-now" into the request (renamed in
whole), which stops at once as before; an empty or unrecognized request takes the
chat-safe restart, and a deferred restart logs why. Also gives the restart
admission activation fixtures the now-required recount.

* docs(orcad): say a directory at the stop-request path is refused, not restarted

* docs(orcad): the plain stop request's comment states both of its meanings

* style: format the restart admission files for the format check

* test(wsl): count the incumbent's recount bridge after the reservation's

* fix(orcad): a Windows stop request retries its rename while antivirus holds the new file

Renaming the just-written stop-now request can briefly fail with EPERM, EACCES
or EBUSY on Windows; the stop now retries those with the same bounded delays as
record-publish instead of answering SIGNAL_FAILED and leaving the server up.

* test(orcad): a real Windows host stops on the slot's stop-now request and relaunches

* test(orcad): recovery stops a real server whose restart admission refuses

* test(orcad): the recovery-stop cell tunnels over the cell's SSH connection

* test(orcad): the recovery-stop cell names itself in the CI log

* fix(settings): a deferred WSL update shows its full reason below the row

* test(orcad): fixtures pass releaseIncumbentAdmission; the census test calls its own method

* test(orcad): the capable-server census in the fence test reports idle chat work

* Record server chat restart offers and desktop creation history

* Offer interrupted chat recovery per managed runtime

* Ship the remote chat resume action labels in the runtime catalog

* Update removal contracts for desktop chat history cleanup

* Scope remote chat recovery to each server occurrence

* Preserve recovery choices and uncertain continuation outcomes

* fix: preserve uncertain restart action outcomes

* Fix restart recovery CI imports and fixtures

* test: align resume assertions with unconfirmed outcomes

* test: bound restart recovery work across scaled populations

* fix(native-chat): title the remote resume dialog with the restart

The dialog can appear out of nowhere when another desktop updates a shared
server, so its title now says what happened: "<machine> restarted".
2026-10-25 17:15:55 -07:00
Kelvin Amoaba 1b0e56e385 feat(browser): make Orca shortcuts, split focus and mouse back/forward work inside pages (#31859) 2026-10-25 22:57:53 +00:00
Kelvin Amoaba eb499478da feat(browser): show agent browsing live, with a jump to the page from chat (#33075) 2026-10-25 22:15:00 +00:00
Brennan Benson 883b768bc7 A chat never starts writing a conversation a terminal is still writing, and restored panes respect open chats (#30930)
* fix(native-chat): one ownership index with link roles, and a chat refuses to start while a terminal writes its conversation

Each chat link now carries its role (head or earlier) and key; Session History names
the chat continuing a conversation first, duplicate adoption checks only head links in
both the pre-commit check and the store transaction, and terminal resumes stay refused
exactly as before. The terminal-writer check now also runs inside acquireOwner before
the spawn, refusing with the typed pre-spawn reason conversationOpenInTerminal.

* fix(native-chat): hold a terminal start Orca issues until its terminal reports it

An in-memory registry of managed terminal starts per conversation, registered in the
same step as the resume admission (terminal.send with a resume command, createTerminal
with a command), bound to the PTY execution that runs it, and ended only when that
terminal's hooks report the conversation or the execution is gone. A chat's agent
start refuses while an entry may hold its conversation.

* fix(terminal): an automatic pane restore never resumes a conversation an open chat continues

Resume recipes built from a pane's saved facts (cold restore, deferred and late reattach,
sleeping wake, host mobile materialization) carry resumeOrigin 'saved'. The execution host
decides before any command is issued: while an open chat continues the conversation, or a
chat may be writing it, the pane starts its plain shell with the started-fresh notice and
its saved claim is forgotten; with unreadable records only a saved restore is refused and
its record kept. An admitted resume registers its managed start in the same step, so the
chat's agent start refuses until the terminal reports it or exits. Clearing the last saved
record now reaches disk.

* Fix saved terminal resume ownership and effective launch results

* Preserve conversation case and clear refused cold restore identity

* fix: publish effective terminal launch results on attachment

* fix: preserve terminal launch callback and candidate ownership

* fix: keep existing terminal attachments observational

* fix: consolidate terminal launch imports

* test: document snapshot runtime fixture boundary

* fix: preserve terminal launch identity types

* fix: accept reattach payload policy sessions

* fix: preserve terminal result validation contracts

* refactor: separate terminal create contract

* test: document partial terminal runtime fixtures

* test: align sleeping resume store fixture

* Fix terminal create type contracts

* fix: return the terminal main started to its waiting view

* fix: register reattached terminals before publishing their handles

* Check unavailable-resume test fixture types

* Complete sidebar resume test store fixture

* test: bound saved resume ownership and refusal work

* Settle refused saved resume claims from actual launch results

* Fix saved resume test type contracts

* test(e2e): capture exact terminal parking diagnostics

* test: verify parked terminal with fresh host inventory

* Consolidate agent session imports in remote terminal transport

* Clarify terminal conversation refusal guidance

* Admit note-only saved conversations without hiding live writers

* Retain terminal writer proofs through final admission
2026-10-25 14:39:05 -07:00
Neil 82a3214049 fix(sleep): reopen a slept agent whose tab is gone in a fresh tab instead of stranding it (#33598)
A slept agent whose tab disappeared now resumes in a fresh tab (with --resume, through the existing duplicate/host guards) when its workspace opens, and main's stranded workspace-sleep record is then dropped (only if no tab still lists that pane), instead of the record sitting forever while the workspace opens a bare shell.

Fixes #22657
2026-10-25 11:00:28 -07:00
Kelvin Amoaba 9485e73207 feat(browser): dock DevTools in the browser pane, with dock side, resizing and pop-out (#33003) 2026-10-25 16:41:56 +00:00
Neil 0b28553093 feat(ssh): show saved port forwards that couldn't start, with why, and let you remove them (#33450)
* feat(ssh): list saved port forwards that couldn't start, with why, and let you remove them (#32484)

A saved forward whose local port is taken, or that keeps failing, stayed
saved but invisible, retried in the background with no way to remove it.
The port-forward lists now show each saved forward that isn't open, with
the last start error, and Remove deletes it by its mapping.

* fix(ssh): a forward whose ssh exits after starting backs off like a failed start

* fix(ssh): re-register the new forward channels, close a forward a restore reopened before removing it, and update IPC fixtures

* fix(ssh): a flapping forward keeps its backoff streak until it stays up

* fix(ssh): translate the saved-forward row, say Stopped for a forward that died, toast a failed remove, and reset backoff on manual edits

* fix(ssh): keep saved forwards listed and removable while the host is offline; label the remove button; test the healthy-uptime reset

* fix(ssh): mirror saved forwards from main with a stale-read guard, including hosts that never connect

* fix(ssh): scroll the offline ports panel when saved forwards overflow it

* test(e2e): give the saved-forward restart case's timeout its reason
2026-10-25 02:37:30 -07:00
NeilandClaude Opus 5.5 8e9f86b119 feat(managed-server): Update and Update anyway for a managed SSH server, keeping its running terminals (#33397)
* feat(managed-server): offer Update and Update anyway on a managed SSH server's row

A managed SSH server only updated on connect, and every connect deferred while
a terminal ran there. Its row now offers Update when this desktop bundles a
newer build than the Orca that runs the host. When running terminals defer it,
Update anyway restarts the server past them; the terminal daemon carries them
across the restart.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(managed-server): Update anyway passes only counted live terminals and keeps Update's version admission

Update anyway now sends the incumbent build it confirmed over; under the host
fence it is refused if another desktop activated a build since, keeps the
newer-host downgrade refusal, and never passes an uncounted terminal census.
The offer also covers hosts activated before the Orca version was recorded,
for the build this desktop already deferred there.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(managed-server): Update anyway also confirms a menu Update the row does not advertise

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(managed-server): the row shows one confirmation at a time

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* ci(e2e): route managed-server update sources to the Update anyway Docker spec

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-24 22:49:18 -07:00
Kelvin Amoaba 25b9ce5168 feat(browser): show a hovered or focused link's URL in a bubble at the bottom of the page (#32978) 2026-10-25 03:09:30 +00:00
Kelvin Amoaba 76f0f35bad feat(browser): speaker icon on tabs playing sound, with tab mute (#32981) 2026-10-25 02:51:37 +00:00
Neil b900d9843f refactor(shared): colocate Jira rules and direct imports (#33164) 2026-10-24 06:46:07 -07:00
Kelvin Amoaba 785daec1e1 fix(browser): show profiles changed outside the window in the profile menu without a reload (#32723) 2026-10-24 05:06:34 +00:00
Kelvin Amoaba 5dc197e5ec feat(browser): history controls: Chrome-style Escape, removable suggestions, history view (#31846) 2026-10-24 04:46:47 +00:00
NeilandJesse Sanford 7cafdb072b feat(ssh): Recover server for a managed host refused on a stale lock, and surface that refusal in seconds (#32951)
* feat(orcad): run --reclaim-stale-lock on a managed host's active slot

The desktop can now dry-run and confirm the stale instance-lock recovery
on a Linux managed host, serialized with launches, then start the server
again and reconnect the way the restart action does.

Co-authored-by: Jesse Sanford <108698+jessesanford@users.noreply.github.com>

* feat(settings): offer Recover server for a managed host refused on a stale lock

When a managed orcad start names --reclaim-stale-lock, the SSH host row
offers Recover server. The dialog dry-runs the check, shows the host's
evidence, and clears the lock only after the user confirms no other
machine sharing the home runs Orca.

Co-authored-by: Jesse Sanford <108698+jessesanford@users.noreply.github.com>

* feat(mobile): point a host refused on a stale lock to the desktop's Recover server

Co-authored-by: Jesse Sanford <108698+jessesanford@users.noreply.github.com>

* fix(settings): mount the Recover server dialog per opening

A fresh mount starts each opening from a new check instead of resetting
state in an effect.

Co-authored-by: Jesse Sanford <108698+jessesanford@users.noreply.github.com>

* fix(orcad): end the readiness wait once the host proves the launch exited

A managed orcad refused at start (such as on a possibly stale instance
lock) left the client waiting out the full readiness deadline. Each
host-side wait now also checks the launch's PID file with kill -0 and
ends early on a proven exit, so the caller reports the same pending
readiness, with the orcad.log tail, within a poll. No PID, a denied
probe or a reused PID keeps the old deadline.

Co-authored-by: Jesse Sanford <108698+jessesanford@users.noreply.github.com>

* fix(orcad): refuse a managed slot without --reclaim-stale-lock before running it

An older orcad ignores the flag and starts serving in the foreground. The
same remote exec now checks that the slot's server bundle names the flag
and answers unsupported otherwise, without invoking orcad.

Co-authored-by: Jesse Sanford <108698+jessesanford@users.noreply.github.com>

* fix(settings): show the manual stale-lock command as literal mono text

The rendered dialog ran the command into the sentence, so it was hard to
read or copy. It now sits in its own mono block, like Details.

Co-authored-by: Jesse Sanford <108698+jessesanford@users.noreply.github.com>

* fix(settings): offer only a manual delete when the host can't reclaim, and format the lock's age

An unsupported host was told to run the very command it lacks. Main now
names the lock path for that case, and the dialog shows only a copyable
rm of it with the shared-home caution. The lock's age uses the UI's
relative-time formatter instead of a hand-rolled minutes count.

Co-authored-by: Jesse Sanford <108698+jessesanford@users.noreply.github.com>

* fix(orcad): pin a confirmed stale-lock reclaim to the record the user approved

--yes reclaimed whatever record held the lock at that moment, so a lock a
same-named machine wrote while the dialog sat open could be cleared.
orcad now takes --expect-nonce and refuses unless the lock still holds
it; the dry run reports the nonce, the desktop sends it back on confirm,
and the managed slot gate requires a build that knows the pin. A
reclaimed result carries the evidence it acted on.

Co-authored-by: Jesse Sanford <108698+jessesanford@users.noreply.github.com>

* fix(settings): keep the Recover server dialog open through its own reclaim

The dialog's mount was gated on the status that offered it, and the
reclaim clears that note before its answer arrives, so the result was
never shown and the card kept the dialog marked open. Only the triggers
are gated now; an open dialog keeps the server it opened for until the
user closes it.

Co-authored-by: Jesse Sanford <108698+jessesanford@users.noreply.github.com>

* fix(settings): put the Recover server confirmation on the security surface

It asks permission to run a remote command that clears an ownership lock
and starts server code, which STYLEGUIDE puts on that surface.

Co-authored-by: Jesse Sanford <108698+jessesanford@users.noreply.github.com>

* fix(orcad): trust a gone launch PID only on the machine that launched it

The PID file sits in a home other login nodes may share, so a wait exec
that lands on another node read a starting server as exited. Both launch
paths now record the launching machine's boot id and node name beside
the PID, and the readiness wait ends early only when its own exec reads
the same record. No record, a host without a boot id, or another machine
keeps the deadline. The PID path is named once in the launch command so
it stays within its sent-byte ceiling.

Co-authored-by: Jesse Sanford <108698+jessesanford@users.noreply.github.com>

* fix(orcad): keep a slot's unreadable stale-lock answer from offering a manual delete

A slot that passed the gate but gave no JSON, or a status this client
does not know, read as unsupported and offered rm of the lock. It now
reads as no-answer, which points to the slot's own reclaim command, and
unsupported is left to a slot without the command, a non-Linux host or
no active version.

Co-authored-by: Jesse Sanford <108698+jessesanford@users.noreply.github.com>

* style(settings): use the text-2xs step for the stale-lock command block

Co-authored-by: Jesse Sanford <108698+jessesanford@users.noreply.github.com>

* fix(mobile): offer the desktop-recovery hint on the same refusal the desktop does

Co-authored-by: Jesse Sanford <108698+jessesanford@users.noreply.github.com>

* test(orcad): name the extra fields a fake stale-lock answer carries

Co-authored-by: Jesse Sanford <108698+jessesanford@users.noreply.github.com>

* fix(settings): end the Recover server flow once the lock is cleared, and keep an unpinned reclaim manual

After a reclaim whose restart failed, the dialog kept the old evidence,
the enabled confirmation and manual recovery for a lock that was gone.
It now shows only that the lock was cleared and why the server did not
start, with Close. A slot that has the reclaim but not the nonce pin is
now told apart from one without the reclaim, and is pointed to its own
reclaim command rather than rm.

Co-authored-by: Jesse Sanford <108698+jessesanford@users.noreply.github.com>

* fix(orcad): keep a stale-lock reclaim's result when the restart after it throws

A wake or tunnel rebuild that threw after the lock was cleared discarded
the reclaimed result, so the caller skipped its reconnect and note
clearing and the dialog kept a confirmation for a lock that was gone.
The restart's failure now becomes the result's notRestarted reason.

Co-authored-by: Jesse Sanford <108698+jessesanford@users.noreply.github.com>

* fix(orcad): report a reclaimed server as started when only its forward failed to follow

A tunnel rebuild that failed after the wake started the server was
reported as the server not starting. The rebuild now goes through
followLaunchedServerPort, as other relaunches do, which warns and leaves
the forward to the next connect. Also drops a caller test that only
covered behaviour the fix did not change.

Co-authored-by: Jesse Sanford <108698+jessesanford@users.noreply.github.com>

* test(mobile): re-record the execution-target goldens for the recovery hint field

The SSH gate's projected state gained `needsDesktopServerRecovery`, which
is false for every recorded reply (no recorded host sends a managed
server refusal). Re-recorded with `pnpm --dir mobile rpc:record` for the
two pilot recordings and the four execution-target reply partitions; the
only change is that field, plus the value pool reordering around it.

Co-authored-by: Jesse Sanford <108698+jessesanford@users.noreply.github.com>

---------

Co-authored-by: Jesse Sanford <108698+jessesanford@users.noreply.github.com>
2026-10-23 17:57:22 -07:00
861299e6cc refactor(ipc): remove unreachable runtime-environment provisioning and control-retry IPC (#32960)
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
2026-10-23 17:54:11 -07:00