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.
- package/bin/agent-canvas.js +14 -0
- package/bin/agent-canvas.ts +377 -0
- package/daemon/build.ts +185 -0
- package/daemon/client/AnnotationProvider.tsx +185 -0
- package/daemon/client/AnnotationSidebar.tsx +314 -0
- package/daemon/client/App.tsx +611 -0
- package/daemon/client/CompareView.tsx +333 -0
- package/daemon/client/FileBrowser.tsx +188 -0
- package/daemon/client/FileIcon.tsx +76 -0
- package/daemon/client/FileViewer.tsx +207 -0
- package/daemon/client/PlanRenderer.tsx +665 -0
- package/daemon/client/Popover.tsx +285 -0
- package/daemon/client/ResponsePreview.tsx +260 -0
- package/daemon/client/RevisionSelect.tsx +97 -0
- package/daemon/client/SessionContext.tsx +3 -0
- package/daemon/client/SessionSwitcher.tsx +72 -0
- package/daemon/client/annotationContext.ts +114 -0
- package/daemon/client/components/Callout.tsx +25 -0
- package/daemon/client/components/Checklist.tsx +37 -0
- package/daemon/client/components/Choice.tsx +164 -0
- package/daemon/client/components/CodeBlock.tsx +35 -0
- package/daemon/client/components/Diff.tsx +65 -0
- package/daemon/client/components/FilePreview.tsx +88 -0
- package/daemon/client/components/Image.tsx +45 -0
- package/daemon/client/components/Mermaid.tsx +277 -0
- package/daemon/client/components/Note.tsx +13 -0
- package/daemon/client/components/Priority.tsx +19 -0
- package/daemon/client/components/ResponseNote.tsx +24 -0
- package/daemon/client/components/Section.tsx +32 -0
- package/daemon/client/components/Table.tsx +35 -0
- package/daemon/client/components/Task.tsx +75 -0
- package/daemon/client/components/UserInput.tsx +180 -0
- package/daemon/client/components/index.ts +15 -0
- package/daemon/client/domDiff.ts +336 -0
- package/daemon/client/exportMarkdown.ts +267 -0
- package/daemon/client/generateMarkdown.ts +194 -0
- package/daemon/client/highlightRange.ts +233 -0
- package/daemon/client/index.html +26 -0
- package/daemon/client/lcs.ts +19 -0
- package/daemon/client/popoverPosition.ts +31 -0
- package/daemon/client/runtime.ts +89 -0
- package/daemon/client/styles.css +306 -0
- package/daemon/client/theme.css +66 -0
- package/daemon/client/unifiedDiff.ts +604 -0
- package/daemon/dist/client.css +2 -0
- package/daemon/dist/client.js +68 -0
- package/daemon/dist/components.js +5 -0
- package/daemon/dist/index.html +108 -0
- package/daemon/dist/jsx-dev-runtime-shim.js +17 -0
- package/daemon/dist/jsx-runtime-shim.js +17 -0
- package/daemon/dist/react-dom-client-shim.js +4 -0
- package/daemon/dist/react-dom-shim.js +4 -0
- package/daemon/dist/react-shim.js +4 -0
- package/daemon/dist/runtime.js +1 -0
- package/daemon/dist/theme.css +66 -0
- package/daemon/src/compiler.ts +92 -0
- package/daemon/src/paths.ts +22 -0
- package/daemon/src/server.ts +448 -0
- package/daemon/src/session.ts +274 -0
- package/daemon/src/watcher.ts +59 -0
- package/hooks/session-start.sh +11 -0
- package/package.json +39 -0
- package/skills/canvas/SKILL.md +174 -0
- package/skills/canvas/components.md +349 -0
- 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
|
+
|