agent-canvas 0.1.44 → 0.2.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.
Files changed (115) hide show
  1. package/README.md +14 -0
  2. package/bin/lib/commands/daemon.ts +3 -6
  3. package/bin/lib/commands/push.ts +1 -1
  4. package/bin/lib/daemon-lifecycle.ts +65 -72
  5. package/bin/lib/helpers.test.ts +1 -0
  6. package/bin/lib/helpers.ts +30 -74
  7. package/daemon/build.ts +37 -16
  8. package/daemon/client/App.tsx +27 -61
  9. package/daemon/client/CompareView.tsx +4 -4
  10. package/daemon/client/FileBrowser.tsx +4 -4
  11. package/daemon/client/FileViewer.tsx +4 -4
  12. package/daemon/client/OverviewView.tsx +2 -2
  13. package/daemon/client/RevisionSelect.tsx +1 -1
  14. package/daemon/client/canvas-runtime.ts +1 -0
  15. package/daemon/client/clientApi.ts +18 -0
  16. package/daemon/client/components/index.ts +3 -16
  17. package/daemon/client/styles.css +4 -457
  18. package/daemon/dist/annotations-runtime.js +1 -0
  19. package/daemon/dist/client.css +1 -1
  20. package/daemon/dist/client.js +76 -76
  21. package/daemon/dist/components.js +59 -59
  22. package/daemon/dist/index.html +1 -0
  23. package/daemon/dist/preact-compat.js +1 -1
  24. package/daemon/dist/runtime.js +1 -1
  25. package/daemon/src/components.ts +13 -0
  26. package/daemon/src/handlers/api.ts +13 -9
  27. package/daemon/src/handlers/files.ts +2 -3
  28. package/daemon/src/handlers/static.ts +1 -1
  29. package/daemon/src/handlers/uploads.ts +2 -3
  30. package/daemon/src/paths.ts +10 -24
  31. package/daemon/src/remote-feedback.test.ts +1 -1
  32. package/daemon/src/remote-feedback.ts +1 -1
  33. package/daemon/src/secrets-api.test.ts +2 -2
  34. package/daemon/src/server.ts +11 -10
  35. package/daemon/src/share-e2e.test.ts +1 -1
  36. package/daemon/src/share.test.ts +1 -1
  37. package/daemon/src/share.ts +1 -1
  38. package/daemon/tsconfig.json +1 -3
  39. package/package.json +11 -4
  40. package/.github/dark.png +0 -0
  41. package/.github/light.png +0 -0
  42. package/.github/workflows/ci.yml +0 -48
  43. package/.github/workflows/deploy-worker-prod.yml +0 -19
  44. package/.github/workflows/deploy-worker-stage.yml +0 -21
  45. package/.github/workflows/deploy-worker.yml +0 -72
  46. package/.github/workflows/release.yml +0 -36
  47. package/CLAUDE.md +0 -86
  48. package/bun.lock +0 -377
  49. package/daemon/client/AnnotationEditor.tsx +0 -315
  50. package/daemon/client/AnnotationProvider.tsx +0 -195
  51. package/daemon/client/AnnotationSidebar.tsx +0 -627
  52. package/daemon/client/FileIcon.tsx +0 -76
  53. package/daemon/client/PlanRenderer.tsx +0 -614
  54. package/daemon/client/Popover.tsx +0 -350
  55. package/daemon/client/RenderErrorContext.ts +0 -11
  56. package/daemon/client/ResponsePreview.tsx +0 -186
  57. package/daemon/client/SessionContext.tsx +0 -3
  58. package/daemon/client/annotationContext.ts +0 -114
  59. package/daemon/client/annotationDom.ts +0 -104
  60. package/daemon/client/annotationDraft.test.ts +0 -188
  61. package/daemon/client/annotationDraft.ts +0 -130
  62. package/daemon/client/compat/preact-all.ts +0 -4
  63. package/daemon/client/components/Callout.tsx +0 -26
  64. package/daemon/client/components/Checklist.tsx +0 -37
  65. package/daemon/client/components/Choice.tsx +0 -176
  66. package/daemon/client/components/CodeBlock.tsx +0 -35
  67. package/daemon/client/components/Diff.tsx +0 -65
  68. package/daemon/client/components/FilePreview.tsx +0 -88
  69. package/daemon/client/components/Image.tsx +0 -45
  70. package/daemon/client/components/Markdown.tsx +0 -44
  71. package/daemon/client/components/Mermaid.tsx +0 -565
  72. package/daemon/client/components/Note.tsx +0 -14
  73. package/daemon/client/components/Priority.tsx +0 -19
  74. package/daemon/client/components/ResponseNote.tsx +0 -19
  75. package/daemon/client/components/Section.tsx +0 -33
  76. package/daemon/client/components/Table.tsx +0 -35
  77. package/daemon/client/components/Task.tsx +0 -76
  78. package/daemon/client/components/UserInput.tsx +0 -174
  79. package/daemon/client/components/renderMarkdownChildren.tsx +0 -97
  80. package/daemon/client/generateMarkdown.ts +0 -273
  81. package/daemon/client/highlightRange.ts +0 -224
  82. package/daemon/client/popoverPosition.ts +0 -31
  83. package/daemon/client/runtime.ts +0 -123
  84. package/daemon/client/theme.css +0 -66
  85. package/daemon/client/useTextAnnotation.tsx +0 -188
  86. package/daemon/client/utils.ts +0 -13
  87. package/daemon/src/compiler.test.ts +0 -57
  88. package/daemon/src/compiler.ts +0 -360
  89. package/daemon/src/handlers/utils.ts +0 -14
  90. package/daemon/src/router.ts +0 -16
  91. package/daemon/src/session-secrets.test.ts +0 -83
  92. package/daemon/src/session.ts +0 -612
  93. package/daemon/src/watcher.ts +0 -71
  94. package/daemon/src/websocket.test.ts +0 -99
  95. package/daemon/src/websocket.ts +0 -211
  96. package/tsconfig.json +0 -14
  97. package/workers/canvas-share/README.md +0 -159
  98. package/workers/canvas-share/oblaka.ts +0 -56
  99. package/workers/canvas-share/package.json +0 -25
  100. package/workers/canvas-share/public/assets/.gitkeep +0 -0
  101. package/workers/canvas-share/public/assets/client.css +0 -2
  102. package/workers/canvas-share/public/assets/client.js +0 -91
  103. package/workers/canvas-share/public/assets/components.js +0 -65
  104. package/workers/canvas-share/public/assets/jsx-runtime.js +0 -3
  105. package/workers/canvas-share/public/assets/preact-compat.js +0 -1
  106. package/workers/canvas-share/public/assets/runtime.js +0 -1
  107. package/workers/canvas-share/public/index.html +0 -385
  108. package/workers/canvas-share/public/screenshot-dark.png +0 -0
  109. package/workers/canvas-share/public/screenshot-light.png +0 -0
  110. package/workers/canvas-share/src/index.test.ts +0 -209
  111. package/workers/canvas-share/src/index.ts +0 -518
  112. package/workers/canvas-share/src/types.ts +0 -119
  113. package/workers/canvas-share/src/util.ts +0 -156
  114. package/workers/canvas-share/src/validation.ts +0 -160
  115. package/workers/canvas-share/tsconfig.json +0 -16
@@ -1,565 +0,0 @@
1
- import React, { useEffect, useRef, useState, useCallback } from "react";
2
- import ReactDOM from "react-dom";
3
- import { useAnnotations, useCanvasFile, Annotation } from "#canvas/runtime";
4
- import { AnnotationCreatePopover } from "../Popover";
5
-
6
- interface MermaidProps {
7
- children?: React.ReactNode;
8
- }
9
-
10
- interface NodePopoverState {
11
- anchorEl: HTMLElement;
12
- snippet: string;
13
- prefix: string;
14
- }
15
-
16
- const ZOOM_STEP = 0.2;
17
- const ZOOM_MIN = 0.25;
18
- const ZOOM_MAX = 4;
19
-
20
- function ZoomToolbar({ zoom, onZoomIn, onZoomOut, onZoomReset, onFullscreen }: {
21
- zoom: number;
22
- onZoomIn: () => void;
23
- onZoomOut: () => void;
24
- onZoomReset: () => void;
25
- onFullscreen?: () => void;
26
- }) {
27
- const btnClass = "flex items-center justify-center w-7 h-7 rounded hover:bg-bg-elevated text-text-secondary hover:text-text-primary transition-colors";
28
- return (
29
- <div className="flex items-center gap-0.5 bg-bg-surface border border-border-subtle rounded-md px-1 py-0.5" style={{ fontSize: "12px" }}>
30
- <button className={btnClass} onClick={onZoomOut} title="Zoom out" disabled={zoom <= ZOOM_MIN}>
31
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><line x1="5" y1="12" x2="19" y2="12"/></svg>
32
- </button>
33
- <button className="px-1.5 h-7 rounded hover:bg-bg-elevated text-text-tertiary hover:text-text-primary transition-colors tabular-nums" onClick={onZoomReset} title="Reset zoom">
34
- {Math.round(zoom * 100)}%
35
- </button>
36
- <button className={btnClass} onClick={onZoomIn} title="Zoom in" disabled={zoom >= ZOOM_MAX}>
37
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
38
- </button>
39
- {onFullscreen && (
40
- <>
41
- <div className="w-px h-4 bg-border-subtle mx-0.5" />
42
- <button className={btnClass} onClick={onFullscreen} title="Fullscreen">
43
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><polyline points="15 3 21 3 21 9"/><polyline points="9 21 3 21 3 15"/><line x1="21" y1="3" x2="14" y2="10"/><line x1="3" y1="21" x2="10" y2="14"/></svg>
44
- </button>
45
- </>
46
- )}
47
- </div>
48
- );
49
- }
50
-
51
- function useZoom(initialZoom = 1) {
52
- const [zoom, setZoom] = useState(initialZoom);
53
- const zoomIn = useCallback(() => setZoom(z => Math.min(ZOOM_MAX, +(z + ZOOM_STEP).toFixed(2))), []);
54
- const zoomOut = useCallback(() => setZoom(z => Math.max(ZOOM_MIN, +(z - ZOOM_STEP).toFixed(2))), []);
55
- const zoomReset = useCallback(() => setZoom(1), []);
56
- return { zoom, zoomIn, zoomOut, zoomReset, setZoom };
57
- }
58
-
59
- function MermaidFullscreenModal({ svgHtml, source, onClose }: {
60
- svgHtml: string;
61
- source: string;
62
- onClose: () => void;
63
- }) {
64
- const modalContentRef = useRef<HTMLDivElement>(null);
65
- const { zoom, zoomIn, zoomOut, zoomReset, setZoom } = useZoom(1);
66
- const [pan, setPan] = useState({ x: 0, y: 0 });
67
- const dragging = useRef<{ startX: number; startY: number; panX: number; panY: number } | null>(null);
68
-
69
- useEffect(() => {
70
- const onKeyDown = (e: KeyboardEvent) => {
71
- if (e.key === "Escape") onClose();
72
- };
73
- window.addEventListener("keydown", onKeyDown);
74
- return () => window.removeEventListener("keydown", onKeyDown);
75
- }, [onClose]);
76
-
77
- // Wheel zoom
78
- const handleWheel = useCallback((e: React.WheelEvent) => {
79
- e.preventDefault();
80
- const delta = e.deltaY > 0 ? -ZOOM_STEP : ZOOM_STEP;
81
- setZoom(z => Math.max(ZOOM_MIN, Math.min(ZOOM_MAX, +(z + delta).toFixed(2))));
82
- }, [setZoom]);
83
-
84
- // Pan via mouse drag
85
- const handleMouseDown = useCallback((e: React.MouseEvent) => {
86
- if (e.button !== 0) return;
87
- dragging.current = { startX: e.clientX, startY: e.clientY, panX: pan.x, panY: pan.y };
88
- e.preventDefault();
89
- }, [pan]);
90
-
91
- const handleMouseMove = useCallback((e: React.MouseEvent) => {
92
- if (!dragging.current) return;
93
- setPan({
94
- x: dragging.current.panX + (e.clientX - dragging.current.startX),
95
- y: dragging.current.panY + (e.clientY - dragging.current.startY),
96
- });
97
- }, []);
98
-
99
- const handleMouseUp = useCallback(() => {
100
- dragging.current = null;
101
- }, []);
102
-
103
- const handleResetView = useCallback(() => {
104
- zoomReset();
105
- setPan({ x: 0, y: 0 });
106
- }, [zoomReset]);
107
-
108
- return ReactDOM.createPortal(
109
- <div
110
- style={{ position: "fixed", inset: 0, zIndex: 9999, background: "var(--color-bg-base)", display: "flex", flexDirection: "column" }}
111
- onMouseMove={handleMouseMove}
112
- onMouseUp={handleMouseUp}
113
- onMouseLeave={handleMouseUp}
114
- >
115
- {/* Header bar */}
116
- <div className="flex items-center justify-between px-4 py-2 border-b border-border-subtle" style={{ flexShrink: 0, background: "var(--color-bg-surface)" }}>
117
- <span className="text-body text-text-secondary" style={{ fontSize: "13px" }}>Mermaid Diagram</span>
118
- <div className="flex items-center gap-2">
119
- <ZoomToolbar zoom={zoom} onZoomIn={zoomIn} onZoomOut={zoomOut} onZoomReset={handleResetView} />
120
- <button
121
- className="flex items-center justify-center w-8 h-8 rounded hover:bg-bg-elevated text-text-secondary hover:text-text-primary transition-colors"
122
- onClick={onClose}
123
- title="Close (Esc)"
124
- >
125
- <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
126
- </button>
127
- </div>
128
- </div>
129
- {/* Diagram area */}
130
- <div
131
- style={{ flex: 1, overflow: "hidden", cursor: dragging.current ? "grabbing" : "grab" }}
132
- onWheel={handleWheel}
133
- onMouseDown={handleMouseDown}
134
- >
135
- <div
136
- ref={modalContentRef}
137
- style={{
138
- width: "100%",
139
- height: "100%",
140
- display: "flex",
141
- alignItems: "center",
142
- justifyContent: "center",
143
- transform: `translate(${pan.x}px, ${pan.y}px) scale(${zoom})`,
144
- transformOrigin: "center center",
145
- transition: dragging.current ? "none" : "transform 0.1s ease-out",
146
- }}
147
- dangerouslySetInnerHTML={{ __html: svgHtml }}
148
- />
149
- </div>
150
- </div>,
151
- document.body,
152
- );
153
- }
154
-
155
- export function Mermaid({ children }: MermaidProps) {
156
- const containerRef = useRef<HTMLDivElement>(null);
157
- const [error, setError] = useState<string | null>(null);
158
- const [showFullscreen, setShowFullscreen] = useState(false);
159
- const [svgHtml, setSvgHtml] = useState("");
160
- const source = typeof children === "string" ? children : String(children ?? "");
161
- const { annotations, addAnnotationWithId, activeAnnotationId, setActiveAnnotationId } = useAnnotations();
162
- const canvasFile = useCanvasFile();
163
- const [nodePopover, setNodePopover] = useState<NodePopoverState | null>(null);
164
- const { zoom, zoomIn, zoomOut, zoomReset, setZoom } = useZoom(1);
165
-
166
- const annotationsRef = useRef(annotations);
167
- annotationsRef.current = annotations;
168
-
169
- const handleNodeClick = useCallback((anchorEl: HTMLElement, snippet: string, prefix: string) => {
170
- const diagramSnippet = `[Diagram ${prefix.toLowerCase()}] ${snippet}`;
171
- const existing = annotationsRef.current.find((a) => a.snippet === diagramSnippet);
172
- if (existing) {
173
- setActiveAnnotationId(existing.id);
174
- } else {
175
- setNodePopover({ anchorEl, snippet, prefix });
176
- }
177
- }, [setActiveAnnotationId]);
178
-
179
- const handleNodeClickRef = useRef(handleNodeClick);
180
- handleNodeClickRef.current = handleNodeClick;
181
-
182
- useEffect(() => {
183
- let cancelled = false;
184
- const render = async () => {
185
- try {
186
- const mermaid = (window as any).mermaid;
187
- if (!mermaid) { setError("Mermaid library not loaded"); return; }
188
- const theme = document.documentElement.dataset.theme === "light" ? "default" : "dark";
189
- mermaid.initialize({ startOnLoad: false, theme });
190
- const id = `mermaid-${Math.random().toString(36).slice(2)}`;
191
- const { svg } = await mermaid.render(id, source.trim());
192
- if (!cancelled && containerRef.current) {
193
- containerRef.current.innerHTML = svg;
194
- setSvgHtml(svg);
195
- setupNodeAnnotations(containerRef.current, (el, snippet, prefix) => handleNodeClickRef.current(el, snippet, prefix));
196
- highlightAnnotatedNodes(containerRef.current, annotationsRef.current);
197
- }
198
- setError(null);
199
- } catch (e: any) {
200
- if (!cancelled) setError(e.message || "Invalid diagram");
201
- }
202
- };
203
- render();
204
-
205
- // Re-render on theme change
206
- const observer = new MutationObserver((mutations) => {
207
- for (const m of mutations) {
208
- if (m.attributeName === "data-theme") render();
209
- }
210
- });
211
- observer.observe(document.documentElement, { attributes: true, attributeFilter: ["data-theme"] });
212
-
213
- return () => { cancelled = true; observer.disconnect(); };
214
- }, [source]);
215
-
216
- // Update visual highlights when annotations change
217
- useEffect(() => {
218
- if (containerRef.current) {
219
- highlightAnnotatedNodes(containerRef.current, annotations, activeAnnotationId);
220
- }
221
- }, [annotations, activeAnnotationId]);
222
-
223
- // Wheel zoom on inline diagram
224
- const handleWheel = useCallback((e: React.WheelEvent) => {
225
- if (!e.ctrlKey && !e.metaKey) return;
226
- e.preventDefault();
227
- const delta = e.deltaY > 0 ? -ZOOM_STEP : ZOOM_STEP;
228
- setZoom(z => Math.max(ZOOM_MIN, Math.min(ZOOM_MAX, +(z + delta).toFixed(2))));
229
- }, [setZoom]);
230
-
231
- if (error) {
232
- return (
233
- <div className="bg-accent-red-muted rounded-md p-4 mt-3 text-body text-accent-red">
234
- Mermaid error: {error}
235
- <pre className="mt-2 text-tiny text-text-tertiary whitespace-pre-wrap">{source}</pre>
236
- </div>
237
- );
238
- }
239
-
240
- const scrollContainer = containerRef.current?.closest("#plan-scroll-container") as HTMLElement | null;
241
-
242
- return (
243
- <>
244
- <div className="mt-3 mermaid-container" data-md="mermaid" data-md-source={source.trim()}>
245
- {/* Zoom toolbar */}
246
- <div className="flex justify-end mb-1">
247
- <ZoomToolbar
248
- zoom={zoom}
249
- onZoomIn={zoomIn}
250
- onZoomOut={zoomOut}
251
- onZoomReset={zoomReset}
252
- onFullscreen={() => setShowFullscreen(true)}
253
- />
254
- </div>
255
- {/* Zoomable diagram */}
256
- <div style={{ overflow: "auto", maxHeight: "70vh" }} onWheel={handleWheel}>
257
- <div
258
- ref={containerRef}
259
- className="flex justify-center"
260
- style={{
261
- transform: `scale(${zoom})`,
262
- transformOrigin: "top center",
263
- transition: "transform 0.1s ease-out",
264
- minHeight: zoom < 1 ? undefined : undefined,
265
- }}
266
- />
267
- </div>
268
- </div>
269
- {nodePopover && (
270
- <AnnotationCreatePopover
271
- anchorEl={nodePopover.anchorEl}
272
- scrollContainer={scrollContainer}
273
- snippet={nodePopover.snippet}
274
- header={
275
- <div style={{ fontSize: "12px", color: "var(--color-text-tertiary)", marginBottom: "8px", lineHeight: "1.4", display: "flex", alignItems: "center", gap: "6px" }}>
276
- <span style={{ width: "8px", height: "8px", borderRadius: "50%", background: "var(--color-accent-amber)", flexShrink: 0 }} />
277
- {nodePopover.snippet}
278
- </div>
279
- }
280
- onAdd={(note, images) => {
281
- const id = `ann-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
282
- addAnnotationWithId(id, `[Diagram ${nodePopover.prefix.toLowerCase()}] ${nodePopover.snippet}`, note, undefined, undefined, images, canvasFile || undefined);
283
- setNodePopover(null);
284
- }}
285
- onCancel={() => setNodePopover(null)}
286
- />
287
- )}
288
- {showFullscreen && svgHtml && (
289
- <MermaidFullscreenModal
290
- svgHtml={svgHtml}
291
- source={source}
292
- onClose={() => setShowFullscreen(false)}
293
- />
294
- )}
295
- </>
296
- );
297
- }
298
-
299
- function setupNodeAnnotations(
300
- container: HTMLElement,
301
- showPopover: (anchorEl: HTMLElement, snippet: string, prefix: string) => void,
302
- ) {
303
- const makeClickable = (el: SVGElement, getSnippet: () => string, prefix: string) => {
304
- el.style.cursor = "pointer";
305
- el.addEventListener("mouseenter", () => { el.style.filter = "brightness(1.2)"; });
306
- el.addEventListener("mouseleave", () => { el.style.filter = ""; });
307
- el.addEventListener("click", (e) => {
308
- e.stopPropagation();
309
- e.preventDefault();
310
- showPopover(el as unknown as HTMLElement, getSnippet(), prefix);
311
- });
312
- };
313
-
314
- // Nodes
315
- for (const node of container.querySelectorAll(".node")) {
316
- makeClickable(node as SVGElement, () => {
317
- const labelEl = node.querySelector(".nodeLabel") || node.querySelector("foreignObject span") || node.querySelector("text");
318
- return labelEl?.textContent?.trim() || "Node";
319
- }, "Node");
320
- }
321
-
322
- // Edge paths (connections)
323
- for (const edge of container.querySelectorAll(".edgePath")) {
324
- const el = edge as SVGElement;
325
- // Widen hit area for thin lines
326
- const path = el.querySelector("path");
327
- if (path) {
328
- path.style.strokeWidth = Math.max(parseFloat(path.style.strokeWidth || "1"), 8) + "px";
329
- path.style.strokeOpacity = "0";
330
- const clone = path.cloneNode(true) as SVGPathElement;
331
- clone.style.strokeWidth = "";
332
- clone.style.strokeOpacity = "";
333
- el.insertBefore(clone, path);
334
- }
335
- // Find the label for this edge (sibling edgeLabel)
336
- const edgeId = el.id || "";
337
- makeClickable(el, () => {
338
- // Try to find matching edge label
339
- const labels = container.querySelectorAll(".edgeLabel");
340
- for (const label of labels) {
341
- if (label.id && edgeId && label.id.replace("label", "").includes(edgeId.replace("L-", "").replace("edge", ""))) {
342
- const text = label.textContent?.trim();
343
- if (text) return text;
344
- }
345
- }
346
- // Fallback: find nearest edge label by position
347
- return "Connection";
348
- }, "Edge");
349
- }
350
-
351
- // Edge labels
352
- for (const label of container.querySelectorAll(".edgeLabel")) {
353
- const text = label.textContent?.trim();
354
- if (!text) continue;
355
- makeClickable(label as SVGElement, () => text, "Edge");
356
- }
357
-
358
- // Sequence diagram: actors (participants)
359
- for (const actor of container.querySelectorAll(".actor")) {
360
- const textEl = actor.closest("g")?.querySelector("text") || actor.parentElement?.querySelector("text");
361
- if (!textEl) continue;
362
- // The actor rect and text are siblings in a <g> — make the whole <g> clickable
363
- const group = actor.closest("g") as SVGElement | null;
364
- if (!group || group.hasAttribute("data-clickable")) continue;
365
- group.setAttribute("data-clickable", "true");
366
- makeClickable(group, () => textEl.textContent?.trim() || "Participant", "Node");
367
- }
368
-
369
- // Sequence diagram: message labels
370
- for (const msgText of container.querySelectorAll(".messageText")) {
371
- const text = msgText.textContent?.trim();
372
- if (!text) continue;
373
- makeClickable(msgText as SVGElement, () => text, "Edge");
374
- }
375
-
376
- // ER diagram: entities
377
- for (const entity of container.querySelectorAll(".entityBox")) {
378
- const group = entity.closest("g") as SVGElement | null;
379
- if (!group || group.hasAttribute("data-clickable")) continue;
380
- const labelEl = group.querySelector(".entityLabel");
381
- if (!labelEl) continue;
382
- group.setAttribute("data-clickable", "true");
383
- makeClickable(group, () => labelEl.textContent?.trim() || "Entity", "Node");
384
- }
385
-
386
- // ER diagram: relationship labels
387
- for (const label of container.querySelectorAll(".relationshipLabel")) {
388
- const text = label.textContent?.trim();
389
- if (!text) continue;
390
- const group = label.closest("g") as SVGElement | null;
391
- const target = group || label as SVGElement;
392
- if (target.hasAttribute("data-clickable")) continue;
393
- target.setAttribute("data-clickable", "true");
394
- makeClickable(target, () => text, "Edge");
395
- }
396
-
397
- // Class diagram: class groups
398
- for (const classGroup of container.querySelectorAll(".classGroup")) {
399
- const el = classGroup as SVGElement;
400
- if (el.hasAttribute("data-clickable")) continue;
401
- const labelEl = el.querySelector(".classTitle, .classTitleText, text");
402
- if (!labelEl) continue;
403
- el.setAttribute("data-clickable", "true");
404
- makeClickable(el, () => labelEl.textContent?.trim() || "Class", "Node");
405
- }
406
-
407
- // State diagram: states
408
- for (const state of container.querySelectorAll(".statediagram-state")) {
409
- const el = state as SVGElement;
410
- if (el.hasAttribute("data-clickable")) continue;
411
- const labelEl = el.querySelector(".state-title, text");
412
- if (!labelEl) continue;
413
- el.setAttribute("data-clickable", "true");
414
- makeClickable(el, () => labelEl.textContent?.trim() || "State", "Node");
415
- }
416
-
417
- // Mindmap: nodes
418
- for (const node of container.querySelectorAll(".mindmap-node")) {
419
- const el = node as SVGElement;
420
- if (el.hasAttribute("data-clickable")) continue;
421
- const labelEl = el.querySelector(".mindmap-node-label, text");
422
- if (!labelEl) continue;
423
- el.setAttribute("data-clickable", "true");
424
- makeClickable(el, () => labelEl.textContent?.trim() || "Node", "Node");
425
- }
426
- }
427
-
428
- function highlightAnnotatedNodes(container: HTMLElement, annotations: Annotation[], activeId?: string | null) {
429
- // Collect annotated diagram snippets
430
- const annotatedSnippets = new Map<string, string>();
431
- for (const ann of annotations) {
432
- const match = ann.snippet.match(/^\[Diagram (?:node|edge)\] (.+)$/);
433
- if (match) annotatedSnippets.set(match[1], ann.id);
434
- }
435
-
436
- // Reset all node outlines
437
- for (const el of container.querySelectorAll("[data-annotated]")) {
438
- (el as SVGElement).style.outline = "";
439
- (el as SVGElement).style.outlineOffset = "";
440
- (el as SVGElement).style.borderRadius = "";
441
- el.removeAttribute("data-annotated");
442
- }
443
-
444
- if (annotatedSnippets.size === 0) return;
445
-
446
- // Highlight nodes
447
- for (const node of container.querySelectorAll(".node")) {
448
- const labelEl = node.querySelector(".nodeLabel") || node.querySelector("foreignObject span") || node.querySelector("text");
449
- const text = labelEl?.textContent?.trim();
450
- if (!text || !annotatedSnippets.has(text)) continue;
451
- const annId = annotatedSnippets.get(text)!;
452
- const el = node as SVGElement;
453
- el.setAttribute("data-annotated", annId);
454
- const isActive = annId === activeId;
455
- el.style.outline = isActive
456
- ? "2px solid var(--color-highlight-border)"
457
- : "2px solid var(--color-highlight-annotation)";
458
- el.style.outlineOffset = "2px";
459
- el.style.borderRadius = "4px";
460
- }
461
-
462
- // Highlight edge labels
463
- for (const label of container.querySelectorAll(".edgeLabel")) {
464
- const text = label.textContent?.trim();
465
- if (!text || !annotatedSnippets.has(text)) continue;
466
- const annId = annotatedSnippets.get(text)!;
467
- const el = label as SVGElement;
468
- el.setAttribute("data-annotated", annId);
469
- const isActive = annId === activeId;
470
- el.style.outline = isActive
471
- ? "2px solid var(--color-highlight-border)"
472
- : "2px solid var(--color-highlight-annotation)";
473
- el.style.outlineOffset = "2px";
474
- el.style.borderRadius = "4px";
475
- }
476
-
477
- // Highlight sequence diagram actors
478
- for (const actor of container.querySelectorAll(".actor")) {
479
- const group = actor.closest("g") as SVGElement | null;
480
- if (!group) continue;
481
- const textEl = group.querySelector("text");
482
- const text = textEl?.textContent?.trim();
483
- if (!text || !annotatedSnippets.has(text)) continue;
484
- if (group.hasAttribute("data-annotated")) continue;
485
- const annId = annotatedSnippets.get(text)!;
486
- group.setAttribute("data-annotated", annId);
487
- const isActive = annId === activeId;
488
- const rect = group.querySelector("rect");
489
- if (rect) {
490
- rect.style.outline = isActive
491
- ? "2px solid var(--color-highlight-border)"
492
- : "2px solid var(--color-highlight-annotation)";
493
- rect.style.outlineOffset = "2px";
494
- rect.style.borderRadius = "4px";
495
- }
496
- }
497
-
498
- // Highlight sequence diagram message labels
499
- for (const msgText of container.querySelectorAll(".messageText")) {
500
- const text = msgText.textContent?.trim();
501
- if (!text || !annotatedSnippets.has(text)) continue;
502
- const annId = annotatedSnippets.get(text)!;
503
- const el = msgText as SVGElement;
504
- el.setAttribute("data-annotated", annId);
505
- const isActive = annId === activeId;
506
- el.style.outline = isActive
507
- ? "2px solid var(--color-highlight-border)"
508
- : "2px solid var(--color-highlight-annotation)";
509
- el.style.outlineOffset = "2px";
510
- el.style.borderRadius = "4px";
511
- }
512
-
513
- // Helper for group-based highlights (ER entities, class groups, states, mindmap nodes)
514
- const highlightGroups = (
515
- selector: string,
516
- labelSelector: string,
517
- findGroup: (el: Element) => SVGElement | null,
518
- ) => {
519
- for (const el of container.querySelectorAll(selector)) {
520
- const group = findGroup(el);
521
- if (!group || group.hasAttribute("data-annotated")) continue;
522
- const labelEl = group.querySelector(labelSelector);
523
- const text = labelEl?.textContent?.trim();
524
- if (!text || !annotatedSnippets.has(text)) continue;
525
- const annId = annotatedSnippets.get(text)!;
526
- group.setAttribute("data-annotated", annId);
527
- const isActive = annId === activeId;
528
- const target = group.querySelector("rect, circle, ellipse, polygon") as SVGElement || group;
529
- target.style.outline = isActive
530
- ? "2px solid var(--color-highlight-border)"
531
- : "2px solid var(--color-highlight-annotation)";
532
- target.style.outlineOffset = "2px";
533
- target.style.borderRadius = "4px";
534
- }
535
- };
536
-
537
- // ER diagram: entities
538
- highlightGroups(".entityBox", ".entityLabel", (el) => el.closest("g") as SVGElement | null);
539
-
540
- // ER diagram: relationship labels
541
- for (const label of container.querySelectorAll(".relationshipLabel")) {
542
- const group = label.closest("g") as SVGElement | null;
543
- const target = group || label as SVGElement;
544
- if (target.hasAttribute("data-annotated")) continue;
545
- const text = label.textContent?.trim();
546
- if (!text || !annotatedSnippets.has(text)) continue;
547
- const annId = annotatedSnippets.get(text)!;
548
- target.setAttribute("data-annotated", annId);
549
- const isActive = annId === activeId;
550
- (target as SVGElement).style.outline = isActive
551
- ? "2px solid var(--color-highlight-border)"
552
- : "2px solid var(--color-highlight-annotation)";
553
- (target as SVGElement).style.outlineOffset = "2px";
554
- (target as SVGElement).style.borderRadius = "4px";
555
- }
556
-
557
- // Class diagram
558
- highlightGroups(".classGroup", ".classTitle, .classTitleText, text", (el) => el as SVGElement);
559
-
560
- // State diagram
561
- highlightGroups(".statediagram-state", ".state-title, text", (el) => el as SVGElement);
562
-
563
- // Mindmap
564
- highlightGroups(".mindmap-node", ".mindmap-node-label, text", (el) => el as SVGElement);
565
- }
@@ -1,14 +0,0 @@
1
- import React from "react";
2
- import { renderMarkdownChildren } from "./renderMarkdownChildren";
3
-
4
- interface NoteProps {
5
- children?: React.ReactNode;
6
- }
7
-
8
- export function Note({ children }: NoteProps) {
9
- return (
10
- <div className="relative bg-bg-elevated rounded-lg px-5 py-4 mt-3 text-body text-text-secondary italic leading-relaxed" data-md="note">
11
- {renderMarkdownChildren(children)}
12
- </div>
13
- );
14
- }
@@ -1,19 +0,0 @@
1
- import React from "react";
2
-
3
- interface PriorityProps {
4
- level: "high" | "medium" | "low";
5
- }
6
-
7
- const config = {
8
- high: "text-accent-red bg-accent-red-muted",
9
- medium: "text-accent-amber bg-accent-amber-muted",
10
- low: "text-text-tertiary bg-border-subtle",
11
- };
12
-
13
- export function Priority({ level }: PriorityProps) {
14
- return (
15
- <span className={`inline text-tiny font-medium font-body ml-2 px-1.5 py-0.5 rounded ${config[level]}`} data-md="priority" data-md-level={level}>
16
- {level}
17
- </span>
18
- );
19
- }
@@ -1,19 +0,0 @@
1
- import React from "react";
2
- import { autoResizeTextarea } from "../utils";
3
-
4
- /** Seamless note input for response components */
5
- export function ResponseNote({ note, onChange }: { show?: boolean; note: string; onToggle?: () => void; onChange: (n: string) => void }) {
6
- return (
7
- <div className="mt-1.5">
8
- <textarea
9
- value={note}
10
- onChange={(e) => onChange(e.target.value)}
11
- className="w-full bg-transparent text-[12px] font-body text-text-primary resize-none focus:outline-none border-none placeholder:text-text-disabled transition-colors min-h-[20px] p-0 leading-relaxed"
12
- placeholder="Add a note..."
13
- rows={1}
14
- onInput={(e) => autoResizeTextarea(e.target as HTMLTextAreaElement)}
15
- ref={(el) => { if (el) autoResizeTextarea(el); }}
16
- />
17
- </div>
18
- );
19
- }
@@ -1,33 +0,0 @@
1
- import React, { useState } from "react";
2
- import { renderMarkdownChildren } from "./renderMarkdownChildren";
3
-
4
- interface SectionProps {
5
- title: string;
6
- children?: React.ReactNode;
7
- }
8
-
9
- export function Section({ title, children }: SectionProps) {
10
- const [collapsed, setCollapsed] = useState(false);
11
-
12
- return (
13
- <div className="relative mb-8 p-4 group/section" data-md="section" data-md-title={title}>
14
- <div className="relative mb-2">
15
- <button
16
- onClick={() => setCollapsed(!collapsed)}
17
- className={`absolute -left-8 top-1/2 -translate-y-1/2 text-text-tertiary opacity-0 group-hover/section:opacity-50 hover:!opacity-100 transition-all duration-150 p-0.5 ${collapsed ? "-rotate-90" : ""}`}
18
- >
19
- <svg width="14" height="14" viewBox="0 0 12 12" fill="none">
20
- <path d="M3 4.5L6 7.5L9 4.5" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
21
- </svg>
22
- </button>
23
- <h2
24
- className="font-heading text-section text-text-primary"
25
- style={{ fontWeight: 400 }}
26
- >
27
- {title}
28
- </h2>
29
- </div>
30
- <div className={collapsed ? "mt-6 hidden" : "mt-6"}>{renderMarkdownChildren(children)}</div>
31
- </div>
32
- );
33
- }