agent-canvas 0.1.5 → 0.1.8
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/.github/workflows/ci.yml +23 -0
- package/.github/workflows/release.yml +36 -0
- package/CLAUDE.md +86 -0
- package/LICENSE +21 -0
- package/README.md +71 -0
- package/bin/agent-canvas.ts +26 -353
- package/bin/lib/commands/daemon.ts +51 -0
- package/bin/lib/commands/fetch.ts +20 -0
- package/bin/lib/commands/install.ts +48 -0
- package/bin/lib/commands/push.ts +67 -0
- package/bin/lib/commands/watch.ts +23 -0
- package/bin/lib/config.ts +10 -0
- package/bin/lib/daemon-lifecycle.ts +74 -0
- package/bin/lib/helpers.ts +78 -0
- package/bun.lock +176 -0
- package/daemon/build.ts +27 -61
- package/daemon/bunfig.toml +2 -0
- package/daemon/client/AnnotationProvider.tsx +2 -2
- package/daemon/client/AnnotationSidebar.tsx +199 -31
- package/daemon/client/App.tsx +39 -3
- package/daemon/client/FileBrowser.tsx +8 -7
- package/daemon/client/FileViewer.tsx +153 -16
- package/daemon/client/PlanRenderer.tsx +6 -3
- package/daemon/client/ResponsePreview.tsx +5 -116
- package/daemon/client/SessionSwitcher.tsx +21 -10
- package/daemon/client/compat/preact-all.ts +4 -0
- package/daemon/client/components/Markdown.tsx +44 -0
- package/daemon/client/components/ResponseNote.tsx +3 -8
- package/daemon/client/components/UserInput.tsx +2 -4
- package/daemon/client/components/index.ts +1 -0
- package/daemon/client/domDiff.ts +82 -2
- package/daemon/client/generateMarkdown.ts +46 -9
- package/daemon/client/highlightRange.ts +8 -6
- package/daemon/client/runtime.ts +2 -0
- package/daemon/client/styles.css +126 -0
- package/daemon/client/unifiedDiff.ts +155 -2
- package/daemon/client/utils.ts +10 -0
- package/daemon/dist/client.css +1 -1
- package/daemon/dist/client.js +83 -36
- package/daemon/dist/components.js +63 -5
- package/daemon/dist/index.html +5 -12
- package/daemon/dist/jsx-runtime.js +3 -0
- package/daemon/dist/preact-compat.js +1 -0
- package/daemon/langMap.ts +8 -0
- package/daemon/src/compiler.ts +28 -2
- package/daemon/src/handlers/api.ts +150 -0
- package/daemon/src/handlers/files.ts +114 -0
- package/daemon/src/handlers/static.ts +61 -0
- package/daemon/src/handlers/utils.ts +14 -0
- package/daemon/src/router.ts +16 -0
- package/daemon/src/server.ts +28 -403
- package/daemon/src/session.ts +43 -24
- package/daemon/src/websocket.ts +83 -0
- package/daemon/tsconfig.json +20 -0
- package/package.json +4 -14
- package/skills/canvas/SKILL.md +47 -22
- package/skills/canvas/components.md +31 -0
- package/skills/canvas/flows.md +2 -2
- package/tsconfig.json +14 -0
- package/daemon/dist/jsx-dev-runtime-shim.js +0 -17
- package/daemon/dist/jsx-runtime-shim.js +0 -17
- package/daemon/dist/react-dom-client-shim.js +0 -4
- package/daemon/dist/react-dom-shim.js +0 -4
- package/daemon/dist/react-shim.js +0 -4
|
@@ -5,6 +5,7 @@ import { useAnnotations } from "./AnnotationProvider";
|
|
|
5
5
|
import { wrapRangeWithMark, updateAllMarkStates, renameMarkId, unwrapMarks, restoreMarks } from "./highlightRange";
|
|
6
6
|
import { extractContext } from "./annotationContext";
|
|
7
7
|
import { AnnotationCreatePopover, AnnotationEditPopover } from "./Popover";
|
|
8
|
+
import { generateAnnotationId } from "./utils";
|
|
8
9
|
|
|
9
10
|
/** All navigable blocks (keyboard arrows) */
|
|
10
11
|
const BLOCK_SELECTOR = "[data-md='item'], [data-md='section'], [data-md='table'] tbody tr, [data-md='callout'], [data-md='note'], [data-md='checklist-item'], [data-md='choice-option'], [data-md='multichoice-option'], [data-md='userinput'], [data-md='rangeinput'], [data-md='image']";
|
|
@@ -58,8 +59,10 @@ export function PlanRenderer({ revision }: PlanRendererProps) {
|
|
|
58
59
|
}, [PlanComponent]);
|
|
59
60
|
|
|
60
61
|
// Update mark active states when activeAnnotationId changes
|
|
62
|
+
const prevActiveRef = useRef<string | null>(null);
|
|
61
63
|
useEffect(() => {
|
|
62
|
-
updateAllMarkStates(activeAnnotationId);
|
|
64
|
+
updateAllMarkStates(activeAnnotationId, prevActiveRef.current);
|
|
65
|
+
prevActiveRef.current = activeAnnotationId;
|
|
63
66
|
}, [activeAnnotationId]);
|
|
64
67
|
|
|
65
68
|
// Click handler for marks in plan
|
|
@@ -383,7 +386,7 @@ export function PlanRenderer({ revision }: PlanRendererProps) {
|
|
|
383
386
|
snippet={createPopover.snippet}
|
|
384
387
|
truncateAt={80}
|
|
385
388
|
onAdd={(note) => {
|
|
386
|
-
const id =
|
|
389
|
+
const id = generateAnnotationId();
|
|
387
390
|
renameMarkId(createPopover.tempId, id);
|
|
388
391
|
addAnnotationWithId(id, createPopover.snippet, note, undefined, createPopover.ctx);
|
|
389
392
|
setCreatePopover(null);
|
|
@@ -421,7 +424,7 @@ export function PlanRenderer({ revision }: PlanRendererProps) {
|
|
|
421
424
|
snippet={blockPopover.snippet}
|
|
422
425
|
truncateAt={80}
|
|
423
426
|
onAdd={(note) => {
|
|
424
|
-
const id =
|
|
427
|
+
const id = generateAnnotationId();
|
|
425
428
|
addAnnotationWithId(id, blockPopover.snippet, note);
|
|
426
429
|
setBlockPopover(null);
|
|
427
430
|
}}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import React, { useEffect, useState, useRef, useMemo } from "react";
|
|
2
|
+
import { marked } from "marked";
|
|
2
3
|
import { useAnnotations } from "./AnnotationProvider";
|
|
3
|
-
import { generateMarkdown,
|
|
4
|
+
import { generateMarkdown, getMissingRequiredLabels } from "./generateMarkdown";
|
|
4
5
|
|
|
5
6
|
interface ResponsePreviewProps {
|
|
6
7
|
open: boolean;
|
|
@@ -119,12 +120,7 @@ export function ResponsePreview({ open, onClose, onSubmit }: ResponsePreviewProp
|
|
|
119
120
|
</button>
|
|
120
121
|
<button
|
|
121
122
|
onClick={() => {
|
|
122
|
-
const
|
|
123
|
-
const missingFeedback = getMissingRequiredFeedback(feedbackEntries);
|
|
124
|
-
const allMissing = [
|
|
125
|
-
...missingResponses.map((r) => r.label),
|
|
126
|
-
...missingFeedback.map((e) => e.label || e.id),
|
|
127
|
-
];
|
|
123
|
+
const allMissing = getMissingRequiredLabels(responses, feedbackEntries);
|
|
128
124
|
if (allMissing.length > 0) {
|
|
129
125
|
setValidationError(`Please answer: ${allMissing.join(", ")}`);
|
|
130
126
|
return;
|
|
@@ -148,113 +144,6 @@ export function ResponsePreview({ open, onClose, onSubmit }: ResponsePreviewProp
|
|
|
148
144
|
}
|
|
149
145
|
|
|
150
146
|
export function MarkdownPreview({ text }: { text: string }) {
|
|
151
|
-
const
|
|
152
|
-
return <div className="font-body text-body text-text-primary leading-relaxed"
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
function InlineText({ text }: { text: string }) {
|
|
156
|
-
const parts: React.ReactNode[] = [];
|
|
157
|
-
const regex = /(\*\*(.+?)\*\*|`(.+?)`)/g;
|
|
158
|
-
let last = 0;
|
|
159
|
-
let match: RegExpExecArray | null;
|
|
160
|
-
let key = 0;
|
|
161
|
-
|
|
162
|
-
while ((match = regex.exec(text)) !== null) {
|
|
163
|
-
if (match.index > last) parts.push(text.slice(last, match.index));
|
|
164
|
-
if (match[2]) parts.push(<strong key={key++} className="font-semibold text-text-primary">{match[2]}</strong>);
|
|
165
|
-
else if (match[3]) parts.push(<code key={key++} className="font-mono text-code bg-bg-code px-1 py-px rounded">{match[3]}</code>);
|
|
166
|
-
last = regex.lastIndex;
|
|
167
|
-
}
|
|
168
|
-
if (last < text.length) parts.push(text.slice(last));
|
|
169
|
-
return <>{parts}</>;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
function parseMarkdown(md: string): React.ReactNode[] {
|
|
173
|
-
const lines = md.split("\n");
|
|
174
|
-
const elements: React.ReactNode[] = [];
|
|
175
|
-
let i = 0;
|
|
176
|
-
let key = 0;
|
|
177
|
-
|
|
178
|
-
while (i < lines.length) {
|
|
179
|
-
const line = lines[i];
|
|
180
|
-
|
|
181
|
-
// Headings
|
|
182
|
-
if (line.startsWith("#### ")) {
|
|
183
|
-
elements.push(<h4 key={key++} className="text-[13px] font-body font-semibold text-text-primary mt-5 mb-1">{line.slice(5)}</h4>);
|
|
184
|
-
i++; continue;
|
|
185
|
-
}
|
|
186
|
-
if (line.startsWith("### ")) {
|
|
187
|
-
elements.push(<h3 key={key++} className="text-[14px] font-body font-semibold text-text-primary mt-5 mb-1">{line.slice(4)}</h3>);
|
|
188
|
-
i++; continue;
|
|
189
|
-
}
|
|
190
|
-
if (line.startsWith("## ")) {
|
|
191
|
-
elements.push(<h2 key={key++} className="text-task-label font-body font-semibold text-text-primary mt-6 mb-2 pb-1 border-b border-border-subtle">{line.slice(3)}</h2>);
|
|
192
|
-
i++; continue;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
// Blockquote — collect consecutive > lines
|
|
196
|
-
if (line.startsWith("> ")) {
|
|
197
|
-
const quoteLines: string[] = [];
|
|
198
|
-
while (i < lines.length && lines[i].startsWith("> ")) {
|
|
199
|
-
quoteLines.push(lines[i].slice(2));
|
|
200
|
-
i++;
|
|
201
|
-
}
|
|
202
|
-
elements.push(
|
|
203
|
-
<blockquote key={key++} className="border-l-2 border-border-hover pl-3 my-2 text-text-secondary italic text-[13px]">
|
|
204
|
-
{quoteLines.map((ql, j) => <div key={j}>{ql}</div>)}
|
|
205
|
-
</blockquote>
|
|
206
|
-
);
|
|
207
|
-
continue;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
// Checkbox list — collect consecutive - [x]/- [ ] lines
|
|
211
|
-
if (line.startsWith("- [x] ") || line.startsWith("- [ ] ")) {
|
|
212
|
-
const items: { checked: boolean; label: string }[] = [];
|
|
213
|
-
while (i < lines.length && (lines[i].startsWith("- [x] ") || lines[i].startsWith("- [ ] "))) {
|
|
214
|
-
items.push({ checked: lines[i].startsWith("- [x] "), label: lines[i].slice(6) });
|
|
215
|
-
i++;
|
|
216
|
-
}
|
|
217
|
-
elements.push(
|
|
218
|
-
<ul key={key++} className="my-1 space-y-0.5">
|
|
219
|
-
{items.map((item, j) => (
|
|
220
|
-
<li key={j} className="flex items-center gap-2 text-[13px]">
|
|
221
|
-
<span className={`w-3.5 h-3.5 rounded flex items-center justify-center flex-shrink-0 ${item.checked ? "bg-accent-amber" : "border border-border-hover"}`}>
|
|
222
|
-
{item.checked && (
|
|
223
|
-
<svg width="8" height="8" viewBox="0 0 10 10" fill="none">
|
|
224
|
-
<path d="M2.5 5L4.5 7L7.5 3" style={{ stroke: "var(--color-text-inverse)" }} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
|
|
225
|
-
</svg>
|
|
226
|
-
)}
|
|
227
|
-
</span>
|
|
228
|
-
<span className={item.checked ? "text-text-primary" : "text-text-tertiary"}>{item.label}</span>
|
|
229
|
-
</li>
|
|
230
|
-
))}
|
|
231
|
-
</ul>
|
|
232
|
-
);
|
|
233
|
-
continue;
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
// Regular list — collect consecutive - lines
|
|
237
|
-
if (line.startsWith("- ")) {
|
|
238
|
-
const items: string[] = [];
|
|
239
|
-
while (i < lines.length && lines[i].startsWith("- ")) {
|
|
240
|
-
items.push(lines[i].slice(2));
|
|
241
|
-
i++;
|
|
242
|
-
}
|
|
243
|
-
elements.push(
|
|
244
|
-
<ul key={key++} className="my-1 space-y-0.5 list-disc list-inside">
|
|
245
|
-
{items.map((item, j) => <li key={j} className="text-[13px] text-text-secondary"><InlineText text={item} /></li>)}
|
|
246
|
-
</ul>
|
|
247
|
-
);
|
|
248
|
-
continue;
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
// Empty line
|
|
252
|
-
if (line.trim() === "") { i++; continue; }
|
|
253
|
-
|
|
254
|
-
// Paragraph
|
|
255
|
-
elements.push(<p key={key++} className="my-1.5 text-[13px]"><InlineText text={line} /></p>);
|
|
256
|
-
i++;
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
return elements;
|
|
147
|
+
const html = useMemo(() => marked.parse(text, { async: false }) as string, [text]);
|
|
148
|
+
return <div className="prose-canvas font-body text-body text-text-primary leading-relaxed" dangerouslySetInnerHTML={{ __html: html }} />;
|
|
260
149
|
}
|
|
@@ -29,7 +29,11 @@ export function SessionSwitcher({ currentSessionId, projectRoot }: SessionSwitch
|
|
|
29
29
|
const fetchSessions = async () => {
|
|
30
30
|
try {
|
|
31
31
|
const res = await fetch("/api/sessions");
|
|
32
|
-
|
|
32
|
+
const data = await res.json() as SessionInfo[];
|
|
33
|
+
setSessions((prev) => {
|
|
34
|
+
const next = JSON.stringify(data);
|
|
35
|
+
return JSON.stringify(prev) === next ? prev : data;
|
|
36
|
+
});
|
|
33
37
|
} catch {}
|
|
34
38
|
};
|
|
35
39
|
fetchSessions();
|
|
@@ -51,17 +55,24 @@ export function SessionSwitcher({ currentSessionId, projectRoot }: SessionSwitch
|
|
|
51
55
|
|
|
52
56
|
return (
|
|
53
57
|
<div className="relative" ref={ref}>
|
|
54
|
-
<
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
58
|
+
<div className="flex items-center gap-1.5 font-body text-body font-medium">
|
|
59
|
+
<a
|
|
60
|
+
href={`/s/${currentSessionId}`}
|
|
61
|
+
className="text-text-primary hover:text-text-secondary transition-colors"
|
|
62
|
+
>
|
|
63
|
+
{displayName}
|
|
64
|
+
</a>
|
|
59
65
|
{otherSessions.length > 0 && (
|
|
60
|
-
<
|
|
61
|
-
|
|
62
|
-
|
|
66
|
+
<button
|
|
67
|
+
onClick={() => setOpen(!open)}
|
|
68
|
+
className="flex items-center justify-center text-text-tertiary hover:text-text-secondary transition-colors"
|
|
69
|
+
>
|
|
70
|
+
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" className={`transition-transform duration-200 ${open ? "rotate-180" : ""}`}>
|
|
71
|
+
<path d="M3 5l3 3 3-3" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
|
|
72
|
+
</svg>
|
|
73
|
+
</button>
|
|
63
74
|
)}
|
|
64
|
-
</
|
|
75
|
+
</div>
|
|
65
76
|
|
|
66
77
|
{open && otherSessions.length > 0 && (
|
|
67
78
|
<div className="absolute top-full left-0 mt-2 w-64 bg-bg-elevated border border-border-hover rounded-lg shadow-lg z-50 py-1 overflow-hidden">
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import React, { useMemo } from "react";
|
|
2
|
+
import { marked } from "marked";
|
|
3
|
+
|
|
4
|
+
interface MarkdownProps {
|
|
5
|
+
/** Inline markdown source (via children) */
|
|
6
|
+
children?: React.ReactNode;
|
|
7
|
+
/** File path (resolved at compile time) */
|
|
8
|
+
file?: string;
|
|
9
|
+
/** Injected by compiler when file prop is used */
|
|
10
|
+
__content?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Configure marked for safe rendering
|
|
14
|
+
marked.setOptions({
|
|
15
|
+
gfm: true,
|
|
16
|
+
breaks: false,
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
export function Markdown({ children, file, __content }: MarkdownProps) {
|
|
20
|
+
const source = __content ?? (typeof children === "string" ? children : String(children ?? ""));
|
|
21
|
+
|
|
22
|
+
const html = useMemo(() => {
|
|
23
|
+
if (!source) return "";
|
|
24
|
+
return marked.parse(source, { async: false }) as string;
|
|
25
|
+
}, [source]);
|
|
26
|
+
|
|
27
|
+
if (!source) {
|
|
28
|
+
return (
|
|
29
|
+
<div className="text-text-tertiary text-tiny mt-3">
|
|
30
|
+
{file ? `Could not load: ${file}` : "No markdown content"}
|
|
31
|
+
</div>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<div
|
|
37
|
+
className="mt-3 prose-canvas"
|
|
38
|
+
data-md="markdown"
|
|
39
|
+
{...(file ? { "data-md-file": file } : {})}
|
|
40
|
+
data-md-source={source}
|
|
41
|
+
dangerouslySetInnerHTML={{ __html: html }}
|
|
42
|
+
/>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
+
import { autoResizeTextarea } from "../utils";
|
|
2
3
|
|
|
3
4
|
/** Seamless note input for response components */
|
|
4
5
|
export function ResponseNote({ note, onChange }: { show?: boolean; note: string; onToggle?: () => void; onChange: (n: string) => void }) {
|
|
@@ -10,14 +11,8 @@ export function ResponseNote({ note, onChange }: { show?: boolean; note: string;
|
|
|
10
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"
|
|
11
12
|
placeholder="Add a note..."
|
|
12
13
|
rows={1}
|
|
13
|
-
onInput={(e) =>
|
|
14
|
-
|
|
15
|
-
t.style.height = "auto";
|
|
16
|
-
t.style.height = t.scrollHeight + "px";
|
|
17
|
-
}}
|
|
18
|
-
ref={(el) => {
|
|
19
|
-
if (el) { el.style.height = "auto"; el.style.height = el.scrollHeight + "px"; }
|
|
20
|
-
}}
|
|
14
|
+
onInput={(e) => autoResizeTextarea(e.target as HTMLTextAreaElement)}
|
|
15
|
+
ref={(el) => { if (el) autoResizeTextarea(el); }}
|
|
21
16
|
/>
|
|
22
17
|
</div>
|
|
23
18
|
);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import React, { useEffect, useRef, useState } from "react";
|
|
2
2
|
import { useAnnotations } from "#canvas/runtime";
|
|
3
3
|
import { ResponseNote } from "./ResponseNote";
|
|
4
|
+
import { autoResizeTextarea } from "../utils";
|
|
4
5
|
|
|
5
6
|
/** Free text input from user */
|
|
6
7
|
interface UserInputProps {
|
|
@@ -29,10 +30,7 @@ export function UserInput({ id, label, placeholder, multiline, required }: UserI
|
|
|
29
30
|
setResponse(id, { ...current!, value: v });
|
|
30
31
|
};
|
|
31
32
|
|
|
32
|
-
const autoResize = (el: HTMLTextAreaElement) =>
|
|
33
|
-
el.style.height = "auto";
|
|
34
|
-
el.style.height = Math.max(multiline ? 60 : 32, el.scrollHeight) + "px";
|
|
35
|
-
};
|
|
33
|
+
const autoResize = (el: HTMLTextAreaElement) => autoResizeTextarea(el, multiline ? 60 : 32);
|
|
36
34
|
|
|
37
35
|
const showError = current?.required && !value.trim();
|
|
38
36
|
|
|
@@ -12,4 +12,5 @@ export { Diff } from "./Diff";
|
|
|
12
12
|
export { Choice, MultiChoice } from "./Choice";
|
|
13
13
|
export { UserInput, RangeInput } from "./UserInput";
|
|
14
14
|
export { ImageView } from "./Image";
|
|
15
|
+
export { Markdown } from "./Markdown";
|
|
15
16
|
export { useFeedback } from "#canvas/runtime";
|
package/daemon/client/domDiff.ts
CHANGED
|
@@ -321,16 +321,96 @@ export function runDomDiff(
|
|
|
321
321
|
oldContainer: HTMLElement,
|
|
322
322
|
newContainer: HTMLElement
|
|
323
323
|
): boolean {
|
|
324
|
+
// Handle mermaid blocks separately via data-md-source comparison
|
|
325
|
+
const mermaidChanged = diffMermaidBlocks(oldContainer, newContainer);
|
|
326
|
+
|
|
324
327
|
const oldExtract = extractWords(oldContainer);
|
|
325
328
|
const newExtract = extractWords(newContainer);
|
|
326
329
|
|
|
327
330
|
const { oldOps, newOps } = computeWordDiff(oldExtract.words, newExtract.words);
|
|
328
331
|
|
|
329
|
-
const
|
|
332
|
+
const hasWordChanges =
|
|
330
333
|
oldOps.some((op) => op.type === "removed") || newOps.some((op) => op.type === "added");
|
|
331
334
|
|
|
332
335
|
applyHighlightsByNode(oldOps, oldExtract.map, "diff-removed");
|
|
333
336
|
applyHighlightsByNode(newOps, newExtract.map, "diff-added");
|
|
334
337
|
|
|
335
|
-
return
|
|
338
|
+
return hasWordChanges || mermaidChanged;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Compare mermaid blocks between old and new containers by data-md-source.
|
|
343
|
+
* Highlights changed mermaid blocks with a border and appends a source diff.
|
|
344
|
+
*/
|
|
345
|
+
function diffMermaidBlocks(oldContainer: HTMLElement, newContainer: HTMLElement): boolean {
|
|
346
|
+
const oldMermaids = Array.from(oldContainer.querySelectorAll<HTMLElement>('[data-md="mermaid"]'));
|
|
347
|
+
const newMermaids = Array.from(newContainer.querySelectorAll<HTMLElement>('[data-md="mermaid"]'));
|
|
348
|
+
let changed = false;
|
|
349
|
+
|
|
350
|
+
// Match by position (simple 1:1 pairing)
|
|
351
|
+
const count = Math.max(oldMermaids.length, newMermaids.length);
|
|
352
|
+
for (let i = 0; i < count; i++) {
|
|
353
|
+
const oldEl = oldMermaids[i];
|
|
354
|
+
const newEl = newMermaids[i];
|
|
355
|
+
|
|
356
|
+
if (!oldEl && newEl) {
|
|
357
|
+
// Added
|
|
358
|
+
newEl.classList.add("diff-block-added");
|
|
359
|
+
newEl.style.position = "relative";
|
|
360
|
+
changed = true;
|
|
361
|
+
continue;
|
|
362
|
+
}
|
|
363
|
+
if (oldEl && !newEl) {
|
|
364
|
+
// Removed
|
|
365
|
+
oldEl.classList.add("diff-block-removed");
|
|
366
|
+
oldEl.style.position = "relative";
|
|
367
|
+
changed = true;
|
|
368
|
+
continue;
|
|
369
|
+
}
|
|
370
|
+
if (!oldEl || !newEl) continue;
|
|
371
|
+
|
|
372
|
+
const oldSource = (oldEl.getAttribute("data-md-source") || "").trim();
|
|
373
|
+
const newSource = (newEl.getAttribute("data-md-source") || "").trim();
|
|
374
|
+
|
|
375
|
+
if (oldSource === newSource) continue;
|
|
376
|
+
changed = true;
|
|
377
|
+
|
|
378
|
+
// Highlight containers
|
|
379
|
+
oldEl.style.borderLeft = "3px solid var(--color-accent-red)";
|
|
380
|
+
oldEl.style.paddingLeft = "8px";
|
|
381
|
+
newEl.style.borderLeft = "3px solid var(--color-accent-green)";
|
|
382
|
+
newEl.style.paddingLeft = "8px";
|
|
383
|
+
|
|
384
|
+
// Append source diffs
|
|
385
|
+
const oldLines = oldSource.split("\n");
|
|
386
|
+
const newLines = newSource.split("\n");
|
|
387
|
+
const lcs = simpleLCS(oldLines, newLines);
|
|
388
|
+
|
|
389
|
+
const buildDiffEl = (lines: string[], lcsArr: string[], side: "old" | "new") => {
|
|
390
|
+
const container = document.createElement("div");
|
|
391
|
+
container.className = "mt-2 bg-bg-code rounded-md overflow-hidden text-tiny font-mono";
|
|
392
|
+
container.setAttribute("data-no-diff", "true");
|
|
393
|
+
|
|
394
|
+
let li = 0, si = 0;
|
|
395
|
+
while (si < lines.length) {
|
|
396
|
+
const row = document.createElement("div");
|
|
397
|
+
row.className = "px-3 py-0.5 whitespace-pre-wrap";
|
|
398
|
+
if (li < lcsArr.length && lines[si] === lcsArr[li]) {
|
|
399
|
+
row.textContent = " " + lines[si];
|
|
400
|
+
si++; li++;
|
|
401
|
+
} else {
|
|
402
|
+
row.classList.add(side === "old" ? "diff-removed" : "diff-added");
|
|
403
|
+
row.textContent = (side === "old" ? "- " : "+ ") + lines[si];
|
|
404
|
+
si++;
|
|
405
|
+
}
|
|
406
|
+
container.appendChild(row);
|
|
407
|
+
}
|
|
408
|
+
return container;
|
|
409
|
+
};
|
|
410
|
+
|
|
411
|
+
oldEl.appendChild(buildDiffEl(oldLines, lcs, "old"));
|
|
412
|
+
newEl.appendChild(buildDiffEl(newLines, lcs, "new"));
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
return changed;
|
|
336
416
|
}
|
|
@@ -99,6 +99,16 @@ export function getMissingRequiredFeedback(entries: Map<string, FeedbackEntry>):
|
|
|
99
99
|
return missing;
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
+
/** Returns list of missing required field labels, or empty if all valid */
|
|
103
|
+
export function getMissingRequiredLabels(responses: Map<string, PlanResponse>, feedbackEntries: Map<string, FeedbackEntry>): string[] {
|
|
104
|
+
const missingResponses = getMissingRequired(responses);
|
|
105
|
+
const missingFeedback = getMissingRequiredFeedback(feedbackEntries);
|
|
106
|
+
return [
|
|
107
|
+
...missingResponses.map((r) => r.label),
|
|
108
|
+
...missingFeedback.map((e) => e.label || e.id),
|
|
109
|
+
];
|
|
110
|
+
}
|
|
111
|
+
|
|
102
112
|
function renderResponse(r: PlanResponse): string {
|
|
103
113
|
const lines: string[] = [];
|
|
104
114
|
lines.push(`**${r.label}**`);
|
|
@@ -134,17 +144,44 @@ function renderResponse(r: PlanResponse): string {
|
|
|
134
144
|
function renderAnnotation(ann: Annotation): string {
|
|
135
145
|
const lines: string[] = [];
|
|
136
146
|
const snippet = ann.snippet.trim();
|
|
137
|
-
const
|
|
147
|
+
const ctx = ann.context;
|
|
148
|
+
const hasLineInfo = ctx?.lineStart != null;
|
|
138
149
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
// Longer selections — just first/last line
|
|
150
|
+
if (ann.filePath && hasLineInfo) {
|
|
151
|
+
// File annotation with line numbers
|
|
152
|
+
const lineStart = ctx!.lineStart!;
|
|
153
|
+
const lineEnd = ctx!.lineEnd ?? lineStart;
|
|
144
154
|
const snippetLines = snippet.split("\n");
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
155
|
+
const isShort = snippet.length < 30 && lineStart === lineEnd;
|
|
156
|
+
|
|
157
|
+
if (isShort && (ctx!.before || ctx!.after)) {
|
|
158
|
+
// Short snippet on single line — show full line context
|
|
159
|
+
const expanded = formatSnippetInContext(ann);
|
|
160
|
+
lines.push(`> L${lineStart}: ${expanded}`);
|
|
161
|
+
} else if (snippetLines.length <= 6) {
|
|
162
|
+
for (let i = 0; i < snippetLines.length; i++) {
|
|
163
|
+
lines.push(`> ${lineStart + i} | ${snippetLines[i]}`);
|
|
164
|
+
}
|
|
165
|
+
} else {
|
|
166
|
+
for (let i = 0; i < 3; i++) {
|
|
167
|
+
lines.push(`> ${lineStart + i} | ${snippetLines[i]}`);
|
|
168
|
+
}
|
|
169
|
+
lines.push(`> ... (${snippetLines.length} lines)`);
|
|
170
|
+
for (let i = snippetLines.length - 3; i < snippetLines.length; i++) {
|
|
171
|
+
lines.push(`> ${lineStart + i} | ${snippetLines[i]}`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
} else {
|
|
175
|
+
// Plan annotations or file annotations without line info
|
|
176
|
+
const context = formatSnippetInContext(ann);
|
|
177
|
+
if (snippet.split("\n").length <= 3) {
|
|
178
|
+
lines.push(`> ${context.split("\n").join("\n> ")}`);
|
|
179
|
+
} else {
|
|
180
|
+
const snippetLines = snippet.split("\n");
|
|
181
|
+
lines.push(`> ${snippetLines[0]}`);
|
|
182
|
+
lines.push(`> ... (${snippetLines.length} lines)`);
|
|
183
|
+
lines.push(`> ${snippetLines[snippetLines.length - 1]}`);
|
|
184
|
+
}
|
|
148
185
|
}
|
|
149
186
|
|
|
150
187
|
// Comment
|
|
@@ -99,13 +99,15 @@ export function setMarkActive(annotationId: string, active: boolean) {
|
|
|
99
99
|
}
|
|
100
100
|
|
|
101
101
|
/**
|
|
102
|
-
* Update active state for
|
|
102
|
+
* Update active state for annotation marks.
|
|
103
|
+
* Only touches marks for the previous and current active IDs to avoid full DOM scans.
|
|
103
104
|
*/
|
|
104
|
-
export function updateAllMarkStates(activeId: string | null) {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
105
|
+
export function updateAllMarkStates(activeId: string | null, prevActiveId?: string | null) {
|
|
106
|
+
if (prevActiveId != null && prevActiveId !== activeId) {
|
|
107
|
+
setMarkActive(prevActiveId, false);
|
|
108
|
+
}
|
|
109
|
+
if (activeId != null) {
|
|
110
|
+
setMarkActive(activeId, true);
|
|
109
111
|
}
|
|
110
112
|
}
|
|
111
113
|
|
package/daemon/client/runtime.ts
CHANGED
package/daemon/client/styles.css
CHANGED
|
@@ -258,6 +258,132 @@ input[type="range"]::-moz-range-track {
|
|
|
258
258
|
background: transparent;
|
|
259
259
|
}
|
|
260
260
|
|
|
261
|
+
/* Prose (Markdown component) */
|
|
262
|
+
.prose-canvas {
|
|
263
|
+
color: var(--color-text-secondary);
|
|
264
|
+
font-size: 0.875rem;
|
|
265
|
+
line-height: 1.7;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
.prose-canvas h1, .prose-canvas h2, .prose-canvas h3,
|
|
269
|
+
.prose-canvas h4, .prose-canvas h5, .prose-canvas h6 {
|
|
270
|
+
color: var(--color-text-primary);
|
|
271
|
+
font-weight: 600;
|
|
272
|
+
margin-top: 1.5em;
|
|
273
|
+
margin-bottom: 0.5em;
|
|
274
|
+
line-height: 1.3;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
.prose-canvas h1 { font-size: 1.5rem; }
|
|
278
|
+
.prose-canvas h2 { font-size: 1.25rem; }
|
|
279
|
+
.prose-canvas h3 { font-size: 1.1rem; }
|
|
280
|
+
|
|
281
|
+
.prose-canvas h1:first-child, .prose-canvas h2:first-child,
|
|
282
|
+
.prose-canvas h3:first-child {
|
|
283
|
+
margin-top: 0;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
.prose-canvas p {
|
|
287
|
+
margin-top: 0.75em;
|
|
288
|
+
margin-bottom: 0.75em;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
.prose-canvas p:first-child {
|
|
292
|
+
margin-top: 0;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
.prose-canvas a {
|
|
296
|
+
color: var(--color-accent-blue);
|
|
297
|
+
text-decoration: underline;
|
|
298
|
+
text-decoration-color: color-mix(in srgb, var(--color-accent-blue) 40%, transparent);
|
|
299
|
+
text-underline-offset: 2px;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
.prose-canvas a:hover {
|
|
303
|
+
text-decoration-color: var(--color-accent-blue);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
.prose-canvas strong {
|
|
307
|
+
color: var(--color-text-primary);
|
|
308
|
+
font-weight: 600;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
.prose-canvas code {
|
|
312
|
+
font-size: 0.8em;
|
|
313
|
+
background: var(--color-bg-code);
|
|
314
|
+
color: var(--color-text-code);
|
|
315
|
+
padding: 0.15em 0.4em;
|
|
316
|
+
border-radius: 4px;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
.prose-canvas pre {
|
|
320
|
+
background: var(--color-bg-code);
|
|
321
|
+
border-radius: 8px;
|
|
322
|
+
padding: 1em;
|
|
323
|
+
overflow-x: auto;
|
|
324
|
+
margin-top: 0.75em;
|
|
325
|
+
margin-bottom: 0.75em;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
.prose-canvas pre code {
|
|
329
|
+
background: transparent;
|
|
330
|
+
padding: 0;
|
|
331
|
+
font-size: 0.8rem;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
.prose-canvas ul, .prose-canvas ol {
|
|
335
|
+
padding-left: 1.5em;
|
|
336
|
+
margin-top: 0.5em;
|
|
337
|
+
margin-bottom: 0.5em;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
.prose-canvas ul { list-style-type: disc; }
|
|
341
|
+
.prose-canvas ol { list-style-type: decimal; }
|
|
342
|
+
.prose-canvas li { margin-top: 0.25em; }
|
|
343
|
+
.prose-canvas li > ul, .prose-canvas li > ol { margin-top: 0.25em; margin-bottom: 0; }
|
|
344
|
+
|
|
345
|
+
.prose-canvas blockquote {
|
|
346
|
+
border-left: 3px solid var(--color-border-strong);
|
|
347
|
+
padding-left: 1em;
|
|
348
|
+
margin-left: 0;
|
|
349
|
+
margin-top: 0.75em;
|
|
350
|
+
margin-bottom: 0.75em;
|
|
351
|
+
color: var(--color-text-tertiary);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
.prose-canvas hr {
|
|
355
|
+
border: none;
|
|
356
|
+
border-top: 1px solid var(--color-border-subtle);
|
|
357
|
+
margin: 1.5em 0;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
.prose-canvas table {
|
|
361
|
+
width: 100%;
|
|
362
|
+
border-collapse: collapse;
|
|
363
|
+
margin-top: 0.75em;
|
|
364
|
+
margin-bottom: 0.75em;
|
|
365
|
+
font-size: 0.8rem;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
.prose-canvas th, .prose-canvas td {
|
|
369
|
+
border: 1px solid var(--color-border-subtle);
|
|
370
|
+
padding: 0.5em 0.75em;
|
|
371
|
+
text-align: left;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
.prose-canvas th {
|
|
375
|
+
background: var(--color-bg-elevated);
|
|
376
|
+
color: var(--color-text-primary);
|
|
377
|
+
font-weight: 600;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
.prose-canvas img {
|
|
381
|
+
max-width: 100%;
|
|
382
|
+
border-radius: 8px;
|
|
383
|
+
margin-top: 0.75em;
|
|
384
|
+
margin-bottom: 0.75em;
|
|
385
|
+
}
|
|
386
|
+
|
|
261
387
|
/* highlight.js overrides */
|
|
262
388
|
.hljs {
|
|
263
389
|
background: transparent !important;
|