Commit graph

216 commits

Author SHA1 Message Date
Pascal André
c82dd8fbf6
fix(notifications): show Windows system notifications and keep their permission (#1109)
Some checks are pending
Publish help / build (push) Waiting to run
Publish help / deploy (push) Blocked by required conditions
## Summary

System notifications on Windows: everything about them in one PR.

1. **Windows data profiles never showed system notifications**
(`fix(windows): show system notifications from every data profile`).
Tauri runs each data profile, such as **dev**, under its own
AppUserModelID (`…client.scope.s<hash>`), and its notifications carry
that ID; Windows shows a desktop app's notifications only under an ID it
knows, and the installer's Start menu shortcut carries the stable ID
alone. Windows recorded every notification (100 on the affected machine)
and displayed none. Every launch now registers the running ID under
`HKCU\Software\Classes\AppUserModelId\<identifier>` (`DisplayName`
CodeNomad, `IconUri` the bundled web app icon
`resources\server\public\pwa-192x192.png`, which the packaged-resource
smoke check requires; `windows_notification_identity.rs`). Electron
always uses the stable ID.
2. **The permission was lost at every restart on Windows.** The
notification plugin's page script reports *denied* there until the page
requests permission, though Windows never asks, so Settings asked again
after each restart. The check now asks the native plugin command, which
answers granted on desktop: granted once, kept.
3. **Notifications were held back whenever the window was visible**
(`fix(notifications): hold system notifications back only while
focused`). With **Notify when the app is focused** off (the default), an
open window stays visible behind other applications or on another
screen, so nothing was sent unless CodeNomad was minimized. Delivery is
now skipped only while the window is visible and focused
(`document.hasFocus()`). The setting's subtitle and the Settings help
say *focused* in all ten languages; `dev-docs/SETTINGS.md` records the
rule.

Out of scope by decision: several CodeNomad windows open on the same
project may each send the event's notification (as they already do when
the setting is on), and focus inside a browser preview counts as leaving
the page.

## Validation

- Affected machine: no profile notification showed before; after
registering its ID as above, a notification sent under it showed in the
notification centre under CodeNomad.
- `cargo test --bins -- windows_notification_identity` (writes the key,
reads back both values).
- `tests/browser/system-notification-focus.test.ts`: real routed
notification from a visible page; nothing while focused, one once focus
is lost, one while focused with the setting on; red on the previous
visibility check.
- `npm run typecheck --workspace @codenomad/ui`, `npm run test:help`.
2026-10-23 04:03:26 +02:00
Pascal André
5b69a52822
fix(brand): let the app icon span its square edge to edge (#1110)
## Summary

The app icon looked smaller than its neighbours in the Windows taskbar:
the square source SVG kept an empty ~4 % margin on each side, so the 24
px icon drew the CN in 22 x 12 px on a transparent background.

- `images/CodeNomad-Icon.svg` is cropped to a square centred on the
mark, touching the left and right edges (height stays about half: the CN
is twice as wide as tall).
- Master PNG, in-app copy, Electron/Tauri `.ico`/`.icns`/`.png` and
Linux sizes regenerated: 24 px now 24 x 14 px, 32 px now 32 x 18 px.
- In-app logos use the same PNG, so their heights shrink by the same
ratio to keep the approved on-screen sizes (5.55rem, 4.44rem narrow,
7.4rem loading); `dev-docs/UI_CONVENTIONS.md` updated.

## Validation

- Regenerated `icon.png` loses no opaque pixel of the master; the mark
spans columns 0-1023.
- Measured bounding boxes in the 16/24/32/48 px `.ico` entries before
and after.
- `npm run typecheck --workspace @codenomad/ui`.
2026-10-23 03:13:06 +02:00
Pascal André
7709860b61
feat(desktop): give macOS windows native traffic lights and one menu bar (#1105)
## Summary

On macOS, CodeNomad showed **two menu bars**: the system menu bar and,
inside the window, the UI title bar with the same
File/Edit/View/Window/Help menus plus Windows-style buttons at the
right.

macOS windows now follow the platform convention (as in VS Code or
Slack):
- The menus live only in the **global menu bar**.
- The **native traffic lights** sit at the left of CodeNomad's title
bar, which keeps the theme colours and shows the CodeNomad mark
(Preferences: its title) after them.
- The main window's title bar now also holds **Settings, the
notification history bell and Remote Access**, so the project tab bar
gives that space to the tabs (and drops its separator).
- Windows, Linux and browsers are unchanged.

## Implementation

- **Tauri**: new `title_bar.rs` configures local application and
Preferences windows: overlay title bar with a hidden title on macOS, no
decorations elsewhere.
- **Electron**: `titleBarStyle: "hidden"` on macOS, `frame: false`
elsewhere, for the same two windows.
- Both keep the traffic lights at their **standard place**. Tauri loses
a custom `traffic_light_position` once the window is shown and resized
(it came back to the default on the VM), so on macOS the UI bar takes
the native **28 px** title bar height instead, which centres them, and
leaves their 60 px clear in every writing direction.
- `NativeTitlebar` drops the in-window menus and window buttons on
macOS. The actions move from `InstanceTabs` into a shared `AppActions`
component, rendered by the title bar on macOS and by the tab bar
elsewhere (`appActionsInTitlebar`).
- The native close button of Preferences goes through the existing
guarded close request, like Cmd+W.
- `dev-docs/UI_SHELL.md` describes the per-platform title bar; the
Settings help says where the gear and bell are on macOS, in all ten
languages.

## Validation

- **macOS 15 test VM**, Electron app and Tauri build patched with this
change: traffic lights and mark centred in the 28 px bar in both hosts
(measured from screenshots), no in-window menus or buttons, actions at
the right end opening Preferences, Remote Access and the notification
history below the bar, Preferences title after the lights, and its red
button closing the window.
- New `tests/browser/macos-titlebar.test.ts` renders the real title bar
and tab bar as macOS and as Windows.
- `tab-chrome`, `new-instance-launcher`, `opencode-setup` and
`notification-icon-badge` browser tests (58 pass, 1 skipped), `npm run
test:help`, `npm run typecheck` pass.
2026-10-23 02:18:04 +02:00
Pascal André
3ef6b0c564
fix(brand): size large CodeNomad logos by their title (#1108)
## Summary

Large logos follow their title instead of the window, on the plain empty
state's ratio (6rem logo above a `text-3xl` title, 3.2x):

- **Home page** and **branded empty session**: 6rem (the home page's
12rem rule from 640 x 721 px goes; the session drops `h-48`).
- **Narrow session column**: the title shrinks to 1.5rem, so the logo
shows 4.8rem.
- **Loading screen**: 8rem (128 px) beside its larger 2.8rem title,
instead of 180 px.
- `dev-docs/UI_CONVENTIONS.md` records the rule.

## Validation

- Real home page, branded and plain empty states, narrow session column
and loading screen in dark and light at 1100 x 760: logo/title 96/30,
96/30, 96/30, 76.8/24 and 128/44.8 px.
- Tried in the owner's installed app through a temporary override.
- `npm run typecheck --workspace @codenomad/ui`.
2026-10-23 02:17:56 +02:00
Pascal André
01b20b5b78
chore(opencode): qualify and recommend runtime 2.1.3 (#1098)
Some checks are pending
Publish help / build (push) Waiting to run
Publish help / deploy (push) Blocked by required conditions
## Summary

- Qualify OpenCode **2.1.3** (latest stable, 22 October) with the
existing locked **2.1.0 client/plugin** dependencies, and **recommend**
it.
- Keep the technical **minimum at 2.1.2**: nothing CodeNomad consumes
changed, so the minimum has no new reason to move.
- A 2.1.2 daemon stays ready and gets an optional upgrade offer. 2.1.3
is the `tested` version. Newer and custom labels keep authenticated
contract recognition.
- The reviewed-contract fast path lists 2.1.3 beside the 2.1.2 minimum.
- The recommended version is updated in the help (all ten locales), both
READMEs, `dev-docs/OPENCODE_COMPATIBILITY.md` and the architecture
skill.

## Contract review (v2.1.2 → v2.1.3, 66 commits)

- **Authenticated OpenAPI:** 119 → **121 paths**, 144 → **146
operations**. Every consumed operation is unchanged. The only changes:
  - added `GET /api/experimental/voice/models`;
- added `POST /api/experimental/voice/session/{sessionID}/instructions`;
- an optional hosted `session.id` in the `POST
/api/experimental/voice/session` response.

  None of these is on the instance proxy allowlist, so they stay closed.
- **Published declarations:** the client adds only `voice.models` /
`voice.instructions`. The schema changes only typing (agent `color`
decoding, `Durable` as a tagged union). The plugin is unchanged.
- **Windows managed services** move their default port from **45278 to
28894** ([#57428](https://github.com/anomalyco/opencode/pull/57428)).
CodeNomad discovers the daemon through the selected CLI's `service
status` and the service registration, and assumes no port.
- Live check: this machine's shared daemon was upgraded to 2.1.3 and
restarted on 28894, and the running CodeNomad desktop kept working
through it.
- **Core:** mostly refactors, plus behaviour fixes that the native
suites below exercise:
  - compaction summary output limit;
  - `keepID` validated as a message ID;
  - MCP media kept out of attachments;
  - only changed instruction entries resent;
  - relative-directory V1 migration;
  - legacy `apply_patch` permission mapped to `edit`.

## Isolated Windows qualification

Base: dev `333d73452`, rebased on `853b14f2f` (#1097) for the final UI
unit run; Node **24.20.0**; exact official CLI **2.1.3**; unchanged
locked client/plugin **2.1.0**.

- **Build and checks:** production server/UI/native plugin builds,
server and root typechecks, native bundle guard.
- **Native runtime (9/9):**
  - Code Mode job ordering, plugin runtime, automation;
  - complete pruning, history, ownership, worktree and event relay;
  - loaded-only pending snapshot;
- Missions contract, family control, native and Compare/Wayfinder
trajectory.
- **Sessions (15/15):** environment, fork, aside, interrupt, lifecycle,
blank cleanup, prompt skills, provider usage/accounts/selection, service
usage, web search, plugin packages, output limit, Promise-tool failure.
- **Recurring (5/5):** all **11 recurring Mission journeys**, subagent
depth, **2 MiB** compaction, permission receipts.
- **Migrations (9/9):**
- history from **2.0.3, beta-19271, 2.0.32, 2.1.0, 2.1.1 and 2.1.2** to
2.1.3;
  - Git degraded/init;
  - temporary workspaces.
- **Installation (3/3), with the 2.1.3 recommendation:**
  - fresh install of the 2.1.2 minimum;
- native **2.0.15 → 2.1.3** upgrade, after which the already-running
daemon keeps its PID and 2.0.15 version;
  - explicit OpenCode **1.18.10 → 2.1.3** replacement.
- **After the recommendation change:**
  - admission/transport/update/setup units **36/36**;
  - full server **2,463 passed, 8 skipped**, no failure or cancellation;
  - UI units **1,694/1,694** (with #1097);
  - focused browser **126/126**;
  - native pruning with the real rendered UI;
  - the exact CI invocation of the Code Mode regression (without tsx);
  - ten-locale help checks.

The UI unit run on `333d73452` itself had two failures, in
`instances-pending-compaction.test.ts` (`toast.dismiss is not a
function`). They come from #1096 and also fail on unmodified `dev`;
#1097 fixes that fixture.

This is Windows automation evidence. In CI, `latest` now resolves to
2.1.3: the compatibility job runs the native suites against the minimum
and latest on Windows, and the migration and Git-degraded certification
runs them on Ubuntu, Windows and macOS. WSL, packaged desktop,
installers/updater end-to-end and real mobile devices keep their
separate release gates.
2026-10-22 16:17:20 +02:00
Pascal André
9f93729882
fix(opencode): explain why OpenCode 2 cannot be installed beside an older command (#1093)
## Problem

On an Ubuntu 24.04 VM, OpenCode 1.17.18 was installed by curl in
`~/.opencode/bin` and also linked as `~/.local/bin/opencode`. With that
setup, the OpenCode panel offered **no installation**, only the generic
"Select a supported executable or install @opencode/cli on the execution
host" advice.

On macOS/Linux the user npm prefix is `~/.local`, so that link occupies
OpenCode's command name exactly where OpenCode 2 would be installed
beside the older version. npm refuses to link over it, and CodeNomad
never modifies a foreign installation. The plan therefore correctly had
no prefix, but nothing told the user why.

## Fix

`sharedInstallationPlan` reports the obstacle on the foreign
installation, forwarded as `foreignInstallation.blocked`:

| `reason` | `path` | When |
|---|---|---|
| `occupied` | the occupying command | a non-npm command holds
OpenCode's name in the user npm bin directory |
| `unwritable` | the user npm prefix | the prefix, or a recognized npm
installation in it (an `@opencode/cli` launcher, a Windows npm shim or
an `opencode-ai` 1.x installation), cannot be written |

The installation notice then says how to clear it: move or rename that
command, or fix the folder's permissions, then choose **Check status and
updates**. The usual curl layout, on PATH only through
`~/.opencode/bin`, still offers **Install OpenCode 2 for CodeNomad**.
This was verified on the same VM, where the installation then succeeded.

The English messages and their nine translations, the `opencode.md` help
page in all ten locales and `dev-docs/OPENCODE_RUNTIME_SETUP.md`
describe the obstacle.

## Tests

- `legacy-installation.test.ts`: builds the exact VM layout (link in the
user npm bin directory) and checks that the plan never offers a prefix
over the foreign command and reports `occupied`. The ordinary layout
must still offer the prefix, and a read-only prefix must report
`unwritable` (POSIX, non-root).
- `opencode-setup.test.ts`: renders the occupied notice with no install
action.
- A second POSIX regression puts a verified npm OpenCode 1.x
installation in a read-only user prefix and expects `unwritable`, never
`occupied`.
- Results:
- the `opencode-update` unit tests: 51/51 on Windows (1 POSIX-only
skipped); on the Ubuntu VM as uid 1000, 48 passed, 0 failed, 4
Windows-only skipped, including both POSIX cases;
  - the `opencode-setup` browser tests: 34/34;
  - the server and UI typechecks;
  - `npm run test:help`.
2026-10-22 13:49:20 +02:00
Pascal André
6b8279ca46
fix(tauri): keep the Linux native menu bar hidden without losing accelerators (#1092)
## Problem

On Linux the **Tauri** app showed a GTK menu bar ("File Edit View Window
Help", in English until the renderer reported its labels) above the
application's own title-bar menus, so every window had two menu rows.

Windows and Linux present menus in the title bar, and the host already
called `hide_menu()` on non-macOS platforms, but on Linux the hide never
stuck. Windows are created invisible, hidden, then shown, and Tao shows
a window with GTK's `show_all()`, which shows every child widget again,
including the menu bar.

## Fix

`menu_bar::hide` (new `menu_bar.rs`) replaces the four scattered
`hide_menu()` calls: local windows, Preferences, fullscreen restore and
fullscreen toggle.

- **Windows**: same `hide_menu()` as before.
- **macOS**: nothing, since its menu bar is global.
- **Linux**: on the main thread, where GTK widgets live, the GTK menu
bar is also marked **no-show-all**, so `show_all()` leaves it hidden.
- GTK only activates the accelerators of on-screen widgets. A merely
hidden bar would therefore have disabled native accelerators such as
`Mod+Shift+N` (New Window) that the visible bar provided, and that
`docs/help` documents. The bar answers `can-activate-accel` with `true`,
connected once per bar.

`gtk` becomes a direct Linux dependency, already locked at 0.18.2
through Tauri. `dev-docs/UI_SHELL.md` documents the hidden native bar;
the public help stays accurate.

## Validation

Ubuntu 24.04 VM, AppImages repacked around the released AppDir:

| Build | GTK menu bar | Ctrl+Shift+N |
|---|---|---|
| Original binary | shown above the title-bar menus | opens a new window
|
| Hide + no-show-all only (intermediate) | gone | falls through to the
renderer's New session ✗ |
| **This PR** | gone in the main, second and Preferences windows | opens
a new window ✓ |

Quitting leaves no helper process or crash report. `cargo check -p
codenomad-tauri` passes on Windows, and the Linux release build compiles
the GTK calls.
2026-10-22 13:05:51 +02:00
Pascal André
9d9fc8d235
fix(tauri): stop AppImage helpers before exit so WebKit never crashes on quit (#1091)
## Problem

Quitting the Linux **Tauri AppImage** can show Ubuntu's "Sorry, Ubuntu
24.04 has experienced an internal error" dialog. apport records a
`WebKitWebProcess` crash with **SIGBUS**.

The AppImage runtime unmounts the bundle as soon as the main process
exits. WebKit's helper processes, started from that mount, can still be
running for a moment: their next page fault in the vanished mount raises
SIGBUS.

- Ubuntu 24.04 VM, `1.0.0-dev-20261022-1840c710` AppImage: 2 crashes in
3 quits.
- The same build extracted with `--appimage-extract` (no mount) never
crashed.
- Stable releases shipped Tauri Linux as a `.deb`, so the problem comes
with the AppImage distribution.

## Fix

On `RunEvent::Exit`, which covers ordinary quits and updater restarts,
an AppImage launch (`APPIMAGE` + `APPDIR` set) stops its direct child
processes whose executable lies under the mount, before the process
exits. The WebKit helpers are direct children of the main process. The
new module is `appimage_exit.rs`:

- each child is pinned with a **pidfd**, then checked again as a child
of this process running from the mount, so a process that reused a PID
is never signalled;
- `SIGTERM`, then pidfd polling up to 1.5 s, then `SIGKILL` and a final
0.5 s;
- children running from elsewhere (the shared OpenCode service) keep
running;
- `.deb`/Electron, Windows and macOS are unchanged; kernels without
`pidfd_open` keep the previous behaviour.

`dev-docs/TAURI_UPDATER.md` documents the exit contract and its
validation.

## Validation

- **Linux unit tests** (new): `/proc/<pid>/stat` parsing with command
names containing spaces and parentheses, and a real child started from a
temporary "mount" that is stopped while a child started elsewhere keeps
running. They pass on Ubuntu 24.04.
- **Real AppImages** on the Ubuntu 24.04 VM, repacked with appimagetool
around the released AppDir, once with the original binary and once with
the fixed one:

| Trial | Original binary | Fixed binary |
|---|---|---|
| Secondary `WebKitWebProcess` frozen (`SIGSTOP`) before closing the
window | survives the main process, i.e. outlives the mount | stopped
before the main process exits |
| Normal quit | 0.06 s, no leftover | 0.08 s, no leftover, no crash
report |

  The shared OpenCode service kept running throughout.
- `cargo check -p codenomad-tauri` passes on Windows.
- Running the whole crate suite on the VM, only the cross-host election
tests (they need the full checkout with `tsx`) and the zsh test (zsh not
installed) failed. Linux CI has both.
2026-10-22 12:36:48 +02:00
Pascal André
1840c71054
fix(timeline): mirror every transcript block in order and keep clicks on the clicked marker (#1086)
Some checks are pending
Publish help / build (push) Waiting to run
Publish help / deploy (push) Blocked by required conditions
## What changes for the user
The timeline now matches the transcript block for block, and a click
lands exactly where it should.

- **One marker per visible block, in transcript order:** each reply,
each thinking run, each tool (or collapsed tool group), each compaction
and each system context. Markers follow Message content: a block that is
hidden in the transcript is hidden in the timeline too.
- **The clicked marker stays active** until you scroll, and its own
block, not just the start of its message, is placed at the reading
position. This also works for history that is not loaded yet.
- **While scrolling,** the active marker is the first block below the
reading line.

## Reproduced in numbers before fixing
We replayed 45 of the user's real sessions (65,241 messages, exported
read-only) into the real `SessionView`, with the outline, windows and
previews served by the real server readers. Each session was measured at
1280×800 with two presets, using 16 real pointer clicks and 8 wheel
scrolls per run. Baseline and fix used the same seeds.

| Metric | Before, Complet | Before, Balanced | After, both presets |
|---|---|---|---|
| Sessions out of sync | 43/45 | 42/45 | 0/90 runs |
| Markers / expected | 68,949 / 130,818 | 68,949 / 98,759 | 130,818 /
130,818 and 98,759 / 98,759 |
| Click: clicked marker active | 53.6% (46.0% the one above) | 57.2% |
1,440 / 1,440 |
| Click: block at reading position | 41.5% (median off by 35 px) | 82.2%
| 1,440 / 1,440 (median 0.27 px) |
| Scroll: reading-line block active | 93.1% | 92.2% | 505 / 505 moving
wheels |

What was wrong with the markers in the Complet runs:
- 29,782 tool markers were collapsed into one marker per step.
- 31,777 thinking blocks and 282 system blocks had no marker.
- There were 10,232 order inversions.

## Causes
1. Once an outline existed, the outline projection replaced the markers
of loaded messages too (`message-section.tsx`). It collapsed a step's
tools into one marker placed before its reply, and dropped system
blocks.
2. Resident segments merged thinking into replies, dropped thinking-only
steps and skipped system parts.
3. After a click or a scroll, the active marker was the message's first
marker. The message itself came from a scroll offset that rounds a row
starting just below the top to the previous message.
4. Clicks navigated to the message, not to the block.
5. Jumps into unloaded history had no row to hold while the target was
not yet rendered.

## How
- **Server (outline):** each assistant step can now carry an optional
`layout` field. It lists the step's visible parts in order: thinking
runs, reply runs and each tool, at most 64 entries. It is computed in
the same bounded SQL. Older plugin copies don't send it, and the client
then falls back to the counts: thinking, then text, then tools.
- **Markers:** `buildTimelineSegments` emits one marker per block, keyed
`messageId:type:ordinal`. Loaded messages keep their exact markers.
Unloaded steps are placed at their native position from `layout`.
- **Click and scroll:** the list's `scrollToKey(key, selector)` holds
the selected block on every frame, survives row remounts, and follows
Virtua's estimate until the row renders. `timeline-transcript-blocks.ts`
maps markers to blocks and finds the block at the reading line.
- **Saved indexes:** outline indexes saved without `layout` are
revalidated once.
- **Docs and help:** updated in `dev-docs/SESSION_HISTORY_NAVIGATION.md`
and in the help pages for all 10 locales.

## Behaviour to be aware of
- **The timeline has more markers:** about +90% with Complet and +43%
with Balanced on this sample, because every tool, thinking and system
block now has its own marker.
- **Tool icons for unloaded steps:** a step not loaded yet shows its
first tool's icon on all of its tool markers, until its window loads.
- **Plugin requirement:** the pruning plugin must ship with the new
`layout` field.

## Validation
- **New regression test:** `tests/browser/timeline-sync.test.ts` passes
4/4 and fails 4/4 on unmodified dev. Fixtures are synthetic.
- **Measurement harness:** the opt-in qualification `npm run
qualify:browser:timeline-sync`.
- **Browser suites:**

  | Suite | Result |
  |---|---|
  | history-navigation | 65 |
  | message-scroll | 128 |
  | session-rendering | 47 |
  | render-cost | 17 |
  | full-source-copy | 12 |
  | search-highlight | 11 |
  | system-messages | 10 |
  | timeline-tool-filters | pass |
| streaming-outline (no full reprojection over 64 token deltas) | pass |
  | session-search | pass |
  | transcript-presets | pass |

- **Other checks:** UI unit tests 75/75, server session-pruning 70/70,
help 9/9 and the UI typecheck pass.
- **After rebasing on dev:** the typecheck passes, along with
timeline-sync 4/4, timeline-tool-filters, full-source-copy 12/12,
usage-metrics 5/5 and streaming-outline.

Oversized files touched: `message-section.tsx` (~1,637 lines),
`virtual-follow-list.tsx` (~1,368), `message-timeline.tsx` (~508),
`message-timeline.css` (204).
2026-10-22 05:32:43 +02:00
Pascal André
761aa66486
fix(opencode): keep a working connection when a periodic recheck is inconclusive (#1090)
The shared OpenCode connection is rechecked by the first acquisition more
than 30 seconds after its last check, by rerunning CLI discovery (`service
status`, `service get password` and an authenticated status probe, bounded by
the caller's deadline). When that discovery failed or reported no service,
the healthy connection was invalidated. A slow daemon, or slow CLI spawns on
Windows, could therefore tear down a working connection. Every event stream
on it then broke and the relay reconnected in the middle of streaming: the
reconnection the transcript now survives (#1088), triggered without any real
failure.

Discovery failure and absence are now inconclusive. The working connection
is kept and the next recheck waits for the following interval, so a slow
service is not re-probed on every request. A discovered daemon that differs
in URL, PID, version or credentials still replaces the connection
immediately. A dead connection is still retired by failed location
validation (`withClient`), proxy transport or authentication failures and
upstream event-stream EOF/errors, after which the next acquisition discovers
or starts the service as before. An inconclusive recheck never starts
another service. A frozen daemon that accepts TCP but never answers is now
detected through those paths, bounded by Undici's inactivity timeouts,
rather than by the periodic recheck.

Validation: two new `opencode-service` tests fail on the previous code. One
keeps the connection through a timed-out and an empty recheck without
rediscovering before the next interval or starting a service. The other
keeps a live event stream delivering events while another request's recheck
fails. The shared-service, connection generation, setup reconnect, event
relay/routing/ownership, subscriber lifetime, pending discovery, plugin
lifecycle and proxy suites pass (82/82), the wider workspace manager,
CLI/WSL lifecycle, native registration and opencode-update suites pass
(227/227), and the server typecheck is clean.
2026-10-22 00:47:04 +02:00
heunghingwan
0d6629c129
fix(transcript): keep streaming live across shared-stream reconnects (#1088)
Some checks are pending
Publish help / build (push) Waiting to run
Publish help / deploy (push) Blocked by required conditions
fix(transcript): keep streaming live across shared-stream reconnects

## Problem

Long model reasoning streams stop updating the transcript roughly 30
seconds in: the visible thinking text freezes, and the display only
catches up when the thinking phase ends. Shorter streams never freeze.

A diagnostic reproduction against the real SDK/dispatcher pinned the
freeze exactly: after a mid-stream `server.connected`, ten live
reasoning deltas left the store text frozen at its pre-reconnect value
and **zero** `message.list` reads were even attempted — even
`session.reasoning.ended` and the execution terminal events could not
recover it.

## Root causes

1. **~30 s trigger** — after `CONNECTION_RECHECK_INTERVAL_MS`, any
request through the shared OpenCode service re-runs discovery; a failed
or changed probe (a busy daemon is slow to answer) invalidates the
healthy connection. The shared event stream tears down and resubscribes,
and every subscription starts with `server.connected`.
2. **Reset mid-stream** — the UI treated `server.connected` as a full
data reset (`destroyOpenCodeData`). The fresh reducer starts empty:
reasoning deltas no-op (`editReasoning` cannot find the message) and
nothing repopulates the projection until a later `session.step.started`
inserts the assistant row again.
3. **Starving recovery** — repopulation requires one `message.list` read
to return with an unchanged event revision; each arriving delta restarts
the read (the guard that never merges an obsolete page into newer
streaming messages). During continuous streaming that read cannot
complete, so any reconciliation starves until the stream pauses.

## Changes

- `stores/opencode-data.ts`: `server.connected` now runs
`reconnectOpenCodeData` instead of a bare destroy. A shared-stream
reconnect is transport recovery, not a data reset:
- The reset semantics are kept (generation fences, observer rebinding,
REST state untouched) — every existing reconnect test passes unchanged.
- Before the reset, each live session's last projected page is captured
and seeded into the fresh reducer through the existing `syncMessages`
snapshot mechanism, so post-reconnect deltas keep applying and the
display keeps streaming.
- One trailing reconciliation is scheduled per session (new non-dropping
`reconnectResync` mode): it fetches only after the stream stays quiet
for 1 s with a stable revision, then swaps in the authoritative page to
repair anything the disconnect window missed. It never drops events and
defers to dropping-mode recovery when an overflow collapse or pruning
takes over.
- `stores/opencode-data-reconnect.test.ts` (new, in the CI unit list):
three regressions — the revision-race read invariant (obsolete pages
never merge), dropping-mode admission (frozen until the stream pauses),
and reconnect continuity (deltas keep displaying; the quiet snapshot
republishes).

## Behavior compatibility

Suites covering reconnect semantics (projection reset, inbox observer
rebinding, compaction reconnect fencing, replay fences, generation
recovery, request authority) pass without modification.

Dropping-mode resync semantics (overflow, pruning) are unchanged and now
pinned by a regression test.

`opencode-data.ts` is 978 lines after this change (over the ~800
target); splitting the reconnect path into its own module is a
reasonable follow-up.

## Pre-existing failures (not caused by this PR)

Four tests fail identically on a clean checkout (verified by stashing
this change and re-running): the pinned `@opencode/client` 2.1.0 /
event-manifest contracts drifted and the fixtures were not updated:

- `opencode-transcript-replay.test.ts` × 2:
`session.compaction.started`/`ended` fixtures no longer decode (the
manifest now requires `data.recent`); both integration tests die in
fixture construction before any assertion runs.
- `opencode-data.test.ts` ("preserves SDK step, snapshot, reasoning…"):
expects the old `native` field on reasoning parts; the 2.1 reducer maps
it to `state`.
- `session-pruning-projection.test.ts`: expects a provider
`reasoningField` mapping that the current contract no longer returns.

These are fixture/expectation updates against the pinned SDK, unrelated
to the streaming machinery; CI may already be red on them. They should
be repaired in a separate PR so this fix stays reviewable.

## Validation

- Red→green: the new reconnect regression fails on the pre-fix tree at
the frozen-store assertion (`'before '` vs `'before after0 after1 '`)
and passes with the fix.
- UI unit baseline (CI file list + new file): 552 tests, 548 pass, the
same 4 pre-existing failures before and after.
- `tsc --noEmit`: the same 4 pre-existing errors before and after.
- Browser: mission-control 24/24, provider-usage 13/13,
mission-task-reader 18/18, message-scroll 128/128. `part-durations`
page-load timeouts reproduce identically on a clean tree (Vite
cold-start environment issue).
2026-10-21 23:34:37 +02:00
Pascal André
1f0fc4fe9f
fix(worktrees): omit a checkout mid-removal instead of failing the whole inventory (#1089)
fix(worktrees): omit a checkout mid-removal instead of failing the whole inventory

Removing a worktree with `git worktree remove` deletes its `.git` file first and
the rest of the folder afterwards, which takes minutes when the checkout still
holds `node_modules`. During that window the folder and its registration still
exist, so the native inventory inspected the entry, `readCheckoutIdentity` hit
ENOENT on `.git`, and the shared scan failed the entire inventory. Every
ownership check depending on it failed too: conversations in other worktrees
showed "Internal Server Error" for messages, timeline and permission receipts,
and pending-request recovery reported every folder of the project as
unverifiable, until the removal finished.

The inventory now treats an entry whose Git layout no longer exists (missing
`.git`, or a `.git` pointer whose administrative directory is gone after an
interrupted removal) like a vanished folder: it is omitted and the other
checkouts are listed normally. Any other identity failure keeps failing the
inventory closed. A half-removed checkout is not a usable worktree, so
conversations still placed there stay unowned until it is gone, while the rest
of the project keeps working.

Validation: a new isolated Git fixture registers healthy, mid-removal and
orphaned worktrees, deleting the orphan's administrative directory during
inspection (after Git's annotation snapshot) to cover that race. On the
previous code it fails with `ENOENT: stat '<removing>/.git'`; with the fix the
inventory lists only the root and the healthy checkout. The native custody, inventory, display fence,
directory, session evacuation and event ownership suites pass (40/40), and the
server typecheck is clean.
2026-10-21 22:24:01 +02:00
Pascal André
1b97c7df8a
fix(transcript): jump between user messages with Alt+Up/Down, also from the composer (#1087)
Shift+Up/Down already selects text in every text field, so the user-message jump could not work from the composer, where people usually are. The defaults become Alt+Up/Down, and Cmd+Option+Up/Down on macOS, where Option+Up/Down moves to a paragraph's start or end in text fields. Neither edits text, so the jump now also works from the composer and other text fields, keeping the draft, its caret and focus; Shift+Up/Down selects text again everywhere. Saved custom bindings are unchanged.

The key guard allows text fields and leaves modified arrows where they already mean something: code editors (Monaco moves lines) and terminals, arrow-key widgets, selects and popup triggers (Alt+Down opens them, such as the new-instance launcher), fields that move a list's active option (aria-activedescendant, input[aria-controls], such as the mission picker search), non-text inputs and IME composition. The composer's / and @ picker moves its selection with every arrow key, so its root carries data-prompt-picker and the jump yields while it is open. Platform defaults live in defaultHotkeyBindings, like prompt-alternate-send. The help shortcuts table (10 locales) and HOTKEYS.md describe the new defaults.

Validation: unit tests for the platform defaults (no conflict on either platform) and the key guard; browser regressions jump with Alt from the conversation and the composer (draft, caret and focus kept; Shift+ArrowUp still selects), keep the key for an open composer picker, and, with a real MissionPicker for Alt and emulated-macOS Cmd+Option, move the picker search's option without moving the conversation (the guard checks fail without their selectors). hotkeys-settings and composer-picker-enter pass; UI own-tsconfig typecheck, root typecheck and test:help pass. Gatekeeper review (GPT-6.1 Sol): one P2 (mission picker search) fixed, then zero actionable findings at 7eb2cb081da4ebb1a04e5a7391322deaeb154dd4.

Refs #1075
2026-10-21 20:36:02 +02:00
heunghingwan
c716f796d8
ui: keep the message token disclosure on one row in both states (#1084)
## Summary

The token/cost line that closes every agent message previously turned
into two
rows when expanded: a bare `Tokens` label line (chevron + label) with
the six
metric chips on a second line below it. This PR keeps the disclosure on
one row
in both states — expanding swaps the collapsed summary chips for all six
chips
on the very same line as the chevron.

## Problem

- Collapsed, the row is a compact single line: `› INPUT 3K · OUTPUT 54 ·
COST $0.76`.
- Expanding it replaced that line with a label-only row (`› Tokens`)
plus a
separate chip row, so a one-line affordance grew to two rows and pushed
the
  transcript content down twice as much.

## Change

- **Expanded = same row.** The six chips (input, output, reasoning,
cache read,
cache write, cost) now render in place of the summary chips, beside the
chevron. The `Tokens` label line and the separate content row are gone;
at
wide widths the expanded card is exactly as tall as the collapsed card.
- **Wrapping is preserved.** Expansion exists to read the values, so the
expanded row is never ellipsized: when width runs out, the chips wrap
onto
further lines in place (this wrapping behaviour is unchanged from
before).
- **Collapsed is untouched.** The collapsed summary keeps its single
  ellipsized line; its rendering is pixel-identical to before.

**Expanded — wide layout** (before shows the label line + chip row;
after is one row):

<img width="884" height="344" alt="wide-expanded"
src="https://github.com/user-attachments/assets/06abd5cd-b15d-4b29-ab39-1202d4c35ca5"
/>


**Expanded — narrow layout, 220 px** (both wrap instead of truncating;
after drops the label line and starts the chips beside the chevron):

<img width="390" height="339" alt="narrow-expanded"
src="https://github.com/user-attachments/assets/e4e5fbe7-a37c-42f6-8f73-3628d21ac2c6"
/>


**Collapsed — unchanged** (wide and narrow; pixel-identical
before/after, narrow still ellipsizes):

<img width="884" height="310" alt="wide-collapsed"
src="https://github.com/user-attachments/assets/71b40a15-6a19-4232-8eef-e0ce1327ff90"
/>


<img width="390" height="175" alt="narrow-collapsed"
src="https://github.com/user-attachments/assets/f77ebdd9-0540-4c74-9f2b-3a3fb64a7d9a"
/>


## Implementation notes

- `packages/ui/src/components/message-usage.tsx`: the toggle now always
renders
`chevron + chips` — the collapsed state renders the three summary chips,
the
  expanded state renders all six in the same slot. The separate expanded
  content `<div>` and its `aria-controls` wiring are removed.
- `packages/ui/src/styles/messaging/message-usage.css`:
`.message-step-usage`
is the expanded wrapping row (`flex-wrap`, no truncation); the collapsed
`.message-step-usage-summary` keeps its single-line ellipsis. The unused
  `.message-step-usage-label` / `.message-step-usage-content` rules are
  deleted. The chevron pins to the first chip row when the row wraps.
- **Accessibility:** the button's `aria-label` carries all six full
values when
expanded (`aria-label` overrides the visible chips, so they are repeated
there deliberately); the collapsed label is unchanged. The shared
disclosure
  chevron rotation keeps working via the generic `aria-expanded` rule.
- **dev-docs/TRANSCRIPT.md:** the message-token disclosure convention
note is
  updated to describe the one-row layout.

## Help pages

No help change needed: `docs/help/*/conversations.md` documents the line
as
*"expand it for reasoning and cache counts"*, which remains true — the
documented behaviour is unchanged and the two-row layout was never
documented.

## Validation

- TDD red→green: the new assertions were watched failing first (`0 !==
6`
  chips on the row, stale `aria-controls`), then passing.
- `tests/browser/usage-metrics.test.ts` — 5/5, now also covering:
expanded
chips on the toggle's row; single row at wide width; wrapping onto
multiple
  rows at 220 px in LTR **and** RTL with no horizontal overflow and no
  ellipsis; chevron level with the first chip row; Space/Enter collapse
  restores the three-chip summary.
- Related suites that render the usage row: `status-tokens` (4/4),
  `transcript-presets` (1/1), `search-highlight` (0 failures).
- Visual: before/after screenshots captured from the real fixture at 900
px and
220 px (the composites above); collapsed rendering verified
pixel-identical.
2026-10-21 18:52:11 +02:00
Pascal André
7a1fc90310
docs(agents): keep AGENTS.md to universal rules and agent docs to the current state (#1082)
AGENTS.md loaded about 33,000 tokens into every agent session, subagent and reviewer, mostly per-feature contracts, and the agent documentation around it had accumulated duplicates, dated logs and history of removed designs. This PR makes AGENTS.md a short set of rules that apply everywhere plus an index of area documents, and leaves AGENTS.md, dev-docs and the repository skills describing only the current state.

Three commits, reviewable separately:

1. **Move (lossless).** Every per-area rule moved verbatim from AGENTS.md into its area document (existing ones, or six new ones: UI_CONVENTIONS, UI_SHELL, COMPOSER, TRANSCRIPT, SETTINGS, PROJECTS). A script split the old file into 208 units, required one destination for each and checked all 199 content units verbatim at their destination. AGENTS.md went from 134,000 to 16,000 characters.
2. **Trim.** A per-area ponytail review removed rules stated two or three times (keeping the more precise copy), dated validation logs, per-version qualification sections below the current minimum, abandoned designs and self-references; seven documents whose surviving rules live elsewhere were deleted. It also fixed statements the code contradicts (locale registration, the Mission play/recovery condition, plugin package updates, cleanup progress UI, timeline index persistence, automation restart/targets, stale pins and minimums).
3. **Current state only.** The new AGENTS.md rule: no history (removed features and their names, former behavior, provenance, dated logs) and no denials; correct a contradicted statement at its source. DEVELOPER_MODE.md is merged into BROWSER_AUTOMATION.md and every Developer Mode mention is gone (including the README and the browser help page in ten locales). Names that only made sense against older designs are renamed with every reference updated, MISSIONS_ENVIRONMENT_ADMISSION is folded into SESSION_ENVIRONMENT, and INDEX lists every document. The server npm README now states the real OpenCode minimum (2.1.2).

AGENTS.md is now about 14,000 characters (roughly 3,500 tokens); AGENTS.md, dev-docs and the vendor README went from about 11,200 lines to under 4,700.

Validation: every relative Markdown link and every cited dev-docs path resolves; no reference to a renamed or deleted document remains; `npm run test:help` passes for all ten locales.

Review: three independent GPT-6.1 Sol (high) reviews by area found binding rules lost in the trim and current-state passes; 25 were restored as present-tense facts or prohibitions and four contradicting statements corrected, each checked against code. A Fable 5.1 gatekeeper review of the whole PR passed; its follow-up routes session recovery from the index, points to dev-docs/INDEX.md and documents the family identity read.
2026-10-21 16:59:43 +02:00
Pascal André
625e445bc2
ci(help): remind PRs that change UI labels to update the user help (#1081)
Some checks are pending
Publish help / deploy (push) Blocked by required conditions
Publish help / build (push) Waiting to run
ci(help): remind PRs that change UI labels to update the user help

The help drifted for a week because about forty user-visible PRs merged without touching it, while CI only checks the help's structure and links. The rule asking for help updates in the same PR was buried in the AGENTS.md i18n section; it now has its own short Pull Requests section next to the commit guidelines, which agents consult when committing and opening PRs.

A new non-blocking workflow, help-update-reminder.yml, catches the most common miss: when a PR changes English UI messages (packages/ui/src/lib/i18n/messages/en/) but no file under docs/help/, it posts one marked comment and a warning annotation; once the PR touches the help, the comment is removed. It runs on pull_request_target like the existing Restrict Non-Dev PRs workflow, but only reads the PR file list and comments through the API and never checks out pull request code, so the write-scoped token never runs PR content. Because pull_request_target uses the base branch's workflow, it starts on PRs opened after this merge. USER_HELP.md documents it.

Validation: replayed the filter read-only on past PRs: it would have flagged #1014, #1026 and #1052, which changed labels without updating the help, and stays quiet for #1043 and #1054, which updated it, and for #1057, #1063, #1079 and #1080. The workflow YAML parses, the step passes bash -n, the comment body has no leading indentation and the marker lookup returns the expected comment ID on a real PR. Behavior-only changes without label edits (such as #1063) are not detected; the AGENTS.md rule covers those.

Review follow-up: the reminder only matches comments authored by github-actions[bot], runs are serialized per PR so overlapping events cannot post twice, clearing removes every owned reminder, and the workflow keeps only pull-requests: write.
2026-10-21 15:26:38 +02:00
Pascal André
9fb6996576
fix(worktrees): separate fresh recovery and display refresh outcomes (#1080)
## Follow-up to #1076
Separate required-fresh outcomes from display refresh demand. Fresh
failure rejects only fresh callers, queued display work still runs, and
no fresh flag leaks into the next display read. Concurrent fresh calls
coalesce; cached completion cannot satisfy fresh recovery.

One serialized read/publication lane retains priorities and ordering.
Initial display fallback cannot erase a fresh snapshot; existing display
dirty/trailing coalescing retained. No backend/move safety change,
polling or mutation replay.

## Validation
Original queue fails deterministic fresh+fresh+display and
concurrent-fresh regressions. Fixed: relevant UI units82/82
(worktree-ready14/14), worktrees browser13/13, focused interruption-dock
recovery2/2, UI typecheck and diff check pass. Node24.20.0, private
dependencies; no full browser sweep or live app/shared daemon/userDB.
Server unchanged; native fixture not rerun.

Three focused files, none oversized.
2026-10-21 14:53:07 +02:00
Pascal André
ad844d0645
feat(transcript): jump to the previous or next user message with Shift+Up/Down (#1077)
Finding "what did the agent answer when I asked…" meant scrolling or hovering the timeline marker by marker. Two new commands, Previous User Message and Next User Message, appear in the command palette and in Settings → Keyboard shortcuts, bound by default to Shift+ArrowUp and Shift+ArrowDown. From the conversation (where focus lands after a send), they place the nearest user message before or after the reading position at the top, paging its history window in when it is not resident, like a timeline click. Text fields, arrow-key widgets, IME composition and modal dialogs keep the keys, so Shift+Arrow still selects text in the composer; key repeat does not repeat the jump.

components/user-message-navigation.ts searches the timeline's markers, including the structural index beyond the resident window, and uses list order for a current message without a marker. MessageSection handles the shortcut in the capture phase, before the transcript reads arrows as a scroll gesture; the palette commands reach it through JUMP_USER_MESSAGE_EVENT. The jump requests the index even while the timeline is hidden, and a jump finding nothing resident before it arrives runs once it does. The last target stays the reference while loading or shown, since the list cannot raise a message near the end to its top. Both are forgotten in cancelWindowNavigation, which every other navigation (First/Latest, sends, paging, session changes, deactivation) passes through, and on scroll gestures, so a late index never overrides a newer navigation.

The public help lists the shortcut in all ten locales and states the 74-action catalog (it said 49); HOTKEYS.md and AGENTS.md describe the dispatch; labels are translated in every locale.

Validation: unit tests for the target search and key guard plus the catalog count (now run in CI); browser regressions for resident and windowed jumps, the composer keeping Shift+ArrowUp, the deferred jump with a hidden timeline, an explicit Latest dropping it, and repeated jumps near the end (the last three fail without their mechanism). hotkeys-settings, header-windows and history-navigation suites pass; UI own-tsconfig typecheck, root typecheck and test:help pass. Gatekeeper review (GPT-6.1 Sol): one P2 (deferred jump surviving Latest) fixed, then zero actionable findings at 7ab3f17d3cac6be694f989fde8d217c313599f9d (rebased on dev as bf6e09990ba7b65789d5789fe00d87b5e1362625; only the help pages' catalog count conflicted).

Refs #1075
2026-10-21 14:42:17 +02:00
Pascal André
1dabb5110f
docs(help): bring the user guide back in line with the current app (#1079)
docs(help): bring the user guide back in line with the current app

The help was written on 2026-10-14 and about forty user-visible changes merged since without touching it, so several pages described controls that no longer exist: Missions still listed four playbooks and a Save step for preferences that now save on each change, the depth editor kept its old names, notifications still had their former switches, and voice input pointed to a microphone button that is not in the composer.

Every English page was re-checked claim by claim against the current labels (English UI messages) and behaviour, then the nine translations were updated to match, using each locale's own UI labels. Notable corrections: Missions (Compare two models, autosaving defaults with Apply to / Global / Reset, Subsession nesting depth, prepared hand-offs, retired-task interrupts, the named causes of "Mission Control is unavailable", standalone server support); conversations and history (reasoning/tool durations, token lines adding up the cleanup selection, presets including Nothing, timeline markers following Message content, in-place reveal of long text, Open in session, focus after send, Queue message / Steer now, Retrying); settings (notification routing table and defaults, Stats, Mode, info tips, Server setting badge, localized native menus); shortcuts (72 actions, Clear shortcut, Ctrl/Cmd+K); voice (More actions entries, Save status); files (live Workspace refresh, Show 200 more); OpenCode, install, server and updates (2.1.1 recommended, OpenCode 2 beside an older installation, version card first, private loopback listener for Missions, preview releases offered once all builds are published); providers, integrations, browser, SideCars, desktop and remote pages. Translations also repaired older drift, mainly the project-tab closing passages.

The Chat settings table header (Default state, its title, item column, the thinking row and its accessible label) was still English in eight locales; it is now translated so the help can cite what users see. AGENTS.md now asks every PR changing user-visible behavior to update the affected help pages in the same PR, since CI validates structure and links but not whether the text still matches the UI; it also corrects the needs-input notification default. USER_HELP.md no longer claims native menus stay English.

Validation: npm run test:help (all ten locales, 25 pages, heading ids unchanged), a full help build, and the i18n and transcript-visibility unit tests.
2026-10-21 14:38:29 +02:00
Pascal André
f314123dd1
fix(opencode): qualify and require 2.1.2 for correct Code Mode execution (#1078)
Raise the approved external runtime minimum and recommendation to 2.1.2 after reproducing incorrect native Code Mode job ordering on 2.1.0/2.1.1. Keep the independently qualified 2.1.0 client/plugin build pins and preserve bounded contract recognition for custom/future runtimes. Add the real isolated execute regression to the minimum/latest compatibility matrix, align supported installer fixtures without weakening assertions, and update setup/help in all ten locales. Two author-side review passes found no blockers; the final focused set passes 93/93 and native Code Mode passes. Qualification includes builds/typechecks, UI/browser, native pruning/Missions/recurrence, migrations and isolated installation/upgrade. Record the broad server shutdown timeout and its two unchanged isolated passes precisely. Admin merge explicitly authorized by the maintainer without waiting for CI.
2026-10-21 14:36:51 +02:00
Pascal André
8c6a90922e
fix(recovery): reconcile removed worktrees without hiding live interruptions (#1076)
## Problem and fix
A cached UI worktree descriptor can keep a removed idle directory
required after native/backend inventory healed. On inventory-only
missing/not-owned recovery failures, perform ONE fresh worktree
inventory read and retry ONLY directories that genuinely became optional
through the existing loaded-only pending broker.

The failed-move clue exposed another guard gap: preserve source/target
locations and pending move identity after failed/incomplete
reconciliation until complete family inventory plus fresh activity
succeeds. An old read cannot release a newer move, and mutations are
never replayed.

## Safety
- Root/catalogue selection, active sessions, known Forms/permissions and
uncertain move locations remain required.
- No blanket exclusion, directory/session deletion/move, daemon restart,
ordinary pending-list fallback, fake empty coverage or warning
suppression.
- Existing broker negative proofs and post-IO ownership/deletion fences
remain; successful sibling recovery and edited drafts retained.
- The precise12:58 warning in the screenshot is not proven causal: both
directories are now absent from live/Git inventory and not active.
Current healed state does not establish historical bad registration.

## Validation
Red store and real-browser regressions reproduce on base. Green:146 UI
tests,78 server tests,2 focused browser cases; both typechecks and diff
check pass. Isolated real Git/filesystem fixture exercises external
removal with warm display/stale native registration. Read upstream2.1
refresh implementation/test (refresh prunes absent registrations; list
alone does not discover).
No full browser sweep/real-daemon fixture. WSL mocks pass, live
WSL/native hosts unverified. No installed app/shared daemon/user
database/config mutations.

Oversized touched sources: api-client.ts~820, instances.ts~2420,
session-api.ts~2050. Added tests extend existing CI-listed files.
2026-10-21 14:34:17 +02:00
Pascal André
92ba215254
fix(missions): run Missions on the standalone server and name each unavailable cause (#1070)
## Problem
Missions dispatch through the owning backend's loopback automation
bridge, not through the desktop window. Yet the bridge and the Missions
plugin were only provided when a desktop host launched the server. On
the standalone server (`npx @neuralnomads/codenomad…`, whose default
listener is HTTPS-only), Missions never loaded, and Mission Control told
users to "Reconnect CodeNomad", which cannot help (#1069). Tauri on
macOS/Linux sets no native parent and was affected the same way.

## Fix
- **Private loopback listener:** the HTTP listener always starts. When
HTTP was not requested, it is private and unannounced, on a random
`127.0.0.1` port.
- **Bridge and plugin:** the bridge is published whenever a loopback
HTTP listener exists, and the Missions plugin is provisioned once that
publication settles, so early connections never skip it.
- **Automation stays desktop-only:** without a desktop host, the
bridge's `browser-claim` answers only `purpose: "mission"`, which the
Missions plugin now sends. Browser tools therefore never select an owner
that cannot open a page. Remote Control keeps routing into the
application users reach.
- **One deletion fence per backend:** all listeners now share one
`WorktreeDeletionFence`, and with it the mission creation/recurrence
holds. Previously each listener had its own, so a deletion through HTTPS
could not drain a mission admitted through the HTTP bridge. This could
already happen in desktop dual-listener mode.
- **Exact unavailable causes,** localized in all ten locales:
- `host-unavailable`: this backend has no bridge; reported before
reading OpenCode.
- `plugin-unavailable`: OpenCode has not loaded the plugin. Refresh,
then use **Reload OpenCode configuration**.
- `plugin-failed`: OpenCode failed to load the plugin; its error is in
the Plugins list.

## Validation
- New real startup test: the actual entry point as a standalone
HTTPS-only server, isolated profile, no OpenCode. It proves the private
`127.0.0.1` bridge is published, authenticated (401 without token),
unannounced and removed on shutdown.
- Bridge route tests: mission-only claims without a desktop host,
ownership still required, native requests fail closed. The plugin test
asserts that the claim carries the purpose.
- Missions route tests cover each unavailable reason, and
`host-unavailable` reads nothing from OpenCode.
- The isolated native Missions fixture
(`scripts/test-missions-native.mjs`) passes on **OpenCode 2.1.2**
through the real bridge route with no desktop host.
- Server and UI typecheck: 0 diagnostics. Full server suite running.

Refs #1069
2026-10-21 12:17:49 +02:00
Pascal André
00f39ada16
fix(timeline): space every marker evenly (#1062)
The rail doubled the gap wherever a tool marker started a different message, a collapsed tool group ended or a prompt followed an assistant with tools: a V1 notion of tools belonging to their assistant message. OpenCode V2 records each model call as its own message, so almost every tool started a new group while tools called together stayed tight, giving irregular, pronounced gaps that meant nothing to the reader. Every visible marker now follows the previous one by one uniform gap, and the message-tool bookkeeping is removed.

Validation: render-cost checks the exact 10,000-marker extent with uniform gaps; timeline-tool-filters, history-navigation and system-messages browser tests and the UI typecheck pass.
2026-10-21 03:28:29 +02:00
Pascal André
da0dc279a7
ci(tauri): stop the refcount negative control at its first crash and warn when a runner never races (#1061)
The Windows runtime handle regression builds a copy with the counter fix reverted and required that negative control to crash within ten processes before the corrected build's five runs. The race is timing-dependent: of the last ten PR builds, five runners reproduced it (1 to 8 crashes in ten) and five never did, so tests-tauri-windows failed about every other pull request, unrelated to the change under test.

The negative control now runs up to fifty processes (about 0.16 s each) and stops at the first native fault. On GitHub Actions, a runner that never reproduces it emits a warning naming its CPU instead of failing; local --baseline runs still fail, so a change to the fix cannot pass on a machine that never races. The corrected build's runs, the accepted fault codes and the clean-teardown check for surviving processes are unchanged; the counter-boundary and vendored-Tao checks guard the fix on every runner. Docs state that corrected runs on a warning machine provide no timing evidence.

Validation: syntax check and two independent reviews; the sample comes from the tests-tauri-windows logs of the ten most recent PR Build Validation runs.
2026-10-21 02:35:20 +02:00
Pascal André
f3c0fe671e
fix(timeline): stop adding an empty agent marker after every tool step (#1058)
fix(timeline): stop adding an empty agent marker after every tool step

In OpenCode V2 each agent step is its own assistant message, and most steps only call tools. The timeline's structural index for history knew an assistant message's tool and reasoning counts but not whether it had text, so the projection drew a tool marker followed by an agent marker for every step. For tool-only and reasoning-only steps that agent marker was empty: no hover preview, and clicking it reached a message showing nothing but its tools, or nothing when tools are hidden in Message content. Resident (loaded) messages already dropped such markers.

The outline index now also counts an assistant message's non-blank text parts that are neither synthetic nor ignored, the same text the hover preview shows. The projection draws an assistant marker only when that count (or the resident segments) shows visible text, so a tool-only step keeps its tool marker alone and a reasoning-only step has none. The count is optional in the contract: an older plugin copy still loaded by the daemon reports no count, and its markers stay as before. Persisted indexes saved without the count display immediately and then revalidate every checkpoint once, like the representative tool names. The unused message-timeline-group-* classes, which no stylesheet or test referenced, are removed; the group spacing that still uses tool membership is kept.

Validation: navigation.test covers text counts for text, tool-only and blank/synthetic/ignored/reasoning-only steps; session-outline-projection covers tool-only, reasoning-only, text, unknown-count, shell and resident cases; session-outline-persistence covers validation and the one-time refresh of indexes without counts; the native navigation script now asserts the counts on real native storage (CI). history-navigation, timeline-tool-filters, system-messages and render-cost browser tests, the session-outline tests and the UI typecheck pass.
2026-10-21 01:49:40 +02:00
Pascal André
f5c5eefefc
chore(opencode): qualify and recommend runtime 2.1.1 (#1055)
Recommend the qualified external OpenCode 2.1.1 runtime while retaining minimum 2.1.0 and the unchanged locked client/plugin build. Align setup/update regression expectations and the recommended-version help tables in all ten locales; preserve authenticated contract recognition and mutation/ownership fences.

Record isolated Windows native runtime, session/account/usage, pruning UI/history, four historical migrations and all eleven recurring Mission journeys. Server units pass 2442 with seven skips, UI units 1635 and focused browser tests 125; final dev Settings/preview-offer deltas and retained-minimum controls are revalidated.

Two review passes found no actionable issue in the PR. Admission/update tests and explicit isolated 1.x-to-2.1.1 replacement pass. The user explicitly authorized administrator merge despite the unrelated Tauri Windows negative-control race test failure, the general test job still running at inspection and the required GitHub review. No stable release is published by this merge.
2026-10-21 01:16:19 +02:00
Pascal André
64ff1a8fc6
fix(settings): show OpenCode version first and drop the Runtime header (#1052)
fix(settings): show OpenCode version first and drop the Runtime header

Settings > OpenCode now starts with the OpenCode version card (status, install/update and service actions), followed by the executable selection and then the log level card. The executable card loses its separate "Runtime" heading and server-setting badge: the binary selector already carries its own "OpenCode binary" heading and description, so the extra row only repeated it. The startup recovery dialog shares the same card and drops the duplicate heading too. The two unused strings are removed from all ten locales and AGENTS.md records the new order.

Validation: the opencode-setup browser suite (33 tests) passes, including the settings layout test now asserting the version card first, the selector second without a header, collapsed disclosures and logs last; a capture confirmed the order. UI typecheck passes.
2026-10-21 00:31:25 +02:00
Pascal André
7d9093de68
feat(timeline): follow Message content tool visibility (#1049)
Timeline tool markers now follow the Message content filters: a marker disappears when the tool type it shows (its icon; the outline's representative tool for indexed history) is hidden there, and the Nothing preset removes every tool marker. As with hidden tools before, the transcript's active marker and markers with an active permission stay visible, so highlighting and reveal keep following the transcript. Collapsed tools keep their marker.

Because the rail now follows those filters, the separate "Show tool calls in the timeline" preference is removed everywhere it surfaced: Settings > General, the View menu item in the renderer, Electron and Tauri hosts, the palette command, its shortcut catalog entry (72 actions, 23 initially unbound) and its strings in all ten locales and the help. Saved values are ignored; the server schema passes unknown keys through, and hosts ignore an extra field from older renderers. The never-updated expandedMessageIds timeline input is dropped. Intended default change: tool markers are visible with the Balanced preset unless their type is hidden in Message content.

Validation: new timeline-tool-filters browser test (hidden, Other-routed and collapsed tools, Nothing/Everything presets, a hidden active marker shown and highlighted); view-menu, a real Electron View menu, hotkeys-settings, transcript-presets, render-cost, history-navigation and other timeline suites (one local-environment failure that also fails on unmodified dev); related unit tests; UI typecheck; rustfmt check of view_menu.rs. Independent review approved after the active-marker fix.
2026-10-20 23:11:08 +02:00
Pascal André
6f8881d5ae
fix(stats): restore cached totals and refresh only while visible (#1041)
## Summary
Replace the renderer-only workspace-ID cache with bounded validated
display snapshots in backend-owned state.ui.serviceUsage. Cache survives
recreated renderer/backend/workspace identity, keyed by authenticated
service/discovery/execution-host/folder fingerprint and period/timezone.
Eight entries, 1 MiB maximum; no plaintext credentials or paths.
Preferences does not write the opener's client-state journal.

Show previous totals/charts immediately with a cached-state label while
revalidating; preserve them on error. Hidden/unmounted Stats starts no
reads, cancels trailing work and fences late results. Visibility resumes
one read; execution hints use the existing activity cooldown instead of
one native scan per descendant. Manual refresh, period, reconnect and
deletion/pruning paths retained.

## Validation
- Rebased on current dev 7a2ee897e, retaining #1037 shared fixture
cache/shutdown and token UI plus #1039 launcher changes.
- Server/UI typechecks pass; cache/scheduler units 8/8; browser
service-usage 5/5, usage-metrics 4/4, status-tokens 4/4, sequentially.
- Real renderer AND backend recreation with a two-second stats delay:
2098 ms cold / 68 ms restored, then fresh totals replace cached values.
- Hidden-start regression: base makes one unwanted read; fixed makes
zero. Tests cover hidden events, visibility, burst scheduling, identity
changes, retention/errors and malformed cache bounds.
- Isolated native fixture with four synthetic sessions passes; repeated
native reads ~15–19 ms. OpenCode still repeats aggregation; no claim to
optimize large-history scans.
- No app/shared daemon/user data touched. Live installed app and
non-Windows native acceptance not tested.

## Oversized touched files
http-server.ts ~2446 lines; api-client.ts ~817; 10 locale settings.ts
files ~735–749. No unrelated refactor.
2026-10-20 19:49:45 +02:00
Pascal André
6a6367b560
chore(opencode): retire adaptations for runtimes and events OpenCode V2 no longer produces (#1034)
## Summary
This PR removes OpenCode compatibility code that nothing can reach any
more:
- Code that was already dead at the former 2.0.27 minimum, and is still
dead at the current 2.1.0 minimum (#1029).
- Handlers for events and data shapes that OpenCode V2 never produces.

Each item was checked against the upstream 2.0.27 and 2.1.0 sources
before removal.

Kept on purpose:
- Workarounds for upstream bugs or limits that are still present in
2.1.0.
- Code that reads conversations written by older runtimes: V1-migrated
tool names and metadata, `workspaceID` data, task summary titles,
`session-legacy` replay.
- Code that recognises and upgrades old CLIs and live older daemons: 1.x
replacement, beta recognition, 2.0.11 service discovery, 2.0.15 native
upgrade, `/api/status` and `/api/health` identification.

## Removed or changed
**Server**
- Negotiation: one "not modern → `canonical_api`" rule replaces the
pre-2.0.4 legacy schema recognition, and runs before #1029's
`native_contract` check.
- The transport no longer rewrites `/api/status`.
- Desktop skills no longer send the pre-2.0.4 `location` and `slash`
fields.
- The legacy `auth.json` usage fallback for daemons without
`credential.list` (pre-2.0.20) is removed, along with its UI reason and
the i18n key in 10 locales. A missing endpoint still fails safely.
- Workspace selectors are rejected up front, without a native resolution
request.
- CLI service probes try `/api/info` first. An admitted daemon no longer
costs two authenticated 404s on every check.

**UI**
- Remaining V1 `session.idle`/`session.status` branches: Missions store,
pending discovery (UI and server), `eventAffectsMessages`.
- `server.instance.disposed` and the now-unused `rehydrateInstance`.
- The V1 `worktree.ready` envelope (the handler now reads the V2 shape).
- V1 tool-state titles and the `success` status alias.
- Part `time.start` and `time.end`.
- Text stored as objects or segment arrays.
- `deriveMessageStatus`.
- The V1 `error.data.message` shape.
- `projectID` and `location` fallbacks on `session.created`.
- The replay-only `session.message.content.updated`.
- The unreachable `output.answers` fallback.

**Fixtures, Missions and docs**
- Legacy branches removed from the location, fork, families, pruning and
automation native fixtures. The WSL setup fixture now compares against
the reported version instead of 2.0.11.
- Unused 2.0.21 launch helpers removed.
- The Missions native and trajectory fixtures also run on the minimum
lane.
- The capability proof checks the reported version.
- Stale 2.0.2x labels now say "as of the 2.1.0 minimum".

**Follow-ups from the #1029 post-merge review**
- Test fixtures no longer send the ignored `recent: ""` compaction field
(18 sites).
- The manual compaction-isolation fixture accepts any published version
at or above the minimum.
- `CONTRIBUTING.md` wording updated.

114 files, +319 / −1117: source +90 / −487, tests/fixtures/CI +214 /
−604, i18n −10, docs +15 / −16.

## Validation (rebased on dev with #1029)
**Typechecks:** server and UI pass.

**Server tests**
- Touched areas: 480/480.
- Full server CI step: 2430 pass, 0 fail, 7 skipped, 1 cancelled. The
cancelled test is the `index.test.ts` real-process shutdown test; it hit
its 5 s timeout under load. It passes alone (4/4), and that file is
unchanged versus dev.

**UI unit tests**
- Touched files: 230/230.
- CI steps: 574/574, 200/200 and 519/519.

**Browser suites**, one at a time:
- Provider and service usage: provider-usage 13/13, service-usage 3/3.
- Missions: mission-visibility 3/3, mission-control-feedback 5/5,
mission-lifecycle-revision 10/10, mission-lifecycle-navigation 10/10,
mission-current-passage 1/1, mission-native-family 11/11.
- Transcript and rendering: compaction-responsiveness 5/5,
session-rendering 47/47, reasoning-collapse 3/3, render-cost 17/17,
tool-images 4/4, system-messages 10/10.
- Other: search-highlight 11 pass (1 skipped by its env gate),
panel-extensions 13/13.

**Native fixtures on an isolated 2.1.0 CLI:** location, session-fork,
automation, session-pruning, missions native, missions trajectory,
provider-usage, Missions capability proof and compaction-isolation all
pass. The WSL setup fixture was not run (it needs a WSL distro with a
Linux CLI).

## Left for later
- `RuntimeIdentity.discovery` is now write-only.
- `ownsLocation` has an unused `_client` parameter (about 30 callers).
- A test-only V1 translation in `auto-accept-manager.test.ts`.
- Out of scope: HTML-entity decoding (a product decision), the
`MutableHashMap` loop, the pruning-UI import-order workaround, unused
log-based Missions readers, and the unshipped managed-owner module with
its 2.0.24-pinned test.

Oversized source files touched: `stores/instances.ts` ~2,209,
`message-block.tsx` ~1,601, `workspaces/manager.ts` ~1,104,
`tool-call.css` ~1,047, `tool-call.tsx` ~865, `opencode-data.ts` ~865,
`api-types.ts` ~784, `session-events.ts` ~606,
`recurring-day-fixture.ts` ~535.
2026-10-20 19:19:47 +02:00
Pascal André
d6a890fba7
chore(opencode): qualify 2.1.0 and adopt its native contracts (#1029)
Adopt the OpenCode 2.1 native replay and compaction contracts, align client/plugin pins and set minimum and recommended runtime to 2.1.0. Preserve authenticated contract recognition for future/custom runtimes and explicit installation/restart boundaries.

Preserve historical conversations through official projection-based pruning revisions and independently checked migrations from 2.0.3, beta-19271 and 2.0.32. Retain ownership, execution claims, stale-selection checks and transaction fences. Update setup, help and qualification evidence.

Isolated Windows native qualification and integration checks cover pruning UI, historical reimport, Compare/Wayfinder, recurring Missions, builds and typechecks. Document broad-suite limitations and outstanding platform/package/mobile acceptance; this merge does not publish CodeNomad 1.0.0.
2026-10-20 16:46:38 +02:00
Pascal André
73c0621d0d
fix(sessions): wire finish, retry and usage signals to OpenCode V2 events (#1033)
## What changes for the user
Four things that silently stopped working with OpenCode V2 work again:

- **"A session finished" notifications** arrive again. A successful or
failed run notifies. A run you stopped yourself does not.
- **Provider retries** show the retrying status and the countdown toast
again.
- **Usage statistics** refresh after each run.
- **The token usage line** under assistant replies (preference,
transcript filter row and hotkey) shows each settled reply's tokens and
cost again.

Per-session transcript reducers are also released again once a run has
settled.

## Why
OpenCode V2 declares `session.idle` and `session.status` in its schema,
but core never publishes them. Runs end with
`session.execution.{succeeded,failed,interrupted}`, and retries arrive
as `session.retry.scheduled`. These features were still keyed only to
the V1 events.

The token usage line had a second problem: it was drawn from V1
`step-finish` parts, which V2 never produces. V2 stores tokens and cost
on each assistant message instead.

A new isolated native fixture,
`scripts/test-session-lifecycle-native.mjs`, confirms this on CLI 2.1.0.
It uses its own storage, port and fake provider, and never the shared
service. In its complete, retry, fail and interrupt scenarios, it sees
no `session.idle` or `session.status` event. Upstream 2.0.27 sources
show the same: these events are declared but never published.

## How
- **Finish notification:** sent from the V2 terminal path after
`session.active` confirms the run ended.
  - Succeeded and failed runs notify.
- User and shutdown interrupts stay silent; idle-timeout interrupts
notify.
  - Subsessions and unknown sessions stay silent.
  - A stray `session.idle` cannot cause a duplicate.
- **Retry:** `session.retry.scheduled` sets the retry state and the
transient toast. The next attempt or the end of the run clears them.
- **Run-end detection:** a shared `lib/native-execution-events.ts`
drives the usage refresh and reducer retirement.
- **Reducer retirement:** it also waits for queued prompts, so prompts
left in the inbox after an interrupt stay listed.
- **Usage line:** moves to `components/message-usage.tsx` with the
existing classes.
- **Removed:**
  - the dead V1 `StepCard`, its step-part types and CSS;
  - the V1 `session.idle`/`session.status` handlers.
- **AGENTS.md:** one bullet so future features wire to V2 lifecycle
events.

Left for the follow-up cleanup PR: the remaining dead `session.idle`
fallbacks in pending discovery, Missions triggers and test fixtures.

## Validation
New tests that fail on unmodified dev and pass here:

| Suite | dev | this PR |
|---|---|---|
| lifecycle unit | 3/4 fail | 4/4 |
| retirement | 8/9 fail | 9/9 |
| browser `usage-metrics` | 3/3 fail | 3/3 |
| browser `service-usage` | 2/3 fail | 3/3 |

- UI typecheck is clean.
- **Passing unit suites:**
- opencode-data-idle 9, runtime-contract 4, session-native-events 8,
message-part-grouping 6
- opencode-transcript-replay 17, opencode-compaction 11,
session-pruning-projection 2, session-pruning-pagination 6
- session-request-authority 77, opencode-data 30, message-timeline-v2 2,
transcript-visibility 8
  - missions 1, instances-pending-compaction 7
- **Passing browser suite:** `system-messages` 10/10.
- The native lifecycle capture passes.

Oversized files touched:

| File | Lines |
|---|---|
| `message-block.tsx` | ~1,760 (down ~240) |
| `message-section.tsx` | ~1,630 |
| `opencode-data.ts` | ~905 |
| `session-events.ts` | ~670 |
| `message-timeline.tsx` | ~645 |
| `message-base.css` | ~675 |
| `session-request-authority.test.ts` | ~2,400 |
| `opencode-data.test.ts` | ~1,215 |
2026-10-20 15:51:48 +02:00
Pascal André
debbdce33f
feat(missions): add a "Compare two models" Mission template (#1026)
## Summary

Adds a fifth Mission template, **Compare two models** (`compare`). It
runs the same bounded task on two different models and ends with a blind
verdict.

- **Creation:** the user picks **model A** and **model B** in the main
creation form. Both are required and must name different models
(provider + model id; a reasoning variant alone is not a different
model). This is enforced on every creation path: HTTP, prepare,
recurrence create and agent-started missions.
- **Roles:** `model-a`, `model-b` and `comparator`.
  - A and B share one instruction text and report contract.
- The coordinator writes the task, inputs and criteria once, and gives
both byte-identical briefs.
- The server refuses a run task whose brief differs from its sibling's,
or whose per-task execution would change its role's model.
- **Blind judge, always on:** the comparator works on neutral X/Y
labels.
- It reports one versioned `comparison` artifact: the A/B task
references, per-criterion scores for both runs, sourced observations,
and a verdict `a | b | equivalent | inconclusive`.
  - Completion requires both runs and that comparison.
- **Judge model:** frozen once at creation, like every other role.
- An explicit judge is kept; otherwise the judge copies the
coordinator's effective model. When that model is unknown, both stay on
the native default.
  - Replays and retries therefore reuse the stored profile.
  - The editor warns when the judge's model equals A or B.
- **Readers:** the verdict, both model names, a score table and evidence
by path. Hashes stay under Technical details, for current and archived
recurring passages.

The template came out of evaluating Harness2's comparison loop. It
deliberately has **no** lesson, human approval, budget or duration
handling, and leaves the Wayfinder human gate unchanged. Up to five
models, per-run worktrees and launching from the composer's model picker
(OpenChamber-style multi-run) are left for later.

`AGENTS.md` records the single editor exception: the A/B selectors sit
outside the collapsed Options disclosure. All ten locales are updated.

## Size and upkeep

62 files, +837/-96:

| Part | Lines |
|---|---|
| Product code (about 16 files are one-line template-list hooks) |
+280/-57 |
| Locales (13 keys × 10) | +131 |
| Tests | +409 |
| Docs | +17 |

`missions/control.ts` stays oversized (~1,500 lines).

## Validation

- Full server suite: 2,426 tests pass, 7 existing skips. Root and server
typechecks, the server build and the plugin build pass.
- UI: 45 unit tests and 36 browser tests pass.
- The native trajectory fixture, extended with a compare case, passes on
OpenCode 2.0.31. It checks two distinct models, a one-newline brief
difference refused with the journal unchanged, a real blind judgment and
a completed mission. The native contract and recurring Q fixtures also
pass.
- Regressions cover:
  - distinct-model refusals on every creation path;
  - brief and override refusals;
  - artifact validation and the completion policy;
- judge freezing at creation, revision replay, and retry after a
coordinator model change;
  - editor inheritance and readable evidence.
- Independent review rounds ended with no P0-P3 findings.
- The editor and reader were checked from screenshots of the real Solid
components in the browser harness, not in a packaged app.
2026-10-20 13:55:12 +02:00
Pascal André
ee035ad272
fix(files): refresh Workspace with scoped, rate-limited invalidations (#1030)
Fix Workspace listings that stayed stale until manual refresh or F5. Reproduced
the issue in the attached Tauri application: a host-created file was absent
until the panel's Refresh button reread it. OpenCode filesystem events were
the only invalidation source, not a subscription to every browsed directory.

Observe authorized listed directories on the host through bounded,
non-recursive native watches, registered before listing. Preserve physical
containment, inode and workspace-lifetime checks and close watches on removal
or shutdown. Forward actual directory observations through the existing SSE
event bus, without inventing native OpenCode events or adding polling,
recursive scans or an independent service.

Coalesce passive cache invalidations before readers wake: a short isolated-
change delay, then at most one publication every five seconds during sustained
activity, as requested. Only pending observations create a timer. Explicit
mutation/recovery invalidations remain immediate. Scope tree versions by
browsing root and listed directory, so only stale visible directories reread;
other worktrees, unchanged siblings and hidden/collapsed folders do not scan.
Retain existing read budgets, cache-first rows and coalesced in-flight demand.

Keep logical listing aliases separate from physical watch targets, so every
internal junction/symlink subscriber receives its own cache invalidation.
Directory listings include an opaque device/inode identity: when a parent
detects removal or replacement, discard only that child's cached subtree,
cancel its old reads and reload still-expanded paths to bind new observations.
Ordinary mtime changes never force a subtree scan.

Validation covers real filesystem create/edit/rename/unlink, ownership,
replacement, aliases, watcher budgets/disposal, continuous cache bursts and
immediate explicit mutations. A real Chromium regression exercises the whole
filesystem-to-watch-to-SSE-to-Workspace path without F5, including parent-only
replacement and multiple junction aliases. Existing Files/Git browser checks,
the full server suite and targeted UI checks pass; typecheck diagnostics remain
at baseline. No installed application or shared OpenCode daemon was replaced
or restarted.

Final server validation: 2,415 tests pass, seven existing skips. Two independent
gatekeeper rounds; the final review reports zero actionable P0-P3 findings at
728bf59861d063863c6bb3351d5ce0a030ce6866. The hosted CI is still running at merge.

Closes #1027.
2026-10-20 00:17:40 +02:00
Pascal André
362efb5911
test(missions): fold the one-time decision journey into the trajectory fixture (#1024)
## Summary

#1023 fixed one-time Wayfinder decisions with about 85 lines of product
code but 410 lines of tests. Its 248-line native script re-implemented
the daemon, bridge and workspace setup that
`test-missions-native-trajectory.mjs` already has.
`native-invocation.test.ts` also carried overlapping cases.

- The decision journey is now one case of the trajectory fixture,
reusing its private daemon, shipped plugin, bridge, workspace and real
subagents. The case covers:
  - refusal before an answer, with no journal mutation;
  - a native child question answered through the authenticated UI route;
  - completion by discovery without a supplied artifact.
- `scripts/test-missions-wayfinder-decision-native.mjs` is deleted.
- `native-invocation.test.ts` (162 to 90 lines) keeps one test per
distinct behaviour:
  - concurrent isolation and stale invocations;
  - exact `Exit.fail`/`Exit.die` preservation;
- interruption joining a raw read that ignores abort, through the real
`acquireNativeHumanAnswers` adapter;
- the per-check deadline between two successful checks and after an
expired one.
- No product code changes. Net -260 lines.

## Validation

- With the pre-#1023 `managed-owner-plugin.ts` substituted in memory,
the trajectory case fails after the UI reply with `Native human answer
graph unavailable`. Restored, all 9 checks pass on OpenCode 2.0.31.
- Detaching reads from the runner, or converting errors to defects,
makes the unit tests fail.
- The full server suite passes (2,410 tests, 7 existing skips), as do
the server typecheck, the plugin build and bundle test, and the native
contract fixture.
- An independent review ended with no P0-P3 findings.
2026-10-19 16:15:50 +02:00
Pascal André
c42cfe740a
fix(missions): run the one-time human gate in the invoking native graph (#1023)
## Summary

A one-time Wayfinder mission could never complete a decision task. The
decision child asks its native question, the human answers it in
CodeNomad (authenticated UI reply, durable mark confirmed `via: "ui"`),
and the coordinator's decision report is still refused with `Native
human answer graph unavailable`.

The one-time human gate is built in `managed-owner-plugin.ts` from the
plugin setup graph. That graph lacks the Location, Form and Database
services that `acquireNativeHumanAnswers` requires. Recurring passages
build their own gate in the Job's Location graph
(`native-recurrence-due.ts`), so they were not affected. This is why the
recurring Wayfinder journey (Q) kept passing while the one-time path was
broken.

## Fix

- `native-invocation.ts` captures each invoking native graph with
`AsyncLocalStorage` around the plugin's tool executes and forwarded
Missions RPC handlers. The one-time gate runs its reads in that graph.
- Concurrent invocations stay isolated. A missing or finished invocation
is refused, never served by another invocation's graph or the setup
graph.
- Each complete human check gets its own 10 s budget, the bound
`native-human-evidence.ts` already uses. Its nested SQL/Form/storage
reads share that budget, which is not reset per query.
- Interruption or expiry aborts those reads. The borrowed graph is
released only after every started read has settled. A truly
non-interruptible native finalizer must still finish first; this is
documented.
- Expiry surfaces as an ordinary refusal.
- Recurring runners are unchanged.

## Validation

- New native fixture
`scripts/test-missions-wayfinder-decision-native.mjs`. It drives the
shipped plugin on a private daemon with a synthetic provider: a real
child asks a native question, and the reply goes through the
authenticated UI route.
- Before the answer, the decision report is refused with no journal
mutation.
- After it, the decision completes by discovery, without a supplied
artifact.
- On unmodified `dev` it fails with the error above; with this change it
passes (OpenCode 2.0.31).
- Unit regressions in `native-invocation.test.ts` use the real
`acquireNativeHumanAnswers`. They cover invocation isolation, missing or
stale invocations, Effect errors, interrupted and blocked reads, and
successive checks separated by slow work. Each new regression fails on
the earlier code.
- The full server suite passes (2,413 tests, 7 existing skips), as do
typechecks, the plugin build and bundle test, and the native
`test-missions-native`, contract, trajectory, family-control and
recurring Q fixtures. After rebasing onto #1022 (UI preferences only),
the server typecheck and the 34 targeted human-answer tests pass again.
- An independent review loop ended with no P0-P3 findings.
2026-10-19 14:52:29 +02:00
Pascal André
e32d9331f4
refactor(automation): unify native browser previews on the shared page engine (#1020)
Unify application-window and session-preview automation on the same server-side
page engine. BrowserPage serializes per-session operations, binds refs to the
exact native guest/document generation, fences cancelled and late results and
always attempts exact lease cleanup. Both tools share page-action validation
and budgeted, scoped plain-text outlines, without arbitrary model evaluation.

Replace Electron and Windows Tauri snapshot/ref/click/type implementations
with the private browser.page.begin/send/check/end channel. Native hosts keep
guest ownership, visibility, URL policy, storage, history, emulation, navigation
and screenshots. Bounded operation leases and fixed protocol allowlists check
identity before native effects and after IO; hidden, ambiguous, replaced,
navigated or expired targets refuse rather than selecting another guest.
Cleanup never detaches another owner's persistent emulation overrides. No
renderer IPC, guest capabilities or legacy page-action fallback is added.

Electron uses WebContents.insertText and sendInputEvent for embedded guest
text and keyboard input: direct CDP input can report success while silently
dropping it before the first pointer click. Preserve key modifiers and text
events without showing or raising a window. ContextMenu is explicitly refused
with Shift+F10 guidance rather than emitted as an unknown native key.

Validation includes native ownership/expiry/deadline/concurrency regressions,
20 Rust browser-controller checks and cargo check --tests, 24 Electron
controller tests, full server validation and isolated OpenCode 2.0.31 plugin
and bundle checks. The real hidden-window Electron integration drives both
production ends through replacing type, select, printable keys, Ctrl+A and
Backspace, Tab, Enter, pointer clicks, waits, scoped snapshots, hidden targets
and stale refs after navigation. Three independent gatekeeper rounds reached
zero actionable P0-P3 findings; subsequent parent-only rebases preserve the
identical reviewed patch.

At merge, the CI tests job failed only an unchanged client-state process
timing assertion (661 ms against a 500 ms ceiling). Its isolated regression
passes, including the affected live-registration case in about 47 ms; native
platform and compatibility jobs passed. No installed app or shared OpenCode
daemon was changed or restarted.

Refs #1008.
2026-10-19 09:27:58 +02:00
Pascal André
b2d5b722ec
feat(automation): complete safe native window interactions (#1018)
Complete application-window interaction on the shared page engine introduced
by #1015, while preserving native window/run and document-scoped ref authority.

Add real pointer clicks, hover, stepped pointer dragging, wheel scrolling,
keyboard chords and bounded text waits. Flat tool commands are validated at
both bridge boundaries. Clicks refuse covered targets; press(ref) refuses a
target that cannot acquire focus, so Enter never reaches an unrelated draft.
Drag requires both ends visible instead of scrolling under a measured source.
Preferences remains outside automation; focusing Preferences or a remote
window falls back to the most recently used local application window.

Trusted native input carries monotonic provenance intervals. Human Form
decisions check the originating event timestamp, not the time a stalled
renderer handles it; delayed keys and their implicit submit clicks remain
automated after lease expiry. Fixed page functions keep their depth marker.
Native events use the page's epoch clock, and cleanup preserves the original
press timestamp, independently of configured command timeouts.
Failed sequences best-effort release held buttons and keys, including when
the cleanup mark write itself times out. An admitted input timeout is reported
as an unknown outcome with inspection-before-retry guidance: the renderer may
deliver the input or finish the click later. Never replay these inputs.

Validation covers the actual Mission editor and InterruptionDock in isolated
Chromium, including a 16-second renderer freeze and a control human click.
Targeted server tests cover key mapping, both bridge parsers, focus/target
fences, failure cleanup and late input outcomes. Full server validation,
isolated OpenCode 2.0.31 automation, bundle regression and Tauri compilation
also pass; typecheck diagnostics remain at baseline. No installed application
or shared daemon was changed or restarted.

Four independent gatekeeper passes; the final review reports zero actionable
P0-P3 findings at 0ffb5a53b425500336854902c7068cd2233695f4.

Refs #1008.
2026-10-19 08:48:14 +02:00
Pascal André
1932da9307
feat(automation): shared page engine with budgeted outlines, select and replacing type (#1015)
codenomad.inspect/act could not fill real forms (#1008): type appended to a
prefilled field, native <select> could not be changed, and inspections were a
flat JSON list silently cut after 750 nodes.

Page automation now lives in a transport-agnostic engine under
packages/server/src/automation/page/, used by DeveloperCdp for the CodeNomad
window and designed to carry the browser preview next:

- snapshot.ts renders the accessibility tree as an indented plain-text outline
  with refs. Wrappers, unnamed images and text repeating a name computed from
  contents are dropped; fields show their value (numeric ones included) instead
  of inner nodes; native selects list options with selected/disabled. A
  32 000-character budget is shared among siblings: small parts stay whole,
  large ones collapse in place with "(+N hidden)" or keep their first and last
  items around "(+N more)". inspect accepts ref (one subtree) and query (exact
  role, or text in name/value/description, with contents and ancestors).
- refs.ts names a DOM node per target, run and navigation epoch; a ref keeps
  its name and stays usable for the whole page generation.
- actions.ts acts through fixed page functions whose inputs travel as protocol
  arguments. type refuses disabled fields and fields that cannot take focus
  (closed disclosure, modal dialog), so text never lands in another field; it
  replaces the field's text by default, clear: false appends (email/number
  through their value). Date/time/number/range/color values are validated
  before anything changes and keep the browser's normalization. select picks
  options by label, else value, dispatching input/change only on a change;
  clicking an option ref picks that exact option. Results return the field
  value or chosen labels; page exceptions are reported.

The trust model is unchanged and documented: in the CodeNomad window page
functions run under __codenomadAutomationDepth, clicks stay programmatic and
native text insertion yields only input events, so automation can never
produce the trusted dock interaction a human Form answer requires.

Validation: snapshot unit tests (exact outlines, query, scope, fair collapse,
budget property), updated controller/plugin/route tests, and a real-Chromium
regression driving DeveloperCdp over the DevTools endpoint, including the real
Mission editor (title replaced, chosen playbook reaches the created mission)
and every reviewed edge case. Full server suite green apart from a load-only
5 s timeout in index.test.ts that passes alone; isolated native automation
fixture passes on OpenCode 2.0.31. Three gatekeeper rounds; the last found no
P0-P3 issue.

Closes #1008.
2026-10-19 07:38:34 +02:00
Pascal André
8cecb08704
feat(settings): compact Settings, unified notification routing and autosaving Missions preferences (#1014)
Settings had grown long captions, inconsistent groups and two separate
notification circuits, and Missions preferences used an explicit draft with
Save/Reset/Reload that no other section needed. This pass makes the screen
shorter and the behaviour uniform without changing what is stored.

Compact layout. Rows keep a title; secondary explanations move behind a
focusable "i" tip (settings-info-tip.tsx). Folded parts share one quiet
disclosure style (settings-disclosure.css). In-group row lines use a new
--border-row token, fainter than the --border-divider between groups.
Providers splits Models and Web search into folded cards; Web search starts
closed and mounts nothing until opened, so its metadata is read on demand.
The separator between them spans the content pane. A global
`details > :not(summary)` box-sizing rule fixes folded content inheriting the
details slot's content-box, which made full-width selects overflow. Unused
messages are removed in all ten locales.

Notifications. Every showToastNotification declares a category, and
Settings > Notifications routes each category to On screen / History /
System from one table (lib/notification-routing.ts). Errors always reach the
screen and History so a rejection is never lost; transient progress messages
stay on screen only. Session needs-input and completion use the same routing.
Saved legacy choices (OS notifications off, needs-input/idle toggles) seed the
table until it is first saved. The tab-bar bell now always opens History.

Keyboard shortcuts. The catalog lists the 24 initially unbound palette
commands too (73 actions), reusing their translated names and keeping existing
aliases as one row.

Missions preferences. Each choice is written at once, compare-and-swapped
against the value stored at its turn. Edits made during a write stay enabled
(disabling a focused control drops keyboard focus) and queue behind it. A
rejected or unconfirmed write is never resent: later queued edits built on it
are dropped and a read-only reload shows the stored values. Reload appears only
when that read fails or the document is unreadable. Agent/model choices now use
one Agent/Model/Reasoning row per role in every scope and at creation, with no
second selector level, sub-fold or Debugging special case. In a mission type,
rows show the inherited global values with a "Global" mark. Editing a field
customizes the role from those values, and a revert icon inherits again.
Inherit, explicit native default and a selected profile stay distinct. Reset
(confirmed) lives in that fold and clears only the shown scope's agents and
models. The project depth saves on Enter or blur; invalid values are flagged
and never written. Reads and writes make the field read-only so focus stays,
and refreshing is offered only after an error or an unconfirmed write. The
depth group is titled "Subsession nesting depth" and shows "OpenCode effective
depth"; scope badges are gone.

Validation: typecheck; unit tests for notification routing, mission i18n
alignment and profile summaries; browser suites mission-defaults-models (new
autosave, conflict, queue, reset and inherited-row tests), mission-control,
mission-editor-profiles, mission-editor-lifetime, mission-create-*,
mission-panel-*, websearch-settings, preferences-startup(-state),
plugin-controls, project-tab-close, new-instance-launcher, tab-chrome,
appearance, git-history and hotkeys-settings.

Review fixes.

Missions defaults: a queued edit now touches one role, or one scope's
task-session policy, and is refused when that stored value is no longer the
one it was made from. A choice made while a rejected write was being reread
could otherwise copy the whole scope from a stale view, resend the rejected
choice and erase another window's change. Independent edits still apply to the
value stored at their turn. The Settings rows report one role at a time
(MissionProfileControls.onRoleChange); creation keeps its whole-request change.

Project depth: pressing Inherit keeps focus in the field, so leaving the field
for the button no longer saves the typed value first and loses the click; it
stays focusable while a read or write runs. A write acknowledged after the
section was left is accepted and reread on the next activation instead of
being shown as unconfirmed. Escape discards an unsaved value without writing.

Notifications: the routing table builds a quick second click on the pending
table instead of reverting the first. Routing is read untracked so effects that
notify do not rerun on every settings change. The link-open error goes through
routing. Web notifications without permission are skipped silently. Wording
that still mentioned a global enable switch now names the System column. A
never-saved former system switch deliberately takes the new defaults
(product decision); a unit test records it.

Settings: info tips toggle on tap for touch screens (Kobalte tooltips ignore
touch hover and close on click, and a tap first focuses the trigger, so the
target state is decided at press time). Settings switches expose role=switch.
The palette strip remeasures when it mounts late. AGENTS.md now records the
measured palette layout, the Settings switch round exception and the shared
OpenCode disclosure styles; dev-docs/MISSIONS.md the autosave semantics.
Removed dead CSS and the unused MissionTaskModeControls children prop; the depth
input uses the field surface and its info tip sits outside the heading.

Validation: typecheck; unit tests for routing, profile role edits and mission
i18n; browser suites mission-defaults-models (new queued-role and touch tip
tests, updated rejection test), mission-control (Inherit after typing, write
acknowledged after leaving, Escape), mission-editor-profiles, appearance,
hotkeys-settings, mission-settings-brief, notification-icon-badge,
plugin-controls, preferences-startup(-state), preferences-window,
provider-accounts, git-history, remote-access/control-settings,
websearch-settings.
2026-10-19 01:23:35 +02:00
Pascal André
b4f1dde4f8
feat(missions): coordinator hand-offs, task interrupts and native decision evidence (#1013)
Completes the coordinator frictions of #1009 left after #1011 (points 4 to 9)
and repairs the recurring Wayfinder human-mark journey checks.

mission.interrupt({ taskKey }) stops a retired native task's conversations
through the authenticated bridge: the coordinator's direct children whose first
user message carries that task's canonical assignment header. Each family is
interrupted deepest first, then the conversation, with lifecycle Stop's
post-interrupt recheck, settle and quiet verification; queued inputs are
cancelled under the worktree-deletion fence. It refuses while any open task
lives in that conversation's family (reuse or nesting chains, or its actor),
and never touches the coordinator, other tasks or the plan.

mission.prepare creates a paused one-time hand-off through the editor's
prepareMissionCreation pipeline, holds and fences, and never starts it. A
trusted preparedBy gives the preparing coordinator read-only visibility
(inspect({ missionID }), inspect({ list: true })), never control. Retries read
the journal's creation event first and are reported uncertain, never as
nothing created.

mission.inspect({ taskKey }) adds the task's observed native conversations as
advisory evidence; the task-key parser is shared with the UI. A native
Wayfinder decision readout may omit its artifact: the gate's discover() derives
candidates from exact native structure only and verifies each; only a complete
discovery with exactly one candidate fills it. requestID "auto" briefings may
let the server pick the revision. One replacement may merge several retired
tasks, and the task reader lists every predecessor. Scheduled passages, which
have no backend route, are not offered the new tools.

The Q and N journeys queried a retired human-marks key pattern; they now read
HUMAN_MARK_STORAGE_PREFIX, and the Q UI run exercises discovery natively.

Validation: the full server suite passes (2,383 tests, 7 existing skips).
Isolated native fixtures pass against a private OpenCode 2.0.31:
test-missions-native, trajectory, contract, family-control, and recurring
journeys ABCD and QN. The task-reader browser suite and bundle regression
pass; typechecks show only existing diagnostics. Three gatekeeper rounds; the
last found no P0-P3 issue.

Closes #1009.
2026-10-19 00:34:27 +02:00
Pascal André
1173beb37c
fix(missions): compact coordinator tool results and state the decision rule (#1011)
Coordinators driving a real Wayfinder mission hit three costly frictions
(#1009): every mission tool mutation echoed the whole map (about 50 KB after
ten tasks), the canonical assignment prompt only existed inside that
response, and the decision role read as a self-contained analysis although
completing it requires the human's own answer to a native question.

mission.delegate, revise, report and briefing now return a compact readout
(missions/tool-result.ts): disposition, current revision, status, run state,
frontier and the affected tasks, plus the contract and exact assignment
prompt for a declared native task. Briefs, reports and the objective stay
out; mission.inspect keeps the full map, and mission.inspect({ taskKey })
rereads one task with its contract and current prompt. The returned
revision feeds expectedRevision and basedOnRevision directly. Reports name
the task the control actually resolved, including an independent actor's
omitted taskKey.

The native Wayfinder decision role, the coordinator contract and native
decision declarations state the rule verifyHumanDecision enforces: the
task's own conversation asks through the native question tool and the user
answers in CodeNomad; self-contained analysis belongs to research.
Independent-mode coordinators, which that gate does not check, do not get
it. The refusal shows the exact provenance shape the gate validates, kept
aligned with nativeDecisionProvenance by a test. Refusals for a withdrawn
readout and unsettled withdrawn work explain what to do, a briefing citing
missing or retired tasks names them, and the Missions skill matches.

No journal, gate, ownership, RPC or UI contract changes. Interrupting a
retired task's native child, multi-task replacement, a paused-mission
creation tool, native execution visibility and Form IDs remain open.

Validation: 1,104 Missions server tests pass (one existing skip), with new
compact-result, task reread, retired readout, briefing source, implicit
report key, refusal shape and decision-rule tests. The isolated native
Missions, trajectory and contract fixtures pass against OpenCode 2.0.31.
Server typechecks show only the existing diagnostics. Two gatekeeper
rounds; the second found no P0-P3 issue.

Refs #1009.
2026-10-18 22:28:39 +02:00
Pascal André
d840271d6b
fix(files): keep large Changes lists responsive across refreshes (#1006)
## What changes for the user
Files → Changes stays responsive on checkouts with thousands of changes.

- **Long lists show 200 files per section.** The rest is behind a "N
files not shown · Show 200 more" line, which moves focus to the first
newly shown file. Section counts stay the full totals.
- **Selected files stay reachable.** The current, previewed and
bulk-selected files always appear, right after the first 200 if needed.
- **Refreshes no longer rebuild the list.** When a filesystem change
leaves the status unchanged, nothing is redrawn. When a file changes,
only its row updates.

## Why
On the installed build, a checkout with 2,680 git changes froze the app
while Files was open: clicks took about a minute and WebView2 reached
4–8 GB.

CDP showed:
- about 62k of the page's 66k DOM nodes under the Changes list;
- 38% of a CPU profile in DOM `insertBefore`/`remove`.

Every native `filesystem.changed` refresh produced fresh objects, so the
keyed `<For>` recreated every row. Each row also scanned the selection
twice. The body stays mounted while hidden in Workspace/Commits, so the
cost applied there too.

## How
- `reuseUnchanged()` in `git-changes-model.ts`:
  - an identical status keeps the previous array, so nothing recomputes;
  - a changed status reuses the objects of unchanged records.
- The new `git-change-window.ts` holds the 200-row window and its reveal
state. The reveal survives refreshes and resets on worktree change.
- `createSelector` gives each row its selection state without a per-row
scan. Shift-ranges follow the rendered rows.
- The unused `entry` back-reference on `GitChangeListItem` is removed,
since it would go stale once items are reused.

## Measurements
Real `FilesPanel` fixture with 2,770 rows (700 staged, 2,070 unstaged),
Chromium. These are dev builds, so absolute times are pessimistic.

| | Before | After |
|---|---|---|
| DOM nodes after opening Changes | 63,197 | 9,216 |
| Refresh with nothing changed | 63,010 nodes replaced, 32.7 s blocking
| 0 nodes, ~3 ms |
| Refresh with one file changed | 63,010 nodes replaced | that row only
(23 nodes), ~10 ms |
| Selecting a row | 2,777 attribute changes | 4 attribute changes, plus
the auto-selected row leaving |

## Validation
- **UI typecheck:** clean.
- **Unit tests:** 23/23 (model, row window, hook, files-panel state).
- **Browser:**
- new `git-changes-scale` 3/3: row cap, zero mutations on an identical
refresh, single-row update, Show more focus and persistence, worktree
reset, cross-section ranges, stage button, preview past the limit,
drag-and-drop;
  - `git-history` 20/20;
  - `git-init` 3/3.

Oversized files touched:
- `useGitChanges.ts`, ~580 lines;
- `git-history.test.ts`, 980 lines.
2026-10-18 15:33:54 +02:00
Pascal André
87639f0275
fix(transcript): reveal display-truncated text in place instead of a copy-only suffix (#1000)
Some checks failed
Publish help / build (push) Has been cancelled
Publish help / deploy (push) Has been cancelled
## Summary
#890 replaced the copy-only footer for **omitted message parts**. A
second display bound from #648 remained: every rendered text block was
cut at 10,000 characters with "[Output truncated for rendering; copy to
access the full output]". Long subagent reports, assistant prose and
tool outputs could only be read by copying, even though the model
received everything.

**Now:** a truncated block shows its prefix, then at the exact cut a
localized line, "N more characters hidden to keep this message
responsive", with **Show all**. Above a ¼ MB remainder the button reads
**Show 250,000 more**. It reveals the rest in place.
- **Reveal state** sits next to #890's part reveals
(`record-display-cache`), keyed by instance/message/part/section. It
survives virtualization and session switches and resets with the
instance.
- **Reading position** is held while the text grows. Tool outputs grow
inside their own pane, and focus moves to the revealed text.
- **Copy, search and speech** keep reading native content.

## Bounds (from measured Chromium render cost)
| Content | Shown before reveal |
|---|---|
| Prose: text parts, compaction summaries, subagent prompts/reports,
skills | 50,000 chars (was 10,000) |
| Tool output: shell, read, write, diffs, edit/patch, execute, webfetch,
inputs, errors | 10,000 chars (unchanged) |
| Each reveal step | up to 250,000 chars |
| Syntax highlighting | off above 100,000 chars |

## Implementation
- **Where bounding happens:** it moved from each renderer into the
rendering components: `renderMarkdown` (with a `codeLanguage` option),
`renderDiff` and a new `BoundedText`. They use a pure window helper
(`lib/render-text-window.ts`) that cuts on nearby line breaks and never
splits a surrogate pair.
- **New pieces:**
  - `components/hidden-text.tsx`;
  - shared `reveal-focus.ts` (also used by #890's reveal);
- `transcript-alignment.ts`, which holds the reveal point through the
list's existing `alignElement`.
- **Removed:** the dead `renderAnsi` path and the copy-only Markdown
source button (with its now-unused i18n key).
- **Docs:** `dev-docs/TEXT_DISPLAY_BOUNDS.md`; `AGENTS.md` extends the
hidden-parts bullet.

## Validation
- Unit tests:
  - `render-text-window`;
  - `record-display-cache` / instance store.
- New `reveal-truncated-text` browser suite:
  - reveal in place;
  - anchor held while following the bottom;
  - tool pane reveal;
  - persistence across scrolling and session switches.
- Updated suites: `full-source-copy`, `markdown-latest-request`,
`mission-primitives`, `render-cost`.
- 31 relevant browser files, run one at a time: 459 pass, 1 opt-in skip.
UI typecheck clean.
- Known follow-up: reasoning text keeps its own silent 10,000-character
cut.

Oversized files touched: `message-block.tsx` (~1,963 lines),
`tool-call.tsx` (~998), `message-item.tsx` (~828),
`tool-call/renderers/task.tsx` (~581).

An independent gatekeeper review will be published before the admin
merge.
2026-10-18 05:01:03 +02:00
Pascal André
16fad5279e
fix(usage): explain the ChatGPT token-sharing quota limitation (#999)
OpenAI's "Sign in with ChatGPT" login can be used for chat, but OpenAI does
not currently expose its quota through the Codex usage endpoint. The Usage
panel previously presented this as a temporary failure and suggested that
reconnecting might restore it.

The native Codex reader now detects this exact selected login and returns a
dedicated, credential-free reason, so the panel explains the limitation in
all ten UI languages instead of calling it a temporary outage. No quota
request or account fallback is attempted for this login, and its caches are
cleared if the selected account changes.

This is a diagnostic explanation only. It does not restore quota access for
this login. The legacy Codex browser/device login keeps its existing reader
and continues to show quotas. The same limitation is documented in
dev-docs/PROVIDER_ACCOUNTS_UX_PLAN.md, with OpenAI's confirmation linked.

Validation: 19 server tests pass (native selection, account changes, cache
revocation, routes, credential redaction); 14 provider-usage browser tests
pass (English/French 320 px, account-change recovery, isolated Vite cache).
Typechecks report the same baseline diagnostics as before the change, with
ES2022 libraries: 14 server errors and the known UI help-url.test.ts error.
No installed build or shared service was restarted.

Independent gatekeeper COMMENT review found zero actionable P0-P3 findings
at 6742620d0153ae1fee6b3f7e0c25b4a8d4dfdf5b.
2026-10-18 04:27:19 +02:00
Pascal André
008e53aa27
docs(sidecars): keep rejected-isolation history out of the architecture notes (#1003)
Documentation only. Removes the 'isolation was evaluated and rejected'
sentence from \dev-docs/architecture.md\: the architecture notes
describe how SideCars work (trusted same-origin extensions), not the
alternatives tried. \AGENTS.md\ and the architecture-guide reference
kept a pointer to that sentence; they now state the reason directly, so
agents are still told not to reintroduce isolation.
2026-10-18 03:47:16 +02:00
Pascal André
c12d473ac1
docs(sidecars): condense the rejected-isolation rationale to one line (#1002)
Documentation only. Replaces the bullet list added in #1001 with one
sentence in the existing SideCar paragraph of
\dev-docs/architecture.md\: isolating SideCars (opaque sandbox, separate
origin or per-view port) was evaluated and rejected because it breaks
service cookies/storage or single-port, proxied and tunnelled access.
The detailed measurements remain in #1001.
2026-10-18 03:40:03 +02:00
Pascal André
1ce629b9a6
docs(sidecars): document SideCars as trusted same-origin extensions (#1001)
## Summary
Documentation only. SideCars are now documented as **trusted same-origin
extensions** of CodeNomad, replacing the "temporary exception /
isolation follow-up" wording introduced with #994.

## Why
The isolation follow-up was prototyped, measured and reviewed, then
rejected:
- **Opaque sandbox:** the frame loses cookies and storage, so the
service's own login and state break.
- **Separate origin:** keeping service cookies requires a same-site
origin (another port or subdomain). Chromium still sends the
application's `Lax` cookie across ports of the same host, and CORS alone
did not block authenticated writes, so every API would need an origin
barrier.
- **Per-view ports:** these are unreachable through a single exposed
port, reverse proxy or tunnel, and reset service storage on every
opening.
- **IP and single port:** with a literal IP and one exposed port, no
frame topology gives both isolation and working `Lax` cookies.

## Changes
- **`dev-docs/architecture.md`:** records the decision and these
constraints.
- **`AGENTS.md` and the architecture-guide `desktop-conventions.md`:**
refer to the decision instead of a pending prototype.
- **`CONTRIBUTING.md`:** no longer claims SideCar frames are sandboxed.
- **`README.md`:** tells users to add only services whose content they
trust.

The #995 cookie filtering stays as is; no behavior change.
2026-10-18 03:38:56 +02:00
Pascal André
54958f4e83
fix(tauri): prevent Windows event-loop refcount corruption (#996)
## Summary

Fix repeated Windows heap-corruption crashes in the installed Tauri
build. The pinned runtime exposes clonable, thread-safe handles whose
unsafe context graph contains Tao's non-atomic `Rc<EventLoopRunner>`.
IPC-worker clone/drop traffic races the event loop's reference count and
can free the runner prematurely.

- Use `Arc` for that internal Windows runner counter, retaining
main-thread confinement of runner state and the target's existing public
threading contract.
- Add an isolated regression using the real pinned Wry runtime's public
handle API, with a negative control that reverses only this counter in a
temporary crate copy.
- Add the Windows regression to CI and document the captured failures,
compatibility rationale and removal criteria.

This is a local compatibility correction for the runtime CodeNomad
ships. [tao#1290](https://github.com/tauri-apps/tao/pull/1290) proposed
the same counter change but was closed **without merging**, because the
underlying contract violation is Tauri moving/cloning the non-Send
target. That distinction is recorded explicitly. Related reports:
[tauri#15408](https://github.com/tauri-apps/tauri/issues/15408) and
[tauri#15793](https://github.com/tauri-apps/tauri/issues/15793).

## Validation

- `node scripts/test-tauri-runner-refcount.mjs --baseline`: **9 native
crashes / 10 negative-control processes; 5 / 5 corrected passes**,
800,000 handle clones plus teardown per process.
- `node scripts/test-tauri-input-deadlock.mjs --baseline`: keydown,
keyup, char, syschar, IME and instrumented IME-boundary checks pass; the
original/reverted negative controls reproduce their expected deadlocks.
- `cargo build --locked --release -p codenomad-tauri`: passes.
- `rustfmt --check` on the new fixture and `node --check` on its runner:
pass.
- Corrected release executable installed locally on the affected Windows
machine, retaining the existing resources and updater verification
public key; original executable and matching new symbols archived
locally.

The runtime reproduction needs no JavaScript or web page. The Virtua
0.53.5 upgrade is absent from the crashing `3394fc7f` build. Sustained
application use remains additional validation beyond the bounded native
regression.
2026-10-18 02:07:17 +02:00
Pascal André
be66133469
fix(sidecars): isolate application cookies from upstream services (#995)
Authenticate SideCar HTTP/WS requests before removing the configured CodeNomad session cookie, desktop session-cookie namespace and Remote Control device credential from upstream requests. Filter reserved response cookie names as well while retaining service cookies. Use the bounded Node HTTP parser for WebSocket handshakes so successful and rejected upgrades receive the same cookie treatment; preserve buffered bytes, binary forwarding and non-101 response framing. Document that this is credential filtering, not same-origin DOM/API isolation. Validation: 143 HTTP/proxy tests, 11 focused cookie/transport/preview tests, server typecheck, and isolated Chromium/Electron prototype exercises. General CI was still running at the explicitly authorized admin merge.
2026-10-18 00:28:05 +02:00