* fix(native-chat): report missing saved conversations once
* fix(native-chat): keep failures of answered Claude sends and translate the missing-conversation row
A Claude result settles its own send before the journal sees it, so a
failed send with no echo yet lost its owner and its failure row. The
result now carries the send it answered. Desktop and phone rows also
show the missing saved conversation sentence in the reader's language.
* fix(native-chat): keep failures of Claude results that name a user message
A root failure result that names a user message answered accepted input, so it keeps its row even when that send's waiter was retired, in doubt or ambiguously written. Only a result with no turn, no started send and no named user message (the pre-init missing-resume capture) is left to the host's final startup row. This replaces the per-frame settlement carry, so the acquisition and sign-in tests return to main.
Also ship the new missing-conversation sentence in the runtime English catalog.
* fix(native-chat): leave only a starting Claude session's failure to the startup row
A Claude failure result with no turn now writes no row only while its own
session is still opening (initialize and resume catch-up). Once open
succeeds, every failure result is reported as before, whatever send
evidence it carries, so per-send ownership checks are removed.
* fix(native-chat): fail a starting Claude session with the startup failure it reports
A turnless failure result that names no user input or resumed turn, arriving
while its own acquisition is still starting, now fails that acquisition's open
(initialize and resume catch-up) with Claude's own diagnostic; only then is its
provider row left to the host's final startup row. Results naming input, and
every result once open succeeds, keep main's reporting. The missing-conversation
classifier is shared by stderr and the result's errors, so the typed refusal
ends in one attempt whichever arrives first; other reported startup failures
stay retryable through the existing provider-request error.
* fix(native-chat): keep a Claude startup result's row once another failure ended the start
A Claude startup failure result was still taken by its acquisition's startup
outcome after initialize, a transport fault or an unexpected exit had already
failed that start, so its diagnostic was neither shown nor kept in the final
failure. The outcome now declines once the open fails by another route or the
exit is observed (synchronously, before buffered frames drain), so such a
result keeps its row as on main. It still takes a repeated result it already
accepted, and a result whose refusal the ending exit already carries; a Stop
still owns its start.
* fix(native-chat): report a Claude start's first ending cause from its open
The startup outcome now records the first cause that ends a start: an
accepted startup result, the raced initialize or catch-up rejecting, a
transport fault or unexpected exit, or any other open failure. That cause
rejects the pending race, open() refuses success after it, and the open
rejects with it, so a hidden result is always the open's error and any
other result keeps its row.
* test(native-chat): check the startup rejection with a type guard instead of an assertion
The ConPTY prelude mock delivered its DA1 query on a microtask, which reached only
the physical owner's early onData listener (added in #36422); the startup watch
subscribes after an await, never saw it, and every Windows-path spawn waited out
CONPTY_STARTUP_DECISION_LIMIT_MS (5 s). Deliver it on a macrotask like real output.
Also give two fixtures the readiness their production peers provide, and fix the
counter-file race in structured-agent-shell-environment and the async sign-in
marker race in claude-status-sign-in.
* fix(plugins): merge disk writes into a custom editor's unsaved edits
A write to the file on disk while a custom editor tab had unsaved edits
never reached the editor: the buffer kept the edits, Orca showed its
Reload/Overwrite banner, and the session's merge never ran. Now a frame
whose document session has merge announces it (editor.merges), and Orca
sends it the new disk text with the text the edits started from
(editor.document with ancestor). The session merges, posts the result,
and the tab stays unsaved with it; the banner is skipped. Without merge
the banner decides as before.
* test(editor): an explicit save under the changed-on-disk banner keeps the edits
* fix(plugins): make the outside merge exact, and narrow it to where it is
- The host settles a merge only on the session's edit made against the
merge's own document version, and always commits that result to the
buffer (even when it equals the disk text); silence past the 1 s flush
deadline, a remount or a throwing merge leave the banner and the
suspended autosave in place.
- Orca stamps exactly the disk snapshot the acknowledged merge used and
records it as the next ancestor; a write that landed meanwhile puts the
conflict back for its own merge.
- The ancestor is the tab's disk text recorded while clean and advanced on
acknowledged merges, and must match the tab's disk baseline.
- Only a local file in one merging editor view, with no other tab on its
buffer, takes this path; anything else gets the banner.
- Frame events keep reaching their handlers after the merge capability.
* fix(plugins): move nothing on a failed disk merge; advance the saved text on success
A merge that throws now leaves the session's document, baseline and
version untouched, so the user's next edit goes out against the old
version: Orca refuses it and keeps the banner instead of taking it as the
merge's acknowledgement. After an acknowledged merge the tab's saved text
moves to the disk text it used, so typing back to the old ancestor no
longer looks clean.
* chore(plugins): regenerate the testing bundle on main
* test: restore plugin routing and prompt receipt checks on main
* test: flush pending receipts before releasing the prompt write
* test: route screenshot CLI sources to the journeys that run them
* plugins: editor.save.before hook (format on save, or refuse the save)
Explicit saves of text files from an editor tab run plugins' editor.save.before
in main before the write (local, SSH, WSL and folder workspaces all save through
that one renderer path). A plugin can return changed text or deny with a reason,
shown in the save error. The buffer takes the hook's text only if it is unchanged
since the hook started; edits made meanwhile stay dirty. Autosave, restart saves
and bytes documents are not hooked, and with no live plugin listing the hook the
save path adds no await.
Co-Authored-By: Claude <noreply@anthropic.com>
* test(plugins): editor.save.before text reaches the visible Monaco model
The hook writes its text into the tab's draft, which reaches the model through
the same content-sync bridge as Reload from disk. A model-level test covers the
formatted text showing, the next keystroke editing it, and edits typed during the
hook staying in the model.
Co-Authored-By: Claude <noreply@anthropic.com>
* plugins: editor.save.before review fixes: flush held typing, update the surviving view, keep the denial
- After the hook answers, pending view text (rich Markdown's serialize timer) is
flushed before deciding whether the buffer still holds the hook's input.
- The hook's text goes to whichever view of the buffer record is still open, so
closing the saving view keeps a sibling view in step with disk.
- The save-request bridge passes Errors through whole, so the toast shows the
plugin and its reason instead of a generic failure.
Co-Authored-By: Claude <noreply@anthropic.com>
* chore(plugins): regenerate after rebasing the save hook on main
---------
Co-authored-by: Claude <noreply@anthropic.com>
orca plugin create --template editor wrote an inline-script HTML editor
whose extension came from the folder name squashed to 12 letters. It now
writes the recipe: editor/main.ts (the view), editor/sync.ts (the
document session with a stand-in text merge), editor/sync.test.ts (a
mockEditorHost test of an outside write merged during a save), a
tsconfig on the generated typings, frame isolated, engines.orca
>=1.4.248, and extension .<id>. It passes plugin check and plugin test
as generated.
* plugins: commands[].replaces lets a worker command take over a built-in action
A worker command with replaces: <action> (the declarative action list,
engines >=1.4.248, needs main) runs in place of that built-in from its
shortcut, plugin aliases and the sidebar menus; a failed or silent (2 s)
worker falls back to Orca's own. The install screen names each replacement,
the plugin page has a per-action switch (settings.pluginReplacementsOff), and
when two plugins replace one action the one installed first wins and the
other's log says so. Update review counts replaces as new access.
* fix(plugins): narrow replaced action ids before looking up their titles
* plugins: harden command replaces after review
- Consent includes the replaced actions, so adding replaces to a dev folder
needs a new approval; CLI approval and update review name each takeover.
- Shortcuts main intercepts (Tasks, Workspace Board, history), search with a
selection and floating maximize now go through the replacement too.
- A worker that only starts after the 2 s fallback no longer runs the
command as well: main refuses to start it past the deadline.
- A workspace-only command does not replace an action without a workspace.
- An update keeps the plugin's first install time, so it keeps its takeover.
- Drop the commands.run promise until Orca offers it.
* plugins: commands.run always runs the built-in, never a replacement
A5b's commands.run dispatches with the 'plugin-host' source; the app
dispatcher now builds handlers that run Orca's own action for it, so a
plugin that replaces an action and calls commands.run cannot loop. Docs and
schema describe wrapping the built-in with commands.run; regenerated.
- shot --act: say edits only dirty the tab; end with Cmd/Ctrl+S to save
- manifest: compound suffixes like .sketch.json open by default
- cli.md: list plugin check's exit codes, including plugin_check_failed
- workflow: how to run and stop the dev app from a source checkout
- plugin create --help: real descriptions for --template/--publisher/--id/--name
- recipes: declaration file for vendored ESM libraries
* feat(plugins): add editor.open and commands.run for full-access workers
Both ride the existing plugin view request channel (the one plugin open and
plugin shot use) to the one main window's renderer; headless serve and a
missing window answer unavailable. editor.open takes a path that stays
inside the workspace (Git worktrees and folders) and opens it as a tab
without switching what the person sees. commands.run takes only the built-in
contributes.commands action list and dispatches Orca's own handler, never a
plugin replacement. Both need workspace:write.
workspace.list now also lists project-group folder workspaces.
* fix(plugins): open folder workspaces and host-naming ids through Orca's window
workspace.open of a project-group folder asks the window to show it
(setActiveFolderWorkspace), since the runtime activates Git worktrees only.
editor.open resolves the identity: ids workspace.list now returns. commands.run
documents that it must call the raw built-in, never a replacement wrapper.
* plugins: regenerate the testing bundle and skill guides on the rebased host actions
Generated files only (lint-staged's oxlint refuses a commit with no lintable files).
Co-Authored-By: Claude <noreply@anthropic.com>
* chore(plugins): regenerate the testing bundle on main
---------
Co-authored-by: Claude <noreply@anthropic.com>
* feat(plugins): let full-access workers act on workspaces, terminals and agents
Host API minor 3 adds worker-only rows: workspace.list (workspace:read),
workspace.open (workspace:write), terminal.create (terminal:send),
terminal.read (terminal:read, at most 2,000 lines) and agent.launch
(agent:launch). Each dispatches the runtime RPC method the orca CLI calls,
in process with the CLI's identity, so it runs on the host that owns the
workspace (local, WSL or SSH) and covers folder workspaces. The new
capabilities arrive in 1.4.248; typings, docs and skill references are
generated from the table.
* fix(plugins): make workspace host actions safe to revoke, cancel and target
- Each runtime effect re-checks the plugin's approval right before the
runtime acts (a dispatcher beforeInvoke hook, after its own waits), and a
worker's cancel or exit now aborts the call's signal through to the runtime.
- agent.launch starts the agent's CLI in a background terminal and returns
its handle at once; a prompt is delivered afterwards through the runtime's
own reuse-terminal path, so the host call never waits for the composer.
- workspace.list adds project-group folder workspaces and returns ids that
name their host (identity: keys), so twins on two hosts are never ambiguous.
- terminal.create passes presentation background; terminal.read counts a
runtime-limited read as truncated. workspace.open refuses folders as
unavailable until a window-side open lands.
* plugins: keep both API extensions and host-call tracking under the line limits after rebasing on the hooks
Worker-only status bar and workspace methods spread into PLUGIN_HOST_API through
one module, the in-flight host call map moves into PluginWorkerHostCalls, and
bindPluginRuntime builds the runtime RPC so main-process-plugins.ts keeps its
size; regenerated docs and typings.
Co-Authored-By: Claude <noreply@anthropic.com>
* chore(plugins): regenerate after rebasing on the editor recipe and shots
* chore(plugins): regenerate after rebasing on the plugin shot recipe
---------
Co-authored-by: Claude <noreply@anthropic.com>
* Show explicit agent sign-out facts before sending a chat
* Drop the Grok status process; keep listed models when Pi or Claude replies arrive late
Grok goes back to one listing process with no Grok-specific sign-out guess;
an agent-authored auth-required reply still reports notSignedIn. Pi reports
notSignedIn only when it has no provider and nothing listable. Once Claude's
required replies are in, the listing deadline returns them instead of failing.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Drop the runner tests' timeout bumps, which sat below the default
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Type the endpoint test's launch env as a string map
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Drive the signed-out Claude re-probe trigger in the scaling contract
N and 10N distinct chats (distinct read claims, one account-scoped
Claude fingerprint) read with waitForListing against the real probe and
store on an injected clock: one listing before the held-reason TTL,
exactly one more once it is due (held until every read has decided),
none within the next TTL, each result carrying notSignedIn and models.
Checked locally that it bites: making heldReasonDue ignore the TTL fails
it (2 listings inside the TTL). Removing the in-flight dedup (the
probeDue isListing guard plus the lister's early return) does not: store
refresh then runs once per chat, but a newer flight replaces each queued
one behind the two admission slots, so still one process per TTL.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Accept Claude control replies as main did, without a type field
The shared reader required type 'control_response' and prefiltered on
that text, dropping success frames main's model and slash-command
parsers accepted (the released-phone wire test's fake listing).
It now prefilters on request_id and keeps the structure limit and
success-only reading.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Narrow the scaling contract's catalog result before reading models
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(shortcuts): give Windows and Linux chat switching a working default
* test(shortcuts): verify the worktree and chat chords independently
* fix(shortcuts): preserve agent newline keys while switching chat views
* fix(shortcuts): use Ctrl+Shift+F11 for Windows and Linux chat switching
The Ctrl+Alt+Shift+J default could not match non-Latin layouts or IME
input, because Ctrl+Alt chords skip the physical-key fallback that
protects AltGr characters. A function-key primary matches on every
layout; Ctrl+Shift+J stays as the alias where Switch worktree doesn't
claim it.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(shortcuts): keep the chat toggle's own default alias when its shortcuts are edited
Saving any change to Switch between Chat UI and terminal on Windows and
Linux kept its untouched Ctrl+Shift+J alias, which the conflict check
then refused against the worktree palette's default, so replacing or
removing the primary failed. The check now exempts a chord per binding
when it is the action's own default and every other owner also holds it
as a default, so non-default explicit claims are still refused.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* test(shortcuts): expect the new Windows/Linux chat switch label
The context menus show the toggle's primary binding, which is now
Ctrl+Shift+F11 on Windows and Linux.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Make phone follow-up delivery follow paired desktop
* Keep the paired desktop's follow-up setting live through a server outage
The phone now keeps listening to the paired desktop's Queue follow-ups
setting while a chat is open, even when the workspace's server relay
drops, so a desktop toggle during the outage is used by the next Send.
The chat reuses the route's existing paired-host connection instead of
acquiring it again per chat, and the follow-up tests load and typecheck.
* Keep the chat lock-reason helper out of modules the desktop tests import
The overlay lock-reason helper moves to its own file so the shared
eligibility module no longer reaches the chat view, which pulled the
phone's UI types into the desktop end-to-end typecheck. The reconnect
test now drops and restores the client's own connection, and checks the
preference stream closes with it and reopens with one fresh read.
* Show the active GitHub list source in Tasks summary
* Tasks header, repo link and source row follow the list's resolved source
The Tasks summary could name the saved upstream project while the list came
from the fork, and the "Open in GitHub" link could open the other kind's
repository when the active kind's source was unresolved. A new search or an
Issues/PRs switch also blanked the source until the new fetch landed.
Compute the source summary once, after mode and item kind are known, from
the list's resolved sources (saved project identity no longer reaches the
formatter). The summary, the single-project repo link (strict active kind)
and the Filters source row share one display-only read: the current query's
entry, else the latest same-scope sibling while the query is fetchable.
Notices, errors, Retry, filter metadata and page rows still read only the
current query's entry. Projects, GitLab, Linear and Jira output is unchanged.
* Tasks source carries the shown entry across queries, read live
While a new search or Issues/PRs switch has no cached result yet, the Tasks
summary, repository link and Filters source row keep showing the source of
the entry they last showed for that project copy, read live from the cache,
instead of picking the newest cached sibling. A refresh that changes that
entry is followed, other queries' results never take over, a preference
change's eviction ends the claim, and a present unresolved result or a
too-large query makes no claim.
The display read now happens in the existing current-entry selection pass
(at most two cache reads per selected project, no cache scan), and the
summary re-translates when the language changes. Adds a SourceBar fixture
with a long fork slug at normal and narrow widths.
* Tasks source display perf contract measures each update on its own
Bound every measured update (another host's patch, a non-cache store write,
current and sibling fetches, a real preference eviction) at 10 and 100
selected rows and 50 or 500 cache entries: no cache enumeration, at most two
reads per row per selector call, a fixed selector-call budget equal across
sizes, no key rebuild on cache writes, linear row visits including pointer
upkeep, and reused output references for writes that change nothing shown.
* Use typed reads in the Tasks source display perf instrumentation
The static-analysis rule rejects Reflect.get; the cache and row proxies now
read by typed keys and count exactly the same reads and enumerations.
* Drop type assertions from the Tasks source display changes
Stage hooks return Object.assign(model, {...}) instead of casting the model
(same object, same writes, same type). The fixture and test stubs keep their
partial-model casts on one line each with a SAFETY reason, the row proxy uses
a type guard, and the filler work item is a typed fixture.
* fix(native-chat): running chats keep effort menus during model discovery; dotted versions in id-derived model labels
* fix(native-chat): options stay usable while a model list refreshes; dated preview ids keep their dashes
A running host listing no longer disables effort, speed or fast mode. Those
controls follow the data they draw from: before any list exists they show
the placeholder or a read-only saved effort, and once a list or the chat's
own answer is shown they stay usable through every refresh, for every agent.
The listing flag is now only the host's raw fact, which keeps a seed from
settling to the built-in list while discovery runs.
Id-derived labels no longer dot a zero-led part, so MM-DD preview dates
(gemini-2.5-flash-preview-05-20) stay dates.
* fix(native-chat): a model list refresh keeps the verified models and default effort already shown
While the host refreshes a Claude or Codex list, the chat keeps the model
ids an earlier finished listing verified that the refreshing rows still
contain, so a running Codex chat's CLI-default effort ("Medium") no longer
blanks from the pill and menu until the listing lands. Ids no finished
listing verified stay unverified, and a finished listing still replaces
the set.
* test(native-chat): type the listing-wait check without an assertion
* fix(model-labels): month-day dates after October stay undotted
* refactor(native-chat): listing-verified model ids get their own module
* Show failed pipelines and check suites without jobs
* Use the parent pipeline as the GitLab checks verdict
The MR's own pipeline status now leads the Source Control Checks panel,
the MR dialog and the phone checks badge whenever the details reply
carries it, even when the returned jobs are empty, partial or all
passing. Job rows and counts stay a separate breakdown. The parent
follows pipeline-level policy (required manual is pending, canceled and
skipped are not failures, mergeable manual is neutral) from one shared
rule, and it is a provider link rather than an expandable job, so it
never shows a details error with Retry. A failed parent leads the
triage strip and joins the Fix prompt by name and link.
Drops the GitHub zero-run failed check-suite rows from this change;
existing approval-required suite rows are unchanged.
* Name the GitLab pipeline outcome and keep its verdict in one rule
The host now settles the parent pipeline's verdict with the same rule
and reply that produce the MR status, so a manual pipeline on older
GitLab reads pending everywhere. The phone checks badge names the
pipeline outcome (Pipeline passed/failed/pending) instead of an
unlabeled one-item count; job wording without a parent is unchanged.
The desktop parent row's status is localized, and a failed MR-details
read still shows its error beside a kept parent with no jobs.
* Keep Fix for failed GitLab jobs and treat waiting_for_callback as pending
The triage strip shows again when a returned job failed under a passing
parent pipeline, so its Fix stays available; a failed parent still
leads. A waiting_for_callback pipeline now reads pending instead of
neutral, in the MR status and the parent pipeline alike.
* Mark the checks-panel action test fixture cast as safe
* Pause the pipeline row spinner offscreen and under reduced motion
* plugins: orca plugin shot --editor, and plugin open --editor opens the file's folder
plugin open --editor now opens the file in the workspace that holds it,
matching its real path too (/tmp vs /private/tmp), and otherwise adds the
file's folder as a folder workspace instead of failing with no_workspace.
plugin shot <dir|key> --editor <file> [--theme] [--width --height]
[--act] loads the editor afresh in a hidden tab, waits for it to get its
document and ask for it (a new editor.subscribed frame event), captures
the editor's guest with the panel shot's capture path, and sends --act
clicks, drags and keys as real input via sendInputEvent. It exits 1 with
the frame's errors when the editor throws, is blocked by its content
policy, or never asks for its document within 5s; theme and size are put
back. New RPC method plugins.shotEditor so older hosts fail cleanly.
* plugins: shot --editor never changes the author's file
With --act the shot copies the file into a temporary folder, shoots the
copy in a temporary folder workspace, then closes its tab and removes the
workspace and folder; --in-place acts on the file itself. Without --act
it opens the real file, leaves its buffer alone and closes the tab if the
shot opened it. Afterwards Orca shows the workspace it showed before.
Regenerates the skill's CLI reference.
* plugins: fix editor shot typecheck (refusal next step, RPC test mock)
* plugins: editor shot targets its own guest, flushes before reload, scopes cleanup per capture
- Main captures the exact guest WebContents the shot's tab mounted, not the first with its URL.
- An open editor's pending edits are flushed before the shot reloads it; the shot fails if not.
- --act --in-place keeps the acted tab and its unsaved edits.
- end-capture undoes only its own capture (per-capture id), so overlapping shots don't clash.
- The file's workspace lookup includes local folder workspaces.
* plugins: shot --status-bar/--window, key acts focus the editor guest, panel shot selects a workspace
- Editor --act keys first give the guest's <webview> DOM focus and refocus its target, so a canvas
that keeps focus no longer drops later keys; never webContents.focus(), which focuses the window.
- orca plugin shot <dir|key> --status-bar | --window [--theme]: captures Orca's status bar or the
whole hidden window (plugins.shotChrome) with the existing capture helpers.
- With no workspace showing, a panel shot selects this machine's most recent workspace in the
hidden window instead of failing.
* plugins: a shot's end restores only its own capture overrides
* plugins: regenerate the testing runtime with the editor subscribed signal
* plugins: editor capture refusals go through one CLI-message helper
* fix(plugins): give the chrome shot refusal a next step
recipes.md gains "A rich editor with a library" beside the hooks and status
bar recipes: an isolated-frame custom editor that vendors its library behind
an import map, syncs through orca.document.session with a merge by id, and
tests that sync with mockEditorHost. host-api.md lists every --orca-* frame
variable and what it paints, generated from the frame token allowlist.
* feat(plugins): check custom editors statically and add mockEditorHost
orca plugin check now follows each custom editor entry (local script src,
inline module scripts, relative imports), transpiled and resolved as the
frame loader does, and fails at file:line:column on syntax errors, missing
files, refused imports, string eval/Function and files over their size
cap, with no running Orca. The check ships in the test hooks bundle.
orca/testing gains mockEditorHost: it runs the frame shell's real
window.orca (document API and session) over an in-memory text or bytes
file, with edits, replace, conflictNext, flush, setTheme and dispose.
Typings, docs and the skill guide cover both.
* feat(plugins): warn on editor eval and check editors in dev watch
editor_eval comes from a lexical scan that can misread rare code, so it is
now a warning that does not fail orca plugin check. dev watch --check runs
the editor check beside the panel check and reports it under editors.
* fix(plugins): check editors by the frame protocol's rules; mock blocks lost saves
The static editor check now reads files through the loader's contained
reader (symlinks out of the plugin are refused), runs only JavaScript
script types, honors the page's import map for native modules, classifies
files by the protocol's content types (JSON imports, extensionless HTML
entries), decodes escaped script URLs, and uses the protocol's caps: 10 MiB
entry, 1 MiB transpiled module, 16 MiB checked dependency.
mockEditorHost refuses an oversized edit before reading it and then rejects
flush() until a later edit lands, as Orca blocks the save.
* test(plugins): type mockEditorHost's frame API instead of reflecting on it
* fix(plugins): keep main's case-spelling check and hook typings through the rebase
The rebase onto main resolved plugin-artifact-validation.ts and
orca-testing.d.ts to B4's side; this puts back main's exact-path-spelling
check and hooks typings beside B4's contained reader and mockEditorHost
types, and regenerates the testing bundle.
* plugins: agent.tool.before and agent.tool.after hooks; Claude Code PreToolUse blocks
The hook server runs plugins' tool hooks after recording status. Claude Code's
POSIX hook script now asks for an answer on PreToolUse (X-Orca-Hook-Decision),
waits at most 8 s (Orca answers within 6 s) and prints the decision: a deny
with the plugin's reason, or updatedInput without approving it. Timeouts,
errors and an unreachable Orca print {}, so the call proceeds. Other events
still print {} before posting. Every other agent, Windows scripts and SSH/WSL
relays only observe or are not hooked yet (per-agent table in the
agent-hooks README).
Co-Authored-By: Claude <noreply@anthropic.com>
* plugins: verify PreToolUse answers against the real Claude CLI; 204 at once when no tool hook listens
A real-CLI test (ORCA_REAL_CLAUDE_CLI_TEST=1) shows Claude Code 2.1.316 runs
updatedInput sent without permissionDecision, still asks for approval of the
rewritten call, and refuses a deny. With no plugin listing a tool hook, the
server answers 204 before any await and runs no chain.
Co-Authored-By: Claude <noreply@anthropic.com>
* test(claude): type the real-CLI PreToolUse answer
* plugins: harden agent tool hooks (partial replies, fenced posts, OpenClaude, Amp/Copilot/Cursor)
- Drop a failed curl's partial body and print only a complete PreToolUse decision.
- Run tool plugins only for posts status ingestion accepted; name the owner's terminal.
- Enable PreToolUse decision mode only for Claude Code, not CLIs reusing its script.
- Decode Amp tool.call/tool.result, Copilot envelope/implied events and Cursor shell/MCP calls.
Co-Authored-By: Claude <noreply@anthropic.com>
* plugins: regenerate the testing bundle on the rebased tool hooks
Generated file only (lint-staged's oxlint refuses a commit with no lintable files).
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
* plugins: agent.launch.before and agent.prompt.before hooks; orca plugin dev hook
agent.launch.before runs at the PTY spawn every agent terminal goes through
(window tabs, runtime/CLI agent launch, automations, orchestration), before
dispatch to its host: change the command line, add env (Orca's own ORCA_*
variables and the agent's environment stay hidden), or deny. Restores and
resumes are not hooked. agent.prompt.before runs in sendTerminalAgentPrompt
(launch prompts, orchestration dispatch, terminal send to an agent) with the
agent named: rewrite or deny. The terminal hooks move into one runtime layer.
orca plugin dev hook runs a hook through one dev plugin and prints the decision.
Co-Authored-By: Claude <noreply@anthropic.com>
* plugins: recheck rewritten launch commands, forward hook env to WSL, keep prompt delivery outside the hook order
- A command agent.launch.before rewrote passes the AI Vault resume check again
on both spawn paths.
- Variables a hook adds are named in WSLENV on Windows so WSL agents get them.
- agent.prompt.before holds the terminal's order only while it decides; a
delivery waiting on a permission answer no longer blocks later input.
Co-Authored-By: Claude <noreply@anthropic.com>
* plugins: plugin dev hook honours onError: deny
Co-Authored-By: Claude <noreply@anthropic.com>
* plugins: regenerate the testing bundle and skill guides on the rebased hooks
Generated files only (lint-staged's oxlint refuses a commit with no lintable files).
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
On Linux the probe launcher appends --no-sandbox, so argv[length - 2] is the --user-data-dir flag rather than the config path. The page then wrote result.json under a directory named after that flag, threw, never exited, and the launcher timed it out at 30s on every Linux run since #37300. The sibling Electron probes already pass the result path in config.json; this one now does too.
* plugins: contributes.hooks runtime with terminal.send and workspace.create hooks
Workers register orca.hooks.on(hook, (event, next) => ...). Orca runs listed
plugins in order before it acts: next(event) continues with allowed fields
changed, { deny } stops, nothing passes. A plugin past its 2 s deadline, or
one that throws, crashes or is disabled, counts as passing and is logged with
plugin, hook and duration. First hooks: terminal.send (runtime sendTerminal),
workspace.create.before (IPC and runtime creates, folders included) and
observe-only workspace.create.after. orca/testing mountWorker gains
hooks.fire. Needs main + trusted-worker and engines.orca >=1.4.248.
Co-Authored-By: Claude <noreply@anthropic.com>
* plugins: keep hook plugins' workers started; bound start-up separately from the hook deadline
Workers of enabled plugins that list hooks start as soon as they are approved
and are never idle-reaped; crash restarts follow the manager's policy. A hook
waits up to the worker ready timeout for start-up, then gives the handler its
full 2 s, so a cold worker no longer lets the first hooked action through.
Co-Authored-By: Claude <noreply@anthropic.com>
* plugins: give hook workers their own soft cap instead of the active-worker cap
Always-running hook workers no longer hold slots of the shared 5-worker cap, so
many hook plugins cannot starve the rest. They have a soft cap of 16; past it a
hook worker waits for another to stop, logged once.
Co-Authored-By: Claude <noreply@anthropic.com>
* test(plugins): type the hook warmer's ensureWorker mock
* plugins: address hook review: ordering, human input, overflow, revalidation, folder creates
- Hook workers past the soft cap start as overflow (logged once) instead of
waiting, so reconciliation, disable and uninstall never hang.
- Writes to one terminal stay in order while terminal.send decides, so a later
Enter cannot submit the previous line.
- terminal.send runs only for programmatic sends (main, plugins, the CLI);
desktop, mobile and legacy client input is a person's typing.
- Text a hook rewrote passes the AI Vault resume check again.
- Project-group folder workspace creates (RPC, agent launch, window) run the
workspace.create hooks through one boundary.
- A decision from a plugin disabled or changed while it ran is discarded.
Co-Authored-By: Claude <noreply@anthropic.com>
* test(plugins): drive hook start-up timing with fake-timer deferreds, no real sleeps
Co-Authored-By: Claude <noreply@anthropic.com>
* plugins: only the hook decision holds a terminal's write order
A delivery that waits (a permission prompt, the launch composer) no longer
blocks later writes, and a person's input, interrupts and query replies never
wait behind a pending decision.
Co-Authored-By: Claude <noreply@anthropic.com>
* plugins: hook DX: onError deny, denied_by_plugin, a Hooks reference, --template hook
- contributes.hooks[].onError: "deny" makes a throw, timeout, crash or bad
answer refuse the action, naming the plugin; the default still passes.
orca/testing mountWorker honours it, and mockHost answers fire-and-forget
methods (result { ok: true }) with success by default.
- A plugin denial reaches RPC clients as denied_by_plugin with the plugin and
reason, and the CLI no longer suggests a retry for it.
- The skill's host-api reference gains a generated Hooks section (event, what
next may change, stop result); the recipe passes sends through unchanged and
warns against awaiting other calls before deciding.
- orca plugin create --template hook: a worker-only terminal.send guard that
fails closed, with a test.
Co-Authored-By: Claude <noreply@anthropic.com>
* plugins: regenerate hook docs on main; bind the plugin runtime and its hook runner in one call
Keeps main-process-plugins.ts under 300 lines now that status bar items landed.
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
The probe waited for an exact count of refusal lines, but markup-only fonts and media load
only on layout, which a hidden Linux CI window may skip, so it timed out at 30s. The page
now requests each kind from script, settles each on its own outcome, and logs a done line
the main process waits on.
* Resume direct work item launches after setup confirmation
* Preserve setup review and settle cancelled work item preparation
* Fix setup test types and synchronize localization artifacts
* Keep accepted setup launches owned through completion
* fix: declare accepted create dialog result
* fix: report direct launch preparation failures
* Reuse workspace setup preparation and section
* Use compact dialog description for setup help
* Construct omitted setup decision fixture through serialization
* Match optional signal in background setup trust test
* ci: full checks on main pushes, changed-file lint on PRs, Slack alert when main breaks
PR Checks now also runs on every push to main, with each commit in its own
concurrency group so main runs never cancel one another. On a push the path
router gets an empty diff, which already fails closed to every job, and the
input-scoped steps (localization extraction, VM rollback replay, orcad smoke)
run in full. Advisory E2E lanes stay PR-only.
On PRs the per-file passes (root oxlint, JS-plugin oxlint, anti-slop, ast-grep
and oxfmt) check only the changed paths. Everything is checked when the diff
cannot be resolved, a lint toolchain input changes (oxlint/oxfmt/ast-grep
configs and plugins, package.json, the lockfile, .gitignore, pr.yml), or more
than 1500 paths change. Whole-program checks stay unscoped.
A new workflow follows completed main runs and posts to the release Slack
webhook only when main turns red, and once when it turns green again.
* ci: alert once per main health streak and tolerate a malformed Slack mapping
* plugins: contributes.statusBarItems with worker updates (statusBar.update)
A plugin declares status bar items { id, text?, icon?, tone?, tooltip?,
command? } (declarative profile, engines >=1.4.247); pressing one runs
its own command. A trusted worker changes them with the new worker-only
host method statusBar.update; main keeps the session's changes and
pushes snapshots to desktop windows over plugins:statusBarChanged.
Regenerates schemas, references, typings and the authoring skill.
* plugins: statusBarItems ship in 1.4.248, beside contributes.hooks
* plugins: clear a plugin's status bar changes when it is disabled, removed or updated
* plugins: list status bar items in the plugin page's Adds section
* plugins: count statusBarItems' since in the host's newest readable version
* i18n: add the plugin page's status bar row label to en.json
* plugins: status bar items respect workspace commands, dev reloads and catalog summaries
- An item whose command needs a workspace is disabled with no open workspace.
- A dev code edit (new devRevision) drops the old worker's status bar changes.
- Catalog summaries list status bar items, and marketplace details show them.
* plugins: authoring skill lists status bar items as buildable; approval lists them
The skill no longer calls status bar items reserved and gains a row for them. The
dev-load/inspect approval already lists them via the contribution summary; a test pins
that and the fingerprint binding.
* chore(plugins): regenerate the testing bundle after rebasing on main
* fix(plugins): skip nested dev packages and count blocked frame requests
Dev folders and their watcher now skip .git, .orca and node_modules at any
depth, so a nested build/node_modules no longer trips the 2,000-file limit
or re-reviews the plugin; installs still hash everything but the root .git.
Requests a plugin frame makes off its own files (session-cancelled or
refused by its content policy) are counted per kind and origin and logged
every 5 s and on teardown, outside the 20-lines-per-10-s console budget:
[frame] blocked font-src https://unpkg.com (x48): plugin frames have no network
* fix(plugins): read current CSP refusal wording and bound blocked-request logging
- Electron 43 prints "Loading the font '…' violates …" (and Connecting to / Framing /
Fetching content from); read it alongside the older "Refused to …" wording, name the
unset directive from the fallback note, and treat fetch's repeat as an echo. An Electron
test captures the bundled runtime's real messages.
- Keep at most 32 kind-and-origin counts and log at most 8 lines per flush, folding the
rest into one overflow count line.
* feat(plugins): add orca.document.session() for custom editors
One call replaces the document sync every editor rebuilds: decode/encode
(plain text by default), optional merge, debounce. One edit in flight with
later changes coalesced; the baseline moves only on accept; a refused edit
keeps the draft and rejects flush(); own accepted edits never reach
onReplace; a conflict merges once against the last accepted value and
resubmits, or keeps the draft without merge. The session answers
editor.flush itself and resolves after hello and the first document.
The --template editor now uses it. Docs and typings state the session and
the undo model (the editor owns undo).
* fix(plugins): let the outside document win a session conflict without merge
Agents write files while people edit them, so keeping the draft lost the
agent's write on the next keystroke. Without merge the session now drops the
draft, fires onReplace with the outside value and rejects flush() with
conflict; editors that want both sides pass merge.
* fix(plugins): serialize session edits through saves and settle before answering
- One edit in flight at all times, saves included; editor.flush is answered
only once every change is accepted or refused with nothing newer to send.
- The editor host commits a held edit when a save starts and admits edits
during the save at once, so waiting fits the 1 s deadline.
- A change generation replaces identity checks, and sent bytes are
snapshotted, so a reused buffer is never mistaken for an accepted edit.
- A waiting flush keeps its conflict rejection when the outside document
wins, and nothing merges or sends after dispose.
* test(plugins): read session test edits through typed helpers
* fix(plugins): drain session flushes, reject dropped drafts, snapshot the baseline
- flush() keeps going until no change made during it is left unsent.
- A waiting flush rejects with conflict whenever the outside document
replaced a draft, newer ones included.
- The accepted baseline is decoded from the sent snapshot, so a reused value
buffer cannot change merge's base.
* test(plugins): name the session test option and message types
* test(cross-version): load moved modules from wherever each release defines them
v1.4.246 is the first release cut after #36909, #36979, #37036 and #37070 moved
the agent-session modules, so suites that imported the old paths from the
newest release failed with ERR_MODULE_NOT_FOUND for every PR running
cross-version wire. importReleaseCheckoutModule now resolves a moved module to
the path the checked-out release ships, from one table of current and earlier
locations, so both the rolling baseline and older pinned refs load. The
released-chat writers drop their own per-module existsSync fallbacks for it.
v1.4.246 also ships outcome storage, so it reads chats this build writes and
closes its own rows with the outcomes marker. The downgrade test pins its
refusing window to the two releases before outcome storage, and the upgrade
test leaves the marker to the outcome reader, which restoreJournal checks.
* test(cross-version): find a moved module by name in the release instead of a path table
A table of earlier paths is a list of stale src literals the path-literal ratchet rightly refuses, and needs an entry for every future move. The release checkout itself says where it ships each module: the one file with that name under the same source root. Prunes the three released-chat-writers entries the ratchet baseline grandfathered.
* test(cross-version): pin the chat-upgrade writers to the releases before outcome storage
v1.4.246 keeps a chat in outcome tables, so its rows hold only the marker and there is no row upgrade to carry on; the newest release stays the reader this build must match.
* feat(plugins): let a custom editor require isolated frames
customEditors[].frame: "isolated" | "any" (default any, since 1.4.248).
An isolated-only editor never runs in the srcdoc fallback: main serves its
fallback shell without the entry, and when this window has no isolated
guest (browser clients, or desktop with isolated frames off) the file opens
in Source with "This editor needs the Orca desktop app". Picking the
editor shows the same notice with an Open Source button.
* i18n: add the needs-desktop editor notice strings
* plugins: refresh frame isolation when its setting changes; browser clients open Source
- The learned isolated-frames answer is tied to the setting it was learned under, so turning
isolated frames back on lets isolated-only editors open by default again.
- A browser client's plugin bridge has no list, so the plugin store settles on no plugins
instead of erroring and retrying; files open in Source.
* fix(relay): give a freshly booted reserve cell a dead-man boot grace until its first director contact
A reserve cell's dead-man starts its 60-120 s window when the switch file applies, but no
placing director can reach a new process until its load balancer routes to it. On the
2026-11-01 production same-cap rolls that took up to about 110 s after the switch file
applied; c10 (104 s window) and c16 (71 s) tripped 1-4 s before the first director poll
landed, latched into db, and failed their roll after a clean drain and restart.
Until the process's first contact (a reserver=1 feed poll or a real booking), the window is
the hashed window plus 180 s, matching autohealing's initial delay. After the first contact
nothing changes. A process that never hears from a placing director still falls back to db,
up to three minutes later than before.
* test(relay): assert the server wires the dead-man boot grace
* docs(relay): state the boot gap behind the dead-man grace as inferred, not measured
* Preserve repositoryless GitHub Project items
* Keep only draft and restricted Project items without a repository
Key the repository-scope exemption on item kind on desktop and phone so
an older host's content-less Issue/PR rows stay hidden as before. Draft
titles, cards and bars stay non-interactive with no row browser icon;
the toolbar's Open view in GitHub is the Project-level action. Phone
labels restricted items "Restricted" to match desktop. Normalization
assertions move to a main-process test so renderer tests no longer
import main code.
* Cover draft Status normalization and phone restricted-item copy
Restore the check that a draft's single-select Status survives
normalization, and render the phone Project list and detail drawer with
a draft and a restricted item using the real status labels.
* Keep recovered conversations out of history warnings
* Name the agent when a History conversation is too large to load
A conversation whose only records exceed the transcript record limit now reports its own coded reason naming the agent and suggesting resuming it in that agent, instead of the generic session-metadata message. Adds a rendered banner test and a non-user Codex case.
* Build History banner test values without type assertions
* Name the conversation's writer in the too-large History reason
Labels the reason with the parsed agent so an OMP transcript found under Pi's folder names OMP, ships the new key in the English runtime catalog, and renders the reason in the phone's History panel test.
* Cover the phone History notice for readable results and Spanish
Test-only: a ready result without issues shows no skipped notice, and the too-large case keeps its English reason after switching the phone to Spanish while the count is translated.