Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .agents/plugins/marketplace.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"name": "devfast",
"plugins": [
{
"name": "whiteboard",
"source": { "source": "local", "path": "./packages/agent-plugins/codex" },
"policy": { "installation": "AVAILABLE" },
"category": "Productivity"
}
]
}
11 changes: 11 additions & 0 deletions .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
@@ -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"
}
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ test("opening historical source requests its version and opens a separate native
{} as never,
{ async getConnection() { return { serverUrl: "http://localhost", token: "test" }; } } as never,
{ async openWindow(...args: unknown[]) { opened.push(args); } } as never,
{ warn() {} } as never,
);
t.after(() => tabs.dispose());
await tabs.openApiSource({ reviewId: "review-a", kind: "version", version: 7 }, "Historical Review");
Expand Down Expand Up @@ -59,7 +60,7 @@ test("native group restoration preserves both reviews, order and pinned source v
};
const groupService = { groups, mainPart: { activeGroup: undefined as EditorGroupModel | undefined } };
const createTabs = () =>
new ReviewCanvasEditorTabsService(instantiation as never, editors as never, groupService as never, {} as never, {} as never);
new ReviewCanvasEditorTabsService(instantiation as never, editors as never, groupService as never, {} as never, {} as never, { warn() {} } as never);
tabs = createTabs();
const registry = Registry.as<IEditorFactoryRegistry>(EditorExtensions.EditorFactory);
registry.start({ get: () => instantiation } as never);
Expand Down Expand Up @@ -126,7 +127,7 @@ test("current Source tabs retain identity and main version tabs still restore",
},
invokeFunction(fn: (accessor: { get(): ReviewCanvasEditorTabsService }) => unknown) { return fn({ get: () => tabs }); },
};
tabs = new ReviewCanvasEditorTabsService(instantiation as never, { onDidCloseEditor: Event.None } as never, {} as never, {} as never, {} as never);
tabs = new ReviewCanvasEditorTabsService(instantiation as never, { onDidCloseEditor: Event.None } as never, {} as never, {} as never, {} as never, { warn() {} } as never);
try {
const serializer = new ReviewApiEditorSerializer();
const restored = serializer.deserialize(instantiation as never, JSON.stringify({ kind: "api-source", reviewId: "a", title: "A", selection: { reviewId: "a", kind: "current" } }));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,6 @@ import type {
ReviewCanvasContent,
ReviewCanvasDiagnostic,
ReviewCanvasHandle,
ReviewCanvasHomeSetup,
ReviewCanvasInstallContent,
ReviewCanvasModule,
ReviewCanvasOnboarding,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -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;
}
}
Expand All @@ -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();
Expand Down Expand Up @@ -779,21 +784,6 @@ export class ReviewCanvasEditorPane extends EditorPane {
return this.configurationService.getValue<boolean>(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<ReviewCanvasHomeSetup | undefined> {
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
Expand All @@ -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
Expand Down

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -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',
);
});
Original file line number Diff line number Diff line change
@@ -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';
}
Loading
Loading