From ac0998af042d198cf9387a7687ce0056a7988011 Mon Sep 17 00:00:00 2001 From: Sidharth Menon Date: Wed, 23 Sep 2026 15:20:24 -0400 Subject: [PATCH 01/12] Expose Whiteboard agent tools and serve connection guidance over MCP Port the connection-prompt and instruction workflow onto existing Review internals. Add the whiteboard command and session tool boundary while preserving storage, transport, profile identifiers and owned legacy launchers. Agent-Session: b320a5ae-50a4-4472-a324-515d964c5f5a Agent-Session: 01a0cf8e-5b2b-7253-9d6c-2d77ac6485b8 --- .agents/plugins/marketplace.json | 11 + .claude-plugin/marketplace.json | 11 + .../browser/parts/canvas/reviewCanvasPart.ts | 30 +- .../vs/review/common/reviewCliInstall.test.ts | 85 -- .../src/vs/review/common/reviewCliInstall.ts | 7 - .../common/reviewCliInstallStartup.test.ts | 43 + .../review/common/reviewCliInstallStartup.ts | 23 + .../install/reviewCliInstall.contribution.ts | 114 +-- .../reviewCanvasEditorTabsService.test.ts | 50 ++ .../services/reviewCanvasEditorTabsService.ts | 19 + .../reviewDesktopConnectionService.test.ts | 22 +- .../reviewDesktopConnectionService.ts | 38 +- apps/review-desktop/scripts/package-linux.sh | 2 +- .../scripts/skill-versions.test.mjs | 90 -- .../stage-review-runtime-docs.test.mjs | 9 +- .../scripts/stage-review-runtime.mjs | 59 +- docs/agents.md | 194 +++-- docs/cli-reference.md | 236 +++--- docs/cli-releases.md | 10 +- docs/how-review-works.md | 30 +- docs/quickstart.md | 26 +- docs/troubleshooting.md | 63 +- package.json | 3 +- .../claude/.claude-plugin/plugin.json | 6 + packages/agent-plugins/claude/.mcp.json | 8 + .../codex/.codex-plugin/plugin.json | 5 + packages/agent-plugins/codex/.mcp.json | 8 + .../codex/skills/whiteboard/SKILL.md | 8 + .../cursor/.cursor-plugin/plugin.json | 5 + packages/agent-plugins/cursor/mcp.json | 8 + packages/agent-plugins/opencode/LICENSE | 21 + packages/agent-plugins/opencode/index.d.ts | 5 + packages/agent-plugins/opencode/index.js | 15 + packages/agent-plugins/opencode/package.json | 28 + packages/agent-plugins/pi/LICENSE | 21 + packages/agent-plugins/pi/package.json | 26 + .../pi/skills/whiteboard/SKILL.md | 8 + .../review-protocol/src/contracts.test.ts | 62 +- packages/review-protocol/src/contracts.ts | 178 ++-- packages/review-protocol/src/index.ts | 27 - .../app/src/agent-setup-card.browser.test.tsx | 101 --- packages/review/app/src/agent-setup-card.tsx | 179 ---- .../app/src/connect-card.browser.test.tsx | 342 ++++++++ packages/review/app/src/connect-card.tsx | 353 ++++++++ packages/review/app/src/desktop-entry.tsx | 1 - packages/review/app/src/prompt-card.tsx | 88 +- .../app/src/review-home-view.browser.test.tsx | 49 +- packages/review/app/src/review-home-view.tsx | 58 -- packages/review/app/src/settings-page.tsx | 38 +- packages/review/app/src/styles.css | 278 +++--- .../trace-capture-section.browser.test.tsx | 49 +- .../review/app/src/trace-capture-section.tsx | 94 +-- .../app/src/welcome-page.browser.test.tsx | 422 ++++------ packages/review/app/src/welcome-page.tsx | 232 ++--- packages/review/app/src/whiteboard.css | 20 +- .../SKILL.md => instructions/authoring.md} | 40 +- packages/review/instructions/file-lenses.md | 13 + packages/review/instructions/scratchpad.md | 23 + .../trace-archaeology.md} | 37 +- packages/review/onboarding.md | 11 +- packages/review/package.json | 14 +- packages/review/plugins/review.ts | 14 - .../review/skills/dev-file-lenses/SKILL.md | 22 - packages/review/skills/scratchpad/SKILL.md | 32 - packages/review/src/agent-fff.ts | 339 -------- packages/review/src/agent-plugins.test.ts | 84 ++ packages/review/src/agent-review-mcp.test.ts | 411 --------- packages/review/src/agent-review-mcp.ts | 259 ------ packages/review/src/cli-install.test.ts | 579 +++++++++---- packages/review/src/cli-install.ts | 796 +++++++----------- packages/review/src/cli-runner.ts | 278 ++---- packages/review/src/cli.test.ts | 288 ++----- packages/review/src/cli.ts | 4 +- packages/review/src/connect-prompts.test.ts | 133 +++ packages/review/src/connect-prompts.ts | 155 ++++ packages/review/src/cursor-deeplink.test.ts | 26 + packages/review/src/cursor-deeplink.ts | 11 + packages/review/src/install-directory.ts | 84 -- packages/review/src/install-rollback.test.ts | 62 -- packages/review/src/install.test.ts | 618 -------------- packages/review/src/install.ts | 664 +-------------- .../review/src/legacy-review-fixtures.test.ts | 3 - packages/review/src/legacy-skills.test.ts | 150 ++++ packages/review/src/legacy-skills.ts | 133 +++ packages/review/src/migrate.test.ts | 57 -- packages/review/src/migrate.ts | 116 --- packages/review/src/review-api/agent-cli.ts | 45 +- .../src/review-api/agent-client.test.ts | 28 +- .../review/src/review-api/agent-client.ts | 9 +- .../review/src/review-api/authoring-tools.ts | 19 +- packages/review/src/review-api/http.ts | 34 +- .../src/review-api/instructions.test.ts | 651 ++++++++++++++ .../review/src/review-api/instructions.ts | 84 ++ packages/review/src/review-api/mcp.ts | 97 ++- .../src/review-api/public-tools.test.ts | 91 ++ .../review/src/review-api/public-tools.ts | 77 ++ packages/review/src/review-api/recovery.ts | 2 + .../review/src/review-api/review-api.test.ts | 25 + packages/review/src/review-home.test.ts | 6 - packages/review/src/review-home.ts | 2 +- packages/review/src/review-preferences.ts | 5 +- packages/review/src/review-telemetry.ts | 2 +- packages/review/src/server/desktop-server.ts | 34 +- .../review/src/server/headless-host.test.ts | 12 +- packages/review/src/server/headless-host.ts | 7 +- .../src/server/scratchpad-preference.test.ts | 38 +- packages/review/src/skill-install-lock.ts | 23 - packages/review/src/skill-updates.test.ts | 352 -------- packages/review/tutorial/document.json | 4 +- pnpm-lock.yaml | 260 +++++- pnpm-workspace.yaml | 4 + scripts/pack-review-cli.mjs | 6 +- .../review-latency/review_latency/render.py | 2 +- .../review-latency/review_latency/surface.py | 14 +- scripts/smoke-review-cli.mjs | 23 +- 115 files changed, 5103 insertions(+), 6227 deletions(-) create mode 100644 .agents/plugins/marketplace.json create mode 100644 .claude-plugin/marketplace.json delete mode 100644 apps/review-desktop/code-oss/src/vs/review/common/reviewCliInstall.test.ts delete mode 100644 apps/review-desktop/code-oss/src/vs/review/common/reviewCliInstall.ts create mode 100644 apps/review-desktop/code-oss/src/vs/review/common/reviewCliInstallStartup.test.ts create mode 100644 apps/review-desktop/code-oss/src/vs/review/common/reviewCliInstallStartup.ts create mode 100644 apps/review-desktop/code-oss/src/vs/review/services/reviewCanvasEditorTabsService.test.ts delete mode 100644 apps/review-desktop/scripts/skill-versions.test.mjs create mode 100644 packages/agent-plugins/claude/.claude-plugin/plugin.json create mode 100644 packages/agent-plugins/claude/.mcp.json create mode 100644 packages/agent-plugins/codex/.codex-plugin/plugin.json create mode 100644 packages/agent-plugins/codex/.mcp.json create mode 100644 packages/agent-plugins/codex/skills/whiteboard/SKILL.md create mode 100644 packages/agent-plugins/cursor/.cursor-plugin/plugin.json create mode 100644 packages/agent-plugins/cursor/mcp.json create mode 100644 packages/agent-plugins/opencode/LICENSE create mode 100644 packages/agent-plugins/opencode/index.d.ts create mode 100644 packages/agent-plugins/opencode/index.js create mode 100644 packages/agent-plugins/opencode/package.json create mode 100644 packages/agent-plugins/pi/LICENSE create mode 100644 packages/agent-plugins/pi/package.json create mode 100644 packages/agent-plugins/pi/skills/whiteboard/SKILL.md delete mode 100644 packages/review/app/src/agent-setup-card.browser.test.tsx delete mode 100644 packages/review/app/src/agent-setup-card.tsx create mode 100644 packages/review/app/src/connect-card.browser.test.tsx create mode 100644 packages/review/app/src/connect-card.tsx rename packages/review/{skills/dev-review/SKILL.md => instructions/authoring.md} (59%) create mode 100644 packages/review/instructions/file-lenses.md create mode 100644 packages/review/instructions/scratchpad.md rename packages/review/{skills/trace-archaeology/SKILL.md => instructions/trace-archaeology.md} (71%) delete mode 100644 packages/review/plugins/review.ts delete mode 100644 packages/review/skills/dev-file-lenses/SKILL.md delete mode 100644 packages/review/skills/scratchpad/SKILL.md delete mode 100644 packages/review/src/agent-fff.ts create mode 100644 packages/review/src/agent-plugins.test.ts delete mode 100644 packages/review/src/agent-review-mcp.test.ts delete mode 100644 packages/review/src/agent-review-mcp.ts create mode 100644 packages/review/src/connect-prompts.test.ts create mode 100644 packages/review/src/connect-prompts.ts create mode 100644 packages/review/src/cursor-deeplink.test.ts create mode 100644 packages/review/src/cursor-deeplink.ts delete mode 100644 packages/review/src/install-directory.ts delete mode 100644 packages/review/src/install-rollback.test.ts delete mode 100644 packages/review/src/install.test.ts create mode 100644 packages/review/src/legacy-skills.test.ts create mode 100644 packages/review/src/legacy-skills.ts create mode 100644 packages/review/src/review-api/instructions.test.ts create mode 100644 packages/review/src/review-api/instructions.ts create mode 100644 packages/review/src/review-api/public-tools.test.ts create mode 100644 packages/review/src/review-api/public-tools.ts create mode 100644 packages/review/src/review-api/recovery.ts delete mode 100644 packages/review/src/skill-install-lock.ts delete mode 100644 packages/review/src/skill-updates.test.ts diff --git a/.agents/plugins/marketplace.json b/.agents/plugins/marketplace.json new file mode 100644 index 000000000..cdad40b8a --- /dev/null +++ b/.agents/plugins/marketplace.json @@ -0,0 +1,11 @@ +{ + "name": "devfast", + "plugins": [ + { + "name": "whiteboard", + "source": { "source": "local", "path": "./packages/agent-plugins/codex" }, + "policy": { "installation": "AVAILABLE" }, + "category": "Productivity" + } + ] +} diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json new file mode 100644 index 000000000..99cfaf44d --- /dev/null +++ b/.claude-plugin/marketplace.json @@ -0,0 +1,11 @@ +{ + "name": "devfast", + "owner": { "name": "dev.fast" }, + "plugins": [ + { + "name": "whiteboard", + "source": "./packages/agent-plugins/claude", + "description": "Connect Claude Code to dev.fast Whiteboard" + } + ] +} diff --git a/apps/review-desktop/code-oss/src/vs/review/browser/parts/canvas/reviewCanvasPart.ts b/apps/review-desktop/code-oss/src/vs/review/browser/parts/canvas/reviewCanvasPart.ts index 34bc8fb17..2d2d48e00 100644 --- a/apps/review-desktop/code-oss/src/vs/review/browser/parts/canvas/reviewCanvasPart.ts +++ b/apps/review-desktop/code-oss/src/vs/review/browser/parts/canvas/reviewCanvasPart.ts @@ -50,7 +50,6 @@ import type { ReviewCanvasContent, ReviewCanvasDiagnostic, ReviewCanvasHandle, - ReviewCanvasHomeSetup, ReviewCanvasInstallContent, ReviewCanvasModule, ReviewCanvasOnboarding, @@ -427,7 +426,6 @@ export class ReviewCanvasEditorPane extends EditorPane { if (input.target.kind === "home") { this.renderedInput = input; this.setCanvasState("home"); - const setup = await this.resolveHomeSetup(); await this.apiCatalog.initialize(); let emptyStateVisible = false; /* The empty-list render suspends on the install fetch below, while @@ -470,7 +468,6 @@ export class ReviewCanvasEditorPane extends EditorPane { return; } }, - setup, // Home shows the Welcome rail while the list is empty. install, setupActions: this.setupActions(), @@ -640,7 +637,7 @@ export class ReviewCanvasEditorPane extends EditorPane { try { return await this.loadInstallContent(); } catch (error) { - this.logService.warn("Review agent setup status failed", error); + this.logService.warn("Review install status failed", error); return undefined; } } @@ -657,6 +654,14 @@ export class ReviewCanvasEditorPane extends EditorPane { await this.desktopConnection.removeCliInstall(request); return this.desktopConnection.getCliInstallStatus(); }, + removeLegacySkills: async () => { + await this.desktopConnection.removeLegacySkills(); + return this.desktopConnection.getCliInstallStatus(); + }, + finishUpdate: async () => { + await this.desktopConnection.finishCliInstallUpdate(); + return this.desktopConnection.getCliInstallStatus(); + }, decline: async () => { await this.desktopConnection.declineCliInstall(); return this.desktopConnection.getCliInstallStatus(); @@ -779,21 +784,6 @@ export class ReviewCanvasEditorPane extends EditorPane { return this.configurationService.getValue(REVIEW_TELEMETRY_SETTING) !== false; } - /** - * Install status for the Home setup banner. Home must render even when the - * status endpoint fails, so a failure yields no banner. - */ - private async resolveHomeSetup(): Promise { - try { - return { - status: await this.desktopConnection.getCliInstallStatus(), - open: () => void this.tabsService.openWelcome(true), - }; - } catch { - return undefined; - } - } - /** * Step state for the Welcome rail, derived from the install status the * caller already fetched, so one render costs one status round-trip. The @@ -803,7 +793,7 @@ export class ReviewCanvasEditorPane extends EditorPane { const checked = new Set(this.readTutorialProgress().checked); const steps = REVIEW_TUTORIAL_STEP_IDS.filter((step) => step !== "openMap" || this.currentSoftwareMapEnabled()); return { - installed: status.agents.some((agent) => agent.installed), + installed: !status.cli || status.shim.installed, tutorialChecked: steps.filter((step) => checked.has(step)).length, tutorialTotal: steps.length, // Drafts are filtered out of this list and the tutorial never diff --git a/apps/review-desktop/code-oss/src/vs/review/common/reviewCliInstall.test.ts b/apps/review-desktop/code-oss/src/vs/review/common/reviewCliInstall.test.ts deleted file mode 100644 index 98d778f88..000000000 --- a/apps/review-desktop/code-oss/src/vs/review/common/reviewCliInstall.test.ts +++ /dev/null @@ -1,85 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) dev.fast. All rights reserved. - * Licensed under the MIT License. See LICENSE in the repository root for license information. - *--------------------------------------------------------------------------------------------*/ - -import assert from 'node:assert/strict'; -import test from 'node:test'; - -import type { ReviewCliInstallStatus } from './reviewProtocol.js'; -import { reviewCliInstallResyncRequest } from './reviewCliInstall.js'; - -const baseStatus: ReviewCliInstallStatus = { - agents: [], - fingerprint: 'current', - stamp: null, - stale: true, - shim: { - path: '/home/test/.local/bin/review', - installed: false, - profileConfigured: false, - onPath: false, - }, - fff: { - serverName: 'fff', - corpusRoot: '/home/test/.dev/trace-search', - binary: { path: '/home/test/.local/bin/fff-mcp', installed: false }, - registrations: [], - }, - trace: { - enabled: false, - configured: false, - autoActivateRepositories: false, - envPath: '/home/test/.dev/trace.env', - settingsPath: '/home/test/.dev/trace-settings.json', - }, - cli: null, -}; - -test('resyncs a CLI-only installation', () => { - assert.deepEqual( - reviewCliInstallResyncRequest({ - ...baseStatus, - agents: [{ target: 'codex', present: true, installed: true }], - stamp: { - consent: 'granted', - targets: [], - shimPath: '/home/test/.local/bin/review', - updatedAt: '2026-09-02T00:00:00.000Z', - }, - }), - { targets: [], shim: true, autoUpdate: true }, - ); -}); - -test('falls back to installed skills for a legacy stamp without targets', () => { - assert.deepEqual( - reviewCliInstallResyncRequest({ - ...baseStatus, - agents: [{ target: 'codex', present: true, installed: true }], - stamp: { - consent: 'granted', - updatedAt: '2026-09-02T00:00:00.000Z', - }, - }), - { targets: ['codex'], shim: false, autoUpdate: true }, - ); -}); - -test('preserves an explicit CLI opt-out while resyncing skills', () => { - assert.deepEqual( - reviewCliInstallResyncRequest({ - ...baseStatus, - stamp: { - consent: 'granted', - targets: ['codex'], - updatedAt: '2026-09-02T00:00:00.000Z', - }, - }), - { targets: ['codex'], shim: false, autoUpdate: true }, - ); -}); - -test('skips resync when neither skills nor the CLI are managed', () => { - assert.equal(reviewCliInstallResyncRequest(baseStatus), undefined); -}); diff --git a/apps/review-desktop/code-oss/src/vs/review/common/reviewCliInstall.ts b/apps/review-desktop/code-oss/src/vs/review/common/reviewCliInstall.ts deleted file mode 100644 index 7f74af15f..000000000 --- a/apps/review-desktop/code-oss/src/vs/review/common/reviewCliInstall.ts +++ /dev/null @@ -1,7 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) dev.fast. All rights reserved. - * Licensed under the MIT License. See LICENSE in the repository root for license information. - *--------------------------------------------------------------------------------------------*/ - -export type { ReviewCliInstallResyncRequest } from './reviewProtocol.js'; -export { reviewCliInstallResyncRequest } from './reviewProtocol.js'; diff --git a/apps/review-desktop/code-oss/src/vs/review/common/reviewCliInstallStartup.test.ts b/apps/review-desktop/code-oss/src/vs/review/common/reviewCliInstallStartup.test.ts new file mode 100644 index 000000000..86c53e7bc --- /dev/null +++ b/apps/review-desktop/code-oss/src/vs/review/common/reviewCliInstallStartup.test.ts @@ -0,0 +1,43 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) dev.fast. All rights reserved. + * Licensed under the MIT License. See LICENSE in the repository root for license information. + *--------------------------------------------------------------------------------------------*/ + +import assert from 'node:assert/strict'; +import test from 'node:test'; + +import type { ReviewCliInstallStatus } from './reviewProtocol.js'; +import { reviewCliInstallStartupAction } from './reviewCliInstallStartup.js'; + +const base: ReviewCliInstallStatus = { + fingerprint: 'f', + stamp: null, + stale: false, + updateNeeded: false, + shim: { path: '/p', installed: true, profileConfigured: true, onPath: true }, + trace: { enabled: false, configured: false, autoActivateRepositories: false, envPath: '/e', settingsPath: '/s' }, + cli: null, + connect: { command: 'review', args: ['mcp'], prompts: { claude: '', codex: '', cursor: '', opencode: '', pi: '' }, plugins: { claude: { label: 'c' }, codex: { label: 'c' }, cursor: { label: 'c' }, opencode: { label: 'c' }, pi: { label: 'c' } } }, + legacySkills: [], +}; + +test('opens Welcome for an upgrader before resyncing', () => { + assert.equal( + reviewCliInstallStartupAction({ ...base, stamp: { consent: 'granted', updatedAt: 't' }, updateNeeded: true, stale: true }), + 'openWelcome', + ); +}); + +test('resyncs a stale stamp', () => { + assert.equal( + reviewCliInstallStartupAction({ ...base, stamp: { consent: 'granted', updatedAt: 't' }, stale: true }), + 'resync', + ); +}); + +test('does nothing when declined', () => { + assert.equal( + reviewCliInstallStartupAction({ ...base, stamp: { consent: 'declined', updatedAt: 't' }, stale: true }), + 'none', + ); +}); diff --git a/apps/review-desktop/code-oss/src/vs/review/common/reviewCliInstallStartup.ts b/apps/review-desktop/code-oss/src/vs/review/common/reviewCliInstallStartup.ts new file mode 100644 index 000000000..ca3554f55 --- /dev/null +++ b/apps/review-desktop/code-oss/src/vs/review/common/reviewCliInstallStartup.ts @@ -0,0 +1,23 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) dev.fast. All rights reserved. + * Licensed under the MIT License. See LICENSE in the repository root for license information. + *--------------------------------------------------------------------------------------------*/ + +import type { ReviewCliInstallStatus } from './reviewProtocol.js'; + +export type ReviewCliInstallStartupAction = 'none' | 'resync' | 'openWelcome'; + +/** + * What a built app does with the install stamp at startup. Only a granted + * stamp acts: an upgrader without the update marker sees the update screen, + * and a stale fingerprint rewrites the review command silently. + */ +export function reviewCliInstallStartupAction(status: ReviewCliInstallStatus): ReviewCliInstallStartupAction { + if (status.stamp?.consent !== 'granted') { + return 'none'; + } + if (status.updateNeeded) { + return 'openWelcome'; + } + return status.stale ? 'resync' : 'none'; +} diff --git a/apps/review-desktop/code-oss/src/vs/review/contrib/install/reviewCliInstall.contribution.ts b/apps/review-desktop/code-oss/src/vs/review/contrib/install/reviewCliInstall.contribution.ts index 581d31766..8affdf8d2 100644 --- a/apps/review-desktop/code-oss/src/vs/review/contrib/install/reviewCliInstall.contribution.ts +++ b/apps/review-desktop/code-oss/src/vs/review/contrib/install/reviewCliInstall.contribution.ts @@ -20,27 +20,12 @@ import { } from "../../../workbench/common/contributions.js"; import { INativeWorkbenchEnvironmentService } from "../../../workbench/services/environment/electron-browser/environmentService.js"; import { LifecyclePhase } from "../../../workbench/services/lifecycle/common/lifecycle.js"; -import { reviewCliInstallResyncRequest } from "../../common/reviewCliInstall.js"; -import { - type ReviewCliInstallStatus, - type ReviewCliInstallTarget, - REVIEW_TUTORIAL_PROGRESS_STORAGE_KEY, -} from "../../common/reviewProtocol.js"; +import { reviewCliInstallStartupAction } from "../../common/reviewCliInstallStartup.js"; +import { REVIEW_TUTORIAL_PROGRESS_STORAGE_KEY } from "../../common/reviewProtocol.js"; +import { IReviewApiCatalogService } from "../../services/reviewApiCatalogService.js"; import { IReviewCanvasEditorTabsService } from "../../services/reviewCanvasEditorTabsService.js"; import { IReviewDesktopConnectionService } from "../../services/reviewDesktopConnectionService.js"; -const TARGET_LABELS: Readonly> = { - claude: "Claude Code", - codex: "Codex", - cursor: "Cursor", - opencode: "OpenCode", - pi: "Pi", -}; - -function formatTargets(targets: readonly ReviewCliInstallTarget[]): string { - return targets.map((target) => TARGET_LABELS[target]).join(", "); -} - /** * The macOS app bundle that contains this build, derived from the resources * path inside it. Development runs live outside a bundle and return undefined. @@ -109,7 +94,7 @@ class InstallReviewCliInPathAction extends Action2 { if (isMacintosh) { await nativeHostService.uninstallShellCommand({ commandName: "review", symlinkOnly: true }); } - const installed = await desktopConnection.applyCliInstall({ targets: [], shim: true }); + const installed = await desktopConnection.applyCliInstall({ shim: true }); notificationService.info( localize( "review.cliInstall.installed", @@ -132,7 +117,7 @@ registerAction2(InstallReviewCliInPathAction); /** * Removes everything the app installed on this machine: the tutorial, the - * agent skills, the review terminal command, and the consent stamp. It then + * review terminal command, managed trace capture, and the consent stamp. It then * points at the app bundle so the user can move it to the Trash. Other Review * data stays untouched. Resetting the stamp makes a later reinstall start as * a first run. @@ -154,26 +139,10 @@ class UninstallReviewDesktopAction extends Action2 { const storageService = accessor.get(IStorageService); const status = await desktopConnection.getCliInstallStatus(); - const targets = status.agents.filter((agent) => agent.installed).map((agent) => agent.target); - const fffTargets = status.stamp?.fffRegistrations?.map((registration) => registration.target) ?? []; - const removalTargets = [...new Set([...targets, ...fffTargets])]; const detail = [ - targets.length > 0 - ? localize( - "review.uninstall.skills", - "Removes the Review skills and unchanged app-managed MCP connections for {0}.", - formatTargets(targets), - ) - : localize("review.uninstall.noSkills", "No agent skills are installed."), status.stamp?.shimPath ? localize("review.uninstall.shim", "Removes the review terminal command at {0}.", status.stamp.shimPath) : localize("review.uninstall.noShim", "The review terminal command is not installed."), - fffTargets.length > 0 - ? localize( - "review.uninstall.fff", - "Removes unchanged fff registrations that Review created. The shared FFF binary stays installed.", - ) - : localize("review.uninstall.noFff", "No fff registrations are managed by Review."), status.stamp?.traceManaged ? localize( "review.uninstall.trace", @@ -193,7 +162,7 @@ class UninstallReviewDesktopAction extends Action2 { } // The tutorial is disposable state: a failed delete must not stop - // the shim and skills removal the user just confirmed. + // the command removal the user just confirmed. let tutorialError: unknown; try { await desktopConnection.deleteTutorial(); @@ -203,9 +172,7 @@ class UninstallReviewDesktopAction extends Action2 { } try { await desktopConnection.removeCliInstall({ - targets: removalTargets, shim: true, - fff: true, ...(status.stamp?.traceManaged ? { trace: true } : {}), }); await desktopConnection.resetCliInstallPrompts(); @@ -217,7 +184,7 @@ class UninstallReviewDesktopAction extends Action2 { } } catch (error) { await dialogService.error( - localize("review.uninstall.failed", "Review could not remove the installed skills and command."), + localize("review.uninstall.failed", "Review could not remove its command and trace setup."), String(error), ); return; @@ -237,7 +204,7 @@ class UninstallReviewDesktopAction extends Action2 { const bundlePath = macAppBundlePath(environmentService.appRoot); if (bundlePath) { const { confirmed: reveal } = await dialogService.confirm({ - message: localize("review.uninstall.done", "The installed skills and command were removed."), + message: localize("review.uninstall.done", "Review's command and trace setup were removed."), detail: localize( "review.uninstall.finish", "To finish, quit Review Desktop and move {0} to the Trash.", @@ -251,7 +218,7 @@ class UninstallReviewDesktopAction extends Action2 { } } else { await dialogService.info( - localize("review.uninstall.done", "The installed skills and command were removed."), + localize("review.uninstall.done", "Review's command and trace setup were removed."), localize("review.uninstall.finishDev", "This is a development build, so there is no app bundle to remove."), ); } @@ -261,13 +228,15 @@ class UninstallReviewDesktopAction extends Action2 { registerAction2(UninstallReviewDesktopAction); /** - * First-run onboarding and silent re-sync. Consent lives in the server's + * First-run onboarding, the upgrade screen, and silent re-sync. Consent lives in the server's * install stamp (~/.dev/review-desktop/state/cli-install.json), not workbench * storage, so the CLI and the app read one source of truth: * - no stamp: open no tab; empty Home renders the Welcome rail, and * Preferences > Getting Started reaches the same pane when Home has * reviews to list instead; - * - granted + stale CLI fingerprint or skill version: re-sync silently after an app update; + * - granted + stamp without the update marker: open Welcome, which shows the + * update screen, unless empty Home already renders the Welcome rail; + * - granted + stale CLI fingerprint: rewrite the review command silently; * - declined or skipped: never open automatically (the menu action stays available). * * Dev sessions (`pnpm dev`, isBuilt false) never auto-open. @@ -277,6 +246,8 @@ class ReviewCliInstallStartup implements IWorkbenchContribution { @INativeWorkbenchEnvironmentService environmentService: INativeWorkbenchEnvironmentService, @INotificationService private readonly notificationService: INotificationService, @IReviewDesktopConnectionService private readonly reviewDesktopConnectionService: IReviewDesktopConnectionService, + @IReviewCanvasEditorTabsService private readonly tabsService: IReviewCanvasEditorTabsService, + @IReviewApiCatalogService private readonly apiCatalog: IReviewApiCatalogService, ) { if (!environmentService.isBuilt) { return; @@ -285,7 +256,7 @@ class ReviewCliInstallStartup implements IWorkbenchContribution { this.notificationService.warn( localize( "review.cliInstall.updateFailed", - "Review could not update its agent skills or CLI: {0}. Retry from Getting Started, or restart Review.", + "Review could not update its CLI: {0}. Retry from Getting Started, or restart Review.", String(error), ), ); @@ -294,41 +265,26 @@ class ReviewCliInstallStartup implements IWorkbenchContribution { private async check(): Promise { const status = await this.reviewDesktopConnectionService.getCliInstallStatus(); - if (status.stamp?.consent === "declined") { - return; - } - if (status.stamp?.consent === "skipped") { - return; - } - if (status.stamp?.consent === "granted") { - if (status.stale) { - await this.resync(status); - } - return; - } - // First run needs no tab: with no reviews to list, Home already renders - // the Welcome rail, so opening one here would show it twice. - } - - private async resync(status: ReviewCliInstallStatus): Promise { - const request = reviewCliInstallResyncRequest(status); - if (!request) { - return; + switch (reviewCliInstallStartupAction(status)) { + case "openWelcome": + // With no reviews to list, Home already renders the Welcome + // rail, so opening a tab here would show it twice. + await this.apiCatalog.initialize(); + if (this.apiCatalog.reviews.length > 0) await this.tabsService.openWelcome(true); + return; + case "resync": + // Without an installed command there is nothing to rewrite or announce. + if (!status.stamp?.shimPath || status.stamp.commandDisabled) return; + await this.reviewDesktopConnectionService.applyCliInstall({ + shim: true, + autoUpdate: true, + }); + // Review has no status bar; status() messages would be dropped. + this.notificationService.info(localize("review.cliInstall.resyncedCli", "Review updated the installed CLI.")); + return; + case "none": + return; } - await this.reviewDesktopConnectionService.applyCliInstall(request); - const message = - request.targets.length === 0 - ? localize("review.cliInstall.resyncedCli", "Review updated the installed CLI.") - : request.shim - ? localize( - "review.cliInstall.resynced", - "Review updated the CLI, agent skills, and MCP connections. Restart your agent or reconnect MCP to load the changes.", - ) - : localize( - "review.cliInstall.resyncedSkills", - "Review updated the agent skills and MCP connections. Restart your agent or reconnect MCP to load the changes.", - ); - this.notificationService.status(message, { hideAfter: 10_000 }); } } diff --git a/apps/review-desktop/code-oss/src/vs/review/services/reviewCanvasEditorTabsService.test.ts b/apps/review-desktop/code-oss/src/vs/review/services/reviewCanvasEditorTabsService.test.ts new file mode 100644 index 000000000..db3c80a4c --- /dev/null +++ b/apps/review-desktop/code-oss/src/vs/review/services/reviewCanvasEditorTabsService.test.ts @@ -0,0 +1,50 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) dev.fast. All rights reserved. + * Licensed under the MIT License. See LICENSE in the repository root for license information. + *--------------------------------------------------------------------------------------------*/ + +import assert from "node:assert/strict"; +import test from "node:test"; + +import { Event } from "../../base/common/event.js"; +import { ReviewCanvasEditorInput } from "../browser/parts/canvas/reviewCanvasEditorInput.js"; +import { ReviewCanvasEditorTabsService } from "./reviewCanvasEditorTabsService.js"; + +async function closeWelcome(updateNeeded: boolean): Promise { + let finished = 0; + const instantiation = { + createInstance(_ctor: unknown, target: never) { + return new ReviewCanvasEditorInput(target, {} as never); + }, + }; + const editors = { onDidCloseEditor: Event.None, async openEditor() {} }; + const groups = { groups: [], mainPart: { activeGroup: undefined } }; + const connection = { + async getCliInstallStatus() { + return { updateNeeded }; + }, + async finishCliInstallUpdate() { + finished += 1; + }, + }; + const tabs = new ReviewCanvasEditorTabsService( + instantiation as never, + editors as never, + groups as never, + connection as never, + { warn() {} } as never, + ); + try { + const welcome = await tabs.openWelcome(true); + welcome.dispose(); + await new Promise((resolve) => setImmediate(resolve)); + return finished; + } finally { + tabs.dispose(); + } +} + +test("closing Welcome finishes the CLI install update only when one is pending", async () => { + assert.equal(await closeWelcome(true), 1); + assert.equal(await closeWelcome(false), 0); +}); diff --git a/apps/review-desktop/code-oss/src/vs/review/services/reviewCanvasEditorTabsService.ts b/apps/review-desktop/code-oss/src/vs/review/services/reviewCanvasEditorTabsService.ts index 70237ae06..4733cd2c0 100644 --- a/apps/review-desktop/code-oss/src/vs/review/services/reviewCanvasEditorTabsService.ts +++ b/apps/review-desktop/code-oss/src/vs/review/services/reviewCanvasEditorTabsService.ts @@ -3,10 +3,12 @@ * Licensed under the MIT License. See LICENSE in the repository root for license information. *--------------------------------------------------------------------------------------------*/ +import { Event } from "../../base/common/event.js"; import { Disposable } from "../../base/common/lifecycle.js"; import { URI } from "../../base/common/uri.js"; import type { ITextEditorOptions } from "../../platform/editor/common/editor.js"; import { createDecorator, IInstantiationService } from "../../platform/instantiation/common/instantiation.js"; +import { ILogService } from "../../platform/log/common/log.js"; import type { EditorInput } from "../../workbench/common/editor/editorInput.js"; import { isResourceDiffEditorInput, isResourceEditorInput, type IUntypedEditorInput } from "../../workbench/common/editor.js"; import { IEditorGroupsService } from "../../workbench/services/editor/common/editorGroupsService.js"; @@ -54,6 +56,7 @@ export class ReviewCanvasEditorTabsService extends Disposable implements IReview @IReviewDesktopConnectionService private readonly desktopConnection: IReviewDesktopConnectionService, @IHostService private readonly host: IHostService, + @ILogService private readonly logService: ILogService, ) { super(); this._register( @@ -157,6 +160,9 @@ export class ReviewCanvasEditorTabsService extends Disposable implements IReview if (!input || input.isDisposed()) { input = this.instantiationService.createInstance(ReviewCanvasEditorInput, target); this.inputs.set(target.kind, input); + if (target.kind === "welcome") { + Event.once(input.onWillDispose)(() => void this.finishCliInstallUpdate()); + } } configure?.(input); // A control command may arrive while an Ask's loading pane has focus. @@ -167,6 +173,19 @@ export class ReviewCanvasEditorTabsService extends Disposable implements IReview return input; } + /** + * Closing Welcome while it shows the update screen counts as finishing the + * update, so an upgrader is not sent back to it on the next launch. + */ + private async finishCliInstallUpdate(): Promise { + try { + const status = await this.desktopConnection.getCliInstallStatus(); + if (status.updateNeeded) await this.desktopConnection.finishCliInstallUpdate(); + } catch (error) { + this.logService.warn("[Review] Could not finish the CLI install update:", error); + } + } + private async openReviewInput(input: ReviewCanvasEditorInput, active: boolean): Promise { // A control command may arrive while an Ask's loading pane has focus. // Reuse the review's group instead of mounting a second canvas there. diff --git a/apps/review-desktop/code-oss/src/vs/review/services/reviewDesktopConnectionService.test.ts b/apps/review-desktop/code-oss/src/vs/review/services/reviewDesktopConnectionService.test.ts index 8a8c9d066..6cc318f67 100644 --- a/apps/review-desktop/code-oss/src/vs/review/services/reviewDesktopConnectionService.test.ts +++ b/apps/review-desktop/code-oss/src/vs/review/services/reviewDesktopConnectionService.test.ts @@ -47,7 +47,7 @@ function mockFetch(t: { after(callback: () => void): void }, handler: typeof fet }); } -test("install status shares concurrent scans and detects agents on subsequent checks", async (t) => { +test("install status shares concurrent scans and refreshes on subsequent checks", async (t) => { const service = serviceWith(); t.after(() => service.dispose()); let requests = 0; @@ -55,12 +55,12 @@ test("install status shares concurrent scans and detects agents on subsequent ch requests += 1; if (requests === 3) return Response.json({ error: "scan failed" }, { status: 500 }); return Response.json({ - agents: [{ target: "codex", present: requests > 1, installed: false }], - fingerprint: "test", stamp: null, stale: false, + fingerprint: "test", stamp: null, stale: requests > 1, updateNeeded: false, shim: { path: "/tmp/review", installed: false, profileConfigured: false, onPath: false }, - fff: { serverName: "fff", corpusRoot: "/tmp/traces", binary: { path: "/tmp/fff", installed: false }, registrations: [] }, trace: { enabled: false, configured: false, autoActivateRepositories: false, envPath: "/tmp/env", settingsPath: "/tmp/settings" }, cli: null, + connect: { command: "review", args: ["mcp"], prompts: { claude: "c", codex: "c", cursor: "c", opencode: "c", pi: "c" }, plugins: { claude: { label: "c" }, codex: { label: "c" }, cursor: { label: "c" }, opencode: { label: "c" }, pi: { label: "c" } } }, + legacySkills: [], }); }); @@ -68,13 +68,13 @@ test("install status shares concurrent scans and detects agents on subsequent ch service.getCliInstallStatus(), service.getCliInstallStatus(), ]); - assert.equal(first.agents[0].present, false); - assert.equal(second.agents[0].present, false); + assert.equal(first.stale, false); + assert.equal(second.stale, false); assert.equal(requests, 1); - assert.equal((await service.getCliInstallStatus()).agents[0].present, true); + assert.equal((await service.getCliInstallStatus()).stale, true); assert.equal(requests, 2); await assert.rejects(service.getCliInstallStatus(), /scan failed/); - assert.equal((await service.getCliInstallStatus()).agents[0].present, true); + assert.equal((await service.getCliInstallStatus()).stale, true); assert.equal(requests, 4); }); @@ -145,13 +145,13 @@ test("tutorial deletion suppresses auto-prepare across restarts until explicit o restoredService.dispose(); }); -test("passes automatic skill updates to the server without enabling optional integrations", async (t) => { +test("passes automatic command updates to the server without enabling optional integrations", async (t) => { const service = serviceWith(); let requestBody: unknown; mockFetch(t, async (_url, init) => { requestBody = JSON.parse(String(init?.body)); return Response.json({ ok: true, output: "updated" }); }); - await service.applyCliInstall({ targets: ["codex"], shim: false, autoUpdate: true }); - assert.deepEqual(requestBody, { targets: ["codex"], shim: false, autoUpdate: true }); + await service.applyCliInstall({ shim: false, autoUpdate: true }); + assert.deepEqual(requestBody, { shim: false, autoUpdate: true }); }); diff --git a/apps/review-desktop/code-oss/src/vs/review/services/reviewDesktopConnectionService.ts b/apps/review-desktop/code-oss/src/vs/review/services/reviewDesktopConnectionService.ts index ea1a94b51..8979c0284 100644 --- a/apps/review-desktop/code-oss/src/vs/review/services/reviewDesktopConnectionService.ts +++ b/apps/review-desktop/code-oss/src/vs/review/services/reviewDesktopConnectionService.ts @@ -26,7 +26,6 @@ parseReviewDesktopVerbFrame, parseReviewTutorialOpenResponse, type ReviewCliInstallApplyResponse, type ReviewCliInstallStatus, -type ReviewCliInstallTarget, type ReviewTutorialOpenResponse, type ReviewVerbResponse } from "../common/reviewProtocol.js"; @@ -56,7 +55,7 @@ export interface IReviewDesktopConnectionService { saveDiffrSummarizer(input: ReviewDiffrSummarizerInput): Promise; testDiffrSummarizer(input: ReviewDiffrSummarizerInput): Promise; setDiffrConfigValue(key: string, value: JsonValue): Promise; - /** The scratchpad preference: a server preference, since `review install` reads it too. */ + /** The scratchpad preference: a server preference, since the review server reads it. */ readScratchpadEnabled(): Promise; setScratchpadEnabled(enabled: boolean): Promise; getTutorialStatus(): Promise<{ version: 1; reviewUuid: string | null }>; @@ -66,17 +65,12 @@ export interface IReviewDesktopConnectionService { getCliInstallStatus(): Promise; applyCliInstall(request: { autoUpdate?: boolean; - targets: readonly ReviewCliInstallTarget[]; shim?: boolean; - fff?: boolean; trace?: true | { endpoint?: string; bucket?: string; key?: string; secret?: string }; }): Promise; - removeCliInstall(request: { - targets: readonly ReviewCliInstallTarget[]; - shim?: boolean; - fff?: boolean; - trace?: true; - }): Promise; + removeCliInstall(request: { shim?: boolean; trace?: true }): Promise; + removeLegacySkills(): Promise; + finishCliInstallUpdate(): Promise; declineCliInstall(): Promise; skipCliInstallPrompts(): Promise; resetCliInstallPrompts(): Promise; @@ -200,7 +194,6 @@ export class ReviewDesktopConnectionService extends Disposable implements IRevie method: "PUT", headers: { ...this.authHeaders(), "content-type": "application/json" }, body: JSON.stringify({ enabled }), - // Also installs or removes the scratchpad skill for every agent. signal: AbortSignal.timeout(120_000), }); await this.requireOk(response, "scratchpad preference"); @@ -339,9 +332,7 @@ export class ReviewDesktopConnectionService extends Disposable implements IRevie async applyCliInstall(request: { autoUpdate?: boolean; - targets: readonly ReviewCliInstallTarget[]; shim?: boolean; - fff?: boolean; trace?: true | { endpoint?: string; bucket?: string; key?: string; secret?: string }; }): Promise { await this.initialize(); @@ -352,10 +343,8 @@ export class ReviewDesktopConnectionService extends Disposable implements IRevie "content-type": "application/json", }, body: JSON.stringify({ - targets: request.targets, ...(request.autoUpdate ? { autoUpdate: true } : {}), ...(request.shim !== undefined ? { shim: request.shim } : {}), - ...(request.fff ? { fff: true } : {}), ...(request.trace !== undefined ? { trace: request.trace } : {}), }), signal: AbortSignal.timeout(120_000), @@ -374,12 +363,7 @@ export class ReviewDesktopConnectionService extends Disposable implements IRevie return parseReviewCliInstallApplyResponse(payload); } - async removeCliInstall(request: { - targets: readonly ReviewCliInstallTarget[]; - shim?: boolean; - fff?: boolean; - trace?: true; - }): Promise { + async removeCliInstall(request: { shim?: boolean; trace?: true }): Promise { await this.initialize(); const response = await fetch(`${this.serverUrl}/install/remove`, { method: "POST", @@ -388,9 +372,7 @@ export class ReviewDesktopConnectionService extends Disposable implements IRevie "content-type": "application/json", }, body: JSON.stringify({ - targets: request.targets, ...(request.shim ? { shim: true } : {}), - ...(request.fff ? { fff: true } : {}), ...(request.trace ? { trace: true } : {}), }), signal: AbortSignal.timeout(30_000), @@ -400,6 +382,14 @@ export class ReviewDesktopConnectionService extends Disposable implements IRevie } } + async removeLegacySkills(): Promise { + await this.postCliInstallVerb("legacy-skills/remove"); + } + + async finishCliInstallUpdate(): Promise { + await this.postCliInstallVerb("finish-update"); + } + async declineCliInstall(): Promise { await this.postCliInstallVerb("decline"); } @@ -412,7 +402,7 @@ export class ReviewDesktopConnectionService extends Disposable implements IRevie await this.postCliInstallVerb("reset"); } - private async postCliInstallVerb(verb: "decline" | "skip" | "reset"): Promise { + private async postCliInstallVerb(verb: "decline" | "skip" | "reset" | "legacy-skills/remove" | "finish-update"): Promise { await this.initialize(); const response = await fetch(`${this.serverUrl}/install/${verb}`, { method: "POST", diff --git a/apps/review-desktop/scripts/package-linux.sh b/apps/review-desktop/scripts/package-linux.sh index a2dc75cbf..0afc72339 100755 --- a/apps/review-desktop/scripts/package-linux.sh +++ b/apps/review-desktop/scripts/package-linux.sh @@ -40,7 +40,7 @@ node "$APP_DIR/scripts/curated-extensions.mjs" \ --copy-to "$PACKAGED_ROOT/resources/app/extensions" # The installed app embeds its own Review server runtime (server, CLI, and -# agent skills) so it never reaches back into this checkout. +# docs) so it never reaches back into this checkout. pnpm --dir "$MONOREPO_ROOT" --filter @dev.fast/review build pnpm --dir "$MONOREPO_ROOT" --filter @dev.fast/review ensure:diffr --required node "$APP_DIR/scripts/stage-review-runtime.mjs" --packaged-root "$PACKAGED_ROOT" diff --git a/apps/review-desktop/scripts/skill-versions.test.mjs b/apps/review-desktop/scripts/skill-versions.test.mjs deleted file mode 100644 index 863a38298..000000000 --- a/apps/review-desktop/scripts/skill-versions.test.mjs +++ /dev/null @@ -1,90 +0,0 @@ -import assert from "node:assert/strict"; -import { - cp, - link, - mkdir, - mkdtemp, - readFile, - rm, - writeFile, -} from "node:fs/promises"; -import { tmpdir } from "node:os"; -import path from "node:path"; -import test from "node:test"; -import { fileURLToPath } from "node:url"; - -import { stampReviewSkills } from "./stage-review-runtime.mjs"; - -const appRoot = path.resolve( - path.dirname(fileURLToPath(import.meta.url)), - "..", -); - -const sourceSkills = path.resolve( - appRoot, - "../../packages/review/skills", -); - -test("stamps all packaged skills with the Desktop release, preserving source hardlinks", async () => { - const root = await mkdtemp(path.join(tmpdir(), "review-stamp-skills-")); - - try { - const runtime = path.join(root, "runtime"); - await cp(sourceSkills, path.join(runtime, "skills"), { recursive: true }); - const source = path.join(root, "source.md"); - const generated = path.join(runtime, "skills/dev-review/SKILL.md"); - const original = await readFile(generated, "utf8"); - await writeFile(source, original); - await rm(generated); - await link(source, generated); - await stampReviewSkills(runtime); - - const { version } = JSON.parse( - await readFile(path.join(appRoot, "package.json"), "utf8"), - ); - - for (const name of ["dev-review", "trace-archaeology"]) { - const output = await readFile( - path.join(runtime, "skills", name, "SKILL.md"), - "utf8", - ); - - assert.ok(output.includes(`review-version: "${version}"`)); - assert.ok(output.includes('review-managed-by: "Review Desktop"')); - assert.ok(output.includes("Do not edit.")); - } - - assert.equal(await readFile(source, "utf8"), original); - await stampReviewSkills(runtime, "2.0.0-preview.1"); - assert.ok( - (await readFile(generated, "utf8")).includes( - 'review-version: "2.0.0-preview.1"', - ), - ); - } finally { - await rm(root, { recursive: true, force: true }); - } -}); - -test("refuses invalid release versions and skills without generated metadata", async () => { - const root = await mkdtemp(path.join(tmpdir(), "review-invalid-skill-")); - - try { - await assert.rejects( - stampReviewSkills(root, "not-a-version"), - /release version/, - ); - const skill = path.join(root, "skills/dev-review"); - await mkdir(skill, { recursive: true }); - await writeFile( - path.join(skill, "SKILL.md"), - "---\nname: dev-review\ndescription: test\n---\n", - ); - await assert.rejects( - stampReviewSkills(root, "1.0.0"), - /Missing generated skill metadata/, - ); - } finally { - await rm(root, { recursive: true, force: true }); - } -}); diff --git a/apps/review-desktop/scripts/stage-review-runtime-docs.test.mjs b/apps/review-desktop/scripts/stage-review-runtime-docs.test.mjs index 5202b0a58..83c246975 100644 --- a/apps/review-desktop/scripts/stage-review-runtime-docs.test.mjs +++ b/apps/review-desktop/scripts/stage-review-runtime-docs.test.mjs @@ -35,15 +35,13 @@ async function temporaryRoot(prefix) { return root; } -test("stages the complete Review documentation tree inside dev-review", async () => { +test("stages the complete Review documentation tree under the runtime root", async () => { const root = await temporaryRoot("review-runtime-docs-"); const runtimeRoot = path.join(root, "review-runtime"); - const skillRoot = path.join(runtimeRoot, "skills", "dev-review"); const docsRoot = path.join(root, "source-docs"); - await mkdir(path.join(skillRoot, "docs"), { recursive: true }); + await mkdir(path.join(runtimeRoot, "docs"), { recursive: true }); await mkdir(path.join(docsRoot, "assets"), { recursive: true }); - await writeFile(path.join(skillRoot, "SKILL.md"), "# dev-review\n"); - await writeFile(path.join(skillRoot, "docs", "stale.md"), "stale\n"); + await writeFile(path.join(runtimeRoot, "docs", "stale.md"), "stale\n"); await writeFile(path.join(docsRoot, "README.md"), "# Review docs\n"); await writeFile(path.join(docsRoot, "guide.md"), "# Guide\n"); await writeFile( @@ -52,6 +50,7 @@ test("stages the complete Review documentation tree inside dev-review", async () ); const destination = await stageReviewDocs(runtimeRoot, docsRoot); + assert.equal(destination, path.join(runtimeRoot, "docs")); assert.equal( await readFile(path.join(destination, "README.md"), "utf8"), diff --git a/apps/review-desktop/scripts/stage-review-runtime.mjs b/apps/review-desktop/scripts/stage-review-runtime.mjs index 6f0a97b67..a32d910c1 100644 --- a/apps/review-desktop/scripts/stage-review-runtime.mjs +++ b/apps/review-desktop/scripts/stage-review-runtime.mjs @@ -11,15 +11,12 @@ import { realpath, rm, stat, - writeFile, } from "node:fs/promises"; import { createRequire } from "node:module"; import path from "node:path"; import { fileURLToPath, pathToFileURL } from "node:url"; import { promisify } from "node:util"; -import { valid as validVersion } from "semver"; - const execFileAsync = promisify(execFile); const appDirectory = path.resolve( @@ -48,9 +45,9 @@ export const REQUIRED_RUNTIME_ENTRIES = [ RUNTIME_SERVER_ENTRY, RUNTIME_CLI_ENTRY, "bin/diffr", - "skills/dev-review/SKILL.md", - "skills/dev-review/docs/README.md", - "skills/trace-archaeology/SKILL.md", + "dist/cli.js", + "docs/README.md", + "instructions/authoring.md", "tutorial/runtime-manifest.json", "node_modules", ]; @@ -135,71 +132,23 @@ export async function stageReviewRuntime(packagedRoot) { await stageReviewDocs(runtimeRoot); await stageDiffrBinary(runtimeRoot); - await stampReviewSkills(runtimeRoot); await makeTreeOwnerWritable(path.join(runtimeRoot, "tutorial", "git-stub")); await assertRuntimeClosure(runtimeRoot); return runtimeRoot; } -/** Stamp only deployed copies, before signing; source skills remain editable. */ -export async function stampReviewSkills(runtimeRoot, version) { - const releaseVersion = - version ?? - JSON.parse(await readFile(path.join(appDirectory, "package.json"), "utf8")) - .version; - - if (!validVersion(releaseVersion)) { - throw new Error( - "A Review Desktop release version is required to stamp skills.", - ); - } - - const skillsRoot = path.join(runtimeRoot, "skills"); - - for (const entry of await readdir(skillsRoot, { withFileTypes: true })) { - if (!entry.isDirectory()) continue; - const skillPath = path.join(skillsRoot, entry.name, "SKILL.md"); - const source = await readFile(skillPath, "utf8"); - const frontmatter = /^---\r?\n([\s\S]*?)\r?\n---/.exec(source); - - if ( - !frontmatter || - !/^ review-version: "[^"\n]+"$/m.test(frontmatter[1]) - ) { - throw new Error(`Missing generated skill metadata: ${skillPath}`); - } - - const stamped = frontmatter[0].replace( - /^ review-version: "[^"\n]+"$/m, - ` review-version: ${JSON.stringify(releaseVersion)}`, - ); - - // pnpm deploy may hardlink files from its store. Never modify that inode. - await rm(skillPath); - await writeFile(skillPath, stamped + source.slice(frontmatter[0].length)); - } -} - export async function stageReviewDocs( runtimeRoot, sourceDocsRoot = path.join(monorepoRoot, "docs"), ) { - const skillRoot = path.join(runtimeRoot, "skills", "dev-review"); - - if (!(await isDirectory(skillRoot))) { - throw new Error( - `Cannot stage Review documentation without the dev-review skill: ${skillRoot}`, - ); - } - if (!(await isDirectory(sourceDocsRoot))) { throw new Error( `Review documentation source is missing: ${sourceDocsRoot}`, ); } - const destination = path.join(skillRoot, "docs"); + const destination = path.join(runtimeRoot, "docs"); await rm(destination, { recursive: true, force: true }); await cp(sourceDocsRoot, destination, { recursive: true }); await assertMatchingFileTrees(sourceDocsRoot, destination); diff --git a/docs/agents.md b/docs/agents.md index 96092b0ec..3d4b3177d 100644 --- a/docs/agents.md +++ b/docs/agents.md @@ -1,78 +1,156 @@ # Coding agents Headless install -> Provider boundary. +Outline: Connect an agent -> Plugins -> Update from an earlier version -> Trace search +-> Review instructions -> Change review -> Architecture review -> Provider boundary. --> -Review works with Claude Code, Codex, and other coding agents. The desktop app -installs a small set of skills that teaches the agent how to create, author, -validate, and update a Review through the Review API and MCP tools. +Review works with Claude Code, Codex, Cursor, OpenCode, and Pi. Each agent +connects to Review's MCP server; the running Review server supplies the +authoring instructions. -## Built-in setup +## Connect an agent -Review Desktop provides setup shortcuts for these agent-specific skill -locations: +Review does not edit agent configuration. Instead, it gives you a prompt that +tells the agent to connect itself. -| Agent | Install target | Skill location | +1. Open Review. Use the **Welcome** tab, or **Settings → Agents**. +2. Choose **Install review in PATH** if Review offers it. The connection + runs `~/.local/bin/review`. +3. Choose **Copy prompt** next to your agent. **Show prompt** reveals the text + so you can read it first. +4. Paste the prompt into a session of that agent. + +The agent registers an MCP server named `review` in its user-level +configuration, not in the project. It replaces an existing `review` entry. The +server runs this command: + +```text +command: sh +args: ["-c", "exec \"$HOME/.local/bin/review\" mcp"] +``` + +Agents started from an app do not see your shell `PATH`, so the prompt uses +the full path through `sh`. When Review has no launcher in `~/.local/bin`, as +when it runs from source, the prompt registers `review` with args `["mcp"]` +instead. That form needs `review` on the agent's `PATH`. + +The agent then deletes old skills that Whiteboard Desktop installed, reloads its +MCP tools or asks for a restart, and calls `session_get_instructions` to confirm +the connection. + +Review does not check which agents are connected. Paste the prompt again at +any time; it replaces the existing entry. To disconnect an agent, remove its +`review` MCP server with that agent's own settings. + +Review and Review Preview share one launcher, which connects to whichever app +is open. Run one at a time. + +### From a terminal + +`whiteboard connect` prints the same prompt that Desktop copies: + +```sh +whiteboard connect codex +whiteboard connect claude cursor +whiteboard connect +``` + +With no agent, it prints the prompts for all five agents. Pass `--json` for a +`connect` event that maps each agent to its prompt. + +### Agent differences + +| Agent | Target | What the prompt sets up | | --- | --- | --- | -| Claude Code | `claude` or `claude-code` | `~/.claude/skills` | -| Codex | `codex` | `~/.agents/skills` | -| Cursor | `cursor` | `~/.cursor/skills` | +| Claude Code | `claude` or `claude-code` | `review` MCP server | +| Codex | `codex` | `review` MCP server and one line in `~/.codex/AGENTS.md` | +| Cursor | `cursor` | `review` MCP server | +| OpenCode | `opencode` | `review` MCP server | +| Pi | `pi` | `review` skill in `~/.agents/skills` | -Other coding agents that follow the shared Agent Skills convention can load the -same Review skills from `~/.agents/skills`. +Codex finds MCP tools only when its instructions name them. Its prompt adds +this line to `~/.codex/AGENTS.md`: "For code reviews and explaining code, use +the `review` MCP server: call `session_get_instructions` first." -Review Desktop is the recommended installation path. On first launch it detects -installed agents, asks which integrations to enable, and keeps their skills in -sync with app updates. Generated skills carry the Review Desktop release version in -`SKILL.md` frontmatter. On the first launch after an update, Desktop replaces -older skills for enabled integrations automatically, including local edits. -Skills already at the bundled version are left alone. Start a new agent session -to load refreshed skills. Reinstall from settings to repair same-version edits -or missing supporting files; terminal-only installs are not automatically enrolled. You can manage the integrations later from Review -settings. +Pi has no MCP support. Its prompt writes a small `review` skill that runs +`whiteboard api session_get_instructions` through `~/.local/bin/review`. -For Codex and Claude Code, Desktop setup also registers a user-level `review` -MCP connection. It launches a small adapter using Review's bundled runtime; no -separate Node installation, agent CLI, port, or token configuration is needed. -The desktop server remains the owner of every review. Other agents can use the -installed `review api` command. +## Plugins -App updates refresh the adapter along with enabled integrations and repair missing -MCP entries. Reinstall in settings runs the same setup again. Review leaves -customized MCP entries alone and explains how to replace them if desired; -uninstall removes only unchanged entries it created. Restart the agent or -reconnect its MCP server after setup. Start a new session for updated skills. +Each agent also has a plugin or package that registers the same `review` MCP +server. Installing it replaces the paste step. The connect card shows the +install command, or the Cursor link, above **or paste this prompt**. -This automatic MCP setup belongs to the Desktop integration flow. The -terminal-only `review install` command still installs skills and the CLI only. +The npm packages and the Cursor listing are not published yet; the Claude Code +and Codex marketplace commands work once the plugins are on `main`. -## Installed skills +| Agent | What installs | Install | +| --- | --- | --- | +| Claude Code | plugin with the `review` MCP server | `/plugin marketplace add devdotfast/review`, then `/plugin install review@devfast` | +| Codex | plugin with the `review` MCP server and a `review` skill | `codex plugin marketplace add devdotfast/review`, then `codex plugin add review@devfast` | +| Cursor | `review` MCP server | **Install in Cursor** on the connect card | +| OpenCode | npm plugin `@dev.fast/opencode-review` | add `"@dev.fast/opencode-review"` to `"plugin"` in `~/.config/opencode/opencode.json` | +| Pi | npm package `@dev.fast/pi-review` with the `review` skill | `pi install npm:@dev.fast/pi-review` | -- `dev-review` authors change and architecture reviews, including software maps. +Where each install comes from: -The authoring skill coordinates the whole workflow. In normal use, ask your -agent for a Review instead of running the lower-level CLI commands yourself. +- Claude Code and Codex read the marketplace files at this repository's root + (`.claude-plugin/marketplace.json`, `.agents/plugins/marketplace.json`) from + the default branch on GitHub. What is on `main` is what installs; there is no + release or package. +- OpenCode and Pi install from npm. +- Cursor uses the link on the connect card. A marketplace listing is separate. -## Start a change review +Every plugin runs the same command as the prompt. Claude Code hides the +plugin's server when a user-level `review` server has the identical command, +so installing both does not create two servers. -Codex: +The Cursor link needs `~/.local/bin/review`. Install the review command first. -```text -Use $dev-review to review my current branch against up to date main, then open -it in Review. -``` +Pi has no MCP support, so its package ships the skill instead of a server. + +## Update from an earlier version + +Earlier versions of Review installed skills into agent configuration. After +the update, Review opens an update screen once. It explains the change and +shows the same **Copy prompt** rows. + +**Remove old Review skills** lists the skill directories it will delete, then +reports what it removed and what it left. It deletes only skills that Review +installed, which have `review-version` in their `SKILL.md` frontmatter. Skills +you wrote are left alone. Until nothing is left to remove, the button also +appears in **Settings → Agents**. + +Choose **Done**, or close the tab, to dismiss the update screen. + +## Trace search + +While trace capture is on, the prompts for Claude Code, Codex, and Pi also set +up trace search with fff. The agent installs `~/.local/bin/fff-mcp` if it is +missing and registers a second MCP server named `fff` over Review's trace +search folder. Pi installs the `@ff-labs/pi-fff` package instead. + +The prompt is generated when you copy it. If you turn on trace capture later, +copy the prompt again. Trace capture itself needs hooks in each agent; see +[Trace storage](cli-reference.md#trace-storage). + +## Review instructions + +Agents call the `session_get_instructions` tool; from a terminal, run +`whiteboard api session_get_instructions '{}'`. The default topic returns the authoring workflow. +Other topics are `file-lenses`, `scratchpad`, and `trace-archaeology`. -Claude Code: +In normal use, ask your agent for a Review instead of running the lower-level +CLI commands yourself. + +## Start a change review ```text -Use the dev-review skill to review my current branch against up to date main, -then open it in Review. +Create a Review of my current branch against up to date main, then open it +in Review. ``` -In Cursor, choose `dev-review` from the `/` menu and give it the same request. - You can replace “current branch” with a pull request URL or tell the agent which base and head revisions to compare. @@ -82,7 +160,7 @@ An architecture Review uses the same canvas without requiring a code diff. Ask for the questions and system boundaries you care about: ```text -Use the Review skill to explain the main data flows, storage boundaries, and +Create a Review that explains the main data flows, storage boundaries, and critical code paths in this repository. Open it in Review when it is ready. ``` @@ -90,20 +168,6 @@ Specific context produces a better Review. Tell the agent what you already believe, which risks you care about, and where you want sequence or database views. -## Install from the terminal - -The desktop app is primary, but headless environments can install integrations -explicitly: - -```sh -review install codex -review install claude cursor -review install all -``` - -With no target, `review install` installs every supported integration. Run -`review install --help` for the current target list. - ## Provider boundary Review runs locally, but a connected coding agent may send source code, prompts, diff --git a/docs/cli-reference.md b/docs/cli-reference.md index 65274c130..638a51f12 100644 --- a/docs/cli-reference.md +++ b/docs/cli-reference.md @@ -2,51 +2,50 @@ Maps -> Agent installation and migration. +-> Maps -> Agent connection and migration. --> -The `review` command is the control surface shared by Review Desktop and coding -agents. Review Desktop installs the preferred CLI in `~/.local/bin` and keeps +The `review` command is the control surface shared by Whiteboard Desktop and coding +agents. Whiteboard Desktop installs the preferred CLI in `~/.local/bin` and keeps it matched to the running app. -Run `review --help` for the authoritative options in your installed +Run `whiteboard --help` for the authoritative options in your installed version. ## Compatibility Review is in beta. Before 1.0, command syntax and JSON event fields may change -between releases. Review Desktop installs an app-managed CLI that matches the +between releases. Whiteboard Desktop installs an app-managed CLI that matches the running app; use that copy instead of relying on compatibility between different CLI and Desktop versions. -The `review map` command group is experimental. Its verbs, Git-notes storage +The `whiteboard map` command group is experimental. Its verbs, Git-notes storage model, and JSON events may change without a migration period before 1.0. -The `review trace` command group and trace capture are experimental and off -by default. `review install` configures S3/R2 capture only when -`--trace-*` credentials are given; Review Desktop exposes it under Settings ▸ -Experimental Features. See [Trace storage](#trace-storage) for the hosted -store and the storage selection commands. +The `whiteboard trace` command group and trace capture are experimental and off +by default. `whiteboard trace storage use s3` configures S3/R2 capture and +`whiteboard trace install` installs the per-agent hooks; Whiteboard Desktop exposes it +under Settings ▸ Experimental Features. See [Trace storage](#trace-storage) +for the hosted store and the storage selection commands. ## Common workflow ```sh -review app launch -review info -review api tools -review app pick --review +whiteboard app launch +whiteboard info +whiteboard api tools +whiteboard app pick --review ``` -Most people let the installed Review skill drive this workflow: it authors -through `review api` or the Review MCP tools. See -`packages/review/skills/dev-review/SKILL.md`. +Most people let their coding agent drive this workflow: it authors through +`whiteboard api` or the Review MCP tools. See [Coding agents](agents.md). -`review api` prints a tool's JSON result, except for text replies such as -`review_get` and `review_diff` patches, which print as-is. List arguments are +`whiteboard api` prints a tool's JSON result, except for text replies such as +`session_get` and `session_diff` patches, which print as-is. List arguments are JSON arrays: ```sh -review api review_diff '{"reviewId":"","format":"patch","paths":["src/app.ts","docs"]}' +whiteboard api session_diff '{"reviewId":"","format":"patch","paths":["src/app.ts","docs"]}' ``` ## Machine-readable output @@ -54,8 +53,8 @@ review api review_diff '{"reviewId":"","format":"patch","paths":["src/app. Commands that expose `--json` accept it after the complete command path: ```sh -review info --json -review app pick --json +whiteboard info --json +whiteboard app pick --json review map check --json review version --json ``` @@ -75,80 +74,78 @@ $ review version --json | Command | Purpose | | ---------------------- | ----------------------------------------------------------------- | -| `review app` | Start Review Desktop (background unless `--focus`). Bare `review app` aliases `app launch`. | -| `review app launch` | Start Review Desktop, or activate a running one with `--focus`. | -| `review app pick` | Select a published Review and optionally choose its opened view. | -| `review info` | List Reviews associated with the current checkout. | -| `review api` | Call a JSON Review authoring tool; `review api tools` lists them. | -| `review mcp` | Serve the same authoring tools over stdio MCP. | -| `review server start` | Run the foreground authoring server without Desktop. | -| `review server status` | Check readiness of the selected headless server. | -| `review map` | Author, validate, and share experimental software maps. | -| `review install` | Install Review skills for supported coding agents. | -| `review migrate apply` | Migrate supported legacy Review data. | -| `review version` | Print the Review package version. | +| `whiteboard app` | Start Whiteboard Desktop (background unless `--focus`). Bare `whiteboard app` aliases `app launch`. | +| `whiteboard app launch` | Start Whiteboard Desktop, or activate a running one with `--focus`. | +| `whiteboard app pick` | Select a published Review and optionally choose its opened view. | +| `whiteboard info` | List Reviews associated with the current checkout. | +| `whiteboard api` | Call a JSON Review authoring tool; `whiteboard api tools` lists them. | +| `whiteboard mcp` | Serve the same authoring tools over stdio MCP. | +| `whiteboard server start` | Run the foreground authoring server without Desktop. | +| `whiteboard server status` | Check readiness of the selected headless server. | +| `whiteboard map` | Author, validate, and share experimental software maps. | +| `whiteboard connect` | Print the prompt that connects a coding agent to Review. | +| `whiteboard migrate apply` | Migrate supported legacy Review data. | +| `whiteboard version` | Print the Review package version. | ## Headless authoring -`review server start` runs the authoring server in the foreground without a -Desktop installation. `review server status --json` checks readiness. Stop the +`whiteboard server start` runs the authoring server in the foreground without a +Desktop installation. `whiteboard server status --json` checks readiness. Stop the server with Ctrl-C or SIGTERM. CLI and MCP clients use the same authoring tools -and shared `dev-review` skill as Desktop. +and instructions as Desktop. Set `DEV_REVIEW_SERVER_DIR` for the server and clients to select a job's saved -state, or use `review --state-dir server start` and -`review --state-dir api …`. The default is +state, or use `whiteboard --state-dir server start` and +`whiteboard --state-dir api …`. The default is `$DEV_REVIEW_HOME` (`~/.dev` by default), shared with Desktop. Optional map generation requires starting with `--software-maps`; existing map uploads remain supported. CI supplies the agent and a prepared checkout with explicit base/head revisions. Each accepted edit is saved as a version immediately; the author holds a -`review_activity` lease while writing, exactly as with Desktop. +`session_activity` lease while writing, exactly as with Desktop. For local testing, open Desktop on the same `DEV_REVIEW_HOME` and select the review from Home. Both hosts use `review-api.db`; edits appear live with the same review ID, history, and resources. The headless server may be stopped after authoring. For a custom `--state-dir` or `DEV_REVIEW_SERVER_DIR`, launch Desktop with `DEV_REVIEW_HOME` set to that directory. From this checkout, use `pnpm dev` -to launch the matching Desktop build. `review server open` has been removed. +to launch the matching Desktop build. `whiteboard server open` has been removed. ## Desktop and discovery ```sh -review app launch -review app pick -review app pick --review --view diff -review info -review info --all +whiteboard app launch +whiteboard app pick +whiteboard app pick --review --view diff +whiteboard info +whiteboard info --all ``` Launches stay in the background; add `--focus` to bring the window forward. -`review app pick` opens an interactive picker when no UUID is given. `review +`whiteboard app pick` opens an interactive picker when no UUID is given. `review info` reports titles, UUIDs, status, and whether each -Review is in sync. It requires Review Desktop to be running. `--all` includes +Review is in sync. It requires Whiteboard Desktop to be running. `--all` includes active Reviews for every worktree in the current repository. -The legacy `review app --review ` form remains a compatibility alias for -`review app pick --review `. +The legacy `whiteboard app --review ` form remains a compatibility alias for +`whiteboard app pick --review `. -`review app pick` accepts `--view` with one of `review`, `commits`, `diff`, +`whiteboard app pick` accepts `--view` with one of `review`, `commits`, `diff`, `map`, or `trace`. ## Authoring -Reviews are created and edited through the JSON API: `review api`, the Review -MCP tools, or the installed dev-review skill. See -`packages/review/skills/dev-review/SKILL.md` -and `packages/review/src/review-api/README.md` -for the authoring workflow and the full tool/route list. `review api tools` -prints the current tool catalog. +Reviews are created and edited through the JSON API: `whiteboard api` or the Review +MCP tools. See `packages/review/src/review-api/README.md` for the authoring +workflow and the full tool/route list. `whiteboard api tools` prints the current +tool catalog. ### Review targets -Register a local checkout with `review_register_repository({path})`, then pass -its `repositoryId` in `target` to `review_create`. Use `review_set_target` to +Register a local checkout with `session_register_repository({path})`, then pass +its `repositoryId` in `target` to `session_create`. Use `session_set_target` to change an existing review's target while preserving its authored content. -These tools are available through `review api` and MCP. +These tools are available through `whiteboard api` and MCP. Live source follows the checkout even in older authored versions; update references as source changes. Choose a commit target when source must stay fixed. @@ -159,7 +156,7 @@ as source changes. Choose a commit target when source must stay fixed. Revisions resolve when the command is accepted. To review the changes introduced by one commit, use its parent as `base`; omitting the base is equivalent to -`base=head`. For a GitHub PR, `review_create({pullRequestUrl})` needs no +`base=head`. For a GitHub PR, `session_create({pullRequestUrl})` needs no target: Review fetches the PR into a registered checkout of its repository (using `gh`, or the public GitHub API for metadata) and pins GitHub's head and diff base, titled from the PR. The pins stay fixed; a later create for the same @@ -186,30 +183,30 @@ Maps are stored per commit in Git notes under `refs/notes/dev-fast/*`. - `prune` removes unreachable notes and fully flushed scratch buffers. - `push` and `fetch` share map notes through `origin`. -Every map verb accepts `--json`. Run `review map --help` for the storage model +Every map verb accepts `--json`. Run `whiteboard map --help` for the storage model and exact verb syntax. ## Trace storage ```sh -review trace status -review trace storage use s3 [--endpoint --bucket --key --secret [--region ]] -review trace storage use hosted [--origin ] -review trace config migrate [--dry-run] [--keep-legacy] -review trace list|show|pull|blame ... [--storage s3|hosted] -review trace sessions [--limit ] [--cursor ] [--storage s3|hosted] [--json] +whiteboard trace status +whiteboard trace storage use s3 [--endpoint --bucket --key --secret [--region ]] +whiteboard trace storage use hosted [--origin ] +whiteboard trace config migrate [--dry-run] [--keep-legacy] +whiteboard trace list|show|pull|blame ... [--storage s3|hosted] +whiteboard trace sessions [--limit ] [--cursor ] [--storage s3|hosted] [--json] review login [--traces] [--origin ] [--no-browser] [--json] review logout review whoami -review trace store create|delete|info [path] -review trace install [--no-harness-hooks] [--all-harnesses] [--json] -review trace uninstall-hooks [--json] -review trace allow [path] [--no-harness-hooks] [--all-harnesses] -review trace deny [path] +whiteboard trace store create|delete|info [path] +whiteboard trace install [--no-harness-hooks] [--all-harnesses] [--json] +whiteboard trace uninstall-hooks [--json] +whiteboard trace allow [path] [--no-harness-hooks] [--all-harnesses] +whiteboard trace deny [path] ``` -`review login` signs in with GitHub identity and verified email access. It does -not enable trace collection. `review login --traces` also requests GitHub +`whiteboard login` signs in with GitHub identity and verified email access. It does +not enable trace collection. `whiteboard login --traces` also requests GitHub repository access. Use `--no-browser` on a remote machine, then open the printed URL on your desktop. Repository access does not replace per-repository trace capture consent. @@ -217,8 +214,8 @@ capture consent. A foreground hosted trace command offers to authorize repositories and resumes once login succeeds. With `--json`, redirected input, or a hook, it never prompts. A missing grant produces `repository_authorization_required` and the remedy -`review login --traces`. Local and direct S3 operations do not request GitHub -permissions. `review login` supplies credentials for both sharing and hosted traces. +`whiteboard login --traces`. Local and direct S3 operations do not request GitHub +permissions. `whiteboard login` supplies credentials for both sharing and hosted traces. ### Sharing a review @@ -231,7 +228,7 @@ The local Review host must be running. Sharing uploads one immutable saved version; an omitted version is resolved once when the request starts. The result contains `shareId`, `version`, and `url`. Running Share again creates a new link unless the same `--request-id` is reused with the same review/version. Recipients do not need a Review account. They do need Git access to the GitHub -repository. Open the link in Review Desktop, or use **Open Shared Review** in +repository. Open the link in Whiteboard Desktop, or use **Open Shared Review** in the command palette. The app downloads the review and fetches its exact base and head commits into a dedicated managed checkout before opening it. @@ -239,7 +236,7 @@ Pin live worktree reviews to commits before sharing. Push the reviewed commits to GitHub before sharing. Publication verifies both commits through a fresh fetch and never pushes them for you. Git uses the machine's existing credentials. Sharing does not request hosted-trace scopes; -use `review login --traces` only when enabling hosted traces. +use `whiteboard login --traces` only when enabling hosted traces. A share includes the saved review, sender attribution, images, retained maps, and whole retained trace conversations. Code, diffs, and commit lists come @@ -253,9 +250,9 @@ Revocation stops new downloads. Already-issued object URLs may work for up to five minutes, and saved copies remain readable. Branch movement and later edits do not change a published snapshot. -`review server start` supports publishing without Desktop. The CLI selects the -same server/profile as `review api`, including `--state-dir`. CI can supply -`DEV_REVIEW_SHARE_TOKEN` instead of a saved `review login`; optional +`whiteboard server start` supports publishing without Desktop. The CLI selects the +same server/profile as `whiteboard api`, including `--state-dir`. CI can supply +`DEV_REVIEW_SHARE_TOKEN` instead of a saved `whiteboard login`; optional `DEV_REVIEW_SHARE_ORIGIN` selects its bare HTTPS service origin. Environment credentials are not written to the profile. Git credentials are still needed for the verification fetch. @@ -305,16 +302,16 @@ S3-compatible bucket you own, R2 included) or the **hosted** store at - `repositories` is hosted-only consent: the repositories you allowed to publish complete session transcripts, and the hosted origins each may publish to (`enabledOrigins` defaults to `https://app.dev.fast`). Written by - `review trace allow` and `review trace deny`. Bucket uploads never read it. + `whiteboard trace allow` and `whiteboard trace deny`. Bucket uploads never read it. An existing bucket setup keeps working unchanged. Without a config file, `~/.config/dev-trace/env` and `settings.json` (or exported `TRACE_R2_*` variables) select the bucket exactly as before; no login, migration, or new configuration is required. -`review trace storage use s3` selects the bucket. With `--endpoint`, +`whiteboard trace storage use s3` selects the bucket. With `--endpoint`, `--bucket`, `--key`, and `--secret` it also saves `stores.s3` after checking -the bucket is reachable. `review trace config migrate` copies an existing +the bucket is reachable. `whiteboard trace config migrate` copies an existing legacy setup into `stores.s3`, refusing to overwrite a different entry or to switch away from a hosted selection; `--dry-run` previews without writing and never prints secrets. After a successful migration the legacy `env` and @@ -323,18 +320,25 @@ their originals so the new file is the only active source; pass `--keep-legacy` to leave them in place. To roll back, rename them back and delete the config file. -`review trace store` manages the hosted store of one repository. `store create` +`whiteboard trace store` manages the hosted store of one repository. `store create` creates it, one time for each repository, and needs push access. `store info` reports the store id, the status, and the stored bytes. `store delete` asks the store to delete the hosted copies, which a repository admin may do; the consent -of this machine stays until `review trace deny` removes it. `review trace -install` installs the harness hooks of this machine and touches no repository. +of this machine stays until `whiteboard trace deny` removes it. `whiteboard trace +install` installs the harness hooks of this machine for every detected agent, +or for all of them with `--all-harnesses`, and touches no repository. Claude +Code, Codex, OpenCode, and Pi have hooks; Cursor has none. For a bucket: -`review trace uninstall-hooks` removes tracing hooks while keeping the CLI, +```sh +whiteboard trace storage use s3 --endpoint --bucket --key --secret +whiteboard trace install +``` + +`whiteboard trace uninstall-hooks` removes tracing hooks while keeping the CLI, login, consent, and captured traces. -`review trace storage use hosted` requires `review login` for the origin, -a store that answers the current contract, and `review trace allow` for the +`whiteboard trace storage use hosted` requires `whiteboard login` for the origin, +a store that answers the current contract, and `whiteboard trace allow` for the checkout's repository at that origin; only then does it persist the selection. Bucket credentials stay saved and inactive. Logging in or creating a store never selects hosted storage by itself, a legacy bucket always @@ -346,17 +350,17 @@ Before setup, review [hosted trace consent and access](privacy.md#hosted-trace-s Use a Desktop release that supports v2 configuration before migration. `--keep-legacy` does not prevent older apps from uploading to their saved bucket. -`review version --verbose [--json]` reports CLI paths, delegation, and build +`whiteboard version --verbose [--json]` reports CLI paths, delegation, and build identity. Set `DEV_FAST_REVIEW_CLI_NO_DELEGATE=1` to inspect the invoked CLI directly. Read commands accept `--storage s3|hosted` to inspect the other store for one operation. The override never changes the selection, capture -settings, or consent. `review trace status` names the effective store, the +settings, or consent. `whiteboard trace status` names the effective store, the configuration sources in use, and the config file, without revealing secrets. On a hosted machine it also prints `Stored bytes`, the size of every completed upload in the repository's store. -`review trace sessions` lists every published session of the current +`whiteboard trace sessions` lists every published session of the current repository's hosted store, ordered by session id, 50 per page. The command needs the hosted store. On a machine that selects s3, pass `--storage hosted`. `--limit` selects a different page size, from 1 to 200. @@ -365,13 +369,13 @@ bytes. When more sessions follow, the last line names the `--cursor` value of the next page, and repeats `--limit` when you gave one. The command refuses a bad `--limit` or `--cursor` before it reads the store. -The command reads the store live. It needs `review login` for the hosted +The command reads the store live. It needs `whiteboard login` for the hosted origin and GitHub read access to the repository. It does not need -`review trace allow`. It never serves saved copies and prints no signed +`whiteboard trace allow`. It never serves saved copies and prints no signed download URL. The hosted store is the only store it lists, so it refuses `--storage s3`. Under `--json` the command prints one `trace.sessions` event. A store older than contract 0.3.0 answers "does not support listing every -session yet"; use `review trace list --commit ` there. +session yet"; use `whiteboard trace list --commit ` there. ### Desktop and npm installed together @@ -383,25 +387,25 @@ and records that absolute path. Desktop preserves an existing npm launcher at `~/.local/bin/review`. Removing Desktop's trace setup leaves another working installation's hooks and shared -capture settings enabled. `review trace uninstall-hooks` is an explicit reset +capture settings enabled. `whiteboard trace uninstall-hooks` is an explicit reset of Review hooks in the selected profile, regardless of which installation created them. ### Tracing without Desktop -The `review trace` commands run without a desktop installation or session. +The `whiteboard trace` commands run without a desktop installation or session. Install the `@dev.fast/review` npm package with Node 24, then run: ```sh review login -review trace store create -review trace allow . -review trace status +whiteboard trace store create +whiteboard trace allow . +whiteboard trace status ``` `allow` writes hooks only for harnesses present on the machine. Use `--all-harnesses` to write all four or `--no-harness-hooks` to write none. -These flags also work with `review trace install`. +These flags also work with `whiteboard trace install`. ## Environment variables @@ -410,12 +414,12 @@ and telemetry administration: | Variable | Meaning, precedence, and default | | ------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| `TRACE_ENV_FILE` | Selects the legacy direct-bucket environment file. The default is `~/.config/dev-trace/env`. Setting either legacy file variable also makes `review trace setup` update the legacy files unless an S3 profile already exists. | -| `TRACE_SETTINGS_FILE` | Selects the legacy capture settings file. The default is `~/.config/dev-trace/settings.json`. Setting either legacy file variable also makes `review trace setup` update the legacy files unless an S3 profile already exists. | +| `TRACE_ENV_FILE` | Selects the legacy direct-bucket environment file. The default is `~/.config/dev-trace/env`. Setting either legacy file variable also makes `whiteboard trace setup` update the legacy files unless an S3 profile already exists. | +| `TRACE_SETTINGS_FILE` | Selects the legacy capture settings file. The default is `~/.config/dev-trace/settings.json`. Setting either legacy file variable also makes `whiteboard trace setup` update the legacy files unless an S3 profile already exists. | | `TRACE_HOME_DIR` | Replaces the operating-system home used to find the installed trace command and the trace repository registry under `.config/dev-trace`. The default is the operating-system home. | | `TRACE_OPENCODE_TRACES_ROOT` | Selects where Review writes fresh OpenCode session exports. The default is `$DEV_REVIEW_HOME/opencode-traces`. | -| `DEV_FAST_REVIEW_DESKTOP_STATE_ROOT` | Gives a launched Review Desktop instance separate `user-data` and `extensions` directories beneath this root. Empty or unset uses the normal Desktop state. | -| `DEV_FAST_REVIEW_DESKTOP_BACKGROUND` | Set to `1` by `review app` launches without `--focus`. Review Desktop then shows its first window without taking focus and ignores focus requests until you click it or run `review app launch --focus`. | +| `DEV_FAST_REVIEW_DESKTOP_STATE_ROOT` | Gives a launched Whiteboard Desktop instance separate `user-data` and `extensions` directories beneath this root. Empty or unset uses the normal Desktop state. | +| `DEV_FAST_REVIEW_DESKTOP_BACKGROUND` | Set to `1` by `whiteboard app` launches without `--focus`. Whiteboard Desktop then shows its first window without taking focus and ignores focus requests until you click it or run `whiteboard app launch --focus`. | | `PROGRESSIVE_REVIEW_TELEMETRY_INTERNAL` | `1` marks telemetry as internal and `0` marks it as external. Either value overrides the stored internal marker and workspace-checkout detection. | | `POSTHOG_KEY` | Legacy PostHog project key alias. The first non-empty value wins in this order: `PROGRESSIVE_REVIEW_POSTHOG_KEY`, `DEV_FAST_POSTHOG_KEY`, `POSTHOG_KEY`, then the embedded key. | | `POSTHOG_HOST` | Legacy PostHog host alias. The first non-empty value wins in this order: `PROGRESSIVE_REVIEW_POSTHOG_HOST`, `DEV_FAST_POSTHOG_HOST`, then `POSTHOG_HOST`. When none is set, the host defaults to `https://us.i.posthog.com`. | @@ -429,15 +433,21 @@ and telemetry administration: See [Telemetry and privacy](telemetry.md) for the complete telemetry controls and data policy. -## Agent integration and migration +## Agent connection and migration ```sh -review install [claude|claude-code|codex|cursor|all] +whiteboard connect [...] [--json] review migrate apply review migrate apply --force review version ``` -The app normally installs and updates agent skills. Use `review install` for a -headless environment. Migration is only for legacy Review state; use `--force` -only to restart an interrupted migration. +`whiteboard connect` prints the prompt that connects an agent to Review, the same +text Whiteboard Desktop copies. Paste it into a session of that agent. Agents are +`claude` (or `claude-code`), `codex`, `cursor`, `opencode`, `pi`, and `all`. +With no agent, it prints every prompt under a heading per agent. `--json` emits one +`connect` event whose `prompts` field maps each agent to its prompt. See +[Coding agents](agents.md#connect-an-agent). + +Migration is only for legacy Review state; use `--force` only to restart an +interrupted migration. diff --git a/docs/cli-releases.md b/docs/cli-releases.md index e15d7ea79..615dbc2ea 100644 --- a/docs/cli-releases.md +++ b/docs/cli-releases.md @@ -1,8 +1,8 @@ # Publishing the Review CLI The `Review CLI Release` workflow publishes the public `@dev.fast/review` npm -package. The package contains `review server`, `review share`, `review trace`, -and the agent skills. It runs independently of Desktop releases and requires +package. The package contains `whiteboard server`, `whiteboard share`, `whiteboard trace`, +and the agent instructions. It runs independently of Desktop releases and requires Node 24; building it does not install or launch Desktop. ## Release from GitHub @@ -34,7 +34,7 @@ are not supported by this workflow. Releases always use a commit already on does not silently publish newer source. Tags are the CLI release history. The workflow stamps the selected version -into the packed manifest, build metadata, and bundled skills without committing +into the packed manifest and build metadata without committing a version bump back to `main`. The source package version is a development baseline, not the latest published version. @@ -68,7 +68,7 @@ Before publishing, the workflow tests release planning, trace behavior, and headless authoring. It then builds one tarball, installs it in a temporary prefix outside the workspace, and checks that its installed CLI can install and remove agent hooks and commit a headless review without a display or Desktop. -The tarball includes the bundled product documentation and stamped skills. +The tarball includes the bundled product documentation. The tested tarball is the one passed to `npm publish`. The published `@dev.fast/review` package is the Node runtime shared by the CLI @@ -102,5 +102,5 @@ next version. An explicitly pushed tag is retried by rerunning its workflow. ```sh npm install --global @dev.fast/review@ -review server start +whiteboard server start ``` diff --git a/docs/how-review-works.md b/docs/how-review-works.md index 48105ac4f..3804d5229 100644 --- a/docs/how-review-works.md +++ b/docs/how-review-works.md @@ -5,14 +5,14 @@ Outline: Product model -> Review contents -> Targets -> Publication -> Lifecycle --> Review separates authoring from reading. A coding agent studies a change and -writes a guided document; Review Desktop gives the human reviewer live code, +writes a guided document; Whiteboard Desktop gives the human reviewer live code, and system views around that document. ```mermaid flowchart LR A[Branch, change, or PR] --> B[Agent authors a Review] B --> C[Server saves each edit through the Review API] - C --> D[Reviewer reads in Review Desktop] + C --> D[Reviewer reads in Whiteboard Desktop] ``` ## A Review is more than a diff @@ -43,13 +43,15 @@ See [review targets](cli-reference.md#review-targets) for the API options. ## Every edit saves immediately -Authoring goes through the JSON API: `review api`, the Review MCP tools, or -the dev-review skill. Every accepted edit is saved as soon as it is applied; -there is no publish, checkpoint, or render-report step. See -`packages/review/skills/dev-review/SKILL.md` -and `packages/review/src/review-api/README.md` +Authoring goes through the JSON API: `whiteboard api` or the Review MCP tools. +Every accepted edit is saved as soon as it is applied; there is no publish, +checkpoint, or render-report step. See `packages/review/src/review-api/README.md` for the full authoring workflow. +Agents connect to the MCP tools by registering `whiteboard mcp` themselves, from a +prompt that Review gives you; Review does not edit agent configuration. See +[Coding agents](agents.md#connect-an-agent). + The published document is `.bundle/document/review-document.json`, with format `review-document/1` and a version-2 manifest. Software-map bundles contain `head-map.json` and `base-map.json`, with format `software-map/1`. The server @@ -80,7 +82,7 @@ exact sealed artifacts at the current document and independent map pointers, including terminal Reviews. It never recompiles editable `review.mdx` or `data.ts`, or converts every private historical revision. Valid JSON artifacts and absent maps are preserved; drafts without a presentation only need a record -upgrade. Repeat reads need no further migration. `review migrate apply` runs +upgrade. Repeat reads need no further migration. `whiteboard migrate apply` runs the same per-review upgrade across the store and also performs repository-level cleanup. @@ -91,8 +93,8 @@ a reason to repair. If sealed conversion fails, the record, authoring inputs, candidates, and private refs stay unchanged. Home lists an attention entry: the review was published with the removed MDX toolchain and its stored files are damaged, so -it cannot be imported. Delete it from Home and recreate it with the Review -skill. Malformed or unsupported records remain explicit list errors. A +it cannot be imported. Delete it from Home and recreate it with your coding +agent. Malformed or unsupported records remain explicit list errors. A current-schema Review with broken artifacts shows the same guidance in its document or map load state. @@ -110,8 +112,8 @@ ${DEV_REVIEW_HOME:-~/.dev}/reviews// The directory contains the document, supporting TypeScript, pinned state, sealed revisions, and disposable build output. Review owns the infrastructure -files; agents author content through the JSON API (`review api`, the Review -MCP tools, or the dev-review skill), never by editing files in this directory +files; agents author content through the JSON API (`whiteboard api` or the Review +MCP tools), never by editing files in this directory directly. `review.json` uses store schema 5 and records the independent document and map @@ -124,8 +126,8 @@ that legacy code into the canvas. Software maps are stored per commit in Git notes under `refs/notes/dev-fast/*`. They do not add generated map files to the reviewed -branch. Map notes can be shared explicitly with `review map push` and -`review map fetch`. +branch. Map notes can be shared explicitly with `whiteboard map push` and +`whiteboard map fetch`. See the [CLI reference](cli-reference.md) for the lifecycle commands and the [privacy overview](privacy.md) for the local and network boundaries. diff --git a/docs/quickstart.md b/docs/quickstart.md index e07bf3aa1..0d3ab9862 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -18,15 +18,17 @@ This guide takes you from a fresh install to a completed first review. [Download the latest disk image](https://install.dev.fast), open it, and launch Review. The app updates itself after installation. -On first launch, Review opens the welcome screen. It detects supported coding -agents and offers to install: +On first launch, Review opens the welcome screen. Its first step connects your +coding agents: -- the `review` command in `~/.local/bin`; -- the `dev-review` skill for authoring reviews and software maps; and -- the Review MCP connection for Codex and Claude Code. +1. Choose **Install review in PATH**. +2. Choose **Copy prompt** next to an agent you use. +3. Paste the prompt into a session of that agent. The agent registers Review's + MCP server and confirms that it can reach Review. -Choose **Install** for the agents you use. Review keeps the app-managed command -and skills in sync after updates. +Repeat for each agent. You can copy the prompts again later from +**Settings → Agents**. See [Coding agents](agents.md#connect-an-agent) for +what the prompt does. ## 2. Take the tour @@ -44,13 +46,13 @@ Open the repository you want to review in your coding agent and tell it to review it! ```text -Use the dev-review skill to review my current branch against up to date main, -then open it in Review. +Create a Review of my current branch against up to date main, then open it +in Review. ``` The agent registers the repository, resolves the base and head pins, creates the review through the Review API, writes and validates the walkthrough, and -opens it in Review Desktop. You can also review a specific GitHub pull request +opens it in Whiteboard Desktop. You can also review a specific GitHub pull request or ask for an architecture review of a repository. ### Add Review guidance @@ -78,8 +80,8 @@ The app normally manages the CLI. These commands are useful for checking it: ```sh review version -review app launch -review info +whiteboard app launch +whiteboard info ``` If the command is missing or behaves like an older browser-based Review, see diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index fda432475..17bdb3e83 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -11,7 +11,7 @@ Start with these checks: command -v review review version review --help -review app launch --json +whiteboard app launch --json ``` The app-managed command should resolve through `~/.local/bin/review` and should @@ -19,16 +19,13 @@ offer the commands documented in the [CLI reference](cli-reference.md). ## `review: command not found` -Open Review Desktop, open the Command Palette, and run **Review: Install CLI in +Open Whiteboard Desktop, open the Command Palette, and run **Review: Install CLI in PATH**. Review refreshes `~/.local/bin/review` and removes the obsolete `/usr/local/bin/review` symlink if it exists. Then open a new terminal and check that `~/.local/bin` is on `PATH`. -For a headless setup with a separately installed CLI, run: - -```sh -review install all -``` +Agents reach Review through `~/.local/bin/review`. After you install it, +restart any agent that could not start Review's MCP server. ## The command opens a browser or shows old options @@ -41,37 +38,36 @@ review --help ``` Remove the legacy PATH entry or put `~/.local/bin` before it. A standalone -current CLI defers to the app's bundled copy while Review Desktop is running so +current CLI defers to the app's bundled copy while Whiteboard Desktop is running so the client and server stay on the same version. -## Review Desktop is not running +## Whiteboard Desktop is not running Start or activate it explicitly: ```sh -review app launch --json +whiteboard app launch --json ``` -`review info`, `review api`, and `review mcp` need a healthy Review Desktop +`whiteboard info`, `whiteboard api`, and `whiteboard mcp` need a healthy Whiteboard Desktop server. If launch reports success but those commands still cannot connect, quit all Review windows, reopen the app, and retry the launch command. ## No Review appears for the checkout -Run `review info` from the source repository. An empty `reviews` list means the +Run `whiteboard info` from the source repository. An empty `reviews` list means the current worktree has no matching active Review. -Create one with the dev-review skill, which registers the repository, resolves -pins, and calls `review_create` (or the equivalent `review api` command). See -`packages/review/skills/dev-review/SKILL.md`. -Use `review info --all` to inspect active Reviews across every worktree in the +Ask your coding agent to create one; it registers the repository, resolves +pins, and calls `session_create` (or the equivalent `whiteboard api` command). +Use `whiteboard info --all` to inspect active Reviews across every worktree in the repository. ## A Review is out of sync The bound branch, bookmark, change, or pull request moved after the Review was -created. Start a fresh version at updated pins with `review_repin` (or the -equivalent `review api` command); examine the diff before carrying content +created. Start a fresh version at updated pins with `session_repin` (or the +equivalent `whiteboard api` command); examine the diff before carrying content over. ## The Map tab is missing or stale @@ -82,21 +78,28 @@ Ask the authoring agent to finish the map, or inspect the current state with: review map check --review ``` -Run `review map --help` before editing map scratch state manually. +Run `whiteboard map --help` before editing map scratch state manually. -## A coding agent is not detected +## A coding agent does not see Review's tools -Make sure the agent's CLI or app is installed, then reopen Review's welcome -screen. You can install a target explicitly even when automatic detection is -unavailable: +Copy the prompt for that agent again from **Settings → Agents**, or print it +with `whiteboard connect `, and paste it into a new session of the agent. +The prompt replaces an existing `review` entry. Then restart the agent or +reload its MCP servers. -```sh -review install codex -review install claude -review install cursor -``` +Check that `~/.local/bin/review` exists. The prompt runs Review through that +path, because agents started from an app do not see your shell `PATH`. + +Codex also needs the Review line in `~/.codex/AGENTS.md`, which its prompt +adds. Pi has no MCP support and uses the `review` skill instead. + +See [Coding agents](agents.md#connect-an-agent) for what each prompt sets up. + +## Old Review skills are still installed -See [Coding agents](agents.md) for the installed locations and prompts. +Earlier versions of Review installed skills into agent configuration. Open +**Settings → Agents** and choose **Remove old Review skills**. It deletes only +skills that Review installed and reports anything it left. ## An update failed @@ -138,7 +141,7 @@ join the [dev.fast Discord](https://discord.gg/wYvd2cpMQg). ## Structural diffs say diffr cannot be found -Review Desktop ships its own `diffr` at `bin/diffr` inside its runtime and +Whiteboard Desktop ships its own `diffr` at `bin/diffr` inside its runtime and uses it unless `REVIEW_DIFFR_BINARY` names another executable. If the message names a path under the app, the install is damaged; reinstall Review. In a source checkout, run diff --git a/package.json b/package.json index 48411b106..ab12eab4c 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,8 @@ "ci": "pnpm --filter @dev.fast/review-desktop app:build && pnpm --filter @dev.fast/review check:tutorial && pnpm lint && pnpm format:check && pnpm typecheck && pnpm test", "test": "node --test scripts/*.test.mjs && pnpm -r --if-present test", "typecheck": "pnpm -r --workspace-concurrency=1 --if-present typecheck", - "build": "pnpm -r --if-present build" + "build": "pnpm -r --if-present build", + "whiteboard": "pnpm --filter @dev.fast/review whiteboard" }, "devDependencies": { "@oxlint/plugins": "1.63.0", diff --git a/packages/agent-plugins/claude/.claude-plugin/plugin.json b/packages/agent-plugins/claude/.claude-plugin/plugin.json new file mode 100644 index 000000000..e7de90458 --- /dev/null +++ b/packages/agent-plugins/claude/.claude-plugin/plugin.json @@ -0,0 +1,6 @@ +{ + "name": "whiteboard", + "version": "0.1.0", + "description": "Connect Claude Code to dev.fast Whiteboard", + "author": { "name": "dev.fast" } +} diff --git a/packages/agent-plugins/claude/.mcp.json b/packages/agent-plugins/claude/.mcp.json new file mode 100644 index 000000000..642b2efa3 --- /dev/null +++ b/packages/agent-plugins/claude/.mcp.json @@ -0,0 +1,8 @@ +{ + "mcpServers": { + "whiteboard": { + "command": "sh", + "args": ["-c", "exec \"$HOME/.local/bin/whiteboard\" mcp"] + } + } +} diff --git a/packages/agent-plugins/codex/.codex-plugin/plugin.json b/packages/agent-plugins/codex/.codex-plugin/plugin.json new file mode 100644 index 000000000..dade99c96 --- /dev/null +++ b/packages/agent-plugins/codex/.codex-plugin/plugin.json @@ -0,0 +1,5 @@ +{ + "name": "whiteboard", + "version": "0.1.0", + "description": "Connect Codex to dev.fast Whiteboard" +} diff --git a/packages/agent-plugins/codex/.mcp.json b/packages/agent-plugins/codex/.mcp.json new file mode 100644 index 000000000..642b2efa3 --- /dev/null +++ b/packages/agent-plugins/codex/.mcp.json @@ -0,0 +1,8 @@ +{ + "mcpServers": { + "whiteboard": { + "command": "sh", + "args": ["-c", "exec \"$HOME/.local/bin/whiteboard\" mcp"] + } + } +} diff --git a/packages/agent-plugins/codex/skills/whiteboard/SKILL.md b/packages/agent-plugins/codex/skills/whiteboard/SKILL.md new file mode 100644 index 000000000..6ebd76af7 --- /dev/null +++ b/packages/agent-plugins/codex/skills/whiteboard/SKILL.md @@ -0,0 +1,8 @@ +--- +name: whiteboard +description: "Explain code in Whiteboard, the architecture-visualization tool: author Whiteboards of branches, changes and pull requests, draw on the Whiteboard scratchpad, or research why code exists from past agent sessions." +--- + +# dev.fast Whiteboard + +Whiteboard serves its own instructions over the `whiteboard` MCP server. Before authoring, call `session_get_instructions` and follow it. Pass `{"topic":"scratchpad"}` to explain code visually, or `{"topic":"trace-archaeology"}` to research why code exists. If the tools are missing, reload the `whiteboard` MCP server or ask the user to start Whiteboard Desktop. diff --git a/packages/agent-plugins/cursor/.cursor-plugin/plugin.json b/packages/agent-plugins/cursor/.cursor-plugin/plugin.json new file mode 100644 index 000000000..103289786 --- /dev/null +++ b/packages/agent-plugins/cursor/.cursor-plugin/plugin.json @@ -0,0 +1,5 @@ +{ + "name": "whiteboard", + "version": "0.1.0", + "description": "Connect Cursor to dev.fast Whiteboard" +} diff --git a/packages/agent-plugins/cursor/mcp.json b/packages/agent-plugins/cursor/mcp.json new file mode 100644 index 000000000..642b2efa3 --- /dev/null +++ b/packages/agent-plugins/cursor/mcp.json @@ -0,0 +1,8 @@ +{ + "mcpServers": { + "whiteboard": { + "command": "sh", + "args": ["-c", "exec \"$HOME/.local/bin/whiteboard\" mcp"] + } + } +} diff --git a/packages/agent-plugins/opencode/LICENSE b/packages/agent-plugins/opencode/LICENSE new file mode 100644 index 000000000..5a204b27e --- /dev/null +++ b/packages/agent-plugins/opencode/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 dev.fast + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/agent-plugins/opencode/index.d.ts b/packages/agent-plugins/opencode/index.d.ts new file mode 100644 index 000000000..02be22fc4 --- /dev/null +++ b/packages/agent-plugins/opencode/index.d.ts @@ -0,0 +1,5 @@ +import type { Hooks } from "@opencode-ai/plugin"; + +export default function whiteboardPlugin(): Promise< + Required> +>; diff --git a/packages/agent-plugins/opencode/index.js b/packages/agent-plugins/opencode/index.js new file mode 100644 index 000000000..51950ff9d --- /dev/null +++ b/packages/agent-plugins/opencode/index.js @@ -0,0 +1,15 @@ +/** Registers dev.fast Whiteboard's MCP server. Requires Whiteboard Desktop with the whiteboard command installed. */ +export default async function whiteboardPlugin() { + return { + config: async (config) => { + config.mcp = { + ...config.mcp, + whiteboard: { + type: "local", + command: ["sh", "-c", 'exec "$HOME/.local/bin/whiteboard" mcp'], + enabled: true, + }, + }; + }, + }; +} diff --git a/packages/agent-plugins/opencode/package.json b/packages/agent-plugins/opencode/package.json new file mode 100644 index 000000000..a7b7d692d --- /dev/null +++ b/packages/agent-plugins/opencode/package.json @@ -0,0 +1,28 @@ +{ + "name": "@dev.fast/opencode-whiteboard", + "version": "0.1.0", + "description": "Connect OpenCode to dev.fast Whiteboard", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/devdotfast/review.git", + "directory": "packages/agent-plugins/opencode" + }, + "files": [ + "LICENSE", + "index.js", + "index.d.ts" + ], + "type": "module", + "main": "index.js", + "types": "index.d.ts", + "publishConfig": { + "access": "public" + }, + "devDependencies": { + "@opencode-ai/plugin": "1.18.31" + }, + "peerDependencies": { + "@opencode-ai/plugin": "*" + } +} diff --git a/packages/agent-plugins/pi/LICENSE b/packages/agent-plugins/pi/LICENSE new file mode 100644 index 000000000..5a204b27e --- /dev/null +++ b/packages/agent-plugins/pi/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 dev.fast + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/agent-plugins/pi/package.json b/packages/agent-plugins/pi/package.json new file mode 100644 index 000000000..a77572a9a --- /dev/null +++ b/packages/agent-plugins/pi/package.json @@ -0,0 +1,26 @@ +{ + "name": "@dev.fast/pi-whiteboard", + "version": "0.1.0", + "description": "Connect Pi to dev.fast Whiteboard", + "keywords": [ + "pi-package" + ], + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/devdotfast/review.git", + "directory": "packages/agent-plugins/pi" + }, + "files": [ + "LICENSE", + "skills" + ], + "publishConfig": { + "access": "public" + }, + "pi": { + "skills": [ + "./skills" + ] + } +} diff --git a/packages/agent-plugins/pi/skills/whiteboard/SKILL.md b/packages/agent-plugins/pi/skills/whiteboard/SKILL.md new file mode 100644 index 000000000..94b4525f3 --- /dev/null +++ b/packages/agent-plugins/pi/skills/whiteboard/SKILL.md @@ -0,0 +1,8 @@ +--- +name: whiteboard +description: "Explain code in Whiteboard, the architecture-visualization tool: author Whiteboards of branches, changes and pull requests, draw on the Whiteboard scratchpad, or research why code exists from past agent sessions." +--- + +# dev.fast Whiteboard + +Whiteboard serves its own instructions. Before authoring, run `"$HOME/.local/bin/whiteboard" api session_get_instructions '{}'` and follow the result. Pass `'{"topic":"scratchpad"}'` to explain code visually, or `'{"topic":"trace-archaeology"}'` to research why code exists. If Whiteboard is not running, the response says how to start it. diff --git a/packages/review-protocol/src/contracts.test.ts b/packages/review-protocol/src/contracts.test.ts index 3771e9ec7..44b36c883 100644 --- a/packages/review-protocol/src/contracts.test.ts +++ b/packages/review-protocol/src/contracts.test.ts @@ -6,6 +6,7 @@ import { REVIEW_DESKTOP_DISCOVERY_VERSION, REVIEW_SCHEMA_VERSION, ReviewCliInstallStampSchema, + ReviewCliInstallStatusSchema, ReviewDesktopDiscoverySchema, ReviewDesktopStateSchema, ReviewDesktopVerbFrameSchema, @@ -220,8 +221,9 @@ describe("Review protocol Zod contracts", () => { }); // Desktop discovery deliberately ignores unknown keys so future additive - // fields never force another protocol version bump. - const tolerantContracts = new Set(["desktop discovery"]); + // fields never force another protocol version bump. The install stamp drops + // the agent records that stamps from before version 2 carry. + const tolerantContracts = new Set(["desktop discovery", "CLI install stamp"]); it.each(contracts)("rejects unknown keys in %s", (name, schema, value) => { expect(schema.safeParse({ ...value, unexpected: true }).success).toBe( @@ -269,3 +271,59 @@ describe("summarizeReviewDiffFiles", () => { ).toEqual({ fileCount: 2, additions: 4, deletions: 3 }); }); }); + +describe("ReviewCliInstallStampSchema", () => { + it("parses a legacy stamp and drops its agent records", () => { + const stamp = ReviewCliInstallStampSchema.parse({ + consent: "granted", + fingerprint: "abc", + targets: ["claude", "codex"], + shimPath: "/home/u/.local/bin/review", + mcpRegistrations: [ + { + target: "claude", + configPath: "/x", + command: "/y", + args: ["mcp"], + env: {}, + }, + ], + fffRegistrations: [{ target: "claude", command: "claude", args: [] }], + updatedAt: "2026-01-01T00:00:00.000Z", + }); + + expect(stamp).toEqual({ + consent: "granted", + fingerprint: "abc", + shimPath: "/home/u/.local/bin/review", + updatedAt: "2026-01-01T00:00:00.000Z", + }); + }); +}); + +describe("ReviewCliInstallStatusSchema", () => { + it("requires connect prompts and the legacy skill list", () => { + const result = ReviewCliInstallStatusSchema.safeParse({ + fingerprint: "f", + stamp: null, + stale: false, + updateNeeded: false, + shim: { + path: "/p", + installed: false, + profileConfigured: false, + onPath: false, + }, + trace: { + enabled: false, + configured: false, + autoActivateRepositories: false, + envPath: "/e", + settingsPath: "/s", + }, + cli: null, + }); + + expect(result.success).toBe(false); + }); +}); diff --git a/packages/review-protocol/src/contracts.ts b/packages/review-protocol/src/contracts.ts index dfc4ba34e..96e4a7a32 100644 --- a/packages/review-protocol/src/contracts.ts +++ b/packages/review-protocol/src/contracts.ts @@ -332,16 +332,14 @@ export interface ReviewCanvasSetupActions { } /** - * Install state and actions the workbench hands to the Home canvas. `apply`, - * `skip`, and `enablePrompts` resolve with the refreshed status so the card can - * re-render without a full canvas update. + * Install state and actions the workbench hands to the Home canvas. Every + * action resolves with the refreshed status so the card can re-render without + * a full canvas update. */ export interface ReviewCanvasInstallContent { status: ReviewCliInstallStatus; apply(request: { - targets: readonly ReviewCliInstallTarget[]; shim?: boolean; - fff?: boolean; trace?: | true | { @@ -353,26 +351,16 @@ export interface ReviewCanvasInstallContent { }; }): Promise; remove(request: { - targets: readonly ReviewCliInstallTarget[]; shim?: boolean; - fff?: boolean; trace?: true; }): Promise; + removeLegacySkills(): Promise; + finishUpdate(): Promise; decline(): Promise; skip(): Promise; enablePrompts(): Promise; } -/** - * Install status handed to the Home canvas so it can show a one-line setup - * banner when the install needs attention. `open` navigates to the Agent - * Setup page. - */ -export interface ReviewCanvasHomeSetup { - status: ReviewCliInstallStatus; - open(): void; -} - /** * Onboarding progress for the Welcome pane: which of the three steps the user * finished. `tutorialChecked` counts the checklist steps the tutorial @@ -507,10 +495,10 @@ export interface ReviewCanvasSettingsContent { setSoftwareMapEnabled(enabled: boolean): Promise; structuralDiffEnabled: boolean; setStructuralDiffEnabled(enabled: boolean): Promise; - // Not a workbench setting: the review server and `review install` both - // read it, so it lives in the server preferences file. Off by default. - // Turning it on makes the pad and installs its skill for set-up agents; - // turning it off hides the pad and removes the skill. + // Not a workbench setting: the review server reads it, so it lives in the + // server preferences file. Off by default. Turning it on shows the pad and + // tells connected agents over MCP that they can draw on it; turning it off + // hides the pad. scratchpadEnabled: boolean; setScratchpadEnabled(enabled: boolean): Promise; // Shared CLI configuration, read when its disclosure opens. @@ -663,8 +651,6 @@ export type ReviewCanvasContent = // Opens the review and pins its read-only source tree open. Absent when // the host cannot show the tree. openSourceTree?(uuid: string): void; - // Absent when the install status endpoint is unavailable. - setup?: ReviewCanvasHomeSetup; // With no reviews, Home renders the Welcome rail instead of a zero // state of its own, so it needs what Welcome needs. Both absent when // the install status endpoint is unavailable. @@ -878,100 +864,36 @@ export type ReviewCliInstallTarget = z.infer< typeof ReviewCliInstallTargetSchema >; -export const ReviewFffInstallTargetSchema = z.enum(["claude", "codex", "pi"], { - error: "must be claude, codex, or pi", -}); - -export type ReviewFffInstallTarget = z.infer< - typeof ReviewFffInstallTargetSchema ->; - -export const ReviewFffManagedRegistrationSchema = z.strictObject({ - target: ReviewFffInstallTargetSchema, - command: requiredString, - args: z.array(requiredString), -}); - -export type ReviewFffManagedRegistration = z.infer< - typeof ReviewFffManagedRegistrationSchema ->; - -export const ReviewMcpRegistrationSchema = z.strictObject({ - target: z.enum(["codex", "claude", "cursor", "opencode"]), - configPath: requiredString, - command: requiredString, - args: z.array(z.string()), - env: z.record(z.string(), z.string()), -}); - -export type ReviewMcpRegistration = z.infer; - -export const ReviewCliInstallStampSchema = z.strictObject({ +export const ReviewCliInstallStampSchema = z.object({ consent: z.enum(["granted", "declined", "skipped"], { error: "must be granted, declined, or skipped", }), fingerprint: requiredString.optional(), - targets: z.array(ReviewCliInstallTargetSchema).optional(), shimPath: requiredString.optional(), - fffRegistrations: z.array(ReviewFffManagedRegistrationSchema).optional(), - mcpRegistrations: z.array(ReviewMcpRegistrationSchema).optional(), + /** The user removed the review command; the shim resync must not reinstall it. */ + commandDisabled: z.literal(true).optional(), traceManaged: z.boolean().optional(), updatedAt: requiredString, }); +// z.object (not strictObject) so stamps from earlier versions parse; their +// extra fields (targets, fffRegistrations, mcpRegistrations) are dropped. export type ReviewCliInstallStamp = z.infer; export const ReviewCliInstallStatusSchema = z.strictObject({ - agents: z.array( - z.strictObject({ - target: ReviewCliInstallTargetSchema, - present: z.boolean(), - installed: z.boolean(), - }), - ), fingerprint: requiredString, stamp: ReviewCliInstallStampSchema.nullable(), + /** The stamp fingerprint differs from the running package: rewrite the shim. */ stale: z.boolean(), - skills: z - .array( - z.strictObject({ - target: ReviewCliInstallTargetSchema, - name: requiredString, - installedVersion: requiredString.nullable(), - bundledVersion: requiredString.nullable(), - stale: z.boolean(), - error: requiredString.optional(), - }), - ) - .optional(), + /** A granted stamp without this build's update marker: show the update screen. */ + updateNeeded: z.boolean(), error: requiredString.optional(), - mcp: z - .array( - z.strictObject({ - target: ReviewMcpRegistrationSchema.shape.target, - state: z.enum(["ready", "missing", "custom", "error"]), - error: requiredString.optional(), - }), - ) - .optional(), shim: z.strictObject({ path: requiredString, installed: z.boolean(), profileConfigured: z.boolean(), onPath: z.boolean(), }), - fff: z.strictObject({ - serverName: z.literal("fff"), - corpusRoot: requiredString, - binary: z.strictObject({ path: requiredString, installed: z.boolean() }), - registrations: z.array( - z.strictObject({ - target: ReviewFffInstallTargetSchema, - present: z.boolean(), - managed: z.boolean(), - }), - ), - }), trace: z.strictObject({ enabled: z.boolean(), configured: z.boolean(), @@ -996,43 +918,47 @@ export const ReviewCliInstallStatusSchema = z.strictObject({ cli: z .strictObject({ path: requiredString, version: requiredString }) .nullable(), + connect: z.strictObject({ + // "sh", or "review" when Desktop has no built CLI. + command: requiredString, + // ["-c", "exec \"$HOME/.local/bin/review\" mcp"], or ["mcp"]. + args: z.array(z.string()), + prompts: z.record(ReviewCliInstallTargetSchema, requiredString), + // The published plugin per harness: an install command, or Cursor's link. + plugins: z.record( + ReviewCliInstallTargetSchema, + z.strictObject({ + label: requiredString, + command: requiredString.optional(), + url: requiredString.optional(), + }), + ), + }), + legacySkills: z.array(z.strictObject({ path: requiredString })), }); export type ReviewCliInstallStatus = z.infer< typeof ReviewCliInstallStatusSchema >; -// Skills and FFF integrations are per-agent. Skill requests install the review -// command by default. The command, FFF binary, and trace configuration are -// per-machine. Silent app updates omit `fff` and `trace`, so they do not run an -// FFF installer or contact R2. -export const ReviewCliInstallApplyRequestSchema = z - .strictObject({ - targets: z.array(ReviewCliInstallTargetSchema), - shim: z.boolean().optional(), - autoUpdate: z.boolean().optional(), - fff: z.boolean().optional(), - trace: z - .union([ - z.literal(true), - z.strictObject({ - endpoint: requiredString.optional(), - bucket: requiredString.optional(), - key: requiredString.optional(), - secret: requiredString.optional(), - region: requiredString.optional(), - }), - ]) - .optional(), - }) - .refine( - (request) => - request.targets.length > 0 || - request.shim === true || - request.fff === true || - request.trace !== undefined, - { message: "must install skills, the command, FFF, or trace capture" }, - ); +// The command and trace configuration are per-machine. Silent app updates +// omit `trace`, so they do not contact R2. +export const ReviewCliInstallApplyRequestSchema = z.strictObject({ + shim: z.boolean().optional(), + autoUpdate: z.boolean().optional(), + trace: z + .union([ + z.literal(true), + z.strictObject({ + endpoint: requiredString.optional(), + bucket: requiredString.optional(), + key: requiredString.optional(), + secret: requiredString.optional(), + region: requiredString.optional(), + }), + ]) + .optional(), +}); export type ReviewCliInstallApplyRequest = z.infer< typeof ReviewCliInstallApplyRequestSchema diff --git a/packages/review-protocol/src/index.ts b/packages/review-protocol/src/index.ts index a1298fcc6..0d171294f 100644 --- a/packages/review-protocol/src/index.ts +++ b/packages/review-protocol/src/index.ts @@ -26,7 +26,6 @@ import { ReviewCliInstallApplyResponseSchema, type ReviewCliInstallStatus, ReviewCliInstallStatusSchema, - type ReviewCliInstallTarget, type ReviewDesktopDiscovery, ReviewDesktopDiscoverySchema, type ReviewDesktopVerbFrame, @@ -70,32 +69,6 @@ export { sessionMetaSchema, } from "@dev.fast/trace-protocol"; -export interface ReviewCliInstallResyncRequest { - readonly targets: readonly ReviewCliInstallTarget[]; - readonly shim: boolean; - readonly autoUpdate: true; -} - -/** Returns the previously consented install scope when a stale install needs to be reapplied. */ -export function reviewCliInstallResyncRequest( - status: ReviewCliInstallStatus, -): ReviewCliInstallResyncRequest | undefined { - if (status.stamp?.consent !== "granted") return undefined; - - const targets = - status.stamp.targets !== undefined - ? status.stamp.targets - : status.agents.flatMap((agent) => - agent.installed ? [agent.target] : [], - ); - - const shim = Boolean(status.stamp.shimPath); - - return targets.length > 0 || shim - ? { targets, shim, autoUpdate: true } - : undefined; -} - export function parseReviewDesktopDiscovery( value: JsonValue, ): ReviewDesktopDiscovery { diff --git a/packages/review/app/src/agent-setup-card.browser.test.tsx b/packages/review/app/src/agent-setup-card.browser.test.tsx deleted file mode 100644 index ac519f920..000000000 --- a/packages/review/app/src/agent-setup-card.browser.test.tsx +++ /dev/null @@ -1,101 +0,0 @@ -import type { - ReviewCanvasInstallContent, - ReviewCliInstallStatus, -} from "@dev.fast/review-protocol"; -import { act } from "react"; -import { type Root, createRoot } from "react-dom/client"; -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; - -import { AgentSetupCard } from "./agent-setup-card"; - -describe("AgentSetupCard", () => { - let container: HTMLDivElement; - let root: Root; - - beforeEach(() => { - container = document.createElement("div"); - document.body.append(container); - root = createRoot(container); - }); - - afterEach(async () => { - await act(async () => root.unmount()); - container.remove(); - vi.restoreAllMocks(); - }); - - it("reports the refreshed status after a per-agent install", async () => { - const onStatusChange = vi.fn<(status: ReviewCliInstallStatus) => void>(); - - const apply = vi.fn( - async () => grantedStatus, - ); - - const install: ReviewCanvasInstallContent = { - // A granted stamp: not first run, so only the per-agent rows install. - status: { ...status, stamp: grantedStatus.stamp }, - apply, - remove: vi.fn(), - decline: vi.fn(), - skip: vi.fn(), - enablePrompts: vi.fn(), - }; - - await act(async () => - root.render( - , - ), - ); - await act(async () => { - const buttons = [ - ...container.querySelectorAll( - ".review-agent-setup-agents button", - ), - ]; - - buttons.find((button) => button.textContent === "Install")?.click(); - }); - - expect(apply).toHaveBeenCalledExactlyOnceWith({ - targets: ["codex"], - }); - expect(onStatusChange).toHaveBeenCalledExactlyOnceWith(grantedStatus); - }); -}); - -const status: ReviewCliInstallStatus = { - agents: [{ target: "codex", present: true, installed: false }], - fingerprint: "fingerprint", - stamp: null, - stale: false, - shim: { - path: "/tmp/review", - installed: false, - profileConfigured: false, - onPath: false, - }, - fff: { - serverName: "fff", - corpusRoot: "/tmp/trace-search", - binary: { path: "/tmp/fff-mcp", installed: false }, - registrations: [{ target: "codex", present: false, managed: false }], - }, - trace: { - enabled: false, - configured: false, - autoActivateRepositories: false, - envPath: "/tmp/trace-env", - settingsPath: "/tmp/trace-settings.json", - }, - cli: { path: "/tmp/cli.js", version: "0.0.1" }, -}; - -const grantedStatus: ReviewCliInstallStatus = { - ...status, - agents: [{ target: "codex", present: true, installed: true }], - stamp: { - consent: "granted", - updatedAt: "2026-08-09T00:00:00.000Z", - targets: ["codex"], - }, -}; diff --git a/packages/review/app/src/agent-setup-card.tsx b/packages/review/app/src/agent-setup-card.tsx deleted file mode 100644 index 6d98318e0..000000000 --- a/packages/review/app/src/agent-setup-card.tsx +++ /dev/null @@ -1,179 +0,0 @@ -import type { - ReviewCanvasInstallContent, - ReviewCliInstallStatus, - ReviewCliInstallTarget, -} from "@dev.fast/review-protocol"; -import { useEffect, useState } from "react"; - -import { AGENT_LOGOS } from "./agent-logos"; - -export const TARGET_LABELS: Record = { - claude: "Claude Code", - codex: "Codex", - cursor: "Cursor", - opencode: "OpenCode", - pi: "Pi", -}; - -type InstallRequest = Pick< - Parameters[0], - "targets" | "fff" ->; - -/** - * Lets the reviewer install or reinstall skills per agent. The card keeps the - * latest action result so its parent can advance without a host re-render. - */ -export function AgentSetupCard({ - install, - onStatusChange, -}: { - install: ReviewCanvasInstallContent; - onStatusChange?: (status: ReviewCliInstallStatus) => void; -}) { - const [status, setStatus] = useState(install.status); - const [busy, setBusy] = useState(null); - const [error, setError] = useState(null); - - useEffect(() => setStatus(install.status), [install.status]); - - const run = async ( - key: string, - action: () => Promise, - ) => { - setBusy(key); - setError(null); - - try { - const next = await action(); - setStatus(next); - onStatusChange?.(next); - } catch (cause) { - setError(cause instanceof Error ? cause.message : String(cause)); - } finally { - setBusy(null); - } - }; - - return ( -
-
    - {status.agents.map((agent) => { - const Logo = AGENT_LOGOS[agent.target]; - - const skills = - status.skills?.filter((skill) => skill.target === agent.target) ?? - []; - - const mcp = status.mcp?.find((item) => item.target === agent.target); - - const needsUpdate = - skills.some((skill) => skill.stale) || - (agent.installed && mcp?.state === "missing"); - - const versions = skills - .map( - (skill) => - `${skill.name}: installed ${skill.installedVersion ?? "unversioned"}; bundled ${skill.bundledVersion ?? "unversioned"}`, - ) - .join("\n"); - - const request: InstallRequest = { targets: [agent.target] }; - - if (status.trace.enabled && supportsFff(agent.target)) { - request.fff = true; - } - - return ( -
  • - - - - - {TARGET_LABELS[agent.target]} - - - {mcp?.state === "error" - ? "setup error" - : mcp?.state === "custom" - ? "custom MCP" - : needsUpdate - ? "update needed" - : agent.installed - ? "installed" - : agent.present - ? "detected" - : "not detected"} - - {agent.installed ? ( - - ) : null} - -
  • - ); - })} -
-

- Installs Review skills, the review command, and MCP tools - where supported. -

- {error || status.error ? ( -

{error ?? status.error}

- ) : null} - {status.skills - ?.filter((skill) => skill.error) - .map((skill) => ( -

- {skill.error} -

- ))} - {status.mcp - ?.filter((item) => item.state === "error" || item.state === "custom") - .map((item) => ( -

- {item.error ?? - `${TARGET_LABELS[item.target]} already has custom Review MCP settings. Remove that entry in your agent's settings, then reinstall here to let Review manage it.`} -

- ))} -
- ); -} - -export function supportsFff(target: ReviewCliInstallTarget): boolean { - return target === "claude" || target === "codex" || target === "pi"; -} diff --git a/packages/review/app/src/connect-card.browser.test.tsx b/packages/review/app/src/connect-card.browser.test.tsx new file mode 100644 index 000000000..3620cd3e9 --- /dev/null +++ b/packages/review/app/src/connect-card.browser.test.tsx @@ -0,0 +1,342 @@ +import type { + ReviewCanvasInstallContent, + ReviewCliInstallStatus, +} from "@dev.fast/review-protocol"; +import { type ReactNode, act } from "react"; +import { type Root, createRoot } from "react-dom/client"; +import { afterEach, describe, expect, it, vi } from "vitest"; + +import { + ConnectCard, + LegacySkillsRow, + REVIEW_CONNECT_TARGET_STORAGE_KEY, +} from "./connect-card"; + +const status: ReviewCliInstallStatus = { + fingerprint: "f", + stamp: null, + stale: false, + updateNeeded: false, + shim: { + path: "/tmp/review", + installed: true, + profileConfigured: true, + onPath: true, + }, + trace: { + enabled: false, + configured: false, + autoActivateRepositories: false, + envPath: "/e", + settingsPath: "/s", + }, + cli: { path: "/tmp/cli.js", version: "0.0.1" }, + connect: { + command: "sh", + args: ["-c", 'exec "$HOME/.local/bin/review" mcp'], + prompts: { + claude: "CLAUDE PROMPT", + codex: "CODEX PROMPT", + cursor: "CURSOR PROMPT", + opencode: "OPENCODE PROMPT", + pi: "PI PROMPT", + }, + plugins: { + claude: { + label: "Install the Claude Code plugin", + command: "CLAUDE COMMAND", + }, + codex: { label: "Install the Codex plugin", command: "CODEX COMMAND" }, + cursor: { + label: "Install in Cursor", + url: "cursor://anysphere.cursor-deeplink/mcp/install?name=review", + }, + opencode: { + label: "Install the OpenCode plugin", + command: "OPENCODE COMMAND", + }, + pi: { label: "Install the Pi package", command: "PI COMMAND" }, + }, + }, + legacySkills: [], +}; + +function content( + overrides: Partial = {}, +): ReviewCanvasInstallContent { + const s = { ...status, ...overrides }; + + const same = async () => s; + + return { + status: s, + apply: vi.fn(same), + remove: vi.fn(same), + removeLegacySkills: vi.fn( + async () => ({ ...s, legacySkills: [] }), + ), + finishUpdate: vi.fn(same), + decline: vi.fn(same), + skip: vi.fn(same), + enablePrompts: vi.fn(same), + }; +} + +const mounted: { root: Root; container: HTMLDivElement }[] = []; + +async function mount(node: ReactNode): Promise { + const container = document.createElement("div"); + document.body.append(container); + + const root = createRoot(container); + mounted.push({ root, container }); + await act(async () => root.render(node)); + + return container; +} + +afterEach(async () => { + localStorage.removeItem(REVIEW_CONNECT_TARGET_STORAGE_KEY); + + for (const { root, container } of mounted.splice(0)) { + await act(async () => root.unmount()); + container.remove(); + } +}); + +function button(container: HTMLElement, label: string) { + return [...container.querySelectorAll("button")].find( + (b) => b.textContent === label, + ); +} + +function body(container: HTMLElement) { + return container.querySelector(".review-home-prompt-body")?.textContent; +} + +function copyButton(container: HTMLElement) { + return container.querySelector(".review-home-prompt-copy"); +} + +describe("ConnectCard", () => { + it("shows the Claude Code prompt first and switches harness on click", async () => { + const container = await mount(); + + expect(button(container, "Claude Code")?.getAttribute("aria-pressed")).toBe( + "true", + ); + expect(body(container)).toBe("CLAUDE PROMPT"); + + await act(async () => button(container, "Codex")?.click()); + expect(button(container, "Codex")?.getAttribute("aria-pressed")).toBe( + "true", + ); + expect(button(container, "Claude Code")?.getAttribute("aria-pressed")).toBe( + "false", + ); + expect(body(container)).toBe("CODEX PROMPT"); + expect(localStorage.getItem(REVIEW_CONNECT_TARGET_STORAGE_KEY)).toBe( + "codex", + ); + }); + + it("preselects the stored harness", async () => { + localStorage.setItem(REVIEW_CONNECT_TARGET_STORAGE_KEY, "pi"); + + const container = await mount(); + + expect(button(container, "Pi")?.getAttribute("aria-pressed")).toBe("true"); + expect(body(container)).toBe("PI PROMPT"); + }); + + it("copies whichever text is shown", async () => { + const writeText = vi + .spyOn(navigator.clipboard, "writeText") + .mockResolvedValue(); + + const container = await mount(); + + await act(async () => button(container, "Codex")?.click()); + expect(copyButton(container)?.getAttribute("aria-label")).toBe( + "Copy prompt for Codex", + ); + await act(async () => copyButton(container)?.click()); + expect(writeText).toHaveBeenLastCalledWith("CODEX PROMPT"); + expect(copyButton(container)?.textContent).toBe("Copied"); + + await act(async () => button(container, "Install the plugin")?.click()); + expect(body(container)).toBe("CODEX COMMAND"); + expect(copyButton(container)?.textContent).toBe("Copy command"); + expect(copyButton(container)?.getAttribute("aria-label")).toBe( + "Copy install command for Codex", + ); + await act(async () => copyButton(container)?.click()); + expect(writeText).toHaveBeenLastCalledWith("CODEX COMMAND"); + expect(copyButton(container)?.textContent).toBe("Copied"); + writeText.mockRestore(); + }); + + it("reports a successful copy", async () => { + const writeText = vi + .spyOn(navigator.clipboard, "writeText") + .mockResolvedValue(); + + const onCopied = vi.fn<() => void>(); + + const container = await mount( + , + ); + + await act(async () => copyButton(container)?.click()); + expect(onCopied).toHaveBeenCalledOnce(); + writeText.mockRestore(); + }); + + it("links to Cursor's installer, or asks for the review command without one", async () => { + localStorage.setItem(REVIEW_CONNECT_TARGET_STORAGE_KEY, "cursor"); + + const container = await mount(); + + await act(async () => button(container, "Install the plugin")?.click()); + + const link = container.querySelector("a"); + + expect(link?.textContent).toBe("Install in Cursor"); + expect(link?.getAttribute("href")).toMatch(/^cursor:\/\//); + expect(link?.getAttribute("target")).toBe("_blank"); + + const bare = await mount( + , + ); + + await act(async () => button(bare, "Install the plugin")?.click()); + expect(bare.querySelector("a")).toBeNull(); + expect(body(bare)).toContain("Install in Cursor"); + expect(body(bare)).toContain("Install the review command first."); + expect(bare.querySelector(".review-connect-note")).toBeNull(); + }); + + it("notes the missing review command without disabling copy", async () => { + const container = await mount( + , + ); + + expect(container.querySelector(".review-connect-note")?.textContent).toBe( + "Install the review command first. The prompt and the plugin both launch it.", + ); + expect(copyButton(container)?.disabled).toBe(false); + }); + + it("collapses a long prompt until the reader expands it, and copies all of it", async () => { + const long = Array.from({ length: 8 }, (_, i) => `line ${i + 1}`).join( + "\n", + ); + + const writeText = vi + .spyOn(navigator.clipboard, "writeText") + .mockResolvedValue(); + + const container = await mount( + , + ); + + const body = container.querySelector("pre"); + expect(body?.dataset.collapsed).toBe("true"); + + const toggle = container.querySelector("[aria-expanded]"); + expect(toggle?.textContent).toBe("Show full prompt"); + await act(async () => (toggle as HTMLButtonElement).click()); + expect(body?.dataset.collapsed).toBe("false"); + expect( + container.querySelector(".review-connect-collapse")?.textContent, + ).toBe("Show less"); + + const copy = [...container.querySelectorAll("button")].find( + (b) => b.textContent === "Copy prompt", + ); + + await act(async () => copy?.click()); + expect(writeText).toHaveBeenCalledWith(long); + + const short = await mount(); + expect(short.querySelector("pre")?.dataset.collapsed).toBe("false"); + expect(short.querySelector("[aria-expanded]")).toBeNull(); + }); + + it("shows the setup error from the status", async () => { + const container = await mount( + , + ); + + expect(container.querySelector(".review-connect-error")?.textContent).toBe( + "boom", + ); + }); +}); + +describe("LegacySkillsRow", () => { + it("renders nothing without legacy skills and reports what it removed", async () => { + const install = content({ + legacySkills: [{ path: "/h/.claude/skills/review" }], + }); + + const onStatusChange = vi.fn<(status: ReviewCliInstallStatus) => void>(); + + const container = await mount( + , + ); + + expect(container.textContent).toContain("Review no longer uses them."); + expect(container.textContent).toContain("/h/.claude/skills/review"); + + const button = [...container.querySelectorAll("button")].find( + (b) => b.textContent === "Remove old Review skills", + ); + + await act(async () => button?.click()); + expect(install.removeLegacySkills).toHaveBeenCalled(); + + const next = onStatusChange.mock.calls[0]?.[0]; + + expect(next?.legacySkills).toEqual([]); + + await act(async () => { + if (next) { + mounted[0]?.root.render( + , + ); + } + }); + expect(container.textContent).toContain("Removed 1 skill"); + expect(container.textContent).toContain("/h/.claude/skills/review"); + expect(container.querySelector("button")).toBeNull(); + + const empty = await mount(); + + expect(empty.textContent).toBe(""); + }); +}); diff --git a/packages/review/app/src/connect-card.tsx b/packages/review/app/src/connect-card.tsx new file mode 100644 index 000000000..43889af62 --- /dev/null +++ b/packages/review/app/src/connect-card.tsx @@ -0,0 +1,353 @@ +import { + type ReviewCanvasInstallContent, + type ReviewCliInstallStatus, + type ReviewCliInstallTarget, + ReviewCliInstallTargetSchema, +} from "@dev.fast/review-protocol"; +import { useEffect, useRef, useState } from "react"; + +import { AGENT_LOGOS } from "./agent-logos"; +import { CopyIcon, copyText } from "./copy-text"; +import { newTabLinkProps } from "./link-props"; + +export const TARGET_LABELS: Record = { + claude: "Claude Code", + codex: "Codex", + cursor: "Cursor", + opencode: "OpenCode", + pi: "Pi", +}; + +export const REVIEW_CONNECT_TARGET_STORAGE_KEY = + "dev.fast.review.connectTarget"; + +const COPIED_RESET_MS = 2000; + +/** Lines of a prompt shown before the reader expands it. */ +const COLLAPSED_LINES = 4; + +type Mode = "prompt" | "plugin"; + +const MODES: ReadonlyArray<{ mode: Mode; label: string }> = [ + { mode: "prompt", label: "Paste a prompt" }, + { mode: "plugin", label: "Install the plugin" }, +]; + +/** + * One agent at a time: a paste-in prompt that has the agent add Review's MCP + * server, or the published plugin (an install command, or Cursor's link). + */ +export function ConnectCard({ + install, + onCopied, +}: { + install: ReviewCanvasInstallContent; + onCopied?: () => void; +}) { + const { status } = install; + + const [target, setTarget] = + useState(readStoredTarget); + + const [mode, setMode] = useState("prompt"); + const [copied, setCopied] = useState(false); + const [expanded, setExpanded] = useState(false); + + const resetTimer = useRef | undefined>( + undefined, + ); + + useEffect(() => () => clearTimeout(resetTimer.current), []); + + const clearCopied = () => { + setCopied(false); + clearTimeout(resetTimer.current); + }; + + const selectTarget = (next: ReviewCliInstallTarget) => { + setTarget(next); + clearCopied(); + setExpanded(false); + + try { + globalThis.localStorage?.setItem(REVIEW_CONNECT_TARGET_STORAGE_KEY, next); + } catch { + // The desktop can disable DOM storage; the in-memory selection still works. + } + }; + + const selectMode = (next: Mode) => { + setMode(next); + clearCopied(); + setExpanded(false); + }; + + const agent = TARGET_LABELS[target]; + + const plugin = status.connect.plugins[target]; + + const text = + mode === "prompt" ? status.connect.prompts[target] : plugin.command; + + const copy = (value: string) => { + void copyText(value).then((ok) => { + if (!ok) { + return; + } + + setCopied(true); + clearTimeout(resetTimer.current); + resetTimer.current = setTimeout(() => setCopied(false), COPIED_RESET_MS); + onCopied?.(); + }); + }; + + const noun = mode === "prompt" ? "prompt" : "install command"; + + // Prompts run to a dozen lines; show the opening and let the reader expand. + const collapsible = (text?.split("\n").length ?? 0) > COLLAPSED_LINES; + + const collapsed = collapsible && !expanded; + + return ( +
+ {/* A packaged Desktop launches agents through the shim, so nothing + works until it exists. From source the prompts use the bare + command instead. */} + {status.cli && !status.shim.installed ? ( +

+ Install the review command first. The prompt and the plugin both + launch it. +

+ ) : null} +
+ {ReviewCliInstallTargetSchema.options.map((tab) => { + const Logo = AGENT_LOGOS[tab]; + + return ( + + ); + })} +
+
+ {MODES.map(({ mode: tab, label }) => ( + + ))} +
+ {text ? ( + <> +
+
+              {text}
+            
+ {collapsed ? ( + + ) : null} +
+
+ {collapsible && expanded ? ( + + ) : null} + +
+ {mode === "prompt" ? ( +

+ Your agent should reply that it reached Review's + instructions. If it says Review is not running, keep this app open + and try again. +

+ ) : null} + + ) : plugin.url ? ( + <> +

+ Opens {agent} and adds the review server. +

+ + + ) : ( +

+ {`${plugin.label}\nInstall the review command first.`} +

+ )} + {status.error ? ( +

{status.error}

+ ) : null} +
+ ); +} + +function CheckIcon() { + return ( + + ); +} + +function readStoredTarget(): ReviewCliInstallTarget { + try { + const stored = ReviewCliInstallTargetSchema.safeParse( + globalThis.localStorage?.getItem(REVIEW_CONNECT_TARGET_STORAGE_KEY), + ); + + if (stored.success) { + return stored.data; + } + } catch { + // Fall through to the default when DOM storage is unavailable. + } + + return "claude"; +} + +/** + * Skills that earlier versions of Review installed into agent configs. After + * a removal the row names what went, until the next status refresh. + */ +export function LegacySkillsRow({ + install, + onStatusChange, +}: { + install: ReviewCanvasInstallContent; + onStatusChange?: (status: ReviewCliInstallStatus) => void; +}) { + const [busy, setBusy] = useState(false); + const [error, setError] = useState(null); + + const [removal, setRemoval] = useState<{ + paths: string[]; + status: ReviewCliInstallStatus; + } | null>(null); + + const { legacySkills } = install.status; + + const removed = removal?.status === install.status ? removal.paths : []; + + if (legacySkills.length === 0 && removed.length === 0) return null; + + const removeSkills = async () => { + setBusy(true); + setError(null); + + try { + const next = await install.removeLegacySkills(); + + const remaining = new Set(next.legacySkills.map((skill) => skill.path)); + + const paths = legacySkills + .map((skill) => skill.path) + .filter((path) => !remaining.has(path)); + + setRemoval({ paths, status: next }); + onStatusChange?.(next); + } catch (cause) { + setError(cause instanceof Error ? cause.message : String(cause)); + } finally { + setBusy(false); + } + }; + + return ( +
+ {legacySkills.length > 0 ? ( + <> +

+ Earlier versions of Review installed these skills. Review no longer + uses them. +

+
    + {legacySkills.map((skill) => ( +
  • + {skill.path} +
  • + ))} +
+ + + ) : null} + {removed.length > 0 ? ( + <> +

+ Removed {removed.length} skill{removed.length === 1 ? "" : "s"} +

+
    + {removed.map((path) => ( +
  • + {path} +
  • + ))} +
+ + ) : null} + {error ?

{error}

: null} +
+ ); +} diff --git a/packages/review/app/src/desktop-entry.tsx b/packages/review/app/src/desktop-entry.tsx index ffc7c64fb..875df47e5 100644 --- a/packages/review/app/src/desktop-entry.tsx +++ b/packages/review/app/src/desktop-entry.tsx @@ -103,7 +103,6 @@ function Home({ onRestore={ restoreReview ? (review) => restoreReview(review.reviewId) : undefined } - setup={content.setup} install={content.install} setupActions={content.setupActions} onboarding={content.onboarding} diff --git a/packages/review/app/src/prompt-card.tsx b/packages/review/app/src/prompt-card.tsx index c1059848e..daa5df6c6 100644 --- a/packages/review/app/src/prompt-card.tsx +++ b/packages/review/app/src/prompt-card.tsx @@ -1,16 +1,7 @@ -import type { ReviewCliInstallStatus } from "@dev.fast/review-protocol"; import { useEffect, useRef, useState } from "react"; import { CopyIcon, copyText } from "./copy-text"; -/** Which agent's invocation syntax the prompt uses. Derived, never asked. */ -export type PromptAgent = - | "claude" - | "codex" - | "cursor" - | "generic" - | "opencode"; - /** What the review covers. This is the only choice the reader makes. */ export type PromptKind = "change" | "architecture"; @@ -23,37 +14,14 @@ const PROMPT_KINDS: ReadonlyArray<{ kind: PromptKind; label: string }> = [ ]; /** - * The architecture prompt names the mode and stops there: the dev-review skill - * documents how to author one, so the prompt does not have to carry the mechanics. + * Prompts name the subject and stop there: Review's server gives the agent + * the authoring instructions, so every agent gets the same wording. */ -export const PROMPT_VARIANTS: Record< - PromptKind, - Record -> = { - change: { - claude: - "Use the dev-review skill to review my current branch against up to date main, then open it in Review.", - codex: - "Use $dev-review to review my current branch against up to date main, then open it in Review.", - cursor: - "/dev-review Review my current branch against up to date main, then open it in Review.", - opencode: - "Use the dev-review skill to review my current branch against up to date main, then open it in Review.", - generic: - "Use the dev-review skill to review my current branch against up to date main: register the repository, resolve pins, create the review, then edit it through the Review MCP tools or `review api`.", - }, - architecture: { - claude: - "Use the dev-review skill to sketch out the main data flows, access patterns, and code paths in this repo, so I can do a full architecture review of it. Open it in Review when you're done.", - codex: - "Use $dev-review to sketch out the main data flows, access patterns, and code paths in this repo, so I can do a full architecture review of it. Open it in Review when you're done.", - cursor: - "/dev-review Sketch out the main data flows, access patterns, and code paths in this repo, so I can do a full architecture review of it. Open it in Review when you’re done.", - opencode: - "Use the dev-review skill to sketch out the main data flows, access patterns, and code paths in this repo, so I can do a full architecture review of it. Open it in Review when you're done.", - generic: - "Use the dev-review skill to sketch out the main data flows, access patterns, and code paths in this repo, so I can do a full architecture review of it: register the repository, resolve pins, create the review, then edit it through the Review MCP tools or `review api`. Open it in Review when you're done.", - }, +export const PROMPTS: Record = { + change: + "Create a Review of my current branch against up to date main, then open it in Review.", + architecture: + "Create a Review that sketches out the main data flows, access patterns, and code paths in this repo, so I can do a full architecture review of it. Open it in Review when you're done.", }; const COPIED_RESET_MS = 2000; @@ -62,10 +30,9 @@ const COPIED_RESET_MS = 2000; * The copy-a-prompt card. Only the user's agent can write a review of their * own repo, so both the Welcome rail and the Home zero state end here. * - * The tabs choose what the review covers. Which agent it is written for is - * passed in, not asked: the app already knows what is installed. + * The tabs choose what the review covers. */ -export function PromptCard({ agent }: { agent: PromptAgent }) { +export function PromptCard() { const [kind, setKind] = useState(readStoredPromptKind); const [copied, setCopied] = useState(false); @@ -91,7 +58,7 @@ export function PromptCard({ agent }: { agent: PromptAgent }) { }; const copyPrompt = () => { - void copyText(PROMPT_VARIANTS[kind][agent]).then((ok) => { + void copyText(PROMPTS[kind]).then((ok) => { if (!ok) { return; } @@ -121,9 +88,7 @@ export function PromptCard({ agent }: { agent: PromptAgent }) { ))} -
-        {PROMPT_VARIANTS[kind][agent]}
-      
+
{PROMPTS[kind]}
-
- ); -} - -export function setupBannerMessage( - status: ReviewCliInstallStatus, -): string | null { - if (!status.cli || status.stamp?.consent === "declined") return null; - - if (!status.stamp || status.stamp.consent === "skipped") { - if (status.agents.some((agent) => agent.installed)) return null; - const present = status.agents.filter((agent) => agent.present); - - if (present.length === 0) return null; - - return "Review is not set up for your coding agents yet."; - } - - if (status.stale) { - return "The installed Review skills are older than this app."; - } - - const missing = status.agents.filter( - (agent) => agent.present && !agent.installed, - ); - - if (missing.length > 0) { - return `The Review skills are not installed for ${missing - .map((agent) => TARGET_LABELS[agent.target]) - .join(", ")}.`; - } - - // Trace capture is experimental and opt-in, so Home never nags about it. - return null; -} - /** * Filter-as-you-type over the review title and the worktree name — the two * labels the page already shows. Escape clears it. diff --git a/packages/review/app/src/settings-page.tsx b/packages/review/app/src/settings-page.tsx index ae7e5926f..66700a85d 100644 --- a/packages/review/app/src/settings-page.tsx +++ b/packages/review/app/src/settings-page.tsx @@ -6,7 +6,7 @@ import type { } from "@dev.fast/review-protocol"; import { type ReactNode, useEffect, useState } from "react"; -import { AgentSetupCard } from "./agent-setup-card"; +import { ConnectCard, LegacySkillsRow } from "./connect-card"; import { DiffrConfigSection } from "./diffr-config-section"; import { TraceCaptureSection } from "./trace-capture-section"; @@ -101,10 +101,42 @@ export function SettingsPage({ {install ? (
- + +
+ ) : null} + + {install?.status.cli ? ( +
+ + +
) : null} @@ -232,7 +264,7 @@ export function SettingsPage({