Commit graph

64 commits

Author SHA1 Message Date
Pascal André
b31d9e3772
feat(settings): add OpenCode image generation and storage settings (#1193)
## Summary

OpenCode 2.2 ships two opt-in experimental image features. This PR
exposes them in **Settings → Providers → Images**, a new folded group
beside Models and Web search:

- **Image generation** (`experimental.image_generate`): Off, **On,
automatic model**, or a default image model from the native `GET
/api/model?types=image` catalog. Agents then get OpenCode's background
`image_generate` tool while a connected provider offers an image model;
each image asks permission first.
- **Store images once** (`experimental.media_blob`): attached and
generated images are stored once in OpenCode's deduplicated blob table
instead of inside every message row. CodeNomad's direct SQL readers
already hydrate these refs.

No new instance-proxy route: everything stays in OpenCode and its
configuration.

## Design

- **Global scope only.** OpenCode reads `media_blob` (like
`subagent_depth`) from the whole `experimental` object of the
highest-priority document that has one. Writing a project block would
silently hide inherited experimental settings, so the project document
is never written; a project that sets its own choice is reported under
the control.
- **Contract-gated, never version-gated.** The native `experimental`
object has `additionalProperties: false`, so an unknown key would break
an older daemon's configuration. Each control appears only when
`Config.InfoEncoded` declares its key; the backend refuses writes
otherwise. 2.2.1 shows both, 2.2.0 only media storage, 2.1.x neither
(the group says so).
- **Existing authorized document path.**
`PluginControls.readConfigDocuments`/`editConfigDocument` plus
`native-setting-document.ts` targeted JSONC edits, as for Web search and
subagent depth: ownership, deletion and connection fences, comments and
unrelated fields preserved. Turning a setting off removes the key and
any `experimental` object it leaves empty.
- **On demand.** The folded group mounts nothing; the contract and the
image-model catalog are read only while it is open, with coalesced
rereads on config/model/credential events and reconnects.

## Validation

- Server units (`image-settings.test.ts`): contract detection, OpenCode
value forms, effective values and project overrides, JSONC preservation,
empty-object cleanup, route validation.
- New isolated native fixture `scripts/test-image-settings-native.mjs`
(added to the compatibility job for the minimum and latest runtimes,
`--expect-supported` on latest). Run locally on Windows against the
exact CLIs:
- **2.2.1**: both settings detected, written, accepted by the native
schema and hot-reloaded; reset restores the original `experimental`
object.
- **2.2.0**: media storage only; generation write refused, file
untouched.
  - **2.1.4 / 2.1.2**: no settings; writes refused, file untouched.
- Browser regression (real `ProvidersSettingsSection`): no read while
folded, one write per change, no replay after a failed write,
unsupported runtime, 900/380 px widths. The existing web-search tests
still pass.
- `npm run typecheck`, server `tsc --noEmit`, help checks (10 locales).

Not covered here: rendering generated images in the transcript
(OpenCode's completion notice is a synthetic message CodeNomad hides
today). That is the follow-up PR.
2026-10-29 16:47:47 +01:00
Pascal André
a097b9a250
feat(extensions): read a project's issues and pull requests through panel extension API 3 (#1191)
#500 asks for a view of a repository's issues and pull requests next to the sessions working on them. Panel extensions are the place for such a view, but they run sandboxed: no network, no project folder, no credentials. Extension API 3 adds one bounded, host-owned capability, `repository.workItems.read`, so an addon lists and opens a project's issues and pull/merge requests without receiving any of those. Worktree actions stay out of scope for a later, separately consented capability.

Host (server/src/forge/): the project folder's `upstream` remote, else `origin`, else any forge remote, strictly validated (host or bracketed IPv6, HTTPS port, path segments), names the repository. github.com, gitlab.com, codeberg.org and gitea.com are read through the user's signed-in `gh api`, `glab api` or `tea api` (tea's stderr status checked, its login matched to the exact authority), falling back to their anonymous REST API for public repositories; any other host is identified and read only through a CLI signed in to it, never contacted anonymously. CodeNomad stores no token. CLIs run without a shell, quietly, within 15 seconds and 4 MiB; anonymous bodies are bounded while streaming. Each read returns one kind, at most 20 items up to page 50, with the open total; GitHub search words are literal quoted terms, and only items under the repository's web address are published. Discovery and page reads are shared loads cancelled with their last caller, never joined once cancelled and never evicted while running; every process or fetch runs within one host-wide budget (four running, sixteen waiting).

Extension API 3: exact permission set, disabled install, dedicated consent text in ten locales; `codenomad.workItems.list({ kind, query, page, fresh })` and `open(kind, number)`. Routes recheck consent before and after each read, cancel the read of a disconnected caller and drop results when the project closed. `open` asks the server for the item's page on the repository's own forge and opens it through the host's opener, one at a time and at most once a second measured at the opener. API 3 panels receive the host colours; the store's consent check is generalised to a permission and also serves API 2.

Docs: PANEL_EXTENSIONS.md and the extensions help page in ten languages.

Validation: forge.test.ts (remotes, CLI output formats, provider mapping and totals, literal search and foreign items, self-hosted access, bounded streams, shared and cancelled loads, budget, pagination), route tests (consent, digest, bounds, revocation, closing, caller disconnection), work-items-channel.test.ts (delayed answers open one tab, three on the first version), API 3 manifests, the extension browser suite, test-panel-extension-native.mjs --api3/--api2, the root typecheck and help checks. Live, an isolated CLI server with the Contribute addon installed from its ZIP listed NeuralNomadsAI/CodeNomad (gh) and gitlab-org/cli (glab) with totals, labels, search, exact numbers and opening, in English and French; Codeberg read anonymously. Three review rounds ended with no findings.

Refs #500.
2026-10-29 16:45:07 +01:00
Pascal André
34406b3ada
feat(help): add a download page with exact release links and a new home page (#1190)
## Summary

- New **Download** help page (10 languages): an OS-detecting button
linking straight to the recommended Tauri build, then a static
Tauri/Electron table with every asset and its size, version and publish
date, and the winget/npm commands. The header Download link,
`install.md`, `support.md` and the README point to it.
- New **help home page** (10 languages): cockpit positioning, a
*Download CodeNomad* call to action, a silent 46 s tour that autoplays
on desktop only (poster and YouTube link on narrow screens and with
reduced motion), and short Missions / phone access / long histories
sections before the existing orientation sections.
- **Exact links without network in the builder**: `release:<id>` links
and `{{release}}`, `{{download-button}}`, `{{tour-video}}` directives
resolve from `docs/help/downloads.json` and optional `--release` data;
without data, links fall back to the latest release page.
- **Automatic refresh**: `scripts/resolve-help-release.mjs` (CI only)
picks the newest stable release whose assets are fully uploaded;
`help-pages.yml` republishes after a successful *Release Binaries* push
run on `main`, building trusted `dev` sources with read-only
permissions, the token scoped to the resolver step and one production
concurrency group. Pull requests never resolve or publish.

## Safety

- Release data is revalidated in the builder (exact
`github.com/NeuralNomadsAI/CodeNomad/releases/download/<tag>/<asset>`
URLs, no credentials/query/fragment/traversal, positive sizes, UTC
dates) and escaped.
- CSP stays `default-src 'none'`; only `media-src 'self'` is added. The
download script is allowed on the download page only, the tour video on
the home page only.

## Validation

- `npm run test:help`: 27/27 (26 pages × 10 locales, offline; resolver
selection/pagination, release validation, directive problems, 22
detector cases, workflow gates).
- `npm run build:help` with and without the release fixture.
- `node scripts/test-help-browser.mjs`: 46/46 (download button,
JavaScript disabled, video playback, reduced motion, narrow widths,
RTL).
- Read-only resolver run against the live API resolved `v1.0.0`.

The `workflow_run` path can only be observed on the next stable release
on `main`; `workflow_dispatch` from `dev` exercises resolver → build →
deploy.
2026-10-29 16:06:06 +01:00
Pascal André
ca514ed2ea
chore(opencode): qualify and recommend runtime 2.2.1 (#1188)
## Summary

- Qualify OpenCode **2.2.1** (latest stable, 29 October) with the
existing locked **2.1.0 client/plugin** dependencies, and **recommend**
it.
- Keep the technical **minimum at 2.1.2**: no consumed contract changed,
so the minimum has no new reason to move.
- 2.1.2, 2.1.4 and 2.2.0 daemons stay ready and get an optional upgrade
offer. 2.2.1 is the `tested` version. Newer and custom labels keep
authenticated contract recognition.
- The reviewed-contract fast path lists the 2.1.2 minimum and the 2.2.1
target.
- The recommended version is updated in the help (all ten locales), both
READMEs, `CONTRIBUTING.md`, `dev-docs/OPENCODE_COMPATIBILITY.md` and the
architecture skill.
## Contract review (v2.1.4 → v2.2.0 → v2.2.1; 661 + 134 commits)

Reviewed from the exact tags `v2.1.4` (`68bc26b7da`), `v2.2.0`
(`1097e2668b`) and `v2.2.1` (`3ebcf9a32c`), the published npm packages,
and an authenticated OpenAPI dump from each official CLI running in its
own sandbox. The [changelog](https://opencode.ai/changelog) covers
2.2.0. It had no 2.2.1 entry at review time (29 October), so 2.2.1 was
reviewed from its commit log.

- **Authenticated OpenAPI:** 124 → 130 → **134 paths**, 149 → 156 →
**162 operations**. Each dump matches the tagged
`packages/protocol/openapi.json`.
- **2.2.0 adds** `GET /api/plugin/gui`; `POST
/api/experimental/generate/{text,image,decisions}`; `POST`/`DELETE
/api/experimental/fs/watch`; `GET /api/experimental/media/{hash}`; and
`GET /api/experimental/usage/limits`.
- **2.2.0 removes** `POST
/api/experimental/voice/session/{sessionID}/instructions`.
- **2.2.1 adds** six `/api/experimental/schedule` operations (list,
create, get, delete, pause, resume), gated by `experimental.schedules`.
  - **Changed operations:**
- `GET /api/model` gains an optional `types` filter. Without it the
route lists chat models only. Image and decision models are new in
2.2.0, so the default listing matches 2.1.4.
    - `GET /api/event` gains an optional `types` filter.
    - `GET /api/vcs/diff` gains an optional `untracked` parameter.
- `POST /api/experimental/generate` becomes the deprecated alias of
`/generate/text`.
- Nine session, project and saved-permission routes drop
`LocationNotFoundError` from their 404 responses: they no longer boot a
Location ([#58997](https://github.com/anomalyco/opencode/pull/58997)).
- `GET /api/agent` can now answer 503 while plugins settle
([#59082](https://github.com/anomalyco/opencode/pull/59082)).
- In 2.2.1, the session log and persistent-PTY operation IDs move under
`experimental.*`. Their paths are unchanged.
- **Schemas:** all 103 consumed-family schemas are byte-identical from
2.1.4 to 2.2.1. These are `V2EventEncoded`, `Session.*` (messages,
parts, inbox, info), `Form.*`, `Permission.*`, `Prompt*`, `Event.*`,
`Location.*` and PTY/Shell. The session prompt, environment, message,
form/permission reply and RPC operations are unchanged too. The only
`Config.InfoEncoded` changes are additive opt-ins:
`experimental.media_blob`, `experimental.image_generate`,
`experimental.schedules` and `timezone`. Model `type`, prompt-cache
`cache` settings, `FileDiff.Info.more`, `Mcp.Server.title` and
`ServerInfo.hostname` are additive fields.
- **Admission:** every predicate CodeNomad's
`compatibility/negotiate.ts` checks holds on 2.2.0 and 2.2.1. That
covers the canonical API, `session_environment` and the 2.1 replay shape
(`native` without `state`, compaction `metadata` without `recent`).
- **Instance proxy:** none of the new routes is on the allowlist. That
includes the GUI plugin bundle, generation, file watch, media blobs,
usage limits and schedules. The removed voice route was never exposed.
The routes CodeNomad does proxy kept their paths.
- **Published declarations** (2.1.0 pins → 2.1.4 → 2.2.1):
- **Client:** 143 → 147 → 185 methods. 2.2.1 mirrors the experimental
methods under `client.experimental.*`. The only removal is
`voice.instructions`, which CodeNomad doesn't use. 2.2.1 doesn't send
the `experimental-media-uri` capability, so media reads stay inline.
- **Schema:** 2.2.0 adds media refs (`MediaRef`,
`Session.Message.MediaUser`, inbox `MediaUserPayload`), `generate`,
`usage-limits` and `Model.Type`. 2.2.1 adds `goal` and `schedule`. The
one removal since 2.1.0 is `SessionEvent.Source` (2.1.4).
- **Plugin:** 2.2.0 adds the `gui/*` SDK (excluded from the host
runtime) and `Host.packageID`. 2.2.1 adds
`SessionIdle`/`SessionIdleBackground`. The event `subscribe` domain
gains an optional `types` filter; CodeNomad's plugins don't subscribe
through it.
- **Typecheck:** the server and UI typecheck with **0 errors** against
the 2.1.4 and 2.2.1 client/schema/plugin declarations, through scratch
`paths` overrides. The pinned 2.1.0 control is also clean.
- **Host modules:** CodeNomad's three daemon-loaded bundles resolve
`@opencode/plugin` (`Plugin`), `/effect` (`Plugin`, `Rpc`, `Location`),
`/promise/adapter` (`fromPromise`), `/rpc` (`Rpc`), `effect` and
`effect/encoding` (`Base64Url`) from the host. `@opencode/schema` and
`@opencode/client` are bundled from the 2.1.0 pins, and
`@opencode/schema/location` imports are type-only. A probe plugin loaded
in an isolated **2.2.1** daemon resolves every one of these modules and
bindings, as on 2.1.4. The host Effect stays **4.0.1**, with the same
lockfile hash.
- **Private native services:** the pending-request reader and Missions
recurrence use keyed native services: `LocationServiceMap`, `Form`,
`Permission`, `Location`, `Job`, `Database`, `Session` and `Bus`. Their
keys are unchanged at v2.2.1, and the members CodeNomad calls are
unchanged. The changes are additive: `Permission.assertNotDenied`,
`Session.*WithRefs`, an `image` Job kind, and
`LocationServiceMap.canonical` normalizing paths.
- **Storage** (migrations from 2.1.4 to 2.2.1): `blob`/`blob_ref`
(always created; written only with `experimental.media_blob`) and
`schedule` are new. `session_pending` is dropped; CodeNomad never reads
it. A `session_v2.allowance` column is added and dropped within 2.2.0.
- The tables CodeNomad reads directly (`session_message`, `session_v2`,
`session_inbox`, `event`, `event_sequence`, `kv`) keep their shape.
- The pruning plugin's `media-blob.ts` already projects `media:<sha256>`
refs.
- The pruning fixture enables `media_blob` explicitly, so the
always-present `blob` table doesn't change its expectation.
- Missions reads `session_inbox` only by count and ID in production
code.
- **Behavior changes checked:**
- **Code Mode:** 2.2.1 moves programs onto one job queue
([#57976](https://github.com/anomalyco/opencode/pull/57976)). The
run-to-completion regression behind the 2.1.2 minimum passes on 2.1.4,
2.2.0 and 2.2.1: `{ total: 2000, beforeAwait: 1000, count: 1000 }`.
- **Session form polling** returns `[]` without booting a cold Location
([#59083](https://github.com/anomalyco/opencode/pull/59083)). Forms are
in memory, so nothing pending is hidden. This matches CodeNomad's
existing-only pending reader.
- **Location keys and `session.create` directories** are canonical on
POSIX too: trailing slash removed, `posix.normalize`
([#59327](https://github.com/anomalyco/opencode/pull/59327)). Windows
already normalized. This is low risk; it was reviewed in source only,
since the location and ownership fixtures run on Windows and the POSIX
certification covers migration, Code Mode and Git-degraded.
- **Location keep-alive:** the changelog line "keep project services
alive … with a 24-hour inactivity cap"
([#57274](https://github.com/anomalyco/opencode/pull/57274)) doesn't
match the shipped tree. That commit was superseded before 2.1.4, and no
cap exists at v2.2.0 or v2.2.1. From 2.1.4 to 2.2.1,
`location-activity.ts` only canonicalizes paths and generalizes Job
kinds. Long-waiting Missions sessions keep their Location as on 2.1.4.
- **Plugin readiness:** each runner step and tool call now waits for
plugin activation
([#51133](https://github.com/anomalyco/opencode/pull/51133)), and
agent/provider lists wait for plugins. Presence leases stay outside the
discovery root, so this adds no reload.
- **Service lifecycle:** registration and config renames are retried on
Windows. The managed service ignores a saved `XDG_STATE_HOME`
([#58815](https://github.com/anomalyco/opencode/pull/58815)); CodeNomad
never sets it. The `OPENCODE_SERVER_USERNAME` change was added and
reverted within 2.2.0. Detached POSIX children are killed when OpenCode
exits; service start uses its own spawn path.
  - **2.2.1 opt-ins:**
- schedules, both the API and the agent tool, need
`experimental.schedules`;
    - `image_generate` needs `experimental.image_generate`;
    - `/goal` runs only when invoked;
- the subagent tool description now invites follow-ups to *running*
subagents, with the same mechanics as 2.1.4.
- **Other changes:** compaction sizing from the provider's count, MCP
malformed-tool tolerance, OAuth refresh coordination
([#54023](https://github.com/anomalyco/opencode/pull/54023)) and
webfetch redirect rules. They touch no consumed contract and were
reviewed in source; of these, the native suites exercise compaction
only.
- **Upstream #52922** (Bus routing before Location fanout) is still
open: keep the discovery and event-relay mitigations.

**Verdict:** no consumed contract changed from 2.1.4 to 2.2.1.

- The **2.1.0 client/plugin pins can stay**.
- The **2.1.2 minimum has no reason to move**.
- 2.2.1 is admitted through the existing contract recognition, and
nothing found blocks recommending it once the native suites pass on it.
- No CodeNomad bug was found.

Bumping the pins is optional. It would bring the 2.2 client's
discard-tool-result-payload option and its "register running sessions
missed while disconnected" fix
([#53012](https://github.com/anomalyco/opencode/pull/53012)). It would
also move the replay reducer, so it belongs in its own PR. The UI and
server already typecheck against 2.2.1.

**Not verified here:**
- the full native suites against 2.2.0/2.2.1 (only the Code Mode
regression and the host-module probe ran);
- the TUI pruning companion under OpenTUI 0.7. It uses only
`Plugin.define`, `ctx.data.session` and `ctx.data.listen`, so the 0.7
renderable changes don't apply, but this wasn't run;
- POSIX canonicalization on a real Linux/macOS runtime;
- the 2.2.1 release notes, which were unpublished at review time.
## Isolated Windows qualification

Base: dev `157766206`; Node **25.2.1** (local; CI runs the pinned
24.20.0); exact official CLI **2.2.1**; unchanged locked client/plugin
**2.1.0**.

- **Build and checks:** production server and native plugin builds
(pruning, automation, Missions), root typecheck.
- **Native runtime:** Code Mode job ordering (`{ total: 2000,
beforeAwait: 1000, count: 1000 }` on 2.2.1), plugin runtime, automation;
complete pruning, history, proxy, navigation and media; Location,
worktree families/management and event relay; permission receipts; Git
init/degraded.
- **Missions:** contract, family control, native and trajectory; **all
11 recurring Mission journeys** (`ABCDGELFWQN`).
- **Sessions:** environment, fork, aside, interrupt, lifecycle, blank
cleanup (`--conditions=browser`, as in CI), compaction isolation, prompt
skills, provider usage/accounts/selection, service usage, web search,
plugin packages, output limit, Promise-tool failure, subagent depth,
temporary workspaces.
- **Migrations:** history from **2.1.0, 2.1.2, 2.1.4 and 2.2.0** to
2.2.1.
- **Installation:** fresh install, native upgrade and legacy replacement
fixtures.
- **Units after the recommendation change:**
compatibility/transport/update **35/35**; full server **2,628 passed, 8
skipped, 1 cancelled** (`index.test.ts` shutdown test timed out at 5 s
under full-suite load and passes **4/4** alone); ten-locale help checks
pass.

In CI, `latest` resolves to 2.2.1: 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-29 15:04:29 +01:00
Pascal André
8e8ea0653e
fix(history): search recent history first, cover more of it and search further back (#1182)
## Summary

Follow-up to #1161 and #1164. In a large project, the ranked project
search covered only the **oldest** messages: the scan walked rowids
upward under a 1.5 s budget, so recent conversations were never reached,
and identical searches covered 5,000 to 7,000 messages depending on
load. Each 1,000-row statement also sorted and evaluated its whole chunk
before returning its first row, blocking the daemon for about 70 ms at a
time.

- **Newest first:**
- The scan streams 128-row ranges from an O(1) horizon, without a
temporary sort.
  - It covers a deterministic window of the 16,384 most recent messages.
- **Cheaper per row:**
- A necessary UTF-8 byte check on the raw row rejects messages that
cannot contain a query term before any JSON is decoded.
  - Only possible matches pay for the exact readable-text mask.
  - The existing time, candidate, session and judge guards remain.
- **40 results** in the project search window; the agent tool keeps 10
by default and 20 at most.
- **Search further back:**
- When older history remains, it ranks the next older window for the
same query and options through an authenticated per-location
continuation.
- The continuation is bound to scope, query, options and the original
horizon, so later appends never enter it.
- Older windows replace the list, because ranking scores are
window-local.
  - The coverage note names the examined range in all ten locales.
- Query, scope or option changes, closing and leaving the session reset
it.
- The conversation scope is unchanged.

## Measurements (synthetic 40,000-message corpus, `node
scripts/benchmark-ranked-history.mjs 681b85486`)

| Metric | Before | After |
|---|---:|---:|
| Messages scanned | 19,001–22,001, oldest first | 16,384 each run,
newest first |
| Scan duration | 1,503–1,550 ms | 650–739 ms |
| First-row latency | 50–76 ms | 0.8–1.8 ms |
| Longest synchronous step | 70–82 ms | 1.4–1.9 ms |
| Rows fully decoded | 30,000–34,500 | 3,276 |

## Validation

- Superset property checks against the exact mask: 12,547 cases,
including Unicode case crossings, escapes and surrogate pairs.
- Deterministic coverage and streaming latency regressions.
- Cursor tampering, binding and excluded appends; store lifecycle.
- Session-pruning and history routes 155/155; native bundle 1/1; UI unit
44/44.
- Browser: history 3/3; search-highlight 12, plus 1 optional benchmark
skipped.
- Help 9/9; both typechecks.
- Isolated native history fixture on OpenCode 2.2.0: 26 checks.

Oversized file touched: `message-section.tsx` (~1,823 lines).
2026-10-28 19:26:40 +01:00
Pascal André
681b854865
feat(missions): bound journal capacity per mission, not per project (#1178)
Step 4 of 6 toward removing the Missions limits (#1167 follow-up), on
top of steps 1-3 (#1175, #1176, #1177).

## Change
- **Per-mission capacity:** each mission holds at most 2,000 ordinary
entries plus its own 36-entry deletion reserve (2,036 per range). **No
project-wide cap.**
- Capacity checks and appends read only the target mission's range (no
full scan). Safety decisions keep their full reads (accepted: their cost
grows with history).
- Reserve: only the mission's own tombstone and already reserved
receipts; works on every storage.
- Full project reads bounded per range (and stray entries together);
memory = per-mission bound × missions, documented trade-off.
- Purge is retention-only (20 newest settled deletions kept);
capacity-pressure purge, creation pre-check and related re-checks
removed.
- Existing mission data is negligible (projects were capped at 20
missions, barely used): **no legacy, migration or rollback handling by
design.**
- Docs: `MISSIONS.md` safety limits; help sentence on freeing storage
updated in 10 locales.

## Validation
- Read-budget test at 10,000 history entries; project beyond one
mission's bound fully works; capacity checks read only their range; full
mission refuses ordinary entries/new Stop but deletes through its
reserve (with and without removal). Each red with its guarantee mutated.
- Native stage: 2,200 entries across 100 finished missions on an
isolated runtime, then create/declare/readout/delete with retention
purge.
- Server and UI typecheck; 1,508 server tests; `test:help`;
`test-missions-native.mjs` on isolated OpenCode 2.2.0 and 2.1.4.
2026-10-28 15:44:19 +01:00
Pascal André
84d2d0a245
fix(missions): free journal capacity when deleted missions settle (#1173)
Follow-up to #1167.

## Problem
A project's mission journal holds 2,000 entries and never removed any.
Deleting a mission appended a tombstone and cleanup receipts on top of
its events, so a project that keeps creating and deleting missions
eventually filled the journal permanently: no create, and no delete
either (the tombstone needed room).

## Change
- **Purge on settlement:** once every cleanup target of a deleted
mission has its receipt, its events are removed from native plugin
storage (`storage.remove`, passed through a narrow
`MissionStorage.remove`; storage without it keeps today's behaviour).
- **Retained:** the 20 most recent settled deletions stay in full
(cleanup history, lost-ACK checks); all eligible ones are purged when
capacity is needed. A deleted mission stays while it references a
session another unsettled mission could still clean up.
- **Deletion record:** one compact record per purged mission at
`codenomad-missions/v2/deleted/<projectToken>/<missionID>`, outside the
scanned prefix (no journal capacity, exact-key reads). It refuses
recreation (`mission-deleted`) before any native effect on create,
prepare and coordinator start replays, and answers an exact deletion
replay with the original summary.
- **Crash-safe, resumable:** record first, then `mission.created`,
tombstone, intents, receipts; an incomplete group with a record is
dropped from readers; late writes are removed by the next purge; damaged
journals are never purged.
- **Full journal recovers:** a 36-entry reserve, usable only by
tombstones and reserved receipts; reads bounded at 2,036.
- Legacy tombstones are purged the same way.
- Docs: `MISSIONS.md`; help (EN + 9 translations) cleanup-history
sentence.

## Known risks
- Rolling back to an older build reads a mid-purge journal as damaged, a
reserve-using journal as over its limit, and ignores deletion records (a
retried creation could recreate a purged mission).
- Under capacity pressure the recent cleanup history is purged too; a
delete whose ACK was lost in that window then shows a false "missing"
error.
- Deletion records accumulate (one small exact-key entry per purged
mission).

## Validation
- `journal-purge.test.ts` (10 tests, each red with its piece removed):
200 create/delete cycles, recreation refusal and replay, pending cleanup
kept, crash at every step, late writes, legacy tombstones, full journal
of live missions, no-remove storage, shared sessions, damaged journals.
- Native `test-missions-native.mjs` gains a settled-deletion purge stage
(red when the plugin lacks `remove`).
- Server and UI typecheck; 1,331 server missions/routes tests;
`test:help`; native run on isolated OpenCode 2.2.0.
2026-10-28 11:59:23 +01:00
Pascal André
49f2efe8f9
fix(sessions): read OpenCode media blobs like native history (#1172)
Follow-up to the #1158 review (pre-existing P3).

## Problem
With OpenCode 2.2.0's opt-in `experimental.media_blob`, inline files are
stored as `media:<sha256>` refs (`blob`/`blob_ref`) and native reads
hydrate them. CodeNomad's direct SQL readers saw the refs: pruning
reported a false `conflict` (the UI hashes hydrated content), assets
looked unavailable, navigation emitted unhydrated JSON.

## Change
- New `session-pruning/media-blob.ts` mirrors native `blob.ts` hydration
(tool files → data URIs, user files → data, missing blob → native
placeholder), bounded before loading bytes.
- Preview, prune candidates, prune transaction, navigation windows and
assets read the hydrated form; existing size bounds apply to it.
- Pruning compares the hydrated form, writes survivors back exactly as
stored, and like native content rewrites releases the message's
`blob_ref` rows for hashes it no longer names, deleting a blob only when
unreferenced.
- Decided by stored representation, not version: no refs → unchanged;
refs without recognisable blob tables → `unsupported_storage` (distinct
from `conflict`).
- Docs: `SESSION_PRUNING_RPC.md`, `SESSION_HISTORY_NAVIGATION.md`,
`PANEL_EXTENSIONS.md`.

## Validation
- `media-blob.test.ts` (projection, placeholders, bounds, preview,
prune, shared blob retained, unsupported storage, assets, navigation).
- New native check `scripts/test-session-media-blob-native.mjs` (run by
`test-session-pruning-native.mjs`): 2.2.0 with media_blob passes; 2.1.4
passes; red on unmodified dev.
- Server and UI typecheck; all session-pruning, route, proxy and
panel-asset tests.
2026-10-28 10:41:25 +01:00
Pascal André
5a49ac36b2
feat(history): rank whole-project transcript search, keep conversation search chronological (#1164)
## Summary

PR 2 of 2 after #1161. The transcript search window's **project scope**
uses the ranked engine; the **conversation scope** keeps its literal,
chronological, paginated search and code path.

- **Project scope:**
- One request returns the best 20 results in relevance order: rare words
weigh more, an exact phrase ranks first, and each conversation appears
once before repeats. There is no next-page traversal.
- Every query term is highlighted in the excerpts, in the preview and in
the destination conversation when a result is opened.
- Compact notes report a partial scan, excluded folders, or a runtime
without ranked search.
- **Route:** `POST /api/workspaces/:id/session-history/ranked` calls
only the plugin's `historyRanked` RPC.
- It applies the same ownership, provenance, deletion, connection and
cancellation fences as the literal history route, through scope helpers
shared in `session-history-scope.ts`.
- It validates counts and every contributing location before publishing
and strips provenance.
  - It records an unsupported method per connection.
- Disjoint worktree roots are interleaved by native rank before session
diversity is applied.
- UI strings and help pages are updated in all ten locales.

## Validation

- Server and UI typechecks pass.
- Routes 13/13, covering ownership rejection, fences, invalid provenance
and the unsupported state.
- Store tests 6/6.
- Chromium:
  - history 2/2;
  - search-highlight 12, plus 1 optional benchmark skipped;
  - session-search 1/1.

The browser cases cover ranked order, highlighting, opening a result and
the unchanged conversation scope.

Oversized files touched: `message-section.tsx` (~1,816 lines),
`message-block.tsx` (~1,775), `api-client.ts` (~833).
2026-10-27 21:43:48 +01:00
Pascal André
a868a8e720
feat(missions): blue prepared missions, quiet interrupted chevron, full-panel create form (#1155)
## Changes

- **Prepared missions are blue.** A created, never-started mission gets
a blue mark (`--accent-primary`), like a prompt ready to send.
- **Interrupted no longer bubbles into the collapsed chevron.** Like a
collapsed session parent, the chevron only counts answers, running work
and unseen completions; interrupted, paused and prepared missions colour
only their own rows. The unused `missionsPanel.attention.interrupted.*`
keys are removed from all locales.
- **The creation form replaces the list and detail.** While creating,
the current-mission line, list, schedules and selected detail are hidden
(still mounted, so pending controls and retries survive) until Create or
Cancel.
- **Nesting depth in the creation form.** Options embed the existing
project depth editor (`MissionSubagentDepth`). OpenCode only reads
`experimental.subagent_depth` per config file, so the value is the
project's and applies to every conversation there; the editor says so.
- **Button templates.** `.window-text-button` had no rule anywhere,
leaving ~18 mission buttons unstyled. It is now a shared primitive in
`styles/components/window.css` (borderless accent text, hover tint,
muted when disabled). The creation footer's **Save as brief** uses the
footer's `button-secondary` template; the bordered **Answer** button
uses `.window-action`.

## Docs

`dev-docs/MISSIONS.md`, `dev-docs/UI_CONVENTIONS.md` (window button
primitives), and `docs/help/*/missions.md` in English plus the nine
translations.

## Validation

- UI typecheck; mission and i18n unit tests; `npm run test:help`.
- All 34 `tests/browser/mission*.test.ts` files pass. Updated:
`mission-status-marks` (blue computed colour, quieter chevron label),
`mission-editor-profiles` and `mission-editor-lifetime` (the form
replaces the list; leave it before browsing).
- Visual check of the real components through the mission-control
fixture: prepared mark, collapsed chevron, full-panel form with depth,
footer buttons, selected detail.
2026-10-27 14:51:10 +01:00
Pascal André
339a0844ca
fix(opencode): stop an emptied project experimental block from capping subagent depth (#1153)
## Problem

A mission asked to nest subagents three levels deep stopped at the first
level: every nested call returned `Subagent depth limit reached (1).
Increase "experimental.subagent_depth"…`, although the global OpenCode
config sets `experimental.subagent_depth: 5`. The project file
`.opencode/opencode.jsonc` contained only `"experimental": {}`.

## Cause

- OpenCode reads `Config.latest(entries, "experimental")?.subagent_depth
?? 1` (`core/src/tool/plugin/subagent.ts:129`): the **whole**
`experimental` object of the highest-priority document that has one,
unmerged (`core/src/config.ts:24-27`). An empty project block therefore
hides the global depth.
- CodeNomad's depth editor reset (**Inherit**) removed only
`subagent_depth`, leaving that empty block behind.
- The editor computed the effective depth as the last document defining
`subagent_depth`, so it showed the global 5 while OpenCode applied 1.

## Change

- **Inherit** also removes a project `experimental` object it leaves
empty; foreign experimental keys, comments and formatting are kept.
- The effective depth follows OpenCode's read; a project block without
the depth reports the native default (unknown when the contract declares
none) instead of the hidden global value.
- `dev-docs/MISSIONS.md` describes both. Help already says that an empty
field inherits, which is now true in every case.

## Validation

- `subagent-depth-settings.test.ts`: shadowing read, Inherit keeps
foreign experimental keys, an empty project block reported as hiding the
global depth and removed by Inherit after an override.
- Server typecheck.
- `scripts/test-subagent-depth-settings-native.mjs` against an isolated
OpenCode 2.1.4.
2026-10-27 13:03:36 +01:00
Pascal André
048cd65183
fix(missions): one status palette, no agent-decided failure, resumable missions (#1150)
fix(missions): one status palette, no agent-decided failure, resumable missions (#1150)

#1147 reported a red chevron beside a running mission because another,
older mission had failed. Missions mixed two colour systems: rows showed
session-style attention when there was some and otherwise a lifecycle
colour (blue running, green completed forever, red failed), the collapsed
chevron added "failed", and the task tree had its own palette. Red meant
both "your answer is expected" and "this ended badly", a failed mission
offered no action, and a one-time coordinator could end a mission as
failed on its own.

Colour now follows the session list everywhere (mission and schedule
rows, the selected mark, task icons, the chevron) and shows only what
needs attention now:
- red: an answer is requested, counted like Needs you (declared actors
  and observed family; schedule rows without a passage read count the
  live passage's conversation and loaded subsessions);
- yellow, pulsing: observed work;
- orange: interrupted or paused, with an action to continue (an
  interrupted schedule, or a running one whose latest run failed);
- green: a completed mission this window has not shown yet;
- everything else is neutral: outcomes (done, failed, blocked, waiting,
  retired) are status text and icon shapes.
missionMark/scheduleMark in mission-picker-model.ts compute one mark per
entry. Like a collapsed session parent, the chevron colours only while
the list is collapsed, from the other entries (paused is not counted).

"Seen" works like an idle session: mission-view-state keeps the
completed missions this window has shown under its own layout key and
signal, so recording one never wakes project-view readers (an early
write through the project view re-ran session-view effects and aborted
an in-flight outline read). The first read counts missions already
completed as seen; a completion is marked when selected while the panel
is visible in a focused window (createDocumentAttention, now shared with
SessionView).

One-time coordinators no longer decide a failure: the plugin's
MissionControl sets completedFinalOnly, and the refusal, coordinator
context and tool description tell the coordinator to ask the human
through the native question tool. Only the human stops a mission.
Recurring passages keep their own control and completed|failed.

Interrupted missions get an action:
- A running mission's coordinator owes its final report: observed idle
  with an empty inbox and no coordinator notification still being
  delivered (native returns excluded), it projects idle-without-report;
  when nothing else runs the mission reads Interrupted and coordinator
  recovery replaces Pause.
- A failed mission reads as interrupted unless stopped since, and the
  same recovery resumes it: under the mutation lock it refuses with
  already-member when one of its sessions joined another active
  mission (as at creation), otherwise appends mission.reopened at the
  exact revision and sends the ordinary recovery nudge at the new
  revision. The reducer resumes only a failure, drops its summary and
  lets a later Stop end it; a later finish uses a revision-bound event
  identity. The unwired authority policy accepts the same resume.

dev-docs/MISSIONS.md records the rules; the Missions help page gains
"Colours and interrupted missions" in all ten languages, and new strings
are added in every locale.

Validation: mission-resume.test.ts (completed-only refusal, passage-style
failed finishes, resume once with conflicts and no second nudge,
shared-session refusal, reducer reopen/Stop rules); activity.test.ts
(owed final report, queued inbox, pending and native-return reports,
non-running missions); mission-picker-model unit tests; the new
mission-status-marks browser test (marks, collapsed chevron, unseen
completion surviving reload, recovery replacing Pause, neutral done and
blocked tasks, failed mission resume, recurring requests including
independent actors and a zero selected count, quiet stopped/paused
schedules), red on dev; updated mission-control, mission-briefing and
mission-recovery tests. All Missions server tests, the full browser
suite (four shards), help checks, UI and root typechecks pass.
2026-10-27 05:05:26 +01:00
Pascal André
24d9a18fa8
fix(shell): hide each panel at the end of its own compression (#1149)
Narrowing a window moved the panels back and forth: at 800 px the right
panel closed while the left one jumped back from 220 to 340 px, then
shrank a second time; at 600 px the left panel closed and the right one
reappeared, only to close again at 580 px. Widening repeated the
sequence in reverse with its own swaps.

Narrowing now shrinks the conversation to its minimum, then the right
panel to its minimum, where it hides, then the left panel to its
minimum, where it hides. Each panel shrinks once and hides at the end of
its own compression, so the other panel never changes width at that
moment and only the conversation takes the room; widening retraces
exactly the same layouts.

Each side panel keeps whether the user wants it (opened, or closed
while docked) and docks while the window has its room, in a fixed order:
the session panel first, the right panel only beside the full session
panel. A wanted panel without room hides and returns with the room, so
the layout follows the window width alone; a minimizing window's tiny
viewport hides both panels and restoring it brings them back. A panel
opened without room floats over the conversation; dismissing it keeps
the docked choice, and it docks once it has room. A panel closed while
docked stays closed. Dismissing a floating panel by clicking outside
leaves focus on the clicked control; a panel the layout hides hands
focus to its toggle when it held focus.

Touch tablets remember the wanted panels per orientation. A floating
panel never docks while the window already has its new shape but the
orientation has not changed yet, so rotation memory records only the
user's choices; the shape is read from the orientation media query,
which stays reliable on zoomed, near-square windows.

Implementation: `resolveEmbeddedDrawers` docks the right panel only
beside the left one at its chosen width and caps each panel's chosen
width by the room. `useDrawerChrome` replaces the open state, the
"awaiting room" marks and their effects with a wanted and a floating
signal per panel; docked and shown states derive from them and the
window width. The public API keeps its shape (`leftOpen` means shown;
`setLeftOpen(true)` opens docked or floating, `setLeftOpen(false)`
dismisses silently).

Validation: a shell-navigation sweep narrows 1400 to 400 px in 20 px
steps, requiring panels never to grow back or reappear, then widens and
requires the same layout at every width. Regressions cover outside
dismissal focus, rotation with a floating panel, a wanted panel docked
by restored room across rotations, and rounded near-square viewports.
drawer-layout unit tests cover each panel hiding at the end of its
compression. Every browser file that drives the panels passes; the user
validated the behaviour in a separate Electron dev instance; four
gatekeeper rounds ended with no P0-P2 finding. Help is updated in all
ten languages.
2026-10-27 03:28:25 +01:00
Pascal André
fb22f6e969
feat(shell): close panels that lose their room and reveal list choices (#1144)
In a window too narrow for a docked panel, the panel floated over the
conversation and covered the messages. On phones and narrow windows, the
floating session panel also stayed open over the session the user had
just chosen, so every choice needed a second tap to see it.

A docked panel that loses its room (the window narrows, or the other panel
opens) now closes instead of floating. It returns when the room does,
unless the user opened it meanwhile; a panel closed by the user stays
closed. Minimizing a window briefly reports a tiny viewport, so without
the return every minimize would lose both panels. A panel opened without
room still floats over the conversation, as before.

Choosing a session, the shown one included, or Instance Info from the
floating session panel's list now closes that panel to show the choice;
reopened, it still marks the shown session. Back and Forward select
without closing it. Docked panels are unchanged.

Implementation: `useDrawerChrome` closes an open drawer when its docked
state turns false and marks it as awaiting room; an effect reopens it,
the session panel first and the other only if both then fit docked, and
opening a drawer clears the mark. The layout inputs are shared by the
initial layout, the docked state and the return. On touch tablets each
rotation's orientation memory decides, counting a panel closed for room
as open in the orientation it leaves. A panel closed by the layout hands
focus to its toggle when it last held focus, even when its re-render
already removed the focused control: the shell remembers which panel's
content last took focus until focus deliberately moves to the page. The
sidebar's list selection handler closes the floating session panel; the
history buttons keep using the ordinary selection path.

Validation: shell-navigation browser tests cover closing and returning
(narrowing, a simulated minimize, the other panel taking the room, a
user-closed panel staying closed, a panel opened without room floating),
the phone list, re-selection and Back/Forward behaviour, and each
panel's focus hand-off; drawer-width covers a 1024x768 tablet rotating
twice. All fail on the previous code. Help is updated in all ten
languages.
2026-10-25 21:52:03 +01:00
heunghingwan
c977ec67cb
test(ui): shard browser CI four ways and give every fixture an owned server (#1139)
The `tests` job spent about two of its 2.2 hours in `test:browser`: more
than 140 fixture files, each starting its own Vite dev server, run one at
a time, many without the protected-teardown wiring or sharing the
checkout's default optimizer cache.

Every Vite-hosting browser test now uses `createFixtureCache` and
`createFixtureShutdown`, and a static `fixture-wiring` test fails when a
file hosts a Vite server without that wiring, launches Chromium without
the navigation budget, or imports a helper through a specifier that does
not resolve. `armFixtureNavigationBudget` lets cold page loads take up
to 120 s while action timeouts keep Playwright's defaults.

Browser rendering regressions move out of `tests` into a four-way
`tests-browser` matrix: each shard runs an interleaved slice serially
(`tests/browser/shard.mjs`), and `build` waits for every shard. Locally,
`npm run test:browser` still runs one file at a time, since the Git
fixtures rebuild one shared Monaco asset directory. The leftover
`docs/superpowers/` spec is removed.

Validation: the four shards took 26, 32, 37 and 25 minutes and `tests`
12 minutes in the last completed CI run, against about 2 h 14 for
`tests` with the browser step; the wiring, navigation-budget and
build-order tests pass, and a 20-file slice of heavy fixtures passed
391/391.
2026-10-25 20:38:46 +01:00
heunghingwan
13450749b8
feat(preview): open loopback conversation links in the web preview (#1119)
Some checks are pending
Publish help / build (push) Waiting to run
Publish help / deploy (push) Blocked by required conditions
When the frontend is reached from a browser, an agent-printed
`http://localhost:3000` link points at the browser's machine, not the
backend host that runs the dev server, so clicking it opens a dead tab.
Many development apps also break inside the opaque preview, where
`localStorage`, `sessionStorage` and `document.cookie` throw.

A plain left click on an explicit http(s) loopback link (`localhost`, a
literal `127.x.x.x`, `[::1]`, `0.0.0.0`) in session Markdown now opens it
in that session's web preview. Middle and modifier clicks, external
links and Markdown rendered without a session keep the browser default.
Interception waits until the server meta is known and stays off where
previews are unavailable, such as Remote Control while the host keeps
web previews off. Intercepted links without an author title get a
localized tooltip naming the Ctrl/⌘-click and middle-click escape hatch.

The preview runtime bridge gives opaque documents whose storage access
throws one stable in-memory `localStorage`, `sessionStorage` and
`document.cookie` object each for the current page load, with property
access and enumeration, `Max-Age` taking precedence over `Expires`, and
detection by reading alone, so working storage (a per-token preview host
opened on its own) stays native. The help in all ten locales and the
preview paragraph of architecture.md describe both.

Validation: the loopback predicate unit test, the runtime-bridge shim
tests, the Markdown loopback-link browser test, the server preview,
proxy and Remote Control suites, both typechecks and the help checks
pass; the interception and the shim were checked in Chromium against
the real preview proxy and Remote Control surface.
2026-10-25 15:46:39 +01:00
Pascal André
cfd47f3af1
docs(help): state that running one-time missions continue without the backend (#1137)
## Summary

The Missions help (Lifetime and service restarts) said one-time
coordination depends on the CodeNomad backend and told users not to
expect new dispatch while it is closed. In native task mode, the
default, a running one-time mission keeps going without CodeNomad: the
coordinator, its native subagent tasks and the mission tools all run in
OpenCode.

The **CodeNomad backend** row now says so, and lists what does wait for
the backend: assigning tasks to independent sessions and delivering
their reports to the coordinator (already admitted independent sessions
keep running in OpenCode), missions the coordinator prepares, stopping a
retired task's conversations, recovery, Mission Control, and continuing
a one-time mission after an OpenCode service restart.

## Why this is accurate

- `opencode/missions/desktop-plugin.ts`: `followPresence(..., () =>
retainMissionWork(...))` keeps the Missions tools registered after
backend presence ends while a mission is active
(`missions/lifetime.ts`).
- Native declarations, reports and briefings write the plugin's own
journal. Only the transport calls cross the authenticated loopback
bridge (`sendMissionInput`): root creation, assignment prompts and
report notifications for independent tasks, `prepare`, `observeTask`,
`interruptTask`, recovery, lifecycle and cleanup
(`missions/control.ts`).
- `desktop-plugin-presence.ts` registers the tools only with presence,
so after an OpenCode restart they return once CodeNomad is back.

## Changes

- `docs/help/<locale>/missions.md`: the **CodeNomad backend** row in
English and the nine translations, reusing each locale's existing terms.

## Validation

- `node scripts/test-help.mjs`: full multilingual check, 9/9 pass.
2026-10-25 12:42:00 +01:00
Pascal André
4b163382bc
feat(remote-control): serve web previews to paired devices (#1131)
Remote Control blocked `/previews/*`, so a phone or browser paired through
the tunnel had no web preview at all: the entry was hidden and the agent's
dev server stayed out of reach. Previews now work there, behind a host
setting that is on by default.

A preview's sandboxed document sends neither the device cookie nor a usable
`Origin`, so the Remote Control gate admits a live `/previews/<token>/…`
capability on its token alone, as local listeners already do, while the
host's Web previews switch is on. The ingress, which refused every
WebSocket, now admits preview upgrades only: `admitRemoteUpgrade()` parses
the target exactly as the preview proxy does and the manager hands the
socket to the application's path-preview upgrade handler, never to the
shared upgrade event (so `/previews/<token>/../../sidecars/…` cannot reach
another handler). Every other upgrade still gets 404.

The switch lives in Settings → Remote Access → Remote Control. Only the
host can change it (`PUT /api/remote-control/previews`); the registry
stores only the opt-out. Turning it off refuses later preview requests,
including the Referer-based root-relative asset fallback, and closes live
preview WebSockets; remote `/api/meta` then reports `previewsUnavailable`,
which hides Open web preview on paired devices.

Exposing previews through a public tunnel makes their address a bearer
credential, so path-proxied preview responses now carry
`Content-Security-Policy: sandbox` with the iframe's own flags (one shared
constant, `previews/sandbox.ts`) and `Referrer-Policy: same-origin`, on
every content type. A preview document opened outside its iframe stays an
opaque origin instead of running with CodeNomad's origin and device cookie,
and the capability path stays out of Referer headers to other sites. Such a
standalone document then behaves like the preview iframe: its root-relative
resources rely on the runtime bridge's path rewriting, without the Referer
fallback. The per-token preview.localhost host keeps its own origin and
headers.

Help (Remote Control, mobile and client overview pages, all ten locales),
dev-docs/REMOTE_CONTROL.md, architecture.md and UI_SHELL.md describe the
behavior; the switch label and caption are translated in all locales.

Validation: a Remote Control surface test drives the real application
through the ingress: a preview page and an `Origin: null` form post work
without a device cookie and carry the sandbox and referrer headers, unknown
tokens and non-preview or dot-segment upgrades get 404, a preview WebSocket
upgrades, a paired device cannot change the switch, and turning it off
closes the live WebSocket, refuses the preview and its Referer asset
fallback, and flags the metadata.
Registry, metadata and proxy tests cover persistence of the opt-out, the
flag and the headers per entry point; browser tests cover the switch and
the conversation header with previews on and off.
2026-10-24 20:25:25 +02:00
Pascal André
036a008c2c
fix(ui): restore the web preview entry on mobile, hide it through Remote Control (#1128)
Phones lost their only entry to the session web preview when the
conversation actions were reorganized: on mobile, the Full screen action
took the preview's place instead of joining it. Previews still work there
through direct (LAN) access, which serves them through the capability
iframe proxy. Remote Control, on the other hand, serves no preview content
(`/previews/*` is host-only), so its clients, desktop browsers included,
were offered a preview that could only fail to load.

The conversation header now offers the preview on every platform, inline
and in its overflow menu, with Full screen added on mobile. `/api/meta`
reports `remoteControl: true` for requests that arrived through the Remote
Control ingress, `lib/server-meta.ts` exposes it as `remoteControlClient()`,
and the shell hides the preview entry for those clients. The shell asks for
the server metadata on mount; the read is cached and shared with the other
callers, and the entry stays visible until a Remote Control answer arrives.

The Remote Control help page says, in all ten locales, that conversation
actions hide Open web preview there; dev-docs/UI_SHELL.md records the
convention.

Validation: a header-windows browser test drives the real conversation
header on a Pixel 5 profile and on desktop Chrome, with and without the
Remote Control metadata, and checks the inline buttons and the overflow
menu. It fails on the previous shell (no preview entry on the phone, and a
preview entry kept through Remote Control). meta.test.ts and the Remote
Control surface test cover the flag locally and through the real ingress
(the surface fixture's metadata gains the `host` field /api/meta needs).
The UI unit tests run by CI, the server routes, proxy and Remote Control
suites, the help checks and both typechecks pass.
2026-10-24 15:07:18 +02:00
Pascal André
39c0fa5833
chore(opencode): qualify and recommend runtime 2.1.4 (#1126)
## Summary

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

## Contract review (v2.1.3 → v2.1.4, 72 commits)

- **Authenticated OpenAPI:** 121 → **124 paths**, 146 → **149
operations**. No operation changed. The changes:
  - added `POST /api/experimental/client-mcp/token`;
  - added `GET /api/experimental/client-mcp` (WebSocket);
  - added `POST /api/debug/heap-dump`;
- `Config.InfoEncoded` gains the opt-in
`experimental.background_compaction`.

The session event (`V2EventEncoded`) and message schemas are unchanged.
None of the new routes is on the instance proxy allowlist, and the proxy
relays WebSockets only for SideCars and previews.
- **Published declarations:**
  - the client adds `mcp.client.token` and `debug.heapDump`;
- the schema and plugin changes are type refactors and unused-export
removals, among them `SessionID.descending`, `SessionEvent.Source` and
`PromptInput.FileAttachment.create`; `Session.Message.ID.fromEvent` now
takes any string instead of an event ID. CodeNomad uses none of these,
and every host module its native plugin bundles import
(`@opencode/plugin`, `/effect`, `/promise/adapter`, `/rpc`,
`@opencode/schema/location`, `/session`) still exists.
- **MCP elicitation forms**
([#58184](https://github.com/anomalyco/opencode/pull/58184)): an MCP
server asking for input during a tool call now creates a global-session
Form whose `form.created` event carries the server's location.
CodeNomad's live path requires that location for global Forms and shows
them as a **Project request**. Review found that the live path dropped
the location from the queued Form, so its reply lacked
`x-opencode-directory` and failed for a worktree or subdirectory until a
pending-request rescan. **#1127** fixes that; recovery already kept it.

A native probe (minimal stdio MCP server, a real Code Mode `execute`
call, CodeNomad-style reply) gives:

| Runtime | Form listed for the location | `form.created` carries the
location | Reply completes the tool |
  | --- | --- | --- | --- |
  | 2.1.3 | yes | **no** | yes |
  | 2.1.4 | yes | **yes** | yes (`action: accept`, `channel: stable`) |

On 2.1.3 such a request only appeared after a pending-request rescan; on
2.1.4 it appears live. The probe sends the location header itself, so it
checks the runtime contract; #1127 covers CodeNomad's live reply path.
- **Other changes** are covered by the native suites below:
- compaction (oldest-first shortening, ignored buffers, overflow
detection);
  - the agent's model for sessions without one;
  - V2 agent files;
  - V1 global-session import attribution;
  - MCP lifecycle fixes.
- **Live check:** this machine's shared daemon runs 2.1.4, and the
CodeNomad desktop session works through it.

## Isolated Windows qualification

Base: dev `9ac792fff`; Node **24.20.0**; exact official CLI **2.1.4**;
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 (10/10):**
- history from **2.0.3, beta-19271, 2.0.32, 2.1.0, 2.1.1, 2.1.2 and
2.1.3** to 2.1.4;
  - Git degraded/init;
  - temporary workspaces.
- **Installation (3/3), with the 2.1.4 recommendation:**
  - fresh install of the 2.1.2 minimum;
- native **2.0.15 → 2.1.4** upgrade, after which the already-running
daemon keeps its PID and 2.0.15 version;
  - explicit OpenCode **1.18.10 → 2.1.4** replacement.
- **After the recommendation change:**
- admission, transport, update, installation and instance proxy units
**116 passed, 1 POSIX-only skipped**;
  - full server **2,471 passed, 8 skipped**, no failure or cancellation;
  - UI units **1,699/1,699**;
  - 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 (**9/9**).

This is Windows automation evidence. In CI, `latest` resolves to 2.1.4:
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-24 13:34:36 +02:00
Pascal André
5994944f84
feat(settings): manage desktop data profiles and drop the startup question (#1123)
Choosing a desktop data profile needed `CODENOMAD_PROFILE` or, on a first
packaged launch with several restorable profiles, a native question that
blocked startup. Settings only named a non-default profile and offered to
delete all the other profiles at once.

Settings > General > Restore previous state now ends with one folded line,
"Data profile: <name>". Unfolded, it lists every profile on the computer,
the open one first and the others by last use, each with its size, creation
date and last use. A radio chooses the profile the next launch opens (any
profile of this configuration; choosing the open one again cancels), "New
profile" creates one by name, "Restart in <name>" restarts into the choice,
and each other profile has its own delete button with a confirmation. A
launch pinned by `CODENOMAD_PROFILE`, its alias or a development run shows
the profiles without letting the user choose; deletion still works.

Without a remembered choice, a packaged launch no longer asks: it opens the
most recently used of the default, `dev` and `dev-v2` profiles (ties go in
that order) and remembers it, as it already did with zero or one candidate.
A launch that finds the selection lock held opens the detected profile
without remembering it instead of waiting.

Implementation: the existing host listing gains the open profile, creation
(folder birth time, links not followed) and last-use (newest client-state
write) dates, the remembered next-launch profile and whether the launch is
pinned. Two host operations are added on both hosts, granted to local and
Preferences windows only and serialized with list and delete: choosing the
next launch's profile (validated with the `CODENOMAD_PROFILE` grammar,
written under the selection lock, overwriting this configuration's entry,
creating a new profile's scope folder inside the lock and removing it again
if the write fails) and restarting through the graceful developer-mode
relaunch. Electron's throwaway question storage, relaunch and private
hand-offs are removed; it still sweeps leftover question folders and drops
the two hand-off variables an older build's relaunch can carry. Tauri keeps
only the invalid-profile error dialog. `CODENOMAD_DESKTOP_PROFILE` and the
server's `desktopProfile` meta field are removed, since nothing displays them.

Only a profile whose name leads back to its own folder can be chosen, so a
folder the deprecated alias made under another profile's name (such as
default-<hash>) is listed under its stored name but not offered. The
listing reports the next launch's profile only when the launch would open
it (the launch's own remembered-profile check), and Restart in <name> lists
again first and restarts only while that still names the shown profile.
Choosing with the keyboard keeps focus on the chosen radio.

Validation: UI browser tests for the manager (listing, choose and restart,
new profile and name validation, single deletion, macOS note, pinned
launches, plain browser), Electron native tests (listing dates, current,
next launch, pinned, set-next-launch outcomes, relaunch wiring, preload,
automatic choice, lock and sweep) and Tauri tests (the same cases, grants
and command wiring). Developer doc and help (10 languages) updated.
2026-10-24 09:46:30 +02:00
Pascal André
628a80a5c9
feat(settings): give Updates its own card and the data profile a Startup row (#1122)
Some checks are pending
Publish help / build (push) Waiting to run
Publish help / deploy (push) Blocked by required conditions
## Summary

- **Updates** is its own card again (About → Updates → Diagnostics →
connectivity), subtitle restored in 10 languages: latest version,
install/download + Refresh, then the **update feed** with its help and
save error last. Rows resuming after actions open with their separator.
Help speaks of the Updates card again (10 locales).
- **Data profile** moves from About to General's startup card (Restore
previous state), above Other saved profiles; still only for an explicit
non-default profile (`current-profile-settings-row.tsx`, shared by
inline settings and Preferences). Label key moved in 10 languages;
desktop help and `DESKTOP_DATA_PROFILES.md` point to the new place.
- The Clear saved startup state button keeps space above the rows'
separator.

## Validation

- `server-info`: card order, feed out of About, after the actions, with
its separator; no data profile in Info.
- `other-profiles`: running profile named above other saved profiles,
clear of the button, Electron and Tauri Preferences; default profile
never named.
- `desktop-updates`, `npm run test:help`, UI typecheck.
2026-10-24 08:44:52 +02:00
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é
019203e31e
feat(notifications): show Clear all and Settings as icons in the history (#1107)
## Summary

- The notification history's **Clear all** and **Settings** text buttons
become icon buttons (red trash can, gear), like the panel's close
button.
- Their labels stay as tooltips (`title`) and accessible names
(`aria-label`), with the existing translations. **Mark all as read**
keeps its text.
- Shared `window-icon-button` geometry; the existing toast-history
action classes keep hover, danger and disabled styling.

## Validation

- Screenshots of the real `ToastHistoryPanel` with two notifications,
dark and light themes, including the hover state.
- `npm run typecheck --workspace @codenomad/ui`.
2026-10-23 02:19:35 +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é
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é
0e5dc954bc
docs(help): describe the macOS Not Opened block and the Open Anyway route (#1094)
## Problem

The **macOS quarantine** troubleshooting section said that macOS "can
report that the application is damaged". On a macOS 15.8 Intel VM, the
`1.0.0-dev-20261022` Tauri and Electron apps behave differently once
downloaded with a quarantine attribute:

- both are rejected at first launch with **"CodeNomad" Not Opened**:
Apple could not verify "CodeNomad" is free of malware… The only choices
are Done or Move to Trash;
- `spctl` rejects both bundles: Electron is only ad-hoc signed, Tauri is
unsigned, and neither is notarized.

The section also limited the **System Settings → Privacy & Security**
step to Intel Macs and did not name the actual action.

## Change

Ten locales, `docs/help/<locale>/troubleshooting.md`, with headings
unchanged:

- The introduction states that the macOS releases are not notarized. A
downloaded copy is therefore blocked at first launch with **"CodeNomad"
Not Opened**, or reported as damaged.
- Step 3: for the **Not Opened** message, **Open Anyway** for CodeNomad
in **System Settings → Privacy & Security**, confirmed with an
administrator password, is an alternative to `xattr`. The Intel-only
wording is gone.
- macOS labels stay in English in every locale, as in the existing
translations.

## Verification

- On the VM, after a blocked launch, Privacy & Security showed
**"CodeNomad" was blocked to protect your Mac.** with **Open Anyway**. I
did not click it, since it needs the user's password.
- The documented `xattr -dr com.apple.quarantine
/Applications/CodeNomad.app` let both apps start.
- `npm run test:help`: 9/9.
2026-10-22 13:49:24 +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é
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é
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
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é
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é
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é
88532c23bd
feat(composer): queue or send now from the More actions menu, Alt+Enter by default (#1054)
The composer's More actions menu now offers the follow-up delivery Enter does not use: Queue message when Enter steers, Steer now when Enter queues. It is enabled only in normal mode while the session runs and a draft can be sent, and it goes through the same delivery resolution as the shortcut, so an idle session always sends normally.

Alt+Enter becomes the default alternate-send shortcut on Windows and Linux, with Ctrl+Shift+Enter kept as a second binding. On macOS Option+Return is the native line break in text fields, so only Cmd+Shift+Return sends there; the platform check lives once in hotkey-definitions. The file/command picker now ignores any modified Enter, so Alt+Enter never selects the highlighted item. The menu tooltip shows the effective shortcuts, the help tables note Alt+Enter is for Windows and Linux, and the menu rebuilds only when its enabled states flip.

Validation: composer-alternate-send and composer-picker-enter browser tests (including Alt+Enter with the picker open), hotkey-bindings unit tests for macOS and other defaults, usePromptKeyDown, submitPrompt, hotkeys-settings, help test and UI typecheck pass.
2026-10-21 01:05:23 +02:00
Pascal André
46208ad902
fix(settings): adopt a live window for Preferences startup state and spin the Stats refresh (#1050)
Native Preferences showed "The CodeNomad window that opened Preferences is no longer available" with disabled startup controls whenever its opener was unknown (Preferences restored at launch) or closed, even with CodeNomad windows open. Startup state (restore previous state, saved snapshot, Clear) is each window's own, so the host now adopts the most recently used local window as the opener in that case: Electron through PreferencesWindowRegistry.adoptOpener (startupStateTargetId), Tauri through set_opener (startup_state_target). The epoch rotates on adoption, so a change prepared for another window degrades to a read of the adopted one before any write. The warning and its string in all ten locales are removed; the card re-reads up to three times while the adopted window starts and stays disabled only when no local window can answer. Code contracts, AGENTS.md and the public help in all ten locales describe the opener, otherwise the most recently used window.

Stats' text Refresh button becomes the shared icon button sized like the period select, keeping its localized accessible name and tooltip; the icon spins while a read runs.

Validation: Electron unit tests for live/closed/unknown openers, adoption, epoch rotation and fenced changes; Tauri unit test for adoption and fencing; preferences-startup-state (Electron and Tauri hosts), other-profiles and service-usage browser tests; UI and Electron typechecks; help test; rustfmt check. Independent review: the first pass found that startup state is per window and that the fence no longer followed the target; the adoption fix was re-reviewed and approved, with two trivial notes applied.
2026-10-20 23:34:52 +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é
04e7aae1db
feat(transcript): add a Nothing preset and a preset menu in Message content (#1043)
Transcript presets are now Nothing, Minimal, Balanced and Everything. Nothing hides every message-content row, including all registered, historical and unknown tools, thinking, diagnostics, tool inputs, tokens and system messages. Detailed is retired: saved Detailed choices load as Custom with Detailed's effective tool column (only read, skill and invalid collapsed, other tools expanded) and the same legacy thinking fallback, so existing transcripts keep their appearance.

The Message content popup gains a native select under its title with the four presets. A disabled Custom entry appears only after a manual row change, and the saved preference always drives the shown value, so refused or failed saves never leave a stale choice. Escape closes an open list before the popup. Preset recognition and patches move from Chat settings into the shared transcript-visibility module used by both surfaces; the tool-output command reuses the shared Other-tools resolution.

Preset names and descriptions were English in most locales; they are now translated in all ten, along with the accent-less French, German and Spanish card strings. The public help lists the new localized presets.

Validation: new transcript-presets browser test (popup order, choices, Custom after a manual change, pointer choice keeps the popup, Escape order, French labels) plus existing status-tokens tests; unit tests for preset round-trips, Nothing visibility and the Detailed migration; behavior-registry tests; help test; key parity across locales; UI typecheck with the pinned 2.1.0 client. Independent review approved with no P0-P2 findings; its two trivial P3 notes were applied.
2026-10-20 19:56:05 +02:00
Pascal André
7a2ee897ec
fix(projects): keep the new-tab launcher project-only (#1039)
## Summary
Remove the three bottom actions (Servers, SideCar, Full home) from the
project-tab plus launcher. Also remove its server disclosure, profile
hook/loading, local expansion state, server actions/dialog binding,
unused props/icons/CSS and four keys in all locales. This is not just
hidden chrome.

Recent search/list, Browse, Temporary and Clone remain. A normal plus
click still opens home. All long-press/context/keyboard gestures and
Escape focus remain. Home keeps remote server and SideCar functionality;
shared server setup is mounted only for its caller.

## Validation
UI typecheck passed. Real new-instance-launcher browser suite 8/8:
retained actions/gestures/focus, no server requests from launcher even
with saved profiles, home remote/SideCar controls retained.
Desktop/narrow captures inspected. git diff --check passed. No
app/daemon/state touched.

## Scope
48 files (+110/-190), largely 10-locale help and removed translations.
App.tsx remains oversized (~915 lines); only obsolete launcher props
changed.
2026-10-20 18:44:36 +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é
1f132dbb32
docs(help): list Remote Control before direct access (#990)
Some checks are pending
Publish help / build (push) Waiting to run
Publish help / deploy (push) Blocked by required conditions
Reorder the shared help navigation to present Remote Control before direct access in all ten locales. Keep existing Settings behavior unchanged: Direct access starts collapsed locally and opens automatically when the server listens on the network. Update the help ordering regression. Final targeted review found zero actionable findings; all nine help tests pass, covering 25 pages across ten locales.
2026-10-17 13:27:43 +02:00
Pascal André
db6685cb55
fix(workspaces): closing a project tab closes the project in CodeNomad (#970)
Some checks failed
Publish help / build (push) Has been cancelled
Publish help / deploy (push) Has been cancelled
## Problem

Closing a project tab only hid it in that window. The backend workspace
record kept routing the folder's events, notifications, auto-accept and
pending-request recovery, invisibly, until the palette-only **Stop
Workspace** command was run on the active tab. That command still showed
the V1 message "Stop OpenCode instance? This will stop the server."
although V2 has one shared OpenCode service, and it called
`debug.location.evict`, which (per OpenCode source) rejects pending
permissions, cancels pending Forms, fails plugin RPC and kills terminals
of that directory, even for sessions started elsewhere. OpenCode loads
locations on demand and unloads idle ones itself.

## Change

- **Closing a project tab** (×, Ctrl/Cmd+W) removes that tab's backend
workspace record. CodeNomad stops following it; sessions keep running in
OpenCode and reappear when the project is reopened.
- **Another tab of the same folder** has its own record and is
unaffected.
- **The same record shown in another window**: that window's tab closes
through the existing `workspace.stopped` broadcast, with an info
notification ("closed from another window"); the initiating window shows
nothing.
- **CodeNomad never evicts OpenCode locations**: eviction, its
serialization helpers and `locationOwned` bookkeeping are removed. A
failed launch now always removes its logical record (the retention only
existed to retry a failed eviction).
- **Stop Workspace removed** (command, confirm dialog, i18n keys in all
locales).
- Unchanged: opening an open project still offers a new tab (recent row)
or switching (switch button); temporary projects keep Keep / Discard /
Cancel; window close, app quit, reload, restore and backend shutdown
delete nothing; disconnected-instance dismissal stays local.
- Delete failure: error toast, record kept and reachable from the home
page, no automatic retry.

## Docs

Help in all 10 locales (`projects`, `index`, `desktop`, `server`,
`opencode`, `missions`), `AGENTS.md`, `CONTRIBUTING.md`,
`dev-docs/architecture.md`, `packages/server/README.md` and the
architecture-guide skill now describe the new contract.

## Validation

- UI store tests (`--conditions=browser`): app-tabs,
temporary-instances, workspace-list-reconciliation-fence,
instances-restore-ownership — 30/30.
- Server: manager + shared service 54/54; event ownership and workspace
identity suites pass.
- New real-component browser regression (click ×, Ctrl+W, Cmd+W): 3/3;
project restoration (Electron/Tauri) and new-instance launcher browser
suites pass.
- `npm run test:help` 9/9 (250 pages); UI, server and Electron
typechecks.
2026-10-15 15:14:29 +02:00
Pascal André
de44f7b29c
fix(pending): name the cause, folders and sessions in the recovery warning (#965)
The "Question and permission recovery is incomplete" warning now says
which project it concerns, why recovery failed, which folders failed and
which sessions made them required, instead of a fixed sentence.

## Why
- The previous warning grouped every possible cause into one fixed text
and named none of them, including unrelated failures such as the initial
workspace load.
- Six earlier fixes each removed one measured cause, but every new cause
produced the same text, so it could only be diagnosed by live captures.
- With several projects open, it did not even say which project was
affected.

## Example
Before:
> Question and permission recovery is incomplete. Known requests are
retained; inactive worktrees are not loaded.

Now, for the live case fixed by #964:
> **CodeNomad: questions and permissions not fully recovered**
> Outside this project: prompt-replay-yield. Affected sessions: ISSUES
Codenomad.

## Change
- **Server:** every `error` directory in the pending snapshot carries a
`reason`:
- `not-owned`, `deleting`, `missing` (an owned folder that no longer
exists), `unverified` (Git/filesystem/WSL translation could not verify
it) and `changed` (ownership or repository changed during the read);
- a native placement outside the workspace is reported as `not-owned`,
and a deletion fence raised during the read as `deleting`.
- **UI, `stores/pending-recovery-cause.ts`:** a small module turns
failures into a structured cause:
- folder issues, plus client-side `timeout`, `unavailable` (with the
HTTP status when known) and `excluded` (a required folder reported as
history);
- the plugin being unavailable, repeated supersession, an initial load
failure and a generic request failure.
- **Messages:**
- folder issues are grouped by cause, named by folder basename, and
capped at three with a remainder;
- only the sessions that made a folder required (active, or holding a
request) are named;
- the title uses the same label as the project tab
(`lib/instance-label.ts`, shared with `instance-tab.tsx`).
- **Diagnostics:** the structured cause is also logged at warn level.
- **Unchanged:** the two-attempt threshold, once-per-episode
notification, re-arming on success, retained queues and deferral
handling.
- **i18n:** the generic key is replaced by the cause keys in all 10
locales.

## Tests
- Server: `pending-requests.test.ts` 40/40; every error assertion now
names its reason.
- UI:
- `instances-pending-requests` passes, including the live scenario: an
active session in a folder reported `not-owned` produces "Outside this
project: prompt-replay-yield. Affected sessions: ISSUES Codenomad.",
while an idle session in the same folder is not named. A timed-out batch
names each folder.
  - The unavailable plugin is named.
- New `pending-recovery-cause.test.ts` covers grouping, caps, the detail
variants, request classification and failure combination.
  - 39/39 together with the compaction and permission lifecycle suites.
- i18n tests and both typechecks are clean.
2026-10-15 09:17:31 +02:00
Pascal André
1a9f202420
feat(desktop): localize native menus and open About in Settings → Info (#963)
## What
- **Native menus follow the UI locale** (Electron and Tauri): top-level
menus and every item of the app, File, Edit, View, Window and Help
menus. Previously only the custom titlebar's top level and the View
panel items were translated.
- **About CodeNomad opens Settings → Info** (the About card with
version, updates and diagnostics) instead of the bare native panel. It
runs a new `open-about` command, also listed in the command palette.

## How
- Renderer translates `NATIVE_MENU_LABEL_KEYS`
(`lib/native/menu-labels.ts`) and sends them as `menuLabels` with the
existing View menu state for the focused window; older hosts ignore the
field.
- Hosts: English until a renderer reports labels; the last locale is
kept for windows without one (loading/sign-in); labels validated (known
keys, non-empty, ≤ 200 chars); `&` escaped outside macOS.
- **Electron**: rebuilds its menu when labels change; role items get
explicit labels; English defaults come from the English messages
(`menu-labels-en.ts`).
- **Tauri**: every translatable item (including predefined Edit/Window
items) is built explicitly, retained and retitled in place on the main
thread, outside our mutexes. `menu_labels.rs` defaults are checked
against the English messages by `menu-labels.test.ts`.
- About fallback when no application window can receive the action:
Electron's native panel, Tauri a short version dialog.
- Help pages now name menu items in each page's language (Help →
CodeNomad Help, Get Updates, New Window, Zoom).
- i18n: 30 `window.menu.native.*` keys + 3 `commands.openAbout.*` keys
in all ten locales; existing New Instance / Command Palette translations
reused.

## Validation
- Electron `view-menu.test.ts` + `menu-target.test.ts`: 3/3 (locale
change, per-window labels, kept locale, invalid labels, `&` escaping).
- Tauri `cargo test -- menu`: 25/25 (label resolution/validation +
existing menu tests).
- `menu-labels.test.ts`: every locale complete; Tauri defaults identical
to English messages.
- `help-command.test.ts`: 5/5 — About opens native Preferences on Info
locally, embedded Info remotely.
- `npm run test:help`: 9/9.
- Typecheck: UI only the pre-existing `help-url.test.ts` error; Electron
only pre-existing `Array.at` lib errors.
- **Not run locally**: the real-Electron menu suite
(`view-menu-electron.test.ts`, Electron not installed here) and a
running Tauri build.

`packages/tauri-app/src-tauri/src/main.rs` is already oversized (~2,700
lines); only `build_menu` and the About handler changed.
2026-10-15 00:42:23 +02:00
Pascal André
d09d1222fa
feat(ui): merge Updates into About and move connectivity last in Info (#962)
Settings → Info showed the CodeNomad version three times: as the UI version
in About, as the server version at the top of the connectivity card, and
again as the server version in the separate Updates card. The network card,
mostly useful for remote-access troubleshooting, also sat between About and
the version/update information most users look for.

About now opens with a single Version row (the server version, else the UI
version). A separate UI version row appears only when the served UI differs
from the server, the one case where both matter. The former Updates card
becomes an Updates section of About, with the same feed selector, latest
version, help text, install/download and Refresh actions, without its own
copy of the version. Diagnostics follows, and the connectivity card moves
last without its version row; its server OS, architecture, listener and
address details are unchanged. A "Version" label is added in all ten
locales and the unused Updates subtitle removed. The Updates help page now
describes a section rather than a card in every locale.

The diagnostic report, bug report environment and update behaviour are
unchanged; only the presentation moves.

Validation: a new browser case checks the card order (About, Diagnostics,
connectivity), the Updates section and feed inside About, that the version
appears once, and that a differing UI version adds its own row. Info and
desktop-update browser suites pass 26/26, diagnostics/client-OS/bug-report
unit suites 19/19, and npm run test:help 9/9. ES2022/DOM typecheck reports
only the pre-existing help-url.test.ts error from #859. Two independent
COMMENT gatekeeper rounds; the round-1 P3 (help still describing an Updates
card) is fixed and the final head reports zero actionable P0-P3 findings.
2026-10-15 00:11:02 +02:00
Pascal André
c0d53f13d4
docs(help): multilingual help site with in-app Help menu and palette entry (#859)
## Summary

A complete public CodeNomad help site in the app's ten languages,
published on GitHub Pages (https://neuralnomadsai.github.io/CodeNomad/)
and opened from the app.

- **Content:** 25 pages in 8 sections (Start, Workflows, OpenCode setup,
Remote access, Customize, Extensions, Desktop, Help), written for
OpenCode power users and covering the current version only. It includes
everything user-relevant from `README.md`, `packages/server/README.md`
(server, network, auth, TLS, Remote Control) and the user-facing
dev-docs, with flags, defaults, labels and safety boundaries checked
against source.
- **Languages:** en, fr, de, es, he (RTL), ja, ne, ru, tr, zh-Hans.
Interface labels reuse each locale's own app strings; native desktop
menu names, code, commands, flags, paths and anchors stay unchanged.
- **Site:** `scripts/build-help.mjs` renders
`docs/help/<locale>/<slug>.md` with translated chrome
(`docs/help/i18n/`), stable `{#id}` anchors identical across locales,
same-article language links with hreflang, a root language chooser, and
a first-party offline per-locale search (Ctrl/⌘+K, substring matching
for CJK, Devanagari and Hebrew, hidden without JS). No analytics or
remote resources.
- **Validation:** `npm run test:help` fails on any missing page or
string, heading drift, broken link/anchor/asset or invalid search
target. `.github/workflows/help-pages.yml` validates PRs and deploys
only from `dev`.
- **App entry points:** one `open-help` command for the palette and a
new **CodeNomad Help** item at the top of the Electron and Tauri Help
menus. It opens the help in the invoking window's UI language through
the existing external opener, falls back to the root URL for windows
without the handler, and reports failures without replay.
- Documents the strict boolean environment-variable rule from #959 and
the updater entry points from #960.

## Review

Delivered as a CodeNomad mission with independent review loops, each to
zero findings:
- English content audit (5 passes, 14 fixes) and technical gatekeeper (2
passes) before translation.
- After translation: QA, UX review, and a final multilingual gatekeeper
(35 translation/label findings fixed, then verified), plus a visual fix
for the search focus ring and a verified sync with #960.

## Validation

- `npm run test:help`: 9/9 (25 pages × 10 locales, strict, including
negative tests); `npm run build:help`: 250 pages.
- `node scripts/test-help-browser.mjs`: 44/44; Playwright: 500 page
visits at 390 and 1280 px, all locales, no overflow, no external
requests.
- UI help-command and desktop-updates browser tests 19/19; help URL unit
tests; Electron menu tests; UI and Electron typechecks; Tauri `cargo
test` 272/272.
2026-10-14 22:44:56 +02:00
Pascal André
4e0b62bc7f
build: ship the Linux deb from Electron and the Windows installer without a ZIP (#955)
## Summary

One download per need, with fewer duplicate or unused assets:

| | Before | After |
|---|---|---|
| **Linux Electron** | portable `tar.gz` | **`.deb`** |
| **Linux Tauri** | `.deb` + AppImage | **AppImage** (self-updating) |
| **Windows Tauri** | ZIP wrapping the installer + the same installer as
`.exe` | **`.exe`** only |
| **Updater signatures** | `latest.json` + separate `.sig` assets |
**`latest.json` only** |

### Linux: Electron deb, Tauri AppImage
- The Electron deb installs as root, so electron-builder's post-install
script gives Chromium's `chrome-sandbox` the `root:root` ownership and
`4755` mode that a user-extracted archive cannot. Electron starts **with
its sandbox** and without any manual step. This fixes #541 properly.
- The package is named `codenomad` and **conflicts with / replaces** the
former Tauri deb (`code-nomad`). Both ship
`/usr/share/applications/CodeNomad.desktop`, so without this, installing
over an existing Tauri deb would fail on the shared file.
- The deb declares Electron's full runtime closure. electron-builder's
default `Depends` omit `libgbm1` and ALSA, which previously arrived only
through Recommends. ALSA is declared as `libasound2t64 | libasound2`,
because `libasound2` is a multi-provider virtual package on Ubuntu
24.04.
- Debian versions: dev builds (`<stable>-dev-<date>-<sha>`) become
`<stable>+dev.<date>.<HHMMSS commit time>.<sha>`, so they install as
upgrades after their stable base and in order on the same day; other
prereleases keep electron-builder's `~`, which sorts before the release.
The workflow rewrites the control `Version` with `dpkg-deb` when it
differs; PR and stable builds rewrite a copy to a synthetic dev version
and check that fields, files, modes, owners and control members are
preserved.
- Tauri Linux builds only the AppImage (`tauri build --bundles
appimage`).
- No unused electron-updater metadata is generated (`publish: null`).

### Windows: installer only
- The Tauri NSIS installer is published directly as
`CodeNomad-Tauri-windows-x64-<version>.exe`. It is also the signed
updater payload, so signed builds upload it once.
- The ZIP that wrapped the same installer is no longer published.
- The WinGet automation defaults now point to the `.exe`.

### Updater signatures
- The updater reads each signature from `latest.json`, never from a
`.sig` asset. Staging still verifies every signature and its trusted
comment and records it in the receipt, and publication still requires
every payload's uploaded digest to match.
- Releases no longer carry `.sig` assets, and staging deletes any left
on a rebuilt tag by older builds.

Rebuilt tags also delete obsolete Electron `tar.gz`, Tauri deb and
Windows ZIP assets.

### Docs
- **README:** shows the deb and AppImage commands. The AppImage uses the
static type2 runtime, so it needs no `libfuse2`;
`--appimage-extract-and-run` covers environments without FUSE. The
Wayland/NVIDIA command now targets the AppImage.
- **BUILD.md, `TAURI_UPDATER.md` and the WinGet guide** describe the new
formats.

## Migration note

Installing the Electron deb over a Tauri deb switches the app from Tauri
to Electron:
- `code-nomad` and `codenomad-tauri` are removed, and the launcher
becomes `CodeNomad`;
- server configuration is shared, but desktop window state is not
migrated.

Tauri deb users never had in-app updates, so the release notes should
say this. The AppImage is their Tauri option.

## Validation

- **Electron deb packaging (CI):** checks package, conflicts, replaces
and version metadata, the contents, the post-install `chmod 4755` step,
the resources/asar smoke checks and `ldd`.
- **Electron deb install (CI):** installs on Ubuntu 24.04 **without
recommends**, so `ldd` proves the declared `Depends` are enough. It
installs over a stand-in `code-nomad` package, then checks that the old
package was replaced, the owner of the desktop entry,
`/usr/bin/CodeNomad`, `chrome-sandbox` `root:root 4755` and shared
libraries.
- **Version mapping:** the mapping, the ordering against stable, later
releases and same-day builds, and both repack branches were checked with
dpkg 1.22.6 on Ubuntu 24.04. The repack keeps the control fields and
root ownership.
- **Dependencies:** every declared dependency resolves on Ubuntu 24.04,
either as a real package or through its t64 provider.
- **Updater scripts:** `updater-artifacts.test.mjs` passes, including
signature-in-manifest publication with no staged `.sig`.
- **Workflow:** the YAML parses.
- **Independent review:** no blocker. The major finding (missing deb
dependencies) and the minor ones (dev version ordering, stale BUILD.md
and README command, unchecked exit codes, unused updater metadata) are
addressed.

## Follow-ups outside this PR

- **WinGet:** the `WINGET_WINDOWS_ASSET_NAME_TEMPLATE` repository
variable still pins the `.zip` name and overrides the new default.
Before the next stable release:
  - update it to `.exe`, or delete it;
- check that the automated `winget-pkgs` PR declares a direct `nullsoft`
installer without the old ZIP fields, and correct it if not.

Until then, a stable release's WinGet job waits for the ZIP and fails
without side effects.
- **Ubuntu desktop trial:** test the Electron deb on a real Ubuntu
desktop.
2026-10-14 16:45:49 +02:00
Pascal André
d873cfd2ee
docs: refresh README and workspace screenshot for 0.20 (#782)
## Summary

- Update the main README for the shared OpenCode V2 service, current
features, managed desktop setup, minimum/recommended OpenCode versions,
and explicit service restart behavior.
- Correct release links, actual Electron/Tauri artifact formats and
architectures, Node guidance, remote authentication and SideCar
configuration fields.
- Align server and contributor documentation with the 0.20 baseline.
- Replace the old hero screenshot with a current 1600x900 workspace
rendered using real Solid components and synthetic demo data.
- Include a reproducible Chromium capture script and provenance notes;
external traffic is blocked and no user backend, daemon or provider is
used.

## Preview

![CodeNomad 0.20
workspace](https://raw.githubusercontent.com/NeuralNomadsAI/CodeNomad/docs/readme-0.20/docs/screenshots/workspace-0.20.png)

## Validation

- UI typecheck passes.
- Chromium capture passes transcript, connected-state, agent, timeline
and MCP readiness checks, with no uncaught renderer errors.
- Resulting image inspected visually.
- 20 local Markdown/image targets verified.
- Staged git diff --check passes.

Regenerate with: node packages/ui/scripts/capture-readme.mjs

Documentation and capture tooling only; CI is not being monitored.
2026-09-26 04:13:30 +02:00
Pascal André
e4cd6d6fcb
fix(release): preserve PR notes and validate WinGet access (#658)
## Summary

- prepend the merged stable release PR body to GitHub-generated release
notes
- keep nightly prerelease notes unchanged
- verify the WinGet token can create and delete branches in the
configured fork before invoking Komac
- document writable PAT and GitHub CLI OAuth token options

## Root cause

The v0.19.0 WinGet job reached Komac successfully, but
`WINGET_GITHUB_TOKEN` could not execute `CreateRef` in
`pascalandr/winget-pkgs`. The repository secret has been replaced with
the currently authenticated writable GitHub CLI token.

## Validation

- `actionlint .github/workflows/reusable-release.yml
.github/workflows/update-winget.yml`
- associated-PR lookup returned the #656 body for merge commit
`db0716e7`
- temporary branch create/delete succeeded in `pascalandr/winget-pkgs`
- `winget validate` succeeded for the v0.19.0 manifests

Manual WinGet submission:
https://github.com/microsoft/winget-pkgs/pull/423225
2026-08-29 16:02:35 +01:00