Commit graph

194 commits

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Oversized files touched:

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

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

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

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

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

## Size and upkeep

62 files, +837/-96:

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

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

## Validation

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

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

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

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

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

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

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

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

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

## Validation

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

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

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

## Fix

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

## Validation

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Review fixes.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

## Validation

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

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

Since 0.20.0, opening a SideCar panel (e.g. ttyd) shows the page but
every in-page request fails: the terminal immediately falls back to
"Press ⏎ to Reconnect". All HTTP subresource fetches
(`/sidecars/<id>/token`, …) and the WebSocket upgrade
(`/sidecars/<id>/ws`) are rejected with 401/403. Connecting to the same
service directly (no iframe sandbox) works fine.

## Root cause

The browser-frame sandbox added in 0.20.0 (`allow-scripts allow-forms
allow-modals allow-popups allow-downloads`, no `allow-same-origin`) also
applies to SideCar frames. That makes the frame an **opaque origin**, so
the browser sends its in-page requests with `Origin: null`, cross-site,
and **without the `SameSite=Lax` session cookie**:

1. `sandbox` lacks `allow-same-origin` → iframe document is an opaque
origin
2. In-page fetch/WS requests become cross-site (`Origin: null`, no
cookies)
3. `codenomad_session` is `SameSite=Lax` → never attached
4. Server enforces session auth on `/sidecars/*` → **401**; the
opaque-origin gate additionally answers **403** (`http-server.ts`,
"Opaque origins may access preview capabilities only")

Only the frame's *subsequent* requests fail — the initial navigation is
issued by the top-level page (same-origin, with cookies), which is why
the panel visibly loads before disconnecting. Browser previews already
solve this with their token-scoped capability path; SideCars have no
such path and depend on the session cookie, so their frames must stay
same-origin.

## Change

Opt-in relaxation, scoped to user-configured app-proxied content only:

- `getBrowserFramePolicy(environment, { trustedSameOrigin })` appends
`allow-same-origin` to the sandbox only when explicitly requested
(`browser-frame-security.ts`)
- `BrowserFrame` gains a documented `trustedSameOrigin` prop; arbitrary
web previews must never set it
- `SideCarView` passes `trustedSameOrigin` — restoring pre-0.20 behavior
for content the user explicitly configured (a local service on
`127.0.0.1` reached through the app's own authenticated proxy)

Security posture is otherwise unchanged: the server-side gates stay
exactly as strict (the new server tests pin this), and browser preview
frames keep the full sandbox.

`allow-scripts` + `allow-same-origin` together do let SideCar content
script its same-origin parent — the same trust level SideCars had before
0.20.0 for services the user deliberately added. A follow-up can remove
even that by issuing short-lived tokens in the iframe URL (see below).

## Tests

- **Unit** (`browser-frame-security.test.ts`): trusted SideCar policy
includes `allow-same-origin`; existing tests still assert the default
policy excludes it
- **Browser** (`tests/browser/sidecar-sandbox.test.ts`, new): asserts in
a real Chromium DOM that the `SideCarView` iframe carries
`allow-same-origin` while a plain preview `BrowserFrame` does not
- **Server** (`server/__tests__/sidecar-proxy.test.ts`, new): WebSocket
upgrade **without cookie → 401**, **with valid session cookie → 101**
(also verifies `strip` prefixMode drops `/sidecars/:id` before
proxying); HTTP with `Origin: null` → 403, same-origin + cookie → 200 —
locking the exact contract that forces same-origin frames, so the server
gates cannot be quietly loosened instead

## Validation

- CI's runnable-UI unit list: 56/56 pass
- Targeted browser suite (`sidecar-sandbox` + full
`browser-frame-native` Electron suite) under xvfb: 20/20 pass
- Full server suite: 2286/2329 pass; all 27 failures are pre-existing
Git/gpg-signing environment failures (reproduced on an unmodified tree
via stash), none touch sidecars/auth/http-server
- `tsc --noEmit` clean for `@codenomad/ui` and the server package
- Manual: `ttyd -i 127.0.0.1 -p 7682 bash` as a port SideCar (insecure,
strip) — terminal stays connected; before the fix the token fetch was
403 and the WS handshake 401

## Follow-ups (not in this PR)

- Short-lived token in the SideCar iframe URL accepted by `/sidecars/*`
HTTP + WS auth, so even trusted sidecars could drop `allow-same-origin`
(mirrors the preview capability mechanism)
- Optional `SameSite=None; Secure` configuration for authenticated
sandboxed iframes behind HTTPS reverse proxies

---------

Co-authored-by: Pascal André <pascalandr@gmail.com>
2026-10-17 21:01:38 +02:00
Pascal André
2428947f81
refactor(chat): render the transcript header as Virtua's first row (#992)
refactor(chat): render the transcript header as Virtua's first row

The content above the transcript rows (permission receipts, older-history
controls, empty/loading/error states) used to sit outside Virtua's data, so
VirtualFollowList corrected every header resize itself: stable offsets, a
browser-clamp zone, a start margin handed to Virtua on every scroll, a
synthetic scroll event, and a correction shared with the rolling-window
anchor. Each rule exposed a new edge case, and the last ones left blank bands
when the reader sat inside the header, when a growth reverted within its frame,
and for one frame after a large shrink while following. The header is now
Virtua's row 0, ahead of the retired prefix, always mounted and created once.
Like every absolutely positioned row, its resizes move nothing until Virtua
measures them and compensates them in the same pass, so the whole header
correction and its trace reason are removed (virtual-follow-list.tsx shrinks
by 84 lines). The list keeps its own indexes and translates them to Virtua's
internally. The header is never an anchor, the active row, a restoration
target, a message or a renderItem call. A Virtualizer remount moves the header
element and restores focus inside it, and it keeps hit testing while Virtua
scrolls. A held native scrollbar thumb is now compensated like a roll: the
content stays in place and the offset moves. Near the bottom this relies on
Virtua 0.52.8+, which compensates from its recorded offset after the browser
clamps. Header tests specific to the removed correction are replaced by
painted-frame checks of the reader's position and blank bands (far from and
near the bottom, same-frame reverts, a reader inside the header, large shrinks
while following), plus focus-across-remount and hit-testing checks.
2026-10-17 16:58:26 +02:00
Pascal André
bbab05dade
chore(ui): upgrade virtua to 0.53.5 to fix near-bottom shrink over-correction (#991)
chore(ui): upgrade virtua to 0.53.5 to fix near-bottom shrink over-correction

Virtua 0.51 compensated a row shrinking above the reader with a relative
scrollBy issued after shrinking its container; near the bottom the browser
had already clamped scrollTop, so the shrink was subtracted twice and an
escaped reader within the clamp zone jumped by the shrink minus its gap
(70 px and 160 px measured for ordinary rows). Virtua 0.52.8 lands such a
jump with an absolute scrollTo when the recorded offset is at or beyond the
new end, and 0.53 keeps the same compensation rule (rows wholly above the
viewport when idle or scrolling up) through an anchor-based implementation.

The list's assumptions were reviewed against the new internals: Virtua's
ResizeObserver and scroll listener still attach at mount, before the
microtask that recreates the row observer and seeds the offset (the ref
now runs at instance creation); retired zero-height rows, startMargin,
keepMounted, item offset/size/index lookups and the CacheSnapshot layout
are unchanged, and no imperative Virtua operation is used. The cache
contract test no longer relied on the removed $estimateDefaultSize and now
pins that a carried itemSize is never re-estimated via $setItemSizes and
$isEstimating, with an estimating control.

A new browser test shrinks an ordinary row above a reader near the bottom
(gaps 0/10/40) and requires the read row to stay put on every painted
frame; it fails on 0.51 (gaps 10 and 40) and passes on 0.53.5. All
VirtualFollowList and session browser suites, UI unit tests, typecheck and
the UI build pass.
2026-10-17 16:10:05 +02:00
Pascal André
2f9ccea747
feat(history): name the conversation and worktree of workspace search results (#993)
## Why

Workspace-wide conversation search listed each hit as raw native
identifiers (`sessionId · messageId · kind`). A result could not tell
which task, worktree or branch it came from, and another conversation
could only be previewed, never opened. When the same file diverges
across parallel tasks, that context is what tells results apart.

## What changes

- Workspace results group consecutive hits by session. Each group shows
the conversation title and a badge for the worktree that currently holds
the session (its label, normally the branch).
- Raw IDs no longer appear in results.
- The other-session preview adds the worktree badge, the message's
relative date and an **Open in session** action that opens the
conversation at the hit.
- No commit is shown: a conversation message is not bound to one.

## How

- `stores/history-search-sessions.ts`: titles/locations come from the
session store, or one native `session.get` per distinct unloaded session
(older catalog pages, collapsed subsessions), at most four in flight.
Display reads never add sessions to the list, end with the results view
and are discarded on reconnect.
- Opening hydrates only that session and its parents
(`hydrateRestoredSessionChain`), stores the hit as its transcript anchor
and selects it; the destination restores the bounded window through the
existing anchor path.
- `components/history-search-results.tsx` renders the grouped list (rows
are described by their group header); `getWorktreeForDirectory` is
shared with the session list.
- i18n for all locales; styles in
`styles/components/history-search.css`; documented in
`SESSION_HISTORY_QUERIES.md` and `AGENTS.md`.

## Validation

- `tests/browser/history.test.ts` extended: grouping, worktree badges,
failed label read, one bounded read per conversation, accessible
descriptions, preview date/badge, open at anchor.
- Browser suites `history`, `header-windows`, `history-navigation`,
`worktrees`: 115/115 pass.
- UI typecheck passes.

Not covered: landing on the message inside a real SessionView after
opening (the anchor restore path itself is covered by the existing
history-navigation suite), and the installed desktop app.
2026-10-17 16:02:16 +02:00
Pascal André
3394fc7f2f
refactor(chat): finish transcript scroll migrations and fix known scroll follow-ups (#980)
Mission final workstream (scroll unification), after #971-#978.

**Migrations:** wheel intent goes through the gesture routing's single
nested-scroller decision (no second ancestor walk; SVG targets in nested
output count as consumed); touch/key intent reuse findNestedScroller;
the viewport observer uses trackHeightDelta; search reveal reuses
scrollAxes; contentRendered no longer reads layout per streamed chunk;
the constant sentinelMarginPx field is removed.

**Fixes (each red before, green after):** a hidden transcript under the
mission reader is no longer pinned to the bottom; an escaped reader
captured at the bottom (hold triggered there) keeps its anchor; a send
before hydration cancels the pending activation restore; a wheel no
longer releases Virtua's imperative operation (scrollBy(0) reasserted
the pre-gesture offset and undid the first notch that mounted rows).

**Documented, not fixed:** two effects of a still-live Virtua index jump
(session switch within its landing frame; frames at the message top
during a deep reveal).

Validation: typecheck; unit 68/68; after rebase on dev: message-scroll +
history-navigation + mission-reader-transcript + session-rendering
160/160; full run also session-search, search-highlight, history,
streaming-outline, project-tab-restore, interruption-selection,
render-cost 17/17.
2026-10-17 10:05:05 +02:00
Pascal André
d36f8a703b
fix(opencode-setup): offer OpenCode 2 when an old or foreign OpenCode blocks setup (#982)
# Offer OpenCode 2 when an old or foreign OpenCode blocks setup

## Problem

CodeNomad prefers the `opencode` found on PATH and only installs or
updates OpenCode through a verified `@opencode/cli` npm installation.
When PATH held OpenCode 1.x — from the npm package `opencode-ai`, or
from winget's `SST.opencode` (still 1.19.0) — setup reported no active
OpenCode service and offered no action at all, only "update manually".
For npm users this was a real dead end: `opencode-ai` and
`@opencode/cli` both claim the `opencode` command, so npm refuses to
install OpenCode 2 over it (EEXIST).

## Expected behavior

**OpenCode 1.x installed with npm.** Recognized only from the package's
`package.json` and the shim's actual target, never from a version
string. Setup offers **Replace OpenCode 1.x with OpenCode 2** behind a
confirmation. The confirmation states that the data folder is kept and
that OpenCode 2 imports 1.x conversations itself; CodeNomad does not
convert them. On confirm, under the existing installation lock,
`opencode-ai` is removed from that npm prefix and the existing shared
installation path installs and verifies `@opencode/cli`, registers PATH
and implies no daemon restart. Consent carries the exact confirmed
installation (`path`, `version`); the route validates it, the service
compares it with current status and the installer re-checks it under the
lock, refusing any mismatch. A broken 1.x install whose `--version`
fails can still be replaced. Before uninstalling on Windows, every
native executable owned by the legacy package (including nested
`opencode-<os>-<arch>` packages used by 1.0–1.15.0) is checked for use.
npm failures map to "in use", "1.x still installed" or "partially
removed" (with manual instructions) from npm's error code only. If
installation fails after removal, the panel reports it and offers a
plain install as retry; nothing is retried automatically or by another
mechanism.

**Foreign installations (winget, Homebrew, curl, manual).** Never
modified. The foreign version probe is asynchronous, bounded to 5 s and
kills its process tree on timeout; an unknown result keeps the PATH
command's priority and expires after 60 s, the setup panel's Check
action forces a fresh probe, only one probe per command runs at a time,
and successful results are cached by path, mtime and size. When the PATH
executable is unusable or below the minimum (the notice names the
minimum version), setup offers **Install OpenCode 2 for CodeNomad** into
the user npm prefix, and CodeNomad then uses that managed copy. The
panel says the older copy is untouched and that the terminal `opencode`
may still start it. When the resolved winget package folder proves the
`SST.opencode` ID, it adds that `winget uninstall SST.opencode` is
optional (text only, never executed). A working foreign OpenCode 2 keeps
today's PATH priority.

**Mixed case.** With winget's copy first on PATH and an npm 1.x behind
it, only the npm package is replaced, with confirmation.

**Custom executable** chosen in settings is never overridden or
installed over.

## Verified facts

- npm bins: `opencode-ai` 1.18.10 / 1.19.2 → `opencode`; `@opencode/cli`
2.0.30 → `opencode`, `opencode2`.
- OpenCode 1.x and 2.x share `<data>/opencode/opencode.db`; the 2.x
server runs a V1 migration layer copying 1.x sessions (checked in
upstream tags v2.0.0–2.0.15).
- winget `SST.opencode` is a portable zip; user scope links
`%LOCALAPPDATA%\Microsoft\WinGet\Links\opencode.exe` to
`…\WinGet\Packages\SST.opencode_<source>\opencode.exe`; machine scope
uses `%ProgramFiles%\WinGet\…`.

## Known limitations

- Machine-wide winget installs and a customized winget Packages root are
not covered by tests; an old executable there still gets the
install-alongside action, without the winget label.
- The 1.x → 2.x conversation import is upstream behavior, verified from
source rather than on a real history.
- POSIX symlink layouts and the POSIX process-group kill are covered by
tests that run only on Linux/macOS.
- A real running 1.x process holding its executable was simulated with a
read-only file; npm's mid-uninstall EPERM/EBUSY parsing is tested with
synthetic stderr.
- For up to 5 s after startup, before the first foreign probe completes,
a launch keeps using the PATH command.

## Validation

- Server: 93 pass in touched areas (2 pre-existing POSIX-only skips);
full suite 2341 pass, with 2 failures in untouched files (`meta.test.ts`
OS-name expectation on the test machine; `binary-validation.test.ts`
timing, passes alone). Coverage includes legacy shim recognition and
rejections, consent, remove-then-install under lock, failure and retry,
winget symlink/hardlink layouts with PATH order, winget + npm 1.x,
working vs unusable foreign installs, custom executable, status/upgrade
logic and route errors.
- Browser: 33/33 setup tests, including replace
(cancel/confirm/failure/retry), broken 1.x, the winget notice and an
outdated foreign 2.x.
- Real npm on Windows:
`scripts/test-opencode-legacy-replacement-native.mjs` installs published
`opencode-ai@1.18.10` and `1.15.0` in temporary prefixes, reproduces
EEXIST, finds the nested native executables, refuses mismatched consent,
then performs the confirmed replacement to 2.0.29 without starting a
service.
- Server and UI typechecks clean; new strings in all 10 locales.
2026-10-16 15:44:17 +02:00
Pascal André
e41bf9401b
perf(chat): persist transcript scroll when it settles; drop dead render notifications (#977)
Scrolling no longer persists the transcript position on every scroll event: MessageSection records it once Virtua reports the scroll ended (40 -> 1 store writes, 1112 -> 160 rect reads over 1.5 s of wheel), and follow-mode changes (hold escapes immediately; a follow resume once the reader is at the bottom) are persisted at once because a pinned stream never settles. No reached position is lost: unmount captures are anchored on rows still laid out (shared with #972), and app shutdown, navigation and pagehide flush unsettled scrolls through messageStoreBus.flushScrollSnapshots(). An A/B trace showed markdown/text render notifications are needed to pin a following reader in the paint frame, while the reasoning-stream and bash RAF notifications moved nothing and the memory-accounting call was redundant; those are removed. Tests cover unsettled scrolls persisted on switch, unmount and flush with anchor and return-gap checks, settle-path persistence without a flush, a resumed follow, and the bus flush API. Gatekeeper rounds 1-6 resolved to zero findings.
2026-10-16 06:15:04 +02:00
Pascal André
69683f8c34
refactor(chat): give transcript navigation a single owner state (#972)
Timeline clicks and search results reach a message through one navigateToMessage path; the unmarked scrollIntoView fallback, redundant setAutoScroll(false) calls and duplicate scrollToKey are gone, and the transcript step of a search reveal goes through the list's alignElement (traced writes only).

Nine coupled navigation flags become one TranscriptNavigation union (unrestored | restoring | paging | idle); restore branches merge and the activation snapshot is read once, so a first gesture after a snapshot-less restore is never restored again (audit C4). Paging records whether the restore had positioned the list: a jump cancelled or failed before positioning returns to the saved passage and its window, a placement or new page load settles superseded jumps to idle, boundary paging waits while a restore loads its anchor, and a gesture cancelling a jump hands the position to the reader.

Scroll snapshots replace the stored object instead of merging stale anchors (audit C9); persistence accepts only captures that identify the reader (at the bottom or anchored), and the unmount capture reads its anchor from rows still laid out. Search occurrences are revealed only by an explicit search navigation through a one-shot request; rows moved by the virtualizer repaint their highlights before paint through one bounded observer per document. Known pre-existing follow-ups (session switch 0-1 frame after a jump lands; one frame at the message top during a deep reveal) are documented.

Browser regressions cover restore/paging interplay, cancellation before hydration, unmount during paging, deep and shallow search placement and highlight continuity. Gatekeeper rounds 1-6 resolved to zero findings.
2026-10-16 05:14:12 +02:00
Pascal André
7436ac8e67
refactor(chat): merge explicit bottom pins and element alignment into existing loops (#978)
One bottom loop (bottomPin) that exists only while following replaces the 3-frame pinDomBottomAfterLayout chain and the eleven-variable explicit pin; leaving follow ends it, and a settled or timed-out explicit pin hands the bottom back to ordinary follow for two frames. Layout priority is written once (restoration > explicit pin > hold > follow): a restoration supersedes an active pin and reports onApplied only after landing (C5), an at-bottom restoration joins an active send pin and keeps its row count, and an at-bottom restoration whose pin loses follow is cancelled. alignElement becomes a reader-settlement hold with the former alignment's fixed 30-frame window, re-aligned every frame, never extended by layout, released by gestures, navigation, focus moving to another element in the transcript, or a lost element; one anchor measurement serves reader settlement, alignment and restoration. MessageSection derives the send-intent token with a memo and cancels pending window paging only on a new send. Browser regressions cover C5, pin cancellation and handoffs, replaced restorations, send-pin row counts, alignment within its window, Tab after an alignment and temporary viewport clamps. Gatekeeper rounds 1-5 resolved to zero findings; foreign-scroll heuristics were deliberately removed in favour of the former bounded lifetime.
2026-10-16 03:35:07 +02:00
Pascal André
2196f1731e
fix(chat): keep escaped readers in place when Virtua re-estimates rows (#974)
An escaped reader near the bottom during streaming, and a reader at the long-reply hold escape, jumped back many messages. The scroll trace showed no transcript write: each measurement-reset remount kept Virtua's one-time median default-size estimate armed, which later resized every never-measured history row at once while the browser clamped before compensation. Remounts now pass the carried default size as itemSize (refreshed to the median once every retained row is measured), and a new instance is seeded with its real offset. The hold publishes its escaped position. Positioning teardown is unified (supersedePositioning, stopPositioning) and deactivation during a restoration invalidates it and releases Virtua's queued scroll. A unit test pins Virtua's internal CacheSnapshot/itemSize contract. Browser regressions cover both jumps and deactivation during restoration. Gatekeeper rounds 1-4 resolved to zero findings.
2026-10-15 21:36:35 +02:00
Pascal André
b768880958
refactor(chat): centralize transcript scroll writes and add opt-in tracing (#971)
Route every VirtualFollowList DOM and Virtua position write through one reason-labelled writeScroll chokepoint that owns programmatic marking, with no change to targets, scheduling, cancellation or recorded offsets. Add an opt-in persisted logger namespace (scroll) tracing writes, follow-mode transitions and unowned movements with no layout reads when disabled; expose data-follow-mode; label the remaining external writers with the closed reason type. Releases and no-op writes keep attribution accurate. Diagnosis: the escaped-reader and hold displacements are Virtua default-size re-estimation, browser clamping and compensation, fixed in #974. Gatekeeper rounds 1-3 resolved to zero findings.
2026-10-15 19:16:10 +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é
3ce7697c80
fix(pending): recover requests in nested checkouts the workspace already owns (#964)
Question and permission recovery now follows the same ownership rule as
prompts and live events, so a session working in a nested checkout of
another repository no longer triggers "Question and permission recovery
is incomplete", and its questions are recovered after a disconnection.

## Problem
Observed on a live installation:
- A session had been moved into
`D:\CodeNomad\.codenomad\worktrees\prompt-replay-yield`. That directory
is a worktree of a separate clone under `%TEMP%`, not of `D:\CodeNomad`.
- Because the directory is inside the opened folder, the workspace
manager owns it: CodeNomad lists the session, accepts prompts and routes
its live events.
- The pending-request broker additionally required the candidate to
share the workspace's Git common directory.
- While the session was idle, its directory was an optional hint and was
excluded.
- As soon as it worked, the directory became required and was rejected
with `403 Directory does not belong to workspace`.
- Two liveness passes later the warning appeared. The first live episode
matches to the minute: the session became active at 21:26:47 UTC and the
warning followed at 21:27.
- Any question that session asked while CodeNomad was closed or
disconnected could not be recovered.

A read-only probe against the running app reproduced both answers:
- required: 403 "Directory does not belong to workspace";
- optional: `excluded`;
- a registered worktree used as a control: `ok` in both cases.

## Change
- `pending-requests.ts` no longer adds a same-repository requirement on
top of the workspace manager's ownership. A candidate owned by the
manager is read.
- **Repository-change fence kept:** each candidate's own Git common
directory is recorded at admission and must be unchanged after the RPC.
- **Unowned paths unchanged:** sibling clones and unowned historical
hints are still excluded or reported as errors exactly as before.
- **Dead code removed:** owned directories can no longer be "outside"
exclusions, so the identity fields of those exclusions are gone.

## Tests
- New manager test with the real `WorkspaceManager`: a worktree of an
independent clone placed in `.codenomad/worktrees` is owned for both
request and event purposes, under the root's worktree identity (so the
root deletion fence covers it).
- New route regression:
- an owned nested checkout of another repository returns its permissions
and forms;
  - removing its `.git` during the RPC withholds coverage.
  - On the previous code it fails with exactly the live 403.
- Updated the tests that encoded the old rule:
  - an unowned historical clone is still excluded and never queried;
  - once owned it is scanned;
- an owned candidate whose identity becomes blocked during the read
still reports `error`.
- Results: `pending-requests.test.ts` 40/40; `manager`,
`instance-event-ownership` and `instance-proxy` tests 101/101; server
typecheck clean.

## Follow-ups (separate PRs)
- Make the warning state its cause and workspace.
- Treat the server's compaction deferral (503 `Pending discovery
deferred`) as a deferral in the UI rather than a failure.
2026-10-15 09:15:42 +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é
8e8ca15934
chore(opencode): qualify runtime 2.0.29 and align client/plugin pins (#948)
## Summary

- Recommend and pin OpenCode **2.0.29** (`@opencode/client`,
`@opencode/plugin`); the technical minimum stays **2.0.27** since
2.0.28/2.0.29 keep Effect 4.0.1.
- Add 2.0.29 as a reviewed contract profile beside 2.0.27; 2.0.28 and
future/custom labels keep authenticated negotiation.
- Test fixture only: the recurring Missions host declares the
`credential` domain that the 2.0.29 Promise adapter reads eagerly (the
managed owner spreads `ctx`, dropping the Proxy fallback). No product
code change.
- Two new isolated fixtures for upstream behavior changes: bounded
output-length continuation, and rejected Promise tools / native denial.
- Compatibility docs, CONTRIBUTING, AGENTS and architecture guide
updated.

## Upstream review

- OpenAPI: same 118 paths; additive schema changes (config `policies`,
provider/model `compatibility`, `encoding`).
- Private LocationServiceMap/Form/Permission/queue contracts used by the
pending snapshot: unchanged.
- Effect stays 4.0.1.

## Validation (Windows, isolated CLIs/databases, never the shared
daemon)

- 2.0.29: plugin runtime, automation, pruning/history/proxy, Missions
contract/family control/full journey, session environment, fork, aside,
prompt skills, Git degraded/init, provider usage/accounts, temporary
workspaces, blank-session cleanup, output-limit continuation, Promise
tool failure, system settings discovery, native upgrade 2.0.15→2.0.29.
- 2.0.27 (minimum): plugin runtime, automation, pruning, Missions
contract/family, session environment, fork, aside, prompt skills, blank
cleanup.
- Server: 2235 passed / 7 skipped / 0 failed. UI: 1524 passed. Tauri
crate 268/268, updater handoff 1/1, UI updater flow 13/13 (dev base
includes #786).

## Known, not caused by this change

`scripts/test-permission-receipts-native.mjs` times out identically on
2.0.27 with the previous pins; tracked separately.
2026-10-14 15:06:35 +02:00
Pascal André
af075a6fa5
feat(missions): add a Product delivery playbook based on NomadWorks (#946)
## Summary

Missions gains a fourth playbook, **Product delivery** (`delivery`; FR
"Livraison produit"), next to Flexible, Debugging and Exploration. Like
them, it is named after what it does. It adapts the NomadWorks
Collective method from the original
[NeuralNomadsAI/NomadWorks](https://github.com/NeuralNomadsAI/NomadWorks)
(`main` at `55eec7e`, v0.1.1). It works without installing that plugin.

- **Coordinator as Product Manager.**
- **Classification:** it classifies each request by complexity (tiny,
standard, complex) and track (implementation, investigation, spec).
- **Specification:** for behavioral work, it has the business analyst
and tech lead draft a Spec Change Request, then asks the human to
approve it through a native Form.
- **Delivery:** with the technical architect, it splits complex work
into slices (foundation, core, logic, ui, polish, qa, docs). Only one
task at a time edits the shared checkout.
- **Closure:** it closes only with QA and tech-lead evidence, and once
the documentation impact is resolved.
- **Six roles:** business analyst, technical architect, tech lead,
developer, QA engineer and UI/UX designer. Each returns the NomadWorks
specialist output sections: summary, work performed, acceptance-criteria
coverage, documentation impact, open risks and next step. Each role can
have its own agent and model in the profile selectors and in Missions
defaults.
- **Deliberate differences from NomadWorks:**
- Request approval is coordinator guidance, not a gate enforced by the
server.
- The Missions safety boundary replaces NomadWorks' agent commit
authority: the human owns commits.
- One playbook covers both NomadWorks team presets (mini and full),
because the coordinator declares only the roles each complexity needs.
- **Fix found along the way:** task-role validation checked every
non-Debugging playbook against the **Exploration** roles. A new
playbook's tasks would have been rejected. Each playbook now admits
exactly its own declared roles.

## Changes

- **Server:**
- the recipe in `missions/recipes.ts` and the roles in
`playbook-profiles.ts`;
- the `delivery` template id in every server, plugin, RPC and route
enum, and in the journal decoder;
  - the role check in `contracts.ts`.
- **UI:** the Playbook selector, the defaults-scope selector and
saved-brief validation.
- **i18n:** the playbook name and six role labels in all ten locales.
- **Docs:** `dev-docs/MISSIONS.md` (Included playbooks) and the
`codenomad-missions` skill.

## Validation

- Server and UI typecheck.
- All Missions server tests, including a new regression showing that
Product delivery admits exactly its own roles and that other playbooks
reject them.
- CI "Missions UI units" (36 files), plus every Mission and i18n UI unit
test (40 files). The locale-ownership test now covers 25 profile labels.
- `build:missions` plugin bundle.
- Browser tests `mission-defaults-models` and `mission-editor-profiles`
pass 24/24. The Flexible-exception loop now also covers the new
playbook.

Not yet validated: a live model run of a NomadWorks mission.
2026-10-14 14:45:38 +02:00
Pascal André
dac236c8fd
feat(tauri): signed cross-platform updates with stable and preview feeds (#786)
Desktop Tauri builds now update themselves in place on Windows (signed NSIS), macOS x64/arm64 (signed .app.tar.gz) and Linux x64 (signed AppImage), and Linux gains AppImage distribution. The application no longer shells out to `winget upgrade`; WinGet remains a distribution channel.

Updates follow the same two feeds as the server notifications: `stable` offers GitHub's latest final release when it is newer; `preview` (dev + release) offers the newest publication of any kind, ordered by publication because dev labels reuse their base version. The saved `server.updateFeed` selects the feed (otherwise the build label). Only releases carrying their signed `latest.json` are installable, so releases still building are never offered.

Native code owns URLs, keys and installers: renderers pass only the feed and, after an explicit "Install and restart", the accepted version. The manifest must announce exactly the selected version and `requireSignedVersion` binds it to the signature. Full verification precedes shutdown; installation and restart are admitted atomically with an all-window flush; timeouts, cancellation and failed cleanup discard the installer without replay. Failures belong to the installing window, survive reloads and are reported once; concurrent checks report "busy". Menu actions reach only the focused local application document. The vendored tauri-plugin-updater 2.12.0 runs its Windows exit callback only after a successful installer launch.

Publication stages four verified payloads (signed version required) and publishes each release's `latest.json` only after validating every receipt and uploaded digest; rebuilt tags drop a stale manifest first. The signing private key reaches only `tauri build` steps. Unsigned builds and PR runs publish nothing. Debian/RPM and unsupported formats use the release-page fallback. The Tauri CLI minimum is 2.11.5.

Validated by eight rounds of independent review (zero findings on both native/CI and renderer/docs), 268 native tests, script and browser regressions, real signed update trials on Windows and Linux (from loopback endpoints, before feed selection), and published-package trials on Windows, Linux and macOS x64. A production-signed dev prerelease, macOS signed updates and draft restoration across an update remain to be exercised.
2026-10-14 12:18:36 +02:00
Pascal André
2e3d4adf8f
ci: refresh actions and monitor the bundled Node/npm runtime
Update official JavaScript actions and QEMU to their latest checked releases, and let Dependabot propose weekly Actions updates against dev. Adapt npm publishing authentication to setup-node v7 without exposing a token during dependency installation.

Add a daily default-branch maintenance check with read-only audit permissions. It identifies the npm release supplied by the exact Node pin, audits that CLI and its bundled dependencies using an isolated script-disabled lockfile, and retains the JSON report. Available npm updates and advisory entries emit warnings only: no independent npm replacement, auto-fix, automatic merge or release.

A separate write-scoped job can propose same-major Node LTS updates as regular, non-draft PRs. Revalidate the pin, preserve existing or closed proposals, never overwrite a branch, and document the maintainer action needed to trigger validation for GITHUB_TOKEN-created PRs. Installed applications and current runtime pins remain unchanged.

Validation: 21 offline maintenance, packaging and artifact-helper tests; root typecheck; real npm audit and metadata lookup; YAML and actionlint checks (apart from the existing disabled ARM64 job warning).
2026-10-14 10:36:24 +02:00
Pascal André
030542d56d
refactor(settings): limit the shortcut manager to renderer shortcuts
Drop native menu/window shortcut customization from the keyboard shortcut
manager to keep the change small and low-risk. Electron and Tauri menus,
zoom, fullscreen, reload, devtools, minimize, close window, quit and macOS
Hide keep their existing accelerators and defaults from dev; they are no
longer listed in the manager and are not suspended during capture.

What remains
- The renderer catalog, persisted overrides, capture/disable/reset UI,
  conflict detection (shared physical key identity, AZERTY/QWERTZ),
  dynamic tooltips and the removal of permanent hint displays.
- All renderer safety fixes: no deferred caret restore after newline,
  prototype-safe key aliases, IME/defaultPrevented guards, combo-free
  tooltips and abort status.
- Desktop hosts only gain the "hotkeys" Preferences section in their
  allowlists; Tauri menu labels no longer embed manual shortcut suffixes.

Removed
- Native hotkey contract/sync/protocol, Electron native-hotkeys module and
  menu rebuild path, Tauri native_hotkeys/window_hotkeys/WebView2
  accelerator handling and their permissions, plus the related tests and
  fixtures. Native-only i18n keys are removed from all 10 locales;
  redundant test cases were pruned and dev-docs/HOTKEYS.md/AGENTS.md now
  describe the renderer-only scope.

Validation
- npm run typecheck
- UI unit tests (hotkey bindings, prompt keydown, preferences, behavior
  registry, Preferences window parity): 57/57
- Browser suites hotkeys-settings, header-windows, shell-navigation,
  composer-picker-enter, composer-draft-safety, session-search,
  session-aside, preferences-window, new-instance-launcher,
  view-menu-electron: 95/95
- Electron npm run test:native: 277 passed, 4 skipped (sequential rerun;
  process-stop is a known intermittent failure on dev)
- Tauri script tests 17/17; cargo test --bin codenomad-tauri 251/251
2026-10-13 22:06:10 +02:00
Pascal André
a877867f99
feat(settings): add a customizable keyboard shortcut manager
Every application shortcut can now be rebound, disabled or reset from a new
"Keyboard shortcuts" section in Settings, in both the in-app settings screen
and the native Preferences window. Permanent shortcut hints are removed from
the UI; effective bindings are shown only in tooltips and in the manager.

Implementation
- A shared static catalog (lib/hotkey-definitions.ts) and binding module
  (lib/hotkey-bindings.ts) define ids, categories, host filters, platform
  defaults (Mod = Cmd on macOS, Ctrl elsewhere), matching, formatting and
  conflict detection. Capture, matching and conflicts share one physical key
  identity so shifted digits/brackets and non-US layouts (AZERTY, QWERTZ)
  are compared the way they are dispatched.
- Overrides persist in ui.settings.hotkeyOverrides (absent = default,
  [] = disabled). Reset one/all use merge-patch null; handlers read the
  effective binding at event time and the dispatcher respects
  defaultPrevented.
- Registry shortcuts, tab/session switching, command palette, new/close
  instance and session, session search and result navigation, background,
  composer send/alternate send/newline/history/exit shell, /btw aside send,
  file save, tab rename and the double-Escape stop are all customizable.
  Local widget navigation, Escape layer dismissal, @ and / triggers, IME
  composition and Forms answer provenance are unchanged.
- The former submit-on-Enter toggle is replaced by the manager; when no
  override exists the composer defaults derive from the stored legacy value.
- The showKeyboardShortcutHints setting, its palette command, the
  data-keyboard-hints dataset and the Kbd/KeyboardHint/HintRow components
  are removed; an old stored value is tolerated without migration.
- Electron and Tauri menus, zoom, fullscreen, reload, devtools, minimize,
  close window, quit and macOS Hide/Hide Others consume a bounded, validated
  snapshot of effective bindings through the existing native bridges. Only
  owning local/remote main windows may sync it; previews and extensions get
  no authority. Native accelerators are suspended (with acknowledgement)
  while a shortcut is being recorded, menu labels no longer embed combos,
  and the F6 WebView2 workaround and vendored Tao patches are untouched.
- Default conflicts are resolved with one execution route per event:
  New Window moves to Mod+Alt+N and Close Window to Mod+Alt+W, leaving
  Mod+Shift+N to New Session and Mod+W to Close Instance.
- Tauri remote window destruction now clears its hotkey snapshot, recording
  state, pending acknowledgement and Windows browser-key mark, guarded by
  the window generation. Electron focus/blur update the menu in place.

Edge cases and docs
- Legacy logical overrides stored as symbols (for example "&") for a
  physical slot must be re-recorded; see dev-docs/HOTKEYS.md.
- Persisted keys are validated with own-property lookups so values such as
  "constructor" or "__proto__" cannot break normalization.
- i18n: new hotkeys message parts in all 10 locales; combos removed from
  translated strings. AGENTS.md documents the new styling/catalog
  conventions.

Validation
- npm run typecheck (UI, Electron, remote tunnel)
- UI unit tests: hotkey bindings/contract/copy, prompt keydown, preferences,
  behavior registry, Preferences window parity
- Browser regressions: hotkeys-settings (rebind, old combo inert, tooltip
  update, reset, capture suppression), header-windows, shell-navigation,
  composer-picker-enter, composer-draft-safety, session-search,
  session-aside, preferences-window, new-instance-launcher,
  view-menu-electron, mission-native-family
- Electron npm run test:native: 292 passed, 4 skipped
- Tauri window-hotkeys/window-zoom script tests: 25 passed;
  cargo test --bin codenomad-tauri: 260 passed
2026-10-13 21:25:39 +02:00
Pascal André
ed578121e9
feat(files): hide whitespace-only changes (Advanced setting, on by default) (#934)
## What changes for the user
New setting in **Settings → Advanced**: **Hide whitespace-only
changes**, **on by default**.

| Change in a file | Setting on (default) | Setting off |
|---|---|---|
| Only indentation, spacing or line endings (CRLF↔LF) | **Hidden from
the Changes list**; stays unstaged | Listed as before |
| Real code change (with some re-indentation) | Listed; re-indented
lines are not highlighted in the diff | Listed; every line is
highlighted |
| Already staged | Always listed, so nothing is ever committed unseen |
Listed |

When such a file is opened with the setting off, a short note says that
only whitespace, or only line endings, changed. A diff is never silently
blank.

## How it works
- **Detection:** server-side, by Git itself. When requested, the
existing bounded status read adds a single `git diff --numstat -z
--ignore-all-space` query, with no per-file processes. An unstaged
tracked text file is hidden when its normal diff is non-empty but its
`-w` diff is empty.
- **Never hidden:** untracked, binary, added, deleted, renamed,
mode-only and conflicted changes. If detection fails, nothing is hidden.
- **Visible list only:** selection, range staging, drag-and-drop and
counts use only the visible list, so hidden files cannot be staged by
accident.
- **Live toggle:** changing the setting re-reads the list. A read still
in flight under the old setting is discarded.
- **Two rules on purpose:** the list uses Git's `-w`, which also ignores
spacing inside a line. The diff reader keeps Monaco's leading/trailing
trim.

## Validation
- **Server tests** on a real repository: whitespace-only, CRLF-only,
real, binary, untracked, staged, and staged-then-re-indented files;
option-keyed request sharing; a failed detection hides nothing.
- **UI unit tests:** staged rows kept, range staging skips hidden files,
live toggle, stale reads.
- **Browser:** `git-history.test.ts` 19/19.
- **Typechecks:** server and UI pass.

Oversized files touched: `useGitChanges.ts` (~554 lines),
`stores/preferences.tsx` (~1,589 lines) and
`lib/settings/behavior-registry.ts` (~589 lines).
2026-10-13 04:05:50 +02:00