mirror of
https://github.com/stablyai/orca.git
synced 2026-10-09 08:02:35 +00:00
defd85a9ebbee89eab2fdc90aef490c10e764ca6
8
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
defd85a9eb |
feat(floating-workspace): render the floating panel through the shared workspace surface (#22302)
* docs(floating-workspace): plan structured native chat in the floating panel
* feat(floating-workspace): resolve the floating workspace so a structured session can run there
* docs(floating-workspace): plan the unification onto the shared workspace surface
* chore(floating-workspace): keep the unification plan out of the branch
* test(floating-workspace): pin the structured route on capability, not workspace kind
The host half removed the 'floating-workspace' blocker; this pin still asserted it and went red
on the merge. Floating now routes like any workspace, so with capabilities un-negotiated the
resolver answers 'runtime-capability-unknown'.
* feat(floating-workspace): resolve the floating workspace's renderer row without a worktree record
The renderer had no Worktree row for the floating id: markdown creation, tab-bar path entry,
native-chat file links, attachments and model discovery all resolve a workspace through
getKnownWorktreeById and silently fail for it. The host already answers with a synthetic row
minted from the resolved floating directory; this gives the renderer the same single seam.
- The repo slice stores the host-resolved floating directory (floatingWorkspacePath); the panel's
existing cwd resolution now writes it there instead of panel-local state, so there is one copy.
- findKnownWorktreeById mints the shared floatingWorkspaceToWorktree row from that path, local
host only, cached while the directory is unchanged — mirroring the folder-workspace branch it
sits beside.
- No consumer changes needed: every row-dependent path already goes through this resolver.
* feat(tabs): scope editor and browser activation to the owning workspace
Shared editor/browser activation always wrote global selection (activeFileId,
activeBrowserTabId, activeTabType). The floating panel is an overlay above the active
workspace, and its active tab must never become global selection — the invariant
documented in agent-auto-ack-targets.ts. Group-scoped activation is a prerequisite for
rendering the floating workspace through the shared tab-group surface.
- setActiveFile and setActiveBrowserTab take an optional target workspace, mirroring
setActiveTabType: global selection moves only when the target IS the globally active
workspace; per-workspace state always updates. Omitted keeps legacy behavior.
- The shared tab-group activation/creation commands pass their workspace, which is a
no-op for the main surface (hidden surfaces are inert, so commands only fire on the
active workspace) and confines floating activation to floating state.
- Terminal setActiveTab was already owner-scoped; now pinned by test.
* refactor(tab-group): separate the workspace drag scope and group tree from main-window chrome
Preparation for hosting the floating workspace on the shared surface, whose shell owns its own
titlebar tab strip and cannot mount the main chrome:
- WorkspaceTabDragLayer: the one dnd-kit scope for a workspace's strips and tree, extracted from
TabGroupSplitLayout as a render-prop layer so a host can put shell-owned chrome (a titlebar
strip) inside the same drag scope as the tree.
- TabGroupSplitNodeTree: the chrome-free recursive group tree, with an explicit tabStrip
composition point on TabGroupPanel ('attached' keeps today's per-group strip; 'external' hosts
render no per-group strip at all — a hidden one would register duplicate drag sortables).
- WorkspacePaneOverlayLayers: the retained pane hosts extracted from WorktreeSplitSurface so any
tree host mounts the same pane lifecycle stack.
- TerminalPaneOverlayLayer gains ownsNativeChatToggleShortcut, following the existing rule that
only one live workspace registers the chord.
TabGroupSplitLayout and WorktreeSplitSurface recompose these parts; the main surface's DOM is
unchanged.
* feat(floating-workspace): decide the panel's emptiness and layout atomically
An empty floating workspace has no tab-group layout at all (the slice starts empty and
hydration skips tab-less workspaces), while the shared group tree requires one. This model
resolves the two together: the panel shows its empty state exactly until the first tab exists,
and mounts the tree only with a layout plus a live focused group (stored focus, else the group
with an active tab, else the first group — matching the shell chrome's focused-group policy).
* feat(floating-workspace): render the floating panel through the shared workspace surface
The panel previously projected exactly one tab group and hand-rendered its panes, so any tab
that left that group — 'Move Tab to Split' creates a second group — stayed alive in state but
vanished from the panel. The body now mounts the shared group tree and retained pane overlay
stack for the floating workspace id, so every group renders, splits work, and structured chat
finally has a floating surface.
- One drag scope: WorkspaceTabDragLayer wraps the titlebar strip AND the body tree, so exactly
one DndContext owns floating tab drag; strip drops reorder, body-edge drops split. The
floating-only FloatingWorkspaceTabDragContext is deleted.
- The titlebar keeps the panel's single TabBar, now fed by the shared per-group workspace model
of the focused group (tabStrip='external' keeps per-group strips out of the shell), with
workspace-scoped activation commands so global selection never moves.
- Panes render through WorkspacePaneOverlayLayers: terminals gate on the resolved cwd and a
settled viewport exactly as before; browser guests ride the shared retention layer; agent
session tabs appear in the strip and render through the structured overlay.
- The empty state renders exactly until the first resolvable tab exists (stale unified entries
still show it), per the atomic layout/emptiness model.
- Deleted: the single-group projection, the hand-written pane maps, FloatingBrowserSlot, and
the floating pane-handle registry.
Known behavior deltas, both narrowing floating onto main-workspace semantics: Cmd/Ctrl+W with
panel (not terminal) focus closes the active tab rather than one pane of a multi-pane tab, and
a pane-owned close of the last tab no longer arms the panel focus reclaim (strip and shortcut
closes still do).
* test: re-register moved pane-ownership sites with the extraction
The retained overlay stack moved into WorkspacePaneOverlayLayers and the floating chrome model
became a guest-paint consumer; the structured-pane layering pin and the browser-guest retention
census both name files, so they follow the seam.
* fix(floating-workspace): mount the shared group tree in a flex frame so panes own their height
The tree's nodes size themselves as flex items, and the floating shell mounted
them in a block container, so every group body — and every pane anchored to
one via anchor-size() — measured 0px tall. Wrap the tree and overlay layers in
the same 'absolute inset-0 flex' frame the worktree split surface uses, record
the host contract on the tree, pin the frame in the panel's element-tree test,
and add a real-renderer e2e spec that measures group-body and pane boxes for
the single-group and split layouts (jsdom computes no layout, which is how the
0px regression passed 50k tests).
* fix(runtime): import the floating worktree minting the structured create path calls
floatingWorkspaceToResolvedWorktree called floatingWorkspaceToWorktree without
importing it. The module is @ts-nocheck (mechanically split runtime), so tsc
never saw the missing import and it only failed at call time — the structured
createSupport probe for the floating workspace threw a ReferenceError. Add the
import and a test that runs the real method, which fails on the missing import
where the typechecker cannot.
* fix(browser-pane): resolve floating and folder workspace paths in notebook navigation
navigateBrowserPageToUrl looked the workspace up in allWorktrees(), which has
no row for folder or floating workspaces, so their notebook navigations ran
with an undefined workspace path. Use getKnownWorktreeById, the same resolver
every other consumer was moved to.
* fix(tabs): keep a workspace's tab selection out of the main window's global selection
* refactor(floating-workspace): launch and close panes like every other workspace
* refactor(tab-group): let the surface that mounts tab groups describe its own chrome
* refactor(floating-workspace): render the floating panel through the shared workspace surface
* fix(floating-workspace): resolve the floating directory once on the host, and keep the controls' inset
* test(floating-workspace): pin the unified floating panel against the shared surface
* fix(floating-workspace): read the floating directory's paths through the app environment
The runtime now resolves the floating directory itself, and the headless
runtime has no Electron app, so the Electron path lookup threw there.
* fix(floating-workspace): keep the main window's frame and shortcuts out of the floating panel
The shared layout drew the main window's 4px titlebar band and sidebar seam
inside the floating panel, and the chat-view toggle fired in every workspace
on screen, so one key press flipped the main window's tab and the panel's.
* fix(floating-workspace): close floating tabs without taking over the main window
An unsaved floating note's save prompt made the floating panel the main
window's active worktree, and never appeared on a fresh launch because the
workbench that owns the prompt was not mounted. The prompt now shows a
floating note in the panel, and the workbench mounts while the panel holds
tabs.
Every close path now reads the same captured emptied-workspace reaction, so a
floating pane closing itself, its shell exiting, a group close, and an unsaved
note close all keep the panel's keyboard ownership, as a tab close already did.
* test(tab-group): pin that a dirty editor close carries its emptied reaction
* refactor(tabs): one rule for when a selection moves the main window's selection
Opening a file kept a floating-only exception while every other writer checked
whether the tab's workspace is the active one. Every writer now asks the same
question, so opening a file in a background worktree no longer points the main
window's editor selection into that worktree.
* refactor(floating-workspace): drop what the shared surface left unused
The terminal pane's imperative close handle lost its last consumer when the
floating panel stopped keeping its own pane registry, and the floating and
folder workspaces built the same lineage-free resolved worktree twice.
* test(floating-workspace): type test fixtures without casts and drop the removed pane ref
* fix(floating-workspace): keep panel focus when a close lands after the panel emptied
Saving the last floating note closes it after an awaited write, so the panel
could render empty before the close armed its keyboard reclaim, leaving the
intent armed with nothing left to consume it. The panel now re-checks whenever
the intent arms as well as when its tab count changes.
A pinned tab's confirmation took focus before the close read panel ownership;
the dispatcher now captures it before any prompt.
* refactor(floating-workspace): finish what the shared surface left behind
- The floating panel no longer accepts AI vault session drops: a session
cannot resume into it, and its window-level drop handler fired beside the
main window's.
- The last global-selection writers that spelled the active-workspace check
inline now use the shared rule.
- Remove the save-dialog hook and item counter the old floating body used, and
correct comments that said the workbench only mounts once a workspace exists.
* fix(floating-workspace): keep a floating leaf detach from changing the main window's tab type
* fix(floating-workspace): give floating chats their workspace identity and directory
* fix(floating-workspace): read one visibility fact for chat attention and auto-ack
* docs(floating-workspace): correct comments the shared surface made stale
* fix(attention): one on-screen rule for every tab's unread marker, main window and floating panel
* fix(agent-session): pin the folder a session launched in; floating chats resume there or refuse
* fix(native-chat): resolve a floating chat's file links, images and skills in its pinned folder
* fix(agent-session): open a floating chat's terminal handoff in its pinned folder
* fix(agent-session): surface refusal text on chat open, require the launch-directory rule, and keep bookkeeping from gating a launch
* fix(floating-workspace): wait for a floating chat's pinned folder and read panel visibility from the store
* test(floating-workspace): assert the toggle event without a cast
* fix(native-chat): show why a chat could not be opened instead of dropping the host's refusal
* fix(floating-workspace): close chat tabs through shared commands
* fix(i18n): remove obsolete floating editor loading key
* fix(floating-workspace): preserve tab ownership on activation and browser close
* fix(floating-workspace): keep terminal activation and focus reclaim in sync
* fix(readme): point translations at tracked media
* fix(floating-workspace): retain split surface with an empty focused group
* docs(tabs): describe workspace-owned activation accurately
* fix(floating-workspace): finish the main merge and name the pin a launch directory
Port what the merge left on main's newer shapes:
- The floating chat's "folder is gone" refusal becomes a typed reason,
launchFolderMissing, worded like every other start refusal (copy, desktop
words, five translations). Older clients drop the unknown reason and show
the code's generic words.
- The PR's launch-folder tests run on main's journal-database record store;
the attach-level case is a row in the pre-spawn refusal table.
- Tests main added since the PR's base learn the PR's close-queue settling,
workspace-scoped notebook lookup and tab-stamped chat host.
- Move the store's visible-tab-index reads into the tab table, so the record
store stays under the line cap with the pin writer in it.
- Drop a duplicate Worktree import the merge left in a @ts-nocheck file.
Rename the persisted field, the status summary field and the renderer slice
from workspacePath to launchDirectory: it records the folder a session first
launched in, not its workspace's path, and it has not shipped yet.
* Fix floating chat visibility, closes, and directory pinning
* fix(i18n): drop the floating save dialog's strings with the dialog
The floating panel now closes through the shared unsaved-changes prompt, so
its own dialog's six strings had no caller and failed the runtime-catalog
check.
* fix(native-chat): pin floating founders and retain pinned editors
* test: align auto-ack fixtures with unified tab visibility
* test: give the forget-status attach fixture a location
Attach now reads params.location to decide whether a new record is a floating founder, so the partial fixture needs the location real attach params always carry.
* test: type the forget-status attach fixture instead of casting it
The cast tripped the changed-code type-assertion gate.
* fix(native-chat): read a floating chat's model defaults in the folder it was created in
The model picker read the floating setting's current folder, so a chat
pinned to another folder could show that folder's project default.
* test(cross-version): give the stub host the record store the catalog read now checks
* test(claude): give main's thinking-display launch stub the store shape the resolver takes
|
||
|
|
e817b0e237 |
refactor(native-chat): keep the provider resume handle opaque to shared code (#24991)
* refactor(native-chat): keep the provider resume handle opaque to shared code
Shared structured-chat code parsed each provider's resume handle: Claude's
session id and branch leaf, Codex's thread id, through a 'claude' | 'codex'
union every new agent had to widen. The in-memory handle is now
{ transport, agent, nativeId, providerData? }: shared readers use nativeId,
lease and handle-chain checks compare transport and agent, and only the
Claude adapter reads its leaf (providerData).
Stored and wire forms are unchanged for Claude and Codex. One encoding
module writes their typed shapes and decodes both those and the neutral
shape a new transport uses, which an older build refuses as unreadable
rather than reading as Codex. Key and root strings, which fork seeds,
superseded creations and resume offers persist, stay byte-identical.
The journal's own handle type becomes the journal-row and attach-wire
encoding of the same handle, and the journal identity carries the
neutral handle (null before the provider proves one).
No user-visible change.
* fix(native-chat): derive journal-row provider handles from the journal identity
The journal row converter now takes the identity every caller already holds,
so a row's handle has one obvious constructor. Tests that wrote the in-memory
handle straight into journal rows now build it through that converter, and the
processless Claude fixture names a not-yet-proved handle as null.
* fix(native-chat): refuse a stored provider handle written in both forms
A typed Claude or Codex handle that also carries the neutral form's
transport, agent, native id or provider data named two identities; it
was read as Claude or Codex and the next write dropped the other one.
Such a row now stays unreadable and is set aside untouched.
* test(native-chat): use opaque handle in queued rejection fixture
* test(native-chat): share one Codex journal identity in the integration suite
Main grew the suite to the 800-line limit; the opaque-handle import pushed it
over. The two tests built the same identity inline.
* refactor(agent-session): name the handle's adapter state resumeCursor
Rename the neutral provider handle's providerData to resumeCursor before any
row persists the neutral form: it is an adapter-owned resume position (Claude's
transcript leaf), never identity. Claude/Codex stored and wire bytes are
unchanged; their typed shapes never carried the field.
State the stored-form contract (a handle's field set is closed; later per-link
data goes on the chain link, which every build preserves) and pin it with a
record round-trip test. Document that transport records the id space the
native id was minted in, which can differ from the agent's current transport.
|
||
|
|
934a2d44a0 |
fix(codex): a native chat's thread opens on the model the chat chose (#23532)
* fix(codex): a native chat's thread opens on the model the chat chose * fix(codex): a resumed thread keeps its own saved model, provider and effort |
||
|
|
eb746a6d32 |
fix(codex): a Codex native chat that never sent a message reopens after restart (#22639)
* fix(codex): start a new thread when a chat's thread was never saved A structured Codex chat records its thread at create time, but Codex writes no rollout until the first input. After a restart, launch resumed that thread, Codex answered "no rollout found for thread id", and the chat could never run again. When the head of the handle chain is the session's own creation and Codex answers that exact error for that exact thread, start a new thread instead. The new link supersedes the unsaved creation in place and names it, so the chain keeps one live identity and does not grow across restarts. A thread a resume, fork or adoption proved is never superseded, and no other resume error starts fresh. * test(codex): build launch-resolution chains without a type assertion * fix(codex): match only Codex's own no-rollout text, pinned through the real connection The fallback matched Orca's own error-wrapper prefix too, and every test built that string itself, so rewording the wrapper would have disabled the fallback with the suite green. Match the method, code -32600 and Codex's exact detail as the message suffix, and drive Codex's raw error frame through the real connection in a test. The link builder now refuses, at the type level, a supersession on an adopted or resumed link, which the chain would reject downstream anyway. * docs(codex): note why the no-rollout text is safe on the resume path |
||
|
|
7f5141ae2d |
Make the Agent Permissions toggle apply to Codex chat (#20977)
* fix(structured-chat): deliver the permission posture through each transport's own contract Codex posture moves off app-server argv onto typed `thread/start` and `thread/resume` params. Manual states `on-request` / `workspace-write` explicitly instead of omitting the fields, which app-server resolved through the mirrored config.toml — a Manual thread on a home carrying `approval_policy = "never"` never prompted. Claude keeps its owned `--dangerously-skip-permissions` flag through SDK `extraArgs`; the SDK's typed bypass option emits a newer allow flag that older user-installed binaries reject. Posture is re-derived from current settings on every session acquisition. * fix(structured-chat): parse permission arguments as argv * fix(structured-chat): keep permission policy authoritative |
||
|
|
c702e77bc7 |
Stop reading the terminal arguments field on the structured chat route (#20944)
* fix(native-chat): stop reading the terminal arguments field on the structured chat route Setting Claude's Arguments to "--dangerously-skip-permissions --model Opus" made every new Claude tab open in the old terminal-backed chat instead of the new structured one, with nothing on screen to explain why. Removing "--model Opus" fixed it. The cause was a whole-string comparison: the configured arguments were checked against a single blessed value per agent, so any added token at all — including one the agent supports — stopped the string matching and the launch was demoted. Structured chat does not run the interactive CLI. It drives Claude through the Agent SDK and Codex through app-server, and those take narrower option sets that are versioned separately from the CLI's, so one free-text field cannot have a guaranteed meaning for all three. The structured route now reads only what it can actually honour: a replaced launch command, or a launch that names its own working directory. Terminal launches still apply the field exactly as before. Permission posture no longer travels as a raw flag. It is derived from the resolved launch arguments, which is the same fact a terminal launch acts on and which falls back to the default Orca ships when the field was never touched, so bypass stays on by default and Manual is still honoured. Claude gets the SDK's typed permissionMode and allowDangerouslySkipPermissions at query start; Codex gets its bypass flag placed before the app-server subcommand. Both are re-derived per acquisition beside the auth policy and environment overlay rather than stored in the session record, so nothing can disagree with the setting. Codex also loses the --profile, --add-dir and -c passthrough that reached app-server through that field. Only the permission posture comes back. * test(native-chat): pin routing authority on the narrowed feasibility input The routing-authority pin still named the old bundled blocker and built its "customized" fixture out of the arguments field, which is no longer a feasibility input. Both are now the launch command, and arguments and environment are customized on both passes of the loop, so the flag handed to the shared resolver tracks the command alone — a caller that resumed reading either one fails here. No case is dropped and no assertion is relaxed: the blocker list is still exhaustive and every caller must still honour a refusal from the shared resolver. |
||
|
|
a899f92402 |
feat(windows): enable structured Codex chat on native Windows (#18519)
* feat(native-chat): enable Windows structured sessions
* fix(codex): prove native Windows process identity
* style(codex): format Windows session seam
* fix Windows structured Codex admission
* fix(windows): reprobe missing process identity capability
* fix(windows): decide folder-workspace WSL routing before the click
Review found pathUsesWslUnc exported but unused, and the folder composer
hardcoding worktreeUsesWslPath:false. Together those meant a folder picked
under a \\wsl.localhost\ parent routed to structured chat, then got refused
by the host and fell back AFTER the click -- which defeats the lane's own
design goal that create cannot fail after the click.
The group's parentPath is in scope at submit and the workspace is created
under it, so the parent decides WSL-ness pre-click. Wires pathUsesWslUnc
there and adds tests for the helper, including the unhydrated-store case
that previously threw.
* fix(windows): collapse the gate derivation to one call, restoring max-lines
CI static analysis failed: launch-agent-in-new-tab.ts crossed the 300-line
oxlint ceiling. Adding a max-lines disable is forbidden, so the two gate
derivations collapse into one readWindowsStructuredGateInputs() call --
a store-backed site now adds one line and one import name instead of two.
Better shape anyway: one derivation entry point rather than two reads a
call site must remember to pair.
* fix(windows): engage the legacy fallback when the host THROWS a refusal
Review found a P1 this merge composes: neither parent could reach it. At the
lane head the only structured entry was launch-agent-in-new-tab (full
store-backed WSL check); on main all win32 was refused. The merge enables
win32 in creation flows that pass no projectRuntime, so a WSL folder
workspace, a WSL-configured repo, or a repair-required runtime now routes
structured -- and the host refuses correctly, but by THROWING rather than
returning {ok:false, refusal}.
Callers engage their legacy-terminal fallback on the refusal CLASS, so an
unmapped throw arrives as a generic RPC rejection: no fallback, empty
workspace, error toast, prompt stranded in the launch outbox. Pre-merge the
same action opened a legacy terminal agent.
Map the host's thrown definitive refusals onto the refusal class at the
launch boundary, so every creation flow -- present and future -- degrades to
the legacy terminal instead of stranding. Narrow predicate: unrelated
failures (ECONNRESET, empty message, non-Error) still propagate untouched.
Ablation-proven: removing the mapping reddens the fallback test.
* fix(windows): teach the mobile RPC double the status probe the lane added
CI's first-ever run on this lane caught a pre-existing lane defect. The lane
changed status.get to resolve through
runtime.getStatusAfterWindowsProcessStartTimeProbe(), but never taught the
mobile-surface runtime double about it, so status.get failed for mobile
clients with "not a function". The lane's own test list did not include this
file and the lane had zero CI, so nothing ever ran it.
The real runtime always implements the method; the double omitted it.
* chore: merge current main and regenerate the localization runtime catalog
CI static analysis failed on a stale en-runtime-required.json: main added
onboarding integration-capability keys, and the generated catalog is checked
against the PR MERGE result, not the branch alone -- so it read clean locally
while failing in CI. Merging current main (
|
||
|
|
fd9125ea8c |
feat(native-chat): Codex structured native chat restructure (#16729)
* feat(native-chat): port structured Codex sessions from restructure-recovery Rebuilds the desktop structured native-chat implementation from brennanb2025/native-chat-restructure-recovery (tip 4e31c08db3) on top of current main as a single commit, scoped to the local Codex path. Ported: - Structured agent-session core: durable record store + single-writer lease, canonical journal, agent-session wire host/attach/eviction/subscribers, `agentSession.*` RPC surface (registered via ALL_RPC_METHODS; host-side mobile allowlist included for wire compat), pty write gate, transcript additions, and the Codex app-server adapter/launch resolution. - Renderer: NativeChatStructuredSession view/composer stack, structured launch path with the single-flight guard, local structured session tabs sync, activation gate + structured inventory (read-only `agentSession.handoffStatus` probe), agent-session tabs in the tab strip, AI-vault structured session activation, and the settings pane with the parent Experimental Chat UI toggle plus the nested "Use updated structured native chat" toggle. New sessions require both flags, agent codex, no prompt, and a local non-WSL, non-Windows-host execution host (structured-native-chat-availability). - Fixes 72c013cea6 (verified Codex launch recovery), 8ddbaf5e3d (defer native terminal view switching affordances), and 4e31c08db3 (release the launch gate after a visibility retry) with their regression tests, including the third-launch-after-retry guard case. - Cross-version agent-session wire test + CI lane, packaging entries (proper-lockfile, agent-tooling asar excludes), and the wire-compat doc section. Deliberately not ported: mobile/ changes, the Claude structured runtime (only the claude-transcript-branch-proof and claude-structured-owner-identity leaf modules remain, backing the kept TUI-recovery arms), the terminal↔chat adoption/handoff flow (`agentSession.adoptTerminal`/`requestHandoff`, the handoff request engine, TUI adoption machinery, orca-runtime adoption methods), renderer switching affordances and their dead leftovers, the hook/subagent-status refactor cluster, and unrelated branch changes. The crash-during-acquisition recovery path (restart handoff adjudication, restore/reverse re-acquire, lease schema handoff keys) is kept because every plain direct launch depends on it; a trimmed handoff coordinator exposes only status/restore/close. Branch edits that targeted files main has since split (ipc/pty.ts, worktrees.ts, rpc/methods/terminal.ts, useIpcEvents, pty-connection, store/slices/terminals.ts, runtime-types, web preload) were re-applied to the split modules, preserving main's newer logic (Windows CIM fallback, browser tab close rework, cold-restore resume flow, dispatcher threading). Known seam: the mobile clipboard image-provenance CONSUMER gate ships (agentSession.send refuses unproven mobile image refs with agent_session_image_untrusted) but the producer hunk in rpc/methods/clipboard.ts stays with the unported mobile cluster, so mobile image sends into structured chat fail closed until that side ports. * fix(native-chat): trust only authenticated local image uploads * fix(build): preserve Windows process-tree patch application * test(windows): include process creation time in addon fixture * fix(build): run windows-process-tree node-gyp from the physical package dir gyp expands the node-addon-api dependency by probing node, whose cwd resolves to the package's physical directory in the store, so the emitted target is a store-relative ../../../../node-addon-api@... hop. gyp then resolves that hop against the rebuild cwd; from the node_modules symlink/junction it escapes the store and configure fails with "node_addon_api.gyp not found" (run 32999886072). Rebuild from realpath(package dir) so both bases agree, matching how the package manager itself runs native install scripts. The regression test replays gyp's expansion+resolution against the planned cwd and fails without the fix. * fix(native-chat): keep chat tabs visible through terminal closes and empty-worktree launches Two proven blockers in the native Codex tab contract: closeTerminalTab pre-empted the canonical unified close. With one terminal left it deactivated the worktree on a terminal/editor/browser-only check, blanking a workspace that still held a renderable agent-session tab; with two or more it pre-picked a successor from terminal entities only, re-stamping the group active before closeUnifiedTab's MRU/neighbor repair could land on the chat tab. Successor choice now defers to the unified contract whenever the terminal has a unified row, and deactivation is gated on the unified renderable count (matching leaveWorktreeIfEmpty), with the legacy pre-pick kept only for terminals without a unified row. A structured session created on an empty worktree was published into the host's headless group while preserveLocalLayout froze the local layout, leaving the tab in store but permanently off screen. A preserveLocalLayout owner now always takes client-owned placement — repairing a rendered leaf whose group record is missing, or materializing a rendered group on a truly empty worktree — and applies the client-derived layout repair while still rejecting host-authored layout. Regression tests drive the real store through closeTerminalTab (git worktree and folder workspace) and the real snapshot applier for the empty-worktree adoption states; all fail without the fixes. * fix(native-chat): close stale turns and retry rejected sends * fix(native-chat): retire hosted rows on structured tab activation * fix(native-chat): preserve rpc defaults across main merge * chore: format remote wire compatibility guide * test(native-chat): cover retry after unconfirmed send * fix(native-chat): reload outbox on session switch * docs(settings): disclose structured chat platform limits * fix(native-chat): await Codex launch-home preparation * fix(codex): align child-process allowlist with async trust bridge * test(identity): update inventory for tab surface refactor * fix(windows): preserve process-tree CRLF patch sources * fix(native-chat): anchor an unmatched chat echo where it was sent (#16117) * fix(native-chat): anchor an unmatched chat echo where it was sent The reported symptom was old user messages replaying below every new turn, so the conversation read as scrambled. The cause was not that the echo failed to match a transcript row. Claude consumes a mid-turn send through a `queued_command` attachment and writes no `type:"user"` record for it, so some echoes can never match, and no amount of matching will change that. The cause was WHERE an unmatched echo rendered: buildMobileNativeChatTransientData appended every pending item after the entire transcript, so it re-read below each turn that landed afterwards. Render each echo directly after the transcript row it was sent against, using the baseline the send already captures. An unmatched echo is then at worst a duplicate in the right position rather than a scrambled one, and it stays visible. Echoes sharing an anchor keep send order; a send with no baseline, or one whose anchor folding dropped, still falls back to the tail. Deliberately NOT fixed by deleting the echo. Inferring from send ordering that an echo can never match, then removing it, loses the user's own text for a message the agent did receive, and it cannot fire in the common case anyway - measured drain groups are 1,017 of size 1 against 55 larger. It also escalates an existing gap: the count pass has no baseline-tail guard, unlike the glue pass, while `messages` is a 40-row window that head-trims, resets on reconnect and grows at the front on loadEarlier, so a false landing there would license deleting a DIFFERENT outstanding message. That count-pass gap is real and left for a separate change; anchoring makes its worst case a duplicate in place rather than a scrambled conversation. * fix(native-chat): preserve folded echo anchors * fix(native-chat): preserve forward-folded echo anchors * fix(native-chat): keep leading folded echoes in place * fix(workspace-cleanup): show git status for every row (#16690) * fix(native-chat): refuse structured chat on every Windows execution path canUseStructuredNativeChat only refused win32 when a project runtime resolved, so folder-workspace keys (and other keys with no project runtime) failed open into structured chat on Windows. Fail closed on win32 unconditionally after the host check, matching the settings copy: local macOS/Linux only; Windows/WSL/SSH stay on terminal chat. * fix(native-chat): restore runtime refusals behind the win32 gate |