@fabrika/canvas-kernel 0.1.45 → 0.3.1

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.
Files changed (70) hide show
  1. package/README.md +33 -67
  2. package/index.ts +20 -83
  3. package/package.json +69 -72
  4. package/src/client/AnnotationProvider.tsx +218 -255
  5. package/src/client/AnnotationSidebar.tsx +393 -338
  6. package/src/client/FileIcon.tsx +105 -66
  7. package/src/client/PlanRenderer.tsx +607 -551
  8. package/src/client/RenderErrorContext.ts +7 -7
  9. package/src/client/ResponsePreview.tsx +192 -173
  10. package/src/client/annotationDom.ts +43 -97
  11. package/src/client/annotationDraft.ts +106 -96
  12. package/src/client/appContext.tsx +40 -40
  13. package/src/client/components/Callout.tsx +20 -20
  14. package/src/client/components/Checklist.tsx +27 -29
  15. package/src/client/components/Choice.tsx +178 -159
  16. package/src/client/components/CodeBlock.tsx +27 -26
  17. package/src/client/components/Diff.tsx +16 -17
  18. package/src/client/components/FilePreview.tsx +102 -74
  19. package/src/client/components/Image.tsx +41 -39
  20. package/src/client/components/Markdown.tsx +33 -32
  21. package/src/client/components/Mermaid.tsx +593 -542
  22. package/src/client/components/Note.tsx +8 -8
  23. package/src/client/components/Priority.tsx +10 -12
  24. package/src/client/components/ResponseNote.tsx +16 -15
  25. package/src/client/components/Section.tsx +28 -26
  26. package/src/client/components/Table.tsx +33 -30
  27. package/src/client/components/Task.tsx +58 -60
  28. package/src/client/components/UserInput.tsx +169 -152
  29. package/src/client/components/index.ts +16 -16
  30. package/src/client/components/renderMarkdownChildren.tsx +86 -68
  31. package/src/client/diffLines.ts +51 -39
  32. package/src/client/generateMarkdown.ts +197 -191
  33. package/src/client/hostContext.tsx +39 -34
  34. package/src/client/index.ts +44 -125
  35. package/src/client/pageGlobals.ts +25 -0
  36. package/src/client/persistenceSchemas.ts +41 -0
  37. package/src/client/preact-all.ts +4 -4
  38. package/src/client/runtime.ts +58 -111
  39. package/src/client/styles.css +1 -74
  40. package/src/engine/compiler.ts +411 -323
  41. package/src/engine/file-admission.ts +25 -0
  42. package/src/engine/paths.ts +44 -44
  43. package/src/engine/session.ts +515 -626
  44. package/src/engine/sessionSchemas.ts +71 -0
  45. package/src/engine/watcher.ts +70 -60
  46. package/src/engine/websocket.ts +134 -132
  47. package/annotate.ts +0 -94
  48. package/daemon.ts +0 -46
  49. package/src/client/AnnotationDraftFooter.tsx +0 -76
  50. package/src/client/AnnotationEditor.tsx +0 -320
  51. package/src/client/AnnotationList.tsx +0 -190
  52. package/src/client/Popover.tsx +0 -361
  53. package/src/client/SessionContext.tsx +0 -3
  54. package/src/client/annotationContext.ts +0 -130
  55. package/src/client/annotationMarkdown.ts +0 -102
  56. package/src/client/annotationTarget.ts +0 -73
  57. package/src/client/blockTarget.ts +0 -127
  58. package/src/client/highlightRange.ts +0 -262
  59. package/src/client/popoverPosition.ts +0 -46
  60. package/src/client/regionTarget.ts +0 -180
  61. package/src/client/testing/dom.ts +0 -74
  62. package/src/client/textTarget.ts +0 -31
  63. package/src/client/useRegionAnnotation.tsx +0 -188
  64. package/src/client/useTextAnnotation.tsx +0 -194
  65. package/src/client/utils.ts +0 -33
  66. package/src/engine/channel.ts +0 -278
  67. package/src/engine/http-utils.ts +0 -14
  68. package/src/engine/lifecycle.ts +0 -302
  69. package/src/engine/router.ts +0 -16
  70. package/src/engine/waiter.ts +0 -188
@@ -0,0 +1,71 @@
1
+ import { annotationAuthorSchema, annotationSchema } from '@fabrika/annotations/schemas'
2
+ import { z } from 'zod'
3
+
4
+ const diffStatsSchema = z.object({ added: z.number(), removed: z.number() }).passthrough()
5
+ const revisionFields = {
6
+ revision: z.number().int(),
7
+ label: z.string().optional(),
8
+ createdAt: z.string(),
9
+ hasFeedback: z.boolean(),
10
+ feedbackConsumed: z.boolean(),
11
+ response: z.string().optional(),
12
+ }
13
+ const revisionSchema = z.object({
14
+ ...revisionFields,
15
+ canvasFiles: z.array(z.object({ filename: z.string(), diffStats: diffStatsSchema.optional() }).passthrough()),
16
+ canvasScope: z.discriminatedUnion('kind', [
17
+ z.object({ kind: z.literal('view'), filename: z.string() }).passthrough(),
18
+ z.object({ kind: z.literal('directory') }).passthrough(),
19
+ ]).optional(),
20
+ }).passthrough()
21
+ const legacyRevisionSchema = z.object({
22
+ ...revisionFields,
23
+ canvasFiles: z.never().optional(),
24
+ sourceFile: z.string().optional(),
25
+ diffStats: diffStatsSchema.optional(),
26
+ }).passthrough()
27
+ const shareSchema = z.object({
28
+ shareId: z.string(),
29
+ url: z.string(),
30
+ revision: z.number().int(),
31
+ createdAt: z.string(),
32
+ ownerToken: z.string().optional(),
33
+ expiresAt: z.string().optional(),
34
+ lastFeedbackAt: z.string().optional(),
35
+ encryptionKey: z.string().optional(),
36
+ }).passthrough()
37
+ const metadataFields = {
38
+ projectRoot: z.string(),
39
+ createdAt: z.string(),
40
+ updatedAt: z.string(),
41
+ }
42
+ export const savedSessionMetadataSchema = z.object({
43
+ ...metadataFields,
44
+ currentRevision: z.number().int(),
45
+ revisions: z.array(z.union([revisionSchema, legacyRevisionSchema])),
46
+ shares: z.array(shareSchema).optional(),
47
+ }).passthrough().transform(meta => ({
48
+ migrated: meta.revisions.some(revision => revision.canvasFiles === undefined),
49
+ meta: {
50
+ ...meta,
51
+ revisions: meta.revisions.map(revision => {
52
+ if (revision.canvasFiles !== undefined) return revision
53
+ const { sourceFile, diffStats, canvasFiles: _canvasFiles, ...fields } = revision
54
+ return { ...fields, canvasFiles: [{ filename: sourceFile || 'plan.jsx', diffStats }] }
55
+ }),
56
+ },
57
+ }))
58
+
59
+ export const remoteFeedbackEntrySchema = z.object({
60
+ id: z.string(),
61
+ shareId: z.string(),
62
+ revision: z.number().int(),
63
+ submittedAt: z.string(),
64
+ author: annotationAuthorSchema,
65
+ annotations: z.array(annotationSchema.omit({ images: true, source: true, author: true }).optional().catch(undefined))
66
+ .transform(annotations => annotations.filter(annotation => annotation !== undefined)),
67
+ generalNote: z.string().optional(),
68
+ }).passthrough()
69
+
70
+ export const remoteFeedbackSchema = z.array(remoteFeedbackEntrySchema.optional().catch(undefined))
71
+ .transform(entries => entries.filter(entry => entry !== undefined)).catch([])
@@ -1,84 +1,94 @@
1
- import { watch, existsSync, readFileSync, readdirSync } from "fs";
2
- import { join } from "path";
3
- import { compileJsx, type CanvasComponents } from "./compiler";
4
- import type { SessionManager } from "./session";
1
+ import { existsSync, watch } from 'fs'
2
+ import { type CanvasComponents, compileJsx } from './compiler'
3
+ import { assertCanvasFileReadable, type AssertReadable, readCanvasFile } from './file-admission'
4
+ import type { SessionManager } from './session'
5
5
 
6
- type BroadcastFn = (sessionId: string) => void;
6
+ type BroadcastFn = (sessionId: string) => void
7
7
 
8
8
  export interface WatchOptions {
9
- /** Scratch dir for recompiles, from `createCanvasPaths()`. */
10
- tempDir?: string;
11
- /** Host components, same value the push path compiles with — a recompile
12
- * that dropped them would fail on canvases the original push accepted. */
13
- components?: CanvasComponents;
9
+ assertReadable?: AssertReadable
10
+ /** Scratch dir for recompiles, from `createCanvasPaths()`. */
11
+ tempDir?: string
12
+ /** Host components, same value the push path compiles with — a recompile
13
+ * that dropped them would fail on canvases the original push accepted. */
14
+ components?: CanvasComponents
14
15
  }
15
16
 
16
- const watchers = new Map<string, ReturnType<typeof watch>[]>();
17
+ const watchers = new Map<string, ReturnType<typeof watch>[]>()
17
18
 
18
19
  export function watchSession(
19
- sessionId: string,
20
- sessionManager: SessionManager,
21
- broadcast: BroadcastFn,
22
- options: WatchOptions = {},
20
+ sessionId: string,
21
+ sessionManager: SessionManager,
22
+ broadcast: BroadcastFn,
23
+ options: WatchOptions = {},
23
24
  ) {
24
- // Close existing watchers for this session
25
- unwatchSession(sessionId);
25
+ // Close existing watchers for this session
26
+ unwatchSession(sessionId)
26
27
 
27
- const session = sessionManager.get(sessionId);
28
- if (!session) return;
28
+ const session = sessionManager.get(sessionId)
29
+ if (!session) return
29
30
 
30
- const rev = session.currentRevision;
31
- const canvasFiles = sessionManager.getRevisionCanvasFiles(sessionId, rev);
32
- if (canvasFiles.length === 0) return;
31
+ const rev = session.currentRevision
32
+ const canvasFiles = sessionManager.getRevisionCanvasFiles(sessionId, rev)
33
+ if (canvasFiles.length === 0) return
33
34
 
34
- const fileWatchers: ReturnType<typeof watch>[] = [];
35
- const debounceTimers = new Map<string, ReturnType<typeof setTimeout>>();
35
+ const fileWatchers: ReturnType<typeof watch>[] = []
36
+ const debounceTimers = new Map<string, ReturnType<typeof setTimeout>>()
37
+ const sources: { filename: string; filePath: string }[] = []
36
38
 
37
- for (const filename of canvasFiles) {
38
- const filePath = sessionManager.getRevisionJsxPath(sessionId, rev, filename);
39
- if (!existsSync(filePath)) continue;
39
+ for (const filename of canvasFiles) {
40
+ const filePath = sessionManager.getRevisionJsxPath(sessionId, rev, filename)
41
+ if (!existsSync(filePath)) continue
42
+ assertCanvasFileReadable(filePath, options.assertReadable)
43
+ sources.push({ filename, filePath })
44
+ }
40
45
 
41
- const watcher = watch(filePath, async (event) => {
42
- if (event !== "change") return;
46
+ for (const { filename, filePath } of sources) {
47
+ const watcher = watch(filePath, async (event) => {
48
+ if (event !== 'change') return
43
49
 
44
- // Debounce per-file
45
- const existing = debounceTimers.get(filename);
46
- if (existing) clearTimeout(existing);
50
+ // Debounce per-file
51
+ const existing = debounceTimers.get(filename)
52
+ if (existing) clearTimeout(existing)
47
53
 
48
- debounceTimers.set(filename, setTimeout(async () => {
49
- debounceTimers.delete(filename);
50
- try {
51
- const jsx = readFileSync(filePath, "utf-8");
52
- const result = await compileJsx(jsx, {
53
- projectRoot: session.projectRoot,
54
- tempDir: options.tempDir,
55
- ...(options.components ? { components: options.components } : {}),
56
- });
57
- if (result.ok) {
58
- sessionManager.saveCompiled(sessionId, filename, result.js, rev);
59
- broadcast(sessionId);
60
- }
61
- } catch {}
62
- }, 200));
63
- });
54
+ debounceTimers.set(
55
+ filename,
56
+ setTimeout(async () => {
57
+ debounceTimers.delete(filename)
58
+ try {
59
+ const jsx = readCanvasFile(filePath, options.assertReadable)
60
+ const result = await compileJsx(jsx, {
61
+ projectRoot: session.projectRoot,
62
+ tempDir: options.tempDir,
63
+ assertReadable: options.assertReadable,
64
+ ...(options.components ? { components: options.components } : {}),
65
+ })
66
+ if (result.ok) {
67
+ sessionManager.saveCompiled(sessionId, filename, result.js, rev)
68
+ broadcast(sessionId)
69
+ }
70
+ } catch {}
71
+ }, 200),
72
+ )
73
+ })
64
74
 
65
- fileWatchers.push(watcher);
66
- }
75
+ fileWatchers.push(watcher)
76
+ }
67
77
 
68
- watchers.set(sessionId, fileWatchers);
78
+ watchers.set(sessionId, fileWatchers)
69
79
  }
70
80
 
71
81
  export function unwatchSession(sessionId: string) {
72
- const sessionWatchers = watchers.get(sessionId);
73
- if (sessionWatchers) {
74
- for (const w of sessionWatchers) w.close();
75
- watchers.delete(sessionId);
76
- }
82
+ const sessionWatchers = watchers.get(sessionId)
83
+ if (sessionWatchers) {
84
+ for (const w of sessionWatchers) w.close()
85
+ watchers.delete(sessionId)
86
+ }
77
87
  }
78
88
 
79
89
  export function unwatchAll() {
80
- for (const [, sessionWatchers] of watchers) {
81
- for (const w of sessionWatchers) w.close();
82
- }
83
- watchers.clear();
90
+ for (const [, sessionWatchers] of watchers) {
91
+ for (const w of sessionWatchers) w.close()
92
+ }
93
+ watchers.clear()
84
94
  }
@@ -4,156 +4,158 @@
4
4
  * so a delivery closes it.
5
5
  */
6
6
 
7
- import { browserFrame, createNotificationChannel, type ChannelSocket, type TopicChannel } from "./channel";
8
- import type { SessionManager, RemoteFeedbackEntry } from "./session";
7
+ import { type ChannelSocket, createNotificationChannel, type TopicChannel } from '@fabrika/daemon-kit'
8
+ import { z } from 'zod'
9
+ import type { RemoteFeedbackEntry, SessionManager } from './session'
9
10
 
10
- export type WSData = { type: "browser" | "wait"; sessionId: string };
11
+ export type WSData = { type: 'browser' | 'wait'; sessionId: string }
11
12
 
12
13
  export interface CanvasSocket extends ChannelSocket {
13
- data: WSData;
14
+ data: WSData
14
15
  }
15
16
 
16
17
  interface CanvasRenderError {
17
- revision: number;
18
- filename: string;
19
- message: string;
20
- stack?: string;
21
- componentStack?: string;
18
+ revision: number
19
+ filename: string
20
+ message: string
21
+ stack?: string
22
+ componentStack?: string
22
23
  }
23
24
 
24
25
  interface SubmitFrame {
25
- type: "submit";
26
- feedback: string;
26
+ type: 'submit'
27
+ feedback: string
27
28
  }
28
29
 
29
30
  interface RenderErrorFrame {
30
- type: "render-error";
31
- error: CanvasRenderError;
31
+ type: 'render-error'
32
+ error: CanvasRenderError
32
33
  }
33
34
 
34
- function isRecord(value: unknown): value is Record<string, unknown> {
35
- return typeof value === "object" && value !== null && !Array.isArray(value);
36
- }
37
-
38
- function isCanvasRenderError(value: unknown): value is CanvasRenderError {
39
- return typeof value === "object" && value !== null
40
- && "revision" in value && Number.isInteger(value.revision)
41
- && "filename" in value && typeof value.filename === "string"
42
- && "message" in value && typeof value.message === "string"
43
- && (!("stack" in value) || value.stack === undefined || typeof value.stack === "string")
44
- && (!("componentStack" in value) || value.componentStack === undefined || typeof value.componentStack === "string");
45
- }
46
-
47
- function isSubmitFrame(value: unknown): value is SubmitFrame {
48
- return isRecord(value) && value.type === "submit" && typeof value.feedback === "string";
49
- }
50
-
51
- function isRenderErrorFrame(value: unknown): value is RenderErrorFrame {
52
- return isRecord(value) && value.type === "render-error" && isCanvasRenderError(value.error);
53
- }
35
+ const browserFrameSchema = z.discriminatedUnion('type', [
36
+ z.object({ type: z.literal('submit'), feedback: z.string() }).passthrough(),
37
+ z.object({
38
+ type: z.literal('render-error'),
39
+ error: z.object({
40
+ revision: z.number().int(),
41
+ filename: z.string(),
42
+ message: z.string(),
43
+ stack: z.string().optional(),
44
+ componentStack: z.string().optional(),
45
+ }).passthrough(),
46
+ }).passthrough(),
47
+ ])
54
48
 
55
49
  export interface WebSocketManagerOptions {
56
- /**
57
- * Who marks feedback consumed once a waiting client has been handed it.
58
- *
59
- * `"on-delivery"` (default) clears it in the same step, so the same feedback
60
- * can never be delivered twice — right when the waiting client *is* the
61
- * consumer. `"external"` leaves it pending for a separate gate to claim
62
- * through the consume endpoint, for hosts where delivery and consumption are
63
- * different steps.
64
- */
65
- feedbackConsumption?: "on-delivery" | "external";
50
+ /**
51
+ * Who marks feedback consumed once a waiting client has been handed it.
52
+ *
53
+ * `"on-delivery"` (default) clears it in the same step, so the same feedback
54
+ * can never be delivered twice — right when the waiting client *is* the
55
+ * consumer. `"external"` leaves it pending for a separate gate to claim
56
+ * through the consume endpoint, for hosts where delivery and consumption are
57
+ * different steps.
58
+ */
59
+ feedbackConsumption?: 'on-delivery' | 'external'
66
60
  }
67
61
 
68
62
  export function createWebSocketManager(
69
- sessionManager: SessionManager,
70
- options: WebSocketManagerOptions = {},
63
+ sessionManager: SessionManager,
64
+ options: WebSocketManagerOptions = {},
71
65
  ) {
72
- const consumeOnDelivery = (options.feedbackConsumption ?? "on-delivery") === "on-delivery";
73
- const pendingRenderErrors = new Map<string, CanvasRenderError>();
74
-
75
- /** Hands a held error to the waiters, if any showed up; the caller announces the departure. */
76
- function deliverPendingRenderError(sessionId: string): boolean {
77
- const error = pendingRenderErrors.get(sessionId);
78
- if (!error) return false;
79
- if (!channel.relayToWaiters(sessionId, { type: "render-error", error })) return false;
80
- pendingRenderErrors.delete(sessionId);
81
- return true;
82
- }
83
-
84
- function submit(frame: SubmitFrame, topic: TopicChannel) {
85
- const sessionId = topic.topic;
86
- const session = sessionManager.get(sessionId);
87
- if (!session) return;
88
-
89
- sessionManager.saveFeedback(sessionId, session.currentRevision, frame.feedback);
90
- broadcastRevisionUpdate(sessionId);
91
- if (!topic.hasWaiter()) return;
92
-
93
- if (consumeOnDelivery) {
94
- sessionManager.consumeFeedback(sessionId, session.currentRevision);
95
- broadcastRevisionUpdate(sessionId);
96
- }
97
- topic.relayToWaiters({ type: "submit", feedback: frame.feedback });
98
- }
99
-
100
- function renderError(frame: RenderErrorFrame, topic: TopicChannel) {
101
- const sessionId = topic.topic;
102
- const session = sessionManager.get(sessionId);
103
- if (!session) return;
104
- if (frame.error.revision !== session.currentRevision) return;
105
- if (!session.canvasFiles.includes(frame.error.filename)) return;
106
-
107
- pendingRenderErrors.set(sessionId, frame.error);
108
- if (deliverPendingRenderError(sessionId)) topic.broadcastPresence();
109
- }
110
-
111
- const channel = createNotificationChannel<CanvasSocket>({
112
- roleOf: (socket) => socket.data.type,
113
- topicOf: (socket) => socket.data.sessionId,
114
- waiterPolicy: "close-on-delivery",
115
- presenceFrame: (watching) => ({ type: "watcher-status", watching }),
116
- // A render error raised before the CLI watcher connected is held for it.
117
- onWaiterOpen: (topic) => { deliverPendingRenderError(topic.topic); },
118
- frames: [
119
- browserFrame(isSubmitFrame, submit),
120
- browserFrame(isRenderErrorFrame, renderError),
121
- ],
122
- });
123
-
124
- function broadcastPlanUpdate(sessionId: string) {
125
- pendingRenderErrors.delete(sessionId);
126
- const session = sessionManager.get(sessionId);
127
- if (!session) return;
128
- channel.broadcast(sessionId, {
129
- type: "plan-updated",
130
- currentRevision: session.currentRevision,
131
- revisions: session.revisions,
132
- });
133
- }
134
-
135
- function broadcastRevisionUpdate(sessionId: string) {
136
- const session = sessionManager.get(sessionId);
137
- if (!session) return;
138
- channel.broadcast(sessionId, {
139
- type: "revision-updated",
140
- revisions: session.revisions,
141
- });
142
- }
143
-
144
- function broadcastRemoteFeedback(sessionId: string, revision: number, entries: RemoteFeedbackEntry[]) {
145
- channel.broadcast(sessionId, { type: "remote-feedback", revision, entries });
146
- }
147
-
148
- function broadcastWatcherStatus(sessionId: string) {
149
- channel.broadcastPresence(sessionId);
150
- }
151
-
152
- return {
153
- handlers: channel.handlers,
154
- broadcastPlanUpdate,
155
- broadcastRevisionUpdate,
156
- broadcastWatcherStatus,
157
- broadcastRemoteFeedback,
158
- };
66
+ const consumeOnDelivery = (options.feedbackConsumption ?? 'on-delivery') === 'on-delivery'
67
+ const pendingRenderErrors = new Map<string, CanvasRenderError>()
68
+
69
+ /** Hands a held error to the waiters, if any showed up; the caller announces the departure. */
70
+ function deliverPendingRenderError(sessionId: string): boolean {
71
+ const error = pendingRenderErrors.get(sessionId)
72
+ if (!error) return false
73
+ if (!channel.relayToWaiters(sessionId, { type: 'render-error', error })) return false
74
+ pendingRenderErrors.delete(sessionId)
75
+ return true
76
+ }
77
+
78
+ function submit(frame: SubmitFrame, topic: TopicChannel) {
79
+ const sessionId = topic.topic
80
+ const session = sessionManager.get(sessionId)
81
+ if (!session) return
82
+
83
+ sessionManager.saveFeedback(sessionId, session.currentRevision, frame.feedback)
84
+ broadcastRevisionUpdate(sessionId)
85
+ if (!topic.hasWaiter()) return
86
+
87
+ if (consumeOnDelivery) {
88
+ sessionManager.consumeFeedback(sessionId, session.currentRevision)
89
+ broadcastRevisionUpdate(sessionId)
90
+ }
91
+ topic.relayToWaiters({ type: 'submit', feedback: frame.feedback })
92
+ }
93
+
94
+ function renderError(frame: RenderErrorFrame, topic: TopicChannel) {
95
+ const sessionId = topic.topic
96
+ const session = sessionManager.get(sessionId)
97
+ if (!session) return
98
+ if (frame.error.revision !== session.currentRevision) return
99
+ if (!session.canvasFiles.includes(frame.error.filename)) return
100
+
101
+ pendingRenderErrors.set(sessionId, frame.error)
102
+ if (deliverPendingRenderError(sessionId)) topic.broadcastPresence()
103
+ }
104
+
105
+ const channel = createNotificationChannel<CanvasSocket>({
106
+ roleOf: (socket) => socket.data.type,
107
+ topicOf: (socket) => socket.data.sessionId,
108
+ waiterPolicy: 'close-on-delivery',
109
+ presenceFrame: (watching) => ({ type: 'watcher-status', watching }),
110
+ // A render error raised before the CLI watcher connected is held for it.
111
+ onWaiterOpen: (topic) => {
112
+ deliverPendingRenderError(topic.topic)
113
+ },
114
+ frames: [
115
+ {
116
+ bind(frame, topic) {
117
+ const parsed = browserFrameSchema.safeParse(frame)
118
+ if (!parsed.success) return undefined
119
+ const data = parsed.data
120
+ return () => data.type === 'submit' ? submit(data, topic) : renderError(data, topic)
121
+ },
122
+ },
123
+ ],
124
+ })
125
+
126
+ function broadcastPlanUpdate(sessionId: string) {
127
+ pendingRenderErrors.delete(sessionId)
128
+ const session = sessionManager.get(sessionId)
129
+ if (!session) return
130
+ channel.broadcast(sessionId, {
131
+ type: 'plan-updated',
132
+ currentRevision: session.currentRevision,
133
+ revisions: session.revisions,
134
+ })
135
+ }
136
+
137
+ function broadcastRevisionUpdate(sessionId: string) {
138
+ const session = sessionManager.get(sessionId)
139
+ if (!session) return
140
+ channel.broadcast(sessionId, {
141
+ type: 'revision-updated',
142
+ revisions: session.revisions,
143
+ })
144
+ }
145
+
146
+ function broadcastRemoteFeedback(sessionId: string, revision: number, entries: RemoteFeedbackEntry[]) {
147
+ channel.broadcast(sessionId, { type: 'remote-feedback', revision, entries })
148
+ }
149
+
150
+ function broadcastWatcherStatus(sessionId: string) {
151
+ channel.broadcastPresence(sessionId)
152
+ }
153
+
154
+ return {
155
+ handlers: channel.handlers,
156
+ broadcastPlanUpdate,
157
+ broadcastRevisionUpdate,
158
+ broadcastWatcherStatus,
159
+ broadcastRemoteFeedback,
160
+ }
159
161
  }
package/annotate.ts DELETED
@@ -1,94 +0,0 @@
1
- // @fabrika/canvas-kernel/annotate — the annotation surface.
2
- //
3
- // Ring 2: collecting a reader's notes on something, and getting them back out
4
- // as markdown an agent can act on. What a note points at is a locator strategy
5
- // (text, block, image region), so a host annotates its own kind of thing by
6
- // writing one target; the list, the editor, the draft and the markdown do not
7
- // change. Grouping, headings and navigation stay with the host — this ring has
8
- // no taxonomy of its own.
9
- //
10
- // Not yet a severed dependency: `AnnotationProvider` still imports the canvas
11
- // response-pruning from `generateMarkdown`, so a bundle built off this entry
12
- // carries that along. The export list is the API statement; the import graph
13
- // has one edge left to cut.
14
-
15
- // --- Annotation state ------------------------------------------------------
16
- export { AnnotationProvider } from "./src/client/AnnotationProvider";
17
- export type { PersistedState } from "./src/client/AnnotationProvider";
18
- export { useAnnotations, AnnotationCtx, SessionContext } from "./src/client/runtime";
19
- export type {
20
- Annotation,
21
- AnnotationAttachment,
22
- AnnotationAuthor,
23
- AnnotationContext,
24
- AnnotationContextValue,
25
- } from "./src/client/runtime";
26
- export {
27
- annotationDraftKey,
28
- carryUnsubmittedDraft,
29
- clearPersistedDraft,
30
- } from "./src/client/annotationDraft";
31
- export type { AnnotationDraftPhase } from "./src/client/annotationDraft";
32
-
33
- // --- Surface ---------------------------------------------------------------
34
- export { AnnotationList } from "./src/client/AnnotationList";
35
- export type { AnnotationListProps } from "./src/client/AnnotationList";
36
- export { AnnotationDraftFooter } from "./src/client/AnnotationDraftFooter";
37
- export type { AnnotationDraftFooterProps } from "./src/client/AnnotationDraftFooter";
38
- export { AnnotationEditor, imageToUrl } from "./src/client/AnnotationEditor";
39
- export { Popover, AnnotationPopover, AnnotationCreatePopover, AnnotationEditPopover } from "./src/client/Popover";
40
-
41
- // --- Drawing ---------------------------------------------------------------
42
- export { useTextAnnotation } from "./src/client/useTextAnnotation";
43
- export { useRegionAnnotation } from "./src/client/useRegionAnnotation";
44
-
45
- // --- Locator strategies ----------------------------------------------------
46
- export { sealTarget } from "./src/client/annotationTarget";
47
- export type { AnnotationTarget, SealedAnnotationTarget, TargetAnnotation } from "./src/client/annotationTarget";
48
- export { blockTarget } from "./src/client/blockTarget";
49
- export type { BlockLocator } from "./src/client/blockTarget";
50
- export { textTarget } from "./src/client/textTarget";
51
- export type { TextLocator } from "./src/client/textTarget";
52
- export {
53
- createRegionOverlay,
54
- describeRegion,
55
- findRegionHost,
56
- isDrawableRegion,
57
- regionBetween,
58
- regionPointIn,
59
- regionStyle,
60
- regionTarget,
61
- REGION_HOST_ATTR,
62
- REGION_UNITS,
63
- } from "./src/client/regionTarget";
64
- export type { RegionBox, RegionLocator, RegionPoint, RegionShape } from "./src/client/regionTarget";
65
- export {
66
- ANNOTATION_TARGETS,
67
- ANNOTATABLE_SELECTOR,
68
- BLOCK_SELECTOR,
69
- describeSnippet,
70
- findAnnotationElement,
71
- findSnippetElement,
72
- getBlockSnippet,
73
- restoreAnnotationTargets,
74
- } from "./src/client/annotationDom";
75
-
76
- // --- Marks -----------------------------------------------------------------
77
- export {
78
- restoreMarks,
79
- renameMarkId,
80
- setMarkActive,
81
- unwrapMarks,
82
- updateAllMarkStates,
83
- wrapRangeWithMark,
84
- } from "./src/client/highlightRange";
85
-
86
- // --- Feedback out ----------------------------------------------------------
87
- export { renderAnnotation } from "./src/client/annotationMarkdown";
88
- export type { RenderableAnnotation } from "./src/client/annotationMarkdown";
89
- export { extractContext, formatSnippetInContext } from "./src/client/annotationContext";
90
-
91
- // --- Host-provided context -------------------------------------------------
92
- export { CanvasHostContext, localCanvasHost, useCanvasHost } from "./src/client/hostContext";
93
- export type { CanvasHost } from "./src/client/hostContext";
94
- export { autoResizeTextarea, fileAnnotationPath, generateAnnotationId, hostAcceptsUploads } from "./src/client/utils";