agent-canvas 0.1.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 (65) hide show
  1. package/bin/agent-canvas.js +14 -0
  2. package/bin/agent-canvas.ts +377 -0
  3. package/daemon/build.ts +185 -0
  4. package/daemon/client/AnnotationProvider.tsx +185 -0
  5. package/daemon/client/AnnotationSidebar.tsx +314 -0
  6. package/daemon/client/App.tsx +611 -0
  7. package/daemon/client/CompareView.tsx +333 -0
  8. package/daemon/client/FileBrowser.tsx +188 -0
  9. package/daemon/client/FileIcon.tsx +76 -0
  10. package/daemon/client/FileViewer.tsx +207 -0
  11. package/daemon/client/PlanRenderer.tsx +665 -0
  12. package/daemon/client/Popover.tsx +285 -0
  13. package/daemon/client/ResponsePreview.tsx +260 -0
  14. package/daemon/client/RevisionSelect.tsx +97 -0
  15. package/daemon/client/SessionContext.tsx +3 -0
  16. package/daemon/client/SessionSwitcher.tsx +72 -0
  17. package/daemon/client/annotationContext.ts +114 -0
  18. package/daemon/client/components/Callout.tsx +25 -0
  19. package/daemon/client/components/Checklist.tsx +37 -0
  20. package/daemon/client/components/Choice.tsx +164 -0
  21. package/daemon/client/components/CodeBlock.tsx +35 -0
  22. package/daemon/client/components/Diff.tsx +65 -0
  23. package/daemon/client/components/FilePreview.tsx +88 -0
  24. package/daemon/client/components/Image.tsx +45 -0
  25. package/daemon/client/components/Mermaid.tsx +277 -0
  26. package/daemon/client/components/Note.tsx +13 -0
  27. package/daemon/client/components/Priority.tsx +19 -0
  28. package/daemon/client/components/ResponseNote.tsx +24 -0
  29. package/daemon/client/components/Section.tsx +32 -0
  30. package/daemon/client/components/Table.tsx +35 -0
  31. package/daemon/client/components/Task.tsx +75 -0
  32. package/daemon/client/components/UserInput.tsx +180 -0
  33. package/daemon/client/components/index.ts +15 -0
  34. package/daemon/client/domDiff.ts +336 -0
  35. package/daemon/client/exportMarkdown.ts +267 -0
  36. package/daemon/client/generateMarkdown.ts +194 -0
  37. package/daemon/client/highlightRange.ts +233 -0
  38. package/daemon/client/index.html +26 -0
  39. package/daemon/client/lcs.ts +19 -0
  40. package/daemon/client/popoverPosition.ts +31 -0
  41. package/daemon/client/runtime.ts +89 -0
  42. package/daemon/client/styles.css +306 -0
  43. package/daemon/client/theme.css +66 -0
  44. package/daemon/client/unifiedDiff.ts +604 -0
  45. package/daemon/dist/client.css +2 -0
  46. package/daemon/dist/client.js +68 -0
  47. package/daemon/dist/components.js +5 -0
  48. package/daemon/dist/index.html +108 -0
  49. package/daemon/dist/jsx-dev-runtime-shim.js +17 -0
  50. package/daemon/dist/jsx-runtime-shim.js +17 -0
  51. package/daemon/dist/react-dom-client-shim.js +4 -0
  52. package/daemon/dist/react-dom-shim.js +4 -0
  53. package/daemon/dist/react-shim.js +4 -0
  54. package/daemon/dist/runtime.js +1 -0
  55. package/daemon/dist/theme.css +66 -0
  56. package/daemon/src/compiler.ts +92 -0
  57. package/daemon/src/paths.ts +22 -0
  58. package/daemon/src/server.ts +448 -0
  59. package/daemon/src/session.ts +274 -0
  60. package/daemon/src/watcher.ts +59 -0
  61. package/hooks/session-start.sh +11 -0
  62. package/package.json +39 -0
  63. package/skills/canvas/SKILL.md +174 -0
  64. package/skills/canvas/components.md +349 -0
  65. package/skills/canvas/flows.md +406 -0
@@ -0,0 +1,207 @@
1
+ import React, { useEffect, useState, useContext, useCallback, useRef, useMemo } from "react";
2
+ import { SessionContext } from "#canvas/runtime";
3
+ import { useAnnotations } from "./AnnotationProvider";
4
+ import { wrapRangeWithMark, updateAllMarkStates, renameMarkId, unwrapMarks, restoreMarks } from "./highlightRange";
5
+ import { AnnotationCreatePopover, AnnotationEditPopover } from "./Popover";
6
+ import { LANG_MAP } from "../langMap";
7
+
8
+ interface FileViewerProps {
9
+ path: string;
10
+ }
11
+
12
+ export function FileViewer({ path }: FileViewerProps) {
13
+ const sessionId = useContext(SessionContext);
14
+ const { annotations, addAnnotationWithId, removeAnnotation, updateAnnotation, activeAnnotationId, setActiveAnnotationId } = useAnnotations();
15
+ const [content, setContent] = useState<string | null>(null);
16
+ const [language, setLanguage] = useState("text");
17
+ const [loading, setLoading] = useState(true);
18
+ const [error, setError] = useState<string | null>(null);
19
+ const contentRef = useRef<HTMLPreElement>(null);
20
+
21
+ const [pendingMarkId, setPendingMarkId] = useState<string | null>(null);
22
+ const pendingSnippetRef = useRef("");
23
+
24
+ // Popover state
25
+ const [editPopover, setEditPopover] = useState<{ anchorEl: HTMLElement; annId: string } | null>(null);
26
+ const [createPopover, setCreatePopover] = useState<{ anchorEl: HTMLElement; tempId: string; snippet: string } | null>(null);
27
+
28
+ const fileAnns = annotations.filter((a) => a.filePath === path);
29
+
30
+ useEffect(() => {
31
+ setLoading(true);
32
+ setError(null);
33
+ fetch(`/api/file?session=${sessionId}&path=${encodeURIComponent(path)}`)
34
+ .then((r) => r.json())
35
+ .then((data: any) => {
36
+ if (data.error) { setError(data.error); setLoading(false); }
37
+ else { setContent(data.content); setLanguage(data.language || "text"); setLoading(false); }
38
+ })
39
+ .catch(() => { setError("Failed to fetch file"); setLoading(false); });
40
+ }, [sessionId, path]);
41
+
42
+ // Restore persisted annotation marks after file content renders
43
+ useEffect(() => {
44
+ if (!content || !contentRef.current) return;
45
+ const timer = setTimeout(() => {
46
+ if (contentRef.current) {
47
+ restoreMarks(contentRef.current, fileAnns);
48
+ }
49
+ }, 50);
50
+ return () => clearTimeout(timer);
51
+ }, [content, path]);
52
+
53
+ // Update mark active states
54
+ useEffect(() => { updateAllMarkStates(activeAnnotationId); }, [activeAnnotationId]);
55
+
56
+ // Click handler for marks
57
+ useEffect(() => {
58
+ const container = contentRef.current;
59
+ if (!container) return;
60
+ const handleClick = (e: MouseEvent) => {
61
+ const mark = (e.target as HTMLElement).closest("[data-annotation-id]") as HTMLElement | null;
62
+ if (!mark) return;
63
+ e.stopPropagation();
64
+ const annId = mark.getAttribute("data-annotation-id")!;
65
+ if (annId === activeAnnotationId) {
66
+ setEditPopover({ anchorEl: mark, annId });
67
+ } else {
68
+ setActiveAnnotationId(annId);
69
+ }
70
+ };
71
+ const handleMouseOver = (e: MouseEvent) => {
72
+ const mark = (e.target as HTMLElement).closest("[data-annotation-id]") as HTMLElement | null;
73
+ if (mark) setActiveAnnotationId(mark.getAttribute("data-annotation-id")!);
74
+ };
75
+ const handleMouseOut = (e: MouseEvent) => {
76
+ const mark = (e.target as HTMLElement).closest("[data-annotation-id]");
77
+ const related = (e.relatedTarget as HTMLElement | null)?.closest?.("[data-annotation-id]");
78
+ if (mark && !related && !document.getElementById("ann-inline-popover")) setActiveAnnotationId(null);
79
+ };
80
+ container.addEventListener("click", handleClick);
81
+ container.addEventListener("mouseover", handleMouseOver);
82
+ container.addEventListener("mouseout", handleMouseOut);
83
+ return () => {
84
+ container.removeEventListener("click", handleClick);
85
+ container.removeEventListener("mouseover", handleMouseOver);
86
+ container.removeEventListener("mouseout", handleMouseOut);
87
+ };
88
+ }, [content, annotations, activeAnnotationId]);
89
+
90
+ useEffect(() => {
91
+ const handler = () => handleMouseUp();
92
+ document.addEventListener("mouseup", handler);
93
+ return () => document.removeEventListener("mouseup", handler);
94
+ }, []);
95
+
96
+ // Use refs so the document-level mouseup always has current values
97
+ const pathRef = useRef(path);
98
+ pathRef.current = path;
99
+ const addAnnotationRef = useRef(addAnnotationWithId);
100
+ addAnnotationRef.current = addAnnotationWithId;
101
+
102
+ const handleMouseUp = useCallback(() => {
103
+ const sel = window.getSelection();
104
+ if (!sel || sel.isCollapsed || !sel.toString().trim()) return;
105
+ if (!contentRef.current) return;
106
+ const range = sel.getRangeAt(0);
107
+ if (!contentRef.current.contains(range.startContainer)) return;
108
+ if ((range.startContainer.parentElement as HTMLElement)?.closest?.("[data-annotation-id]")) return;
109
+ const snippet = sel.toString().trim();
110
+ if (snippet.length < 2) return;
111
+ const tempId = `__pending_${Date.now()}`;
112
+ const cloned = range.cloneRange();
113
+ try { wrapRangeWithMark(cloned, tempId); } catch {}
114
+ sel.removeAllRanges();
115
+
116
+ const marks = document.querySelectorAll(`[data-annotation-id="${tempId}"]`);
117
+ const lastMark = marks[marks.length - 1] as HTMLElement | undefined;
118
+ if (!lastMark) return;
119
+
120
+ pendingSnippetRef.current = snippet;
121
+ setPendingMarkId(tempId);
122
+ setCreatePopover({ anchorEl: lastMark, tempId, snippet });
123
+ }, []);
124
+
125
+ // Highlight content
126
+ const highlightedLines = useMemo(() => {
127
+ if (!content) return null;
128
+ const hljs = (window as any).hljs;
129
+ if (!hljs) return null;
130
+ const ext = path.split(".").pop() || "";
131
+ const lang = LANG_MAP[ext];
132
+ try {
133
+ const result = lang ? hljs.highlight(content, { language: lang }) : hljs.highlightAuto(content);
134
+ return result.value.split("\n");
135
+ } catch { return null; }
136
+ }, [content, path]);
137
+
138
+ if (loading) return <div className="flex items-center justify-center h-64 text-text-tertiary font-body text-[13px]">Loading {path}...</div>;
139
+ if (error) return <div className="p-8 text-accent-red font-body text-[13px]">{error}</div>;
140
+ if (!content) return null;
141
+
142
+ const lines = content.split("\n");
143
+
144
+ const addWholeFileWithPopup = () => {
145
+ const preview = content!.split("\n").slice(0, 3).join("\n") + (lines.length > 3 ? "\n..." : "");
146
+ const id = `ann-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
147
+ addAnnotationWithId(id, preview, "", path);
148
+ // Focus the new annotation's note in sidebar by activating it
149
+ setActiveAnnotationId(id);
150
+ };
151
+
152
+ const scrollContainer = contentRef.current?.closest("#plan-scroll-container") as HTMLElement | null;
153
+
154
+ return (
155
+ <div>
156
+ {/* Code */}
157
+ <pre ref={contentRef} className="text-code font-mono text-text-code bg-bg-base select-text cursor-text py-3 hljs">
158
+ {lines.map((line, i) => (
159
+ <div key={i} className="flex hover:bg-bg-elevated-half px-5">
160
+ <span className="text-text-tertiary opacity-40 select-none w-10 text-right pr-4 shrink-0 py-px text-[12px]">{i + 1}</span>
161
+ {highlightedLines ? (
162
+ <code className="py-px" dangerouslySetInnerHTML={{ __html: highlightedLines[i] || " " }} />
163
+ ) : (
164
+ <code className="py-px">{line || " "}</code>
165
+ )}
166
+ </div>
167
+ ))}
168
+ </pre>
169
+
170
+ {editPopover && (() => {
171
+ const ann = annotations.find((a) => a.id === editPopover.annId);
172
+ if (!ann) return null;
173
+ return (
174
+ <AnnotationEditPopover
175
+ anchorEl={editPopover.anchorEl}
176
+ scrollContainer={scrollContainer}
177
+ initialNote={ann.note}
178
+ onUpdate={(note) => updateAnnotation(editPopover.annId, note)}
179
+ onDelete={() => { removeAnnotation(editPopover.annId); setActiveAnnotationId(null); }}
180
+ onClose={() => setEditPopover(null)}
181
+ />
182
+ );
183
+ })()}
184
+
185
+ {createPopover && (
186
+ <AnnotationCreatePopover
187
+ anchorEl={createPopover.anchorEl}
188
+ scrollContainer={scrollContainer}
189
+ snippet={createPopover.snippet}
190
+ onAdd={(note) => {
191
+ const id = `ann-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
192
+ renameMarkId(createPopover.tempId, id);
193
+ addAnnotationWithId(id, createPopover.snippet, note, path);
194
+ setPendingMarkId(null);
195
+ setCreatePopover(null);
196
+ }}
197
+ onCancel={() => {
198
+ unwrapMarks(createPopover.tempId);
199
+ setPendingMarkId(null);
200
+ setCreatePopover(null);
201
+ }}
202
+ />
203
+ )}
204
+ </div>
205
+ );
206
+ }
207
+