Skip to content

[upstream #10078] feat(terminal): opt-in wide East Asian Ambiguous character width - #18

Open
innocarpe wants to merge 599 commits into
mainfrom
fix/terminal-east-asian-ambiguous-width
Open

innocarpe wants to merge 599 commits into
mainfrom
fix/terminal-east-asian-ambiguous-width

Conversation

@innocarpe

Copy link
Copy Markdown
Owner

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

Upstream

Summary

Summary Opt-in setting for East Asian Ambiguous character cell width (iTerm2-style escape hatch), requested in stablyai#9958. On Windows CJK locales, characters like ① ■ ● ★ → render poorly in Orca because: 1. Unicode 11 width tables report them as width 1 (modern CLI def

Note

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

nwparker and others added 30 commits July 26, 2026 22:39
Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com>
…lyai#10872)

Closing Ctrl+F left one match highlighted until the window was minimized
and restored.

xterm's DecorationService keys its SortedList on `decoration.marker.line`,
but `SortedList.delete()` only records an index and defers compaction,
while `Marker.dispose()` sets `line = -1` — mutating that same sort key.
After the first disposal the array is no longer sorted, so the binary
search inside `delete()` can miss a decoration that is present. It returns
false, `onDecorationRemoved` never fires, and the decoration stays live and
keeps painting. Repaints don't help; they faithfully re-paint a live
decoration, which is why only a window cycle appeared to fix it.

`clearDecorations()` disposes the active match before the match
highlights, which is exactly the order that trips this.

Patch `delete()` to retry once after compacting pending deletions, on the
miss path only, so the common bulk delete keeps its O(log n) search and
deferred batching. A 3000-trial randomized differential against upstream
semantics shows no behavior change for well-ordered lists.
…rkers, marketplace v0 (experimental) (stablyai#8549)

* feat(plugins): Orca plugin system — kernel, content packs, panels, workers, marketplace v0 (experimental)

Adds Orca's experimental plugin system behind a settings flag: a
supervised kernel, declarative content packs (VM recipes, commands and
keybindings, language packs), sandboxed iframe panels, forked worker
hosts, and a Git-backed marketplace v0 with consent, provenance and
kill-list enforcement.

Theme, icon-theme and terminal-theme contributions are deferred to a
follow-up pass.

* fix(plugins): make unsupported marketplace listings unreachable by key

findPlugin() backs preview/install/previewInstalledUpdate via
requireListing(), so filtering only listPlugins() hid the catalog card
while leaving the dead install path reachable one click later.

* fix(plugins): fan Pi session-only status out to plugin subscribers

The providerSessionOnly early-return in applyNormalizedStatus emitted to
onAgentStatus (main-window fanout) but skipped enrichedStatusListeners, so
plugins subscribed to agent.status.changed silently missed every Pi
session_start event. Route both emit sites through one helper so a future
early return cannot drop the plugin tap again.

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

* plugins: drop dead code and hoist duplicated trust-boundary patterns

Cleanup pass over the P1 diff, no behavior change:

- Delete `readPluginTreeSnapshot`/`readSnapshotFile` and their types, plus
  the now-vestigial `directories`/`signal` plumbing in `collectFiles`.
- Delete `resolveContainedPluginDirectory` (no callers).
- Delete `plugin-content-load-pool.ts`; it reimplemented the existing
  `mapWithConcurrency`, whose index arg also removes the pairing wrapper
  in `buildPluginList`.
- Hoist `PLUGIN_CONTENT_HASH_PATTERN` and `PLUGIN_COMMIT_PATTERN` into
  the install-lockfile module; 11 sites hand-rolled these identically.
- Point the new reliability gate at the PR instead of gitignored docs
  paths, matching every other gate's link form.

* fix(plugins): retry plugin state renames on Windows AV/EPERM locks

Six plugin write paths (lockfile, provenance, current pointer, kill
list, marketplace cache, staged install dir) did a plain rename, so an
antivirus or indexer holding the target open surfaced as a failed
install. The repo already retries this hazard for issue stablyai#1507, but only
through a sync helper; these paths are all async.

Adds one bounded async retry + atomic write used by all six, and trims a
consent-provenance header that restated its own JSX.

* test(plugins): cover the Windows rename retry path

The retry loop shipped untested: both existing cases hit the non-retry path,
and the temp-cleanup test passed identically with the `finally` removed.
Mock `rename` to queue errno codes so CI can exercise locks it cannot provoke.

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

* fix(plugins): pin bundled plugin resources to LF

Windows CI checks out with autocrlf, so the byte-hashed launch tree arrived
as CRLF and verify-packaged-plugin-resources rejected it — the packaged build
could never pass on Windows. Reproduced locally: CRLF yields the exact CI
error, LF verifies clean. Files are already LF, so nothing renormalizes.

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

* test: guard the bundled-plugin LF pin against a CRLF checkout

The byte-hash mismatch only surfaced in Windows packaging CI. Assert the
.gitattributes pin and that a CRLF tree is rejected, so a regression fails
on any platform instead of waiting for a packaged Windows build.

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

* ci: trigger packaged-build check on bundled plugin resource changes

The launch tree is byte-hashed during packaging, but no trigger path covered
it — so the CRLF fix for that check would not have re-run the check. Add the
resources, verifier and .gitattributes paths that can break packaging.

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

* perf(plugins): rebuild the panel frame only when its baked theme values change

The revision keys the panel iframe, so every bump destroys the sandboxed
frame and its in-panel state. It counted root attribute mutations, but
--workspace-sidebar-live-width is written every rAF of a sidebar drag, so
dragging with a panel open blanked it ~60x/sec. Compare the two values the
shell actually bakes in instead.

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

* test: stop pinning a plugin name in the CRLF guard

The CRLF case rewrites every launch file, so the reported mismatch is
whichever plugin sorts first. P2 adds theme plugins that sort ahead of
orca-navigation-shortcuts, which broke the assertion there.

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

* style: drop stray blank lines left by the rebase resolutions

Both sides of the agent-hooks and orca-runtime conflicts contributed a
trailing blank, which oxfmt rejects. Whitespace only.

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

* test(plugins): stop the startup budget failing on machine load

P95 runs 16-34ms idle but exceeds the 50ms bound under full-suite
parallelism, so the gate flaked. Widen it to catch an order-of-magnitude
regression instead; the no-worker/no-plugin-code assertions are the real
guarantee. Verified a 400ms regression still fails.

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

---------

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

* fix(remote): accelerate shared-control and pane recovery on resume/online

Narrow stablyai#8255 onto current main after stablyai#9774: fire pending shared-control
reconnect timers and pane recovery backoffs on system resume and browser
online, without replacing the per-pane recovery state machine or reconnect
banner UX.

* test(remote): cover online and occluded-resume recovery triggers

* fix(remote): centralize recovery acceleration

---------

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
* fix(mobile): block iOS uploads below the last shipped App Store version

The closed-train guard looked up each candidate version's own App Store
record, but a version only gets one once it is submitted for review.
0.0.34 reached TestFlight and was never submitted, so it had no record,
nothing looked closed, and the patch-bump walk stopped there — while
0.0.35 had already shipped. Apple rejected the upload after a 24-minute
build (90186 closed train, 90062 needs a higher CFBundleShortVersionString).

Fetch the highest closed version once and treat everything at or below it
as closed, comparing semver numerically so 0.0.10 outranks 0.0.9.

Also read appVersionState alongside appStoreState: the latter is
deprecated in App Store Connect API 3.3 and renames the shipped state to
READY_FOR_DISTRIBUTION, so reading only the old field would silently find
zero closed versions once Apple stops populating it.

* chore(mobile): prepare 0.0.36

app.json sat at 0.0.32 while 0.0.35 shipped on the App Store, because
release versions are resolved on the runner and never committed back.
Close the four-version drift so the checked-in version matches reality
and the iOS release no longer depends on the closed-train walk to find
an open version.

Bump Android versionCode 8 -> 9 in the same commit: the version is shared
between platforms, and shipping 0.0.36 with the code that already shipped
for 0.0.32 produces an APK that cannot install over the released build.
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
…acter (stablyai#10866)

* perf(agent-status): strip terminal control bytes by run, not per character

stripTerminalControl built its result with a per-character `+=`, allocating a
fresh string for every retained character. The Command Code status detector
calls it four times per PTY chunk — the scan text, the chunk-boundary variant,
and both previous-text lengths — so an agent pane paid that on every write.

Control bytes are sparse in real output, so copy the spans between them instead:
2.3x-2.6x from 5 KiB to 106 KiB chunks. Output is byte-identical, checked
exhaustively over every string up to length 4 across a 13-symbol control/unicode
alphabet plus 200k random strings (224,831 inputs, 0 mismatches).

* docs(agent-status): condense the run-copy rationale comments

Review feedback: both comments walked through the implementation. Keep one line
of non-obvious rationale each, per the repo's comment guidelines.

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

* test(agent-status): correct terminal strip benchmark

* test(agent-status): bound terminal strip benchmark

---------

Co-authored-by: Orca <help@stably.ai>
* fix(terminal): restore link hover after mouseleave

* test(terminal): verify mouseleave listener cleanup

* test(terminal): assert link hover listener wiring
…dle (stablyai#8459) (stablyai#10893)

* fix(resource-manager): never destroy a session Orca cannot prove is idle (stablyai#8459)

Resource Manager decided a session was an "orphan" from the absence of a
renderer binding, then force-killed it with no prompt. Absence of a binding is
not evidence a session is idle — during restore the binding map is legitimately
empty, and deferred SSH sessions never appear in it at all. Live agent sessions
were destroyed this way, losing unrecoverable work.

Three gaps, one rule: only positive evidence authorizes destruction.

- `pty:listSessions` dropped `agentSessionOwners` at the IPC boundary, so the
  renderer could not see the one fact that proves work is running. It now
  reports `hasAgentOwner`, typed once in `shared/pty-listed-session.ts` so the
  main handler, both preload surfaces, and the renderer cannot drift.
- The binding index ignored `deferredSshSessionIdsByTabId` — sessions restore
  knows are live on an SSH host but has not reattached. No other binding source
  can see them.
- The bulk-kill handler filtered sessions separately from the button's count,
  so the set killed could differ from the set advertised. Both now call
  `selectUnboundDaemonSessions`.

The single-row kill path had the same defect: it skipped confirmation whenever
`bound` was false. `requiresKillConfirmation` now also holds for agent-owned
sessions, and snapshot-derived rows carry ownership across from the daemon list
rather than reporting `false`.

* fix(resource-manager): distinguish unprovable ownership from proven absence

Adversarial review of the previous commit found it committed the same class of
error it was fixing: it collapsed "no agent owns this" and "this provider cannot
tell me" into one boolean `false`, and both destructive paths read that as proof.

A daemon generation below the claim protocol, an older SSH relay, or the
in-process local fallback all list no owners for a session that may well have
one. `pty.ts` already encodes the rule at :613 — "only providers that serialize
claims may make listing absence authoritative" — and the new IPC row ignored it.
So after upgrading with a legacy daemon still holding a live agent terminal,
bulk cleanup would have destroyed it: exactly stablyai#8459, one layer down.

`hasAgentOwner: boolean` is now `agentOwnership: 'present' | 'absent' | 'unknown'`,
derived via `providesAgentSessionOwnerListings`. Only `absent` authorizes
destruction, so `unknown` protects and confirms.

Second defect, found independently by four review lenses: the deferred-SSH
bindings reached the bulk selector but not `mergeSnapshotAndSessions`, because
the merge call site re-listed the binding fields instead of reusing the object.
A deferred SSH session therefore rendered `bound: false`, and its single-row kill
skipped confirmation while bulk cleanup correctly spared it. The call site now
spreads `resourceSessionBindings`, and a parity test fails if any binding field
is re-listed inline — the drift itself is now impossible to reintroduce quietly.

The e2e ownership assertion was also weak: it checked only that a boolean
arrived. It now asserts the exact arm, and that the live local provider reports
`absent` rather than `unknown`, so a degenerate all-unknown implementation fails.

---------

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
…10822)

* feat(usage): price Claude 5 family and GPT-5.6 token usage

Claude Opus 5, Sonnet 5, Fable 5 and Codex gpt-5.6 sol/terra/luna were
absent from the usage pricing tables, so their turns aggregated tokens
but reported no estimated cost.

Rates from Anthropic and OpenAI published pricing. Sonnet 5 gets no
long-context tier: Claude 4.6 and later bill the full 1M window flat.

Sonnet 5 uses the standard $3/$15 rate, not the $2/$10 introductory rate
that runs through 2026-08-31 — the table has no date dimension.

* fix(usage): price the bare gpt-5.6 alias and assert Opus 4.5 separately

OpenAI routes the bare `gpt-5.6` alias to Sol, but only the explicit
`-sol` / `-terra` / `-luna` IDs resolved, so alias-recorded sessions still
reported no cost. Match it exactly rather than by prefix so it cannot
swallow the tier IDs or a future cheaper variant.

Also split the Claude 5 shadowing guard into per-model breakdown
assertions and add the missing Opus 4.5 fixture the test name claimed.

* docs(usage): note Sonnet 5 uses standard, not introductory, rates
…stablyai#10634) (stablyai#10986)

* fix(worktrees): resolve a two-host project by the worktree's own host (stablyai#10634)

A project registered on both a local host and an SSH host permanently poisoned
every one of its workspaces with "Workspace identity is ambiguous across hosts.
Refresh projects and try again." Refresh could never help: nothing was stale,
both host setups were valid and intentional. The error survived restarts.

The ambiguity was manufactured. `resolveExactWorktreeRoute` starts from a
worktree that already carries exactly one `hostId`, then throws that away and
asks `resolveIndexedRepoOperationRoute` which host owns the *repo* — a question
with two right answers once a project spans hosts. Only the project spans hosts;
each worktree never does.

Route resolution now filters repo setups to the ones matching the worktree's own
host before looking for a transport, so a two-host project resolves as cleanly as
a one-host project. Genuine ambiguity still returns `ambiguous`.

Second half: the error escaped as an *uncaught renderer error* because passive
background paths — unread marking, activity bumps — called a helper that threw.
Those callers now degrade: `trySettingsForWorktreeOwner` returns null, the
passive update is skipped with a warning, and local state stays consistent.
Explicit user actions still surface the error.

* fix(worktrees): cover every passive path and warn once for ambiguous owners

Adversarial review found the routing fix sound but its coverage thin: only
markWorktreeUnread had an ambiguous-owner test, so restoring the throw in
clearWorktreeUnread or bumpWorktreeActivity would have reproduced the uncaught
renderer error with the suite still green. Both are now covered, verified by
mutation.

bumpWorktreeActivity also skipped silently where the other paths warned. It now
warns — but once per workspace, not per event: activity bumps fire on every PTY
event, so an unbounded warn would flood the console for exactly the users already
hitting this bug.

---------

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
…10908)

* perf(runtime): remove timer clamps from cooperative yields

Renderer paste and input loops can schedule more than a thousand zero-delay timer yields for a maximum-size payload. Chromium clamps nested timers to 4ms, adding seconds of idle wall time.

Use MessageChannel tasks in renderer runtimes and setImmediate in Node while retaining a timer fallback for tests and unsupported environments.

* fix(runtime): preserve pacing and release yield callbacks

Adversarial review found that concurrent producers could retain resolved callbacks until global quiescence. Route renderer yields by token and delete each resolver before resuming its producer.

Keep timer pacing in terminal paste and accepted-write loops where SSH and local PTYs do not provide drain acknowledgement. Use the shared scheduler for the OpenCode scanner.
…down variant (stablyai#9161)

Co-authored-by: viniciussilva <vinicius.silva@plus10.de>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* ci: parallelize and accelerate PR checks

* fix(ci): make accelerated checks runtime-safe

* fix(ci): address review findings

* fix(ci): retry transient Electron downloads

* test(ci): cover Electron download retry limits
…blyai#8938)

* fix(jira): render issue images and open them in a lightbox

Jira ADF media nodes were dropped when converting descriptions/comments
to Markdown, so screenshots never appeared in the Tasks drawer.

Download image attachments with authenticated Jira API access, embed them
as data URLs on issue/comment detail loads, and add a viewport-centered
lightbox. Closing with X/Esc only dismisses the preview, not the issue sheet.

* fix(jira): open comment images in the same lightbox as description

Jira issue comments still used compact markdown, so screenshots rendered
but could not expand. Use the document renderer for comment bodies, add a
regression test for the expand control, and sync MarkdownImageLightbox
locale keys.

* fix(jira): harden inline image handling

* fix(jira): harden inline image discovery, escaping, and downloads

Address PR review findings: correct media-attachment pairing, Server/DC
attachment lookup base path, markdown-safe external URLs, wider HTML
discovery with gated alt fallback, concurrent downloads outside the API
semaphore, and a main-process attachment data-URL cache with lower caps.

* fix(jira): Option A multi-same-name attachments and post-map media warns

Fix discovery so repeated alts (image.png) get distinct attachment ids,
flush resolution warns after ADF mapping using attachment-only stats,
clear attachment cache on clearToken with epoch-guarded singleflight,
and add Server comment path plus release-before-binary regression tests.

* fix(jira): simplify comment media request skip condition

Only needingCount determines whether to skip the attachment metadata
request — htmlIds alone cannot produce a download without needing
media. Add type annotation for mediaAttrs for clarity.

---------

Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com>
Reviewed with an independent reproduction on macOS and Ubuntu 20.04. Fixed five test files that asserted the old literal strings, and switched the reader to `command -p cat` so it also survives hosts without /bin/cat.
…shell (stablyai#9608)

A TUI killed hard (SIGKILL, OOM, crash) never restores the modes it armed. When
its parent shell survives, the emulator keeps mouse tracking, focus reporting
and Kitty keyboard flags on: every pointer move over the pane lands as typed SGR
motion reports at the prompt, and the doomed process burns CPU parsing the
motion firehose while it lives.

Orca's existing mode cleanups all hang off dead-PTY paths (hibernation kill,
daemon reattach), so an agent dying under a live shell crossed none of them.

Fire POST_REPLAY_REATTACH_RESET at the pane-foreground-agent tracker's confirmed
return-to-shell transition, next to the sibling stale-title cleanup. That
transition is gated on a real foreground-process read rather than the bare
OSC 133;D, because a full-screen agent's nested command shells leak their own D
onto the main PTY. The write goes through the replay guard so xterm's auto
replies cannot leak to the shell as input.

The reused constant already excludes ?2004l, so the bracketed-paste protection
the live shell re-arms at its prompt is preserved.
…ai#6487) (stablyai#10719)

* fix(release): restore the Windows inner-binary signature gate

electron-builder 26.9+ dropped the bundled 7zip-bin package, so the gate's
hardcoded node_modules/7zip-bin path stopped resolving in 1d2cd33. The
gate is fail-open, so it swallowed the error and 11 releases shipped with
no signature verification and an evidence artifact that looked clean.

Resolve 7za through app-builder-lib's toolset instead, and always record a
verdict so a degraded gate can't pass for a healthy one.

Refs stablyai#6487

* test(release): make the signing-gate structural tests assert executed code, not text

The round-2 harness matched /\bthrow\b/ and /\bcatch\b/ against raw block text, so
the word satisfied the assertion wherever it appeared. Downgrading the resolver
throw to `Write-Host "...would normally throw..."` — the exact silent fail-open
this PR exists to kill — left all 11 tests green.

Every span is now classified once (code / string / comment) by the same walk that
pairs braces, and assertions run against the string-and-comment-blanked view.
Blanking preserves length, so indices still line up across views.

Also re-anchors the catch-ordering test: `blockAfter(step, '} catch {')` picked
the first catch in the step, which stopped being the gate's own once the
persistence helpers grew theirs — moving the policy throw inside the try was
passing again.

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

* test(release): pin the evidence filename the gate writes to the one the upload collects

The upload step is `if-no-files-found: ignore`, so renaming the evidence file on
one side and not the other ships a green run whose artifact silently omits the
verdict — the same silent-degradation class this PR exists to close.

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

* fix(release): preserve 7za resolver failures

---------

Co-authored-by: Orca <help@stably.ai>
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
AmethystLiang and others added 19 commits July 29, 2026 19:50
…ai#11296)

* Link Jira issues from workspace create dialog

Add Jira issue linking to workspace creation, matching existing GitHub and Linear workflows. Users can paste Jira issue URLs in the smart name field to auto-populate workspace names and link the issue to the created workspace/worktree.

Linked Jira issues appear on workspace cards via the new 'jira-issue' card property. Implements cancellable searches and summary reads to prevent stalled requests from blocking the shared Jira pool. Persists paired issue + source context metadata with validation of provider/site identity.

Fixes git-username rate-limit handling to reject malformed JSON responses so garbage never becomes branch prefixes.

* feat(jira): link issues during workspace creation

- Display linked Jira issues on worktree cards
- Fetch issue summaries and timestamps via Jira API
- Gate Jira linking behind runtime capability check
- Preserve user-typed names during async lookups

* Enforce git check-ref-format rules in login validation

Extend isBranchSafeHostedLogin to reject usernames that git rejects as
invalid branch components: trailing dots, consecutive dots, and .lock
suffix. Prevents invalid branch names from login usernames.

* Enforce filesystem filename cap for branch-safe logins

Loose refs store logins as single filenames, so the real constraint is the
255-byte filesystem cap, not git check-ref-format rules. This allows longer
provider-agnostic logins while staying platform-safe.
Prevent setup prompt inspection, caching, dismissal, saves, telemetry, and settings navigation from leaking across local, direct SSH, and runtime-relayed hosts.
* feat(feedback): attach images to feedback submissions

Users pasting screenshots into the feedback dialog were silently dropped:
the textarea had no paste handler, the IPC payload had no image field, and
the endpoint had nowhere to put one. Reports arrived saying "images
attached" with nothing attached, which is why feedback-sourced tickets
never have a screenshot to work from.

Adds paste, drag-drop, and a file picker with thumbnail previews (up to 4
images, 8 MB each, png/jpeg/webp/gif). Rejected files raise a toast rather
than disappearing — silent loss is the bug being fixed.

Images ride the existing multipart lane, which previously activated only
for crash diagnostic bundles. Crash submissions still drop images; that
lane already carries bundles and the server rejects them there.

When the server reports imagesDelivered: false the dialog says the
feedback sent but the images did not, instead of a blanket success. A 2xx
without the field counts as delivered so this keeps working against a
server that predates the field.

Requires the marketing-site half to deploy first.

* copy(feedback): shorten attachment hint to 'Attach up to 4 screenshots'

* fix(feedback): make dropped screenshots actually attach

Three defects that discarded a user's image without telling them — the exact
failure this feature exists to fix.

Drag-and-drop never worked. `DataTransfer.files` is empty until the drop
lands, so the dragenter guard always saw zero files and the highlight never
armed. Worse, preload consumes native file drops on document capture with
`stopPropagation()` and routes the paths to the editor, so React's `onDrop`
never ran at all: dropping a screenshot on the dialog opened it in an editor
behind the modal. The drop is now claimed one phase earlier on window capture
and scoped to the dialog element, and the highlight keys off the drag types
the OS advertises — matching useComposerFileDragOver and useSidebarProjectDrop.

`crypto.randomUUID()` is undefined in non-secure browser contexts (the LAN web
client over plain HTTP), so building draft ids with it rejected the read and
dropped every image in the batch with no message and an unhandled rejection.
Use createBrowserUuid, the repo's fallback for exactly this.

`readFeedbackImageFiles` had no rejection handler, so any read failure (file
removed after picking, permission error) silently lost the whole batch.

Also: capacity was checked against a ref mirroring committed state, so two
pastes landing during an in-flight read both saw room for four and the main
process then rejected the entire submission; in-flight batches now count
against capacity. And the non-en catalogs still carried the pre-amendment
English copy for the attachment hint.

* fix(feedback): close the prototype-chain hole in the image allow-list

`contentType in FEEDBACK_IMAGE_EXTENSIONS` walks the prototype chain, so
"constructor", "__proto__", "toString", "valueOf" and "hasOwnProperty" all
cleared the allow-list. feedbackImageFilename then indexed the same object and
named the upload after the inherited value — "feedback-image-1.function
Object() { [native code] }" — and the part went out with that content type.

Only reachable by invoking feedback:submit directly (the renderer screens
types with Array.includes), which is exactly the threat model this function's
own doc comment claims to cover. Object.hasOwn matches the 54 other uses in
the repo and is identical for the four real types.

The inherited values carry no quotes or CRLF, so this was a bypassed allow-list
and a malformed upload, not multipart header injection.

Adds unit coverage for the module, which had none, plus an IPC-level case; all
six new assertions fail against `in`.

* fix(feedback): accept the drag on dragover so the drop can fire

The window-capture drop interception only fires if something first
preventDefaults `dragover`. In Electron that comes free from preload's
document-capture handler, but the same renderer is served to browsers as
web-index.html, where `installWebPreloadApi` builds `window.api` in JS and
installs no drag listeners at all. Nothing else in the renderer
preventDefaults dragover for a native file drag.

So on the web client the dialog is not a valid drop target: `drop` never
fires and the browser falls back to its default action for a file dropped
on a page — it navigates the tab to the file, taking the user's typed
feedback with it. The new types-based dragenter guard makes this worse
than before, because the highlight now arms and invites the drop that the
old `files`-based guard could never light up.

Mirrors useSidebarProjectDrop.onDragOver, which the drop rework already
claimed to match. In Electron it is a harmless duplicate of the
preventDefault preload already applied.

* fix(feedback): revoke batch previews when a read rejects partway

readFeedbackImageFiles creates the object URL for each accepted file as it
goes. If a later file in the same batch fails `arrayBuffer()` — the
removed-after-picking case the new rejection handler was added for — the
whole promise rejects and the already-built drafts are never returned, so
nothing ever revokes their previews.

Each leaked URL pins its blob for the life of the renderer, up to three at
8 MB. Release them before rethrowing; the caller's rejection handler is
unaffected.

* fix(feedback): cancel non-image drops the dialog already accepted

dragover advertises copy for every native file drag over the dialog, but
drop only cancelled for images. On the web client an uncancelled drop
navigates the tab to the file, taking the typed feedback with it.

* fix(feedback): stop image validation from aborting crash reports

buildSubmitBody drops images on the crash lane, but validation ran
unconditionally, so a crash submission carrying an invalid image would
have failed outright over attachments that were never going to be sent —
losing a crash report the user needs delivered. Gate validation the same
way body construction is gated.

Not reachable today (the IPC handler forces submissionType 'feedback' and
internal crash callers pass no images), but the two gates disagreeing is a
trap for the next caller. Raised by CodeRabbit.

Also documents why the image lane deliberately skips the 5xx retry the
text lane performs: replaying up to 32 MiB on a flaky link costs more than
it saves, and the dialog preserves the draft and thumbnails on failure.

* fix(feedback): stop mutating the image-count ref during render

React Doctor fails CI on "Ref mutated during render": the count was
assigned in the component body, where React can discard or replay work
that never commits.

Read the committed count from the callback closure instead of a ref.
Syncing the ref in an effect (the suggested fix) would reintroduce the
race a previous commit removed — right after an add, the ref is stale-low
until the effect flushes, so a paste in that window over-accepts and the
main process rejects the whole submission. The closure value is always the
committed count, and pendingImageReadsRef still covers in-flight reads.

Costs a re-registration of the drop listeners per attach, which is the
same teardown the hook already does when the dialog opens or closes.

* fix(feedback): stop an unsupported pasted image from eating co-pasted text

The paste handler consumed the event whenever the clipboard held any
image/* file, but only the four allow-listed types can actually attach.
Pasting text alongside an SVG or BMP therefore lost the text and attached
nothing — a silent loss of the user's own input, in the dialog where they
are mid-sentence.

Consume the paste only when something is attachable. Unsupported types
still route through readFeedbackImageFiles for their rejection toast, so
nothing is dropped silently; the difference is that the default paste is
left alone when we have nothing to offer in exchange.

Extraction deliberately stays broad. Narrowing it there (as suggested by
review) would skip handleAddFiles entirely, and a file paste into a
textarea does nothing visible — the image would vanish with no feedback.

The drop path is untouched: it must keep cancelling every native file drop
or the browser navigates the tab to the file.

* fix(feedback): stop the dialog accepting more than the endpoint will take

The endpoint rejects reports over 5000 characters with a 400, which the
dialog surfaces as a generic "Failed to submit feedback. Please try again."
Nothing said length was the problem, so retrying could not help — the draft
survived but the user had no way to know what to change.

Cap the textarea at the same 5000 and show a counter once 500 characters
remain, so the limit is visible before it bites rather than after. The
counter stays hidden until then; an always-on count reads as a word limit
to hit.

Extracted rather than inlined: the dialog is already past the 300-line mark
React Doctor warns on.

* fix(feedback): prevent silent attachment loss

* fix(feedback): improve attachment failure feedback

* fix(feedback): bound attachment response parsing

* fix(feedback): surface response body timeouts

* fix(feedback): harden image delivery

* fix(feedback): bound image preview resources

* fix(feedback): honor atomic image delivery response

Production’s single-message feedback endpoint uploads text and images atomically, then returns 202 {"ok":true} without an imagesDelivered field. Treating that omission as false warned users that every successful production attachment had failed.

Treat a settled successful JSON response with ok: true and no image field as delivered. Explicit imagesDelivered: false still surfaces partial delivery, while malformed, oversized, aborted, and stalled bodies remain unconfirmed or fail through the existing response bound and timeout path.
* fix(agent-hooks): skip unavailable agent homes

* refactor(agent-hooks): separate Pi and OMP home fix

* test(agent-hooks): update merged protocol harnesses

* fix(agent-hooks): avoid redundant reconciliation

* fix(agent-hooks): harden reconciliation and detection

* test(agent-hooks): cover settings reconciliation

* fix(agent-hooks): hydrate PATH for paired clients
* fix(tabs): prefer retained agent identity for icons

* test(tab-bar): include retained agent store state
* fix(terminal): log pane recovery at warn, not error

STA-2373 made this path routine: every daemon death now remounts each live
pane, so error level floods logs and crash telemetry with a message that
reports recovery *succeeding*. The breadcrumb right below is what
diagnostics actually consume.

* fix(terminal): correct recovery log comment and test console spy

The comment claimed error level floods telemetry; nothing forwards renderer
console into telemetry, and the breadcrumb below is untouched, so this change
alters telemetry volume by zero. The test's console.error spy silenced the
old call site and now stubs nothing, leaking 26 stderr lines under verbose.
…it before the serialize debounce (stablyai#9730) (stablyai#9823)

* fix(editor): flush markdown preview saves before teardown (stablyai#9730)

* fix(editor): keep rich markdown blur saves policy-safe

---------

Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
* fix(floating-workspace): route markdown renames locally

* test(floating-workspace): strengthen rename regression

* test(floating-workspace): verify rename restart persistence

* fix(filesystem): serialize local rename destinations

* fix(filesystem): serialize Unicode rename aliases

* fix(filesystem): align rename locks with native aliases

* fix(filesystem): canonicalize rename parent locks

---------

Co-authored-by: Dzmitry Bachko <dbachko@users.noreply.github.com>
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
* fix: make remote server pairing failures actionable

* refactor: extract daemon router event types

* fix: address remote pairing review findings

* fix: address final remote pairing review feedback
…ai#11521)

Match Run on field layout so short provider details like stablyai/orca
sit on the far right of the committed Project field instead of next to the name.
)

* perf(orchestration): bound mutation ledger and run pages

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

* fix(orchestration): close retention pagination gaps

* fix(orchestration): preserve unpaginated run listing

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

* fix(orchestration): reject malformed run cursors

---------

Co-authored-by: Orca <help@stably.ai>
Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com>
…#11422)

* perf(terminal): serialize checkpoints with one payload walk

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

* fix(terminal): bound checkpoint serialization

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

* test(terminal): correct bounded serialization proof

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

* test(terminal): cover over-limit multibyte checkpoints

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

---------

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

Copy link
Copy Markdown
Owner Author

Sync update (bae2e4e42)

@DoDAON Thanks for the check-in — still on this, and the remaining two activation sites are now wired.

Headless (headless-emulator.ts)

  • Sets East Asian Ambiguous mode before activateOrcaTerminalUnicodeProvider
  • Runtime passes store.getSettings().terminalEastAsianAmbiguousWidth when creating PTY mirrors
  • Main process also syncs the process-wide mode on settings load/change so live headless buffers stay aligned with the renderer (SSH fidelity)

Mobile (terminal-webview-html.ts + engine)

  • Bundles Orca unicode into the WebView engine (window.OrcaTerminalUnicode)
  • After Unicode11, activates the Orca provider (this path previously stopped at Unicode11 and bypassed Orca entirely)
  • init accepts eastAsianAmbiguousWidth; wide mode also inserts CJK UI fonts ahead of Consolas (same coupling as desktop)

Tests

  • Headless: wide vs narrow ① cell advance
  • Mobile: activate order (open → Unicode11 → Orca → replay); Chrome 74 parse floor still green

Happy to adjust if you want mobile to pull the host setting via settings.get at session open as a follow-up (prop/init path is ready).

DoDAON / stablyai#9958 asked for all three activation sites. Desktop already set
mode + OrcaUnicodeProvider; this pass completes the twins:

- Headless: honor eastAsianAmbiguousWidth (runtime store + settings sync)
- Mobile: bundle Orca unicode into the WebView engine and activate after
  Unicode11 (was bypassing Orca entirely); CJK font stack when wide
- Provider as plain object so the Chrome 74 engine floor parses cleanly

Evidence: headless-emulator-unicode-width + mobile engine/html tests
Place TERMINAL_EAW_JS after the default terminalFontFamily assignment so
vm-based font fallback tests still slice a valid isIOSWebView block.
@innocarpe

Copy link
Copy Markdown
Owner Author

Sync update (26f03fc82)

Follow-up: 1cb8121 + font-extract test fix are on the branch (earlier sync comment referenced a pre-push tip).

@DoDAON — remaining activation sites are wired as described above. Full headless/mobile coverage for stablyai#9958 is in this PR tip.

nwparker and others added 5 commits July 30, 2026 02:05
* fix(worktrees): stop terminals after external deletion

* fix(worktrees): request teardown per caller and revalidate uncached

Two defects let the original fix silently strand PTYs:

- teardown rode the scan's coalescing promise, so any caller that joined an
  in-flight scan purged its renderer state without ever asking for a sweep;
  it now runs per caller against its own known-id snapshot, deduped on the
  request it actually produces so fan-out still shares one host sweep.
- the runtime's authoritative recheck was served from the 30s worktree-scan
  cache, which can still list a directory git already dropped. The renderer
  purges either way, so a stale miss leaked those processes permanently.

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

* perf(worktrees): enumerate the host once per teardown sweep

An agent cleaning up N workspaces made killAllProcessesForWorktree issue one
full provider enumeration per missing worktree: O(N) relay round-trips carrying
O(N^2) rows. At 30 worktrees over an 80ms-RTT SSH link that is 30 scans and
~1.3s of stalled teardown; it scales linearly from there.

Share one point-in-time process list across the sweep — every worktree in it is
already known-missing, so a single snapshot answers all of them. A failed scan
is never shared: it falls back to a per-caller scan so one transient relay error
cannot suppress the sweep for the whole batch. Pinned requirePhysicalStop:false
since that path re-lists after shutdown and must not read a pre-shutdown snapshot.

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

* test(worktrees): pin the disconnected-SSH no-teardown invariant

main's new directSshAuthority gate bails before any refresh when an SSH target
is not connected. That is exactly the stablyai#10562 safety rule — "host unreachable"
must never be read as "worktree deleted" — so pin it: a disconnected target
issues no teardown RPC and keeps its renderer state.

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

* fix(worktrees): keep selector grammar intact when scoping by connection

resolveRepoSelectorForConnection matched the selector as a bare repo id, so an
explicit connection identity silently changed the grammar: `path:` and `name:`
selectors resolved to repo_not_found on that path alone, losing the whole sweep.
A connection identity should only *narrow* the candidate set.

Extract the selector matching both paths now share, and stop re-resolving an
already-resolved repo: teardown rescanned via `id:<repo.id>`, which throws
selector_ambiguous when an id is duplicated across hosts even though the
caller's own selector was unambiguous.

Reported as a P2 by Greptile (as redundant work); it is load-bearing.

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

* fix(worktrees): keep the shared snapshot out of provider internals

The snapshot proxy passed itself as the Reflect.get receiver, so prototype
methods invoked through it ran with `this` bound to the proxy. A provider whose
own shutdown() re-read state via `this.listProcesses()` would then silently get
this sweep's cached snapshot instead of the live host — batching leaking past
the calls it was built for.

Bind non-listProcesses members to the target so only the sweep's own calls share
the snapshot. No shipped provider does this today; the point is that adding one
must not quietly change teardown semantics.

Raised by Greptile as an undocumented implicit constraint; closed structurally
rather than by comment.

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

---------

Co-authored-by: Orca <help@stably.ai>
…idebar (stablyai#11530)

* fix(sidebar): stop background workspace creation from scrolling the sidebar

Creating a workspace in the background still spawns its terminals, and the
renderer treated "no presentation stated" as "point the user at this
terminal" -- revealing (scrolling to) the owning workspace.

Split adoption from surfacing with an explicit surfaceOwner flag: background
worktree creates and worker dispatch adopt their tabs silently, while
`orca terminal create` keeps its discoverability reveal.

* fix(sidebar): keep split-mode setup panes silent, tighten surfaceOwner

Review catch: with setupScriptLaunchMode split-vertical/horizontal the Setup
terminal goes through splitTerminal, whose reveal payload had no surfaceOwner,
so a background create still scrolled the sidebar in that configuration.

Also narrow surfaceOwner to `false` so "surface it" can only be expressed by
omitting the key, and fold the repeated conditional spreads into ownerSurfacing.
…i#11528)

* fix(relay): refuse silent fallback when pairing invite fails

When Orca Relay pairing fails, don't silently degrade to a LAN-only QR under the Relay label. Instead, surface structured failure information so the UI can clearly inform the user and offer recovery options.

* fix issues
…yai#11513)

* fix(remote): recover stalled terminal streams

* fix(i18n): localize manual disconnect error

* fix(remote): park paired terminals with host snapshots

* test(remote): mock authoritative resync snapshots

* fix(terminal): defer startup mounts until hydration

* fix(remote): raise paired terminal stream capacity

* fix(remote): harden terminal recovery lifecycle

* fix(remote): preserve calls across control refresh

* test(remote): harden paired recovery oracle

* test(workspace): seed Jira source context

* test(remote): assert raw host terminal identities

* test(terminal): keep restore sentinels atomic

* test(terminal): keep restore sentinel on one row

---------

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
Resolve conflict with upstream wheel-scroll inject (keep both EAW + wheel).

Review (Greptile stablyai#10078):
- Remove BMP PUA U+E000–U+F8FF from ambiguous ranges so Powerline/OMP
  icons stay single-cell in wide mode.
- Set process-wide EAW mode once before the appearance pane loop.
- Extract HeadlessEmulatorOptions to stay under max-lines.

Evidence: east-asian-ambiguous-width PUA tests + width suites.
@innocarpe

Copy link
Copy Markdown
Owner Author

Sync update (46640c27f)

Merge + review follow-up (46640c27f)

  • Rebased/merged onto latest main — conflict was only mobile imports; kept both TERMINAL_EAW_JS and TERMINAL_WHEEL_SCROLL_JS.
  • Greptile P2: dropped BMP PUA U+E000–U+F8FF from the EAW ambiguous table so Nerd Font / Powerline / OMP icons stay single-cell in wide mode (with regression tests).
  • Greptile note: setTerminalEastAsianAmbiguousWidthMode now runs once before the appearance pane loop (not per pane).
  • Extracted HeadlessEmulatorOptions to stay under max-lines after the main merge.

Evidence: east-asian-ambiguous-width.test.ts PUA cases + existing width suites.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.