Skip to content

[upstream #12818] fix(serve): recognize CLI-form serve args on the Electron process - #139

Closed
innocarpe wants to merge 1061 commits into
mainfrom
fix/serve-cli-form-is-serve-mode
Closed

[upstream #12818] fix(serve): recognize CLI-form serve args on the Electron process#139
innocarpe wants to merge 1061 commits into
mainfrom
fix/serve-cli-form-is-serve-mode

Conversation

@innocarpe

Copy link
Copy Markdown
Owner

Portfolio mirror of my contribution to upstream stablyai/orca.
Exhibition only — the real review/merge target is upstream.

Upstream

Summary

Description Recognize CLI-form serve argv on the Electron process so headless mode, GPU disable, and serve flags engage when the binary is launched as … serve … without the CLI rewrite. ## Focused fix - In scope: serve-mode-argv normalize + early process.argv rewrite

Note

  • Do not merge this into innocarpe/orca main until the upstream PR is merged.
  • After upstream merges: sync fork from upstream, then close this mirror PR.
  • This open PR exists so visitors see in-flight work on this fork's Pull requests tab.

OrcaWin and others added 30 commits August 2, 2026 19:16
…emoval (stablyai#12153)

Destructive worktree removal proves every PTY is dead before touching the filesystem. When a stop
RPC failed, it re-listed the provider to check whether the PTY had already exited — but on the
same deadline the sweeps had just spent, so it timed out without ever asking and read "could not
verify" as "still live". The sweep spends that budget every run, making the refusal deterministic;
--force never reached the gate, so the workspace was unremovable forever.

- Verification gets its own budget instead of an exhausted remainder.
- Verdicts split into exited / live / unverifiable; the error names the blocking PTY ids and why.
- A reachable escape hatch: allowUnverifiedPtyStop, set only by genuine Force Delete affordances
  and the CLI's --force — never by the force the ordinary delete confirmation already sets — with
  an 'unstopped-pty' classifier reason so the desktop actually offers the button.
- Force also survives a sweep that cannot complete; the non-force path still fails fast.

Fixes stablyai#11960
…view cap (stablyai#12159)

* fix(native-chat): stop clipping assistant text blocks at the tool-preview cap

Long assistant messages read over a paired connection (headless orca
serve viewed from desktop or mobile) were cut at 4,000 chars with a
'… (truncated)' marker and no way to read the rest. The mobile payload
diet in nativeChat RPC applied the tool-preview char cap to text blocks,
which are the fully rendered message body. Give text blocks their own
64k safety ceiling so real replies pass through whole while pathological
multi-hundred-KB blocks still can't freeze the phone.

Fixes STA-3230

* test(native-chat): cover long text stream frames
Groups 5/6 filled; point community join flow at group 6 + group 7.
Older groups are full; community join path is group 7 only.
…missal (stablyai#12167)

* Keep your New Linear and Jira creation drafts after an accidental dismissal

* Avoid draft store writes while typing

* Update draft retention refs after commit
…tablyai#12172)

* Open SSH host add/edit form in modal dialog

Form moves from inline to a viewport-stable modal (STA-3067) so fields stay
accessible with long host lists. Includes sticky header/footer, dirty-state
protection against outside click, and session-aware Advanced state reset on
cancel/reopen.

* fix: add missing SshTargetForm localization keys

Sync en.json catalog for modal title/description strings so
verify:localization-catalog passes in static analysis.

* fix: translate SshTargetForm modal strings in es/ja/ko/zh

Add non-English catalog entries for the new modal title and
description keys so localized UIs match English.

* Prevent SSH form double-submit and fix dismissal detection

Adds a saving state to prevent concurrent saves when a user double-clicks
the submit button. Fixes outside-click dismissal by correctly tracking form
state across re-renders using refs. Extracts session termination logic to
a reusable module.

* fix: stop mutating formRef during render in SshTargetForm

React Doctor fails the static-analysis gate when refs are written during
render. Sync form into formRef in an effect so render stays pure.
…tablyai#12183)

* fix(updater): recover Linux .deb/.rpm installs that fail escalation

A `.deb` install fails with `No authentication agent found` when the session
has no polkit agent. Orca reported "Quit and reopen Orca, then try again" —
wrong advice — and its only action was Retry Download, discarding a verified
160 MB package that was still in the updater cache.

Keep the one-click install path, but make a failed root-package install
recoverable without downloading again:

- Retain the downloaded package and its expected SHA-512 from the
  `update-downloaded` event, mirroring electron-updater's cache-name rule.
- Capture the child stderr that BaseUpdater logs but drops from the `error`
  event, redact it (ANSI, control bytes, `<home>`, `<package>`, `<user>`,
  1 KiB cap), and classify the failure. Classification reads the original
  text — redaction can rewrite a matched phrase.
- Send a structured `linux-package-install` recovery status and render a
  dedicated card: Copy Install Command / Try Automatic Install Again /
  Show Package.
- Revalidate on every action: cache containment, lstat, streamed SHA-512,
  timingSafeEqual. Concurrent requests coalesce into one hash pass.
- Build the command from fixed tokens plus one POSIX-single-quoted absolute
  path, resolving sudo and the package manager only from /usr/bin, /bin,
  /usr/sbin, /sbin. Orca never runs it.
- Disable `autoInstallOnAppQuit` for .deb/.rpm so an ordinary quit cannot
  trigger the same failing escalation after the UI is gone.

Extracts the error-card presentation into UpdateErrorCardContent so
UpdateCard does not absorb another stateful surface.

Lifecycle breadcrumbs carry package type, reason, exit code and version —
never a path, command, username or raw child output.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Improve Linux package install recovery diagnostics

- Distinguish invalid-package-path errors from missing package manager
- Expand ANSI escape sequence stripping to handle OSC hyperlinks and DCS
- Prevent generic error logs from overwriting specific diagnostic verdicts
- Add error handling for shell.openUrl in update UI
- Fix test isolation with proper afterEach hooks

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…tablyai#12191)

The parent-drift repair path (destroyPersistentWebview with
preserveViewport: true, introduced in stablyai#12137) tears down and rebuilds the
webview under the same browserTabId, but unconditionally forgot the tab's
explicit user zoom. BrowserPane then re-seeds from the Settings default on
the next mount, silently resetting per-tab zoom.

Only forget explicit zoom on a real close; a preserveViewport rebuild keeps
the same logical tab, so its zoom must survive.
…ver a bare renderer PTY (stablyai#12197)

A mobile New Tab -> Codex create resolves the launch command and hands it
to the renderer, but when the renderer's startup queue is lost (the stablyai#7587
stall class) the pane spawns a plain shell and the create still settles
ready via PTY adoption - silently binding the phone to a bare terminal
forever, since the ready status also disables the stablyai#7837 activation-time
materialize recovery.

Record the resolved launch command on the pending create and, at every
renderer-backed settle point, deliver it to the adopted PTY when no spawn
command was recorded for it. Spawn commands are noted per PTY by both
spawn IPC handlers, so a missing record on the locally registered live
PTY proves the launch never ran; delivery types the command exactly like
the create would have, and the note prevents double delivery.

Fixes STA-3214
…r breadcrumbs (stablyai#12198)

* feat(crash-reports): add byte attribution to renderer memory highwater breadcrumbs

Entry counts stay flat when a slice grows by value weight (97b9e86d leaked
~700MB while its biggest slice grew by 4 entries), so highwater breadcrumbs
now carry sampled per-slice KB estimates for the store plus a live pane
census (managers, panes, estimated scrollback KB) — the dominant heap cost
the store census cannot see.

* fix(crash-reports): bound renderer OOM profiling

* fix(crash-reports): total raw store estimate bytes
…tablyai#12194)

* fix(browser): bound retained webview guests across worktree switches

* fix(browser): merge duplicate imports flagged by import/no-duplicates

* fix(browser): evict guests without unmounting the worktree surface

Unmounting the surface disposed every parked terminal byte watcher for the
evicted worktree (bell/title/agent-completion facts dropped for good after
the 15s handoff TTL) and skipped the force-park scrollback capture remote
panes rely on. Destroy the guests only: hidden slots mount no BrowserPane,
so nothing resurrects a destroyed guest before the next visit, and no slot
ever unmount-detaches a live guest (STA-3228). Terminal-state vetoes and
the eviction re-render bump are no longer needed; the only veto left is a
guest an automation/mobile controller is actively driving.

* fix(browser): veto eviction for downloading pages; keep zoom; add kill switch

Main cancels a page's active downloads when its guest unregisters (tab-close
semantics), so eviction now vetoes worktrees with a page still writing a
download, tracked app-wide because download state was pane-local and hidden
panes are unmounted. Eviction is not a user close: re-remember explicit zoom
past the destroy-path forget so a revisit reasserts the user's zoom instead
of resetting same-host siblings through Chromium's partition-wide HostZoomMap.
browserGuestWorktreeRetentionBudget mirrors the terminal budget kill switch.
…yai#12233)

* fix(terminal): route remote-runtime link clicks to the system browser

Terminal link clicks classified ownership from the global
activeRuntimeEnvironmentId, which is null when runtimes are bound per
workspace, so a link clicked in a remote-hosted pane opened a local-only
Orca browser tab and never reached the host. Thread each pane's resolved
runtimeEnvironmentId into openHttpLink as sourceOwner across the OSC 8,
WebLinksAddon, and click-fallback paths.

Co-authored-by: Orca <help@stably.ai>

* fix(terminal): route link clicks based on pane ownership, not global sta

Clicking links on remote-hosted panes was routing based on global runtime state, causing unexpected reconnections. Now link routing decisions (where to open: Orca vs system browser) are based on the actual pane's owner — local, SSH connection, remote runtime, or unknown — regardless of whether any runtime is globally active. This ensures a local pane can route to Orca while another pane's remote runtime is active, and a remote pane always routes to the system browser.

---------

Co-authored-by: Orca <help@stably.ai>
…tablyai#12103) (stablyai#12204)

* fix(onboarding): run skill setup in the configured Windows runtime (stablyai#12103)

Onboarding was the one skill-setup surface that did not route its install
command through the resolved runtime. Settings, the feature-wall panels and
the Linear prompt all wrap theirs as `wsl.exe -d <distro> -- sh -c ...` and
pass a matching shell override; onboarding spawned a bare terminal and handed
it the raw `npx skills add ...`. With Node inside WSL, npx is not on the
Windows PATH, so the install failed.

The runtime resolver had a second gap behind that: it only consulted
per-project settings, and onboarding runs before any project exists. With no
project it returned undefined and fell through to the Windows host, ignoring
a global WSL default entirely. `getLocalAgentPreflightContext` already had a
no-project fallback for PATH detection; the skill-install path had none.

- extract that fallback as `getGlobalWindowsExecutionRuntimeContext` and
  rewire the existing agent-preflight branch through it so the two cannot drift
- adopt it in `useActiveProjectSkillRuntime` when no project is active. WSL
  only: a windows-host default already matches the old no-project behavior,
  and resolving it would hand skill discovery a target where it had none,
  re-triggering scans for every host-default user
- build the onboarding terminal's command for the runtime and pass its shell
  override
- register the CLI in WSL rather than on the host, so `orca` lands on the PATH
  the install actually runs on, and wrap the copied command to match

* fix(onboarding): keep skill setup runtime consistent

* test(onboarding): satisfy runtime settings contract

---------

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
…rktrees (STA-3123) (stablyai#12235)

* fix(mobile): surface worktree catalog failures instead of showing 0 worktrees (STA-3123)

A connected host whose worktree.ps request fails now shows an explicit
catalog-failure state (with the RPC error code) on the host page, and
'Worktree list unavailable' on the home host card, instead of silently
rendering as a healthy host with zero workspaces.

* fix(mobile): mark cached worktree catalogs unavailable
…#11987)

* fix(terminal): expand variables in Windows PATH

* fix(terminal): preserve expanded Windows PATH at spawn

---------

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
Adapted from stablyai#11616 commit 9f449d7 and stablyai#12120 commit 290f42a.

Co-authored-by: holdn2 <club.makersfarm@gmail.com>
… Enter-keyup newline

On Windows, the Enter-keyup synthesis path inferred the modified-Enter
chord from release-time modifier state. A plain committing Enter
(Process/229, no modifiers) followed by a rolled-over Shift for the next
doubled consonant made the keyup report shiftKey=true and synthesized a
Shift+Enter the user never chorded; a directly-sent Shift+Enter could
likewise send a second newline from its keyup once the next composition
started. Record observed Enter keydowns (code -> timeStamp) and let the
keyup synthesis run only for presses whose keydown the IME swallowed
entirely; a balancing keyup that copies the keydown timeStamp keeps the
evidence for the later physical release.

Refs stablyai#11878

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A single slot per code let a rapid second Enter press go unguarded: the
first release found a mismatched timeStamp, dropped the only entry, and
the second release then synthesized the Shift+Enter this guard exists to
prevent. Track one entry per press and drain exactly one per physical
release, so every press stays guarded until its own release. Auto-repeat
keydowns do not stack an entry, since the whole run ends in one release,
and the list is bounded so a press whose release never arrives cannot
grow it without end.

Refs stablyai#11878

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adapted from the physical Windows event trace contributed on stablyai#11878.
Backspacing away an entire Pinyin preedit ended the composition with
empty data, no textarea residue, and no input/keypress events — yet
_sendPendingComposition fell back to the last non-empty
compositionupdate data and typed its first character into the PTY.
Only trust that fallback when observed input evidence corroborates it;
a composition with no evidence in any channel was cancelled.

Fixes the macOS Pinyin regression from stablyai#11293 (stray letter left after
deleting a preedit); same fix covers IBus/fcitx Backspace cancellation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Bound exclusive host navigation to a generation-aware latest-wins
single-flight so bulk open and switch fan-out stay responsive on large
remote fleets. Add freeze repro harnesses and navigated settlement.
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
Co-authored-by: yoke233 <yoke2012@gmail.com>
brennanb2025 and others added 23 commits August 5, 2026 12:51
* fix(browser): align cookie import safeguards

* fix(browser): preserve sessions on failed cookie imports

* fix(browser): bound single-label cookie replacement

* fix(browser): preserve host-only parent cookies

* fix(build): bundle cookie scope parser
…tablyai#12614)

PR 9501 shipped real-home routing for the host system default, and the
env override that could turn it back off was never a shipped control. The
managed-account half of the shared runtime mirror has been unreachable
since: every host account routes to its own self-contained CODEX_HOME
before that code runs.

Delete the flag module and its env plumbing plus the managed branch of
syncForCurrentSelection and the six helpers only it called. The three
lanes that still use the shared mirror -- Windows, a custom CODEX_HOME,
and a hook-lane gate that reports unusable -- are untouched, as are every
legacy migration and the WSL read-back helpers.
* fix(mobile): stop native-chat send button flicker

* fix(mobile): keep composer lock rendering pure
* fix(mobile): preserve multi-image chat attachments

* fix(mobile): use preferred array syntax

* fix(mobile): harden multi-image attachment flow

* fix(mobile): retain first-send image previews
…stablyai#11782)

* fix(windows): make managed grok-hook.cmd safe when GROK_HOME is unset

Fixes stablyai#9358 and stablyai#9941.

cmd.exe expands %VAR:~n,m% at parse time. When GROK_HOME is unset (default
outside Orca terminals), the generated length/trailing-backslash guards
became a syntax error and every Grok hook event failed with exit 255.

- Skip substring work when GROK_HOME is undefined (if defined + goto)
- Replace if "%x:~-1%"=="\" (itself a quote-parser bug) with findstr
- Extract Windows script builder; add template + spawn tests

* fix(windows): harden grok-hook GROK_HOME guards and tests

Address review on stablyai#11782:
- Inject grokHome via buildWindowsAgentHookPostCommand extra form lines
  (no fragile string replace of the shared payload line)
- Spawn tests delete GROK_HOME and keep PORT/TOKEN/PANE_KEY set so the
  GROK_HOME path actually runs before curl

* fix(windows): cover Grok hook home boundaries

---------

Co-authored-by: OrcaWin <alpha-eng@stably.ai>
* fix(mobile): open host editor from root navigation

* test(mobile): update task navigation router contract
)

* fix(mobile): activate source-control diff tabs on phones

* fix(mobile): reveal legacy source control file tabs
* Add branch line total chip to source control header

Display the total lines added and removed across a branch from its fork point, measured via `git diff <mergeBase>`. Only computed when the chip is visible (request gate on merge base OID), with 500ms soft deadline to protect status latency and 15s hard timeout. Deduplicated across concurrent pollers and cached alongside line stats. Omitted on failure — always shows exact or nothing, never a partial estimate. Updates throughout the stack: native git status, relay, renderer store/API, and UI components.

* Pin branch line total to app locale

Format line counts using the app's configured locale instead of the system
locale, ensuring consistent cross-platform display and test reliability.

* test: wait for coalescer joins instead of fixed sleep

Hold the diff until the second status pass actually takes the
branch-total coalescer lease instead of using a fixed 400ms sleep.
Fixes timing-dependent flakiness on slow machines.
- Replace 'Send answer' with 'Submit' for clarity and consistency
- Update all locale translations (en, es, ja, ko, zh)
- Remove fixed button width and add whitespace-nowrap for flexible sizing
- Update component and test references
* Reorder source control to show staged changes first by default

Stages are closest to the commit action and most relevant to the
commit workflow. Merges untracked files into Changes visually while
preserving their Git area. Removes the untracked-first preset and
includes migration logic for existing user settings.

* Drop source control group order user preference

Remove the sourceControlGroupOrder setting and related UI, migrations, and persistence logic. The source control view now always displays sections in the order: staged changes, unstaged changes, untracked files.

* Reorder source control to show changes before staged

Aligns with the edit-stage-commit workflow by showing unstaged
changes (active edits) before staged changes (queued for commit).
…12646)

* Display SSH worktrees immediately using persisted metadata

Users can now see known worktrees for SSH hosts without waiting for the
provider connection to establish. Worktrees are fetched from local metadata
and displayed as non-authoritative, then merged without replacing richer
live data once the provider becomes available.

* Show SSH folder workspaces immediately via persisted metadata

Add safeguards for metadata fallback: track authoritatively removed
worktrees per host to prevent resurrection, position new rows within
the host block to avoid jumping on authoritative scan arrival, and
preserve co-owner detection status during merge. Coalesce concurrent
metadata fetches to dedupe overlapping queries.
Add a new census module that tracks pending and retained OSC sequences
across all active PTY output processors. Each processor registers a gauge
at creation and unregisters it on dispose, detach, or destroy — this
prevents retained gauges from inflating later heap high-water profiles
and allows the memory profiler to detect stalled processors as a sign of
leaks.
…isting surface (stablyai#11576)

* fix(file-explorer): sort numbered file names naturally

The File Explorer compared names with bare localeCompare, so numbered
files listed 100, 200 before 99. Hoist the numeric collator Source
Control file rows already use (stablyai#10850) into src/shared and apply it to
the local and runtime directory listings, the name-filtered view, and
Source Control directory nodes, which were inconsistent with the file
rows one line below (stablyai#11426).

* fix(file-explorer): natural sort on SSH funnels, relay, and pickers

Adversarial-review round 1 rework:
- Both readDir funnels short-circuited to the SSH filesystem provider
  before the patched sort, so SSH workspaces kept lexicographic order;
  re-sort locally after the provider returns (the remote relay may be an
  older build), and fix the relay's own comparator for relay-native
  consumers.
- sortDirEntries (shared, unit-tested) owns the directories-first +
  natural-order listing contract used by every funnel.
- compareFileNames breaks numeric-collation ties ('2' vs '02') by code
  units so sibling order stays total instead of readdir order, and pins
  the collator locale to 'en' so every host produces one order.
- The SSH folder browser and runtime server dir picker now match the
  Explorer they browse into.
- Ordering pinned by tests at the relay, source-control tree, and shared
  helper.

* fix(mobile): natural sort in the mobile file explorer

Mobile re-sorted host readDir results with bare localeCompare, undoing
the host funnel's natural order (round-2 review). Reuse the shared
comparator and pin the order in the mobile suite.

* fix(file-explorer): natural sort at the renderer choke point and remaining ties

Round-3 review: the remote-runtime RPC and paired-web routes return the
host's order verbatim, so re-sort in readFileExplorerDirectory where
every desktop route converges; pin the SSH funnel with a handler-level
test; and route Source Control path compares through compareFileNames so
numeric-collation ties share one total order with the Explorer.

* docs(file-name-sort): state the real perf baseline in the hoist comment

* refactor(source-control): drop the dead collator export; pin the test oracle locale

* fix(file-listings): cover remaining natural-sort surfaces
…ve (stablyai#12791)

`orca serve` publishes a ready graph under HEADLESS_RUNTIME_WINDOW_ID with no
BrowserWindow behind it. `shouldCreateInBackground` only degraded when the
create was renderer-backed, so any focus-requested create fell through to
getAuthoritativeWindow() and threw "No renderer window available" — leaving
`terminal create --focus` with no workaround on a remote server (stablyai#10333).

With a worktree selector and no renderer window, a background spawn is the only
usable path, so collapse the renderer-backed window check into a plain
"no window" check. That is the existing rendererBacked clause plus exactly the
missing focus case, and it drops the confusing `rendererWindow === null`
indirection (rendererWindow is already gated on rendererBacked).

Focus is not lost by the degrade: the spawned pane is still published to the
session-tab model and revealed with `activate: true`, which is how a paired
client learns about it. Mirrors the in-tree precedent in
runCreateMobileSessionTerminal.

Headed hosts are unaffected — the clause only fires when no window exists.

Co-authored-by: Jinwoo-H <Jinwoo-H@users.noreply.github.com>
…ablyai#12495)

* fix(mobile): keep the cached transcript visible while reconnecting

A manual retry closes the client and opens a fresh one, so the chat session
hook saw a new client under an unchanged identity, dropped its settled read,
and handed out an empty list — the transcript collapsed to a full-screen
spinner until the swapped client's snapshot landed.

Hold the last settled list per identity (captured post-commit) and keep
rendering it while the re-read is in flight. `transcriptLoading` still gates
consumers that decide from an empty transcript, so the launch-draft seed is
unaffected. The held list is keyed by a new `sourceIdentity` (host/workspace)
in addition to agent/session/transcript, so it can never serve another
source's messages.

Refs STA-3333.

* test(mobile): assert the whole reconnect window, not just its first frame

The re-subscribe lands a commit after the first render of the swap, so a
regression that cleared the held list there left frame 0 green and still
blanked the transcript. Verified: clearing the cache in the subscribe
cleanup now fails this test, where before only the view-toggle test caught it.

* fix(mobile): don't derive a tappable ask card from the held transcript

The cache this PR adds keeps the previous list rendered while a swapped
client re-reads. useMobileNativeChatPrompts was the one consumer reading
`messages` without honouring `transcriptLoading`, so an ask answered on
the terminal resurrected as a live, tappable card during that window.

Gating on `transcriptLoading` is exactly base behaviour: `setRead` only
ever stores 'ready'/'error', so status==='loading' implied an empty list
before this PR. The live `askFromStatus` path is untouched.

* chore: keep merge formatting scoped
When the binary is launched as `… serve --port …` without the CLI rewrite
that injects `--serve`, normalize argv so isServeMode, headless GPU flags,
and serve option parsing all engage.

Preserves existing `--serve*` flag behavior for the CLI-spawned path.

Fixes stablyai#12677
Parse bare `serve` as the first positional token after flags/values so an
option value named `serve` cannot enable headless mode.

Addresses CodeRabbit on stablyai#12818.
@innocarpe
innocarpe force-pushed the fix/serve-cli-form-is-serve-mode branch from 0b03783 to 09a6891 Compare August 6, 2026 01:59
@innocarpe

Copy link
Copy Markdown
Owner Author

Sync update (09a6891b1f)

Address CodeRabbit: bare serve only matches the first CLI positional subcommand (not option values). @coderabbitai please re-review

Rewriting argv before maybeRedirectAppImageCliLaunch replaced the `serve`
positional with `--serve`, so the redirect's command-name lookup saw a port
number and bailed — dropping AppImage serve launches out of the CLI path.

Also translate `--port=6768` (the CLI accepts it, getServeOptions only reads
the next token) and the mixed `--serve --port` form, so a security-shaped flag
like `--no-pairing` can no longer read as accepted while pairing stays on.
Map lookups replace `in` on object literals, which turned a stray `serve
toString` positional into a function spliced onto argv.
second-instance: shouldActivateDesktopForSecondInstance matched only `--serve`,
so a duplicate `<binary> serve --port …` — the ExecStart shape documented in
docs/reference/headless-linux-server.md — promoted the live headless server to a
desktop window, un-fixing stablyai#11935 on exactly the launch shape this PR legitimizes.

findServeSubcommandIndex consumed a flag's value unconditionally while the
rewrite consumed it only when the next token was not flag-shaped. The two could
disagree and swallow the `serve` token, leaving `--serve` uninjected: stablyai#12677
again in a new shape (`--port --port serve`, `--port -- serve`). Both scans now
share one definition of value consumption.

`<binary> serve --help` / `serve help` bound a network-exposed runtime server
with pairing on and printed nothing; the AppImage redirect already routes those
three tokens to the CLI, so refuse them here too.

`--no-pairing=false` translated to `--serve-no-pairing` with the value dropped,
disabling pairing for an operator who asked for the opposite. The CLI reads its
serve booleans as `flags.get(name) === true`, so a boolean is now translated only
in its bare form and the `=` form rides through as the CLI treats it.

Tests: spec-derived parity between src/cli/specs/serve.ts and the rewrite,
covering both ends of the contract (serveOrcaApp and getServeOptions); a
source-text lock on the index.ts redirect/rewrite ordering, which reverted
silently green before; an exhaustive self-consistency property test; and the
real GUI launch argv shapes that must never enter serve mode.
@innocarpe

Copy link
Copy Markdown
Owner Author

Closing portfolio mirror: upstream stablyai#12818 merged into stablyai/orca.

(Do not merge this into fork main — exhibition track only.)

@innocarpe innocarpe closed this Aug 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.