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.
- package/README.md +14 -0
- package/bin/lib/commands/daemon.ts +3 -6
- package/bin/lib/commands/push.ts +1 -1
- package/bin/lib/daemon-lifecycle.ts +65 -72
- package/bin/lib/helpers.test.ts +1 -0
- package/bin/lib/helpers.ts +30 -74
- package/daemon/build.ts +37 -16
- package/daemon/client/App.tsx +27 -61
- package/daemon/client/CompareView.tsx +4 -4
- package/daemon/client/FileBrowser.tsx +4 -4
- package/daemon/client/FileViewer.tsx +4 -4
- package/daemon/client/OverviewView.tsx +2 -2
- package/daemon/client/RevisionSelect.tsx +1 -1
- package/daemon/client/canvas-runtime.ts +1 -0
- package/daemon/client/clientApi.ts +18 -0
- package/daemon/client/components/index.ts +3 -16
- package/daemon/client/styles.css +4 -457
- package/daemon/dist/annotations-runtime.js +1 -0
- package/daemon/dist/client.css +1 -1
- package/daemon/dist/client.js +76 -76
- package/daemon/dist/components.js +59 -59
- package/daemon/dist/index.html +1 -0
- package/daemon/dist/preact-compat.js +1 -1
- package/daemon/dist/runtime.js +1 -1
- package/daemon/src/components.ts +13 -0
- package/daemon/src/handlers/api.ts +13 -9
- package/daemon/src/handlers/files.ts +2 -3
- package/daemon/src/handlers/static.ts +1 -1
- package/daemon/src/handlers/uploads.ts +2 -3
- package/daemon/src/paths.ts +10 -24
- package/daemon/src/remote-feedback.test.ts +1 -1
- package/daemon/src/remote-feedback.ts +1 -1
- package/daemon/src/secrets-api.test.ts +2 -2
- package/daemon/src/server.ts +11 -10
- package/daemon/src/share-e2e.test.ts +1 -1
- package/daemon/src/share.test.ts +1 -1
- package/daemon/src/share.ts +1 -1
- package/daemon/tsconfig.json +1 -3
- package/package.json +11 -4
- package/.github/dark.png +0 -0
- package/.github/light.png +0 -0
- package/.github/workflows/ci.yml +0 -48
- package/.github/workflows/deploy-worker-prod.yml +0 -19
- package/.github/workflows/deploy-worker-stage.yml +0 -21
- package/.github/workflows/deploy-worker.yml +0 -72
- package/.github/workflows/release.yml +0 -36
- package/CLAUDE.md +0 -86
- package/bun.lock +0 -377
- package/daemon/client/AnnotationEditor.tsx +0 -315
- package/daemon/client/AnnotationProvider.tsx +0 -195
- package/daemon/client/AnnotationSidebar.tsx +0 -627
- package/daemon/client/FileIcon.tsx +0 -76
- package/daemon/client/PlanRenderer.tsx +0 -614
- package/daemon/client/Popover.tsx +0 -350
- package/daemon/client/RenderErrorContext.ts +0 -11
- package/daemon/client/ResponsePreview.tsx +0 -186
- package/daemon/client/SessionContext.tsx +0 -3
- package/daemon/client/annotationContext.ts +0 -114
- package/daemon/client/annotationDom.ts +0 -104
- package/daemon/client/annotationDraft.test.ts +0 -188
- package/daemon/client/annotationDraft.ts +0 -130
- package/daemon/client/compat/preact-all.ts +0 -4
- package/daemon/client/components/Callout.tsx +0 -26
- package/daemon/client/components/Checklist.tsx +0 -37
- package/daemon/client/components/Choice.tsx +0 -176
- package/daemon/client/components/CodeBlock.tsx +0 -35
- package/daemon/client/components/Diff.tsx +0 -65
- package/daemon/client/components/FilePreview.tsx +0 -88
- package/daemon/client/components/Image.tsx +0 -45
- package/daemon/client/components/Markdown.tsx +0 -44
- package/daemon/client/components/Mermaid.tsx +0 -565
- package/daemon/client/components/Note.tsx +0 -14
- package/daemon/client/components/Priority.tsx +0 -19
- package/daemon/client/components/ResponseNote.tsx +0 -19
- package/daemon/client/components/Section.tsx +0 -33
- package/daemon/client/components/Table.tsx +0 -35
- package/daemon/client/components/Task.tsx +0 -76
- package/daemon/client/components/UserInput.tsx +0 -174
- package/daemon/client/components/renderMarkdownChildren.tsx +0 -97
- package/daemon/client/generateMarkdown.ts +0 -273
- package/daemon/client/highlightRange.ts +0 -224
- package/daemon/client/popoverPosition.ts +0 -31
- package/daemon/client/runtime.ts +0 -123
- package/daemon/client/theme.css +0 -66
- package/daemon/client/useTextAnnotation.tsx +0 -188
- package/daemon/client/utils.ts +0 -13
- package/daemon/src/compiler.test.ts +0 -57
- package/daemon/src/compiler.ts +0 -360
- package/daemon/src/handlers/utils.ts +0 -14
- package/daemon/src/router.ts +0 -16
- package/daemon/src/session-secrets.test.ts +0 -83
- package/daemon/src/session.ts +0 -612
- package/daemon/src/watcher.ts +0 -71
- package/daemon/src/websocket.test.ts +0 -99
- package/daemon/src/websocket.ts +0 -211
- package/tsconfig.json +0 -14
- package/workers/canvas-share/README.md +0 -159
- package/workers/canvas-share/oblaka.ts +0 -56
- package/workers/canvas-share/package.json +0 -25
- package/workers/canvas-share/public/assets/.gitkeep +0 -0
- package/workers/canvas-share/public/assets/client.css +0 -2
- package/workers/canvas-share/public/assets/client.js +0 -91
- package/workers/canvas-share/public/assets/components.js +0 -65
- package/workers/canvas-share/public/assets/jsx-runtime.js +0 -3
- package/workers/canvas-share/public/assets/preact-compat.js +0 -1
- package/workers/canvas-share/public/assets/runtime.js +0 -1
- package/workers/canvas-share/public/index.html +0 -385
- package/workers/canvas-share/public/screenshot-dark.png +0 -0
- package/workers/canvas-share/public/screenshot-light.png +0 -0
- package/workers/canvas-share/src/index.test.ts +0 -209
- package/workers/canvas-share/src/index.ts +0 -518
- package/workers/canvas-share/src/types.ts +0 -119
- package/workers/canvas-share/src/util.ts +0 -156
- package/workers/canvas-share/src/validation.ts +0 -160
- package/workers/canvas-share/tsconfig.json +0 -16
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import React from "react";
|
|
2
|
-
|
|
3
|
-
interface TableProps {
|
|
4
|
-
headers: string[];
|
|
5
|
-
rows: string[][];
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export function Table({ headers, rows }: TableProps) {
|
|
9
|
-
return (
|
|
10
|
-
<div className="overflow-x-auto mt-3" data-md="table">
|
|
11
|
-
<table className="w-full text-body">
|
|
12
|
-
<thead>
|
|
13
|
-
<tr className="border-b border-border-subtle">
|
|
14
|
-
{headers.map((h, i) => (
|
|
15
|
-
<th key={i} className="text-left px-4 py-2.5 text-meta font-medium uppercase tracking-wider text-text-tertiary font-body">
|
|
16
|
-
{h}
|
|
17
|
-
</th>
|
|
18
|
-
))}
|
|
19
|
-
</tr>
|
|
20
|
-
</thead>
|
|
21
|
-
<tbody>
|
|
22
|
-
{rows.map((row, ri) => (
|
|
23
|
-
<tr key={ri} className={`relative border-b border-border-subtle last:border-b-0 transition-colors hover:bg-bg-surface/50 ${ri % 2 === 1 ? "bg-bg-surface/30" : ""}`}>
|
|
24
|
-
{row.map((cell, ci) => (
|
|
25
|
-
<td key={ci} className="px-4 py-2.5 text-text-secondary font-body">
|
|
26
|
-
{cell}
|
|
27
|
-
</td>
|
|
28
|
-
))}
|
|
29
|
-
</tr>
|
|
30
|
-
))}
|
|
31
|
-
</tbody>
|
|
32
|
-
</table>
|
|
33
|
-
</div>
|
|
34
|
-
);
|
|
35
|
-
}
|
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
import React from "react";
|
|
2
|
-
import { renderMarkdownChildren } from "./renderMarkdownChildren";
|
|
3
|
-
|
|
4
|
-
type Status = "todo" | "done" | "blocked" | "in-progress";
|
|
5
|
-
type BadgeVariant = "info" | "success" | "warning" | "danger" | "neutral";
|
|
6
|
-
|
|
7
|
-
interface ItemProps {
|
|
8
|
-
id: string;
|
|
9
|
-
label: string;
|
|
10
|
-
status?: Status;
|
|
11
|
-
badge?: string;
|
|
12
|
-
badgeVariant?: BadgeVariant;
|
|
13
|
-
children?: React.ReactNode;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
const dotColors: Record<Status, string> = {
|
|
17
|
-
todo: "bg-border-hover",
|
|
18
|
-
done: "bg-accent-green",
|
|
19
|
-
blocked: "bg-accent-red",
|
|
20
|
-
"in-progress": "bg-accent-blue",
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
const statusLabels: Record<Status, string> = {
|
|
24
|
-
todo: "Todo",
|
|
25
|
-
done: "Done",
|
|
26
|
-
blocked: "Blocked",
|
|
27
|
-
"in-progress": "In progress",
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
const badgeStyles: Record<BadgeVariant, string> = {
|
|
31
|
-
info: "bg-accent-blue-muted text-accent-blue",
|
|
32
|
-
success: "bg-accent-green-muted text-accent-green",
|
|
33
|
-
warning: "bg-accent-amber-muted text-accent-amber",
|
|
34
|
-
danger: "bg-accent-red-muted text-accent-red",
|
|
35
|
-
neutral: "bg-border-subtle text-text-tertiary",
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
export function Item({ id, label, status, badge, badgeVariant = "neutral", children }: ItemProps) {
|
|
39
|
-
return (
|
|
40
|
-
<div
|
|
41
|
-
className="group/task relative -mx-4 px-4 py-3 rounded-md transition-colors duration-150 hover:bg-bg-input"
|
|
42
|
-
data-task-id={id}
|
|
43
|
-
data-md="item"
|
|
44
|
-
data-md-label={label}
|
|
45
|
-
{...(status ? { "data-md-status": status } : {})}
|
|
46
|
-
{...(badge ? { "data-md-badge": badge } : {})}
|
|
47
|
-
>
|
|
48
|
-
<div className="flex items-start gap-3">
|
|
49
|
-
{status && <span className={`w-2 h-2 rounded-full mt-[7px] flex-shrink-0 ${dotColors[status]}`} />}
|
|
50
|
-
<div className="flex-1 min-w-0">
|
|
51
|
-
<div className="flex items-baseline gap-2">
|
|
52
|
-
<span className="font-body text-task-label font-semibold text-text-primary">{label}</span>
|
|
53
|
-
{status && (
|
|
54
|
-
<span className="font-body text-tiny uppercase tracking-wider text-text-tertiary opacity-0 group-hover/task:opacity-100 transition-opacity duration-150">
|
|
55
|
-
{statusLabels[status]}
|
|
56
|
-
</span>
|
|
57
|
-
)}
|
|
58
|
-
{badge && (
|
|
59
|
-
<span className={`font-body text-tiny font-medium px-1.5 py-px rounded-full ${badgeStyles[badgeVariant]}`}>
|
|
60
|
-
{badge}
|
|
61
|
-
</span>
|
|
62
|
-
)}
|
|
63
|
-
</div>
|
|
64
|
-
{children && (
|
|
65
|
-
<div className="text-body text-text-secondary mt-1 leading-relaxed">{renderMarkdownChildren(children)}</div>
|
|
66
|
-
)}
|
|
67
|
-
</div>
|
|
68
|
-
</div>
|
|
69
|
-
</div>
|
|
70
|
-
);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/** @deprecated Use <Item> instead */
|
|
74
|
-
export function Task(props: ItemProps) {
|
|
75
|
-
return <Item {...props} />;
|
|
76
|
-
}
|
|
@@ -1,174 +0,0 @@
|
|
|
1
|
-
import React, { useRef, useState } from "react";
|
|
2
|
-
import { useAnnotations } from "#canvas/runtime";
|
|
3
|
-
import { ResponseNote } from "./ResponseNote";
|
|
4
|
-
import { autoResizeTextarea } from "../utils";
|
|
5
|
-
|
|
6
|
-
/** Free text input from user */
|
|
7
|
-
interface UserInputProps {
|
|
8
|
-
id: string;
|
|
9
|
-
label: string;
|
|
10
|
-
placeholder?: string;
|
|
11
|
-
multiline?: boolean;
|
|
12
|
-
required?: boolean;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export function UserInput({ id, label, placeholder, multiline, required }: UserInputProps) {
|
|
16
|
-
const { responses, setResponse } = useAnnotations();
|
|
17
|
-
const current = responses.get(id);
|
|
18
|
-
const value = (current?.value as string) ?? "";
|
|
19
|
-
const note = current?.note || "";
|
|
20
|
-
const [showNote, setShowNote] = useState(false);
|
|
21
|
-
const ref = useRef<HTMLTextAreaElement>(null);
|
|
22
|
-
|
|
23
|
-
if (!responses.has(id)) {
|
|
24
|
-
setResponse(id, { id, type: "text", label, value: "", required });
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const handleChange = (v: string) => {
|
|
28
|
-
setResponse(id, { ...current!, value: v });
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
const autoResize = (el: HTMLTextAreaElement) => autoResizeTextarea(el, multiline ? 60 : 32);
|
|
32
|
-
|
|
33
|
-
const showError = current?.required && !value.trim();
|
|
34
|
-
|
|
35
|
-
return (
|
|
36
|
-
<div data-md="userinput" data-md-label={label} className="-mx-4 px-4 py-3 my-1 rounded-lg transition-colors duration-150 hover:bg-bg-input">
|
|
37
|
-
<div className="flex items-baseline gap-2 mb-2">
|
|
38
|
-
<span className="text-[13px] font-body font-medium text-text-primary">{label}</span>
|
|
39
|
-
{required && <span className="text-[10px] text-accent-red font-body">*</span>}
|
|
40
|
-
</div>
|
|
41
|
-
<textarea
|
|
42
|
-
ref={ref}
|
|
43
|
-
value={value}
|
|
44
|
-
onChange={(e) => { handleChange(e.target.value); autoResize(e.target); }}
|
|
45
|
-
placeholder={placeholder || "Type your response..."}
|
|
46
|
-
rows={multiline ? 3 : 1}
|
|
47
|
-
className="w-full bg-bg-input text-[13px] font-body text-text-primary rounded-lg px-3 py-2 resize-none focus:outline-none border border-border-subtle focus:border-border-hover placeholder:text-text-tertiary transition-colors"
|
|
48
|
-
onInput={(e) => autoResize(e.target as HTMLTextAreaElement)}
|
|
49
|
-
onFocus={(e) => autoResize(e.target)}
|
|
50
|
-
/>
|
|
51
|
-
<ResponseNote show={showNote} note={note} onToggle={() => setShowNote(!showNote)} onChange={(n) => setResponse(id, { ...current!, note: n })} />
|
|
52
|
-
{showError && <p className="text-[11px] text-accent-red font-body mt-1">This field is required.</p>}
|
|
53
|
-
</div>
|
|
54
|
-
);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/** Numeric range / slider */
|
|
58
|
-
interface RangeInputProps {
|
|
59
|
-
id: string;
|
|
60
|
-
label: string;
|
|
61
|
-
min?: number;
|
|
62
|
-
max?: number;
|
|
63
|
-
step?: number;
|
|
64
|
-
required?: boolean;
|
|
65
|
-
minLabel?: string;
|
|
66
|
-
maxLabel?: string;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
export function RangeInput({ id, label, min = 1, max = 10, step = 1, required, minLabel, maxLabel }: RangeInputProps) {
|
|
70
|
-
const { responses, setResponse } = useAnnotations();
|
|
71
|
-
const current = responses.get(id);
|
|
72
|
-
const value = current?.value as number | null;
|
|
73
|
-
const note = current?.note || "";
|
|
74
|
-
const [showNote, setShowNote] = useState(false);
|
|
75
|
-
|
|
76
|
-
if (!responses.has(id)) {
|
|
77
|
-
setResponse(id, { id, type: "range", label, value: null, required });
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
const handleChange = (v: number) => {
|
|
81
|
-
setResponse(id, { ...current!, value: v });
|
|
82
|
-
};
|
|
83
|
-
|
|
84
|
-
const hasValue = value !== null && value !== undefined;
|
|
85
|
-
const displayValue = hasValue ? value : Math.round((min + max) / 2);
|
|
86
|
-
const pct = ((displayValue - min) / (max - min)) * 100;
|
|
87
|
-
const stepCount = Math.round((max - min) / step);
|
|
88
|
-
const showDots = stepCount <= 20;
|
|
89
|
-
|
|
90
|
-
const showError = current?.required && !hasValue;
|
|
91
|
-
|
|
92
|
-
return (
|
|
93
|
-
<div data-md="rangeinput" data-md-label={label} className="-mx-4 px-4 py-3 my-1 rounded-lg transition-colors duration-150 hover:bg-bg-input">
|
|
94
|
-
<div className="flex items-baseline justify-between mb-1">
|
|
95
|
-
<div className="flex items-baseline gap-2">
|
|
96
|
-
<span className="text-[13px] font-body font-medium text-text-primary">{label}</span>
|
|
97
|
-
{required && <span className="text-[10px] text-accent-red font-body">*</span>}
|
|
98
|
-
</div>
|
|
99
|
-
<span className="flex items-center gap-1.5">
|
|
100
|
-
<span className={`text-[13px] font-mono ${hasValue ? "text-accent-amber" : "text-text-disabled"}`}>
|
|
101
|
-
{hasValue ? value : "—"}
|
|
102
|
-
</span>
|
|
103
|
-
{hasValue && (
|
|
104
|
-
<button
|
|
105
|
-
onClick={() => setResponse(id, { ...current!, value: null })}
|
|
106
|
-
className="text-text-disabled hover:text-text-tertiary transition-colors text-[10px] leading-none"
|
|
107
|
-
title="Clear"
|
|
108
|
-
>
|
|
109
|
-
✕
|
|
110
|
-
</button>
|
|
111
|
-
)}
|
|
112
|
-
</span>
|
|
113
|
-
</div>
|
|
114
|
-
<div className="relative h-6 flex items-center">
|
|
115
|
-
{/* Track background */}
|
|
116
|
-
<div className="absolute left-0 right-0 h-1.5 rounded-full bg-border-medium">
|
|
117
|
-
{hasValue && <div className="h-full rounded-full bg-accent-amber transition-all duration-150" style={{ width: `${pct}%` }} />}
|
|
118
|
-
</div>
|
|
119
|
-
{/* Discrete step dots */}
|
|
120
|
-
{showDots && Array.from({ length: stepCount + 1 }, (_, i) => {
|
|
121
|
-
const dotVal = min + i * step;
|
|
122
|
-
const dotPct = ((dotVal - min) / (max - min)) * 100;
|
|
123
|
-
const isSelected = hasValue && dotVal <= value!;
|
|
124
|
-
const isActive = hasValue && dotVal === value;
|
|
125
|
-
return (
|
|
126
|
-
<div
|
|
127
|
-
key={i}
|
|
128
|
-
className={`absolute rounded-full pointer-events-none transition-all duration-150 ${
|
|
129
|
-
isActive ? "w-2.5 h-2.5 bg-accent-amber" : isSelected ? "w-1.5 h-1.5 bg-accent-amber" : "w-1.5 h-1.5 bg-border-hover"
|
|
130
|
-
}`}
|
|
131
|
-
style={{ left: `${dotPct}%`, transform: "translate(-50%, -50%)", top: "50%" }}
|
|
132
|
-
/>
|
|
133
|
-
);
|
|
134
|
-
})}
|
|
135
|
-
{/* Thumb indicator */}
|
|
136
|
-
{hasValue ? (
|
|
137
|
-
<div className="absolute h-5 w-5 rounded-full bg-accent-amber shadow-md pointer-events-none transition-all duration-150" style={{ left: `calc(${pct}% - 10px)`, borderWidth: "2px", borderStyle: "solid", borderColor: "var(--color-bg-base)" }} />
|
|
138
|
-
) : (
|
|
139
|
-
<div className="absolute h-4 w-4 rounded-full bg-border-hover pointer-events-none opacity-50" style={{ left: "calc(50% - 8px)", borderWidth: "2px", borderStyle: "solid", borderColor: "var(--color-bg-base)" }} />
|
|
140
|
-
)}
|
|
141
|
-
<input
|
|
142
|
-
type="range"
|
|
143
|
-
min={min}
|
|
144
|
-
max={max}
|
|
145
|
-
step={step}
|
|
146
|
-
value={displayValue}
|
|
147
|
-
onChange={(e) => handleChange(Number(e.target.value))}
|
|
148
|
-
className="absolute inset-0 w-full opacity-0 cursor-pointer"
|
|
149
|
-
/>
|
|
150
|
-
</div>
|
|
151
|
-
{/* Labels: show step values if few enough, otherwise just min/max */}
|
|
152
|
-
{showDots && stepCount <= 10 ? (
|
|
153
|
-
<div className="flex justify-between -mt-0.5 px-0">
|
|
154
|
-
{Array.from({ length: stepCount + 1 }, (_, i) => {
|
|
155
|
-
const dotVal = min + i * step;
|
|
156
|
-
const isActive = hasValue && dotVal === value;
|
|
157
|
-
return (
|
|
158
|
-
<span key={i} className={`text-[10px] font-mono transition-colors ${isActive ? "text-accent-amber font-medium" : "text-text-disabled"}`} style={{ minWidth: "1ch", textAlign: "center" }}>
|
|
159
|
-
{dotVal}
|
|
160
|
-
</span>
|
|
161
|
-
);
|
|
162
|
-
})}
|
|
163
|
-
</div>
|
|
164
|
-
) : (
|
|
165
|
-
<div className="flex justify-between -mt-0.5">
|
|
166
|
-
<span className="text-[11px] text-text-tertiary font-body">{minLabel || min}</span>
|
|
167
|
-
<span className="text-[11px] text-text-tertiary font-body">{maxLabel || max}</span>
|
|
168
|
-
</div>
|
|
169
|
-
)}
|
|
170
|
-
<ResponseNote show={showNote} note={note} onToggle={() => setShowNote(!showNote)} onChange={(n) => setResponse(id, { ...current!, note: n })} />
|
|
171
|
-
{showError && <p className="text-[11px] text-accent-red font-body mt-1">Please select a value.</p>}
|
|
172
|
-
</div>
|
|
173
|
-
);
|
|
174
|
-
}
|
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
import React from "react";
|
|
2
|
-
import { marked } from "marked";
|
|
3
|
-
|
|
4
|
-
marked.setOptions({ gfm: true, breaks: false });
|
|
5
|
-
|
|
6
|
-
/** Host tags that flow inline and must not break a prose run into separate blocks. */
|
|
7
|
-
const INLINE_TAGS = new Set([
|
|
8
|
-
"a", "abbr", "b", "br", "code", "del", "em", "i", "kbd", "mark",
|
|
9
|
-
"q", "s", "small", "span", "strong", "sub", "sup", "time", "u",
|
|
10
|
-
]);
|
|
11
|
-
|
|
12
|
-
/** Custom components that render inline (e.g. <Priority>). */
|
|
13
|
-
const INLINE_COMPONENTS = new Set(["Priority"]);
|
|
14
|
-
|
|
15
|
-
function isInlineNode(node: React.ReactNode): boolean {
|
|
16
|
-
if (typeof node === "string" || typeof node === "number") return true;
|
|
17
|
-
if (!React.isValidElement(node)) return false;
|
|
18
|
-
const type = node.type;
|
|
19
|
-
if (typeof type === "string") return INLINE_TAGS.has(type);
|
|
20
|
-
if (typeof type === "function") {
|
|
21
|
-
const name = (type as { displayName?: string; name?: string }).displayName ?? (type as { name?: string }).name;
|
|
22
|
-
return name ? INLINE_COMPONENTS.has(name) : false;
|
|
23
|
-
}
|
|
24
|
-
return false;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function renderTextAsMarkdown(text: string, key: string): React.ReactNode {
|
|
28
|
-
const trimmed = text.trim();
|
|
29
|
-
if (!trimmed) return null;
|
|
30
|
-
const html = marked.parse(trimmed, { async: false }) as string;
|
|
31
|
-
return (
|
|
32
|
-
<div
|
|
33
|
-
key={key}
|
|
34
|
-
className="prose-canvas"
|
|
35
|
-
data-md="markdown"
|
|
36
|
-
data-md-source={trimmed}
|
|
37
|
-
dangerouslySetInnerHTML={{ __html: html }}
|
|
38
|
-
/>
|
|
39
|
-
);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Render a run of inline content (text + inline JSX like <code>/<strong>) as a single
|
|
44
|
-
* flowing paragraph. Text segments are parsed as inline markdown; JSX passes through.
|
|
45
|
-
* No `data-md` attribute so the markdown exporter treats it as an inline container.
|
|
46
|
-
*/
|
|
47
|
-
function renderInlineRun(run: React.ReactNode[], key: string): React.ReactNode {
|
|
48
|
-
return (
|
|
49
|
-
<div key={key} className="prose-canvas">
|
|
50
|
-
{run.map((node, i) => {
|
|
51
|
-
if (typeof node === "string" || typeof node === "number") {
|
|
52
|
-
const html = marked.parseInline(String(node), { async: false }) as string;
|
|
53
|
-
return <span key={i} dangerouslySetInnerHTML={{ __html: html }} />;
|
|
54
|
-
}
|
|
55
|
-
return node;
|
|
56
|
-
})}
|
|
57
|
-
</div>
|
|
58
|
-
);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Auto-render markdown for string children, passing JSX children through.
|
|
63
|
-
* Block-level JSX (Callout, Diff, etc.) breaks the prose into separate markdown blocks,
|
|
64
|
-
* while inline JSX (code, strong, links) stays within the surrounding paragraph so text
|
|
65
|
-
* keeps flowing instead of stacking one fragment per line.
|
|
66
|
-
*/
|
|
67
|
-
export function renderMarkdownChildren(children: React.ReactNode): React.ReactNode {
|
|
68
|
-
const kids = React.Children.toArray(children);
|
|
69
|
-
if (kids.length === 0) return null;
|
|
70
|
-
|
|
71
|
-
const result: React.ReactNode[] = [];
|
|
72
|
-
let segment: React.ReactNode[] = [];
|
|
73
|
-
let segIdx = 0;
|
|
74
|
-
|
|
75
|
-
const flush = () => {
|
|
76
|
-
if (segment.length === 0) return;
|
|
77
|
-
const hasInlineJsx = segment.some((n) => typeof n !== "string" && typeof n !== "number");
|
|
78
|
-
if (hasInlineJsx) {
|
|
79
|
-
result.push(renderInlineRun(segment, `md-${segIdx++}`));
|
|
80
|
-
} else {
|
|
81
|
-
const node = renderTextAsMarkdown(segment.join(""), `md-${segIdx++}`);
|
|
82
|
-
if (node) result.push(node);
|
|
83
|
-
}
|
|
84
|
-
segment = [];
|
|
85
|
-
};
|
|
86
|
-
|
|
87
|
-
for (const child of kids) {
|
|
88
|
-
if (isInlineNode(child)) {
|
|
89
|
-
segment.push(child);
|
|
90
|
-
} else {
|
|
91
|
-
flush();
|
|
92
|
-
result.push(child);
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
flush();
|
|
96
|
-
return result;
|
|
97
|
-
}
|
|
@@ -1,273 +0,0 @@
|
|
|
1
|
-
import type { Annotation, PlanResponse, FeedbackEntry } from "./AnnotationProvider";
|
|
2
|
-
import { formatSnippetInContext } from "./annotationContext";
|
|
3
|
-
import { RESPONSE_ANNOTATION_PATH } from "./utils";
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Generate human-readable markdown feedback from annotations and responses.
|
|
7
|
-
*/
|
|
8
|
-
export function generateMarkdown(
|
|
9
|
-
annotations: Annotation[],
|
|
10
|
-
generalNote: string,
|
|
11
|
-
responses?: Map<string, PlanResponse>,
|
|
12
|
-
feedbackEntries?: Map<string, FeedbackEntry>,
|
|
13
|
-
includedRemoteIds?: Set<string>,
|
|
14
|
-
): string {
|
|
15
|
-
const parts: string[] = [];
|
|
16
|
-
const included = annotations.filter((a) =>
|
|
17
|
-
a.source !== "remote" || (includedRemoteIds && includedRemoteIds.has(a.id))
|
|
18
|
-
);
|
|
19
|
-
const responseAnns = included.filter((a) => a.filePath === RESPONSE_ANNOTATION_PATH);
|
|
20
|
-
const canvasAnns = included.filter((a) => !a.filePath);
|
|
21
|
-
const fileAnns = included.filter((a) => a.filePath && a.filePath !== RESPONSE_ANNOTATION_PATH);
|
|
22
|
-
const resps = responses ? Array.from(responses.values()).filter((r) => hasValue(r)) : [];
|
|
23
|
-
const fbEntries = feedbackEntries
|
|
24
|
-
? Array.from(feedbackEntries.values()).filter((e) => e.markdown.trim())
|
|
25
|
-
: [];
|
|
26
|
-
|
|
27
|
-
// Responses section
|
|
28
|
-
const hasResponseSection = resps.length > 0 || fbEntries.length > 0;
|
|
29
|
-
if (hasResponseSection) {
|
|
30
|
-
parts.push("## Responses");
|
|
31
|
-
parts.push("");
|
|
32
|
-
for (const r of resps) {
|
|
33
|
-
parts.push(renderResponse(r));
|
|
34
|
-
}
|
|
35
|
-
for (const entry of fbEntries) {
|
|
36
|
-
parts.push(entry.markdown.trim());
|
|
37
|
-
parts.push("");
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// Response annotations (on agent response banner)
|
|
42
|
-
if (responseAnns.length > 0) {
|
|
43
|
-
parts.push("## Annotations on agent response");
|
|
44
|
-
parts.push("");
|
|
45
|
-
for (const ann of responseAnns) {
|
|
46
|
-
parts.push(renderAnnotation(ann));
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// Canvas annotations grouped by canvas file, then hierarchy
|
|
51
|
-
if (canvasAnns.length > 0) {
|
|
52
|
-
parts.push("## Canvas annotations");
|
|
53
|
-
parts.push("");
|
|
54
|
-
|
|
55
|
-
// Group by canvasFile
|
|
56
|
-
const byCanvas = new Map<string, Annotation[]>();
|
|
57
|
-
for (const ann of canvasAnns) {
|
|
58
|
-
const key = ann.canvasFile || "";
|
|
59
|
-
if (!byCanvas.has(key)) byCanvas.set(key, []);
|
|
60
|
-
byCanvas.get(key)!.push(ann);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
const canvasKeys = [...byCanvas.keys()].sort();
|
|
64
|
-
const multipleCanvases = canvasKeys.length > 1 || (canvasKeys.length === 1 && canvasKeys[0] !== "");
|
|
65
|
-
|
|
66
|
-
for (const canvasKey of canvasKeys) {
|
|
67
|
-
const anns = byCanvas.get(canvasKey)!;
|
|
68
|
-
if (multipleCanvases && canvasKey) {
|
|
69
|
-
parts.push(`### ${canvasKey.replace(/\.jsx$/, "")}`);
|
|
70
|
-
parts.push("");
|
|
71
|
-
}
|
|
72
|
-
const grouped = groupByHierarchy(anns);
|
|
73
|
-
parts.push(renderHierarchyGroup(grouped));
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// File annotations grouped by path
|
|
78
|
-
if (fileAnns.length > 0) {
|
|
79
|
-
const groups: Record<string, Annotation[]> = {};
|
|
80
|
-
for (const ann of fileAnns) {
|
|
81
|
-
const key = ann.filePath!;
|
|
82
|
-
if (!groups[key]) groups[key] = [];
|
|
83
|
-
groups[key].push(ann);
|
|
84
|
-
}
|
|
85
|
-
parts.push("## File annotations");
|
|
86
|
-
parts.push("");
|
|
87
|
-
for (const [filePath, anns] of Object.entries(groups)) {
|
|
88
|
-
parts.push(`### ${filePath}`);
|
|
89
|
-
parts.push("");
|
|
90
|
-
for (const ann of anns) {
|
|
91
|
-
parts.push(renderAnnotation(ann));
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// General notes
|
|
97
|
-
if (generalNote.trim()) {
|
|
98
|
-
parts.push("## General notes");
|
|
99
|
-
parts.push("");
|
|
100
|
-
parts.push(generalNote.trim());
|
|
101
|
-
parts.push("");
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
return parts.join("\n").trim() + "\n";
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
export function hasValue(r: PlanResponse): boolean {
|
|
108
|
-
if (r.note?.trim()) return true;
|
|
109
|
-
if (r.value === null || r.value === undefined) return false;
|
|
110
|
-
if (r.type === "text" && !(r.value as string).trim()) return false;
|
|
111
|
-
if (r.type === "checkbox" && (r.value as string[]).length === 0) return false;
|
|
112
|
-
return true;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
export function getMissingRequired(responses: Map<string, PlanResponse>): PlanResponse[] {
|
|
116
|
-
const missing: PlanResponse[] = [];
|
|
117
|
-
for (const r of responses.values()) {
|
|
118
|
-
if (!r.required) continue;
|
|
119
|
-
if (r.value === null || r.value === undefined) { missing.push(r); continue; }
|
|
120
|
-
if (r.type === "text" && !(r.value as string).trim()) { missing.push(r); continue; }
|
|
121
|
-
if (r.type === "checkbox" && (r.value as string[]).length === 0) { missing.push(r); continue; }
|
|
122
|
-
}
|
|
123
|
-
return missing;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
export function getMissingRequiredFeedback(entries: Map<string, FeedbackEntry>): FeedbackEntry[] {
|
|
127
|
-
const missing: FeedbackEntry[] = [];
|
|
128
|
-
for (const entry of entries.values()) {
|
|
129
|
-
if (entry.required && !entry.markdown.trim()) {
|
|
130
|
-
missing.push(entry);
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
return missing;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
/** Returns list of missing required field labels, or empty if all valid */
|
|
137
|
-
export function getMissingRequiredLabels(responses: Map<string, PlanResponse>, feedbackEntries: Map<string, FeedbackEntry>): string[] {
|
|
138
|
-
const missingResponses = getMissingRequired(responses);
|
|
139
|
-
const missingFeedback = getMissingRequiredFeedback(feedbackEntries);
|
|
140
|
-
return [
|
|
141
|
-
...missingResponses.map((r) => r.label),
|
|
142
|
-
...missingFeedback.map((e) => e.label || e.id),
|
|
143
|
-
];
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
function renderResponse(r: PlanResponse): string {
|
|
147
|
-
const lines: string[] = [];
|
|
148
|
-
lines.push(`**${r.label}**`);
|
|
149
|
-
|
|
150
|
-
switch (r.type) {
|
|
151
|
-
case "radio":
|
|
152
|
-
case "select":
|
|
153
|
-
lines.push(`Answer: ${r.value}`);
|
|
154
|
-
break;
|
|
155
|
-
case "checkbox":
|
|
156
|
-
for (const v of r.value as string[]) {
|
|
157
|
-
lines.push(`- [x] ${v}`);
|
|
158
|
-
}
|
|
159
|
-
break;
|
|
160
|
-
case "text":
|
|
161
|
-
lines.push("");
|
|
162
|
-
lines.push((r.value as string).trim());
|
|
163
|
-
break;
|
|
164
|
-
case "range":
|
|
165
|
-
lines.push(`Answer: ${r.value}`);
|
|
166
|
-
break;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
if (r.note?.trim()) {
|
|
170
|
-
lines.push("");
|
|
171
|
-
lines.push(`Note: ${r.note.trim()}`);
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
lines.push("");
|
|
175
|
-
return lines.join("\n");
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
function renderAnnotation(ann: Annotation): string {
|
|
179
|
-
const lines: string[] = [];
|
|
180
|
-
const snippet = ann.snippet.trim();
|
|
181
|
-
const ctx = ann.context;
|
|
182
|
-
const hasLineInfo = ctx?.lineStart != null;
|
|
183
|
-
|
|
184
|
-
if (ann.filePath && hasLineInfo) {
|
|
185
|
-
// File annotation with line numbers
|
|
186
|
-
const lineStart = ctx!.lineStart!;
|
|
187
|
-
const lineEnd = ctx!.lineEnd ?? lineStart;
|
|
188
|
-
const snippetLines = snippet.split("\n");
|
|
189
|
-
const isShort = snippet.length < 30 && lineStart === lineEnd;
|
|
190
|
-
|
|
191
|
-
if (isShort && (ctx!.before || ctx!.after)) {
|
|
192
|
-
// Short snippet on single line — show full line context
|
|
193
|
-
const expanded = formatSnippetInContext(ann);
|
|
194
|
-
lines.push(`> L${lineStart}: ${expanded}`);
|
|
195
|
-
} else if (snippetLines.length <= 6) {
|
|
196
|
-
for (let i = 0; i < snippetLines.length; i++) {
|
|
197
|
-
lines.push(`> ${lineStart + i} | ${snippetLines[i]}`);
|
|
198
|
-
}
|
|
199
|
-
} else {
|
|
200
|
-
for (let i = 0; i < 3; i++) {
|
|
201
|
-
lines.push(`> ${lineStart + i} | ${snippetLines[i]}`);
|
|
202
|
-
}
|
|
203
|
-
lines.push(`> ... (${snippetLines.length} lines)`);
|
|
204
|
-
for (let i = snippetLines.length - 3; i < snippetLines.length; i++) {
|
|
205
|
-
lines.push(`> ${lineStart + i} | ${snippetLines[i]}`);
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
} else {
|
|
209
|
-
// Plan annotations or file annotations without line info
|
|
210
|
-
const context = formatSnippetInContext(ann);
|
|
211
|
-
if (snippet.split("\n").length <= 3) {
|
|
212
|
-
lines.push(`> ${context.split("\n").join("\n> ")}`);
|
|
213
|
-
} else {
|
|
214
|
-
const snippetLines = snippet.split("\n");
|
|
215
|
-
lines.push(`> ${snippetLines[0]}`);
|
|
216
|
-
lines.push(`> ... (${snippetLines.length} lines)`);
|
|
217
|
-
lines.push(`> ${snippetLines[snippetLines.length - 1]}`);
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
// Comment
|
|
222
|
-
if (ann.note.trim()) {
|
|
223
|
-
lines.push("");
|
|
224
|
-
lines.push(ann.note.trim());
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
// Attached images
|
|
228
|
-
if (ann.images?.length) {
|
|
229
|
-
lines.push("");
|
|
230
|
-
for (const img of ann.images) {
|
|
231
|
-
lines.push(``);
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
lines.push("");
|
|
236
|
-
return lines.join("\n");
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
interface HierarchyNode {
|
|
240
|
-
label: string;
|
|
241
|
-
annotations: Annotation[];
|
|
242
|
-
children: Map<string, HierarchyNode>;
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
function groupByHierarchy(annotations: Annotation[]): HierarchyNode {
|
|
246
|
-
const root: HierarchyNode = { label: "", annotations: [], children: new Map() };
|
|
247
|
-
for (const ann of annotations) {
|
|
248
|
-
const path = ann.context?.hierarchy || [];
|
|
249
|
-
let current = root;
|
|
250
|
-
for (const segment of path) {
|
|
251
|
-
if (!current.children.has(segment)) {
|
|
252
|
-
current.children.set(segment, { label: segment, annotations: [], children: new Map() });
|
|
253
|
-
}
|
|
254
|
-
current = current.children.get(segment)!;
|
|
255
|
-
}
|
|
256
|
-
current.annotations.push(ann);
|
|
257
|
-
}
|
|
258
|
-
return root;
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
function renderHierarchyGroup(node: HierarchyNode, depth: number = 0): string {
|
|
262
|
-
const parts: string[] = [];
|
|
263
|
-
for (const ann of node.annotations) {
|
|
264
|
-
parts.push(renderAnnotation(ann));
|
|
265
|
-
}
|
|
266
|
-
for (const [, child] of node.children) {
|
|
267
|
-
const prefix = depth === 0 ? "###" : "####";
|
|
268
|
-
parts.push(`${prefix} ${child.label}`);
|
|
269
|
-
parts.push("");
|
|
270
|
-
parts.push(renderHierarchyGroup(child, depth + 1));
|
|
271
|
-
}
|
|
272
|
-
return parts.join("\n");
|
|
273
|
-
}
|