The empty-chat check read the pane's old draft key, while the composer and
every hand-back write the conversation's. A chat with typed or given-back
text read as empty, so a bare new chat reused it and a text request claimed
it. It reads the conversation's draft now.
Drafts save to IndexedDB, which commits after the hand-back returns, while the
outbox copy of the message was removed at once. A crash in between lost the
text. Now every hand-back (a send's answer, the journal, Stop, closing a chat,
an older build's entry) commits the entry marked `returning` first, writes the
draft, and removes the entry, firing its ending (which clears its notes), only
once storage confirms the draft. A refused save keeps it returning.
A returning entry is never sent again (its id proved no record, so a resend
would be a new first send), holds nothing up and draws nothing. On load, one
still marked re-runs its hand-back, which the draft's suffix rule and image id
check make safe to repeat, then leaves once saved.
The notes-side wait from the previous commit goes: the ending itself now fires
only after the draft is saved.
Drafts now save to IndexedDB, which commits after the hand-back returns, while
the message that carried the notes leaves the outbox at once. Clearing the
notes then could leave neither if Orca crashed before the draft landed. The
notes stay held off the shelf until storage confirms the draft (and, if it
refuses, until a later save lands), and are cleared after that. A message the
host took clears its notes at once, as before.
Takes #24905's new layout: drafts in IndexedDB behind the storage adapter,
the startup load, owner-stamped records deleted by owner, and early appends
that are read-modify-writes replayed on the loaded draft.
C2's hand-backs now go through that append, with their checks inside it:
- returnNativeChatDraftText's suffix rule runs on the draft as it is and again
on the loaded draft, so a hand-back repeated before the load lands (a crash
between the save and its copy's removal) still comes back once;
- the image hand-back's id check runs the same way, keeping the SSH connection.
A test pins both against a slow load.
Main's #24489 deletes the in-memory path grant system: every desktop file
request now declares its access kind, and the composer preview reads a paste
as a chat image. The paste code this branch carries from #24905 still granted
its files, so:
- a composer paste is written without a grant, and a restored paste is kept
only when its real path is a file inside the paste folder, as before, with
no grant for either spelling;
- the tests keep every containment case and drop the grant assertions; a
restore still never makes an outside file readable.
The composer attachment cache keeps this branch's draft-store reader.
* fix(editor): read files outside projects without a grant a restart loses
A file opened from outside every project (e.g. ~/notes.txt from the floating
workspace) read through an in-memory grant. After a restart the restored tab
only renewed that grant when it stored a full path, so a tab saved relative to
the floating workspace folder failed with "Access denied" and Retry repeated it.
Single-file reads (read, stat, exists) and open-editor-tab saves now resolve a
path outside every project in place. Paths inside a project keep the full
containment check, so a project's symlinks still cannot escape it, and every
other write stays inside projects.
* fix(editor): re-grant restored floating-workspace tabs by owner, not path shape
Problem: a file opened from the floating workspace (e.g. ~/notes.txt via
Cmd-click in the floating terminal, the floating markdown picker, or a .md
opened from the OS) loads until restart, then shows "Access denied: path
resolves outside allowed directories". Main's external-path grants live only
in memory. On restore the editor re-granted only tabs that stored an absolute
path, but floating tabs store a path relative to the floating root (~ by
default), which is deliberately not an authorized root, so they were never
re-granted. Restored floating notebooks also failed to start a kernel.
The previous commit on this branch let main read and save any path outside a
project without a grant. That widened fs:readFile/stat/pathExists for every
caller, including automatic reads of untrusted content (markdown preview
images), which opened a Windows UNC credential leak and a /dev/zero
main-process memory blowup. This reverts that model entirely.
Fix: one helper decides which client-local path a tab needs re-granted by
ownership: a floating-workspace tab, or a tab stored outside its own project.
It never grants paths a local project root covers (a grant would also
authorize a project symlink's outside target), and skips SSH-owned,
runtime-owned and not-yet-hydrated owners. Every reader that can touch a
restored tab before or without the editor loader uses it: the loader, the
restored dirty-tab conflict scan, and the paired-mobile markdown bridge.
* fix(editor): let main decide which restored-tab paths a project already covers
Problem: the restore re-grant helper decided "already inside a project" in
the renderer from its worktree list. At startup that list only holds repos
the session references, so a floating tab inside an unlisted repo was granted
(including a symlink's outside target), and the renderer's path matcher
disagrees with main's on WSL \\wsl$ vs \\wsl.localhost, which stranded a
folder-workspace tab with "Access denied" after restart. The helper also
treated a folder workspace with a missing or ambiguous host as local.
Fix: the renderer now decides only by owner (a floating-workspace tab, or a
tab stored outside a project whose owner is explicitly local) and asks main
with `skipIfInsideAllowedRoots`. Main checks the path against its own allowed
and registered roots, in both the named and canonical-parent spelling against
both root spellings: a path a project covers gets no grant, an alias spelling
of a project path gets only that spelling, and a project symlink's outside
target is never granted. Explicit-open grants (Cmd-click, drag, explorer) are
unchanged. Tests now prove each reader waits for the grant before reading.
* fix(fs): decide a restored tab's project membership from every ancestor's real path
Problem: the restore re-grant decided "inside a project" from the named path
and the real path of its parent only. When a tab path crossed a project
directory symlink and named the project through a spelling that was neither
the registered root nor its realpath (a second alias, a `..` segment, a case
variant on a case-insensitive disk, a /var-style alias of an ancestor), no
check matched, the path took the full grant, and the symlink's outside target
became readable and writable.
Fix: a path is inside a project when the named path is inside a root, or the
real path of any ancestor folder is inside a root in its registered or real
spelling. Such a path gets at most its named spelling, never its realpath. An
ancestor that fails to resolve for any reason other than "missing" now fails
closed to the named-spelling grant instead of falling through to the full one.
Tests cover each spelling; the non-symlink case also runs on Windows.
* fix(fs): read local files as regular files only, from one bounded handle
Problem: fs:readFile stat'ed a path and then read it to EOF. A character
device such as /dev/zero reports size 0, passes the size limit and never ends,
so the main process buffers until memory runs out; a FIFO hangs the open.
Writes could also target an existing device or FIFO.
Fix: every local fs:readFile (editor and log snapshot) opens the path once,
non-blocking, refuses anything but a regular file, and reads the size check,
binary probe and content from that same handle, capped at the limit even if
the file lies about its size. The AI Vault log tail opens non-blocking too,
and fs:writeFile refuses an existing non-regular target.
* feat(fs): let desktop file requests declare their shape
Problem: main decided every local file request against one allow-list plus a
set of in-memory grants the renderer had to recreate after every restart, so
a file the user opened outside a project (for example from the floating
workspace) was denied once Orca restarted.
This adds the request shape the common pattern uses, alongside the grants for
now:
- no shape (the default): the path must be inside a project root main
recognises, symlinks included. Desktop requests also accept the app-owned
floating-workspace folder; paired-client RPC never does.
- user-file: a single file the user named by absolute path, used in place.
Only fs:readFile/stat/pathExists and saving (fs:writeFile) accept it.
- document-resource: an image or PDF a document references, limited to every
project root when the document is in one, else to the document's folder,
and refused by path text before any disk or network access.
Notebook kernels and AI Vault log tails check their open file as user-named.
* feat(editor): send each local file request's shape from the renderer
Problem: after a restart, a tab opened outside every project (a floating
workspace file, a file opened by absolute path, an OS-opened markdown) could
only be read if the renderer first re-granted its path, and readers that ran
before the editor loaded the tab had no grant at all.
The renderer now says what kind of request it is making, and main checks that:
- A persisted tab opened outside its owner's root (floating workspace, or an
absolute stored path) whose owner is explicitly local reads and saves as a
user-named file, from every reader: the editor loader, the restored-tab
conflict scan, the change banner and compare dialog, the paired-phone
markdown bridge and the save queue. Project tabs stay inside their root.
- Clicks, drops, typed paths and browser-opened notebooks stat as user-named.
- Markdown preview and rich-editor images are document resources, limited to
the document's roots or folder. Images the user pasted or attached into a
chat show as user-named; agent images stay inside the project.
- The image cache keys on the shape, so one shape's image never answers
another's request.
A ratchet test lists every renderer file allowed to create a user-named
request.
* refactor(fs): delete the in-memory path grant system
Problem: main kept a set of paths the renderer had asked it to allow
(fs:authorizeExternalPath). The set lived only in memory, so a file the user
opened outside every project could be read until Orca restarted and was then
denied, and every new reader of a restored tab had to remember to recreate
the grant first. Three rounds of re-deriving grants at restore each found
another reader or path spelling it missed.
Now that every desktop request declares its shape, nothing needs a grant:
- delete the grant set, authorizeExternalPath, the restore re-grant from the
earlier commits on this branch, the fs:authorizeExternalPath channel and its
preload and web-client entries;
- delete every renderer grant call (terminal and markdown link clicks, drops,
typed paths, the file explorer, AI Vault logs, chat attachments, browser
notebooks) and every main one (floating markdown picker and folder, OS-opened
markdown, keybindings.json, pasted images, import and upload sources);
- the floating workspace's picker-approved folders stay a terminal-cwd
allowlist only.
Main now holds no per-path permission, so a restart can't change any answer.
* feat(editor): open project links that lead outside the project as named files
Problem: a file inside a project that is a symlink to something outside it
opened fine from the file explorer or a terminal Cmd-click, then showed
"Access denied" after a restart: its tab was stored as a project file, and a
project request is refused when it resolves out of the project. A folder link
out of the project expanded in the explorer until restart and then failed with
a raw access error.
Now the click decides and the tab keeps that decision. Both gestures stat the
path inside the project first; if only the user-named check passes, the path
leads out of the project:
- a file opens by its absolute path, so it reads and saves as a file the user
named, the same before and after a restart;
- the explorer does not follow a folder link out of the project and says so
("This folder links outside the project, so it can't be opened here.").
Paths that stay inside the project still open as contained project tabs. Also
drops the AI Vault "path not authorized" message, which nothing shows now.
* chore: drop the casts the changed-code quality gate flags on this branch
The FileContent casts in the editor loader and the paired-phone markdown
bridge were never needed (the read result is already assignable). Tests stub
window.api through vi.stubGlobal and pass narrow stores without casting; the
one test store that still needs a cast states why.
* fix(fs): load chat images by type, and keep escaping project links readable
Problems found in review:
- Chat transcript images were trusted by message role: any user-role
"[Image: source: <path>]" (an injected Claude record, `orca terminal send`,
a paired client's image-ref) became an automatic user-named read as the row
scrolled into view, of any file type, and on Windows a network-share path
would have opened an SMB connection to that host.
- A document image named like an image but linking to a text file
(logo.png -> .env) was read as text.
- Windows device names (NUL.png, COM1.jpg) passed the path-text check of the
automatic image loads.
- A project symlink leading out of the project, opened by a typed path, a
tab-strip drop or a browser file:// notebook, was stored as a project tab
and immediately refused.
Fix:
- New chat-image request shape for every transcript image and the composer
preview, whoever's turn named it: an absolute local path whose requested and
real targets are image files, a regular file, size-capped; network-share and
device-namespace paths and Windows device names are refused by path text
before any filesystem call. Pasted screenshots still show after a restart,
and agent images outside the project now render.
- Document resources check the real target's type too, and refuse Windows
device names by path text.
- Typed paths, tab-strip drops and browser notebooks stat through the same
check as the explorer and terminal, and open an escaping link by its
absolute path.
- Tests pin the shape at the change banner, compare dialog, markdown preview
and image prewarm; a second ratchet lists every file that can open a tab the
tab rule reads as user-named, and its comment says what it can't see.
- Stale grant wording removed.
* fix(fs): tighten automatic image loads and the project-link check
Problems found in review:
- Two unit tests went red on this branch: the browser-share test still
expected reads without a shape, and the rename test's electron mock had no
app, which the desktop root check now needs.
- The device-name check ran on the raw path, so `NUL.png\.` or
`COM1.png\x\..` (reachable from markdown ``) reached the
filesystem; a document image whose real target was a device name passed.
- Chat images in a project that lives on a Windows network share no longer
rendered, though the markdown preview showed them.
- Any failed project check (a missing file, a dropped connection) was taken
as "this link leads out of the project" and opened as an absolute tab.
- Every local read allocated about 2 MiB, even for a tiny image.
Fix:
- Device names and device-namespace paths are checked on the resolved path
and on the real target, for chat images and document resources alike.
- A network-share path in an automatic load is read only inside a project
root (the user chose that share when adding the project); anywhere else it
is still refused by path text before any filesystem call.
- Only main's "outside allowed directories" refusal marks a project path as
leading out of the project; other errors surface as before. The message now
lives in shared code so both sides agree on it.
- Reads size their first buffer from fstat and confirm EOF with a 1-byte
probe; a file that grows past its reported size is still read in bounded
chunks up to the cap.
- Fixed the two red tests.
* refactor(fs): name file access by its role, not its structure
Problem: the static-analysis anti-slop check failed the PR because the new
code named the request's file access a "shape" (`shape`, `RequestShape`,
`TabShape`), which describes structure rather than the role.
Rename the main-process module filesystem-request-shape.ts (and its tests) to
local-file-access-resolution.ts, rename the symbols to fileAccess,
FileAccessResolution and TabFileAccessFields, and say "file access" or
"access kind" in the comments and test names. No behaviour change.
* fix(fs): refuse every Windows device-name spelling in automatic image loads
Problem: the device-name check split a file name only on '.', so names such
as NUL:.png, COM1:.png, NUL:stream.png (an alternate data stream) slipped
through, and CONIN$, CONOUT$, CLOCK$, COM0 and LPT0 were not listed. Those
reached the filesystem from a document or chat image before being refused.
Split on ':' as well, list the missing device names, and test each with
Windows path rules and zero filesystem calls. Also cover the case of a local
link that leads onto a network share outside every project (refused for chat
images), and correct the shared comment on chat-image access.
* fix(editor): let users rename and insert images into files opened outside projects
Renaming a file opened outside every project (tab double-click, editor
header) and inserting an image into such a markdown document failed with
"Access denied", even before a restart: both writes only passed the
project-root check. Document resources and chat images were also limited
by file type more strictly than users expect.
- Add a "document-folder" access kind for writes beside a document the
user opened: main allows renaming only that document, to a name inside
its own folder, and importing new files only into that folder, checked
by path text and again by real path, with Windows device names refused.
The renderer sends it only for local user-named, writable tabs (rename,
its undo/rollback, image insert); SSH and runtime requests never carry it.
- Document resources: drop the image/PDF type allowlist; folder
confinement, regular-file reads, the cap and path-text refusals remain.
- Chat images: judge only the real target's type, against every
previewable image type (AVIF added).
* fix(fs): a declared file-access kind never refuses what the project check allows
A full-path tab for a file inside a project (for example a link that
leads out, opened by its absolute path) was renamed under the
document-folder rule, which limited the new name to the file's own
folder, although the same rename with no declared access could move it
anywhere in the project. Any declared kind could be stricter than the
default in the same way.
Every desktop local file request now goes through one resolver,
resolveLocalRequestPath: it runs the default project check first (roots,
Orca's floating folder, symlink containment, outside-root path text
refused before any filesystem call) and only on a refusal applies the
declared kind's rule, which adds paths outside projects. Reads, saves,
rename source and target, and import destinations all use it, so a new
kind gets the rule for free. Automatic loads (document and chat) still
refuse Windows device paths and names by text first, even inside a
project; a device is never a file to show.
The document-resource rule no longer needs its own project branch, and
chat images no longer re-run the roots check for shares.
* fix(fs): symmetric outside-project renames, notebook real folder, same-share images
- Renaming a file opened outside every project accepted a name in a
subfolder (`archive/todo.md`), but the Undo and the rollback rename,
declared from the moved file, were then refused and the file stayed
moved. A rename under document-folder access must now land directly in
the document's own folder (checked by path text before any filesystem
call), so rename, Undo and rollback are symmetric. Image import still
accepts the folder or a folder under it. Inside projects the default
check still allows any in-project target.
- A notebook opened through a link inside a project started its kernel in
the link's folder instead of the real file's folder (main's behaviour),
because notebook and AI Vault log-tail paths skipped the project check.
Both now resolve through resolveLocalRequestPath (project check first,
then the user-file rule).
- A markdown file opened from a Windows share outside every project could
not show the images beside it. Document images on a share are now
allowed inside the document's own folder; the folder text check refuses
every other host and share before any filesystem call.
- resolveDesktopAuthorizedPath is async, so a synchronous failure in the
default check rejects like any other refusal.
* fix(fs): refuse share images outside projects again; keep renames and kernels as on main
- Reverts the same-share document image rule from the previous commit.
Its folder check compared hosts case-insensitively, so a host spelled
with U+212A KELVIN SIGN (or a decomposed accent) passed as the
document's own share and was contacted, reopening the network
credential leak. Document and chat images on a share outside every
project are again refused by path text before any filesystem call;
tests now cover the look-alike hosts with zero filesystem calls.
- Renaming a file opened outside every project into a project folder
passed the project check, but its Undo (declared from the new path)
was refused and the file stayed moved. When the rename source is
allowed only as the opened document, the new name must now land
directly in the document's folder even if a project would accept it
(resolveLocalRenamePaths).
- A notebook opened through a link outside every project started its
kernel in the link's folder; main used the real file's folder. The
kernel cwd is now the real file's folder in every case.
* fix(fs): a file opened outside every project renames to any path, and keeps its access
Renaming a document the user opened (floating workspace or full-path tab) now
follows the user-file rule: the source must be the opened document, and the
new path can be any absolute path, so a rename into another folder, a
subfolder or a project works, and its Undo (declared from the moved file)
comes back from there. Any other rename keeps the project check only. Remove
the same-folder rename rule and its tests; image import stays in the
document's own folder.
After a move, a tab stored by its full path keeps its full path instead of
being recomputed project-relative, so it keeps user-file access for save,
the next rename, image insert and restore after restart. Folder moves go
through the same remap.
Also un-export unused resolver exports and avoid a copy for single-chunk reads.
Brings main through d3afb5c5a9, which includes #24606 as it landed: in the
native chat that is the version C2 already merged, so those files keep C2's
resolution, and the locales keep C2's messageNotConfirmed line beside main's
new keys.
From #24918:
- Not-sent rows come back at their journal places, so the phone draws them
where the host recorded them.
- A command's reply stays silent only once the loaded journal draws it as not
sent (structuredAgentSessionJournalShowsRejection), so a pending command a
Stop then withdraws still gets its reply.
- The phone's echo matching no longer double-counts not-sent rows.
- The delivery-line tone test is taken in C2's terms: a recorded rejection
reads muted from its row or from this client's copy, and a message on its
way reads only as sending. #24918's `notSent`/Retry notice shape stays out.
Takes #24917's round 7 as written: a new chat with no text reuses an empty
chat (still starting, or published and idle) in the tab group it was opened
from, with the group threaded from the provisional tab to the launch request.
C2's requestId, carriedNoteKeys and notes hand-off wiring is unchanged.
- The empty-chat check reads the composer's images from the draft store, not
the composer hook: the launch path must not load the composer, which closes
the store import cycle C2 already removed once.
- Two cases pin carried notes beside an empty chat: a notes send (it always has
text) never reuses an idle chat and stages its one message with its notes,
in its own chat or in the starting empty chat it takes.
#24918 and C2 already agreed on everything this head adds over main except
wording sources, so the resolution keeps C2's mechanism:
- A rejected /compact is case 1, shown muted and said once. The command's reply
stays silent only while the journal draws it: a withdrawn, card-held or
superseded copy still speaks (structuredAgentSessionJournalShowsSubmission
now reads the shown-in-place set).
- The phone draws recorded rejections in place with its live card ids.
- The row's words come from the host's reason and fact, through C2's
send-failure words module (send-disposition stays deleted).
- The not-sent line keeps C2's `muted` flag; #24918's `notSent` field is not
taken, and its Retry-era outbox notice tests stay out.
- The list test for a rejected /compact is taken, on C2's notice signature.
Since "Sending…" landed, an unconfirmed entry the probe still resends reads as
sending, so the case no longer tested the doubt tone. It is now one the user
already retried, as main's notices test does.
- Before the load lands, an append (text or images given back, a paste)
reads the stored draft and writes it back with the append in one storage
change, so a load that failed and was retried can never lose the saved
draft. On landing, only appends that load could not have read (made after
it began reading, or never written) are applied again, by in-run order
rather than by comparing clocks.
- The editor saves a value set from the store when this window has a change
of its own behind it, so a mention accepted next to a skill chip keeps the
chip; another window's draft or a late load still isn't saved back.
- A refused draft is journaled again; the 256,000-character cap bounds it.
- The draft load starts before the startup chain, and the first write
starts it in a window whose startup never did; startup still waits for it
before the session's tabs mount.
Restore IME Enter protection in workspace details by reusing the existing composition tracker. Reset Notes ownership at textarea detachment and preserve sizing behavior. Repair isolated native test-window delivery without changing the production foreground policy or original native input assertions.
Fixes#24097
Related contributor history: #10711, #11067, #13128, #13282.
Original implementation and macOS recordings: @setodeve, commit b30f095.
Verified on required stock Linux X11/Wayland checks and independent frozen-source review.
Co-authored-by: setodeve <keinick11@outlook.com>
Main's #24710 draws a message Orca recorded and then rejected where the host
placed it, as "Not sent", from the host's own history. The host side (the
rejection moves the message to its place; a failed start writes its rejections
and its row in one transaction) is taken as written.
On the client, C2's settlement stays the only place an outbox entry ends:
- #24710's reconcile module is folded into C2's: an entry whose rejected row is
not loaded yet stays, never sent again, and draws the message until that row
loads (or the host's window for its id ends). Nothing new is stored; the held
submission says it was rejected. A batch that settles nothing writes nothing.
- One "not sent" surface: main's rule hides a rejected message a live card
holds or a later copy of the same text superseded, in the rows and the
notices alike, on the desktop and the phone. The notice stays C2's muted one,
and notices are kept as the same objects across unchanged batches.
- No Retry: a message refused before it was recorded still goes back to the
conversation's draft with the reason, as before.
- A rejected /compact stays in the chat (#24918's rule), since the command's
own reply says nothing once the journal shows it.
- The phone and the desktop both draw these rows; only the host's outline,
which older clients read, leaves them out.
Echo matching skipped every row shown as not sent, so a send whose own row
first appeared already rejected was never settled: a "Delivery unconfirmed"
banner followed 20 s later, or its bubble stayed beside the row. A send now
records the not-sent rows already present when it went out, as it records the
queued cards; only those claim nothing, and a later one is its own.
* fix(native-chat): no "unconfirmed" line while Orca resends a send on its own
A send whose answer was lost (for example after reopening the chat mid-send)
showed "Message delivery is unconfirmed." with a Retry for the moment before
the automatic resend under the same message id confirmed it. One rule now
decides both: the resend runs, and the notice stays quiet, only while the
send's answer is lost, the user never retried it, it was not still going out
when the user pressed Stop, and the host has no record of it yet. Once the
host has any record of it, the line and Retry are exactly as before.
* fix(native-chat): say "Sending…" on a message until the host confirms it
A send whose answer was lost was resent quietly under the same id but looked
like any delivered message. Every message still in the outbox with no failure
to show now says "Sending…", muted, in place of its time, until the journal
holds a row for it. Rows that say a message did not go through keep only that
line and its Retry. The notices read the outbox through the same reconcile as
the transcript, so a row that lands clears the marker in one step.
* fix(native-chat): keep "Sending…" until the host has the message, in the time's slot
A message the user retried, a second message in doubt, or one requeued over a
rejected row had a journal row that did not hold it, so it showed nothing and
looked sent. "Sending…" now stays until the row is pending or accepted.
The marker took the place of the whole meta row, so the copy button went away
while sending and the row jumped when it cleared. The row now stays mounted:
copy keeps its hover reveal and "Sending…" sits where the time goes.
* test(native-chat): move the delivery probe tests into their own file
NativeChatStructuredSessionDelivery.test.tsx went over the 800-line lint
limit after main was merged. The eight tests for the automatic probe of an
unconfirmed message move unchanged to
NativeChatStructuredSessionDelivery.probe.test.tsx, which uses the shared
structured-session test harness for its mocks. The outbox seeding and probe
clock helpers move into that harness so both files share them.
The at-most-once reliability gate now lists the new file, with a fresh
evidence run.
* fix: make keep-awake tooltip lid behavior platform-aware
Extracted platform-specific lid behavior notes into a reusable function and
updated the keep-awake tooltip to show accurate descriptions for each OS:
macOS explains that closing the lid may still sleep the device, Windows
references device power settings, and Linux clarifies Orca's lid-close request
behavior. Updated copy to be device-agnostic instead of Caffeinate/MacBook
specific.
* fix: clarify macOS keep-awake tooltip copy and add translations
Update agent keep-awake descriptions on macOS to accurately explain that the feature prevents idle sleep (not all sleep modes) and only works with the lid open. Add translations for Spanish, French, Japanese, Korean, and Chinese.
Notes sent from the right-hand split could land in, and focus, an empty chat still starting in the
left-hand one. A request with text now claims an empty starting chat only in the tab group it opens
in, the same rule a request without text follows; elsewhere it opens its own chat there.
* fix(terminal): leave the alt screen before replaying a pushed host snapshot
A remote terminal running a full-screen agent (Claude Code) could show old
output (e.g. a setup script's pnpm log) interleaved with the agent's screen
after switching back to the tab. The host's pushed snapshot is a serialized
image that starts on the normal buffer and enters the alternate screen
itself, but the replay drain cleared with ESC[2J without leaving alt. The
image's history painted into the agent's screen, its own ?1049h was a no-op,
and the agent's diff paints landed on top of the stale cells.
The remote-runtime transport now marks snapshots as serialized images, and
the drain grounds them with the shared snapshot prologue (switching to the
normal buffer first). Raw byte replays (SSH relay ring buffers) keep the
in-place clear.
* fix(terminal): paint folded remote snapshots from the normal buffer everywhere
Review follow-ups:
- Rename the flag to carriesNormalBuffer: what the painters rely on is that
the image starts on the normal buffer and enters alt itself.
- Hidden-output restore had the same bug for remote requested snapshots
(history folded into data, alternateScreen set): the painter entered alt
first and painted the normal buffer into the TUI's screen. Requested remote
snapshots now carry the flag and take the normal-buffer start.
- Flag pushed snapshots replayed after a cancelled shutdown too.
- Pushed snapshots carry only the screen, so over a live TUI the drain keeps
the normal-buffer history it covers instead of wiping it.
- Read the pane's buffer after queued output parses.
- Tests compare whole buffers against a fresh terminal, use production image
shapes, and pin the raw-replay path with the same oracle.
* fix(terminal): let the replay drain own the recovery snapshot clear
The recovery prefix's own \x1b[3J ran after the drain's prologue and wiped the
history the drain keeps under a live TUI. Keep only the latch release, which
still makes an empty recovery snapshot non-empty so it is applied.
* fix(terminal): keep the recovery snapshot's own screen clear
Consumers that write recovery snapshots straight into xterm (no replay drain)
rely on the prefix clearing the stale screen. Restore \x1b[2J\x1b[H and drop
only \x1b[3J, which wiped history the image does not carry.
* test(terminal): drive the drain with the real recovery payload
The multiplexer prepends its own screen clear; replaying that exact payload
pins that the prefix cannot wipe the history a TUI covers.
* fix(terminal): keep TUI-covered history only when the grids match
Second review follow-ups:
- A pushed image carries only the host's screen; the pane's frozen history
continues it exactly only on the same grid. On another grid keeping it
duplicated or dropped lines, so the image replaces it there.
- Tests replay the pane's writes and grid changes into a real terminal and
compare whole buffers with the host, including a mismatched-grid case.
- The split branch shares the normal-buffer preamble; drop the now
single-use abort helper.
- Type serializeBuffer as RemoteRuntimeSnapshotImage so the flag is carried
by type, not by object pass-through.
- Register the grid and raw-replay cases in the reliability gate.
* fix(terminal): keep TUI-covered history only while the host is still on alt
The drain kept the pane's history whenever the pane was on the alt screen. If
the host's TUI exited while the tab was hidden, the shell wrote past that
history and the kept lines no longer continued the host's screen. Require the
host's own alternateScreen too; an absent flag proves nothing, so the image
replaces history as on main.
Also: one buildSnapshotReplayPreamble for every first replay write, the drain's
image and raw clears as separate branches (raw byte-identical to main, comment
restored), and a single paneAtSourceGrid check.
* fix(terminal): gate kept history on the host's shell-owner proof
The host sends alternateScreen only with terminalOwner 'shell', i.e. once it has
proven the TUI exited, so `alternateScreen === true` never held for a live TUI
and the drain wiped the history it should keep. Replace the history only once
the host proves the TUI exited; tests now use production snapshot shapes. Move
the relay-overlap comment into the raw-replay branch it describes.
* fix(terminal): keep TUI-covered history only on a proven shared grid
An image without its grid cannot prove the pane's history continues its screen,
so it now replaces history. Also update a stale recovery-prefix test comment.
* test(terminal): replay host-serialized snapshots through the real wire and drain
Hand-written replay meta hid a gate that production never satisfies. Drive the
host's own emulator, ownership mirror, serializer and recovery publisher through
the real wire, client parser, remote transport and pane drain, for a live TUI
and after the host proves it exited.
* fix(terminal): repaint only the alt frame over a live TUI, leaving history alone
Keeping the pane's history by clearing only the normal screen assumed a pushed
image carries no history, which the host does not guarantee: a 0-row push can
reuse a concurrent requested capture, and its history then landed twice in
scrollback. It also wiped history on a grid mismatch, where main kept it.
When pane and image are both on alt, the image's normal part adds nothing (the
normal buffers froze together), so paint only its alt payload after an alt-side
clear, split at the host's own boundary (splitAtAlternateScreenEntry, now shared
with the daemon). Any other image paints from the normal buffer. This drops
keepScrollback, the owner and grid gates, and the recovery-prefix change, so
history behaves exactly as on main.
* test(terminal): pin the parse wait and the cancelled-shutdown flag
Final-review follow-ups: a drain test where the TUI's ?1049h is still queued
when the image arrives, and a transport test replaying a push buffered during a
cancelled shutdown; each fails when its guard is removed. The requested-image
test now names the exited-TUI case it covers, requestSnapshot shares the
snapshot image type, and the clear comment no longer implies every clear drops
scrollback.
* Redesign the phone-control and phone-size terminal dialogs
Drop the eyebrow label and circled icon, shorten the copy so it no longer
restates the buttons, and give each state one primary action with a quieter
"all" action. Collapse moves out of the button row into a Minimize icon in
the corner. Behavior is unchanged.
* Point the phone settings copy at the renamed Restore button; drop dead ko overrides
The phone app and desktop update independently, so name only "Restore",
which matches both the old and new desktop banner labels.
Pressing + in the right-hand split focused an empty chat sitting in the left-hand split. The reuse
now looks only in the tab group the pick targets (the caller's group, else the workspace's active
group, which is where its tab would open); a pick in another split opens a new chat there. Applies
to both an empty chat still starting and one that published and sits idle.
Update tooltip and settings pane help text to accurately describe
Caffeinate's behavior: it prevents idle sleep while active, but
MacBook lid closing may still trigger sleep per device power policy.
The previous copy incorrectly suggested Orca could prevent lid-close
sleep.
* fix(sidebar): stop Manual sort from mirroring sortEpoch into state
In Manual mode the sort hook copied every sortEpoch bump into state from an
effect, adding a nested React update per bump. A burst of store bumps at
startup stacked those into 'Maximum update depth exceeded' (React #185).
Manual now reads the live epoch directly; debounced modes are unchanged.
* feat(sidebar): tell people when a drop switches sort to Manual
Reordering by drag still switches the sidebar to Manual so the drop sticks,
but it used to happen silently. Show a toast with a 'Back to <previous sort>'
action; it retires itself on any later sort change so it can't override a
newer choice. Drops while already in Manual stay silent.
* feat(store): settle sortEpoch in the store instead of in React state
Add settledSortEpoch plus a 3 s settle timer owned by a store listener
installed in the state creator. Every write path (slice actions, the web
session sync patch) goes through it, so the settled value stays correct
with no sidebar mounted. Manual, a sort-mode switch, and a bump that
changes the non-archived row count settle in the same notify; other bumps
restart the window. A reset that lands settled clears the timer, and the
disposer runs on HMR teardown.
* fix(sidebar): read the settled sort epoch instead of mirroring it into state
The sort hook no longer copies sortEpoch into React state from an effect in
any mode; it reads the store's settledSortEpoch directly. That pattern added
a nested update per bump and stacked into "Maximum update depth exceeded"
(React #185) under flushSync bursts. Tests cover Manual, add/remove, burst
reset, no-bump row changes, mode switch, and 80 flushSync bumps in Recent
while worktrees are added.
* cleaning up
* fix(store): settle bumps when rows update during pending window
Detect structural changes on worktreesByRepo updates in addition to sort
epoch changes. When a row arrives without its own bump during a pending
settlement window, the changed composition must still trigger settlement.
A resend the host answered as unknown reads "Orca couldn't confirm what
happened" and may have landed, but its line drew muted like a plain "not
sent". It keeps the error color now; its words are unchanged.
A /compact reply was silenced as soon as the journal held its submission, even
while pending. If the reply beat the stream and a Stop then withdrew the
command, the row was hidden too, so the command vanished with nothing said and
its text gone. The reply is now silent only once the loaded journal draws the
command as not sent.
The phone counted a row shown as not sent when it matched a send's echo, and
took it as the newest row the echo had to land after. Once the resend lands,
the host hides that row, so the echo expected one copy too many and never
retired, leaving a plain duplicate bubble; an answer-lost resend read as
unconfirmed. Not-sent rows no longer count toward either.
The projection listed rows shown as not sent after the whole conversation and
left ordering to the reader. The desktop sorts; the phone draws the list as it
comes, so an old not-sent message sat under every later message. The
projection now merges them in at their journal positions: the desktop's sorted
output is unchanged, and the phone draws them where the host recorded them.
- The editor no longer saves a value the field sets from the store (a
late-loaded or adopted draft): that echo made another window's draft a
local change here, so a send in one web tab came back from the other.
A whole draft set on the editor uses its saved document, keeping its
skill chips. Equal documents no longer count as a change.
- Text or images given back, or attached, before the startup load lands
are added to the loaded draft instead of replacing it; only typing wins.
- The unload journal leaves out drafts already refused, keeps to 256,000
characters, and drops a draft's entry once any window confirms a change
to it, so a replay never brings back a draft sent since.
- A failed load is retried three times with backoff, warned about once,
then left.
- Adopting another window's write keeps images already checked, so their
chips don't flash.
- A refused draft shows one icon, by Send, with plainer copy; the chip
badge and its string are gone.
- A comment that said drafts' bounds retire orphans is corrected.
Two bare "+ > Claude" picks (or new-tab search, the new-agent shortcut, the dashboard) opened two
empty chats. A request with no text now focuses an existing empty chat for that agent in that
workspace: one still starting (joined in the launch, as a re-delivery is) or one that published and
sits idle (its host's journal holds no request, read live from the status feed). Empty also means
nothing queued, no composer text or images, and no launch draft its composer has not taken. Failed,
unconfirmed, resumed and other-agent/workspace chats are never reused. A request with text still
opens its own chat, or claims an empty starting one as before; request-id dedupe is unchanged.
* Isolate code editor text and undo history by execution host
* Verify editor owner resolution and Windows model path isolation
* Respect native model line endings in content sync history coverage
* Preserve selection and scroll when editor ownership resolves
* Verify owner synchronization against a reachable editor change callback
Design D6: a not-sent message is no prompt the agent saw and the host's outline
leaves it out, so the loaded rail does too (the rail skip and the active-tick
walk-back already on this branch); the parity test pins that again.
The not-sent line drew in the error color as if the user had something to fix.
Main's notices now mark every plain "not sent" (the host's row, its outbox copy
until the row loads, and a held send) so the row draws it muted, as the phone
does; a line still in doubt keeps the error color. A batch that changes only
that mark re-renders its row.
Takes #24917's request ids everywhere a launch is started: a launch joins another only on an equal
id during its first attempt, and every launch call site passes one. C2's notes hold stays the only
one: the staged message carries the notes' keys and the hold derives from the saved outbox, so
#24917's in-memory hold and new-agent-prompt-outcome stay deleted. Its new test case (a chat
still starting is closed without waiting on its create) is ported into notes-carried-by-chat.