mirror of
https://github.com/NeuralNomadsAI/CodeNomad.git
synced 2026-11-03 04:15:41 +00:00
267 commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
b31d9e3772
|
feat(settings): add OpenCode image generation and storage settings (#1193)
## Summary OpenCode 2.2 ships two opt-in experimental image features. This PR exposes them in **Settings → Providers → Images**, a new folded group beside Models and Web search: - **Image generation** (`experimental.image_generate`): Off, **On, automatic model**, or a default image model from the native `GET /api/model?types=image` catalog. Agents then get OpenCode's background `image_generate` tool while a connected provider offers an image model; each image asks permission first. - **Store images once** (`experimental.media_blob`): attached and generated images are stored once in OpenCode's deduplicated blob table instead of inside every message row. CodeNomad's direct SQL readers already hydrate these refs. No new instance-proxy route: everything stays in OpenCode and its configuration. ## Design - **Global scope only.** OpenCode reads `media_blob` (like `subagent_depth`) from the whole `experimental` object of the highest-priority document that has one. Writing a project block would silently hide inherited experimental settings, so the project document is never written; a project that sets its own choice is reported under the control. - **Contract-gated, never version-gated.** The native `experimental` object has `additionalProperties: false`, so an unknown key would break an older daemon's configuration. Each control appears only when `Config.InfoEncoded` declares its key; the backend refuses writes otherwise. 2.2.1 shows both, 2.2.0 only media storage, 2.1.x neither (the group says so). - **Existing authorized document path.** `PluginControls.readConfigDocuments`/`editConfigDocument` plus `native-setting-document.ts` targeted JSONC edits, as for Web search and subagent depth: ownership, deletion and connection fences, comments and unrelated fields preserved. Turning a setting off removes the key and any `experimental` object it leaves empty. - **On demand.** The folded group mounts nothing; the contract and the image-model catalog are read only while it is open, with coalesced rereads on config/model/credential events and reconnects. ## Validation - Server units (`image-settings.test.ts`): contract detection, OpenCode value forms, effective values and project overrides, JSONC preservation, empty-object cleanup, route validation. - New isolated native fixture `scripts/test-image-settings-native.mjs` (added to the compatibility job for the minimum and latest runtimes, `--expect-supported` on latest). Run locally on Windows against the exact CLIs: - **2.2.1**: both settings detected, written, accepted by the native schema and hot-reloaded; reset restores the original `experimental` object. - **2.2.0**: media storage only; generation write refused, file untouched. - **2.1.4 / 2.1.2**: no settings; writes refused, file untouched. - Browser regression (real `ProvidersSettingsSection`): no read while folded, one write per change, no replay after a failed write, unsupported runtime, 900/380 px widths. The existing web-search tests still pass. - `npm run typecheck`, server `tsc --noEmit`, help checks (10 locales). Not covered here: rendering generated images in the transcript (OpenCode's completion notice is a synthetic message CodeNomad hides today). That is the follow-up PR. |
||
|
|
a097b9a250
|
feat(extensions): read a project's issues and pull requests through panel extension API 3 (#1191)
#500 asks for a view of a repository's issues and pull requests next to the sessions working on them. Panel extensions are the place for such a view, but they run sandboxed: no network, no project folder, no credentials. Extension API 3 adds one bounded, host-owned capability, `repository.workItems.read`, so an addon lists and opens a project's issues and pull/merge requests without receiving any of those. Worktree actions stay out of scope for a later, separately consented capability. Host (server/src/forge/): the project folder's `upstream` remote, else `origin`, else any forge remote, strictly validated (host or bracketed IPv6, HTTPS port, path segments), names the repository. github.com, gitlab.com, codeberg.org and gitea.com are read through the user's signed-in `gh api`, `glab api` or `tea api` (tea's stderr status checked, its login matched to the exact authority), falling back to their anonymous REST API for public repositories; any other host is identified and read only through a CLI signed in to it, never contacted anonymously. CodeNomad stores no token. CLIs run without a shell, quietly, within 15 seconds and 4 MiB; anonymous bodies are bounded while streaming. Each read returns one kind, at most 20 items up to page 50, with the open total; GitHub search words are literal quoted terms, and only items under the repository's web address are published. Discovery and page reads are shared loads cancelled with their last caller, never joined once cancelled and never evicted while running; every process or fetch runs within one host-wide budget (four running, sixteen waiting). Extension API 3: exact permission set, disabled install, dedicated consent text in ten locales; `codenomad.workItems.list({ kind, query, page, fresh })` and `open(kind, number)`. Routes recheck consent before and after each read, cancel the read of a disconnected caller and drop results when the project closed. `open` asks the server for the item's page on the repository's own forge and opens it through the host's opener, one at a time and at most once a second measured at the opener. API 3 panels receive the host colours; the store's consent check is generalised to a permission and also serves API 2. Docs: PANEL_EXTENSIONS.md and the extensions help page in ten languages. Validation: forge.test.ts (remotes, CLI output formats, provider mapping and totals, literal search and foreign items, self-hosted access, bounded streams, shared and cancelled loads, budget, pagination), route tests (consent, digest, bounds, revocation, closing, caller disconnection), work-items-channel.test.ts (delayed answers open one tab, three on the first version), API 3 manifests, the extension browser suite, test-panel-extension-native.mjs --api3/--api2, the root typecheck and help checks. Live, an isolated CLI server with the Contribute addon installed from its ZIP listed NeuralNomadsAI/CodeNomad (gh) and gitlab-org/cli (glab) with totals, labels, search, exact numbers and opening, in English and French; Codeberg read anonymously. Three review rounds ended with no findings. Refs #500. |
||
|
|
34406b3ada
|
feat(help): add a download page with exact release links and a new home page (#1190)
## Summary
- New **Download** help page (10 languages): an OS-detecting button
linking straight to the recommended Tauri build, then a static
Tauri/Electron table with every asset and its size, version and publish
date, and the winget/npm commands. The header Download link,
`install.md`, `support.md` and the README point to it.
- New **help home page** (10 languages): cockpit positioning, a
*Download CodeNomad* call to action, a silent 46 s tour that autoplays
on desktop only (poster and YouTube link on narrow screens and with
reduced motion), and short Missions / phone access / long histories
sections before the existing orientation sections.
- **Exact links without network in the builder**: `release:<id>` links
and `{{release}}`, `{{download-button}}`, `{{tour-video}}` directives
resolve from `docs/help/downloads.json` and optional `--release` data;
without data, links fall back to the latest release page.
- **Automatic refresh**: `scripts/resolve-help-release.mjs` (CI only)
picks the newest stable release whose assets are fully uploaded;
`help-pages.yml` republishes after a successful *Release Binaries* push
run on `main`, building trusted `dev` sources with read-only
permissions, the token scoped to the resolver step and one production
concurrency group. Pull requests never resolve or publish.
## Safety
- Release data is revalidated in the builder (exact
`github.com/NeuralNomadsAI/CodeNomad/releases/download/<tag>/<asset>`
URLs, no credentials/query/fragment/traversal, positive sizes, UTC
dates) and escaped.
- CSP stays `default-src 'none'`; only `media-src 'self'` is added. The
download script is allowed on the download page only, the tour video on
the home page only.
## Validation
- `npm run test:help`: 27/27 (26 pages × 10 locales, offline; resolver
selection/pagination, release validation, directive problems, 22
detector cases, workflow gates).
- `npm run build:help` with and without the release fixture.
- `node scripts/test-help-browser.mjs`: 46/46 (download button,
JavaScript disabled, video playback, reduced motion, narrow widths,
RTL).
- Read-only resolver run against the live API resolved `v1.0.0`.
The `workflow_run` path can only be observed on the next stable release
on `main`; `workflow_dispatch` from `dev` exercises resolver → build →
deploy.
|
||
|
|
ca514ed2ea
|
chore(opencode): qualify and recommend runtime 2.2.1 (#1188)
## Summary
- Qualify OpenCode **2.2.1** (latest stable, 29 October) with the
existing locked **2.1.0 client/plugin** dependencies, and **recommend**
it.
- Keep the technical **minimum at 2.1.2**: no consumed contract changed,
so the minimum has no new reason to move.
- 2.1.2, 2.1.4 and 2.2.0 daemons stay ready and get an optional upgrade
offer. 2.2.1 is the `tested` version. Newer and custom labels keep
authenticated contract recognition.
- The reviewed-contract fast path lists the 2.1.2 minimum and the 2.2.1
target.
- The recommended version is updated in the help (all ten locales), both
READMEs, `CONTRIBUTING.md`, `dev-docs/OPENCODE_COMPATIBILITY.md` and the
architecture skill.
## Contract review (v2.1.4 → v2.2.0 → v2.2.1; 661 + 134 commits)
Reviewed from the exact tags `v2.1.4` (`68bc26b7da`), `v2.2.0`
(`1097e2668b`) and `v2.2.1` (`3ebcf9a32c`), the published npm packages,
and an authenticated OpenAPI dump from each official CLI running in its
own sandbox. The [changelog](https://opencode.ai/changelog) covers
2.2.0. It had no 2.2.1 entry at review time (29 October), so 2.2.1 was
reviewed from its commit log.
- **Authenticated OpenAPI:** 124 → 130 → **134 paths**, 149 → 156 →
**162 operations**. Each dump matches the tagged
`packages/protocol/openapi.json`.
- **2.2.0 adds** `GET /api/plugin/gui`; `POST
/api/experimental/generate/{text,image,decisions}`; `POST`/`DELETE
/api/experimental/fs/watch`; `GET /api/experimental/media/{hash}`; and
`GET /api/experimental/usage/limits`.
- **2.2.0 removes** `POST
/api/experimental/voice/session/{sessionID}/instructions`.
- **2.2.1 adds** six `/api/experimental/schedule` operations (list,
create, get, delete, pause, resume), gated by `experimental.schedules`.
- **Changed operations:**
- `GET /api/model` gains an optional `types` filter. Without it the
route lists chat models only. Image and decision models are new in
2.2.0, so the default listing matches 2.1.4.
- `GET /api/event` gains an optional `types` filter.
- `GET /api/vcs/diff` gains an optional `untracked` parameter.
- `POST /api/experimental/generate` becomes the deprecated alias of
`/generate/text`.
- Nine session, project and saved-permission routes drop
`LocationNotFoundError` from their 404 responses: they no longer boot a
Location ([#58997](https://github.com/anomalyco/opencode/pull/58997)).
- `GET /api/agent` can now answer 503 while plugins settle
([#59082](https://github.com/anomalyco/opencode/pull/59082)).
- In 2.2.1, the session log and persistent-PTY operation IDs move under
`experimental.*`. Their paths are unchanged.
- **Schemas:** all 103 consumed-family schemas are byte-identical from
2.1.4 to 2.2.1. These are `V2EventEncoded`, `Session.*` (messages,
parts, inbox, info), `Form.*`, `Permission.*`, `Prompt*`, `Event.*`,
`Location.*` and PTY/Shell. The session prompt, environment, message,
form/permission reply and RPC operations are unchanged too. The only
`Config.InfoEncoded` changes are additive opt-ins:
`experimental.media_blob`, `experimental.image_generate`,
`experimental.schedules` and `timezone`. Model `type`, prompt-cache
`cache` settings, `FileDiff.Info.more`, `Mcp.Server.title` and
`ServerInfo.hostname` are additive fields.
- **Admission:** every predicate CodeNomad's
`compatibility/negotiate.ts` checks holds on 2.2.0 and 2.2.1. That
covers the canonical API, `session_environment` and the 2.1 replay shape
(`native` without `state`, compaction `metadata` without `recent`).
- **Instance proxy:** none of the new routes is on the allowlist. That
includes the GUI plugin bundle, generation, file watch, media blobs,
usage limits and schedules. The removed voice route was never exposed.
The routes CodeNomad does proxy kept their paths.
- **Published declarations** (2.1.0 pins → 2.1.4 → 2.2.1):
- **Client:** 143 → 147 → 185 methods. 2.2.1 mirrors the experimental
methods under `client.experimental.*`. The only removal is
`voice.instructions`, which CodeNomad doesn't use. 2.2.1 doesn't send
the `experimental-media-uri` capability, so media reads stay inline.
- **Schema:** 2.2.0 adds media refs (`MediaRef`,
`Session.Message.MediaUser`, inbox `MediaUserPayload`), `generate`,
`usage-limits` and `Model.Type`. 2.2.1 adds `goal` and `schedule`. The
one removal since 2.1.0 is `SessionEvent.Source` (2.1.4).
- **Plugin:** 2.2.0 adds the `gui/*` SDK (excluded from the host
runtime) and `Host.packageID`. 2.2.1 adds
`SessionIdle`/`SessionIdleBackground`. The event `subscribe` domain
gains an optional `types` filter; CodeNomad's plugins don't subscribe
through it.
- **Typecheck:** the server and UI typecheck with **0 errors** against
the 2.1.4 and 2.2.1 client/schema/plugin declarations, through scratch
`paths` overrides. The pinned 2.1.0 control is also clean.
- **Host modules:** CodeNomad's three daemon-loaded bundles resolve
`@opencode/plugin` (`Plugin`), `/effect` (`Plugin`, `Rpc`, `Location`),
`/promise/adapter` (`fromPromise`), `/rpc` (`Rpc`), `effect` and
`effect/encoding` (`Base64Url`) from the host. `@opencode/schema` and
`@opencode/client` are bundled from the 2.1.0 pins, and
`@opencode/schema/location` imports are type-only. A probe plugin loaded
in an isolated **2.2.1** daemon resolves every one of these modules and
bindings, as on 2.1.4. The host Effect stays **4.0.1**, with the same
lockfile hash.
- **Private native services:** the pending-request reader and Missions
recurrence use keyed native services: `LocationServiceMap`, `Form`,
`Permission`, `Location`, `Job`, `Database`, `Session` and `Bus`. Their
keys are unchanged at v2.2.1, and the members CodeNomad calls are
unchanged. The changes are additive: `Permission.assertNotDenied`,
`Session.*WithRefs`, an `image` Job kind, and
`LocationServiceMap.canonical` normalizing paths.
- **Storage** (migrations from 2.1.4 to 2.2.1): `blob`/`blob_ref`
(always created; written only with `experimental.media_blob`) and
`schedule` are new. `session_pending` is dropped; CodeNomad never reads
it. A `session_v2.allowance` column is added and dropped within 2.2.0.
- The tables CodeNomad reads directly (`session_message`, `session_v2`,
`session_inbox`, `event`, `event_sequence`, `kv`) keep their shape.
- The pruning plugin's `media-blob.ts` already projects `media:<sha256>`
refs.
- The pruning fixture enables `media_blob` explicitly, so the
always-present `blob` table doesn't change its expectation.
- Missions reads `session_inbox` only by count and ID in production
code.
- **Behavior changes checked:**
- **Code Mode:** 2.2.1 moves programs onto one job queue
([#57976](https://github.com/anomalyco/opencode/pull/57976)). The
run-to-completion regression behind the 2.1.2 minimum passes on 2.1.4,
2.2.0 and 2.2.1: `{ total: 2000, beforeAwait: 1000, count: 1000 }`.
- **Session form polling** returns `[]` without booting a cold Location
([#59083](https://github.com/anomalyco/opencode/pull/59083)). Forms are
in memory, so nothing pending is hidden. This matches CodeNomad's
existing-only pending reader.
- **Location keys and `session.create` directories** are canonical on
POSIX too: trailing slash removed, `posix.normalize`
([#59327](https://github.com/anomalyco/opencode/pull/59327)). Windows
already normalized. This is low risk; it was reviewed in source only,
since the location and ownership fixtures run on Windows and the POSIX
certification covers migration, Code Mode and Git-degraded.
- **Location keep-alive:** the changelog line "keep project services
alive … with a 24-hour inactivity cap"
([#57274](https://github.com/anomalyco/opencode/pull/57274)) doesn't
match the shipped tree. That commit was superseded before 2.1.4, and no
cap exists at v2.2.0 or v2.2.1. From 2.1.4 to 2.2.1,
`location-activity.ts` only canonicalizes paths and generalizes Job
kinds. Long-waiting Missions sessions keep their Location as on 2.1.4.
- **Plugin readiness:** each runner step and tool call now waits for
plugin activation
([#51133](https://github.com/anomalyco/opencode/pull/51133)), and
agent/provider lists wait for plugins. Presence leases stay outside the
discovery root, so this adds no reload.
- **Service lifecycle:** registration and config renames are retried on
Windows. The managed service ignores a saved `XDG_STATE_HOME`
([#58815](https://github.com/anomalyco/opencode/pull/58815)); CodeNomad
never sets it. The `OPENCODE_SERVER_USERNAME` change was added and
reverted within 2.2.0. Detached POSIX children are killed when OpenCode
exits; service start uses its own spawn path.
- **2.2.1 opt-ins:**
- schedules, both the API and the agent tool, need
`experimental.schedules`;
- `image_generate` needs `experimental.image_generate`;
- `/goal` runs only when invoked;
- the subagent tool description now invites follow-ups to *running*
subagents, with the same mechanics as 2.1.4.
- **Other changes:** compaction sizing from the provider's count, MCP
malformed-tool tolerance, OAuth refresh coordination
([#54023](https://github.com/anomalyco/opencode/pull/54023)) and
webfetch redirect rules. They touch no consumed contract and were
reviewed in source; of these, the native suites exercise compaction
only.
- **Upstream #52922** (Bus routing before Location fanout) is still
open: keep the discovery and event-relay mitigations.
**Verdict:** no consumed contract changed from 2.1.4 to 2.2.1.
- The **2.1.0 client/plugin pins can stay**.
- The **2.1.2 minimum has no reason to move**.
- 2.2.1 is admitted through the existing contract recognition, and
nothing found blocks recommending it once the native suites pass on it.
- No CodeNomad bug was found.
Bumping the pins is optional. It would bring the 2.2 client's
discard-tool-result-payload option and its "register running sessions
missed while disconnected" fix
([#53012](https://github.com/anomalyco/opencode/pull/53012)). It would
also move the replay reducer, so it belongs in its own PR. The UI and
server already typecheck against 2.2.1.
**Not verified here:**
- the full native suites against 2.2.0/2.2.1 (only the Code Mode
regression and the host-module probe ran);
- the TUI pruning companion under OpenTUI 0.7. It uses only
`Plugin.define`, `ctx.data.session` and `ctx.data.listen`, so the 0.7
renderable changes don't apply, but this wasn't run;
- POSIX canonicalization on a real Linux/macOS runtime;
- the 2.2.1 release notes, which were unpublished at review time.
## Isolated Windows qualification
Base: dev `
|
||
|
|
1577662068
|
fix(transcript): finish settlement reads from liveness and inbox evidence (#1187)
A settlement whose terminal event fell inside a disconnect window had no trigger: finishOpenCodeDataEvent never ran, so the SDK's settlement read was never finished and a tool row stayed "pending" until a reload. A queued prompt delivered during the gap also vanished from both the queue and the transcript. The settlement read from #1169 is extracted as completeSettlementRead and now also runs from two pieces of evidence the client already gathers: the 30-second liveness pass reports each run it believed working that the final activity read no longer lists (only while the transcript still holds an unsettled tool), and the inbox sync reports an admitted item the authoritative inbox no longer lists. Neither path has a terminal event that invalidated the SDK's message key, so both invalidate it before the read; otherwise the sync engine would skip it on any reducer that synced before. The execution-epoch fence still lets only a newer execution suppress the publication, and the display window keeps its membership. Regressions start from a reducer whose first sync completed and expect exactly one read: a liveness-observed settlement heals a stranded tool, a newer execution suppresses that publication while the reducer still reconciles, an inbox sync admits the delivered prompt, the production liveness pass heals end to end, and a still-active run reads nothing. Store suites and the UI typecheck pass. |
||
|
|
940d7e9960
|
fix(ui): take authority for unchanged parts of rows that streamed during a read (#1184)
Reducer reads merge their page with the rows streamed during the fetch: a row that changed keeps its live version. That decision was per row, so progress on one tool of an assistant message kept the whole live row, including a sibling tool the stream never settled. With a tool stranded "pending" after a lost session.tool.called and a background tool reporting progress in the same message, the settlement read fetched the completed state and the merge threw it away, leaving the tool pending. Within a row changed since the read's snapshot, each identified content part (a tool call, for example) left unchanged since the snapshot now takes the page's version, while changed or new parts, parts without an identity and the row's own fields keep their live version. Rows that did not change, rows created during the fetch, the time.completed exclusion and the destructive fence are unchanged. The snapshot is the serialized row the read already captures; only rows that changed parse it again. A merge unit test and a reducer sync where a sibling tool reports progress during the fetch cover it; both fail without the change. The store suites around opencode-data, session-api, compaction, settlement, idle, session events, request authority and pruning, and the UI typecheck pass. |
||
|
|
cde99a3af5
|
fix(transcript): settle tools stranded pending after a lost tool.called (#1169)
A tool row could stay "pending" forever while later tools completed and even the next round streamed, until the user switched sessions. The UI's "pending" is the reducer's tool status `streaming`; a tool leaves it only through `session.tool.called`, and `session.tool.success` is reduced only from `running`, so a lost `called` made the later success a no-op. The SDK already reconciles such rows: on an execution terminal event with a streaming or running tool it invalidates and re-reads the session's messages, and CodeNomad keeps the reducer alive for it. That read was skipped while the stream was not connected, and its result was never published to the display. finishOpenCodeDataEvent now completes it: on an execution terminal event whose reducer still holds a streaming or running tool, it calls the SDK's message.sync without invalidating, sharing the SDK's in-flight read when connected and starting it when not, then projects the reconciled page with the display window's membership and cursor kept, and retries retirement. The read completes while the session keeps streaming and keeps every live event. Only a newer session.execution.started supersedes the publication; same-round activity such as a late tool event does not. Regressions cover disconnected and connected recovery, a new step streaming during the read and a late same-round tool success; each fails without the change. Store suites, both PR CI UI unit steps, project-tab-restore, session-rendering and the UI typecheck pass. |
||
|
|
a448bb1f6a
|
perf(missions): send summaries of closed missions to the panel (#1180)
Step 5 of 6 toward removing the Missions limits (#1167 follow-up), on
top of steps 1-4.
## Change
- Plugin RPCs `list` (summary row per mission from hints; full maps only
for open missions + up to 4 requested, each re-read from its range;
unchanged rows skipped via `summariesVersion`) and `mission` (one
range's map + its cleanups).
- `GET /missions?include=&summaries=` and `GET /missions/:missionID`;
fallback to the full `snapshot` when the plugin lacks the new reads
(`rpc.method_not_found`).
- UI store holds full maps + rows; picker/marks/completion seeding use
rows; detail, Needs you, task tree and readers use full maps of the
selection and reader target. Activity observes only the maps sent.
- Bridge routes that authorize native effects keep the full snapshot
(hint contract).
- Visible: a selected closed mission shows the loading line until its
map arrives; a closed unselected row's permission mark counts only its
declared actors' requests. No help change (nothing documented there).
## Read counts (10,000 history entries, 200 closed + 1 live)
`list()`: 0 full scans, live range only, 201 rows · with one closed
included: +50 entries, rows not resent · `mission(id)`: its 50 entries ·
`GET /missions`: 0 full scans after the index build; activity only for
open/included.
## Validation
- `mission-list.test.ts` (equivalence over random histories with
damage/purges, version skipping, staleness, label bound), read-budget
tests, route fallback tests, new `mission-summaries` browser test;
adapted fixtures keep assertions. Mechanism mutations each fail a test.
- Server and UI typecheck; 1,210 server tests; UI mission/i18n tests;
all 36 mission browser files; `test-missions-native.mjs` on isolated
OpenCode 2.2.0 and 2.1.4.
- `automation-forms.test.ts` fails identically on unmodified `
|
||
|
|
8e8ea0653e
|
fix(history): search recent history first, cover more of it and search further back (#1182)
## Summary
Follow-up to #1161 and #1164. In a large project, the ranked project
search covered only the **oldest** messages: the scan walked rowids
upward under a 1.5 s budget, so recent conversations were never reached,
and identical searches covered 5,000 to 7,000 messages depending on
load. Each 1,000-row statement also sorted and evaluated its whole chunk
before returning its first row, blocking the daemon for about 70 ms at a
time.
- **Newest first:**
- The scan streams 128-row ranges from an O(1) horizon, without a
temporary sort.
- It covers a deterministic window of the 16,384 most recent messages.
- **Cheaper per row:**
- A necessary UTF-8 byte check on the raw row rejects messages that
cannot contain a query term before any JSON is decoded.
- Only possible matches pay for the exact readable-text mask.
- The existing time, candidate, session and judge guards remain.
- **40 results** in the project search window; the agent tool keeps 10
by default and 20 at most.
- **Search further back:**
- When older history remains, it ranks the next older window for the
same query and options through an authenticated per-location
continuation.
- The continuation is bound to scope, query, options and the original
horizon, so later appends never enter it.
- Older windows replace the list, because ranking scores are
window-local.
- The coverage note names the examined range in all ten locales.
- Query, scope or option changes, closing and leaving the session reset
it.
- The conversation scope is unchanged.
## Measurements (synthetic 40,000-message corpus, `node
scripts/benchmark-ranked-history.mjs
|
||
|
|
954bb13d10
|
fix(ui): keep nested repositories' sessions and requests in their own project (#1174)
With a project open on a parent folder (such as the home directory) and another on a repository inside it, the repository's new sessions showed in the parent's session list, and its permission requests in the parent's interruption dock, banner and header count. The workspace manager owns nested checkouts of other repositories inside an opened folder and routes their events and requests to it; that ownership is unchanged. The UI applied its session-list scope only when loading the inventory, not to sessions arriving from live events. Sessions are scoped at display. The store keeps every session it receives; getWorkspaceSessionScope (session-state.ts) derives, reactively and cached per instance, the complete families with a member in the opened folder or a registered worktree (selectWorkspaceFamilyIds, shared with the inventory), plus the active session's family so a session opened explicitly is shown where it was opened. An unread or failed inventory and a Git-unavailable folder keep everything in scope. The session list, the tab indicator (now derived from the sessions snapshot and scope instead of incremental counters, which removes syncInstanceSessionIndicator), the idle fade, and completion, compaction and retry notices read it. Being derived, the scope converges with a late inventory and either family order without refusing, deferring or refetching sessions. Requests are shown where they belong: the dock, banner and header show a nested repository's request in an open project that lists its directory and holds the request, else in the owning folder, so no request is left without a dock; a request of a session in the project's scope stays with it, and needs-input notices follow the dock. Native queues, answering, recovery and receipts are unchanged. An unknown session's compaction notice and retry toast wait for its metadata read, then apply the same scope check, and only while the project is open on the same OpenCode data generation. fetchSessionInfo publishes nothing once its project closed or its client was replaced, and pending reads coalesce per generation. Validation: reproduced on dev against an isolated OpenCode 2.2.0 and the real CLI server (nested session in the parent's list, its permission in the parent's dock); with this change the parent's list, dock and tab indicator stay empty while the nested project shows both. New session-workspace-scope.test.ts and interruption-scope.test.ts fail on dev and pass here, and are in the CI list. Session, status, event, interruption and pending-request unit tests, the dock, permission, header and session-list browser tests, the full browser suite (automation-forms' Mission editor case also fails on the unmodified base) and the root typecheck pass. Eight review rounds ended with no findings. Fixes #1154. |
||
|
|
d1e3fe6946
|
fix(ui): seed an opened session's reducer from its loaded page (#1181)
Opening a session that was not known to be idle filled its empty session
reducer with the SDK's own message.sync, a second transcript read on top
of the window load. Restoring project tabs therefore read the selected
transcript twice, which project-tab-restore pins at one read
("restoring other workspaces must not restart and requeue the selected
transcript read"), and every such open paid an extra request.
The first latest-window load now seeds that empty reducer from the page
it just loaded: its newest 20 rows, the SDK's own page size, through the
existing syncMessages snapshot path, so the reducer reaches the same
state without another read and later deltas for a message that was
already streaming still reach the display. The guards are unchanged:
only an empty reducer outside rotation and resyncs is seeded, for the
session's own directory, and known-idle sessions are skipped.
session-api-running-open covers the warm, cold and new-step opens and
now asserts a single transcript read; the cold case fails without the
seed. project-tab-restore (50/50), the store suites around opencode-data,
session-api, request authority and pruning, and the UI typecheck pass.
|
||
|
|
87ae7050c6
|
fix(tauri): ship a single desktop entry in the Linux AppImage (#1179)
## Problem Every Tauri Linux build since 2026-10-28 fails in "Package and verify Tauri AppImage" with `Expected exactly one desktop entry in the Tauri AppImage, found 2`. This blocks dev pre-releases and releases. ## Cause Tauri's AppImage bundler copies `bundle.linux.appimage.files` into `usr/share/applications` next to its generated `CodeNomad.desktop`, symlinks `CodeNomad.desktop` at the AppDir root, then runs linuxdeploy without a desktop-file argument (tauri-bundler `linuxdeploy.rs`). linuxdeploy logs `No desktop file specified, using first desktop file found` and deploys whichever entry the directory listing returns first. When that is the hidden alias `ai.neuralnomads.codenomad.client.desktop` (`NoDisplay=true`, added in #334), it lands at the root beside Tauri's entry. The 2026-10-27 build picked `CodeNomad.desktop`; today's builds pick the alias. No repository change between them touches packaging. ## Fix - Remove the alias from `bundle.linux.appimage.files` only. Inside an AppImage, files under `usr/share/applications` are not registered with the desktop; integration tools install the root entry. deb/rpm keep the alias for GTK app id association. - Document the constraint in `dev-docs/TAURI_UPDATER.md`. ## Validation - This PR's Tauri Linux packaging job extracts the AppImage and requires exactly one root desktop entry. |
||
|
|
67ae94e5c5
|
fix(ui): complete transcript reads under sustained session streaming (#1156)
Opening a session that is already generating (after a UI restart, or a subagent box opened while its child session runs) could leave its transcript empty or frozen. Two reads restarted on every same-session event, so a continuously streaming session never completed them: the session reducer's message.list proxy looped until no event arrived during the fetch (26,239 reads in two seconds in the regression), and loadMessages retried a latest-window load on any live revision change until it showed a load error. The reducer of a newly opened session also held no rows, so a message already streaming when it opened stayed frozen. Both reads now snapshot the session's reducer rows when the fetch starts. Only destructive events during the fetch (deletion, revert commit, inbox cancellation, compaction start, end or failure) restart a reducer read; otherwise the page is merged with the snapshot: the page is the base, a live row new or changed since the snapshot keeps its live version (a row changed only in the root time.completed that SDK step boundaries stamp takes the page), and rows created during the fetch ride along. Compaction text no longer restarts reads; buffered text is flushed before the snapshot and after the fetch, so it is reduced exactly once. A first latest-window load merges its page the same way and commits it while live revisions advance, then, unless the session is known idle, fills an empty session reducer with the SDK's own latest page for the session's directory, so later deltas reach the display. Reloads and history windows keep their revision checks, and rows kept live are reconciled by the SDK's full-text endings and end-of-round refresh. Regressions cover the sustained-streaming read (one fetch), the reconnect and settlement races, revert and cancellation fences, rows created during a fetch, compaction text during a read and warm and cold opens of a streaming session; each fails with its fix reverted. Store suites, both PR CI UI unit steps, history-navigation (70/70) and the UI typecheck pass. Co-authored-by: Pascal André <pascalandr@gmail.com> |
||
|
|
681b854865
|
feat(missions): bound journal capacity per mission, not per project (#1178)
Step 4 of 6 toward removing the Missions limits (#1167 follow-up), on top of steps 1-3 (#1175, #1176, #1177). ## Change - **Per-mission capacity:** each mission holds at most 2,000 ordinary entries plus its own 36-entry deletion reserve (2,036 per range). **No project-wide cap.** - Capacity checks and appends read only the target mission's range (no full scan). Safety decisions keep their full reads (accepted: their cost grows with history). - Reserve: only the mission's own tombstone and already reserved receipts; works on every storage. - Full project reads bounded per range (and stray entries together); memory = per-mission bound × missions, documented trade-off. - Purge is retention-only (20 newest settled deletions kept); capacity-pressure purge, creation pre-check and related re-checks removed. - Existing mission data is negligible (projects were capped at 20 missions, barely used): **no legacy, migration or rollback handling by design.** - Docs: `MISSIONS.md` safety limits; help sentence on freeing storage updated in 10 locales. ## Validation - Read-budget test at 10,000 history entries; project beyond one mission's bound fully works; capacity checks read only their range; full mission refuses ordinary entries/new Stop but deletes through its reserve (with and without removal). Each red with its guarantee mutated. - Native stage: 2,200 entries across 100 finished missions on an isolated runtime, then create/declare/readout/delete with retention purge. - Server and UI typecheck; 1,508 server tests; `test:help`; `test-missions-native.mjs` on isolated OpenCode 2.2.0 and 2.1.4. |
||
|
|
b38af07fbe
|
perf(missions): serve read-mostly paths from journal hints (#1177)
Step 3 of 6 toward removing the Missions limits (#1167 follow-up), on top of steps 1 (#1175) and 2 (#1176). ## Change Read-mostly paths take candidates from the activation's journal hints and re-read only those ranges: | Path | Before | After | |---|---|---| | Per-turn context hook | 2 full reads per model turn | 1 hinted membership read; 0 for sessions no mission names | | `inspect` (no start), list, `observeTask`, decision candidates | 1 full each | 0 full + candidate ranges | | Outbox listing | 1 full + 4 per item | 0 full + 1 range per open mission (per-item damage gates stay full) | | Presence retention | 1 full per interval | 0 when hints show open work; full read confirms an empty answer before release | | Change notifications after lifecycle/delete | 1 unused full read | 0 | Safety decisions keep authoritative full reads (creation, membership refusal, damage gates, delegation, reports, lifecycle, deletion, cleanup authority, purge, capacity). **Staleness (≤ 60 s, writers outside the generation protocol):** hook treats the coordinator as a non-member meanwhile (tools still select by full read); outbox only delays (deterministic message/receipt IDs, re-checked by a full read). `MISSIONS.md` lists the hinted callers. ## Validation - `journal-read-budget.test.ts` (1,900 finished-mission events): hinted paths do no full scan; safety paths still see writes the hints miss; presence release confirmed by full read; hook single read and tool hiding; ordering/damage parity; 60 s bound. 15 source mutations each fail a named test. - New native stage (100 finished missions, isolated runtime only): `inspect` and the model turn read no full journal; snapshot and damage gate do. - `receipt-projection.test.ts`: an outbox pass refused by its item's full read now counts as failed instead of throwing (same backoff, no duplicate message). - Server and UI typecheck; 1,358 server tests; `test-missions-native.mjs` on isolated OpenCode 2.2.0 and 2.1.4. Note for step 4: mutations stay O(history) (capacity scans, rule-1 full reads). |
||
|
|
21bfe855ce
|
feat(missions): keep an in-memory index of the mission journal (#1176)
Step 2 of 6 toward removing the Missions limits (#1167 follow-up), on top of step 1 (#1175). ## Change - New `MissionJournalIndex` (`missions/journal-index.ts`): activation-owned, never persisted. Holds per-range session references (coordinator, prepared-by, dispatching, native-bound), ordering data, open state, local damage, cleanups and stray-entry count; answers `refs`, `openIDs`, `aggregate`, `cleanups`. - Built by one key-ordered scan (`MissionJournal.scanRanges`) with `missionView`'s parsing/purge/reducer; kept current by marking each mission dirty **before** every ordinary write attempt; a per-project write generation (host-global, like the append lock) detects foreign writers and rebuilds; any failure drops it. Nominates candidates only: decisions must re-read storage. - Cannot observe bytes written outside `MissionJournal` after its build (other OS process, earlier builds); documented and pinned by a test. - Recurrence passage storage marked `isolated`: never feeds or serves the index. - Wiring only (activation creates, passes to `MissionControl`, disposes). **No caller reads it yet.** `MISSIONS.md` "Journal index" paragraph. ## Validation - `journal-index.test.ts`: property test (24 seeds × 70 steps, failed/unconfirmed writes, interrupted purges, damage, new activations, foreign writes) equals full `snapshot()`/`events()` on every answer and rescans only on activation/foreign write; deterministic cross-mission fixture; unreadable records; incarnation/blind-spot. Passage trap test in `native-passage-business.test.ts`. - 11 source mutations each fail a named test. - Server and UI typecheck; 1,343 server tests; `test-missions-native.mjs` on isolated OpenCode 2.2.0. |
||
|
|
9ed72ae068
|
refactor(missions): read one mission's journal range (#1175)
Step 1 of 6 toward removing the Missions limits (#1167 follow-up): make per-operation cost depend on active work, not history. ## Change - `MissionJournal.missionEvents(missionID)` / `missionView(missionID)`: one scan of `<prefix>/<missionID>/` with the same parsing, append/purge exclusion, read bound and purge-remnant resolution as `events()`, reduced by the same private `reduce()` that `snapshot()` now delegates to. - A view's discarded count and damage flags are its own; the snapshot's are their aggregate plus entries outside every mission range. - Recurrence passage adapter accepts its own mission's range. - **No caller switches yet**: behaviour unchanged. `MISSIONS.md` describes the read. ## Validation - `journal-mission-view.test.ts`: 24 seeded 70-step histories of real `MissionControl` operations (all 15 written event types, shared sessions, failed ACKs, pending cleanups, interrupted purges) with injected damage, prefix-extension IDs and timestamp ties; after every step each view equals its snapshot mission, views partition `events()`, counts/flags/cleanups aggregate to the snapshot. Red when the discarded count is dropped, the range's trailing slash removed, or the record check skipped. - `recurrence-passage.test.ts`: 121-event passage read by range. - Server and UI typecheck; all server missions/routes/opencode-missions tests; `test-missions-native.mjs` on isolated OpenCode 2.2.0. |
||
|
|
84d2d0a245
|
fix(missions): free journal capacity when deleted missions settle (#1173)
Follow-up to #1167. ## Problem A project's mission journal holds 2,000 entries and never removed any. Deleting a mission appended a tombstone and cleanup receipts on top of its events, so a project that keeps creating and deleting missions eventually filled the journal permanently: no create, and no delete either (the tombstone needed room). ## Change - **Purge on settlement:** once every cleanup target of a deleted mission has its receipt, its events are removed from native plugin storage (`storage.remove`, passed through a narrow `MissionStorage.remove`; storage without it keeps today's behaviour). - **Retained:** the 20 most recent settled deletions stay in full (cleanup history, lost-ACK checks); all eligible ones are purged when capacity is needed. A deleted mission stays while it references a session another unsettled mission could still clean up. - **Deletion record:** one compact record per purged mission at `codenomad-missions/v2/deleted/<projectToken>/<missionID>`, outside the scanned prefix (no journal capacity, exact-key reads). It refuses recreation (`mission-deleted`) before any native effect on create, prepare and coordinator start replays, and answers an exact deletion replay with the original summary. - **Crash-safe, resumable:** record first, then `mission.created`, tombstone, intents, receipts; an incomplete group with a record is dropped from readers; late writes are removed by the next purge; damaged journals are never purged. - **Full journal recovers:** a 36-entry reserve, usable only by tombstones and reserved receipts; reads bounded at 2,036. - Legacy tombstones are purged the same way. - Docs: `MISSIONS.md`; help (EN + 9 translations) cleanup-history sentence. ## Known risks - Rolling back to an older build reads a mid-purge journal as damaged, a reserve-using journal as over its limit, and ignores deletion records (a retried creation could recreate a purged mission). - Under capacity pressure the recent cleanup history is purged too; a delete whose ACK was lost in that window then shows a false "missing" error. - Deletion records accumulate (one small exact-key entry per purged mission). ## Validation - `journal-purge.test.ts` (10 tests, each red with its piece removed): 200 create/delete cycles, recreation refusal and replay, pending cleanup kept, crash at every step, late writes, legacy tombstones, full journal of live missions, no-remove storage, shared sessions, damaged journals. - Native `test-missions-native.mjs` gains a settled-deletion purge stage (red when the plugin lacks `remove`). - Server and UI typecheck; 1,331 server missions/routes tests; `test:help`; native run on isolated OpenCode 2.2.0. |
||
|
|
49f2efe8f9
|
fix(sessions): read OpenCode media blobs like native history (#1172)
Follow-up to the #1158 review (pre-existing P3). ## Problem With OpenCode 2.2.0's opt-in `experimental.media_blob`, inline files are stored as `media:<sha256>` refs (`blob`/`blob_ref`) and native reads hydrate them. CodeNomad's direct SQL readers saw the refs: pruning reported a false `conflict` (the UI hashes hydrated content), assets looked unavailable, navigation emitted unhydrated JSON. ## Change - New `session-pruning/media-blob.ts` mirrors native `blob.ts` hydration (tool files → data URIs, user files → data, missing blob → native placeholder), bounded before loading bytes. - Preview, prune candidates, prune transaction, navigation windows and assets read the hydrated form; existing size bounds apply to it. - Pruning compares the hydrated form, writes survivors back exactly as stored, and like native content rewrites releases the message's `blob_ref` rows for hashes it no longer names, deleting a blob only when unreferenced. - Decided by stored representation, not version: no refs → unchanged; refs without recognisable blob tables → `unsupported_storage` (distinct from `conflict`). - Docs: `SESSION_PRUNING_RPC.md`, `SESSION_HISTORY_NAVIGATION.md`, `PANEL_EXTENSIONS.md`. ## Validation - `media-blob.test.ts` (projection, placeholders, bounds, preview, prune, shared blob retained, unsupported storage, assets, navigation). - New native check `scripts/test-session-media-blob-native.mjs` (run by `test-session-pruning-native.mjs`): 2.2.0 with media_blob passes; 2.1.4 passes; red on unmodified dev. - Server and UI typecheck; all session-pruning, route, proxy and panel-asset tests. |
||
|
|
4e113583c8
|
fix(missions): surface a failed first recurring admission at once (#1171)
Follow-up to the #1158 review (pre-existing P2). ## Problem When the first native admission of a due recurring passage threw, the runner returned `unknown`, the due adapter reported `pending` and the clock treated the wake as successful: warning cleared, sleep up to 1 h. The passage looked silently `starting` for that hour. ## Change - `native-recurrence-due.ts`: new wake result `admission-unknown` for that case. - `native-service-clock.ts`: after its reread, while the passage is still pending and unadmitted, that result is a failed wake: the existing redacted `admission-failed` warning is published at once and the existing 30 s backoff retries the same pending identities. Nothing archived, no cursor advance, no new identity. - `MISSIONS_RECURRING.md` describes it. ## Validation - `recurring-day.e2e` test M: injected admission refusal and failing observation read (new fixture `failQuery`): warning on the first wake, no start before 30 s, exactly one admission with the original identities at 30 s, survives restart. Red without the fix. - Server typecheck; all missions tests; `test-recurring-simple-native.mjs ABCD` on isolated OpenCode 2.2.0 and 2.1.4. |
||
|
|
4d8db216b8
|
fix(missions): remove the 20-mission project limit (#1170)
Fixes #1167. ## Problem A project refused its 21st mission with `Project mission limit reached`, counting finished missions too. Long sessions whose coordinator starts missions itself hit it and could only continue after deleting missions by hand. The limit only bounded refresh cost. ## Change - Creation (panel, `mission_prepare`, the coordinator's `mission_inspect` start) no longer counts missions; the snapshot lists every mission. - The `mission-limit` code, its no-effect receipt path and `missions.control.creation.limit` (10 locales) are removed; `request-conflict` stays the only no-effect creation refusal. - Refresh cost stays bounded: activity observation reads at most 160 actors per refresh (the former 8 × 20), active missions first, then the most recently updated; the rest read as unknown. - Unchanged: 8 actors and 96 tasks per mission, the 2,000-entry project journal (follow-up), the 20 most recent settled cleanups. - `dev-docs/MISSIONS.md` states the current bounds. Help never documented the limit; nothing to update there. ## Validation - `control.test`: 25 missions created and listed. `activity.test`: 31 missions / 241 actors → exactly 160 session reads, the oldest active mission observed. - Creation-fence forged/mismatched no-effect receipt matrix kept on `request-conflict`; damaged-storage refusal kept. - Server and UI typecheck; all server missions, routes and native missions tests; UI mission and i18n unit tests. |
||
|
|
e3a03f3fde
|
fix(ui): reconnect silently dead SSE streams so missed questions recover (#1162)
On mobile, and behind tunnels or proxies that black-hole an idle connection, the shared event stream could stay open while no data arrived. The UI kept showing a connected state, so a question asked meanwhile never reached the interruption dock until the app reconnected for another reason. ServerEvents now runs a stream staleness watchdog (lib/stream-staleness.ts): a connected stream that carries no batch, open or ping for 45 s, checked every 15 s, is restarted once per silent episode, and an `online` event or the page becoming visible restarts it as soon as that threshold has elapsed. Each connection attempt gets its own threshold, and only callbacks from the current connection count as activity. The restart takes the ordinary reconnect path, whose refresh reloads pending questions and permissions through syncPendingRequests. The server pings every 15 s, so a healthy idle stream is never restarted; the refresh conventions document that coupling. The watchdog interval is unref'd so Node test processes exit naturally. stream-staleness.test.ts covers the threshold, episodes, the per-connection baseline, the probes and the lifecycle, and now runs in PR CI. The UI typecheck, the CI UI unit-test set, the server-events lifecycle test and the event-subscriber and interruption-dock browser tests pass. |
||
|
|
444991d352
|
fix(events): reconcile a recipient instead of dropping an unroutable event (#1165)
When the event relay could not resolve whether a workspace owns an
event's location (the lookup failed twice, for example while the shared
OpenCode connection was being replaced), it logged a warning, cached
"not owner" for two seconds and dropped the event while the stream stayed
connected. A question form or any other update for that workspace could
then stay missing until an unrelated refresh.
The relay now treats such a failure like a lost connection for that
recipient alone: the failed lookup is evicted from the cache, and that
instance receives an `error` status ("event routing failed") followed by
`connected`, which runs the client's existing reconnect reconciliation.
Other workspaces keep receiving the event, and the shared subscription is
not failed, so one workspace's lookup cannot resync every instance.
Pulses are leading and trailing per instance, at most every 5 s and
doubling to 60 s while failures persist; pending pulses are cleared on
reconnect, workspace stop or error and shutdown, and skipped when the
stream is not connected or the workspace is no longer listed.
Repeated pulses exposed that TrailingResyncCoordinator chained every
request, so a slow pass accumulated one full reconciliation per request.
It now keeps one active pass per instance plus a dirty bit: requests
during a pass share one trailing pass, and each returned promise settles
after a pass that began after its request. Passes still start on a
microtask, so callers keep their timing.
A relay regression covers a persistently failing recipient next to a
healthy owner (immediate pulse, trailing pulse, doubling backoff), and a
coordinator regression covers eight requests during an active pass; both
fail without their change. The server workspace and event suites, the
CI UI unit-test set and both typechecks pass.
|
||
|
|
5a49ac36b2
|
feat(history): rank whole-project transcript search, keep conversation search chronological (#1164)
## Summary PR 2 of 2 after #1161. The transcript search window's **project scope** uses the ranked engine; the **conversation scope** keeps its literal, chronological, paginated search and code path. - **Project scope:** - One request returns the best 20 results in relevance order: rare words weigh more, an exact phrase ranks first, and each conversation appears once before repeats. There is no next-page traversal. - Every query term is highlighted in the excerpts, in the preview and in the destination conversation when a result is opened. - Compact notes report a partial scan, excluded folders, or a runtime without ranked search. - **Route:** `POST /api/workspaces/:id/session-history/ranked` calls only the plugin's `historyRanked` RPC. - It applies the same ownership, provenance, deletion, connection and cancellation fences as the literal history route, through scope helpers shared in `session-history-scope.ts`. - It validates counts and every contributing location before publishing and strips provenance. - It records an unsupported method per connection. - Disjoint worktree roots are interleaved by native rank before session diversity is applied. - UI strings and help pages are updated in all ten locales. ## Validation - Server and UI typechecks pass. - Routes 13/13, covering ownership rejection, fences, invalid provenance and the unsupported state. - Store tests 6/6. - Chromium: - history 2/2; - search-highlight 12, plus 1 optional benchmark skipped; - session-search 1/1. The browser cases cover ranked order, highlighting, opening a result and the unchanged conversation scope. Oversized files touched: `message-section.tsx` (~1,816 lines), `message-block.tsx` (~1,775), `api-client.ts` (~833). |
||
|
|
7a206a9297
|
feat(history): rank past-session search and expose it to agents as session_search (#1161)
## Summary Agents can now find past discussions themselves. The bundled session-pruning plugin registers a Code Mode tool, `codenomad.session_search`, ranked by relevance over the calling session's own project history. This is PR 1 of 2: server/plugin only; the search window keeps its current behaviour until PR 2 wires the same engine. - **Tool scope:** the caller's directory tree, native workspace and project (plus `global`) history, never other projects. The caller's location is checked before and after the read, and excerpts are flagged as untrusted transcript data. The tool follows backend presence like the plugin's RPCs. - **Ranking:** up to six Unicode terms; a precise SQL prefilter over rowid chunks (superset of the JS judge: ASCII folding, Unicode case classes, literal `\u` escape fallback) yields per-term IDF weights. The best candidates are judged one bounded body at a time on word boundaries, with an exact-phrase bonus, prose before tool payloads, recency as tie-break and one hit per session before repeats. Optional inclusive UTC `since`/`until`. Candidate, session, judge and time caps are reported. - **Shared limits:** scope SQL, readable-part extraction, the 16 MiB message bound and 40 ms slices move to `history-content.ts` and are shared with the literal search. - **For PR 2:** a versioned `historyRanked` RPC returns the same hit fields plus per-session provenance, so the broker can validate ownership as for `history`. Existing RPC methods, HTTP routes and UI are unchanged; the generic RPC proxy stays closed. Inspired by `@rizal72/oc-session-search`. ## Validation - Session-pruning tests 98/98, including the package regression; native plugin bundle 1/1; server typecheck. - Regressions: Unicode terms and accented case variants, weights, phrase bonus, diversity, dates, prefilter precision (multi-line and accented messages lacking a term are not candidates), a rare old match outranking 600+ newer frequent ones, scope isolation, cancellation, caps, skipped rows, tool registration and presence disposal. - Isolated native history fixture on OpenCode 2.2.0: 23 checks, including a ranked query (3 hits across 2 sessions). No shared daemon or user database was used. |
||
|
|
687085463f
|
fix(ui): keep session alerts out of the Preferences window (#1163)
#1148: with the native Preferences window open, a provider's usage-limit error showed the same modal in the main window and in Preferences, and each copy had to be acknowledged separately. Preferences renders only the settings screen, but it imports the shared stores, and stores/sessions.ts wired every session event handler on import. Its event stream therefore ran handleSessionError, which opens an alert for each session.execution.failed, and the other session handlers (TUI toasts, permissions, session and worktree updates) as well. Session toasts only stayed silent there because no session is loaded. The session handlers are now wired only outside the Preferences window, which shows no session. Catalog invalidations and connection status keep their own wiring, so settings still follow live changes. A session alert, toast or notice appears in the windows showing sessions and is acknowledged once. dev-docs/SETTINGS.md records the rule. Validation: a new preferences-window browser test renders the real Preferences window, delivers a session.execution.failed event through the real server-event dispatcher (the connection status proves the window received it) and asserts no alert appears; it fails on dev. The Preferences browser tests, fixture wiring, session event unit tests and the root typecheck pass. |
||
|
|
a868a8e720
|
feat(missions): blue prepared missions, quiet interrupted chevron, full-panel create form (#1155)
## Changes - **Prepared missions are blue.** A created, never-started mission gets a blue mark (`--accent-primary`), like a prompt ready to send. - **Interrupted no longer bubbles into the collapsed chevron.** Like a collapsed session parent, the chevron only counts answers, running work and unseen completions; interrupted, paused and prepared missions colour only their own rows. The unused `missionsPanel.attention.interrupted.*` keys are removed from all locales. - **The creation form replaces the list and detail.** While creating, the current-mission line, list, schedules and selected detail are hidden (still mounted, so pending controls and retries survive) until Create or Cancel. - **Nesting depth in the creation form.** Options embed the existing project depth editor (`MissionSubagentDepth`). OpenCode only reads `experimental.subagent_depth` per config file, so the value is the project's and applies to every conversation there; the editor says so. - **Button templates.** `.window-text-button` had no rule anywhere, leaving ~18 mission buttons unstyled. It is now a shared primitive in `styles/components/window.css` (borderless accent text, hover tint, muted when disabled). The creation footer's **Save as brief** uses the footer's `button-secondary` template; the bordered **Answer** button uses `.window-action`. ## Docs `dev-docs/MISSIONS.md`, `dev-docs/UI_CONVENTIONS.md` (window button primitives), and `docs/help/*/missions.md` in English plus the nine translations. ## Validation - UI typecheck; mission and i18n unit tests; `npm run test:help`. - All 34 `tests/browser/mission*.test.ts` files pass. Updated: `mission-status-marks` (blue computed colour, quieter chevron label), `mission-editor-profiles` and `mission-editor-lifetime` (the form replaces the list; leave it before browsing). - Visual check of the real components through the mission-control fixture: prepared mark, collapsed chevron, full-panel form with depth, footer buttons, selected detail. |
||
|
|
339a0844ca
|
fix(opencode): stop an emptied project experimental block from capping subagent depth (#1153)
## Problem
A mission asked to nest subagents three levels deep stopped at the first
level: every nested call returned `Subagent depth limit reached (1).
Increase "experimental.subagent_depth"…`, although the global OpenCode
config sets `experimental.subagent_depth: 5`. The project file
`.opencode/opencode.jsonc` contained only `"experimental": {}`.
## Cause
- OpenCode reads `Config.latest(entries, "experimental")?.subagent_depth
?? 1` (`core/src/tool/plugin/subagent.ts:129`): the **whole**
`experimental` object of the highest-priority document that has one,
unmerged (`core/src/config.ts:24-27`). An empty project block therefore
hides the global depth.
- CodeNomad's depth editor reset (**Inherit**) removed only
`subagent_depth`, leaving that empty block behind.
- The editor computed the effective depth as the last document defining
`subagent_depth`, so it showed the global 5 while OpenCode applied 1.
## Change
- **Inherit** also removes a project `experimental` object it leaves
empty; foreign experimental keys, comments and formatting are kept.
- The effective depth follows OpenCode's read; a project block without
the depth reports the native default (unknown when the contract declares
none) instead of the hidden global value.
- `dev-docs/MISSIONS.md` describes both. Help already says that an empty
field inherits, which is now true in every case.
## Validation
- `subagent-depth-settings.test.ts`: shadowing read, Inherit keeps
foreign experimental keys, an empty project block reported as hiding the
global depth and removed by Inherit after an override.
- Server typecheck.
- `scripts/test-subagent-depth-settings-native.mjs` against an isolated
OpenCode 2.1.4.
|
||
|
|
048cd65183
|
fix(missions): one status palette, no agent-decided failure, resumable missions (#1150)
fix(missions): one status palette, no agent-decided failure, resumable missions (#1150) #1147 reported a red chevron beside a running mission because another, older mission had failed. Missions mixed two colour systems: rows showed session-style attention when there was some and otherwise a lifecycle colour (blue running, green completed forever, red failed), the collapsed chevron added "failed", and the task tree had its own palette. Red meant both "your answer is expected" and "this ended badly", a failed mission offered no action, and a one-time coordinator could end a mission as failed on its own. Colour now follows the session list everywhere (mission and schedule rows, the selected mark, task icons, the chevron) and shows only what needs attention now: - red: an answer is requested, counted like Needs you (declared actors and observed family; schedule rows without a passage read count the live passage's conversation and loaded subsessions); - yellow, pulsing: observed work; - orange: interrupted or paused, with an action to continue (an interrupted schedule, or a running one whose latest run failed); - green: a completed mission this window has not shown yet; - everything else is neutral: outcomes (done, failed, blocked, waiting, retired) are status text and icon shapes. missionMark/scheduleMark in mission-picker-model.ts compute one mark per entry. Like a collapsed session parent, the chevron colours only while the list is collapsed, from the other entries (paused is not counted). "Seen" works like an idle session: mission-view-state keeps the completed missions this window has shown under its own layout key and signal, so recording one never wakes project-view readers (an early write through the project view re-ran session-view effects and aborted an in-flight outline read). The first read counts missions already completed as seen; a completion is marked when selected while the panel is visible in a focused window (createDocumentAttention, now shared with SessionView). One-time coordinators no longer decide a failure: the plugin's MissionControl sets completedFinalOnly, and the refusal, coordinator context and tool description tell the coordinator to ask the human through the native question tool. Only the human stops a mission. Recurring passages keep their own control and completed|failed. Interrupted missions get an action: - A running mission's coordinator owes its final report: observed idle with an empty inbox and no coordinator notification still being delivered (native returns excluded), it projects idle-without-report; when nothing else runs the mission reads Interrupted and coordinator recovery replaces Pause. - A failed mission reads as interrupted unless stopped since, and the same recovery resumes it: under the mutation lock it refuses with already-member when one of its sessions joined another active mission (as at creation), otherwise appends mission.reopened at the exact revision and sends the ordinary recovery nudge at the new revision. The reducer resumes only a failure, drops its summary and lets a later Stop end it; a later finish uses a revision-bound event identity. The unwired authority policy accepts the same resume. dev-docs/MISSIONS.md records the rules; the Missions help page gains "Colours and interrupted missions" in all ten languages, and new strings are added in every locale. Validation: mission-resume.test.ts (completed-only refusal, passage-style failed finishes, resume once with conflicts and no second nudge, shared-session refusal, reducer reopen/Stop rules); activity.test.ts (owed final report, queued inbox, pending and native-return reports, non-running missions); mission-picker-model unit tests; the new mission-status-marks browser test (marks, collapsed chevron, unseen completion surviving reload, recovery replacing Pause, neutral done and blocked tasks, failed mission resume, recurring requests including independent actors and a zero selected count, quiet stopped/paused schedules), red on dev; updated mission-control, mission-briefing and mission-recovery tests. All Missions server tests, the full browser suite (four shards), help checks, UI and root typechecks pass. |
||
|
|
24d9a18fa8
|
fix(shell): hide each panel at the end of its own compression (#1149)
Narrowing a window moved the panels back and forth: at 800 px the right panel closed while the left one jumped back from 220 to 340 px, then shrank a second time; at 600 px the left panel closed and the right one reappeared, only to close again at 580 px. Widening repeated the sequence in reverse with its own swaps. Narrowing now shrinks the conversation to its minimum, then the right panel to its minimum, where it hides, then the left panel to its minimum, where it hides. Each panel shrinks once and hides at the end of its own compression, so the other panel never changes width at that moment and only the conversation takes the room; widening retraces exactly the same layouts. Each side panel keeps whether the user wants it (opened, or closed while docked) and docks while the window has its room, in a fixed order: the session panel first, the right panel only beside the full session panel. A wanted panel without room hides and returns with the room, so the layout follows the window width alone; a minimizing window's tiny viewport hides both panels and restoring it brings them back. A panel opened without room floats over the conversation; dismissing it keeps the docked choice, and it docks once it has room. A panel closed while docked stays closed. Dismissing a floating panel by clicking outside leaves focus on the clicked control; a panel the layout hides hands focus to its toggle when it held focus. Touch tablets remember the wanted panels per orientation. A floating panel never docks while the window already has its new shape but the orientation has not changed yet, so rotation memory records only the user's choices; the shape is read from the orientation media query, which stays reliable on zoomed, near-square windows. Implementation: `resolveEmbeddedDrawers` docks the right panel only beside the left one at its chosen width and caps each panel's chosen width by the room. `useDrawerChrome` replaces the open state, the "awaiting room" marks and their effects with a wanted and a floating signal per panel; docked and shown states derive from them and the window width. The public API keeps its shape (`leftOpen` means shown; `setLeftOpen(true)` opens docked or floating, `setLeftOpen(false)` dismisses silently). Validation: a shell-navigation sweep narrows 1400 to 400 px in 20 px steps, requiring panels never to grow back or reappear, then widens and requires the same layout at every width. Regressions cover outside dismissal focus, rotation with a floating panel, a wanted panel docked by restored room across rotations, and rounded near-square viewports. drawer-layout unit tests cover each panel hiding at the end of its compression. Every browser file that drives the panels passes; the user validated the behaviour in a separate Electron dev instance; four gatekeeper rounds ended with no P0-P2 finding. Help is updated in all ten languages. |
||
|
|
2064682d40
|
test(ui): share one fixture daemon per run across the browser suite (#1146)
Browser tests that don't need their own server now borrow their fixture page from one Vite server per run instead of each starting one. run.mjs (local) and shard.mjs (CI) spawn the fixture daemon as a child. It announces its endpoint and secret as one stdout line; the runner hands them to the test processes through the environment and stops the daemon with an authenticated request when its slice ends, failing the run when that teardown fails or has to be forced. The daemon watches its owner's pid and cleans up after an owner that ended without stopping it; on Windows it is spawned detached so the owner's exit doesn't end it first. A test file run on its own gets a daemon child of its own. fixture-borrow.ts registers namespaced routes, fixture-exemptions.ts lists the files that keep their own servers (stateful SSE middleware, custom transforms, Electron hosts), and fixture-wiring.test.ts enforces borrow-or-exempt. AGENTS.md sanctions the daemon's single-writer optimizer cache, and dev-docs/BROWSER_TESTING.md documents the commands, the daemon model, logs and limits. Twenty owned fixtures also dispose their cache when createServer() rejects. Measured locally, 20 representative files run one at a time took 797 s against 1,091 s on owned servers (about 27 % less); the CI shards' test streams show no consistent change. Validation: the daemon, run, borrow and wiring tests pass (11/11), including split and noisy handshakes, a start that never announces, pipelined shutdown requests, a forced stop seen through by a minimal runner, and protected cleanup after a killed owner; the full suite ran through npm run test:browser on Windows, and the four CI shards passed. |
||
|
|
fb22f6e969
|
feat(shell): close panels that lose their room and reveal list choices (#1144)
In a window too narrow for a docked panel, the panel floated over the conversation and covered the messages. On phones and narrow windows, the floating session panel also stayed open over the session the user had just chosen, so every choice needed a second tap to see it. A docked panel that loses its room (the window narrows, or the other panel opens) now closes instead of floating. It returns when the room does, unless the user opened it meanwhile; a panel closed by the user stays closed. Minimizing a window briefly reports a tiny viewport, so without the return every minimize would lose both panels. A panel opened without room still floats over the conversation, as before. Choosing a session, the shown one included, or Instance Info from the floating session panel's list now closes that panel to show the choice; reopened, it still marks the shown session. Back and Forward select without closing it. Docked panels are unchanged. Implementation: `useDrawerChrome` closes an open drawer when its docked state turns false and marks it as awaiting room; an effect reopens it, the session panel first and the other only if both then fit docked, and opening a drawer clears the mark. The layout inputs are shared by the initial layout, the docked state and the return. On touch tablets each rotation's orientation memory decides, counting a panel closed for room as open in the orientation it leaves. A panel closed by the layout hands focus to its toggle when it last held focus, even when its re-render already removed the focused control: the shell remembers which panel's content last took focus until focus deliberately moves to the page. The sidebar's list selection handler closes the floating session panel; the history buttons keep using the ordinary selection path. Validation: shell-navigation browser tests cover closing and returning (narrowing, a simulated minimize, the other panel taking the room, a user-closed panel staying closed, a panel opened without room floating), the phone list, re-selection and Back/Forward behaviour, and each panel's focus hand-off; drawer-width covers a 1024x768 tablet rotating twice. All fail on the previous code. Help is updated in all ten languages. |
||
|
|
2eb1fb1797
|
fix(tauri): stop F11 from freezing the Linux app (#1143)
Pressing F11 froze the Tauri app on Linux under X11 or XWayland: GNOME reported "CodeNomad is not responding" and the window never went fullscreen. The released 1.0.0 AppImage froze the same way, so this predates the AppImage display-stack fix (#1138), which keeps GTK on X11 for this very shortcut. A backtrace of the frozen process showed a deadlock between two threads: - global-hotkey's X11 thread received F11 and ran the plugin handler on that thread. toggle_fullscreen_window then asked the main thread whether the window was focused (is_focused) and waited. - The key grab itself moves X11 focus. Its focus events reached the main thread, where update_fullscreen_shortcut re-registered F11 and waited for the X11 thread to confirm. The handler now queues the toggle on the main thread (run_on_main_thread) and returns, so the shortcut thread is free to answer the registration. Two leaks from #1138 are closed on the way: - prefer_x11_display exported GDK_BACKEND=x11,wayland, which every child inherited: the backend, the shared OpenCode service it may start, agent shells, editors and the browser opened for links. Their GUI programs then ran under XWayland. CodeNomad now sets its own GTK's preference with gdk_set_allowed_backends, and children keep the session's choice. A GDK_BACKEND set by the session still wins. - Children that leave an AppImage's mount started in AppRun's working directory inside it (PWD=/tmp/.mount_*/usr for the browser opened by a link). use_host_environment now starts them in / while ours is inside the mount; callers that pick a directory (backend, editors) still do. dev-docs/HOTKEYS.md records that a global-shortcut handler only queues work on the main thread; dev-docs/TAURI_UPDATER.md describes the GTK backend preference and the working directory. Validation on Ubuntu 24.04 GNOME Wayland, with the #1141 CI AppImage repacked around a binary built from this branch: - After focusing the window, about ten F11 presses toggle fullscreen in and out. The main thread stays in its event loop (poll), where the CI build and the 1.0.0 release deadlock on the first press (gdb backtraces). - The window is still an XWayland client. The backend and the OpenCode service carry neither GDK_BACKEND nor mount paths. - The help link opens Firefox with / as working directory, PWD=/ and no GDK_BACKEND. - The appimage_environment and appimage_exit tests pass, and the Linux release binary builds. |
||
|
|
77675a4a0b
|
fix(missions): keep an unsent mission draft across tabs and projects (#1142)
An unfinished mission draft behaved differently depending on how the user left it (#1130). After switching to another project and back, the draft was still shown but Create stayed disabled until the editor was cancelled. After switching the right panel to Files or Status, the draft was gone. Two causes: - MissionControl kept each open editor's activity as a one-shot view capture (createMissionViewFence). A capture is invalidated by any identity or visibility transition, including a round trip, so once the project was hidden the editor stayed inactive for good, with Create and its network-backed controls disabled. - The open editor and its draft lived in component signals. The right panel renders only the selected tab, so another tab disposed both. The open editor now lives in window memory per panel scope (stores/mission-editor-state.ts, keyed by instance and folder), never in the native view layout. Its activity is the live view activity, so a hidden project's editor works again on return. Operations still capture their own view fence at submit and ignore completions from an earlier view lifetime. When the panel unmounts, an idle editor stashes its draft in its entry and reopens with it: objective, title, notes, schedule, playbook, profiles and execution mode, the chosen brief, open sections, and its request identity. An editor with a request in flight or held still closes and settles through its creation or recurrence hold, which keeps precedence over a stashed draft. Cancel, a saved result or a deletion closes the entry, so a later editor starts blank. dev-docs/MISSIONS.md records the editor's window-memory scope and that a view capture guards one operation, never a component's lifetime. Validation: a new mission-editor-lifetime browser test drives a create and an edit draft through a hidden/visible round trip, a panel remount and an instance round trip. It checks the restored fields, that Create or Save is enabled and sends the kept draft once, and that cancelled or saved editors stay closed. On dev it fails at the first round trip, with Create disabled. The existing transition matrix also asserts that an editor unmounted while its request is in flight does not reopen. The Missions browser suites, the UI own-tsconfig and root typechecks pass. |
||
|
|
13450749b8
|
feat(preview): open loopback conversation links in the web preview (#1119)
When the frontend is reached from a browser, an agent-printed `http://localhost:3000` link points at the browser's machine, not the backend host that runs the dev server, so clicking it opens a dead tab. Many development apps also break inside the opaque preview, where `localStorage`, `sessionStorage` and `document.cookie` throw. A plain left click on an explicit http(s) loopback link (`localhost`, a literal `127.x.x.x`, `[::1]`, `0.0.0.0`) in session Markdown now opens it in that session's web preview. Middle and modifier clicks, external links and Markdown rendered without a session keep the browser default. Interception waits until the server meta is known and stays off where previews are unavailable, such as Remote Control while the host keeps web previews off. Intercepted links without an author title get a localized tooltip naming the Ctrl/⌘-click and middle-click escape hatch. The preview runtime bridge gives opaque documents whose storage access throws one stable in-memory `localStorage`, `sessionStorage` and `document.cookie` object each for the current page load, with property access and enumeration, `Max-Age` taking precedence over `Expires`, and detection by reading alone, so working storage (a per-token preview host opened on its own) stays native. The help in all ten locales and the preview paragraph of architecture.md describe both. Validation: the loopback predicate unit test, the runtime-bridge shim tests, the Markdown loopback-link browser test, the server preview, proxy and Remote Control suites, both typechecks and the help checks pass; the interception and the shim were checked in Chromium against the real preview proxy and Remote Control surface. |
||
|
|
a6c19d19d7
|
fix(tauri): leave the display stack to the host in the Linux AppImage (#1138)
The Tauri AppImage showed a black window on up-to-date Linux systems (#1136: Arch, sway, AMD, Mesa 26.2). WebKit aborted with "Could not create surfaceless EGL display: EGL_BAD_ALLOC", and no WebKit or GDK variable helped. Tauri CLI 2.11.5's linuxdeploy copied the build runner's display stack into the image: Ubuntu 24.04's libwayland-client, -cursor, -egl and -server, libxkbcommon, libxcb-render and -shm, libXau and libXdmcp. AppRun puts usr/lib first on the library path, so the host's Mesa EGL loaded the stale libwayland-client and failed even on the surfaceless platform. This is tauri-apps/tauri#15976, fixed upstream in CLI 2.12.1. On a current Arch userland (Mesa 26.2.5, wayland 1.26), `eglinfo -p surfaceless` initializes with the host libraries and fails with the 1.0.0 image's bundled ones, or with its libwayland-client alone. Packaging: - The Tauri CLI moves to ^2.12.4. - The Linux job defines APPIMAGE_HOST_LIBRARIES once (wayland, xkbcommon, xcb, X11, Xau, Xdmcp, EGL, GL, gbm, drm). It is passed to linuxdeploy as LINUXDEPLOY_EXCLUDED_LIBRARIES, because 2.12.4's own excludelist still deploys GDK's display libraries. - The AppImage verification step rejects an image that bundles any of them. Runtime, because the 2.12 GTK hook leaves GDK_BACKEND unset and exports mount-scoped module paths: - main() sets GDK_BACKEND=x11,wayland unless the session sets it. This keeps the XWayland windowing that the F11 X11 key grab needs, with a Wayland fallback. - appimage_environment.rs drops variables inside the mount (byte-wise, plus the lossy spelling of the Node-serialized shell snapshot) and the mount's path-list entries. GIO_MODULE_DIR, unlike GIO_EXTRA_MODULES, replaces the host's module directory. It applies to children that outlive the mount: the backend (and through it the shared OpenCode service), the editors CodeNomad opens, and link handlers. - Links open through external_open.rs. On Linux it uses the opener plugin's launchers with the host environment, detached; elsewhere it delegates to the plugin. The renderer reaches it through a new open_external_url command (http, https, mailto). The windows hold that command instead of opener:allow-open-url. - The plugin's injected click handler is off. Its replacement, lib/native/external-link-clicks.ts, takes the same decisions in the application and Preferences windows and in trusted same-origin SideCar documents. Opaque previews and panel extensions stay fenced. - The unused @tauri-apps/plugin-opener npm dependency is dropped. Validation: - Ubuntu 24.04: the Linux crate compiles, and the appimage_* tests (path lists, non-UTF-8 entries and mounts, lossy snapshot) and the capability test pass. - The inventory check accepts GTK, WebKit and GLib, and rejects flat, multiarch, dangling-symlink and GLES copies. - Browser tests drive the real link handler (target="_blank", Ctrl, Shift, nested, mailto; plain, Alt, tel and custom schemes untouched) and the real SideCarView and preview frames in a stubbed Tauri window. Each fails without its fix. - The UI own-tsconfig and root typechecks pass. |
||
|
|
4ced2d18da
|
fix(missions): give mission roots OpenCode session identifiers (#1133)
## Problem Missions on OpenCode Zen's free models (for example `opencode/deepseek-v4.1-flash-free`) fail at the coordinator's very first model request with **"Free usage limit reached. Connect a provider or subscribe to OpenCode Go…"**, while ordinary conversations on the same model and network keep working. ## Cause Missions created its coordinators, recurring passage coordinators and independent task roots with `ses_` + 26 hex characters of a SHA-256. OpenCode passes that ID to the provider as the session identity (`x-opencode-session-id`, `x-opencode-session`, `X-Session-Id`, `x-session-affinity`; `core/src/session/model-request.ts`), and native children send their **root's** ID as affinity (`core/src/session/affinity.ts`), so one such root blocks the whole mission family. Isolated probes against the real free tier (keyless OpenCode 2.1.4, OpenCode-generated control IDs accepted throughout) characterized two checks: | Check | Requirement | Otherwise | |---|---|---| | Shape | `ses_` + 12 lowercase hex + 14 alphanumerics (any case/charset for the 14) | 403 `FreeTierError` | | Time | the 12 hex decode, as OpenCode's descending identifier, to a date between ~86–90 days before and 24–30 h after the request | 429 `FreeUsageLimitError` | A hash prefix decodes to an arbitrary date over the ~795-day cycle, so most mission roots failed the time check. A real mission A/B (UI-created vs. the same mission attached to an OpenCode-created root) reproduced it: derived coordinator refused, native coordinator completed; a child with an ordinary ID under a hash-ID parent was refused too. ## Change - `missions/session-identity.ts`: mission roots take OpenCode's descending identifier for their **real creation time** followed by 14 hex characters derived from the mission or task identity. Ownership stays provable from the ID alone. - Creation: one-time coordinators (`control.ts`), independent task roots (`control.ts`) and recurring passage coordinators (`recurrence-store.ts`, passage `createdAt`). The time comes from the real clock (`options.now ?? Date.now()`), not the journal's monotonic timestamp, which may run ahead of it. - Ownership checks match the derived suffix and also accept the original 26-hex derivation held by existing journals: managed-root creation (`mission-root-creation.ts`, `ensureActorSession`), cleanup targets (`session-cleanup.ts`), recurring passage parsing/archive/current reads, and the creation readback (`mission-creation-pipeline.ts`, which no longer needs the exact coordinator ID in advance; the hold's `sessionID` is optional). - `dev-docs/MISSIONS.md` documents the identity rule. Task roots and recurring coordinators record their ID before creating the session, so retries still reuse the exact root. A one-time coordinator takes the clock at creation. ## Compatibility Existing missions keep their coordinator IDs (and remain refused on free models); new missions are accepted. Like any OpenCode session, a mission root older than about three months is refused by the free tier. No UI, API or journal schema change; no user-facing help change. ## Validation - `session-identity.test.ts`: time prefix against IDs OpenCode 2.1.4 generated at known milliseconds, descending order, suffix derivation, legacy acceptance, rejection of other missions/tasks/shapes. - Server typecheck; all Missions server tests (`src/missions`, mission routes, plugin, `src/opencode/missions`): 1140 pass, 1 pre-existing environment skip. - Native scripts against an isolated OpenCode 2.1.4: `test-missions-native`, `-trajectory`, `-family-control`, `-contract` pass. - Real free tier: a mission created through the real CodeNomad routes and plugin had its coordinator's first request accepted with the new identifier. The remaining real checks (native subagent child, independent task root, recurring coordinator) are pending: the probing exhausted the network's free allowance (an OpenCode-generated control is now refused too) and will be rerun after it resets. |
||
|
|
3f9be8e364
|
docs(previews): describe how root-relative preview resources resolve (#1132)
architecture.md stated that opaque preview documents send no Referer to CodeNomad, so all their root-relative resources relied on the runtime bridge's path rewriting. Stylesheet subresources are the exception: a CSS `@import "/theme.css"` or `url()` that the proxy does not rewrite is requested with the stylesheet's URL as Referer, even from an opaque document. The static UI and dev-proxy handlers then proxy it to the preview named by that Referer when the request has a CodeNomad session, as every request has with authentication disabled. The paragraph now lists what the proxy and the runtime bridge rewrite, which other root-relative requests the Referer fallback serves and under which condition, and that Remote Control refuses that fallback while previews are off. Validation: the statements match a real-Chromium review of the preview proxy, with authentication enabled and disabled, across sandboxed iframes, standalone previews, nested frames and popups; help checks pass. |
||
|
|
4b163382bc
|
feat(remote-control): serve web previews to paired devices (#1131)
Remote Control blocked `/previews/*`, so a phone or browser paired through the tunnel had no web preview at all: the entry was hidden and the agent's dev server stayed out of reach. Previews now work there, behind a host setting that is on by default. A preview's sandboxed document sends neither the device cookie nor a usable `Origin`, so the Remote Control gate admits a live `/previews/<token>/…` capability on its token alone, as local listeners already do, while the host's Web previews switch is on. The ingress, which refused every WebSocket, now admits preview upgrades only: `admitRemoteUpgrade()` parses the target exactly as the preview proxy does and the manager hands the socket to the application's path-preview upgrade handler, never to the shared upgrade event (so `/previews/<token>/../../sidecars/…` cannot reach another handler). Every other upgrade still gets 404. The switch lives in Settings → Remote Access → Remote Control. Only the host can change it (`PUT /api/remote-control/previews`); the registry stores only the opt-out. Turning it off refuses later preview requests, including the Referer-based root-relative asset fallback, and closes live preview WebSockets; remote `/api/meta` then reports `previewsUnavailable`, which hides Open web preview on paired devices. Exposing previews through a public tunnel makes their address a bearer credential, so path-proxied preview responses now carry `Content-Security-Policy: sandbox` with the iframe's own flags (one shared constant, `previews/sandbox.ts`) and `Referrer-Policy: same-origin`, on every content type. A preview document opened outside its iframe stays an opaque origin instead of running with CodeNomad's origin and device cookie, and the capability path stays out of Referer headers to other sites. Such a standalone document then behaves like the preview iframe: its root-relative resources rely on the runtime bridge's path rewriting, without the Referer fallback. The per-token preview.localhost host keeps its own origin and headers. Help (Remote Control, mobile and client overview pages, all ten locales), dev-docs/REMOTE_CONTROL.md, architecture.md and UI_SHELL.md describe the behavior; the switch label and caption are translated in all locales. Validation: a Remote Control surface test drives the real application through the ingress: a preview page and an `Origin: null` form post work without a device cookie and carry the sandbox and referrer headers, unknown tokens and non-preview or dot-segment upgrades get 404, a preview WebSocket upgrades, a paired device cannot change the switch, and turning it off closes the live WebSocket, refuses the preview and its Referer asset fallback, and flags the metadata. Registry, metadata and proxy tests cover persistence of the opt-out, the flag and the headers per entry point; browser tests cover the switch and the conversation header with previews on and off. |
||
|
|
036a008c2c
|
fix(ui): restore the web preview entry on mobile, hide it through Remote Control (#1128)
Phones lost their only entry to the session web preview when the conversation actions were reorganized: on mobile, the Full screen action took the preview's place instead of joining it. Previews still work there through direct (LAN) access, which serves them through the capability iframe proxy. Remote Control, on the other hand, serves no preview content (`/previews/*` is host-only), so its clients, desktop browsers included, were offered a preview that could only fail to load. The conversation header now offers the preview on every platform, inline and in its overflow menu, with Full screen added on mobile. `/api/meta` reports `remoteControl: true` for requests that arrived through the Remote Control ingress, `lib/server-meta.ts` exposes it as `remoteControlClient()`, and the shell hides the preview entry for those clients. The shell asks for the server metadata on mount; the read is cached and shared with the other callers, and the entry stays visible until a Remote Control answer arrives. The Remote Control help page says, in all ten locales, that conversation actions hide Open web preview there; dev-docs/UI_SHELL.md records the convention. Validation: a header-windows browser test drives the real conversation header on a Pixel 5 profile and on desktop Chrome, with and without the Remote Control metadata, and checks the inline buttons and the overflow menu. It fails on the previous shell (no preview entry on the phone, and a preview entry kept through Remote Control). meta.test.ts and the Remote Control surface test cover the flag locally and through the real ingress (the surface fixture's metadata gains the `host` field /api/meta needs). The UI unit tests run by CI, the server routes, proxy and Remote Control suites, the help checks and both typechecks pass. |
||
|
|
39c0fa5833
|
chore(opencode): qualify and recommend runtime 2.1.4 (#1126)
## Summary
- Qualify OpenCode **2.1.4** (latest stable, 23 October) with the
existing locked **2.1.0 client/plugin** dependencies, and **recommend**
it.
- Keep the technical **minimum at 2.1.2**: no consumed contract changed,
so the minimum has no new reason to move.
- 2.1.2 and 2.1.3 daemons stay ready and get an optional upgrade offer.
2.1.4 is the `tested` version. Newer and custom labels keep
authenticated contract recognition.
- The reviewed-contract fast path lists the 2.1.2 minimum and the 2.1.4
target.
- The recommended version is updated in the help (all ten locales), both
READMEs, `CONTRIBUTING.md`, `dev-docs/OPENCODE_COMPATIBILITY.md` and the
architecture skill.
## Contract review (v2.1.3 → v2.1.4, 72 commits)
- **Authenticated OpenAPI:** 121 → **124 paths**, 146 → **149
operations**. No operation changed. The changes:
- added `POST /api/experimental/client-mcp/token`;
- added `GET /api/experimental/client-mcp` (WebSocket);
- added `POST /api/debug/heap-dump`;
- `Config.InfoEncoded` gains the opt-in
`experimental.background_compaction`.
The session event (`V2EventEncoded`) and message schemas are unchanged.
None of the new routes is on the instance proxy allowlist, and the proxy
relays WebSockets only for SideCars and previews.
- **Published declarations:**
- the client adds `mcp.client.token` and `debug.heapDump`;
- the schema and plugin changes are type refactors and unused-export
removals, among them `SessionID.descending`, `SessionEvent.Source` and
`PromptInput.FileAttachment.create`; `Session.Message.ID.fromEvent` now
takes any string instead of an event ID. CodeNomad uses none of these,
and every host module its native plugin bundles import
(`@opencode/plugin`, `/effect`, `/promise/adapter`, `/rpc`,
`@opencode/schema/location`, `/session`) still exists.
- **MCP elicitation forms**
([#58184](https://github.com/anomalyco/opencode/pull/58184)): an MCP
server asking for input during a tool call now creates a global-session
Form whose `form.created` event carries the server's location.
CodeNomad's live path requires that location for global Forms and shows
them as a **Project request**. Review found that the live path dropped
the location from the queued Form, so its reply lacked
`x-opencode-directory` and failed for a worktree or subdirectory until a
pending-request rescan. **#1127** fixes that; recovery already kept it.
A native probe (minimal stdio MCP server, a real Code Mode `execute`
call, CodeNomad-style reply) gives:
| Runtime | Form listed for the location | `form.created` carries the
location | Reply completes the tool |
| --- | --- | --- | --- |
| 2.1.3 | yes | **no** | yes |
| 2.1.4 | yes | **yes** | yes (`action: accept`, `channel: stable`) |
On 2.1.3 such a request only appeared after a pending-request rescan; on
2.1.4 it appears live. The probe sends the location header itself, so it
checks the runtime contract; #1127 covers CodeNomad's live reply path.
- **Other changes** are covered by the native suites below:
- compaction (oldest-first shortening, ignored buffers, overflow
detection);
- the agent's model for sessions without one;
- V2 agent files;
- V1 global-session import attribution;
- MCP lifecycle fixes.
- **Live check:** this machine's shared daemon runs 2.1.4, and the
CodeNomad desktop session works through it.
## Isolated Windows qualification
Base: dev `
|
||
|
|
719bc1c967
|
fix(forms): keep a live global Form's location for its reply (#1127)
## Problem OpenCode 2.1.4 ([#58184](https://github.com/anomalyco/opencode/pull/58184)) delivers MCP elicitation requests live, as global-session Forms (`sessionID: "global"`) whose `form.created` event carries the server's location. CodeNomad's live event path (`stores/instances.ts`) queued `event.data.form`, which has no `location`. `formRequestOptions()` reads `form.location`, so the dock's reply and cancellation went out **without `x-opencode-directory`**. For a Form created in a worktree or subdirectory, OpenCode answered `FormNotFoundError` until a pending-request rescan replaced the entry with its located copy. Found by the independent review of the 2.1.4 qualification (#1126), which reproduced the headerless `FormNotFoundError` on an isolated 2.1.4 daemon. ## Fix Recovery already attaches the snapshot location to global Forms. The live path now does the same with the event location before queuing. Session-owned Forms are unchanged, since their replies need no location header. `dev-docs/NATIVE_INTERRUPTION_UX.md` states that live and recovered global Forms keep their location. ## Tests - New regression in `instances-pending-requests.test.ts`: a real SSE dispatch of two global Forms from a worktree, then a reply and a cancellation. Both must send `x-opencode-directory` for that worktree. It **fails without the fix**, because the queued Form has no location. - With the fix: - pending-request, compaction and Form store tests: 55/55; - full UI unit suite: 1700/1700; - interruption dock and mobile interruption browser tests: 43/43; - UI typecheck. |
||
|
|
6c2601b065
|
fix(previews): never forward the app's cookies to the previewed site (#1125)
When an iframe preview loads, the browser sends the CodeNomad origin's cookies with the frame's document request: the parent page initiates that navigation, so the SameSite=Lax session cookie and any SideCar service cookie set at the app origin are attached. The HTTP preview proxy forwarded that Cookie header unchanged to the preview target, which can be any external site. Opening an untrusted page in a browser or mobile client's preview therefore handed it a working CodeNomad session. Preview HTTP requests now drop the Cookie header, matching preview WebSocket upgrades, which already forwarded none. Target Set-Cookie headers were and remain stripped, so previews keep their behavior: the path proxy never carried cookies a previewed site could have set. All three entry points share proxyPreviewTargetRequest: the /previews/<token> path proxy, the per-token preview.localhost capability host and the root-relative asset fallback resolved from a preview Referer. Desktop native guests load the target directly and are unaffected. Validation: a new sidecar-proxy regression sends the CodeNomad session and a SideCar service cookie through each entry point and asserts the preview upstream receives no Cookie header. It fails on the previous proxy (both cookies reached the upstream through all three). The server proxy, preview, SideCar and Remote Control suites pass (192 tests) and the server typecheck is clean. |
||
|
|
9ac792fffe
|
fix(server): authenticate the path the router routes, not the raw target (#1124)
The authentication hook decided whether a request needs a session from the
raw request target, while Fastify's router matches a decoded path. A
percent-encoded or absolute-form target therefore reached protected routes
without a session: "/%61pi/workspaces" routed as "/api/workspaces", and
"/%73idecars/<id>/..." reached a SideCar, on any listener the server
exposes, including LAN listeners protected by the server password. Remote
Control was not affected: its gate already judged a decoded path.
Both checks now share `routedPath()` (server/routed-path.ts), which returns
exactly the path find-my-way matches: cut at the first "?" or ";" (Fastify's
default semicolon delimiter) and decoded with decodeURI, so reserved
characters such as %2F stay encoded and %25 is never decoded twice. Targets
the router would reinterpret (absolute-form, a leading "//", a fragment) or
reject (malformed percent-encoding) are refused with 400 before any access
decision. The Remote Control gate used a broader decodeURIComponent
variant; sharing the router-exact helper keeps the two classifications
identical to routing.
Validation: `routed-path.test.ts` checks the helper against the decoder of
the router Fastify itself loads and its refusals. `request-path-auth.test.ts`
drives the real HTTP application over raw sockets: encoded /api, /sidecars
and /workspaces targets, queries and semicolons get 401 without a session,
reinterpreted or malformed targets get 400, public routes stay public and a
session still reaches encoded paths. It fails on the previous hook
("/%61pi/test/echo" answered 200). The Remote Control surface tests and the
whole server suite pass, and the server typecheck is clean.
|
||
|
|
5994944f84
|
feat(settings): manage desktop data profiles and drop the startup question (#1123)
Choosing a desktop data profile needed `CODENOMAD_PROFILE` or, on a first packaged launch with several restorable profiles, a native question that blocked startup. Settings only named a non-default profile and offered to delete all the other profiles at once. Settings > General > Restore previous state now ends with one folded line, "Data profile: <name>". Unfolded, it lists every profile on the computer, the open one first and the others by last use, each with its size, creation date and last use. A radio chooses the profile the next launch opens (any profile of this configuration; choosing the open one again cancels), "New profile" creates one by name, "Restart in <name>" restarts into the choice, and each other profile has its own delete button with a confirmation. A launch pinned by `CODENOMAD_PROFILE`, its alias or a development run shows the profiles without letting the user choose; deletion still works. Without a remembered choice, a packaged launch no longer asks: it opens the most recently used of the default, `dev` and `dev-v2` profiles (ties go in that order) and remembers it, as it already did with zero or one candidate. A launch that finds the selection lock held opens the detected profile without remembering it instead of waiting. Implementation: the existing host listing gains the open profile, creation (folder birth time, links not followed) and last-use (newest client-state write) dates, the remembered next-launch profile and whether the launch is pinned. Two host operations are added on both hosts, granted to local and Preferences windows only and serialized with list and delete: choosing the next launch's profile (validated with the `CODENOMAD_PROFILE` grammar, written under the selection lock, overwriting this configuration's entry, creating a new profile's scope folder inside the lock and removing it again if the write fails) and restarting through the graceful developer-mode relaunch. Electron's throwaway question storage, relaunch and private hand-offs are removed; it still sweeps leftover question folders and drops the two hand-off variables an older build's relaunch can carry. Tauri keeps only the invalid-profile error dialog. `CODENOMAD_DESKTOP_PROFILE` and the server's `desktopProfile` meta field are removed, since nothing displays them. Only a profile whose name leads back to its own folder can be chosen, so a folder the deprecated alias made under another profile's name (such as default-<hash>) is listed under its stored name but not offered. The listing reports the next launch's profile only when the launch would open it (the launch's own remembered-profile check), and Restart in <name> lists again first and restarts only while that still names the shown profile. Choosing with the keyboard keeps focus on the chosen radio. Validation: UI browser tests for the manager (listing, choose and restart, new profile and name validation, single deletion, macOS note, pinned launches, plain browser), Electron native tests (listing dates, current, next launch, pinned, set-next-launch outcomes, relaunch wiring, preload, automatic choice, lock and sweep) and Tauri tests (the same cases, grants and command wiring). Developer doc and help (10 languages) updated. |
||
|
|
628a80a5c9
|
feat(settings): give Updates its own card and the data profile a Startup row (#1122)
## Summary - **Updates** is its own card again (About → Updates → Diagnostics → connectivity), subtitle restored in 10 languages: latest version, install/download + Refresh, then the **update feed** with its help and save error last. Rows resuming after actions open with their separator. Help speaks of the Updates card again (10 locales). - **Data profile** moves from About to General's startup card (Restore previous state), above Other saved profiles; still only for an explicit non-default profile (`current-profile-settings-row.tsx`, shared by inline settings and Preferences). Label key moved in 10 languages; desktop help and `DESKTOP_DATA_PROFILES.md` point to the new place. - The Clear saved startup state button keeps space above the rows' separator. ## Validation - `server-info`: card order, feed out of About, after the actions, with its separator; no data profile in Info. - `other-profiles`: running profile named above other saved profiles, clear of the button, Electron and Tauri Preferences; default profile never named. - `desktop-updates`, `npm run test:help`, UI typecheck. |
||
|
|
f400d5beb9
|
fix(transcript): load large tool images on demand instead of inline (#1118)
A session whose history holds image tool results (for example `read` on PNG files) never opened over a slow link (#1113): the 200-message page was 16.8 MB, 15.7 MB of it base64 in five tool results. It took 20 s at 6.5 Mbit/s, and a reverse proxy cut the transfer, so the UI showed "Failed to fetch". Every open, reload and history window paid for every image, shown or not. Transcript pages now carry large tool images by reference. The UI's native message list reads (createInstanceFetch) and history windows send the `x-codenomad-media: deferred` opt-in. For those reads only, the server replaces each tool image whose data URI exceeds 32 KiB with a `/api/workspaces/<id>/session-media/<session>/<message>/<part>/<index>/ <sha256>` path, in the instance proxy (forwardRuntimeRequest gains an optional whole-JSON transform for successful JSON responses) and in the history window route. toolImageSource resolves that path on the API base, so the existing lazy `<img>` loads only the images that are shown, as binary rather than base64. The new session-media route checks that the workspace owns the session, reads the native message, and refuses a session that moved during the read and messages at or after an undo boundary. It serves the bytes only when the image still matches the digest, as an immutable, nosniff, sandboxed image; anything else falls back to "Unable to display this image". What stays whole: - Single-message reads (`session.message.get`), so pruning revisions keep hashing native content. - Live events, user attachments, small images, non-image files and error responses. - Requests without the opt-in. The opt-in header is never forwarded to OpenCode. On the reported session, the page drops from 16.8 MB to about 1.1 MB. Validation: - session-media.test.ts: only large renderable tool images change, and only their URI. - routes/session-media.test.ts: exact bytes and headers; invalid references, digest and position mismatches, the undo boundary, ownership and a move during the read. - proxy.test.ts: the transform applies to successful JSON only and releases once. - instance-proxy.test.ts: an opted-in list read is deferred, the opt-in is not forwarded, and plain list or single-message reads stay whole. - session-navigation.test.ts: history windows. - sdk-manager.test.ts: only list reads opt in, keeping other headers. - tool-content.test.ts: path resolution, forged paths rejected; it is now listed in the PR build. - tool-images.test.ts: a deferred image decodes from the route in the real ToolCall renderer, and a missing one shows the fallback. The UI own-tsconfig, server and root typechecks pass, as do: - the server suite (2,467 passed; one index.test.ts process test timed out under load and passes alone, as on dev); - the UI unit step of the PR build (564 tests); - seven browser suites (278 tests). |
||
|
|
47e02795b2
|
fix(session): clear a queued follow-up's badge when it is delivered (#1117)
A follow-up sent while the agent works (follow-up behaviour "queue") shows the "Queued" badge until the engine delivers it. After some turns the badge never cleared: the message was answered, the engine went idle, and only closing and reopening the session removed it (#1112). The stale pending entry also kept the message pinned below the running turn. The session view lists queued prompts from the session reducer's inbox through a memo. CodeNomad replaces that reducer while the session stays open: an authoritative resync after an event-queue overflow, a rotation failure or a reconnect, and a pruning invalidation. The replacement was a plain property write, so the memo kept reading the disposed reducer, which never sees `session.inbox.delivered`. On 0.20.1 nothing rebound it until the view remounted; on dev the reducer retirement at settlement happened to rebind it at the end of the turn, but the badge stayed for the rest of the turn, and indefinitely when retirement waits (another prompt still parked, an unsettled tool or shell). Every replacement now goes through replaceTranscriptEntry, which assigns the new reducer, disposes the previous one and bumps a per-transcript entryRevision signal that getOpenCodeSessionInbox tracks. Only that session's inbox observers re-run; a transcript destroyed and recreated is still rebound by the instance revision. Several queued follow-ups keep their badges until each is delivered; steer-mode actions on a delivered prompt disappear the same way. Validation: opencode-inbox-rebinding.test.ts queues two follow-ups, replaces the reducer by pruning invalidation and by an event-queue overflow, then delivers them one by one with no terminal event: each leaves the memo at once. Both cases fail without the fix. A browser test in session-rendering.test.ts runs the real SessionView and SSE dispatcher: queued badge, a pruning event, delivery, badge gone while the turn still runs; it fails without the fix with the badge still visible. The session fixture's native inbox now lists parked prompts as an array and stubs the permission and form lists, as a real daemon does, so an authoritative resync completes. The new unit file is listed in the PR build's UI unit step, which passes locally (64 files, 562 tests), as do the UI own-tsconfig and root typechecks and the six browser suites using the session fixture (206 tests). |
||
|
|
7f1ee6ccfb
|
fix(titlebar): show the hexagon logo at 20 px (#1116)
## Summary At 16 px the title bar's hexagon was too small for its C and n to read. Compared in the Windows and macOS title bars on light and dark palettes at 16, 20 and 24 px, 20 px reads clearly, stays close to the menu text and leaves margins in macOS's 28 px bar (24 px nearly filled it). ## Validation - `brand-logo` browser test expects the 20 px title bar logo; `macos-titlebar` still finds it centred and clear of the traffic lights. - `dev-docs/UI_CONVENTIONS.md` records the size. |
||
|
|
42cab13a0e
|
feat(brand): adopt the hexagon CN logo, black or white by palette (#1114)
## Summary - New logo: a hexagon framing a C and an n, in equal bands (ring, gap, strokes, counters share one width), readable down to 16 px. - **App icon**: white ring and letters over black gaps, transparent canvas, hexagon spanning top to bottom; one fixed icon for light and dark taskbars, docks and tabs. - **In the UI** (`components/brand-logo.tsx`): black hexagon with white gaps on light palettes, white version on dark ones, picked in CSS from `data-theme` (system appearance when unset), so it also works outside ThemeProvider (title bar). Loading screen (always dark) uses the white one. Sizes: 4rem beside a text-3xl title, 3.2rem narrow column, 5.3rem loading screen, 16px title bar. - **Icons rendered from the vector**: `scripts/generate-icons.mjs` draws every `.ico` entry (16-256 px), `.icns` slot, Linux size and the 1024 px master straight from `images/CodeNomad-Icon.svg` with Chromium, instead of shrinking one large PNG; it writes the containers itself, so Electron drops `png2icons`/`pngjs`. One command replaces BUILD.md's four manual steps. ## Validation - New `tests/browser/brand-logo.test.ts`: real title bar + home page; which hexagon each logo shows on light, dark and system (both ways) palettes, with heights; red without the logo stylesheet. - UI + Electron typechecks, Electron tests (283), `macos-titlebar` and `notification-icon-badge` browser tests, `npm run test:help`. - Icon checks: ICO entries 16-256 (DIB, PNG at 256), ICNS slots icp4-ic14; UI captures on porcelain, linen, mist and ember palettes. |