Commit graph

6 commits

Author SHA1 Message Date
Pascal André
66a85afa1c
feat(plugins): add, remove and show the version of any OpenCode plugin (#1232)
## Summary

Status → Plugins can now **add**, **remove** and **show the version** of
any OpenCode plugin (SCR §4.1, slices C1/U1/D1/Q1, AC-P1…AC-P9).

- **Add**: a `+` icon beside Refresh opens a small `.window-*` popover:
package spec (`--surface-field`), scope (Global default; Project shows
the warning), trust sentence. Registry and remote Git specs only.
- **Remove**: each row's menu gets **Remove from Global/Project** for
declarations in that scope's own write target. `id`/`-id` rules stay;
the package cache is never touched.
- **States**: a just-added source shows **Installing** until
`plugin.list` reports its exact target (then an ordinary active/failed
row). After `PLUGIN_SOURCE_PENDING_TIMEOUT_MS` (120 s) it shows **Not
confirmed** with Refresh.
- **Tooltips**: version, **Update available**, failure + native `ref`,
GUI notice when a `features.gui` package is declared outside Global.

## Implementation

- Server: new `opencode/plugin-sources.ts` + `routes/plugin-sources.ts`
(`POST`/`DELETE /api/workspaces/:id/plugin-sources`), built on
`PluginControls.editConfigDocument` (ownership, scope resolution,
deletion fence, connection fence, lock, atomic replace, JSONC/CRLF/BOM,
WSL). Pure helpers in `plugin-source-document.ts` (npm-package-arg
validation, package identity, verified removal). Types in
`plugin-source-types.ts`. `plugin-controls.ts` only gains `pluginId` on
sources and the `gui` passthrough.
- Errors carry a stable `code`: `invalid-spec`, `local-path`,
`plugin-id` (422), `duplicate`, `stale` (409).
- UI: `stores/plugin-sources.ts`, `components/plugin-source-add.tsx`,
`components/plugin-row-actions.tsx` (replaces
`plugin-package-action.tsx`, memoized item descriptors), i18n part file
in the 10 locales, help in 10 languages, `dev-docs/PLUGIN_CONTROLS.md`
rewritten.

## Deviations to note

- `disabled` state for unloaded sources: `plugin.list` omits disabled
plugins and exposes no package→ID link, so an unloaded configured source
is shown as **Not confirmed** ("may be disabled, or installation not
confirmed"). A disabled package with an exact `-id` rule therefore shows
its ID row (switches) and its unconfirmed source row (Remove items).
Documented in `PLUGIN_CONTROLS.md`.
- AC-P7 "a GUI package is added Global-only, Project refused":
`features.gui` is known only once OpenCode has loaded the package, so
pre-install refusal is not possible. This PR keeps Global as default,
warns for Project, and flags a Project-declared GUI package in its
tooltip afterwards. Native-family Global-only enforcement (D18) waits
for the integration catalogue (later slice).

## Tests

- `node --import tsx --test
packages/server/src/opencode/plugin-source-document.test.ts
packages/server/src/opencode/plugin-sources.test.ts` (13 pass)
- Full server suite: 2931 pass, 0 fail (one load-timeout in
`index.test.ts`, passes alone)
- `CODENOMAD_TEST_WSL_PLUGIN_CONTROLS=Ubuntu …
plugin-control-document-wsl.test.ts` (9 pass, incl. new add/remove)
- `node --import tsx scripts/test-plugin-sources-native.mjs <opencode
2.2.5>`: PASS (loopback fake registry, 404 → failed with `ref`); added
to CI runtime contracts
- UI store tests (45 pass), browser
`plugin-sources`/`plugin-controls`/`plugin-packages` (14 pass), `npm run
typecheck`, `npm run test:help` (31 pass)
2026-10-31 18:43:49 +01:00
heunghingwan
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.
2026-10-27 02:11:40 +01:00
heunghingwan
c977ec67cb
test(ui): shard browser CI four ways and give every fixture an owned server (#1139)
The `tests` job spent about two of its 2.2 hours in `test:browser`: more
than 140 fixture files, each starting its own Vite dev server, run one at
a time, many without the protected-teardown wiring or sharing the
checkout's default optimizer cache.

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

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

Validation: the four shards took 26, 32, 37 and 25 minutes and `tests`
12 minutes in the last completed CI run, against about 2 h 14 for
`tests` with the browser step; the wiring, navigation-budget and
build-order tests pass, and a 20-file slice of heavy fixtures passed
391/391.
2026-10-25 20:38:46 +01:00
Pascal André
b4be76c978
feat(plugins): native package check and update (6/8) (#794)
## Behavior
Package-backed plugins in Status → Plugins get one compact action beside
their name. Activating it opens a menu naming the exact native
operation:
- **Check updates for {target}**: `PackageSearch` icon.
- **Update {target}**: `Download` icon, shown once the target is
outdated.

The tooltip states that an update changes the daemon-wide shared package
cache. Busy actions pulse but keep their own icon; `RefreshCw` stays
reserved for the list refresh. Global/Project activation switches are
unchanged. Local, builtin and SDK sources never offer package actions.

A package that fails before exporting a plugin ID stays visible as a
target-only row, with check/update and failure details in its tooltip.
No activation switch or fabricated ID is created, and the row is
replaced by the ordinary plugin row once native loading succeeds.

Pending admission is keyed by instance and target and survives
hiding/remounting. Failures toast only for the live owner. Every
completion invalidates the display snapshot, and nothing is replayed
automatically.

## Native boundary
Only `POST /api/plugin/check` and `POST /api/plugin/update` are added to
the instance allowlist. They go through the existing directory-ownership
and worktree-deletion admission, so a foreign location is refused
whether it is in the query or the body. Native OpenCode validates
targets against the current Location inventory and serializes updates
per target. There is no `location.reload` and no daemon restart.

## Validation
- Server and UI typechecks pass.
- `instance-proxy.test.ts`: 56/56, including owned check/update and
foreign query/body locations rejected before forwarding.
- Browser: `plugin-controls` and `plugin-packages` pass, 10/10
scenarios. Captures inspected for LTR, RTL, narrow and
failed-package-only layouts.
- `scripts/test-plugin-packages-native.mjs` passes on an isolated copy
of OpenCode 2.0.29 with a loopback npm registry. It covers 1.0.0 → 1.1.0
update detection, unknown-target rejection and native live reload, using
a private home/database/npm config/cache only.

## History
PRs 1–5 of this series were squash-merged into `dev`. This branch now
merges `dev` and carries only its own plugin-package changes plus review
fixes (25 files).

Size notes: the existing `http-server.ts`, `instance-proxy.test.ts` and
locale settings files exceed the size guidance.
2026-10-15 02:54:19 +02:00
Pascal André
f8faf0709c
fix(plugins): simplify activation controls UI (follow-up #726) (#778)
## Summary

Follow-up to #726 implementing the presentation explicitly reviewed and
approved by the user:

- Use a compact **Refresh icon / Global / Project** header, without
repeating the section's Plugins title. Per the user's final request, the
Refresh button replaces the intermediate Name label and spins during
loading and refresh.
- Keep each plugin to a compact name and two scoped switches.
- Remove the general description, inline status/source commentary,
configuration-path footer and success commentary.
- Show plugin details only in the name's hover/focus tooltip.
- Preserve automatic refresh through native events, renderer
reconnection and visible demand, plus focus handling and
unavailable-scope accessibility.
- Reuse the existing localized Refresh label and document the minimal UI
convention.

## Validation

- French minimal rendered preview reviewed by the user, followed by
their explicit request to restore the animated Refresh button in place
of Name.
- Rebased the isolated follow-up commit onto current `dev`.
- UI typecheck: passed with the final Refresh button restored.
- Real-component browser suite: **8 passed** with the final Refresh
button restored, covering manual refresh and its busy animation/disabled
state, scoped switches, disabled Project, hover/focus details, compact
long names, mutation focus, disposal and demand-driven refresh.
- `git diff --check upstream/dev...HEAD`: passed.

The user explicitly requested creation of this follow-up PR and an admin
squash merge without waiting for CI.
2026-09-25 12:45:51 +02:00
Pascal André
ec1c0bedbf
feat(plugins): add scoped V2 plugin activation controls (#726)
## Summary

- Add narrow CodeNomad routes for native OpenCode V2 plugin inventory
and activation-rule mutations.
- Present one row per user/configured plugin with exactly two compact
**Global** and **Project** switches, sharing Yolo/MCP switch geometry.
Builtins are hidden; exact configured disabled IDs remain available.
- Keep complete names, source paths, failures and configuration targets
readable on narrow keyboard/touch layouts. Retain focused rows across
refreshes and guard repeated pending activation without removing
keyboard focus.
- Provide translated feedback in all ten locales and suppress stale
feedback after the owning surface is disposed.

## Authority and refresh behavior

- Discover roots through the authenticated daemon's `config.get`, after
native-location ownership and connection-generation checks.
- Append only exact activation rules while preserving JSONC comments,
source options, unknown keys, formatting, BOM and native permissions.
Bound reads, check optimistic conflicts, serialize by write target and
use atomic replacement with nonce-owned locks.
- Require proven raw/normalized alignment before acknowledging a no-op.
Watcher lag cannot report success for a rule that is no longer saved on
disk.
- Account for inherited `.opencode` precedence by choosing a
higher-precedence target in the active project location; preserve
ancestor files and reject an escaping fallback target.
- Use native WSL filesystem operations. Retain canonical invalidation
and one trailing reconciliation when host/native cache records converge.
- Cache display snapshots by instance/worktree, share them across
sessions in that worktree, and refresh only on visible demand. Global
writes invalidate sibling snapshots without background reads.

## Compatibility

The current server/UI/bundled-plugin dependency pin and recommended
runtime are **2.0.16**, aligned with `dev`. The demonstrated technical
minimum remains **2.0.7**. Activation writes use the narrow local JSONC
operation rather than a runtime-specific write endpoint.

## Validation status and provenance

Final reviewed head: **`5204d97694ca8ced954bddc9cc94ea464e46119d`**,
aligned with `dev@9d7ea5d6`. Both independent resumed review loops
reached **ZERO actionable findings**: UX R6 and quality/optimization R5.

Local validation on the final head: **100 plugin/server/cache/WSL tests
passed**, **eight real-component browser regressions passed**, **14
restoration/metadata integration tests passed**, server/UI typechecks
and UI production build passed. Translation keys/placeholders match
across all ten locales. The current-dev baseline and PR browser
comparison passed the same 34 shared cases with one optional native
Electron skip; detailed revision provenance is in the final review
comment.

**CI qualification is excluded at the user's explicit request.** No
final CI success is claimed, and CI completion is not being awaited.

The earlier head `38845060` failed typechecking because `noticeId` was
undeclared; the earlier historical UX R4 zero-finding claim was also
incorrect and has been [explicitly
corrected](https://github.com/NeuralNomadsAI/CodeNomad/pull/726#issuecomment-5816956417).

Historical test results and the installer built from `bd31dcd0` are not
validation or artifacts of the final corrected head. See the final
review comment for completed counts, review evidence and revision
boundaries. The PR remains open.

## Scope note

Implemented independently; no code was copied from PR #593.

This separate V2 implementation supersedes PR #593.
2026-09-25 01:29:14 +02:00