agent-canvas 0.2.1 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +42 -17
- package/bin/agent-canvas.js +7 -7
- package/bin/agent-canvas.ts +48 -37
- package/bin/lib/commands/daemon.ts +46 -43
- package/bin/lib/commands/exec.ts +100 -100
- package/bin/lib/commands/fetch.ts +15 -15
- package/bin/lib/commands/install.ts +36 -36
- package/bin/lib/commands/instructions.ts +51 -51
- package/bin/lib/commands/push.ts +115 -97
- package/bin/lib/commands/review.ts +190 -0
- package/bin/lib/commands/watch.ts +24 -24
- package/bin/lib/config.ts +12 -12
- package/bin/lib/daemon-lifecycle.ts +47 -51
- package/bin/lib/helpers.ts +82 -76
- package/bin/lib/instructions/app-review.md +88 -0
- package/bin/lib/instructions/component-checklist.md +7 -5
- package/bin/lib/instructions/component-custom.md +20 -14
- package/bin/lib/instructions/component-diff.md +4 -3
- package/bin/lib/instructions/component-item.md +18 -8
- package/bin/lib/instructions/component-markdown.md +4 -2
- package/bin/lib/instructions/component-mermaid.md +12 -6
- package/bin/lib/instructions/component-multichoice.md +6 -2
- package/bin/lib/instructions/component-note.md +1 -1
- package/bin/lib/instructions/component-priority.md +1 -1
- package/bin/lib/instructions/component-rangeinput.md +9 -2
- package/bin/lib/instructions/component-secretinput.md +12 -11
- package/bin/lib/instructions/component-svg.md +62 -16
- package/bin/lib/instructions/component-table.md +6 -6
- package/bin/lib/instructions/component-usefeedback.md +8 -8
- package/bin/lib/instructions/component-userinput.md +6 -2
- package/bin/lib/instructions/flow-feature.md +22 -5
- package/bin/lib/instructions/flow-plan.md +5 -5
- package/bin/lib/instructions/flow-runbook.md +28 -25
- package/bin/lib/instructions/overview.md +27 -18
- package/bin/lib/instructions/theming.md +5 -0
- package/daemon/build.ts +171 -152
- package/daemon/client/App.tsx +1205 -1077
- package/daemon/client/CompareView.tsx +478 -458
- package/daemon/client/FileBrowser.tsx +241 -203
- package/daemon/client/FileViewer.tsx +220 -211
- package/daemon/client/OverviewView.tsx +283 -267
- package/daemon/client/ReviewerIdentityDialog.tsx +72 -71
- package/daemon/client/RevisionSelect.tsx +111 -95
- package/daemon/client/SessionSwitcher.tsx +81 -81
- package/daemon/client/ShareDialog.tsx +211 -212
- package/daemon/client/apiSchemas.ts +78 -0
- package/daemon/client/canvas-runtime.ts +1 -1
- package/daemon/client/clientApi.ts +194 -206
- package/daemon/client/components/SecretInput.tsx +138 -138
- package/daemon/client/components/index.ts +2 -2
- package/daemon/client/domDiff.ts +341 -333
- package/daemon/client/errorMessage.ts +3 -0
- package/daemon/client/exportMarkdown.ts +222 -201
- package/daemon/client/lcs.ts +19 -14
- package/daemon/client/shareCrypto.ts +57 -57
- package/daemon/client/unifiedDiff.ts +660 -649
- package/daemon/dist/annotations-runtime.js +1 -1
- package/daemon/dist/client.css +2 -2
- package/daemon/dist/client.js +77 -80
- package/daemon/dist/components.js +59 -59
- package/daemon/dist/preact-compat.js +1 -1
- package/daemon/dist/review.js +361 -0
- package/daemon/dist/runtime.js +1 -1
- package/daemon/langMap.ts +24 -6
- package/daemon/review-widget/anchor.ts +187 -0
- package/daemon/review-widget/client.ts +95 -0
- package/daemon/review-widget/index.tsx +36 -0
- package/daemon/review-widget/markup.ts +71 -0
- package/daemon/review-widget/screenshot.ts +91 -0
- package/daemon/review-widget/source.ts +103 -0
- package/daemon/review-widget/styles.css +359 -0
- package/daemon/review-widget/text-modules.d.ts +4 -0
- package/daemon/review-widget/tsconfig.json +16 -0
- package/daemon/review-widget/widget.tsx +436 -0
- package/daemon/src/components.ts +3 -3
- package/daemon/src/handlers/api.ts +330 -328
- package/daemon/src/handlers/files.ts +112 -106
- package/daemon/src/handlers/static.ts +47 -46
- package/daemon/src/handlers/uploads.ts +61 -56
- package/daemon/src/paths.ts +13 -10
- package/daemon/src/remote-feedback.ts +192 -192
- package/daemon/src/review/markdown.ts +52 -0
- package/daemon/src/review/model.ts +132 -0
- package/daemon/src/review/proxy.ts +177 -0
- package/daemon/src/review/server.ts +199 -0
- package/daemon/src/review/store.ts +151 -0
- package/daemon/src/server.ts +127 -86
- package/daemon/src/share.ts +163 -169
- package/daemon/src/stubFetch.ts +7 -0
- package/daemon/tsconfig.json +19 -19
- package/package.json +47 -47
- package/skills/canvas/SKILL.md +5 -1
- package/bin/lib/commands/exec.test.ts +0 -27
- package/bin/lib/helpers.test.ts +0 -79
- package/daemon/src/files-api.test.ts +0 -78
- package/daemon/src/remote-feedback.test.ts +0 -165
- package/daemon/src/secrets-api.test.ts +0 -123
- package/daemon/src/share-e2e.test.ts +0 -277
- package/daemon/src/share.test.ts +0 -289
|
@@ -1,30 +1,30 @@
|
|
|
1
|
-
import { parseArgs } from
|
|
2
|
-
import { ensureDaemon } from
|
|
3
|
-
import {
|
|
1
|
+
import { parseArgs } from 'util'
|
|
2
|
+
import { ensureDaemon } from '../daemon-lifecycle.ts'
|
|
3
|
+
import { consumeFeedback, getSessionId, waitForFeedback } from '../helpers.ts'
|
|
4
4
|
|
|
5
5
|
export async function handleWatch(args: string[]) {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
6
|
+
const { values } = parseArgs({
|
|
7
|
+
args,
|
|
8
|
+
options: {
|
|
9
|
+
session: { type: 'string' },
|
|
10
|
+
},
|
|
11
|
+
})
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
const sessionId = getSessionId(values.session)
|
|
14
|
+
await ensureDaemon()
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
16
|
+
const feedback = await consumeFeedback(sessionId)
|
|
17
|
+
if (feedback) {
|
|
18
|
+
process.stdout.write(feedback)
|
|
19
|
+
process.exit(0)
|
|
20
|
+
}
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
22
|
+
try {
|
|
23
|
+
const submittedFeedback = await waitForFeedback(sessionId)
|
|
24
|
+
process.stdout.write(submittedFeedback)
|
|
25
|
+
} catch (error) {
|
|
26
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
27
|
+
console.error(`Error: ${message}`)
|
|
28
|
+
process.exit(1)
|
|
29
|
+
}
|
|
30
30
|
}
|
package/bin/lib/config.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { readFileSync } from 'fs'
|
|
2
|
+
import { tmpdir } from 'os'
|
|
3
|
+
import { dirname, join, resolve } from 'path'
|
|
4
4
|
|
|
5
|
-
export const PACKAGE_ROOT = resolve(join(dirname(import.meta.path),
|
|
6
|
-
export const VERSION = JSON.parse(readFileSync(join(PACKAGE_ROOT,
|
|
7
|
-
export const TEMP_DIR = join(tmpdir(),
|
|
8
|
-
export const DAEMON_PORT = parseInt(process.env.CANVAS_PORT ||
|
|
9
|
-
export const BASE_URL = `http://localhost:${DAEMON_PORT}
|
|
10
|
-
export const WS_URL = `ws://localhost:${DAEMON_PORT}
|
|
11
|
-
export const TIMEOUT_MS = parseInt(process.env.CANVAS_TIMEOUT || String(60 * 60 * 1000), 10)
|
|
12
|
-
export const PID_FILE = join(TEMP_DIR,
|
|
13
|
-
export const CLI_AUTH_FILE = join(TEMP_DIR,
|
|
5
|
+
export const PACKAGE_ROOT = resolve(join(dirname(import.meta.path), '../..'))
|
|
6
|
+
export const VERSION = JSON.parse(readFileSync(join(PACKAGE_ROOT, 'package.json'), 'utf-8')).version as string
|
|
7
|
+
export const TEMP_DIR = join(tmpdir(), 'agent-canvas')
|
|
8
|
+
export const DAEMON_PORT = parseInt(process.env.CANVAS_PORT || '19400', 10)
|
|
9
|
+
export const BASE_URL = `http://localhost:${DAEMON_PORT}`
|
|
10
|
+
export const WS_URL = `ws://localhost:${DAEMON_PORT}`
|
|
11
|
+
export const TIMEOUT_MS = parseInt(process.env.CANVAS_TIMEOUT || String(60 * 60 * 1000), 10)
|
|
12
|
+
export const PID_FILE = join(TEMP_DIR, 'daemon.pid')
|
|
13
|
+
export const CLI_AUTH_FILE = join(TEMP_DIR, 'daemon-auth-token')
|
|
@@ -1,19 +1,15 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
DaemonStartError,
|
|
6
|
-
type DaemonEvent,
|
|
7
|
-
} from "@fabrika/daemon-kit";
|
|
8
|
-
import { PACKAGE_ROOT, TEMP_DIR, BASE_URL, PID_FILE, CLI_AUTH_FILE, VERSION } from "./config.ts";
|
|
1
|
+
import { createDaemonLifecycle, type DaemonEvent, DaemonStartError } from '@fabrika/daemon-kit'
|
|
2
|
+
import { readFileSync } from 'fs'
|
|
3
|
+
import { join } from 'path'
|
|
4
|
+
import { BASE_URL, CLI_AUTH_FILE, PACKAGE_ROOT, PID_FILE, TEMP_DIR, VERSION } from './config.ts'
|
|
9
5
|
|
|
10
6
|
interface CanvasHealth {
|
|
11
|
-
|
|
12
|
-
|
|
7
|
+
pid: number
|
|
8
|
+
version: string
|
|
13
9
|
}
|
|
14
10
|
|
|
15
11
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
16
|
-
|
|
12
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
17
13
|
}
|
|
18
14
|
|
|
19
15
|
/**
|
|
@@ -21,67 +17,67 @@ function isRecord(value: unknown): value is Record<string, unknown> {
|
|
|
21
17
|
* behind. Drop this fallback once no such daemon can still be running.
|
|
22
18
|
*/
|
|
23
19
|
function legacyPidFromFile(): number | undefined {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
20
|
+
try {
|
|
21
|
+
const pid = Number(readFileSync(PID_FILE, 'utf-8').trim())
|
|
22
|
+
return Number.isInteger(pid) && pid > 0 ? pid : undefined
|
|
23
|
+
} catch {
|
|
24
|
+
return undefined
|
|
25
|
+
}
|
|
30
26
|
}
|
|
31
27
|
|
|
32
28
|
function parseHealth(body: unknown): CanvasHealth | undefined {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
29
|
+
if (!isRecord(body) || body.ok !== true || typeof body.version !== 'string') return undefined
|
|
30
|
+
const pid = typeof body.pid === 'number' ? body.pid : legacyPidFromFile()
|
|
31
|
+
if (pid === undefined) return undefined
|
|
32
|
+
return { pid, version: body.version }
|
|
37
33
|
}
|
|
38
34
|
|
|
39
35
|
function report(event: DaemonEvent) {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
36
|
+
if (event.type === 'version-mismatch') {
|
|
37
|
+
console.error(`Daemon version mismatch (daemon: ${event.running}, CLI: ${VERSION}), restarting...`)
|
|
38
|
+
}
|
|
39
|
+
if (event.type === 'starting') console.error('Starting canvas daemon...')
|
|
40
|
+
if (event.type === 'started') console.error('Daemon started.')
|
|
45
41
|
}
|
|
46
42
|
|
|
47
43
|
const lifecycle = createDaemonLifecycle<CanvasHealth>({
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
})
|
|
44
|
+
baseUrl: BASE_URL,
|
|
45
|
+
parseHealth,
|
|
46
|
+
pidFile: PID_FILE,
|
|
47
|
+
// The running Bun, not a PATH lookup — `Bun.spawn` throws on a missing executable.
|
|
48
|
+
command: [process.execPath, 'run', join(PACKAGE_ROOT, 'daemon', 'src', 'server.ts')],
|
|
49
|
+
cwd: join(PACKAGE_ROOT, 'daemon'),
|
|
50
|
+
logFile: join(TEMP_DIR, 'daemon-startup.log'),
|
|
51
|
+
versionCheck: { expected: VERSION, of: (health) => health.version },
|
|
52
|
+
cleanupFiles: [CLI_AUTH_FILE],
|
|
53
|
+
onEvent: report,
|
|
54
|
+
})
|
|
59
55
|
|
|
60
56
|
export async function isDaemonRunning(): Promise<boolean> {
|
|
61
|
-
|
|
57
|
+
return lifecycle.isRunning()
|
|
62
58
|
}
|
|
63
59
|
|
|
64
60
|
function reportStartFailure(error: unknown): never {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
61
|
+
if (error instanceof DaemonStartError && error.logTail) {
|
|
62
|
+
console.error('Daemon stderr:\n' + error.logTail)
|
|
63
|
+
}
|
|
64
|
+
console.error(`Error: ${error instanceof Error ? error.message : String(error)}`)
|
|
65
|
+
process.exit(1)
|
|
70
66
|
}
|
|
71
67
|
|
|
72
68
|
export async function startDaemon(): Promise<void> {
|
|
73
|
-
|
|
69
|
+
await lifecycle.start().catch(reportStartFailure)
|
|
74
70
|
}
|
|
75
71
|
|
|
76
72
|
export async function ensureDaemon(): Promise<void> {
|
|
77
|
-
|
|
73
|
+
await lifecycle.ensure().catch(reportStartFailure)
|
|
78
74
|
}
|
|
79
75
|
|
|
80
76
|
export async function stopDaemon(): Promise<boolean> {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
77
|
+
const result = await lifecycle.stop()
|
|
78
|
+
if (result.reason === 'stopped') console.log('Daemon stopped.')
|
|
79
|
+
else if (result.reason === 'stale-pid') console.log('Daemon was not running (stale PID file).')
|
|
80
|
+
else if (result.reason === 'not-running') console.log('No daemon PID file found.')
|
|
81
|
+
else console.log(result.message)
|
|
82
|
+
return result.stopped
|
|
87
83
|
}
|
package/bin/lib/helpers.ts
CHANGED
|
@@ -1,111 +1,117 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
1
|
+
import { type WaiterSocket, waitForEvent, type WaitVerdict } from '@fabrika/daemon-kit'
|
|
2
|
+
import { spawn } from 'child_process'
|
|
3
|
+
import { randomUUID } from 'crypto'
|
|
4
|
+
import { z } from 'zod'
|
|
5
|
+
import { BASE_URL, TIMEOUT_MS, WS_URL } from './config.ts'
|
|
5
6
|
|
|
6
7
|
export function getSessionId(session?: string): string {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
8
|
+
if (session) return session
|
|
9
|
+
return process.env.CANVAS_SESSION_ID || (() => {
|
|
10
|
+
const id = randomUUID()
|
|
11
|
+
console.error(`Warning: CANVAS_SESSION_ID not set, using generated ID: ${id}`)
|
|
12
|
+
return id
|
|
13
|
+
})()
|
|
13
14
|
}
|
|
14
15
|
|
|
16
|
+
const consumeResponseSchema = z.discriminatedUnion('found', [
|
|
17
|
+
z.object({ found: z.literal(true), feedback: z.string() }),
|
|
18
|
+
z.object({ found: z.literal(false) }),
|
|
19
|
+
])
|
|
20
|
+
|
|
15
21
|
export async function consumeFeedback(sessionId: string): Promise<string | null> {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
22
|
+
const res = await fetch(`${BASE_URL}/api/session/${sessionId}/feedback/consume`, { method: 'POST' })
|
|
23
|
+
const data = consumeResponseSchema.parse(await res.json())
|
|
24
|
+
if (data.found) return data.feedback
|
|
25
|
+
return null
|
|
20
26
|
}
|
|
21
27
|
|
|
22
28
|
export function openBrowser(url: string) {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
29
|
+
const candidates = process.platform === 'darwin'
|
|
30
|
+
? ['open']
|
|
31
|
+
: ['xdg-open', 'wslview', 'sensible-browser']
|
|
26
32
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
for (const cmd of candidates) {
|
|
34
|
+
const which = Bun.spawnSync(['which', cmd])
|
|
35
|
+
if (which.exitCode === 0) {
|
|
36
|
+
spawn(cmd, [url], { detached: true, stdio: 'ignore' }).unref()
|
|
37
|
+
return
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
console.error(`Open this URL in your browser: ${url}`)
|
|
35
41
|
}
|
|
36
42
|
|
|
37
43
|
interface FeedbackWaitOptions {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
44
|
+
createSocket?: (url: string) => WaiterSocket
|
|
45
|
+
timeoutMs?: number
|
|
46
|
+
reconnectAttempts?: number
|
|
47
|
+
reconnectDelayMs?: number
|
|
42
48
|
}
|
|
43
49
|
|
|
44
50
|
interface CanvasRenderError {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
51
|
+
revision: number
|
|
52
|
+
filename: string
|
|
53
|
+
message: string
|
|
54
|
+
stack?: string
|
|
55
|
+
componentStack?: string
|
|
50
56
|
}
|
|
51
57
|
|
|
52
58
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
53
|
-
|
|
59
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
54
60
|
}
|
|
55
61
|
|
|
56
62
|
function isCanvasRenderError(value: unknown): value is CanvasRenderError {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
+
return typeof value === 'object' && value !== null
|
|
64
|
+
&& 'revision' in value && Number.isInteger(value.revision)
|
|
65
|
+
&& 'filename' in value && typeof value.filename === 'string'
|
|
66
|
+
&& 'message' in value && typeof value.message === 'string'
|
|
67
|
+
&& (!('stack' in value) || value.stack === undefined || typeof value.stack === 'string')
|
|
68
|
+
&& (!('componentStack' in value) || value.componentStack === undefined || typeof value.componentStack === 'string')
|
|
63
69
|
}
|
|
64
70
|
|
|
65
71
|
function formatCanvasRenderError(error: CanvasRenderError): string {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
72
|
+
const details = [`Canvas render failed in ${error.filename} (revision ${error.revision}):`, error.message]
|
|
73
|
+
if (error.componentStack?.trim()) details.push(`Component stack:${error.componentStack}`)
|
|
74
|
+
if (error.stack?.trim()) details.push(`Browser stack:\n${error.stack}`)
|
|
75
|
+
return details.join('\n')
|
|
70
76
|
}
|
|
71
77
|
|
|
72
78
|
/** A canvas that failed to render is the answer — retrying the socket cannot fix it. */
|
|
73
79
|
function readFeedbackFrame(frame: unknown): WaitVerdict<string> | undefined {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
80
|
+
if (!isRecord(frame)) return undefined
|
|
81
|
+
if (frame.type === 'submit' && typeof frame.feedback === 'string') {
|
|
82
|
+
return { type: 'settle', value: frame.feedback }
|
|
83
|
+
}
|
|
84
|
+
if (frame.type === 'render-error' && isCanvasRenderError(frame.error)) {
|
|
85
|
+
return { type: 'fail', error: new Error(formatCanvasRenderError(frame.error)) }
|
|
86
|
+
}
|
|
87
|
+
return undefined
|
|
82
88
|
}
|
|
83
89
|
|
|
84
90
|
export function waitForFeedback(sessionId: string, options: FeedbackWaitOptions = {}): Promise<string> {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
91
|
+
return waitForEvent<string>({
|
|
92
|
+
url: `${WS_URL}/ws/wait/${sessionId}`,
|
|
93
|
+
onFrame: readFeedbackFrame,
|
|
94
|
+
...(options.createSocket ? { createSocket: options.createSocket } : {}),
|
|
95
|
+
timeoutMs: options.timeoutMs ?? TIMEOUT_MS,
|
|
96
|
+
reconnect: {
|
|
97
|
+
attempts: options.reconnectAttempts ?? 3,
|
|
98
|
+
delayMs: options.reconnectDelayMs ?? 1_000,
|
|
99
|
+
},
|
|
100
|
+
messages: {
|
|
101
|
+
timeout: 'Timeout waiting for feedback.',
|
|
102
|
+
closed: 'WebSocket closed before feedback was submitted.',
|
|
103
|
+
connectionFailed: 'WebSocket connection failed.',
|
|
104
|
+
},
|
|
105
|
+
})
|
|
100
106
|
}
|
|
101
107
|
|
|
102
108
|
export async function readLine(): Promise<string> {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
109
|
+
const buf: number[] = []
|
|
110
|
+
for await (const chunk of Bun.stdin.stream()) {
|
|
111
|
+
for (const byte of chunk) {
|
|
112
|
+
if (byte === 10) return Buffer.from(buf).toString('utf-8')
|
|
113
|
+
buf.push(byte)
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return Buffer.from(buf).toString('utf-8')
|
|
111
117
|
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# Review mode — comments on a running application
|
|
2
|
+
|
|
3
|
+
Review mode lets the user click through a running web application (a prototype, a mock, a dev server) and leave
|
|
4
|
+
comments on its elements, the way they comment on a canvas. A comment stays a note until the user sends it to you.
|
|
5
|
+
You receive sent threads as markdown, change the code, and answer each thread; the answer appears next to the element.
|
|
6
|
+
|
|
7
|
+
Review mode has no session ID. Threads belong to a **project**. The commands default to `CANVAS_REVIEW_PROJECT`, then
|
|
8
|
+
to the git repository's directory name; pass `--project <name>` to name another.
|
|
9
|
+
|
|
10
|
+
## Setup
|
|
11
|
+
|
|
12
|
+
Open the application through the review proxy. The application stays as it is:
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
bunx agent-canvas review open <url> --project <name>
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
`<url>` is where the application's dev server runs, e.g. `http://localhost:5173/`; read it from the running server or
|
|
19
|
+
the project's dev script, and ask the user when you cannot tell. The daemon serves the application on a port of its own
|
|
20
|
+
with the widget added to every HTML page, opens it in the user's browser and prints the address. Hot reload passes
|
|
21
|
+
through. The port stays the same across daemon restarts. Only applications on `localhost` can be opened.
|
|
22
|
+
|
|
23
|
+
When the user wants the widget on the application's own address instead, add the tag to its HTML page in development
|
|
24
|
+
only:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
bunx agent-canvas review snippet --project <name>
|
|
28
|
+
# <script src="http://localhost:19400/assets/review.js" data-project="<name>"></script>
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Tell the user to use **Comment** in the bottom-right toolbar: click an element, or select text first, write the
|
|
32
|
+
comment, then **Send to Claude** or **Save note**.
|
|
33
|
+
|
|
34
|
+
## Loop
|
|
35
|
+
|
|
36
|
+
Start the watcher right away. The toolbar shows the user "Claude is listening" while it runs. Do NOT poll, sleep, or
|
|
37
|
+
check proactively.
|
|
38
|
+
|
|
39
|
+
**Claude Code**: open the application and watch in one command, with the Monitor tool and its longest timeout:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
bunx agent-canvas review open <url> --follow --project <name>
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Its first stdout line is the address the user reviews at. Then it keeps one connection open and prints every batch the
|
|
46
|
+
user sends as one event. Its only other stdout line is the one that ends the watch when the daemon stays unreachable.
|
|
47
|
+
When the monitor expires, re-arm it with `bunx agent-canvas review watch --follow --project <name>`, which does not
|
|
48
|
+
open the browser again. With the snippet instead of the proxy, use that command from the start.
|
|
49
|
+
|
|
50
|
+
**Other hosts**: run the one-shot watcher in the background. It prints one batch and exits; start it again after
|
|
51
|
+
answering:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
bunx agent-canvas review watch --project <name>
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
The output lists each thread with:
|
|
58
|
+
|
|
59
|
+
- `Page` — the URL the comment was made on, hash route included. Through the proxy it carries the proxy's port; the
|
|
60
|
+
path and the hash are the application's.
|
|
61
|
+
- `Where` — the page heading and the section or dialog around the element.
|
|
62
|
+
- `Element` — ARIA role and accessible name, the way you would target the element in a browser.
|
|
63
|
+
- `Selector` — a CSS path to the element.
|
|
64
|
+
- `Components` — React components that own the element, outermost first.
|
|
65
|
+
- `Source` — `file:line:column` where the element and its nearest components are written, innermost first. Start
|
|
66
|
+
there. It needs React 19 in development and source maps from the dev server; without them, search for the innermost
|
|
67
|
+
component instead.
|
|
68
|
+
- `Selected text` — when the user commented on a text selection.
|
|
69
|
+
- `Screenshot` — when the user attached one: a PNG of the element in its surroundings, the element outlined in red.
|
|
70
|
+
Read it; it shows what the user sees, which matters for visual comments.
|
|
71
|
+
- `Markup` — the element's HTML, trimmed: classes, attributes and structure, not whole subtrees.
|
|
72
|
+
- The conversation so far.
|
|
73
|
+
|
|
74
|
+
For each thread:
|
|
75
|
+
|
|
76
|
+
1. Make the change. The application's dev server reloads it; the user sees it in place.
|
|
77
|
+
2. Answer the thread:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
bunx agent-canvas review reply <thread-id> "<what you changed>" --resolve --project <name>
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Leave out `--resolve` when you ask a question back; the thread returns to the user.
|
|
84
|
+
|
|
85
|
+
With the one-shot watcher, start `review watch` again for the next batch.
|
|
86
|
+
|
|
87
|
+
If the user says they already sent comments and no watcher was running, take them with `review fetch`. `review list`
|
|
88
|
+
shows every thread that is not resolved, notes included.
|
|
@@ -9,9 +9,11 @@ Visual checklist. Read-only display (feedback via annotations).
|
|
|
9
9
|
## Usage
|
|
10
10
|
|
|
11
11
|
```jsx
|
|
12
|
-
<Checklist
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
12
|
+
<Checklist
|
|
13
|
+
items={[
|
|
14
|
+
{ label: 'Run migration on staging', checked: true },
|
|
15
|
+
{ label: 'Verify data integrity', checked: false },
|
|
16
|
+
{ label: 'Update API docs', checked: false },
|
|
17
|
+
]}
|
|
18
|
+
/>
|
|
17
19
|
```
|
|
@@ -6,23 +6,29 @@ When you use the full module format (`export default function Canvas()`), you ca
|
|
|
6
6
|
|
|
7
7
|
```jsx
|
|
8
8
|
function Metric({ label, value, trend }) {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
9
|
+
const color = trend === 'up'
|
|
10
|
+
? 'var(--color-accent-green)'
|
|
11
|
+
: trend === 'down'
|
|
12
|
+
? 'var(--color-accent-red)'
|
|
13
|
+
: 'var(--color-text-secondary)'
|
|
14
|
+
return (
|
|
15
|
+
<div style={{ display: 'flex', gap: 12, alignItems: 'baseline' }}>
|
|
16
|
+
<span style={{ color: 'var(--color-text-tertiary)', fontSize: '0.75rem' }}>
|
|
17
|
+
{label}
|
|
18
|
+
</span>
|
|
19
|
+
<span style={{ color, fontSize: '1.25rem', fontWeight: 600 }}>{value}</span>
|
|
20
|
+
</div>
|
|
21
|
+
)
|
|
16
22
|
}
|
|
17
23
|
|
|
18
24
|
export default function Canvas() {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
25
|
+
return (
|
|
26
|
+
<Section title="Performance Summary">
|
|
27
|
+
<Metric label="P95 Latency" value="42ms" trend="down" />
|
|
28
|
+
<Metric label="Error Rate" value="0.3%" trend="up" />
|
|
29
|
+
<Metric label="Throughput" value="1.2k rps" trend="up" />
|
|
30
|
+
</Section>
|
|
31
|
+
)
|
|
26
32
|
}
|
|
27
33
|
```
|
|
28
34
|
|
|
@@ -11,8 +11,9 @@ Shows a diff view between two code snippets.
|
|
|
11
11
|
## Usage
|
|
12
12
|
|
|
13
13
|
```jsx
|
|
14
|
-
<Diff
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
<Diff
|
|
15
|
+
language="typescript"
|
|
16
|
+
before={`function auth(req) {\n return true;\n}`}
|
|
17
|
+
after={`function auth(req) {\n const token = req.headers.authorization;\n if (!token) throw new Error('Unauthorized');\n return verify(token);\n}`}
|
|
17
18
|
/>
|
|
18
19
|
```
|
|
@@ -21,23 +21,33 @@ Previously called "Task" — Item is the generalized version.
|
|
|
21
21
|
```jsx
|
|
22
22
|
{/* As a plan task */}
|
|
23
23
|
<Item id="migrate-db" label="Run database migration" badge="todo">
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
Execute the migration script against staging first.
|
|
25
|
+
<CodeBlock language="bash">npm run migrate:staging</CodeBlock>
|
|
26
26
|
</Item>
|
|
27
27
|
|
|
28
28
|
{/* As a review finding */}
|
|
29
|
-
<Item
|
|
30
|
-
|
|
31
|
-
|
|
29
|
+
<Item
|
|
30
|
+
id="finding-1"
|
|
31
|
+
label="Missing null check in auth handler"
|
|
32
|
+
badge="critical"
|
|
33
|
+
badgeVariant="danger"
|
|
34
|
+
>
|
|
35
|
+
The handler doesn't check for missing tokens.
|
|
36
|
+
<FilePreview path="src/auth.ts" lines={[42, 48]} />
|
|
32
37
|
</Item>
|
|
33
38
|
|
|
34
39
|
{/* As a requirement */}
|
|
35
|
-
<Item
|
|
36
|
-
|
|
40
|
+
<Item
|
|
41
|
+
id="req-auth"
|
|
42
|
+
label="JWT authentication"
|
|
43
|
+
badge="must-have"
|
|
44
|
+
badgeVariant="danger"
|
|
45
|
+
>
|
|
46
|
+
All API endpoints must require a valid JWT token.
|
|
37
47
|
</Item>
|
|
38
48
|
|
|
39
49
|
{/* As an architecture component */}
|
|
40
50
|
<Item label="API Gateway" badge="core" badgeVariant="info">
|
|
41
|
-
|
|
51
|
+
Routes requests to downstream services. Handles rate limiting.
|
|
42
52
|
</Item>
|
|
43
53
|
```
|
|
@@ -5,7 +5,8 @@ Renders markdown content with full GFM support (headings, lists, tables, code bl
|
|
|
5
5
|
## Inline source — markdown as children
|
|
6
6
|
|
|
7
7
|
```jsx
|
|
8
|
-
<Markdown>
|
|
8
|
+
<Markdown>
|
|
9
|
+
{`
|
|
9
10
|
# Summary
|
|
10
11
|
|
|
11
12
|
This is **bold** and *italic*. Here's a list:
|
|
@@ -17,7 +18,8 @@ This is **bold** and *italic*. Here's a list:
|
|
|
17
18
|
\`\`\`typescript
|
|
18
19
|
const x = 42;
|
|
19
20
|
\`\`\`
|
|
20
|
-
`}
|
|
21
|
+
`}
|
|
22
|
+
</Markdown>
|
|
21
23
|
```
|
|
22
24
|
|
|
23
25
|
## File reference — loads file content at compile time
|