style: format codebase (#16935)

* style: format codebase

* style: format codebase

* refactor: extract skill install dialog footer and content

Extract footer and content sections from SkillInstallDialog and
SkillInstallManagementDialog into separate components for improved
maintainability and clarity of component responsibilities.
This commit is contained in:
Jinjing
2026-08-28 00:59:21 -07:00
committed by GitHub
parent 59515beb70
commit c4b39295c1
52 changed files with 842 additions and 605 deletions
-1
View File
@@ -112,7 +112,6 @@ All stable kinds (`patch`, `minor`, `major`) are computed off the latest _stable
The scheduled 2x/day RC cron in [`release-rc.yml`](../../actions/workflows/release-rc.yml) is independent and continues to run automatically from `main`.
## Release Channels
The public Homebrew cask tracks stable desktop releases:
-1
View File
@@ -308,7 +308,6 @@ jobs:
echo "tag=$TAG" >>"$GITHUB_OUTPUT"
echo "notes_file=$notes_file" >>"$GITHUB_OUTPUT"
- name: Publish daily macOS artifacts
if: steps.freshness.outputs.should_build == 'true'
uses: nick-fields/retry@v4
-1
View File
@@ -290,7 +290,6 @@ jobs:
including back to Stable, works in-app from there."
echo "tag=$TAG" >>"$GITHUB_OUTPUT"
- name: Publish hourly macOS artifacts
if: steps.freshness.outputs.should_build == 'true'
uses: nick-fields/retry@v4
+1 -2
View File
@@ -34,8 +34,7 @@ jobs:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
GOOGLE_GENERATIVE_AI_API_KEY:
${{ secrets.GOOGLE_GENERATIVE_AI_API_KEY }}
GOOGLE_GENERATIVE_AI_API_KEY: ${{ secrets.GOOGLE_GENERATIVE_AI_API_KEY }}
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
XAI_API_KEY: ${{ secrets.XAI_API_KEY }}
DEEPSEEK_API_KEY: ${{ secrets.DEEPSEEK_API_KEY }}
+5 -2
View File
@@ -7,13 +7,15 @@ All UI work — layout, color, typography, spacing, component selection, UX beha
Use the `$electron` skill and Playwright CDP for rendered Orca UI checks. Do not use computer-use for Orca UI validation.
# Style
## Reuse Before Reimplementing
Before writing new logic at any scale — a function, component, IPC channel, state store, or whole subsystem/flow — check whether an existing implementation already does the job (or nearly does). Extend or generalize it instead of building a parallel version; only write from scratch when nothing fits. Keep the check proportionate: a quick search for trivial code, a real one before building anything substantial.
## Concise/Brief Non-obvious Comments ONLY
* DO NOT: be verbose, explain the obvious, walk through the code ("WHY not HOW")
* BE CONCISE. 1 LINE if possible
- DO NOT: be verbose, explain the obvious, walk through the code ("WHY not HOW")
- BE CONCISE. 1 LINE if possible
## Lint Rules: Do Not Disable Max Lines
@@ -32,6 +34,7 @@ Never use vague names like `helpers`, `utils`, `common`, `misc`, or `shared-stuf
- **Lint**: `oxlint`, or `pnpm run check:code-quality:changed` for changed files (full `pnpm lint` is slow); format with `pnpm format`
# Considerations
## Worktree Safety
Always use the primary working directory (the worktree) for all file reads and edits. Never follow absolute paths from subagent results that point to the main repo.
+1
View File
@@ -262,6 +262,7 @@ Want to contribute or run locally? See our [CONTRIBUTING.md](.github/CONTRIBUTIN
</p>
## Signed Builds
Windows code signing sponored/provided by [SignPath.io](https://signpath.io), certificate by [SignPath Foundation](https://signpath.org).
## License
+2 -6
View File
@@ -2148,9 +2148,7 @@
},
{
"file": "src/cli/runtime/serve-signal-exit-diagnostic.test.ts",
"assertions": [
"late update handoff failure cannot rearm termination after child exit"
]
"assertions": ["late update handoff failure cannot rearm termination after child exit"]
},
{
"file": "src/main/serve-update-handoff.test.ts",
@@ -16840,9 +16838,7 @@
},
{
"file": "src/main/browser/agent-browser-bridge-tab-routing.test.ts",
"assertions": [
"closing a tab retires the exact named agent-browser session"
]
"assertions": ["closing a tab retires the exact named agent-browser session"]
},
{
"file": "src/main/startup/serve-signal-handlers.test.ts",
+10 -6
View File
@@ -132,12 +132,16 @@ function compileNodePty(dir) {
return built
}
console.log('[orcad-prebuilds] compiling node-pty from patched source ...')
const result = spawnSync(process.platform === 'win32' ? 'npx.cmd' : 'npx', ['node-gyp', 'rebuild'], {
cwd: dir,
stdio: 'inherit',
env: process.env,
windowsHide: true
})
const result = spawnSync(
process.platform === 'win32' ? 'npx.cmd' : 'npx',
['node-gyp', 'rebuild'],
{
cwd: dir,
stdio: 'inherit',
env: process.env,
windowsHide: true
}
)
if (result.status !== 0) {
throw new Error(`[orcad-prebuilds] node-gyp rebuild failed (status ${result.status})`)
}
@@ -11,7 +11,8 @@ import {
slotName
} from './build-orcad-prebuilds.mjs'
const PATCHED_BINDING_GYP = "'ldflags': ['-Wl,--no-as-needed,-l:libutil.so.1,-l:libpthread.so.0,--as-needed']"
const PATCHED_BINDING_GYP =
"'ldflags': ['-Wl,--no-as-needed,-l:libutil.so.1,-l:libpthread.so.0,--as-needed']"
const PATCHED_PTY_CC = '__asm__(".symver openpty,openpty@" ORCA_GLIBC_COMPAT_VERSION);'
const dirs = []
@@ -107,8 +108,9 @@ describe('mergeManifest', () => {
it('does not duplicate a slot rebuilt twice', () => {
const once = mergeManifest(null, { slot: 'darwin-arm64', version: '1.1.0', nodeAbi: '127' })
expect(mergeManifest(once, { slot: 'darwin-arm64', version: '1.1.0', nodeAbi: '127' }).slots)
.toEqual(['darwin-arm64'])
expect(
mergeManifest(once, { slot: 'darwin-arm64', version: '1.1.0', nodeAbi: '127' }).slots
).toEqual(['darwin-arm64'])
})
})
+1 -5
View File
@@ -1,10 +1,6 @@
'use strict'
const NODE_PTY_JOB_EXPORTS = [
'listJobProcessIds',
'terminateJob',
'assignCurrentProcessToJob'
]
const NODE_PTY_JOB_EXPORTS = ['listJobProcessIds', 'terminateJob', 'assignCurrentProcessToJob']
function assertNodePtyJobOwnership({ nativeName, native, platform = process.platform }) {
if (platform !== 'win32' || nativeName !== 'conpty') {
+14 -14
View File
@@ -137,20 +137,20 @@ Browse `src/renderer/src/components/ui/` for the full list. Most wrap a Radix UI
When a control has multiple plausible primitives, use this fork:
| You want… | Reach for | Don't use |
| ------------------------------------------------------------ | -------------------------------------------------------------------- | ------------------------------------- |
| Hover-only label on an icon-only button | `Tooltip` | `HoverCard` (too heavy), title attr |
| Hover preview of richer content (avatar + summary) | `HoverCard` | `Tooltip` (no rich content) |
| Click-revealed menu with actions | `DropdownMenu` | `Popover` with hand-rolled list |
| Right-click contextual actions | `ContextMenu` | `DropdownMenu` (different invocation) |
| Click-revealed surface with arbitrary content (form, picker) | `Popover` | `Dialog` (it traps focus and dims) |
| Modal that demands a decision before you continue | `Dialog` | `Popover`, inline overlay |
| Drawer / panel sliding in from an edge | `Sheet` | `Dialog` centered |
| Single choice from a known list | `Select` | Custom listbox |
| Single choice with search / fuzzy filtering | `Command` inside `Popover` | `Select` (no search) |
| Multi-select with search | `repo-multi-combobox` (mirror its pattern) | Roll a new one |
| Transient confirmation ("Saved", "Copied") | `sonner` toast | `Dialog`, inline banner |
| Persistent inline status ("3 errors") | inline text + `Badge` | toast (toasts disappear) |
| You want… | Reach for | Don't use |
| ------------------------------------------------------------ | ------------------------------------------ | ------------------------------------- |
| Hover-only label on an icon-only button | `Tooltip` | `HoverCard` (too heavy), title attr |
| Hover preview of richer content (avatar + summary) | `HoverCard` | `Tooltip` (no rich content) |
| Click-revealed menu with actions | `DropdownMenu` | `Popover` with hand-rolled list |
| Right-click contextual actions | `ContextMenu` | `DropdownMenu` (different invocation) |
| Click-revealed surface with arbitrary content (form, picker) | `Popover` | `Dialog` (it traps focus and dims) |
| Modal that demands a decision before you continue | `Dialog` | `Popover`, inline overlay |
| Drawer / panel sliding in from an edge | `Sheet` | `Dialog` centered |
| Single choice from a known list | `Select` | Custom listbox |
| Single choice with search / fuzzy filtering | `Command` inside `Popover` | `Select` (no search) |
| Multi-select with search | `repo-multi-combobox` (mirror its pattern) | Roll a new one |
| Transient confirmation ("Saved", "Copied") | `sonner` toast | `Dialog`, inline banner |
| Persistent inline status ("3 errors") | inline text + `Badge` | toast (toasts disappear) |
If you find yourself styling around a primitive (`<Popover>` to act like a `<Dialog>`, or vice versa), stop and reconsider — the focus-management semantics differ and a future contributor will be misled by the mismatch.
+1 -1
View File
@@ -838,7 +838,7 @@ refuse to run there and print the command to run on the machine you want.
- Clients cannot connect: make sure `--pairing-address` is an address reachable
from the client, and make sure firewalls allow the selected `--port`.
- Journal shows `Another Orca instance is already running for this userData
profile` and the unit exits `3`: another process already owns the profile, so
profile` and the unit exits `3`: another process already owns the profile, so
`RestartPreventExitStatus=3` leaves the unit `failed` on purpose. Find the
owner with `systemctl status orca-serve` and `pgrep -af orca`. Stop it (or
keep it and leave the unit down), then run
+5 -5
View File
@@ -25,11 +25,11 @@ broke launch on Ubuntu 20.04 ([#9902](https://github.com/stablyai/orca/issues/99
The specific trap is glibc's 2.322.34 "libpthread/libutil merge", which moved
several long-stable functions into libc under brand-new symbol versions:
| Symbol | New version | node-pty use |
| ----------------- | ------------- | ----------------------- |
| `pthread_sigmask` | `GLIBC_2.32` | reset child signal mask |
| `openpty` | `GLIBC_2.34` | allocate the pty |
| `forkpty` | `GLIBC_2.34` | fork the shell |
| Symbol | New version | node-pty use |
| ----------------- | ------------ | ----------------------- |
| `pthread_sigmask` | `GLIBC_2.32` | reset child signal mask |
| `openpty` | `GLIBC_2.34` | allocate the pty |
| `forkpty` | `GLIBC_2.34` | fork the shell |
Electron itself (glibc 2.25) and the other bundled native modules
(`sherpa-onnx`, `@parcel/watcher`, both prebuilt on old glibc) stay well under
+20 -19
View File
@@ -10,12 +10,12 @@ Design background: `docs/design/shipping-orcad.html` §00c and §04.
A deployment is **orcad** plus **the terminal daemon**.
| | orcad | terminal daemon |
| --- | --- | --- |
| Started by | the supervisor | orcad, detached |
| Owns | RPC, git, worktrees, persistence | every local PTY |
| Lifetime | one supervised run | **outlives orcad** |
| Endpoint | `ws://<bind>:<port>` | `<data-root>/daemon/daemon-v<N>.sock` |
| | orcad | terminal daemon |
| ---------- | -------------------------------- | ------------------------------------- |
| Started by | the supervisor | orcad, detached |
| Owns | RPC, git, worktrees, persistence | every local PTY |
| Lifetime | one supervised run | **outlives orcad** |
| Endpoint | `ws://<bind>:<port>` | `<data-root>/daemon/daemon-v<N>.sock` |
The daemon outliving orcad is the property the whole peer model is recommended for
(`docs/reference/ssh-execution-boundary.md`): daemon-backed PTYs stay `live` across a runtime
@@ -52,13 +52,13 @@ The data root is `$ORCA_USER_DATA`, else `$XDG_DATA_HOME/Orca`, else `~/.orca`.
Before the profile index or the store is touched, orcad takes `<data-root>/orcad.lock`.
It refuses to start when:
| Code | Meaning |
| --- | --- |
| `orcad_data_root_wrong_owner` | the root is owned by another uid (POSIX) |
| `orcad_data_root_shared` | the root is group/world accessible and could not be tightened |
| `orcad_instance_lock_held` | another live orcad owns this root |
| `orcad_instance_lock_foreign_identity` | the lock belongs to a different identity |
| `orcad_data_root_unusable` | the root cannot be created, stat'd or written |
| Code | Meaning |
| -------------------------------------- | ------------------------------------------------------------- |
| `orcad_data_root_wrong_owner` | the root is owned by another uid (POSIX) |
| `orcad_data_root_shared` | the root is group/world accessible and could not be tightened |
| `orcad_instance_lock_held` | another live orcad owns this root |
| `orcad_instance_lock_foreign_identity` | the lock belongs to a different identity |
| `orcad_data_root_unusable` | the root cannot be created, stat'd or written |
A root that is merely too permissive and that we own is tightened to `0700` rather than
refused — orcad stores credentials there unsealed (no OS keyring on this host), so the goal
@@ -91,14 +91,15 @@ An external supervisor (systemd, launchd, a process manager). orcad conforms to
and exits 1, so the failure stays attributable instead of arriving as an unlogged kill.
- **Exit codes.**
| Code | Meaning | Supervisor should |
| --- | --- | --- |
| 0 | clean shutdown | restart per policy |
| 1 | startup or shutdown failure | restart with backoff |
| 78 | configuration fault (bind address, data root, instance lock) | **not** restart |
| Code | Meaning | Supervisor should |
| ---- | ------------------------------------------------------------ | -------------------- |
| 0 | clean shutdown | restart per policy |
| 1 | startup or shutdown failure | restart with backoff |
| 78 | configuration fault (bind address, data root, instance lock) | **not** restart |
78 is `EX_CONFIG`. Put it in systemd's `RestartPreventExitStatus`: restarting on a data
root owned by someone else is a restart-spin, not a recovery.
- **Logs.** orcad writes human-readable diagnostics to **stderr** and its readiness contract
to **stdout**; the supervisor owns capture and rotation. The daemon, being detached, writes
its own NDJSON lifecycle log to `<data-root>/logs/daemon.log` (suppressed by
@@ -110,7 +111,7 @@ An external supervisor (systemd, launchd, a process manager). orcad conforms to
- **Launch.** Forked detached from `daemon-entry.js` beside `orcad.js`, with its own PID
record, token and socket under `<data-root>/daemon`.
- **Adoption before spawn.** A daemon already answering the endpoint is adopted, not
replaced, unless it is unhealthy, foreign, or built from a superseded bundle *and* owns no
replaced, unless it is unhealthy, foreign, or built from a superseded bundle _and_ owns no
live sessions. Replacing a healthy daemon kills its PTYs, so code freshness always defers
to live work.
- **Restart.** The adapter respawns the daemon on death, transparently to callers.
+2 -2
View File
@@ -113,7 +113,7 @@ getting that wrong turns a skew into a false "nothing is blocked".
unverifiable, the pane was unreadable, or the agent probe did not answer in time.
A new client against an old host sees the field absent, which is why absence must read as
*unknown* and never as *not waiting*. Collapsing absent into `null` at any hop — including a
_unknown_ and never as _not waiting_. Collapsing absent into `null` at any hop — including a
convenience `?? null` in an RPC handler — makes an old or unreachable peer indistinguishable
from a healthy idle worker, which is the exact failure the field exists to remove.
@@ -180,7 +180,7 @@ has to be there before the first snapshot is interpreted, which is earlier than
renderer could wait on. Every other viewer — a second desktop, the web client, which installs no
page renderer at all, the dashboard pop-out, which is deliberately left unstamped — keeps tracking
the host, which is the only reason a mirrored viewer shows anything but its first snapshot
forever. Improving what a *second* client sees still means fixing the publish, not the carve-out;
forever. Improving what a _second_ client sees still means fixing the publish, not the carve-out;
the carve-out no longer stands in the way of it.
The two failure fields above are deliberately left on the looser `placement?.kind !== 'client'`
+19 -18
View File
@@ -28,11 +28,11 @@ Three corrections to the first draft:
verified. The ssh2 proxy-spawn at `:697` is effectively unreachable. Good news for migration, and
the first draft's motivating example was simply wrong.
- **Agent forwarding was overstated.** `agentForward` is gated on the user's `ForwardAgent yes`
(`ssh-connection-utils.ts:203-205`). `config.agent` is always set, but that is agent *auth*, whose
(`ssh-connection-utils.ts:203-205`). `config.agent` is always set, but that is agent _auth_, whose
signatures bind the session id and cannot be replayed onward. The risk applies to users who opted
into `ForwardAgent`, not everyone.
- **Credential theft was understated, and the relay claim was backwards.** `isAgentFallbackError`
treats *any* auth error as agent fallback (`ssh-connection-utils.ts:59-61`), so a MITM that rejects
treats _any_ auth error as agent fallback (`ssh-connection-utils.ts:59-61`), so a MITM that rejects
publickey walks the user to the password prompt (`ssh-connection.ts:844`) and the private-key
**passphrase** prompt (`:834`), and `cachedPassword` is replayed without prompting on every
reconnect (`:709`). Meanwhile the relay upload matters less than assumed — the attacker already
@@ -65,7 +65,7 @@ Two consequences to own rather than discover:
There is no `ssh`-only way out of this: `-F /dev/null` does NOT invert the exclusion, it reports
built-in defaults, so a probe built on it looks permissive on every machine. Verified against
OpenSSH 10.2p1. So the file is read directly, answering a deliberately weaker question — *could*
OpenSSH 10.2p1. So the file is read directly, answering a deliberately weaker question — _could_
the site config be restricting host keys — where anything ambiguous (unreadable, an unresolvable
`Include`, the directive present at all) keeps the refusal. Only a site config that demonstrably
says nothing about host keys clears it, which is what stops the rule punishing every devcontainer,
@@ -130,16 +130,16 @@ connect, training them to dismiss the one warning that matters.
> **Corrected against a live client.** The premise above is wrong about OpenSSH, though the
> conclusion survives. `check_key_in_hostkeys` is not type-scoped at all: ANY non-marker entry for
> the host that is not byte-equal produces `HOST_CHANGED`. Verified on 127.0.0.1:2223 — known_hosts
> the host that is not byte-equal produces `HOST_CHANGED`. Verified on 127.0.0.1:2223 — `known_hosts`
> holding only `ssh-rsa` against an ed25519-only server prints `IDENTIFICATION HAS CHANGED` and
> refuses. So ssh does not avoid the false alarm by scoping; it avoids the *situation* via
> refuses. So ssh does not avoid the false alarm by scoping; it avoids the _situation_ via
> `order_hostkeyalgs`, and hard-fails when the situation arises anyway. Our split into `mismatch`
> and `unknown-type-known-host` therefore only chooses the wording — both refuse, which is ssh's
> action. What the ordering below buys us is what it buys ssh: the situation mostly never arises.
**But scoping alone is a downgrade vector, and this is the correction that most changes the design.**
OpenSSH is safe here only because `order_hostkeyalgs()` reorders the client's proposed host-key
algorithms to put the types already in `known_hosts` first, and RFC 4253 gives the *client's* order
algorithms to put the types already in `known_hosts` first, and RFC 4253 gives the _client's_ order
priority — so a server cannot choose a type the client deprioritised. ssh2 negotiates ed25519 first
regardless. An attacker who cannot forge the RSA key on file simply presents ed25519 and receives a
friendly first-contact prompt instead of a hard failure.
@@ -147,7 +147,7 @@ friendly first-contact prompt instead of a hard failure.
Therefore: **set ssh2's `algorithms.serverHostKey` to lead with the key types already known for that
host.** Type scoping without algorithm ordering is not a safe design.
And when the presented type is unknown *while other types are known for this host*, that is
And when the presented type is unknown _while other types are known for this host_, that is
`unknown-type-known-host` — never a plain TOFU prompt. It must say we already hold a different key
for this host.
@@ -193,7 +193,7 @@ means nothing is known.
### D5. Recovery must not live in the failure dialog
A "forget this host key" button *in* the mismatch dialog is D4's rejected "trust anyway" with one
A "forget this host key" button _in_ the mismatch dialog is D4's rejected "trust anyway" with one
extra click. Recovery lives in target settings: a separate, deliberate surface, no auto-retry, and it
shows the stored fingerprint so the user is choosing knowingly.
@@ -201,7 +201,7 @@ Offer it only when **our** store is what disagreed; when `known_hosts` disagrees
record cannot unblock the connect. Messages, written to avoid naming internals:
> **Ours disagreed** — "The host key for `build-01` changed since you last connected from Orca. If you
> rebuilt or reprovisioned this machine, this is expected." → *Forget the saved key* / *Cancel*
> rebuilt or reprovisioned this machine, this is expected." → _Forget the saved key_ / _Cancel_
> **`known_hosts` disagreed** — "The host key for `build-01` does not match the entry in
> `~/.ssh/known_hosts`. `ssh` and `git` will refuse this host too. Run `ssh-keygen -R build-01`." →
@@ -239,7 +239,7 @@ machines, two targets can name one machine, and a re-created target must not los
The store is a **dedicated file**, not the main persistence blob (`persistence.ts:7088`): a settings
restore or rollback must not silently reset trust. Accept and mismatch events are logged.
`hostKeyFingerprint` is now security-relevant *and* wire-relevant — it is an isolation namespace sent
`hostKeyFingerprint` is now security-relevant _and_ wire-relevant — it is an isolation namespace sent
to the host (`ssh-relay-session.ts:1298`, `managed-hook-owner-identity.ts:187`). It is `undefined` on
the system transport, so **no trust logic may key off it**, and its format must not change (see
Traps).
@@ -247,11 +247,11 @@ Traps).
## Phasing — ship the defence before the dialog
Review made the case that the riskiest part of this change is not the security model but the modal.
Startup restore fires eager connects for *all* previously-active targets in parallel (`App.tsx:1041`)
Startup restore fires eager connects for _all_ previously-active targets in parallel (`App.tsx:1041`)
with a 15s timeout, while a prompt would live 120s — N unknown hosts means N stacked dialogs
outliving the timeout that already deferred them. Runtime-owned ephemeral VMs
(`ephemeral-vm-runtime-ssh.ts:31`) dial a freshly provisioned host with a brand-new key on every
launch. Paired-web connects run on the *host desktop* (`runtime/rpc/methods/ssh.ts:32`), so the
launch. Paired-web connects run on the _host desktop_ (`runtime/rpc/methods/ssh.ts:32`), so the
dialog would open on someone else's screen while the web user watches a spinner.
**Phase 1 — no new modal.** Consult `known_hosts` + our store. `match` connects. `unknown` persists
@@ -301,10 +301,11 @@ Each of these makes the fix silently do nothing. All confirmed in our tree.
## Scope
**In scope, corrected:** IPv6 literals and `[host]:port` bracket parsing. Review was right that this
is a *parser* requirement, not a scope call — getting it wrong means hosts `ssh` knows come back
is a _parser_ requirement, not a scope call — getting it wrong means hosts `ssh` knows come back
`unknown`, which is the prompt-training harm D3 exists to avoid.
**Out of scope, with consequences stated:**
- **`CheckHostIP`** — OpenSSH defaults it off; we form candidates from the hostname only.
- **WSL** — `src/main/ssh/` has no WSL awareness; a distro's `known_hosts` is unreachable, so WSL
users get first-contact treatment for hosts they already verified.
@@ -337,7 +338,7 @@ verified by running an OpenSSH 10.2p1 client against a real `sshd` on `127.0.0.1
its verdict:
- **The bare-host fallback pass never reports a change.** With `StrictHostKeyChecking=accept-new`, a
bare line holding a *different* key, dialed on a non-default port, made ssh connect and append a
bare line holding a _different_ key, dialed on a non-default port, made ssh connect and append a
new `[127.0.0.1]:2222` line — first contact, no `IDENTIFICATION HAS CHANGED`. Reporting `mismatch`
on that pass would refuse hosts ssh connects to happily, and would have looked like the cautious
choice.
@@ -355,16 +356,16 @@ subtly wrong.**
Fixed after review:
1. **Our own store was type-downgradable.** The inline lookup filtered by key type first and could
only answer match/mismatch/unknown, so a record of a *different* type read as `unknown`. D3's
only answer match/mismatch/unknown, so a record of a _different_ type read as `unknown`. D3's
downgrade, applied to the records we create ourselves. Stored types now also feed the algorithm
ordering — without that the guard is only half present.
2. **We keyed on the Orca label.** `ssh -G` echoes its own argument back as `hostname` when no Host
block matches, so for a manual target `resolved.hostname` *is* the label — the one name D2
block matches, so for a manual target `resolved.hostname` _is_ the label — the one name D2
forbids. We consulted no entries at all.
3. **A refused key still walked the credential ladder.** ssh2 reports a denial as a generic auth
failure, so we went on to prompt for the passphrase and hand it to the host we had just refused.
Rejections are now a typed error recognised before any fallback.
4. **Fail-closed nearly became fail-always.** "No readable known_hosts" counted a *missing* file the
4. **Fail-closed nearly became fail-always.** "No readable `known_hosts`" counted a _missing_ file the
same as an unreadable one, so a profile that had never connected — everyone's first run — would
have been refused, and the suite passed only because dev machines have a `known_hosts`.
5. **Ephemeral runtimes were refused for a policy they cannot satisfy.** The carve-out sat below the
@@ -404,7 +405,7 @@ tooltip, and the terminal reconnect overlay never asked for it at all. Still ope
`getPublicSshState`, so a paired-web client always sees exactly `SSH connection unavailable`.
Pre-existing, but it makes the Phase 2 dialog message unreachable there without a change. Note the
redaction is not web-only: any target owned by a paired runtime environment is redacted, so a
*desktop* user viewing a remote-Orca-server-owned host gets the same generic string.
_desktop_ user viewing a remote-Orca-server-owned host gets the same generic string.
- **RPC fail-fast** becomes load-bearing the moment the dialog exists (see Phasing).
**Known gaps that Phase 1 accepts, listed so they are choices and not surprises:**
+16 -13
View File
@@ -15,7 +15,10 @@ since removed.
table. It wraps a Toolhelp32 snapshot from `@vscode/windows-process-tree`.
```ts
import { readWindowsProcessTable, readWindowsProcessTableFresh } from '../windows/windows-process-table'
import {
readWindowsProcessTable,
readWindowsProcessTableFresh
} from '../windows/windows-process-table'
```
- `readWindowsProcessTable()` — shared TTL cache. Use for anything periodic.
@@ -31,11 +34,11 @@ survived its own teardown (#9045).
Measured on Windows 11 with 1050 processes (p50 / p95):
| | p50 | p95 |
| --- | --- | --- |
| pid + ppid + name | 15.9 ms | 17.5 ms |
| + memory + command line | 30.6 ms | 33.7 ms |
| `Get-CimInstance` via PowerShell | 706 ms | 723 ms |
| | p50 | p95 |
| -------------------------------- | ------- | ------- |
| pid + ppid + name | 15.9 ms | 17.5 ms |
| + memory + command line | 30.6 ms | 33.7 ms |
| `Get-CimInstance` via PowerShell | 706 ms | 723 ms |
Those CIM numbers are from a 1050-process host. The scan scales with process
count: on a 1486-process Windows SSH host it measured **1.36 s** and produced
@@ -62,7 +65,7 @@ Two guards, and they work together:
the module refuses every further read until that read's callback fires.
The wedge used to be a 30 s cooldown that let one probe through per window. That
bounded the *rate* of new callbacks but not the total: a permanently wedged
bounded the _rate_ of new callbacks but not the total: a permanently wedged
reader retained one more closure every 30 s for as long as the app ran, and each
probe also blocked its caller for the full 3 s deadline first. Gating on the
outstanding read instead bounds retention at exactly one callback, and gives up
@@ -90,7 +93,7 @@ checked on a real Windows SSH host with 1486 processes:
**Installing it normally rebuilds from source, and that build fails.** The
tarball carries a `binding.gyp`, so npm runs `node-gyp rebuild` regardless of
what is already compiled inside it. On a host that *already had* MSVC Build
what is already compiled inside it. On a host that _already had_ MSVC Build
Tools 2022 installed, that build still failed:
```
@@ -103,7 +106,7 @@ would then break outright rather than degrade: `installNativeDeps` throws on
failure, and the toolchain-skip retry is gated to Linux.
**Skipping the build and using the shipped binary returns a truncated table.**
Contrary to what this file used to claim, the published 0.8.0 tarball *does*
Contrary to what this file used to claim, the published 0.8.0 tarball _does_
contain `build/Release/windows_process_tree.node` — an MSVC build directory that
looks accidentally published (`.obj` and `.tlog` files ship with it). It is
N-API, so it loads on any modern Node. But it predates our patch and still has
@@ -151,14 +154,14 @@ it as an optional relay artifact.
`config/scripts/build-windows-process-tree-relay-addon.mjs` builds it from the
source pnpm has already patched, on a Windows runner, and refuses to run if
any patch hunk is missing — the Spectre hunk fails loudly, the 1024-process
hunk fails *silently*, and the relative gyp path dies at configure on Windows.
hunk fails _silently_, and the relative gyp path dies at configure on Windows.
The source is checked rather than the install trusted. It also reads the PE
machine field of the output, because a cross-build that quietly emitted host
arch would ship a binary the target cannot load.
Windows arm64 cross-compiles from the x64 runner — verified on real hardware,
producing `IMAGE_FILE_MACHINE_ARM64` (0xaa64) against x64's 0x8664. It needs the
optional *MSVC v143 ARM64 build tools* component; without it node-gyp fails with
optional _MSVC v143 ARM64 build tools_ component; without it node-gyp fails with
`MSB8020`, which is why the addon build runs before the long packaging step.
`ORCA_REQUIRE_RELAY_NATIVE_ADDONS` is a per-arch list so a future arch can be
added best-effort before it is promoted to required.
@@ -258,7 +261,7 @@ The per-PTY job deliberately does **not** set
`JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE`. Measured on Windows 11: with that flag,
releasing the handle when the shell exits also kills whatever the user left
running, so typing `exit` in a pane reaped a `start /b` server that used to
survive. The job exists to make an *explicit* teardown exact, not to redefine
survive. The job exists to make an _explicit_ teardown exact, not to redefine
what a clean exit means.
Reaping a dead daemon's shells (#9195, #10415) is therefore a **second, nested
@@ -283,7 +286,7 @@ kill-on-close job on the app, which is exactly what the crash-survival
guarantee forbids.
Once the shell exits, node-pty drops its handle record and closes the job, so a
terminated tree reports `null` rather than `[]`. Null means *unverifiable* in
terminated tree reports `null` rather than `[]`. Null means _unverifiable_ in
the sense of [`ssh-execution-boundary.md`](./ssh-execution-boundary.md) — no job
support, not a ConPTY, or no longer tracked. It is never evidence that
processes died.
+1 -1
View File
@@ -25,7 +25,7 @@ scripts:
## Why the script declares it, not the terminal preference
`terminalWindowsShell` says which shell *interactive terminals* open in. It says nothing about the
`terminalWindowsShell` says which shell _interactive terminals_ open in. It says nothing about the
language a project's setup script is written in. Deriving the runner from it had two consequences:
- Windows users with batch-syntax setup scripts silently switched to bash on upgrade, so `copy`,
+31 -31
View File
@@ -35,7 +35,7 @@ whichever poller arrives first afterwards pays a full-fleet `git worktree list`
fan-out. The many high-frequency callers — `listTerminals` without a selector,
`showTerminal`, `getWorktreePs`, `listManagedWorktrees`,
`resolveWorktreeSelector`, orchestration authority refresh — only determine
*who* pays, not *how often*. Steady-state subprocess volume is therefore
_who_ pays, not _how often_. Steady-state subprocess volume is therefore
`repos / 30 s`, independent of poll rate, which is exactly the observed
~1 sweep / 30.5 s.
@@ -86,16 +86,16 @@ common directory without a subprocess (read `.git`; if it is a `gitdir:` file,
follow it and then its `commondir`; if `.git` is absent, treat `repoPath` as a
bare gitdir), then records:
| Input | External change it catches |
| --- | --- |
| sorted entry names of `<commonDir>/worktrees` | `worktree add`, `worktree remove`, `worktree prune` |
| existence of `repoPath` | main checkout deleted |
| `<commonDir>/packed-refs` mtime + size | a tip moved while its loose ref is packed away |
| `<commonDir>/reftable` mtime + size | a tip moved under the reftable backend |
| per checkout: `HEAD` contents | branch switch, detach (the detached oid is in HEAD itself) |
| per checkout: contents of the ref HEAD names | a plain `git commit`, `reset`, or `fetch` that moves the tip |
| per entry: `gitdir` contents | `worktree move`, `worktree repair` |
| per entry: `locked` presence | `worktree lock` / `unlock` |
| Input | External change it catches |
| -------------------------------------------------- | ------------------------------------------------------------- |
| sorted entry names of `<commonDir>/worktrees` | `worktree add`, `worktree remove`, `worktree prune` |
| existence of `repoPath` | main checkout deleted |
| `<commonDir>/packed-refs` mtime + size | a tip moved while its loose ref is packed away |
| `<commonDir>/reftable` mtime + size | a tip moved under the reftable backend |
| per checkout: `HEAD` contents | branch switch, detach (the detached oid is in HEAD itself) |
| per checkout: contents of the ref HEAD names | a plain `git commit`, `reset`, or `fetch` that moves the tip |
| per entry: `gitdir` contents | `worktree move`, `worktree repair` |
| per entry: `locked` presence | `worktree lock` / `unlock` |
| per entry: existence of the path named by `gitdir` | a worktree directory deleted with `rm -rf` (flips `prunable`) |
"per checkout" covers the main worktree and each linked worktree. Reading the
@@ -159,21 +159,21 @@ rescans. Capturing after the scan would let that mutation be masked forever.
Untouched. `invalidateWorktreeScanCacheForRepo` deletes the entry (fingerprint
included) and bumps the generation, so every event-driven path still forces a
real scan on the next read. The fingerprint only ever *extends* an entry that
real scan on the next read. The fingerprint only ever _extends_ an entry that
the TTL alone would have refreshed.
## Freshness budget
| Change | Before | After |
| --- | --- | --- |
| Orca-initiated create/remove/rename/sparse/repo edit | immediate (event) | immediate (event) |
| SSH reconnect / provider generation bump | immediate (event) | immediate (event) |
| External `worktree add/remove/move/prune/lock` | ≤ 30 s | ≤ 30 s |
| External `git checkout` / `commit` / `reset` in any worktree | ≤ 30 s | ≤ 30 s |
| External `rm -rf <worktree>` | ≤ 30 s | ≤ 30 s |
| External sparse-checkout pattern edit | ≤ 30 s | ≤ 5 min |
| Packed/reftable tip moved within one mtime tick at an equal file size | ≤ 30 s | ≤ 5 min |
| SSH / WSL repos, folder workspaces | unchanged | unchanged |
| Change | Before | After |
| --------------------------------------------------------------------- | ----------------- | ----------------- |
| Orca-initiated create/remove/rename/sparse/repo edit | immediate (event) | immediate (event) |
| SSH reconnect / provider generation bump | immediate (event) | immediate (event) |
| External `worktree add/remove/move/prune/lock` | ≤ 30 s | ≤ 30 s |
| External `git checkout` / `commit` / `reset` in any worktree | ≤ 30 s | ≤ 30 s |
| External `rm -rf <worktree>` | ≤ 30 s | ≤ 30 s |
| External sparse-checkout pattern edit | ≤ 30 s | ≤ 5 min |
| Packed/reftable tip moved within one mtime tick at an equal file size | ≤ 30 s | ≤ 5 min |
| SSH / WSL repos, folder workspaces | unchanged | unchanged |
The two regressions are bounded by the reconciliation interval and are both
changes Orca does not make itself.
@@ -185,10 +185,10 @@ thread" in the naive sense — but they are not equally free there. Measured on
macOS with a 1 ms interval sampling event-loop lag while each ran 30 times
against a repo with 20 linked worktrees:
| | wall per call | main-thread stall per call | worst single stall |
| --- | --- | --- | --- |
| `git worktree list` | 18.66 ms | 2.69 ms | 3.02 ms |
| fingerprint probe | 1.66 ms | 0.01 ms | 0.04 ms |
| | wall per call | main-thread stall per call | worst single stall |
| ------------------- | ------------- | -------------------------- | ------------------ |
| `git worktree list` | 18.66 ms | 2.69 ms | 3.02 ms |
| fingerprint probe | 1.66 ms | 0.01 ms | 0.04 ms |
`fs/promises` dispatches to libuv's threadpool, so ~99 % of the probe's latency
is off-thread. Spawning Git does not: `uv_spawn`, fd and pipe setup, and stdout
@@ -212,10 +212,10 @@ sparse-checkout probes already do.
reported steady state — 10 idle local repos, a caller polling at 1 Hz for 30
simulated minutes — and counts `git worktree list` invocations:
| | `git worktree list` per 30 min | per hour |
| --- | --- | --- |
| TTL only (before) | 600 | 1,200 |
| fingerprint gate (after) | 60 | 120 |
| | `git worktree list` per 30 min | per hour |
| ------------------------ | ------------------------------ | -------- |
| TTL only (before) | 600 | 1,200 |
| fingerprint gate (after) | 60 | 120 |
A 90 % reduction, with the remainder being the bounded reconciliation. Repos
with genuine external activity keep rescanning at the 30 s cadence because the
@@ -227,7 +227,7 @@ Extrapolating to the original trace's shape (10 repos, 3 h 27 min): 4,272
## Rejected alternatives
**Raise `WORKTREE_SCAN_CACHE_TTL_MS` to 5 min.** One line, same subprocess
reduction, but it degrades *every* external-change latency to 5 min, including
reduction, but it degrades _every_ external-change latency to 5 min, including
the common "I ran `git worktree add` in a terminal" case. The fingerprint buys
the same reduction without that regression.
+1 -1
View File
@@ -26,7 +26,7 @@ exactly the case a POSIX script uses to mean a literal dollar.
Build argv with `buildWslExecArgs()` in `src/shared/wsl-login-shell-command.ts`. A test walks the
tree and fails if the `--` form reappears.
The `--` inside `sh -s -- <path>` is a *shell* argument separator and is unrelated; leave it alone.
The `--` inside `sh -s -- <path>` is a _shell_ argument separator and is unrelated; leave it alone.
## 2. Machine-read output must be fenced
+15 -15
View File
@@ -4,24 +4,24 @@ Three layers of coverage, because each catches what the others structurally cann
## 1. Unit — runs everywhere, every PR
| Suite | Pins |
|---|---|
| `src/main/wsl/wsl-runner.test.ts` | Separator, lane selection, fencing, WSLENV, guest cwd, script interpreter, budget split, refusal on unresolved PATH |
| `src/main/wsl/wsl-guest-environment.test.ts` | Burst collapse, per-distro isolation, malformed-payload rejection, transient vs permanent, retry windows, joiner budget |
| `src/main/wsl/wsl-w1-w3-contract.test.ts` | The W1→W3 chain end to end: absolute `wsl.exe`, argv array, bounded call, no `--`, script byte-identical, WSLENV, no shell on probe, login PATH still applied |
| `src/shared/source-scan/source-tree-scan.test.ts` | The guard helpers. A guard that under-reports is worse than none |
| Suite | Pins |
| ------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `src/main/wsl/wsl-runner.test.ts` | Separator, lane selection, fencing, WSLENV, guest cwd, script interpreter, budget split, refusal on unresolved PATH |
| `src/main/wsl/wsl-guest-environment.test.ts` | Burst collapse, per-distro isolation, malformed-payload rejection, transient vs permanent, retry windows, joiner budget |
| `src/main/wsl/wsl-w1-w3-contract.test.ts` | The W1→W3 chain end to end: absolute `wsl.exe`, argv array, bounded call, no `--`, script byte-identical, WSLENV, no shell on probe, login PATH still applied |
| `src/shared/source-scan/source-tree-scan.test.ts` | The guard helpers. A guard that under-reports is worse than none |
## 2. Ratchets — the goalposts, enforced continuously
| Guard | Measures |
|---|---|
| `wsl-invocation-boundary.test.ts` | Files spawning `wsl.exe` outside the runner, plus bash-only payloads that fail to declare `shell: 'bash'` |
| `windows-console-visibility.test.ts` | Direct child-process calls missing `windowsHide` |
| `child-process-import-boundary.test.ts` | Files importing `child_process` outside the chokepoint |
| `wsl-exec-mode-separator.test.ts` | The banned `--` separator |
| `pty-descendant-termination-job-coverage.test.ts` | Every sweep passes `terminateOwnedTree` |
| Guard | Measures |
| ------------------------------------------------- | --------------------------------------------------------------------------------------------------------- |
| `wsl-invocation-boundary.test.ts` | Files spawning `wsl.exe` outside the runner, plus bash-only payloads that fail to declare `shell: 'bash'` |
| `windows-console-visibility.test.ts` | Direct child-process calls missing `windowsHide` |
| `child-process-import-boundary.test.ts` | Files importing `child_process` outside the chokepoint |
| `wsl-exec-mode-separator.test.ts` | The banned `--` separator |
| `pty-descendant-termination-job-coverage.test.ts` | Every sweep passes `terminateOwnedTree` |
Each fails on a **new** offender *and* on a **stale** entry, so the count can only fall. Verify a guard by planting a violation and watching it get named — that step has found a bug in the guard itself three times.
Each fails on a **new** offender _and_ on a **stale** entry, so the count can only fall. Verify a guard by planting a violation and watching it get named — that step has found a bug in the guard itself three times.
## 3. Real-binary — the assertions nothing else can make
@@ -48,4 +48,4 @@ Recorded rather than implied, because a guard that looks complete is worse than
### Verifying a guard change
Plant a violation and watch it fail. Every guard fix in this workstream that was verified only by reading was wrong — three consecutive attempts at an exact lexer each shipped a desync that *reduced* the offender count, which read as progress. Plant at least: a plain call, one in a template-literal-heavy file, one in a regex-heavy file, `windowsHide: false`, a ternary first argument, and a renamed import.
Plant a violation and watch it fail. Every guard fix in this workstream that was verified only by reading was wrong — three consecutive attempts at an exact lexer each shipped a desync that _reduced_ the offender count, which read as progress. Plant at least: a plain call, one in a template-literal-heavy file, one in a regex-heavy file, `windowsHide: false`, a ternary first argument, and a renamed import.
+1 -1
View File
@@ -115,7 +115,7 @@ the resulting patch is silently wrong — the failure mode is a `.mjs` that is
`forbiddenBuildScripts` in the manifest encodes this and the generator refuses
to run a build step that names one of those scripts.
The generator also builds the *unmodified* commit first and asserts that it
The generator also builds the _unmodified_ commit first and asserts that it
reproduces the published `lib/` byte for byte before it emits anything. A
toolchain or build-order problem therefore surfaces as an explicit "did not
reproduce the published bundles" error rather than as 7 MB of mystery diff.
+14 -14
View File
@@ -89,20 +89,20 @@ issues `adb shell input` events; AVD names resolve to running adb serials.
Use `--json` for agent-friendly output. Coordinates are **normalized 0..1**
(top-left origin) — never pixels; Orca converts using the live screen size.
| Goal | Command | Notes |
|----------------------------|----------------------------------------------------------------|-------|
| List devices + AVDs | `ORCA emulator devices --json` | Cross-platform; shows iOS + Android with a platform column, booted vs shutdown. |
| Single tap | `ORCA emulator tap <x> <y> --device <serial>` | Normalized 0..1. Preferred for single taps. |
| Swipe / gesture | `ORCA emulator gesture '<json>' --device <serial>` | adb approximates the path by its endpoints (start→end). |
| Type text | `ORCA emulator type "user@example.com" --device <serial>` | US ASCII; spaces handled. No newlines. |
| Hardware button | `ORCA emulator button back --device <serial>` | home, back, recents, power, volume_up, volume_down. |
| Rotate | `ORCA emulator rotate landscape_left --device <serial>` | Sets user_rotation (disables auto-rotate). |
| Install an APK | `ORCA emulator install ./app-debug.apk --reinstall --device <serial>` | `--reinstall` passes `-r`. |
| Launch an app | `ORCA emulator launch com.acme.app --activity .MainActivity --device <serial>` | Omit `--activity` to launch the default LAUNCHER activity. |
| Grant a permission | `ORCA emulator permissions grant com.acme.app android.permission.CAMERA --device <serial>` | grant / revoke / reset. |
| Accessibility tree | `ORCA emulator ax --device <serial> --json` | `uiautomator dump` parsed to a node tree. |
| Logcat (one-shot) | `ORCA emulator logcat --lines 200 --device <serial>` | Dumps recent lines; parsed to entries. |
| Raw adb shell | `ORCA emulator exec --command "getprop ro.build.version.sdk" --device <serial>` | Runs `adb -s <serial> shell <command>`. |
| Goal | Command | Notes |
| ------------------- | ------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------- |
| List devices + AVDs | `ORCA emulator devices --json` | Cross-platform; shows iOS + Android with a platform column, booted vs shutdown. |
| Single tap | `ORCA emulator tap <x> <y> --device <serial>` | Normalized 0..1. Preferred for single taps. |
| Swipe / gesture | `ORCA emulator gesture '<json>' --device <serial>` | adb approximates the path by its endpoints (start→end). |
| Type text | `ORCA emulator type "user@example.com" --device <serial>` | US ASCII; spaces handled. No newlines. |
| Hardware button | `ORCA emulator button back --device <serial>` | home, back, recents, power, volume_up, volume_down. |
| Rotate | `ORCA emulator rotate landscape_left --device <serial>` | Sets user_rotation (disables auto-rotate). |
| Install an APK | `ORCA emulator install ./app-debug.apk --reinstall --device <serial>` | `--reinstall` passes `-r`. |
| Launch an app | `ORCA emulator launch com.acme.app --activity .MainActivity --device <serial>` | Omit `--activity` to launch the default LAUNCHER activity. |
| Grant a permission | `ORCA emulator permissions grant com.acme.app android.permission.CAMERA --device <serial>` | grant / revoke / reset. |
| Accessibility tree | `ORCA emulator ax --device <serial> --json` | `uiautomator dump` parsed to a node tree. |
| Logcat (one-shot) | `ORCA emulator logcat --lines 200 --device <serial>` | Dumps recent lines; parsed to entries. |
| Raw adb shell | `ORCA emulator exec --command "getprop ro.build.version.sdk" --device <serial>` | Runs `adb -s <serial> shell <command>`. |
## Critical gotchas (teach agents)
+17 -15
View File
@@ -36,6 +36,7 @@ shell-neutral for POSIX shells, PowerShell, and cmd.exe.
- The agent should use Orca's preview pane instead of external Simulator.app or raw serve-sim URLs.
**When NOT to use**
- Android emulators → use the `orca-emulator-android` skill (same `ORCA emulator` namespace, cross-platform via adb/emulator).
- Building or installing the app itself → use `xcodebuild`, `xcrun simctl install`, `expo run:ios`, etc. (launch the app, then use `ORCA emulator` to drive it).
- In-app debugging (state, network, views) → use the app's own tools or the browser pane if it's a webview.
@@ -75,6 +76,7 @@ An active emulator "session" for the worktree is required for most commands. Use
```
Orca owns:
- Starting/stopping the serve-sim helper (via --detach or direct).
- Per-worktree "active" emulator (like active browser tab).
- Explicit targeting with `--worktree`, `--device`, `--emulator <id>`.
@@ -82,26 +84,26 @@ Orca owns:
Agents use the Orca executable chosen above (on PATH in Orca terminals) and never have to manage PIDs, state files in /tmp, or raw WS URLs themselves.
**For `pnpm dev` testing:** run `pnpm build:cli` first (rebuilds the CLI + ensures the `orca-dev` shim points at *this* worktree). Then inside the dev app use `orca-dev emulator ...` (or the direct `./config/scripts/orca-dev.mjs emulator ...` from the repo root). The orchestration preambles and dev launchers automatically select the dev command name so the CLI reaches your in-memory EmulatorBridge / runtime. Plain `orca` reaches a packaged install instead.
**For `pnpm dev` testing:** run `pnpm build:cli` first (rebuilds the CLI + ensures the `orca-dev` shim points at _this_ worktree). Then inside the dev app use `orca-dev emulator ...` (or the direct `./config/scripts/orca-dev.mjs emulator ...` from the repo root). The orchestration preambles and dev launchers automatically select the dev command name so the CLI reaches your in-memory EmulatorBridge / runtime. Plain `orca` reaches a packaged install instead.
## Common operations
Use `--json` for agent-friendly output. Commands are workspace-scoped by default (current worktree's active emulator).
| Goal | Command | Notes |
|-----------------------------|----------------------------------------------|-------|
| List available / running | `ORCA emulator list [--worktree <sel>]` | Shows Orca-managed + raw serve-sim streams. Use output for explicit --device/--emulator. |
| Attach / make active | `ORCA emulator attach "iPhone 16 Pro" [--worktree <sel>] [--focus]` | Starts helper if needed (serve-sim --detach). Sets active for unqualified commands. --focus optional (does not auto-steal UI focus by default). |
| Single tap | `ORCA emulator tap <x> <y> [--device <id>]` | Normalized 0..1 coords. **Preferred over gesture for simple taps.** |
| Multi-step gesture | `ORCA emulator gesture '<json>'` | See gestures reference (begin/move/end). Use tap for singles. |
| Type text | `ORCA emulator type "text" [--device <id>]` | US ASCII only. Supports stdin/file via exec if needed. |
| Hardware button | `ORCA emulator button home [--device <id>]` | home, swipe_home, app_switcher, lock, siri, side_button. |
| Rotate device | `ORCA emulator rotate landscape_left` | Remembers orientation for subsequent gestures. |
| Camera injection | `ORCA emulator camera com.acme.App --webcam` | Or --file, placeholder. Hot-swap with switch. May (re)launch app. |
| Permissions | `ORCA emulator permissions grant camera com.acme.App` | grant/revoke/reset/list. See full subcommand help. |
| Accessibility tree | `ORCA emulator ax [--device <id>]` | Raw serve-sim AX node tree (labels, roles, nested children, capped at 500 nodes; frames normalized 0..1 with top-left origin — tap an element at its frame center: x+width/2, y+height/2). Needs an active session. |
| Raw / advanced | `ORCA emulator exec --command "tap 0.5 0.7"` | Or "ca-debug blended on", "memory-warning", full serve-sim subcommands (no "serve-sim" prefix needed in the command string). Bridge injects active device context. |
| Stop | `ORCA emulator kill [--device <id>]` | Or let pane close / Orca quit clean up. |
| Goal | Command | Notes |
| ------------------------ | ------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| List available / running | `ORCA emulator list [--worktree <sel>]` | Shows Orca-managed + raw serve-sim streams. Use output for explicit --device/--emulator. |
| Attach / make active | `ORCA emulator attach "iPhone 16 Pro" [--worktree <sel>] [--focus]` | Starts helper if needed (serve-sim --detach). Sets active for unqualified commands. --focus optional (does not auto-steal UI focus by default). |
| Single tap | `ORCA emulator tap <x> <y> [--device <id>]` | Normalized 0..1 coords. **Preferred over gesture for simple taps.** |
| Multi-step gesture | `ORCA emulator gesture '<json>'` | See gestures reference (begin/move/end). Use tap for singles. |
| Type text | `ORCA emulator type "text" [--device <id>]` | US ASCII only. Supports stdin/file via exec if needed. |
| Hardware button | `ORCA emulator button home [--device <id>]` | home, swipe_home, app_switcher, lock, siri, side_button. |
| Rotate device | `ORCA emulator rotate landscape_left` | Remembers orientation for subsequent gestures. |
| Camera injection | `ORCA emulator camera com.acme.App --webcam` | Or --file, placeholder. Hot-swap with switch. May (re)launch app. |
| Permissions | `ORCA emulator permissions grant camera com.acme.App` | grant/revoke/reset/list. See full subcommand help. |
| Accessibility tree | `ORCA emulator ax [--device <id>]` | Raw serve-sim AX node tree (labels, roles, nested children, capped at 500 nodes; frames normalized 0..1 with top-left origin — tap an element at its frame center: x+width/2, y+height/2). Needs an active session. |
| Raw / advanced | `ORCA emulator exec --command "tap 0.5 0.7"` | Or "ca-debug blended on", "memory-warning", full serve-sim subcommands (no "serve-sim" prefix needed in the command string). Bridge injects active device context. |
| Stop | `ORCA emulator kill [--device <id>]` | Or let pane close / Orca quit clean up. |
Most support `--worktree <selector>` and explicit `--device <udid|name>` or `--emulator <id>` (from list) for targeting.
+14 -10
View File
@@ -76,7 +76,7 @@ a long time, or need the user at the keyboard. Never create an Orca workspace or
- **Coding-agent CLI + account:** which agent runs in the VM (`codex`, `claude`, …) and that the user
has an account for it — it gets logged in during the Phase-3 auth snapshot (§4).
- **Git auth:** the token source for cloning a private repo (`GH_TOKEN`/`GITHUB_TOKEN` or `gh auth
token`; §5).
token`; §5).
3. **Check prerequisites (§2)** — detect the provider CLI + auth and confirm the items above are in
place before any paid step.
4. **Scaffold scripts + state file** from §7 (worked Vercel example: §7f; SSH host: §7g; Docker SSH:
@@ -221,9 +221,9 @@ reserve stdout for the final JSON and log progress to stderr. Include a shared `
- **Local-side** (`create`/`suspend`/`resume`/`destroy` + the base-snapshot/auth scripts the user
invokes) runs **on the user's desktop**, so it must run on their OS. macOS/Linux: `#!/usr/bin/env
bash`, `set -euo pipefail`, quoted paths. **Windows:** a bare `.sh` won't run — scaffold `.ps1`/`.cmd`
bash`, `set -euo pipefail`, quoted paths. **Windows:** a bare `.sh` won't run — scaffold `.ps1`/`.cmd`
or require WSL/Git-Bash and point `orca.yaml` at the right launcher.
- **Remote-side** (commands you `exec` *inside* the Linux VM) always runs in the VM's Linux shell, so
- **Remote-side** (commands you `exec` _inside_ the Linux VM) always runs in the VM's Linux shell, so
bash is fine there regardless of the user's OS.
### 7a. Base-snapshot (`<provider>-base-snapshot.sh`) — Phase 2
@@ -303,7 +303,11 @@ There is **no `--host` flag**. `--project-root` must be an absolute directory on
keeps serving:
```json
{ "schemaVersion": 1, "pairingCode": "<orca pairing URL>", "projectRoot": "<the --project-root you passed>" }
{
"schemaVersion": 1,
"pairingCode": "<orca pairing URL>",
"projectRoot": "<the --project-root you passed>"
}
```
`pairingCode` is the pairing URL, already pointing at whatever you passed as `--pairing-address` — so set
@@ -498,7 +502,7 @@ git checkout -B "$ORCA_REPO_BRANCH" "$ORCA_REPO_REF_HEAD"
Fail if the requested schema is not `2`; do not silently fall back to the ordinary recipe shape.
**Networking → which `target` fields to set** (how *your desktop* reaches the box — there is no
**Networking → which `target` fields to set** (how _your desktop_ reaches the box — there is no
`orca serve` URL in SSH mode):
- Public IP / DNS, or a Tailscale/VPN address → `host`; SSH port → `port` (usually 22).
@@ -511,7 +515,7 @@ Fail if the requested schema is not `2`; do not silently fall back to the ordina
reconnect grace window.
**Toolchain & agent auth on a persistent (no-snapshot) host — do this ONCE, by hand, before wiring the
recipe** (there's no base image to bake; the host *is* the base). Run the §7f Phase-2 install steps and
recipe** (there's no base image to bake; the host _is_ the base). Run the §7f Phase-2 install steps and
the §7f Phase-3 `<agent> login --device-auth` **directly over SSH on the host** (interactive, e.g.
`ssh -t user@host '<agent> login --device-auth'`). After that the host stays ready across workspaces.
@@ -615,7 +619,7 @@ $ErrorActionPreference = 'Stop'
# progress/errors → Write-Error / the error stream, never stdout.
```
The remote-side commands you run *inside* the Linux VM stay bash regardless of the desktop OS.
The remote-side commands you run _inside_ the Linux VM stay bash regardless of the desktop OS.
---
@@ -702,10 +706,10 @@ each stage so you can self-diagnose without asking the user to relay logs:
```json
{
"ok": false,
"checks": [ { "id": "recipe.provision", "status": "fail", "message": "…" } ],
"checks": [{ "id": "recipe.provision", "status": "fail", "message": "…" }],
"provisionTranscript": {
"provision": { "exitCode": 0, "signal": null, "stdout": "…", "stderr": "…", "parseError": "…" },
"destroy": { "exitCode": 0, "signal": null, "stdout": "…", "stderr": "…" }
"destroy": { "exitCode": 0, "signal": null, "stdout": "…", "stderr": "…" }
}
}
```
@@ -743,7 +747,7 @@ startup-only `docker run` before the full clone/install path.
- **Agent verified as "not logged in" despite a good login.** `codex login status` (and similar) print
"Logged in …" to **stderr**; an stdout-only `grep` misses it. Prefer the status **exit code**; if you
grep, fold stderr first (`status 2>&1 | grep …`) and match the exact success line — not `grep -qi
'logged in'`, which also matches "not logged in".
'logged in'`, which also matches "not logged in".
- **Headless agent login hangs.** Plain OAuth `login` starts a loopback callback server on a VM/container
port the host browser can't reach. Use the **device-auth** flow (`login --device-auth`) — it prints a
URL + code the user opens on the host.
File diff suppressed because one or more lines are too long
@@ -8,14 +8,19 @@ import { mkdtempSync, rmSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
const { spawnMock, isPwshAvailableMock, resolveAgentForegroundProcessMock, readConptyMock, jobReadableMock } =
vi.hoisted(() => ({
spawnMock: vi.fn(),
isPwshAvailableMock: vi.fn(),
resolveAgentForegroundProcessMock: vi.fn(),
readConptyMock: vi.fn(),
jobReadableMock: vi.fn()
}))
const {
spawnMock,
isPwshAvailableMock,
resolveAgentForegroundProcessMock,
readConptyMock,
jobReadableMock
} = vi.hoisted(() => ({
spawnMock: vi.fn(),
isPwshAvailableMock: vi.fn(),
resolveAgentForegroundProcessMock: vi.fn(),
readConptyMock: vi.fn(),
jobReadableMock: vi.fn()
}))
vi.mock('node-pty', () => ({ spawn: spawnMock }))
vi.mock('../pwsh', () => ({ isPwshAvailable: isPwshAvailableMock }))
+2 -6
View File
@@ -117,11 +117,7 @@ export function parseUnmetGlibcVersion(loaderError: string): string | null {
}
/** `NODE_MODULE_VERSION 115 ... requires NODE_MODULE_VERSION 127` -> { built: '115', host: '127' }. */
export function parseNodeAbiMismatch(
loaderError: string
): { built: string; host: string } | null {
const match = loaderError.match(
/NODE_MODULE_VERSION\s+(\d+)\D+NODE_MODULE_VERSION\s+(\d+)/
)
export function parseNodeAbiMismatch(loaderError: string): { built: string; host: string } | null {
const match = loaderError.match(/NODE_MODULE_VERSION\s+(\d+)\D+NODE_MODULE_VERSION\s+(\d+)/)
return match ? { built: match[1], host: match[2] } : null
}
+9 -2
View File
@@ -27,7 +27,12 @@ export type PrebuiltSlotManifest = {
export type PrebuiltSlotOutcome =
| { installed: true; slot: string; spawnHelper: boolean }
| { installed: false; slot: string; why: 'no-slot' | 'no-prebuilds-dir' | 'abi-mismatch'; detail?: string }
| {
installed: false
slot: string
why: 'no-slot' | 'no-prebuilds-dir' | 'abi-mismatch'
detail?: string
}
/**
* Where a deployment's prebuilds live: beside the bundle that is running. `argv[1]` is
@@ -55,7 +60,9 @@ export function readPrebuiltSlotManifest(prebuildsDir: string): PrebuiltSlotMani
module: typeof manifest.module === 'string' ? manifest.module : 'node-pty',
version: manifest.version,
nodeAbi: manifest.nodeAbi,
slots: Array.isArray(manifest.slots) ? manifest.slots.filter((s) => typeof s === 'string') : []
slots: Array.isArray(manifest.slots)
? manifest.slots.filter((s) => typeof s === 'string')
: []
}
} catch {
return null
@@ -163,4 +163,3 @@ export function formatMissingToolchainError(
]
return lines.join('\n')
}
+1 -3
View File
@@ -169,7 +169,5 @@ export function orcadGcPinnedDirNames(
const versions = [record.active, record.previous, daemonEntryVersion ?? null].filter(
(v): v is string => typeof v === 'string' && v.length > 0
)
return [...new Set(versions)].map((version) =>
remoteInstallDirName(ORCAD_INSTALL_MODEL, version)
)
return [...new Set(versions)].map((version) => remoteInstallDirName(ORCAD_INSTALL_MODEL, version))
}
+3 -1
View File
@@ -209,7 +209,9 @@ describe('deployOrcad', () => {
const result = await deployOrcad(options())
expect(result).toMatchObject({ code: 'orcad_activation_daemon_degraded' })
expect(
vi.mocked(writeRelayFile).mock.calls.some((call) => String(call[2]).endsWith('orcad-active.json'))
vi
.mocked(writeRelayFile)
.mock.calls.some((call) => String(call[2]).endsWith('orcad-active.json'))
).toBe(false)
})
+18 -11
View File
@@ -32,10 +32,7 @@ import {
type OrcadActivationRecord,
type OrcadStateSnapshot
} from './orcad-activation-record'
import {
orcadActivationPath,
readOrcadActivationRecord
} from './orcad-activation-record-store'
import { orcadActivationPath, readOrcadActivationRecord } from './orcad-activation-record-store'
import { evaluateOrcadActivation, type OrcadActivationVerdict } from './orcad-activation-gate'
import { planOrcadUpdate, type OrcadTerminalCensus } from './orcad-update-plan'
import {
@@ -111,9 +108,11 @@ async function installOrcadBundle(
fullVersion: string,
remoteDir: string
): Promise<void> {
if (await isRemoteInstallComplete(options.conn, ORCAD_INSTALL_MODEL, remoteDir, options.host, {
signal: options.signal
})) {
if (
await isRemoteInstallComplete(options.conn, ORCAD_INSTALL_MODEL, remoteDir, options.host, {
signal: options.signal
})
) {
return
}
await acquireInstallLock(options.conn, remoteDir, options.host, { signal: options.signal })
@@ -151,7 +150,12 @@ async function captureSnapshot(
takenAt: Date
): Promise<OrcadStateSnapshot | null> {
const dirName = orcadSnapshotDirName(fullVersion, takenAt.getTime())
const snapshotDir = joinRemotePath(options.host, baseDir(options), ORCAD_STATE_SNAPSHOT_DIR, dirName)
const snapshotDir = joinRemotePath(
options.host,
baseDir(options),
ORCAD_STATE_SNAPSHOT_DIR,
dirName
)
const capture = parseOrcadSnapshotCapture(
await exec(
options,
@@ -287,9 +291,12 @@ export async function deployOrcad(options: OrcadDeployOptions): Promise<OrcadDep
record.active
)
const stopped = parseOrcadStopOutcome(
await exec(options, stopOrcadCommand(options.host, outgoingDir, {
waitSeconds: STOP_WAIT_SECONDS
}))
await exec(
options,
stopOrcadCommand(options.host, outgoingDir, {
waitSeconds: STOP_WAIT_SECONDS
})
)
)
if (!orcadStopFreedTheHost(stopped)) {
return {
+3 -1
View File
@@ -78,7 +78,9 @@ describe('orcad GC', () => {
currentDirAbsPath: '/home/u/.orca-remote/orcad-0.2.0+bb',
record: emptyOrcadActivationRecord()
})
const listCommand = mockExec.mock.calls.map((call) => String(call[1])).find((c) => c.includes('find'))
const listCommand = mockExec.mock.calls
.map((call) => String(call[1]))
.find((c) => c.includes('find'))
expect(listCommand).toContain("-name 'orcad-*'")
expect(listCommand).not.toContain("-name 'relay-*'")
})
+4 -5
View File
@@ -25,10 +25,7 @@ import type { ServeReadiness } from '../server/serve-readiness'
export const ORCAD_READINESS_FILENAME = '.orcad-readiness'
/** Stderr, including the bind-exposure line and every supervision message. */
export const ORCAD_LOG_FILENAME = 'orcad.log'
export {
ORCAD_PID_FILENAME,
OrcadRemoteLaunchUnsupportedError
} from './orcad-remote-host-support'
export { ORCAD_PID_FILENAME, OrcadRemoteLaunchUnsupportedError } from './orcad-remote-host-support'
export type OrcadLaunchSpec = {
remoteInstallDir: string
@@ -51,7 +48,9 @@ export type OrcadLaunchSpec = {
export function orcadLaunchCommand(host: RemoteHostPlatform, spec: OrcadLaunchSpec): string {
assertPosixHost(host)
const dir = shellEscape(spec.remoteInstallDir)
const readiness = shellEscape(joinRemotePath(host, spec.remoteInstallDir, ORCAD_READINESS_FILENAME))
const readiness = shellEscape(
joinRemotePath(host, spec.remoteInstallDir, ORCAD_READINESS_FILENAME)
)
const log = shellEscape(joinRemotePath(host, spec.remoteInstallDir, ORCAD_LOG_FILENAME))
const pidFile = shellEscape(joinRemotePath(host, spec.remoteInstallDir, ORCAD_PID_FILENAME))
const entry = shellEscape(joinRemotePath(host, spec.remoteInstallDir, 'orcad.js'))
+1 -4
View File
@@ -13,10 +13,7 @@ vi.mock('./ssh-relay-install-transfers', () => ({
import { execCommand } from './ssh-relay-deploy-helpers'
import { writeRelayFile } from './ssh-relay-install-transfers'
import { rollbackOrcad, type OrcadRollbackOptions } from './orcad-remote-rollback'
import {
emptyOrcadActivationRecord,
type OrcadActivationRecord
} from './orcad-activation-record'
import { emptyOrcadActivationRecord, type OrcadActivationRecord } from './orcad-activation-record'
import { getRemoteHostPlatform } from './ssh-remote-platform'
import type { SshConnection } from './ssh-connection'
+2 -8
View File
@@ -109,9 +109,7 @@ async function readStateWritesSinceActivation(
return newest === null ? null : newest >= activatedAtSeconds
}
export async function rollbackOrcad(
options: OrcadRollbackOptions
): Promise<OrcadRollbackResult> {
export async function rollbackOrcad(options: OrcadRollbackOptions): Promise<OrcadRollbackResult> {
const now = options.now ?? ((): Date => new Date())
const snapshotPresent = options.record.snapshot
? (
@@ -183,11 +181,7 @@ export async function rollbackOrcad(
}
}
const targetDir = computeRemoteInstallDir(
ORCAD_INSTALL_MODEL,
options.remoteHome,
safety.target
)
const targetDir = computeRemoteInstallDir(ORCAD_INSTALL_MODEL, options.remoteHome, safety.target)
await exec(
options,
orcadLaunchCommand(options.host, {
@@ -143,9 +143,9 @@ describe('liveness and stop commands, run for real', () => {
const exited = new Promise<NodeJS.Signals | null>((resolve) =>
child.once('exit', (_code, signal) => resolve(signal))
)
expect(parseOrcadStopOutcome(sh(stopOrcadCommand(host, versionDir, { waitSeconds: 10 })))).toBe(
'stopped'
)
expect(
parseOrcadStopOutcome(sh(stopOrcadCommand(host, versionDir, { waitSeconds: 10 })))
).toBe('stopped')
expect(await exited).toBe('SIGTERM')
expect(parseOrcadLiveness(sh(orcadLivenessProbeCommand(host, versionDir)))).toBe('DEAD')
} finally {
+3 -2
View File
@@ -211,7 +211,9 @@ async function isCandidateSafeToRemove(
if (!(await isRelayInstallLockStale(conn, lockDir, host))) {
return false
}
process.stderr.write?.(`[${model.id}] GC: lock at ${lockDir} is stale; treating as recoverable\n`)
process.stderr.write?.(
`[${model.id}] GC: lock at ${lockDir} is stale; treating as recoverable\n`
)
}
// Legacy dirs predate .install-complete; skip the sentinel and rely on the live-socket probe alone.
@@ -231,7 +233,6 @@ async function isCandidateSafeToRemove(
return !(await options.isDirLive(dir))
}
/**
* The relay's GC, bound to its own namespace and its own liveness probe (a live unix socket
* or Windows pipe inside the version dir).
+5 -1
View File
@@ -92,7 +92,11 @@ export function computeRemoteInstallDir(
pathFlavor === 'windows'
? getRemoteHostPlatform('win32-x64')
: getRemoteHostPlatform('linux-x64')
return joinRemotePath(host, remoteHome, ...remoteInstallDirSegments(model, fullVersion, pathFlavor))
return joinRemotePath(
host,
remoteHome,
...remoteInstallDirSegments(model, fullVersion, pathFlavor)
)
}
/**
@@ -522,5 +522,4 @@ describe('rich markdown Tab key handler', () => {
editor.destroy()
}
})
})
@@ -193,7 +193,10 @@ export function SkillBundleInstallFlow(props: {
} else if (operation.status !== 'ok') {
setError(
operation.status === 'reconnect-required'
? translate('auto.components.skills.install.reconnectBeforeInstalling', 'Reconnect your Orca account before installing.')
? translate(
'auto.components.skills.install.reconnectBeforeInstalling',
'Reconnect your Orca account before installing.'
)
: operation.message
)
} else {
@@ -208,7 +211,12 @@ export function SkillBundleInstallFlow(props: {
}
} catch (cause) {
console.warn('[skills] bundle install failed:', cause)
setError(translate('auto.components.skills.install.bundleVerificationFailed', 'Installation failed before Orca could verify the requested bundle.'))
setError(
translate(
'auto.components.skills.install.bundleVerificationFailed',
'Installation failed before Orca could verify the requested bundle.'
)
)
} finally {
installProgress.finish()
setBusy(false)
@@ -225,7 +233,12 @@ export function SkillBundleInstallFlow(props: {
...(environmentId === 'local' || environmentId.startsWith('ssh:') ? {} : { environmentId })
})
if (!cancelled.cancelled) {
setError(translate('auto.components.skills.install.destinationAlreadyFinished', 'The destination had already finished this installation.'))
setError(
translate(
'auto.components.skills.install.destinationAlreadyFinished',
'The destination had already finished this installation.'
)
)
}
}
@@ -1,13 +1,6 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { Download, Loader2, ShieldCheck } from 'lucide-react'
import { Button } from '@/components/ui/button'
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle
} from '@/components/ui/dialog'
import { Loader2 } from 'lucide-react'
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
import { useAppStore } from '@/store'
import type {
SkillInstallDestination,
@@ -32,6 +25,7 @@ import { useSkillInstallProgress } from './skill-install-progress-state'
import { translate } from '@/i18n/i18n'
import { resolveSkillShareForInstall } from './skill-warning-preview-gate'
import { useSkillInstallRisk } from './use-skill-install-risk'
import { SkillInstallDialogFooter } from './SkillInstallDialogFooter'
export function SkillInstallDialog({
open,
@@ -91,7 +85,12 @@ export function SkillInstallDialog({
const resolveLink = useCallback(async (value: string): Promise<void> => {
const shareId = parseSkillShareId(value)
if (!shareId) {
setError(translate('auto.components.skills.install.enterShareLink', 'Enter an Orca skill share link.'))
setError(
translate(
'auto.components.skills.install.enterShareLink',
'Enter an Orca skill share link.'
)
)
return
}
setBusy(true)
@@ -103,14 +102,22 @@ export function SkillInstallDialog({
setError(
operation.status === 'unconfigured'
? operation.message
: translate('auto.components.skills.install.shareUnavailable', 'This share is unavailable. The link may be invalid, expired, or revoked.')
: translate(
'auto.components.skills.install.shareUnavailable',
'This share is unavailable. The link may be invalid, expired, or revoked.'
)
)
return
}
setPreview({ shareId, version: operation.value.version })
} catch (cause) {
console.warn('[skills] share resolution failed:', cause)
setError(translate('auto.components.skills.install.shareUnavailable', 'This share is unavailable. The link may be invalid, expired, or revoked.'))
setError(
translate(
'auto.components.skills.install.shareUnavailable',
'This share is unavailable. The link may be invalid, expired, or revoked.'
)
)
} finally {
setBusy(false)
}
@@ -204,7 +211,10 @@ export function SkillInstallDialog({
if (operation.status !== 'ok') {
setError(
operation.status === 'reconnect-required'
? translate('auto.components.skills.install.reconnectBeforeInstalling', 'Reconnect your Orca account before installing.')
? translate(
'auto.components.skills.install.reconnectBeforeInstalling',
'Reconnect your Orca account before installing.'
)
: operation.message
)
return
@@ -236,7 +246,12 @@ export function SkillInstallDialog({
...(environmentId === 'local' || environmentId.startsWith('ssh:') ? {} : { environmentId })
})
if (!cancelled.cancelled) {
setError(translate('auto.components.skills.install.destinationAlreadyFinished', 'The destination had already finished this installation.'))
setError(
translate(
'auto.components.skills.install.destinationAlreadyFinished',
'The destination had already finished this installation.'
)
)
}
}
@@ -353,65 +368,21 @@ export function SkillInstallDialog({
{installProgress.phaseLabel}
</p>
) : null}
{!bundleVersion ? (
<DialogFooter>
<Button type="button" variant="ghost" onClick={close} disabled={busy}>
{translate('auto.components.skills.SkillInstallDialog.d198ec91e5', 'Close')}
</Button>
{!preview && !resolvingInitialLink ? (
<Button type="button" disabled={busy || !link.trim()} onClick={() => void inspect()}>
{busy ? (
<Loader2 className="size-4 animate-spin" />
) : (
<ShieldCheck className="size-4" />
)}
{busy
? translate(
'auto.components.skills.SkillInstallReviewContent.69236de8d6',
'Checking…'
)
: translate(
'auto.components.skills.SkillInstallReviewContent.157de228b4',
'Inspect skill'
)}
</Button>
) : null}
{busy && installProgress.activeOperationId ? (
<Button type="button" variant="secondary" onClick={() => void cancelInstall()}>
{translate(
'auto.components.skills.SkillInstallDialog.05588076a9',
'Cancel installation'
)}
</Button>
) : null}
{preview &&
(!result || ['conflict', 'partial', 'failed', 'cancelled'].includes(result.status)) ? (
<Button
type="button"
disabled={busy || (scope === 'workspace' && !workspace)}
onClick={() => void install()}
className="w-32"
>
{busy ? (
<Loader2 className="size-4 animate-spin" />
) : (
<Download className="size-4" />
)}
{busy
? translate('auto.components.skills.SkillInstallDialog.241e72f9d6', 'Installing…')
: result
? translate(
'auto.components.skills.SkillInstallDialog.59c3b76cdd',
'Retry install'
)
: translate(
'auto.components.skills.SkillInstallDialog.39acb9e8f4',
'Install skill'
)}
</Button>
) : null}
</DialogFooter>
) : null}
<SkillInstallDialogFooter
activeOperationId={installProgress.activeOperationId}
busy={busy}
hasBundleVersion={Boolean(bundleVersion)}
hasPreview={Boolean(preview)}
link={link}
resolvingInitialLink={resolvingInitialLink}
result={result}
scope={scope}
workspace={workspace}
onCancelInstall={() => void cancelInstall()}
onClose={close}
onInspect={() => void inspect()}
onInstall={(discardLocal) => void install(discardLocal)}
/>
</DialogContent>
</Dialog>
)
@@ -0,0 +1,81 @@
import { Download, Loader2, ShieldCheck } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { DialogFooter } from '@/components/ui/dialog'
import type { SkillInstallResult } from '../../../../shared/skill-install-contract'
import { translate } from '@/i18n/i18n'
type SkillInstallDialogFooterProps = {
activeOperationId: string | null
busy: boolean
hasBundleVersion: boolean
hasPreview: boolean
link: string
resolvingInitialLink: boolean
result: SkillInstallResult | null
scope: 'global' | 'workspace'
workspace: string
onCancelInstall: () => void
onClose: () => void
onInspect: () => void
onInstall: (discardLocal?: boolean) => void
}
export function SkillInstallDialogFooter({
activeOperationId,
busy,
hasBundleVersion,
hasPreview,
link,
resolvingInitialLink,
result,
scope,
workspace,
onCancelInstall,
onClose,
onInspect,
onInstall
}: SkillInstallDialogFooterProps): React.JSX.Element | null {
if (hasBundleVersion) {
return null
}
return (
<DialogFooter>
<Button type="button" variant="ghost" onClick={onClose} disabled={busy}>
{translate('auto.components.skills.SkillInstallDialog.d198ec91e5', 'Close')}
</Button>
{!hasPreview && !resolvingInitialLink ? (
<Button type="button" disabled={busy || !link.trim()} onClick={onInspect}>
{busy ? <Loader2 className="size-4 animate-spin" /> : <ShieldCheck className="size-4" />}
{busy
? translate('auto.components.skills.SkillInstallReviewContent.69236de8d6', 'Checking…')
: translate(
'auto.components.skills.SkillInstallReviewContent.157de228b4',
'Inspect skill'
)}
</Button>
) : null}
{busy && activeOperationId ? (
<Button type="button" variant="secondary" onClick={onCancelInstall}>
{translate('auto.components.skills.SkillInstallDialog.05588076a9', 'Cancel installation')}
</Button>
) : null}
{hasPreview &&
(!result || ['conflict', 'partial', 'failed', 'cancelled'].includes(result.status)) ? (
<Button
type="button"
disabled={busy || (scope === 'workspace' && !workspace)}
onClick={() => onInstall()}
className="w-32"
>
{busy ? <Loader2 className="size-4 animate-spin" /> : <Download className="size-4" />}
{busy
? translate('auto.components.skills.SkillInstallDialog.241e72f9d6', 'Installing…')
: result
? translate('auto.components.skills.SkillInstallDialog.59c3b76cdd', 'Retry install')
: translate('auto.components.skills.SkillInstallDialog.39acb9e8f4', 'Install skill')}
</Button>
) : null}
</DialogFooter>
)
}
@@ -1,14 +1,5 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { Loader2 } from 'lucide-react'
import { Button } from '@/components/ui/button'
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle
} from '@/components/ui/dialog'
import { Dialog } from '@/components/ui/dialog'
import { useAppStore } from '@/store'
import type {
ManagedSkillInstall,
@@ -17,18 +8,15 @@ import type {
import type { SkillBundleInstallResult } from '../../../../shared/skill-bundle-install-contract'
import type { SkillCloudPackageDetails } from '../../../../shared/skill-cloud-contract'
import { notifyInstalledAgentSkillsChanged } from '@/hooks/useInstalledAgentSkills'
import { skillInstallResultLabel } from './skill-install-result-label'
import { skillInstallManagementCopy } from './skill-install-management-copy'
import { useSkillInstallProgress } from './skill-install-progress-state'
import { SkillManagedInstallRow } from './SkillManagedInstallRow'
import { summarizeManagedSkillRemoval } from './skill-managed-removal-summary'
import {
groupManagedSkillInstalls,
type SkillManagedInstallGroup
} from './skill-managed-install-groups'
import { SkillInstallMachineSelect } from './SkillInstallMachineSelect'
import { SkillInstallManagementStatus } from './SkillInstallManagementStatus'
import { translate } from '@/i18n/i18n'
import { SkillInstallManagementDialogContent } from './SkillInstallManagementDialogContent'
export function SkillInstallManagementDialog({
open,
@@ -91,7 +79,12 @@ export function SkillInstallManagementDialog({
return
}
console.warn('[skills] managed install listing failed:', cause)
setError(translate('auto.components.skills.install.inspectManagedFailed', 'Orca could not inspect managed installs on this machine.'))
setError(
translate(
'auto.components.skills.install.inspectManagedFailed',
'Orca could not inspect managed installs on this machine.'
)
)
} finally {
if (generation === loadGeneration.current) {
setBusy(false)
@@ -121,7 +114,10 @@ export function SkillInstallManagementDialog({
if (operation.status !== 'ok') {
setError(
operation.status === 'reconnect-required'
? translate('auto.components.skills.install.reconnectForVersionHistory', 'Reconnect your Orca account to load version history.')
? translate(
'auto.components.skills.install.reconnectForVersionHistory',
'Reconnect your Orca account to load version history.'
)
: operation.message
)
return
@@ -133,7 +129,12 @@ export function SkillInstallManagementDialog({
return
}
console.warn('[skills] package history failed:', cause)
setError(translate('auto.components.skills.install.versionHistoryUnavailable', 'Version history is unavailable for this skill.'))
setError(
translate(
'auto.components.skills.install.versionHistoryUnavailable',
'Version history is unavailable for this skill.'
)
)
} finally {
if (generation === detailGeneration.current) {
setBusy(false)
@@ -169,7 +170,12 @@ export function SkillInstallManagementDialog({
installedNames.has(skill.name)
)
if (selectedSkills.length === 0) {
setError(translate('auto.components.skills.install.bundleSkillsMissing', 'This version does not contain any of the installed bundle skills.'))
setError(
translate(
'auto.components.skills.install.bundleSkillsMissing',
'This version does not contain any of the installed bundle skills.'
)
)
return
}
const operation = await window.api.skills.installBundlePackageVersion({
@@ -194,7 +200,10 @@ export function SkillInstallManagementDialog({
if (operation.status !== 'ok') {
setError(
operation.status === 'reconnect-required'
? translate('auto.components.skills.install.reconnectBeforeVersionChange', 'Reconnect your Orca account before changing versions.')
? translate(
'auto.components.skills.install.reconnectBeforeVersionChange',
'Reconnect your Orca account before changing versions.'
)
: operation.message
)
return
@@ -220,7 +229,10 @@ export function SkillInstallManagementDialog({
if (operation.status !== 'ok') {
setError(
operation.status === 'reconnect-required'
? translate('auto.components.skills.install.reconnectBeforeVersionChange', 'Reconnect your Orca account before changing versions.')
? translate(
'auto.components.skills.install.reconnectBeforeVersionChange',
'Reconnect your Orca account before changing versions.'
)
: operation.message
)
return
@@ -234,7 +246,12 @@ export function SkillInstallManagementDialog({
}
} catch (cause) {
console.warn('[skills] version installation failed:', cause)
setError(translate('auto.components.skills.install.versionVerificationFailed', 'Orca could not verify the requested version.'))
setError(
translate(
'auto.components.skills.install.versionVerificationFailed',
'Orca could not verify the requested version.'
)
)
} finally {
installProgress.finish()
setBusy(false)
@@ -250,7 +267,12 @@ export function SkillInstallManagementDialog({
...(environmentId === 'local' || environmentId.startsWith('ssh:') ? {} : { environmentId })
})
if (!cancelled.cancelled) {
setError(translate('auto.components.skills.install.destinationAlreadyFinished', 'The destination had already finished this installation.'))
setError(
translate(
'auto.components.skills.install.destinationAlreadyFinished',
'The destination had already finished this installation.'
)
)
}
}
@@ -295,7 +317,12 @@ export function SkillInstallManagementDialog({
}
} catch (cause) {
console.warn('[skills] managed removal failed:', cause)
setError(translate('auto.components.skills.install.removeFailed', 'Orca could not safely remove this skill.'))
setError(
translate(
'auto.components.skills.install.removeFailed',
'Orca could not safely remove this skill.'
)
)
} finally {
setBusy(false)
}
@@ -312,77 +339,39 @@ export function SkillInstallManagementDialog({
onOpenChange(false)
}
const destructiveConflict =
result?.status === 'conflict' ||
Boolean(selected?.installs.some((install) => install.state === 'modified'))
return (
<Dialog open={open} onOpenChange={(next) => !next && !busy && close()}>
<DialogContent className="max-h-[calc(100vh-3rem)] overflow-x-hidden overflow-y-auto scrollbar-sleek sm:max-w-2xl [&>*]:min-w-0">
<DialogHeader>
<DialogTitle>{copy.title}</DialogTitle>
<DialogDescription>{copy.description}</DialogDescription>
</DialogHeader>
<SkillInstallMachineSelect
value={environmentId}
onChange={setEnvironmentId}
localLabel={copy.localMachine}
sshLabel={copy.ssh}
disconnectedLabel={copy.disconnected}
environments={runtimeEnvironments}
sshTargets={[...sshTargetLabels.entries()].map(([id, label]) => ({
id: `ssh:${id}`,
label,
connected: sshConnectionStates.get(id)?.status === 'connected'
}))}
/>
{busy && installs.length === 0 ? <Loader2 className="mx-auto size-5 animate-spin" /> : null}
{!busy && installs.length === 0 ? (
<p className="rounded-md border border-border p-4 text-sm text-muted-foreground">
{copy.noInstalls}
</p>
) : null}
{groups.length > 0 ? (
<ul className="divide-y divide-border rounded-md border border-border">
{groups.map((group) => (
<SkillManagedInstallRow
key={group.key}
group={group}
open={selectedKey === group.key}
details={selectedKey === group.key ? details : null}
versionId={selectedKey === group.key ? versionId : ''}
busy={busy}
confirmRemove={selectedKey === group.key && confirmRemove}
installActive={Boolean(installProgress.activeOperationId)}
editedWarning={selectedKey === group.key && destructiveConflict}
bundleResult={selectedKey === group.key ? bundleResult : null}
result={selectedKey === group.key ? result : null}
onOpenChange={(next) => (next ? void selectInstall(group) : collapse())}
onVersionChange={setVersionId}
onInstall={(discardLocal) => void installVersion(discardLocal)}
onCancelInstall={() => void cancelInstall()}
onSendToMachine={(shareId) => {
close()
useAppStore.getState().openSkillShare(shareId)
}}
onRemove={(discardLocal) => void remove(discardLocal)}
/>
))}
</ul>
) : null}
<SkillInstallManagementStatus
resultLabel={result ? skillInstallResultLabel(result) : null}
progressLabel={installProgress.phaseLabel}
error={error}
notice={notice}
/>
<DialogFooter>
<Button type="button" variant="ghost" onClick={close} disabled={busy}>
{copy.close}
</Button>
</DialogFooter>
</DialogContent>
<SkillInstallManagementDialogContent
bundleResult={bundleResult}
busy={busy}
confirmRemove={confirmRemove}
copy={copy}
details={details}
environmentId={environmentId}
error={error}
groups={groups}
installs={installs}
installActive={Boolean(installProgress.activeOperationId)}
notice={notice}
progressLabel={installProgress.phaseLabel}
result={result}
runtimeEnvironments={runtimeEnvironments}
selectedKey={selectedKey}
sshConnectionStates={sshConnectionStates}
sshTargetLabels={sshTargetLabels}
versionId={versionId}
onCancelInstall={() => void cancelInstall()}
onClose={close}
onEnvironmentChange={setEnvironmentId}
onInstall={(discardLocal) => void installVersion(discardLocal)}
onOpenChange={(group, next) => (next ? void selectInstall(group) : collapse())}
onRemove={(discardLocal) => void remove(discardLocal)}
onSendToMachine={(shareId) => {
close()
useAppStore.getState().openSkillShare(shareId)
}}
onVersionChange={setVersionId}
/>
</Dialog>
)
}
@@ -0,0 +1,151 @@
import { Loader2 } from 'lucide-react'
import { Button } from '@/components/ui/button'
import {
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle
} from '@/components/ui/dialog'
import type {
ManagedSkillInstall,
SkillInstallResult
} from '../../../../shared/skill-install-contract'
import type { SkillBundleInstallResult } from '../../../../shared/skill-bundle-install-contract'
import type { SkillCloudPackageDetails } from '../../../../shared/skill-cloud-contract'
import type { skillInstallManagementCopy } from './skill-install-management-copy'
import { skillInstallResultLabel } from './skill-install-result-label'
import { SkillInstallMachineSelect } from './SkillInstallMachineSelect'
import { SkillInstallManagementStatus } from './SkillInstallManagementStatus'
import { SkillManagedInstallRow } from './SkillManagedInstallRow'
import type { SkillManagedInstallGroup } from './skill-managed-install-groups'
type SkillInstallManagementDialogContentProps = {
bundleResult: SkillBundleInstallResult | null
busy: boolean
confirmRemove: boolean
copy: ReturnType<typeof skillInstallManagementCopy>
details: SkillCloudPackageDetails | null
environmentId: string
error: string | null
groups: SkillManagedInstallGroup[]
installs: ManagedSkillInstall[]
installActive: boolean
notice: string | null
result: SkillInstallResult | null
progressLabel: string | null
runtimeEnvironments: readonly { id: string; name: string }[]
selectedKey: string
sshConnectionStates: ReadonlyMap<string, { status: string }>
sshTargetLabels: ReadonlyMap<string, string>
versionId: string
onCancelInstall: () => void
onClose: () => void
onEnvironmentChange: (value: string) => void
onInstall: (discardLocal?: boolean) => void
onOpenChange: (group: SkillManagedInstallGroup, open: boolean) => void
onRemove: (discardLocal?: boolean) => void
onSendToMachine: (shareId: string) => void
onVersionChange: (versionId: string) => void
}
export function SkillInstallManagementDialogContent({
bundleResult,
busy,
confirmRemove,
copy,
details,
environmentId,
error,
groups,
installs,
installActive,
notice,
progressLabel,
result,
runtimeEnvironments,
selectedKey,
sshConnectionStates,
sshTargetLabels,
versionId,
onCancelInstall,
onClose,
onEnvironmentChange,
onInstall,
onOpenChange,
onRemove,
onSendToMachine,
onVersionChange
}: SkillInstallManagementDialogContentProps): React.JSX.Element {
const destructiveConflict =
result?.status === 'conflict' ||
Boolean(
groups
.find((group) => group.key === selectedKey)
?.installs.some((install) => install.state === 'modified')
)
return (
<DialogContent className="max-h-[calc(100vh-3rem)] overflow-x-hidden overflow-y-auto scrollbar-sleek sm:max-w-2xl [&>*]:min-w-0">
<DialogHeader>
<DialogTitle>{copy.title}</DialogTitle>
<DialogDescription>{copy.description}</DialogDescription>
</DialogHeader>
<SkillInstallMachineSelect
value={environmentId}
onChange={onEnvironmentChange}
localLabel={copy.localMachine}
sshLabel={copy.ssh}
disconnectedLabel={copy.disconnected}
environments={runtimeEnvironments}
sshTargets={[...sshTargetLabels.entries()].map(([id, label]) => ({
id: `ssh:${id}`,
label,
connected: sshConnectionStates.get(id)?.status === 'connected'
}))}
/>
{busy && installs.length === 0 ? <Loader2 className="mx-auto size-5 animate-spin" /> : null}
{!busy && installs.length === 0 ? (
<p className="rounded-md border border-border p-4 text-sm text-muted-foreground">
{copy.noInstalls}
</p>
) : null}
{groups.length > 0 ? (
<ul className="divide-y divide-border rounded-md border border-border">
{groups.map((group) => (
<SkillManagedInstallRow
key={group.key}
group={group}
open={selectedKey === group.key}
details={selectedKey === group.key ? details : null}
versionId={selectedKey === group.key ? versionId : ''}
busy={busy}
confirmRemove={selectedKey === group.key && confirmRemove}
installActive={installActive}
editedWarning={selectedKey === group.key && destructiveConflict}
bundleResult={selectedKey === group.key ? bundleResult : null}
result={selectedKey === group.key ? result : null}
onOpenChange={(next) => onOpenChange(group, next)}
onVersionChange={onVersionChange}
onInstall={onInstall}
onCancelInstall={onCancelInstall}
onSendToMachine={onSendToMachine}
onRemove={onRemove}
/>
))}
</ul>
) : null}
<SkillInstallManagementStatus
resultLabel={result ? skillInstallResultLabel(result) : null}
progressLabel={progressLabel}
error={error}
notice={notice}
/>
<DialogFooter>
<Button type="button" variant="ghost" onClick={onClose} disabled={busy}>
{copy.close}
</Button>
</DialogFooter>
</DialogContent>
)
}
+98 -98
View File
@@ -1,98 +1,98 @@
| Emoji | Name | Category | Unicode | Description | Mood | Popularity | Year Added |
|-------|------|----------|---------|-------------|------|------------|------------|
| 😀 | Grinning Face | Smileys | U+1F600 | A happy grinning face | Positive | 95 | 2015 |
| 🚀 | Rocket | Travel | U+1F680 | A soaring rocket | Exciting | 88 | 2010 |
| 🧑‍💻 | Technologist | People | U+1F9D1 U+200D U+1F4BB | Person at computer | Focused | 92 | 2020 |
| 🎉 | Party Popper | Events | U+1F389 | Celebration popper | Festive | 97 | 2010 |
| 🐱 | Cat Face | Animals | U+1F431 | A cute cat face | Cute | 85 | 2010 |
| 🍕 | Pizza | Food | U+1F355 | Slice of pizza | Hungry | 90 | 2010 |
| 💻 | Laptop | Objects | U+1F4BB | A personal computer | Productive | 78 | 2010 |
| 🌍 | Globe Europe-Africa | Travel | U+1F30D | Earth showing Europe/Africa | Wanderlust | 72 | 2010 |
| ❤️ | Red Heart | Symbols | U+2764 | A red heart symbol | Loving | 99 | 2010 |
| 🎵 | Musical Note | Music | U+1F3B5 | Single eighth note | Musical | 76 | 2010 |
| 🤖 | Robot Face | Smileys | U+1F916 | A mechanical robot | Neutral | 81 | 2016 |
| ☕ | Hot Beverage | Food | U+1F375 | A steaming coffee cup | Awake | 89 | 2010 |
| 🌈 | Rainbow | Nature | U+1F308 | A colorful rainbow | Hopeful | 83 | 2010 |
| 🦄 | Unicorn Face | Animals | U+1F984 | A fantasy unicorn | Magical | 91 | 2015 |
| ⚡ | High Voltage | Symbols | U+26A1 | Lightning bolt | Energetic | 86 | 2010 |
| 🎸 | Guitar | Music | U+1F3B8 | A six-string guitar | Rocking | 71 | 2010 |
| 🍀 | Four Leaf Clover | Nature | U+1F340 | A lucky clover | Lucky | 82 | 2010 |
| 🏆 | Trophy | Awards | U+1F3C6 | A gold championship cup | Victorious | 87 | 2010 |
| 🧠 | Brain | Body | U+1F9E0 | A human brain | Smart | 93 | 2018 |
| 🌮 | Taco | Food | U+1F32E | A folded taco | Tasty | 84 | 2015 |
| 🛸 | Flying Saucer | Travel | U+1F6F8 | An alien spacecraft | Mysterious | 69 | 2017 |
| 🎨 | Artist Palette | Arts | U+1F3A8 | A painter's palette | Creative | 77 | 2010 |
| 🐧 | Penguin | Animals | U+1F427 | A cute penguin | Chill | 79 | 2010 |
| 💎 | Gem Stone | Objects | U+1F48E | A sparkling diamond | Precious | 74 | 2010 |
| 🎮 | Video Game | Objects | U+1F3AE | A game controller | Gaming | 88 | 2010 |
| 🌊 | Water Wave | Nature | U+1F30A | A crashing ocean wave | Calm | 80 | 2010 |
| 🔥 | Fire | Nature | U+1F525 | A burning flame | Lit | 98 | 2010 |
| 🌟 | Glowing Star | Symbols | U+1F31F | A twinkling star | Bright | 85 | 2010 |
| 🎤 | Microphone | Music | U+1F3A4 | A stage microphone | Singing | 73 | 2010 |
| 🐶 | Dog Face | Animals | U+1F436 | A friendly dog face | Loyal | 86 | 2010 |
| 🍩 | Doughnut | Food | U+1F369 | A frosted doughnut | Sweet | 82 | 2010 |
| 🚲 | Bicycle | Travel | U+1F6B2 | A push bike | Active | 67 | 2010 |
| 🎭 | Performing Arts | Arts | U+1F3AD | Theater masks | Dramatic | 64 | 2010 |
| 🐉 | Dragon | Animals | U+1F409 | A mythical dragon | Powerful | 78 | 2010 |
| 🎪 | Circus Tent | Events | U+1F3AA | A striped circus tent | Fun | 61 | 2010 |
| 🧊 | Ice Cube | Objects | U+1F9CA | A block of ice | Cool | 70 | 2019 |
| 🎯 | Bullseye | Symbols | U+1F3AF | A dart hitting target | Focused | 83 | 2010 |
| 🐬 | Dolphin | Animals | U+1F42C | A leaping dolphin | Playful | 80 | 2010 |
| 🚁 | Helicopter | Travel | U+1F681 | A flying helicopter | Aerial | 63 | 2010 |
| 🦊 | Fox Face | Animals | U+1F98A | A clever fox | Sneaky | 79 | 2016 |
| 🎲 | Game Die | Objects | U+1F3B2 | A six-sided die | Random | 68 | 2010 |
| 🍔 | Hamburger | Food | U+1F354 | A classic burger | Juicy | 91 | 2010 |
| 🦋 | Butterfly | Animals | U+1F98B | A colorful butterfly | Graceful | 84 | 2016 |
| 🏔️ | Snow-Capped Mountain | Travel | U+1F3D4 | A snowy mountain peak | Adventurous | 71 | 2010 |
| 🎩 | Top Hat | Objects | U+1F3A9 | A formal top hat | Classy | 62 | 2010 |
| 🐳 | Spouting Whale | Animals | U+1F433 | A whale spraying water | Majestic | 76 | 2010 |
| 🚀 | Rocket | Travel | U+1F680 | A soaring rocket | Exciting | 88 | 2010 |
| 🎹 | Musical Keyboard | Music | U+1F3B9 | A piano keyboard | Melodic | 66 | 2010 |
| 🍉 | Watermelon | Food | U+1F349 | A slice of watermelon | Refreshing | 81 | 2010 |
| 🧩 | Puzzle Piece | Objects | U+1F9E9 | A jigsaw puzzle piece | Challenging | 75 | 2018 |
| 🦀 | Crab | Animals | U+1F980 | A red crab | Feisty | 68 | 2015 |
| 🏄 | Surfing | Sports | U+1F3C4 | Someone riding a wave | Gnarly | 65 | 2010 |
| 🧑‍🎤 | Singer | People | U+1F9D1 U+200D U+1F3A4 | A stage performer | Talented | 78 | 2020 |
| 🌵 | Cactus | Nature | U+1F335 | A desert cactus | Tough | 73 | 2010 |
| 🧲 | Magnet | Objects | U+1F9F2 | A horseshoe magnet | Attractive | 58 | 2018 |
| 🦆 | Duck | Animals | U+1F986 | A yellow duck | Quirky | 74 | 2016 |
| 🚇 | Metro | Travel | U+1F687 | A subway train | Urban | 59 | 2010 |
| 🧾 | Receipt | Objects | U+1F9FE | A paper receipt | Transactional | 56 | 2018 |
| 🫘 | Beans | Food | U+1FAD8 | A bowl of beans | Nutritious | 42 | 2021 |
| 🦕 | Sauropod | Animals | U+1F995 | A long-neck dinosaur | Prehistoric | 72 | 2016 |
| 🏵️ | Rosette | Awards | U+1F3F5 | A decorative rosette | Honorable | 51 | 2010 |
| 🦮 | Guide Dog | Animals | U+1F9AE | A seeing-eye dog | Helpful | 67 | 2019 |
| 🦴 | Bone | Body | U+1F9B4 | A dog bone | Gnawing | 60 | 2019 |
| 🥏 | Flying Disc | Sports | U+1F94F | A frisbee | Airborne | 48 | 2017 |
| 🦧 | Orangutan | Animals | U+1F9A7 | A red-haired ape | Wise | 55 | 2019 |
| 🥭 | Mango | Food | U+1F96D | A ripe tropical fruit | Tropical | 71 | 2018 |
| 🧋 | Bubble Tea | Food | U+1F9CB | Tapioca pearl drink | Trendy | 78 | 2020 |
| 🪀 | Yo-Yo | Objects | U+1FA80 | A string toy | Nostalgic | 44 | 2020 |
| 🦤 | Dodo | Animals | U+1F9A4 | An extinct flightless bird | Extinct | 53 | 2019 |
| 🪐 | Ringed Planet | Space | U+1FA90 | Saturn with rings | Astronomical | 77 | 2020 |
| 🛹 | Skateboard | Sports | U+1F6F9 | A kick push skateboard | Rad | 67 | 2019 |
| 🥲 | Smiling Face with Tear | Smileys | U+1F972 | Happy but crying inside | Bittersweet | 90 | 2020 |
| 🫠 | Melting Face | Smileys | U+1FAE0 | Face slowly melting | Uncomfortable | 69 | 2021 |
| 🪩 | Mirror Ball | Objects | U+1FAA9 | A disco ball | Groovy | 73 | 2022 |
| 🪭 | Folding Hand Fan | Objects | U+1FAAF | A collapsible hand fan | Elegant | 38 | 2023 |
| 🫎 | Moose | Animals | U+1FACE | A large antlered mammal | Majestic | 45 | 2023 |
| 🪸 | Coral | Nature | U+1FAB8 | A coral reef formation | Vibrant | 52 | 2023 |
| 🪼 | Jellyfish | Animals | U+1FABC | A floating gelatinous sea creature | Ethereal | 49 | 2023 |
| 🫏 | Donkey | Animals | U+1FACF | A gray donkey | Stubborn | 41 | 2023 |
| 🫅 | Person with Crown | People | U+1FAC5 | A crowned royal figure | Regal | 58 | 2022 |
| 🪇 | Maracas | Music | U+1FA87 | A handheld percussion shaker | Rhythmic | 43 | 2023 |
| 🫘 | Beans | Food | U+1FAD8 | A bowl of legumes | Hearty | 42 | 2021 |
| 🦪 | Oyster | Food | U+1F9AA | A closed shellfish | Briny | 54 | 2019 |
| 🪨 | Rock | Nature | U+1FAA8 | A grey stone | Solid | 47 | 2020 |
| 🥸 | Disguised Face | Smileys | U+1F978 | Face with fake nose/glasses | Sneaky | 66 | 2021 |
| 🦬 | Bison | Animals | U+1F9AC | A large North American bovine | Sturdy | 50 | 2019 |
| 🥟 | Dumpling | Food | U+1F95F | A folded filled dough | Steamy | 76 | 2018 |
| 🪴 | Potted Plant | Nature | U+1FAB4 | A houseplant in a pot | Green | 68 | 2020 |
| 🛼 | Roller Skate | Sports | U+1F6FC | A quad-wheeled skate | Retro | 56 | 2021 |
| 🥏 | Flying Disc | Sports | U+1F94F | A plastic throwing disc | Soaring | 48 | 2017 |
| 🧃 | Beverage Box | Food | U+1F9C3 | A juice box | Convenient | 63 | 2018 |
| 🦚 | Peacock | Animals | U+1F99A | A bird with iridescent tail | Proud | 75 | 2018 |
| 🫧 | Bubbles | Nature | U+1FAE7 | Floating iridescent spheres | Light | 57 | 2023 |
| 🥅 | Goal Net | Sports | U+1F945 | A soccer/hockey goal | Scoring | 44 | 2017 |
| 🦺 | Safety Vest | Objects | U+1F9BA | A high-visibility vest | Protected | 51 | 2019 |
| ✈️ | Airplane | Travel | U+2708 | A flying commercial jet | Traveling | 94 | 2010 |
| Emoji | Name | Category | Unicode | Description | Mood | Popularity | Year Added |
| ----- | ---------------------- | -------- | ---------------------- | ---------------------------------- | ------------- | ---------- | ---------- |
| 😀 | Grinning Face | Smileys | U+1F600 | A happy grinning face | Positive | 95 | 2015 |
| 🚀 | Rocket | Travel | U+1F680 | A soaring rocket | Exciting | 88 | 2010 |
| 🧑‍💻 | Technologist | People | U+1F9D1 U+200D U+1F4BB | Person at computer | Focused | 92 | 2020 |
| 🎉 | Party Popper | Events | U+1F389 | Celebration popper | Festive | 97 | 2010 |
| 🐱 | Cat Face | Animals | U+1F431 | A cute cat face | Cute | 85 | 2010 |
| 🍕 | Pizza | Food | U+1F355 | Slice of pizza | Hungry | 90 | 2010 |
| 💻 | Laptop | Objects | U+1F4BB | A personal computer | Productive | 78 | 2010 |
| 🌍 | Globe Europe-Africa | Travel | U+1F30D | Earth showing Europe/Africa | Wanderlust | 72 | 2010 |
| ❤️ | Red Heart | Symbols | U+2764 | A red heart symbol | Loving | 99 | 2010 |
| 🎵 | Musical Note | Music | U+1F3B5 | Single eighth note | Musical | 76 | 2010 |
| 🤖 | Robot Face | Smileys | U+1F916 | A mechanical robot | Neutral | 81 | 2016 |
| ☕ | Hot Beverage | Food | U+1F375 | A steaming coffee cup | Awake | 89 | 2010 |
| 🌈 | Rainbow | Nature | U+1F308 | A colorful rainbow | Hopeful | 83 | 2010 |
| 🦄 | Unicorn Face | Animals | U+1F984 | A fantasy unicorn | Magical | 91 | 2015 |
| ⚡ | High Voltage | Symbols | U+26A1 | Lightning bolt | Energetic | 86 | 2010 |
| 🎸 | Guitar | Music | U+1F3B8 | A six-string guitar | Rocking | 71 | 2010 |
| 🍀 | Four Leaf Clover | Nature | U+1F340 | A lucky clover | Lucky | 82 | 2010 |
| 🏆 | Trophy | Awards | U+1F3C6 | A gold championship cup | Victorious | 87 | 2010 |
| 🧠 | Brain | Body | U+1F9E0 | A human brain | Smart | 93 | 2018 |
| 🌮 | Taco | Food | U+1F32E | A folded taco | Tasty | 84 | 2015 |
| 🛸 | Flying Saucer | Travel | U+1F6F8 | An alien spacecraft | Mysterious | 69 | 2017 |
| 🎨 | Artist Palette | Arts | U+1F3A8 | A painter's palette | Creative | 77 | 2010 |
| 🐧 | Penguin | Animals | U+1F427 | A cute penguin | Chill | 79 | 2010 |
| 💎 | Gem Stone | Objects | U+1F48E | A sparkling diamond | Precious | 74 | 2010 |
| 🎮 | Video Game | Objects | U+1F3AE | A game controller | Gaming | 88 | 2010 |
| 🌊 | Water Wave | Nature | U+1F30A | A crashing ocean wave | Calm | 80 | 2010 |
| 🔥 | Fire | Nature | U+1F525 | A burning flame | Lit | 98 | 2010 |
| 🌟 | Glowing Star | Symbols | U+1F31F | A twinkling star | Bright | 85 | 2010 |
| 🎤 | Microphone | Music | U+1F3A4 | A stage microphone | Singing | 73 | 2010 |
| 🐶 | Dog Face | Animals | U+1F436 | A friendly dog face | Loyal | 86 | 2010 |
| 🍩 | Doughnut | Food | U+1F369 | A frosted doughnut | Sweet | 82 | 2010 |
| 🚲 | Bicycle | Travel | U+1F6B2 | A push bike | Active | 67 | 2010 |
| 🎭 | Performing Arts | Arts | U+1F3AD | Theater masks | Dramatic | 64 | 2010 |
| 🐉 | Dragon | Animals | U+1F409 | A mythical dragon | Powerful | 78 | 2010 |
| 🎪 | Circus Tent | Events | U+1F3AA | A striped circus tent | Fun | 61 | 2010 |
| 🧊 | Ice Cube | Objects | U+1F9CA | A block of ice | Cool | 70 | 2019 |
| 🎯 | Bullseye | Symbols | U+1F3AF | A dart hitting target | Focused | 83 | 2010 |
| 🐬 | Dolphin | Animals | U+1F42C | A leaping dolphin | Playful | 80 | 2010 |
| 🚁 | Helicopter | Travel | U+1F681 | A flying helicopter | Aerial | 63 | 2010 |
| 🦊 | Fox Face | Animals | U+1F98A | A clever fox | Sneaky | 79 | 2016 |
| 🎲 | Game Die | Objects | U+1F3B2 | A six-sided die | Random | 68 | 2010 |
| 🍔 | Hamburger | Food | U+1F354 | A classic burger | Juicy | 91 | 2010 |
| 🦋 | Butterfly | Animals | U+1F98B | A colorful butterfly | Graceful | 84 | 2016 |
| 🏔️ | Snow-Capped Mountain | Travel | U+1F3D4 | A snowy mountain peak | Adventurous | 71 | 2010 |
| 🎩 | Top Hat | Objects | U+1F3A9 | A formal top hat | Classy | 62 | 2010 |
| 🐳 | Spouting Whale | Animals | U+1F433 | A whale spraying water | Majestic | 76 | 2010 |
| 🚀 | Rocket | Travel | U+1F680 | A soaring rocket | Exciting | 88 | 2010 |
| 🎹 | Musical Keyboard | Music | U+1F3B9 | A piano keyboard | Melodic | 66 | 2010 |
| 🍉 | Watermelon | Food | U+1F349 | A slice of watermelon | Refreshing | 81 | 2010 |
| 🧩 | Puzzle Piece | Objects | U+1F9E9 | A jigsaw puzzle piece | Challenging | 75 | 2018 |
| 🦀 | Crab | Animals | U+1F980 | A red crab | Feisty | 68 | 2015 |
| 🏄 | Surfing | Sports | U+1F3C4 | Someone riding a wave | Gnarly | 65 | 2010 |
| 🧑‍🎤 | Singer | People | U+1F9D1 U+200D U+1F3A4 | A stage performer | Talented | 78 | 2020 |
| 🌵 | Cactus | Nature | U+1F335 | A desert cactus | Tough | 73 | 2010 |
| 🧲 | Magnet | Objects | U+1F9F2 | A horseshoe magnet | Attractive | 58 | 2018 |
| 🦆 | Duck | Animals | U+1F986 | A yellow duck | Quirky | 74 | 2016 |
| 🚇 | Metro | Travel | U+1F687 | A subway train | Urban | 59 | 2010 |
| 🧾 | Receipt | Objects | U+1F9FE | A paper receipt | Transactional | 56 | 2018 |
| 🫘 | Beans | Food | U+1FAD8 | A bowl of beans | Nutritious | 42 | 2021 |
| 🦕 | Sauropod | Animals | U+1F995 | A long-neck dinosaur | Prehistoric | 72 | 2016 |
| 🏵️ | Rosette | Awards | U+1F3F5 | A decorative rosette | Honorable | 51 | 2010 |
| 🦮 | Guide Dog | Animals | U+1F9AE | A seeing-eye dog | Helpful | 67 | 2019 |
| 🦴 | Bone | Body | U+1F9B4 | A dog bone | Gnawing | 60 | 2019 |
| 🥏 | Flying Disc | Sports | U+1F94F | A frisbee | Airborne | 48 | 2017 |
| 🦧 | Orangutan | Animals | U+1F9A7 | A red-haired ape | Wise | 55 | 2019 |
| 🥭 | Mango | Food | U+1F96D | A ripe tropical fruit | Tropical | 71 | 2018 |
| 🧋 | Bubble Tea | Food | U+1F9CB | Tapioca pearl drink | Trendy | 78 | 2020 |
| 🪀 | Yo-Yo | Objects | U+1FA80 | A string toy | Nostalgic | 44 | 2020 |
| 🦤 | Dodo | Animals | U+1F9A4 | An extinct flightless bird | Extinct | 53 | 2019 |
| 🪐 | Ringed Planet | Space | U+1FA90 | Saturn with rings | Astronomical | 77 | 2020 |
| 🛹 | Skateboard | Sports | U+1F6F9 | A kick push skateboard | Rad | 67 | 2019 |
| 🥲 | Smiling Face with Tear | Smileys | U+1F972 | Happy but crying inside | Bittersweet | 90 | 2020 |
| 🫠 | Melting Face | Smileys | U+1FAE0 | Face slowly melting | Uncomfortable | 69 | 2021 |
| 🪩 | Mirror Ball | Objects | U+1FAA9 | A disco ball | Groovy | 73 | 2022 |
| 🪭 | Folding Hand Fan | Objects | U+1FAAF | A collapsible hand fan | Elegant | 38 | 2023 |
| 🫎 | Moose | Animals | U+1FACE | A large antlered mammal | Majestic | 45 | 2023 |
| 🪸 | Coral | Nature | U+1FAB8 | A coral reef formation | Vibrant | 52 | 2023 |
| 🪼 | Jellyfish | Animals | U+1FABC | A floating gelatinous sea creature | Ethereal | 49 | 2023 |
| 🫏 | Donkey | Animals | U+1FACF | A gray donkey | Stubborn | 41 | 2023 |
| 🫅 | Person with Crown | People | U+1FAC5 | A crowned royal figure | Regal | 58 | 2022 |
| 🪇 | Maracas | Music | U+1FA87 | A handheld percussion shaker | Rhythmic | 43 | 2023 |
| 🫘 | Beans | Food | U+1FAD8 | A bowl of legumes | Hearty | 42 | 2021 |
| 🦪 | Oyster | Food | U+1F9AA | A closed shellfish | Briny | 54 | 2019 |
| 🪨 | Rock | Nature | U+1FAA8 | A grey stone | Solid | 47 | 2020 |
| 🥸 | Disguised Face | Smileys | U+1F978 | Face with fake nose/glasses | Sneaky | 66 | 2021 |
| 🦬 | Bison | Animals | U+1F9AC | A large North American bovine | Sturdy | 50 | 2019 |
| 🥟 | Dumpling | Food | U+1F95F | A folded filled dough | Steamy | 76 | 2018 |
| 🪴 | Potted Plant | Nature | U+1FAB4 | A houseplant in a pot | Green | 68 | 2020 |
| 🛼 | Roller Skate | Sports | U+1F6FC | A quad-wheeled skate | Retro | 56 | 2021 |
| 🥏 | Flying Disc | Sports | U+1F94F | A plastic throwing disc | Soaring | 48 | 2017 |
| 🧃 | Beverage Box | Food | U+1F9C3 | A juice box | Convenient | 63 | 2018 |
| 🦚 | Peacock | Animals | U+1F99A | A bird with iridescent tail | Proud | 75 | 2018 |
| 🫧 | Bubbles | Nature | U+1FAE7 | Floating iridescent spheres | Light | 57 | 2023 |
| 🥅 | Goal Net | Sports | U+1F945 | A soccer/hockey goal | Scoring | 44 | 2017 |
| 🦺 | Safety Vest | Objects | U+1F9BA | A high-visibility vest | Protected | 51 | 2019 |
| ✈️ | Airplane | Travel | U+2708 | A flying commercial jet | Traveling | 94 | 2010 |
+57 -57
View File
@@ -16,15 +16,15 @@ Commits under test (vs main):
## Already covered (do **not** re-test in E2E)
| Area | Where |
|------|--------|
| `listConfigHosts` / `resolveConfigHost` IPC registration | `src/main/ipc/ssh.test.ts` |
| Search, result limit, suppressed aliases, alreadyInOrca | `ssh-config-host-picker.test.ts` |
| Generation guard, freeze-while-resolving, late resolve | `AddRemoteHostDialog.config-picker.test.tsx` |
| Bulk `importConfig()` without `reAdopt` | `add-remote-host-ssh-actions.test.ts` |
| Alias folding / duplicate save check | `ssh-target-duplicate.test.ts` |
| `configured-only` host registry / setup fail-closed | unit tests in shared + project-host-workspace-target |
| Settings modal viewport stability | `ssh-host-form-modal.spec.ts` |
| Area | Where |
| -------------------------------------------------------- | ---------------------------------------------------- |
| `listConfigHosts` / `resolveConfigHost` IPC registration | `src/main/ipc/ssh.test.ts` |
| Search, result limit, suppressed aliases, alreadyInOrca | `ssh-config-host-picker.test.ts` |
| Generation guard, freeze-while-resolving, late resolve | `AddRemoteHostDialog.config-picker.test.tsx` |
| Bulk `importConfig()` without `reAdopt` | `add-remote-host-ssh-actions.test.ts` |
| Alias folding / duplicate save check | `ssh-target-duplicate.test.ts` |
| `configured-only` host registry / setup fail-closed | unit tests in shared + project-host-workspace-target |
| Settings modal viewport stability | `ssh-host-form-modal.spec.ts` |
E2E is reserved for real Electron HOME isolation, real `~/.ssh/config` parse,
real `ssh -G` resolve, and user-visible DOM outcomes.
@@ -66,74 +66,74 @@ Optional second file if the Settings Import case grows:
### P1 — Empty config empty state
| | |
|--|--|
| **Setup** | Do not create `~/.ssh/config` (or write empty file). |
| **Steps** | Open Add SSH host → Fill from ~/.ssh/config… |
| | |
| ---------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| **Setup** | Do not create `~/.ssh/config` (or write empty file). |
| **Steps** | Open Add SSH host → Fill from ~/.ssh/config… |
| **Expect** | Dialog title **Choose from ~/.ssh/config**; body **No hosts in ~/.ssh/config**; **Add all to Orca** disabled; **Back** returns to form. |
### P2 — Seeded hosts listed with summary lines
| | |
|--|--|
| **Setup** | Write config with ≥2 concrete Hosts, e.g. `e2e-alpha` / `e2e-bravo` with HostName, User, Port. |
| **Steps** | Open picker. |
| | |
| ---------- | --------------------------------------------------------------------------------------------------------------------- |
| **Setup** | Write config with ≥2 concrete Hosts, e.g. `e2e-alpha` / `e2e-bravo` with HostName, User, Port. |
| **Steps** | Open picker. |
| **Expect** | Host list `SSH config hosts` shows both aliases; subtitle `user@hostname:port`; button **Add all 2 to Orca** enabled. |
### P3 — Select host prefills form (and Save persists)
| | |
|--|--|
| **Setup** | Config Host `e2e-prod` → HostName `prod.example.test`, User `deploy`, Port `2222`. |
| **Steps** | Pick `e2e-prod` → wait for form → click **Save**. |
| **Expect** | After pick: Host/alias field = `prod.example.test`, Username `deploy`, Port `2222`, Label `e2e-prod` (or alias); optional toast *Filled from e2e-prod*; Identity file may stay empty with config hint. After Save: dialog closes; target appears in Settings → SSH (or listTargets shows matching host). |
| | |
| ---------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Setup** | Config Host `e2e-prod` → HostName `prod.example.test`, User `deploy`, Port `2222`. |
| **Steps** | Pick `e2e-prod` → wait for form → click **Save**. |
| **Expect** | After pick: Host/alias field = `prod.example.test`, Username `deploy`, Port `2222`, Label `e2e-prod` (or alias); optional toast _Filled from e2e-prod_; Identity file may stay empty with config hint. After Save: dialog closes; target appears in Settings → SSH (or listTargets shows matching host). |
### P4 — Filter narrows list
| | |
|--|--|
| **Setup** | Hosts `e2e-alpha`, `e2e-bravo`. |
| **Steps** | Open picker; filter `bravo`. |
| | |
| ---------- | ------------------------------------------------------------------------ |
| **Setup** | Hosts `e2e-alpha`, `e2e-bravo`. |
| **Steps** | Open picker; filter `bravo`. |
| **Expect** | Only bravo row; alpha gone; **No matching hosts** if filter is nonsense. |
### P5 — Already-in-Orca badge + disabled row
| | |
|--|--|
| **Setup** | Config hosts alpha + bravo. Seed Orca target with `configHost`/`label` matching alpha (via `ssh.addTarget`). |
| **Steps** | Open picker. |
| **Expect** | Alpha shows **In Orca** badge and is not clickable; bravo still selectable; **Add all 1 to Orca** (not 2). |
| | |
| ---------- | ------------------------------------------------------------------------------------------------------------ |
| **Setup** | Config hosts alpha + bravo. Seed Orca target with `configHost`/`label` matching alpha (via `ssh.addTarget`). |
| **Steps** | Open picker. |
| **Expect** | Alpha shows **In Orca** badge and is not clickable; bravo still selectable; **Add all 1 to Orca** (not 2). |
### P6 — Add all N to Orca imports new hosts only
| | |
|--|--|
| **Setup** | Config with 2 new hosts; no Orca targets for them. |
| **Steps** | **Add all 2 to Orca** → wait for success toast / return to form or list refresh. |
| | |
| ---------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| **Setup** | Config with 2 new hosts; no Orca targets for them. |
| **Steps** | **Add all 2 to Orca** → wait for success toast / return to form or list refresh. |
| **Expect** | Both targets exist (DOM in Settings SSH and/or listTargets); re-open picker shows **All hosts already in Orca** / both **In Orca**. |
### P7 — Add all does **not** re-adopt deleted hosts
| | |
|--|--|
| **Setup** | Config with alpha + bravo; Add all → remove alpha via API (creates suppress tombstone). |
| **Steps** | Re-open picker; note count; optionally click Add all again. |
| | |
| ---------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| **Setup** | Config with alpha + bravo; Add all → remove alpha via API (creates suppress tombstone). |
| **Steps** | Re-open picker; note count; optionally click Add all again. |
| **Expect** | Alpha absent from picker (suppressed) or not re-created; only new hosts counted; `listTargets` still lacks deleted alpha after second Add all. |
### P8 — Back discards pending pick path
| | |
|--|--|
| **Setup** | Seeded config. |
| **Steps** | Open picker → **Back** without selecting. |
| | |
| ---------- | ------------------------------------------------------ |
| **Setup** | Seeded config. |
| **Steps** | Open picker → **Back** without selecting. |
| **Expect** | Form fields still empty (Host blank); no filled toast. |
### P9 — Settings Import re-adopts (contrast with P7)
| | |
|--|--|
| **Setup** | Same as P7 after delete. |
| **Steps** | Settings → SSH → **Import** (explicit reAdopt path). |
| | |
| ---------- | ---------------------------------------------------------------------- |
| **Setup** | Same as P7 after delete. |
| **Steps** | Settings → SSH → **Import** (explicit reAdopt path). |
| **Expect** | Deleted config host reappears as an Orca target; toast sync count ≥ 1. |
---
@@ -155,17 +155,17 @@ Skip: 100-host truncation, resolve races, GSSAPI system-default, composer host-a
## Implementation status (done)
| Case | Spec |
|------|------|
| P1 empty state | `ssh-config-host-picker.spec.ts` |
| P2 list + Add all enabled | `ssh-config-host-picker.spec.ts` |
| Case | Spec |
| ------------------------------------- | -------------------------------- |
| P1 empty state | `ssh-config-host-picker.spec.ts` |
| P2 list + Add all enabled | `ssh-config-host-picker.spec.ts` |
| P3 select + Save (+ N3 identity hint) | `ssh-config-host-picker.spec.ts` |
| P4 filter | `ssh-config-host-picker.spec.ts` |
| P5 In Orca badge / count | `ssh-config-host-import.spec.ts` |
| P6 Add all imports | `ssh-config-host-import.spec.ts` |
| P7 no re-adopt after delete | `ssh-config-host-import.spec.ts` |
| P8 Back without select | `ssh-config-host-picker.spec.ts` |
| P9 Settings Import re-adopts | `ssh-config-host-import.spec.ts` |
| P4 filter | `ssh-config-host-picker.spec.ts` |
| P5 In Orca badge / count | `ssh-config-host-import.spec.ts` |
| P6 Add all imports | `ssh-config-host-import.spec.ts` |
| P7 no re-adopt after delete | `ssh-config-host-import.spec.ts` |
| P8 Back without select | `ssh-config-host-picker.spec.ts` |
| P9 Settings Import re-adopts | `ssh-config-host-import.spec.ts` |
Shared helpers: `tests/e2e/helpers/ssh-config-host-picker.ts`
@@ -58,11 +58,11 @@ dir holding `conpty.dll` + `OpenConsole.exe`).
Tiers differ only in which top-level `*.dll` files they carry:
| Tier | Top-level DLLs |
| --------- | ---------------------------------------------------------------- |
| `full` | **all** top-level `*.dll` |
| Tier | Top-level DLLs |
| --------- | ------------------------------------------------------------------------------------------------------------------------------ |
| `full` | **all** top-level `*.dll` |
| `no-gpu` | all **except** GPU/render DLLs (`libEGL`, `libGLESv2`, `vk_swiftshader`, `vulkan-1`, `d3dcompiler_47`); **keeps** `ffmpeg.dll` |
| `minimal` | **none** (exe + data blobs + daemon bundle + node-pty only) |
| `minimal` | **none** (exe + data blobs + daemon bundle + node-pty only) |
Trimming further is a config change (edit `TIER_DEFINITIONS` / `GPU_DLLS`), not
a code change.
@@ -95,7 +95,7 @@ The reverted #7421 added a `node-pty` patch that reads
`config/patches/node-pty@1.1.0.patch` does NOT contain that override** — it was
reverted. The spike therefore relies on **layout preservation** (copying the
node-pty tree at its default relative path) rather than the env override. The
spike still *sets* `ORCA_NODE_PTY_NATIVE_DIR` to the relocated native dir so it
spike still _sets_ `ORCA_NODE_PTY_NATIVE_DIR` to the relocated native dir so it
keeps working if pointed at a build that carries the patch, but on this branch
the var is inert.
@@ -243,13 +243,19 @@ async function finalHealthCheck(baseDir) {
let gotEvent = false
let sub = null
try {
sub = await client.subscribeViaWatcherProcess(dir, (err, events) => {
if (!err && events.length > 0) {
gotEvent = true
}
}, OPTS)
sub = await client.subscribeViaWatcherProcess(
dir,
(err, events) => {
if (!err && events.length > 0) {
gotEvent = true
}
},
OPTS
)
} catch (err) {
process.stderr.write(`[fixed-harness] health subscribe attempt ${attempt} failed: ${err.message}\n`)
process.stderr.write(
`[fixed-harness] health subscribe attempt ${attempt} failed: ${err.message}\n`
)
rmrf(dir)
await sleep(5000)
continue
@@ -279,10 +285,13 @@ async function main() {
// Watchdog: a stuck lane must fail loudly, and a premature natural exit
// (empty event loop) must not read as success.
const watchdog = setTimeout(() => {
process.stderr.write('[fixed-harness] FAIL: watchdog — scenarios did not complete\n')
process.exit(7)
}, durationMs * 4 + 60_000)
const watchdog = setTimeout(
() => {
process.stderr.write('[fixed-harness] FAIL: watchdog — scenarios did not complete\n')
process.exit(7)
},
durationMs * 4 + 60_000
)
process.on('exit', (code) => {
if (!completed && code === 0) {
process.exitCode = 8
@@ -313,7 +322,9 @@ async function main() {
`interruptions=${stats.interruptions} healthy=${healthy} fallback=${stats.fallbackDetected}\n`
)
if (stats.fallbackDetected) {
process.stderr.write('[fixed-harness] FAIL: in-process fallback used — isolation not exercised\n')
process.stderr.write(
'[fixed-harness] FAIL: in-process fallback used — isolation not exercised\n'
)
process.exitCode = 6
return
}
+3 -1
View File
@@ -52,7 +52,9 @@ async function main() {
const { code, signal } = await runOnce(s, durationMs)
const elapsed = Date.now() - started
const meaning = KNOWN[code] || (code === 0 ? 'clean exit' : 'unexpected')
console.log(`=== [${s}] iter ${i}: exit=${code} (${hex(code ?? -1)}) signal=${signal} ${meaning} after ${elapsed}ms`)
console.log(
`=== [${s}] iter ${i}: exit=${code} (${hex(code ?? -1)}) signal=${signal} ${meaning} after ${elapsed}ms`
)
results.push({ scenario: s, iteration: i, code, signal, elapsed })
if (code !== 0 && code !== null) {
crashed = true
+18 -18
View File
@@ -99,13 +99,13 @@ and the safety guards above would (correctly) refuse to run. **Isolated mode**
real install.
**The /D mechanism.** electron-builder's NSIS honors the standard NSIS `/D=<path>`
override for the install *directory* (`node_modules/app-builder-lib/templates/nsis/multiUser.nsh`).
override for the install _directory_ (`node_modules/app-builder-lib/templates/nsis/multiUser.nsh`).
`/D` is special: it must be the **last** argument and **cannot be quoted**, so the
path must be absolute and **spaces-free** (validated by `validateInstallDir`). The
installer's kill-sweep only matches processes under its own `$INSTDIR`, so a
separate directory never touches the real install's app or daemon processes.
**Why registry/shortcut backup-restore exists.** `/D` relocates *files only*.
**Why registry/shortcut backup-restore exists.** `/D` relocates _files only_.
Regardless of `/D`, the installer writes `InstallLocation` + the uninstall entry to
the **same per-user HKCU keys** as the real install
(`HKCU\Software\<APP_GUID>` and
@@ -191,22 +191,22 @@ powershell -File tests/tools/win-update-e2e/window-enum.ps1
## Files
| File | Responsibility |
| -------------------------- | ----------------------------------------------------------------------------- |
| `run.mjs` | Orchestrator + CLI entry |
| `cli-args.mjs` | Argument parsing / validation |
| `preflight.mjs` | win32/elevation checks, pre-existing-app refusal, baseline snapshot |
| `installer-steps.mjs` | Silent install/update/uninstall, exe discovery, gh download |
| `registry-shortcut-backup.mjs` | Isolated mode: snapshot/restore the shared HKCU keys + Orca shortcuts |
| `app-driver.mjs` | Playwright Electron launch + terminal driving (production-safe DOM selectors) |
| `interactivity-probes.mjs` | Sentinel-file echo / heartbeat / Ctrl+C probes |
| `daemon-processes.mjs` | Daemon PID discovery (command-line marker + pid file), scoped |
| `window-enum.ps1` | Shared visible-top-level-window enumerator (P/Invoke `EnumWindows`) |
| `window-watch.ps1` | Background baseline-diff watch loop → JSONL |
| `window-watch.mjs` | Node wrapper: start/stop watch, `--selftest`, baseline capture |
| `assertions.mjs` | Window-event classification + profile PASS/FAIL table |
| `platform-guard.mjs` | `assertWin32`, elevation detection |
| `powershell-runner.mjs` | Windows PowerShell 5.1 spawn helpers |
| File | Responsibility |
| ------------------------------ | ----------------------------------------------------------------------------- |
| `run.mjs` | Orchestrator + CLI entry |
| `cli-args.mjs` | Argument parsing / validation |
| `preflight.mjs` | win32/elevation checks, pre-existing-app refusal, baseline snapshot |
| `installer-steps.mjs` | Silent install/update/uninstall, exe discovery, gh download |
| `registry-shortcut-backup.mjs` | Isolated mode: snapshot/restore the shared HKCU keys + Orca shortcuts |
| `app-driver.mjs` | Playwright Electron launch + terminal driving (production-safe DOM selectors) |
| `interactivity-probes.mjs` | Sentinel-file echo / heartbeat / Ctrl+C probes |
| `daemon-processes.mjs` | Daemon PID discovery (command-line marker + pid file), scoped |
| `window-enum.ps1` | Shared visible-top-level-window enumerator (P/Invoke `EnumWindows`) |
| `window-watch.ps1` | Background baseline-diff watch loop → JSONL |
| `window-watch.mjs` | Node wrapper: start/stop watch, `--selftest`, baseline capture |
| `assertions.mjs` | Window-event classification + profile PASS/FAIL table |
| `platform-guard.mjs` | `assertWin32`, elevation detection |
| `powershell-runner.mjs` | Windows PowerShell 5.1 spawn helpers |
## Known limitations