agent-canvas 0.8.1 → 0.8.2
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/daemon/dist/client.css +1 -1
- package/daemon/dist/review.js +33 -3
- package/daemon/review-widget/anchor.ts +21 -4
- package/daemon/review-widget/browser.ts +46 -0
- package/daemon/review-widget/client.ts +7 -0
- package/daemon/review-widget/source.ts +43 -7
- package/daemon/review-widget/style.ts +51 -0
- package/daemon/review-widget/styles.css +30 -0
- package/daemon/review-widget/tsconfig.json +2 -1
- package/daemon/review-widget/widget.tsx +120 -16
- package/daemon/src/review/markdown.ts +6 -1
- package/daemon/src/review/model.ts +18 -1
- package/daemon/src/review/proxy.ts +30 -3
- package/daemon/src/review/server.ts +14 -0
- package/daemon/src/review/store.ts +11 -0
- package/daemon/src/server.ts +1 -1
- package/daemon/tsconfig.json +1 -1
- package/package.json +4 -4
|
@@ -1,5 +1,22 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
1
2
|
import type { ReviewStore } from './store'
|
|
2
3
|
|
|
4
|
+
/** Vite's hot reload message that makes its client reload the page. */
|
|
5
|
+
const viteFullReloadSchema = z.object({ type: z.literal('full-reload'), path: z.string().default('*') })
|
|
6
|
+
|
|
7
|
+
/** The page path a Vite full reload names, or `undefined` for any other frame. */
|
|
8
|
+
export function viteFullReloadPath(frame: string): string | undefined {
|
|
9
|
+
if (!frame.includes('full-reload')) return undefined
|
|
10
|
+
let message: unknown
|
|
11
|
+
try {
|
|
12
|
+
message = JSON.parse(frame)
|
|
13
|
+
} catch {
|
|
14
|
+
return undefined
|
|
15
|
+
}
|
|
16
|
+
const parsed = viteFullReloadSchema.safeParse(message)
|
|
17
|
+
return parsed.success ? parsed.data.path : undefined
|
|
18
|
+
}
|
|
19
|
+
|
|
3
20
|
/** One browser socket proxied to the application, e.g. its dev server's hot reload. */
|
|
4
21
|
interface ProxySocketData {
|
|
5
22
|
upstream: WebSocket
|
|
@@ -38,7 +55,15 @@ function connectUpstream(url: URL, protocols: string[]): Promise<WebSocket> {
|
|
|
38
55
|
export class ReviewProxies {
|
|
39
56
|
private readonly running = new Map<string, RunningProxy>()
|
|
40
57
|
|
|
41
|
-
|
|
58
|
+
/**
|
|
59
|
+
* `onFullReload` receives the full reloads the application's dev server asks for: the proxy holds them back from
|
|
60
|
+
* the page, so a reload does not take the comment the reviewer is writing.
|
|
61
|
+
*/
|
|
62
|
+
constructor(
|
|
63
|
+
private readonly store: ReviewStore,
|
|
64
|
+
private readonly widgetScript: string,
|
|
65
|
+
private readonly onFullReload: (project: string, path: string) => void,
|
|
66
|
+
) {}
|
|
42
67
|
|
|
43
68
|
/** Serves the project's application at `target` and returns where the reviewer opens it, `target`'s path kept. */
|
|
44
69
|
open(project: string, target: string): string {
|
|
@@ -94,7 +119,7 @@ export class ReviewProxies {
|
|
|
94
119
|
const upstreamUrl = new URL(`${url.pathname}${url.search}`, target)
|
|
95
120
|
if (req.headers.get('Upgrade')?.toLowerCase() === 'websocket') {
|
|
96
121
|
upstreamUrl.protocol = upstreamUrl.protocol === 'https:' ? 'wss:' : 'ws:'
|
|
97
|
-
return await this.upgrade(req, server, upstreamUrl)
|
|
122
|
+
return await this.upgrade(project, req, server, upstreamUrl)
|
|
98
123
|
}
|
|
99
124
|
return await forward(req, upstreamUrl, url.origin, widgetTag)
|
|
100
125
|
},
|
|
@@ -116,7 +141,7 @@ export class ReviewProxies {
|
|
|
116
141
|
}
|
|
117
142
|
|
|
118
143
|
/** Connects to the application first, so the browser gets the subprotocol the application chose. */
|
|
119
|
-
private async upgrade(req: Request, server: ProxyServer, upstreamUrl: URL): Promise<Response | undefined> {
|
|
144
|
+
private async upgrade(project: string, req: Request, server: ProxyServer, upstreamUrl: URL): Promise<Response | undefined> {
|
|
120
145
|
const protocols = (req.headers.get('Sec-WebSocket-Protocol') ?? '').split(',').map((protocol) => protocol.trim()).filter((protocol) =>
|
|
121
146
|
protocol !== ''
|
|
122
147
|
)
|
|
@@ -129,6 +154,8 @@ export class ReviewProxies {
|
|
|
129
154
|
const data: ProxySocketData = { upstream, early: [], browser: null }
|
|
130
155
|
upstream.onmessage = (event) => {
|
|
131
156
|
if (typeof event.data !== 'string' && !(event.data instanceof ArrayBuffer)) return
|
|
157
|
+
const reloadPath = typeof event.data === 'string' ? viteFullReloadPath(event.data) : undefined
|
|
158
|
+
if (reloadPath !== undefined) return this.onFullReload(project, reloadPath)
|
|
132
159
|
if (data.browser === null) data.early.push(event.data)
|
|
133
160
|
else data.browser.send(event.data)
|
|
134
161
|
}
|
|
@@ -130,6 +130,15 @@ export function createReviewServer(store: ReviewStore, proxies: ReviewProxies) {
|
|
|
130
130
|
notifyAgent(project)
|
|
131
131
|
return { thread }
|
|
132
132
|
}),
|
|
133
|
+
route('POST', '/api/review/:project/notes/send', 'browser', async (_req, match) => {
|
|
134
|
+
const project = projectOf(match)
|
|
135
|
+
const threads = store.sendNotes(project)
|
|
136
|
+
if (threads.length > 0) {
|
|
137
|
+
publish(project)
|
|
138
|
+
notifyAgent(project)
|
|
139
|
+
}
|
|
140
|
+
return { threads }
|
|
141
|
+
}),
|
|
133
142
|
route('POST', '/api/review/:project/threads/:thread/resolve', 'browser', async (_req, match) => {
|
|
134
143
|
const project = projectOf(match)
|
|
135
144
|
const thread = store.setResolved(project, threadIdOf(match), true)
|
|
@@ -170,6 +179,11 @@ export function createReviewServer(store: ReviewStore, proxies: ReviewProxies) {
|
|
|
170
179
|
|
|
171
180
|
return {
|
|
172
181
|
channel,
|
|
182
|
+
/** Hands a full reload the application asked for to the project's widgets, which reload unless the reviewer is writing. */
|
|
183
|
+
fullReload(project: string, path: string): void {
|
|
184
|
+
const frame: ReviewBrowserFrame = { type: 'reload', path }
|
|
185
|
+
channel.broadcast(project, frame)
|
|
186
|
+
},
|
|
173
187
|
/** Answers the request when it is review mode's: CORS preflight, a route, or a WebSocket upgrade. */
|
|
174
188
|
fetch(
|
|
175
189
|
req: Request,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'fs'
|
|
2
2
|
import { join } from 'path'
|
|
3
3
|
import {
|
|
4
|
+
isUnsentNote,
|
|
4
5
|
PROJECT_RE,
|
|
5
6
|
type ReviewAnchor,
|
|
6
7
|
type ReviewAuthor,
|
|
@@ -61,6 +62,16 @@ export class ReviewStore {
|
|
|
61
62
|
})
|
|
62
63
|
}
|
|
63
64
|
|
|
65
|
+
/** Sends every unsent note at once, so the agent picks them up as one batch. */
|
|
66
|
+
sendNotes(project: string): readonly ReviewThread[] {
|
|
67
|
+
const threads = this.list(project)
|
|
68
|
+
const updatedAt = this.timestamp()
|
|
69
|
+
const sent = threads.filter(isUnsentNote).map((thread): ReviewThread => ({ ...thread, state: 'queued', updatedAt }))
|
|
70
|
+
const sentById = new Map(sent.map((thread) => [thread.id, thread]))
|
|
71
|
+
this.save(project, threads.map((thread) => sentById.get(thread.id) ?? thread))
|
|
72
|
+
return sent
|
|
73
|
+
}
|
|
74
|
+
|
|
64
75
|
setResolved(project: string, threadId: string, resolved: boolean): ReviewThread {
|
|
65
76
|
return this.update(project, threadId, (thread) => ({ ...thread, state: resolved ? 'resolved' : 'open', updatedAt: this.timestamp() }))
|
|
66
77
|
}
|
package/daemon/src/server.ts
CHANGED
|
@@ -27,7 +27,7 @@ chmodSync(CLI_AUTH_FILE, 0o600)
|
|
|
27
27
|
const sessionManager = new SessionManager(SESSIONS_DIR)
|
|
28
28
|
const wsManager = createWebSocketManager(sessionManager)
|
|
29
29
|
const reviewStore = new ReviewStore(REVIEWS_DIR)
|
|
30
|
-
const reviewProxies = new ReviewProxies(reviewStore, `http://localhost:${PORT}/assets/review.js
|
|
30
|
+
const reviewProxies = new ReviewProxies(reviewStore, `http://localhost:${PORT}/assets/review.js`, (project, path) => review.fullReload(project, path))
|
|
31
31
|
const review = createReviewServer(reviewStore, reviewProxies)
|
|
32
32
|
|
|
33
33
|
type SocketData = WSData | ReviewSocketData
|
package/daemon/tsconfig.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-canvas",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.2",
|
|
4
4
|
"description": "Interactive visual canvas for Claude Code — rich annotatable documents in the browser",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -30,9 +30,9 @@
|
|
|
30
30
|
"test": "bun test"
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"@fabrika/annotations": "0.8.
|
|
34
|
-
"@fabrika/canvas-kernel": "0.8.
|
|
35
|
-
"@fabrika/daemon-kit": "0.8.
|
|
33
|
+
"@fabrika/annotations": "0.8.2",
|
|
34
|
+
"@fabrika/canvas-kernel": "0.8.2",
|
|
35
|
+
"@fabrika/daemon-kit": "0.8.2",
|
|
36
36
|
"@jridgewell/trace-mapping": "^0.3.31",
|
|
37
37
|
"marked": "^17.0.4",
|
|
38
38
|
"mermaid-parser-bundle": "^0.1.2",
|