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,627 +0,0 @@
|
|
|
1
|
-
import React, { useRef, useEffect, useCallback, useState, useContext, useMemo } from "react";
|
|
2
|
-
import { useAnnotations, Annotation } from "./AnnotationProvider";
|
|
3
|
-
import { setMarkActive } from "./highlightRange";
|
|
4
|
-
import { generateMarkdown, hasValue, getMissingRequiredLabels } from "./generateMarkdown";
|
|
5
|
-
import { RevisionContext, ActiveViewContext, type ActiveView } from "./App";
|
|
6
|
-
import { SessionContext } from "#canvas/runtime";
|
|
7
|
-
import { MarkdownPreview } from "./ResponsePreview";
|
|
8
|
-
import { FileIcon } from "./FileIcon";
|
|
9
|
-
import { autoResizeTextarea, RESPONSE_ANNOTATION_PATH } from "./utils";
|
|
10
|
-
import { AnnotationEditor, ImageThumbnails } from "./AnnotationEditor";
|
|
11
|
-
import { findAnnotationElement, scrollToAnnotation } from "./annotationDom";
|
|
12
|
-
import { MODE, FS_AVAILABLE } from "./clientApi";
|
|
13
|
-
import { ResponsePreview } from "./ResponsePreview";
|
|
14
|
-
|
|
15
|
-
interface AnnotationSidebarProps {
|
|
16
|
-
onSubmit: (feedback: string) => void;
|
|
17
|
-
collapseButton?: React.ReactNode;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export function AnnotationSidebar({ onSubmit, collapseButton }: AnnotationSidebarProps) {
|
|
21
|
-
const { isReadOnly, selectedRevision, currentRevision, revisions, agentWatching } = useContext(RevisionContext);
|
|
22
|
-
const sessionId = useContext(SessionContext);
|
|
23
|
-
const selectedRevInfo = revisions.find((r) => r.revision === selectedRevision);
|
|
24
|
-
const isCurrentButSubmitted = isReadOnly && selectedRevision === currentRevision;
|
|
25
|
-
const feedbackConsumed = !!selectedRevInfo?.feedbackConsumed;
|
|
26
|
-
const roundLabel = selectedRevInfo?.label || `Round ${selectedRevision}`;
|
|
27
|
-
|
|
28
|
-
if (isReadOnly) {
|
|
29
|
-
return <ReadOnlyAnnotationSidebar sessionId={sessionId} revision={selectedRevision} label={roundLabel} waitingForUpdate={isCurrentButSubmitted} feedbackConsumed={feedbackConsumed} agentWatching={agentWatching} collapseButton={collapseButton} />;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
return <AnnotationSidebarInner onSubmit={onSubmit} agentWatching={agentWatching} collapseButton={collapseButton} />;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function FeedbackDisplay({ sessionId, revision, label, waitingForUpdate, feedbackConsumed, agentWatching, collapseButton }: { sessionId: string; revision: number; label: string; waitingForUpdate?: boolean; feedbackConsumed?: boolean; agentWatching?: boolean; collapseButton?: React.ReactNode }) {
|
|
36
|
-
const isSharedMode = MODE.isShared;
|
|
37
|
-
const [feedback, setFeedback] = useState<string | null>(null);
|
|
38
|
-
const [loading, setLoading] = useState(true);
|
|
39
|
-
|
|
40
|
-
useEffect(() => {
|
|
41
|
-
setLoading(true);
|
|
42
|
-
// Shared mode has no prior local feedback to fetch — shared canvases
|
|
43
|
-
// are always one-off snapshots. Skip the call entirely.
|
|
44
|
-
if (!FS_AVAILABLE) { setFeedback(null); setLoading(false); return; }
|
|
45
|
-
fetch(`/api/session/${sessionId}/revision/${revision}/feedback`)
|
|
46
|
-
.then((r) => r.json())
|
|
47
|
-
.then((data: any) => { setFeedback(data.feedback || null); setLoading(false); })
|
|
48
|
-
.catch(() => { setFeedback(null); setLoading(false); });
|
|
49
|
-
}, [sessionId, revision]);
|
|
50
|
-
|
|
51
|
-
return (
|
|
52
|
-
<div className="flex flex-col h-full">
|
|
53
|
-
<div className="px-4 py-3 flex items-center justify-between flex-shrink-0">
|
|
54
|
-
<span className="text-[11px] font-medium uppercase tracking-widest text-text-tertiary font-body">
|
|
55
|
-
{waitingForUpdate ? "Feedback sent" : `Feedback — ${label}`}
|
|
56
|
-
</span>
|
|
57
|
-
{collapseButton}
|
|
58
|
-
</div>
|
|
59
|
-
|
|
60
|
-
{waitingForUpdate && !isSharedMode && <WaitingBanner feedbackConsumed={feedbackConsumed} agentWatching={agentWatching} />}
|
|
61
|
-
|
|
62
|
-
<div className="flex-1 overflow-y-auto px-4">
|
|
63
|
-
{loading ? (
|
|
64
|
-
<p className="text-[12px] text-text-tertiary font-body py-4">Loading...</p>
|
|
65
|
-
) : feedback ? (
|
|
66
|
-
<MarkdownPreview text={feedback} />
|
|
67
|
-
) : (
|
|
68
|
-
<p className="text-[12px] text-text-tertiary font-body py-4">No feedback was submitted for this revision.</p>
|
|
69
|
-
)}
|
|
70
|
-
</div>
|
|
71
|
-
</div>
|
|
72
|
-
);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function WaitingBanner({ feedbackConsumed, agentWatching }: { feedbackConsumed?: boolean; agentWatching?: boolean }) {
|
|
76
|
-
if (feedbackConsumed) {
|
|
77
|
-
return (
|
|
78
|
-
<div className="mx-4 mb-3 px-3 py-2.5 rounded-lg bg-accent-green-muted flex items-center gap-2 flex-shrink-0">
|
|
79
|
-
<span className="w-2 h-2 rounded-full bg-accent-green animate-pulse" />
|
|
80
|
-
<span className="text-[12px] font-body text-accent-green">Feedback received — waiting for next revision...</span>
|
|
81
|
-
</div>
|
|
82
|
-
);
|
|
83
|
-
}
|
|
84
|
-
if (agentWatching) {
|
|
85
|
-
return (
|
|
86
|
-
<div className="mx-4 mb-3 px-3 py-2.5 rounded-lg bg-accent-blue-muted flex items-center gap-2 flex-shrink-0">
|
|
87
|
-
<span className="w-2 h-2 rounded-full bg-accent-blue animate-pulse" />
|
|
88
|
-
<span className="text-[12px] font-body text-accent-blue">Waiting for agent to pick up feedback...</span>
|
|
89
|
-
</div>
|
|
90
|
-
);
|
|
91
|
-
}
|
|
92
|
-
return (
|
|
93
|
-
<div className="mx-4 mb-3 px-3 py-2.5 rounded-lg bg-accent-amber-muted flex items-center gap-2 flex-shrink-0">
|
|
94
|
-
<span className="w-2 h-2 rounded-full bg-accent-amber" />
|
|
95
|
-
<span className="text-[12px] font-body text-accent-amber">Agent disconnected — tell Claude to check feedback</span>
|
|
96
|
-
</div>
|
|
97
|
-
);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
type ReadOnlyTab = "feedback" | "annotations";
|
|
101
|
-
|
|
102
|
-
function ReadOnlyAnnotationSidebar({ sessionId, revision, label, waitingForUpdate, feedbackConsumed, agentWatching, collapseButton }: { sessionId: string; revision: number; label: string; waitingForUpdate?: boolean; feedbackConsumed?: boolean; agentWatching?: boolean; collapseButton?: React.ReactNode }) {
|
|
103
|
-
const { annotations, generalNote, activeAnnotationId, setActiveAnnotationId } = useAnnotations();
|
|
104
|
-
const { setActiveView } = useContext(ActiveViewContext);
|
|
105
|
-
const hasAnnotations = annotations.length > 0 || generalNote.trim().length > 0;
|
|
106
|
-
const [activeTab, setActiveTab] = useState<ReadOnlyTab>("feedback");
|
|
107
|
-
|
|
108
|
-
// If no annotations in localStorage, just show feedback
|
|
109
|
-
if (!hasAnnotations) {
|
|
110
|
-
return <FeedbackDisplay sessionId={sessionId} revision={revision} label={label} waitingForUpdate={waitingForUpdate} feedbackConsumed={feedbackConsumed} agentWatching={agentWatching} collapseButton={collapseButton} />;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
return (
|
|
114
|
-
<div className="flex flex-col h-full">
|
|
115
|
-
{/* Header with tab switcher */}
|
|
116
|
-
<div className="px-4 py-3 flex items-center justify-between flex-shrink-0">
|
|
117
|
-
<div className="flex items-center gap-1 bg-bg-input rounded-md p-0.5">
|
|
118
|
-
<button
|
|
119
|
-
onClick={() => setActiveTab("feedback")}
|
|
120
|
-
className={`px-2.5 py-1 rounded text-[11px] font-medium font-body transition-colors ${
|
|
121
|
-
activeTab === "feedback"
|
|
122
|
-
? "bg-bg-elevated text-text-primary shadow-sm"
|
|
123
|
-
: "text-text-tertiary hover:text-text-secondary"
|
|
124
|
-
}`}
|
|
125
|
-
>
|
|
126
|
-
Feedback
|
|
127
|
-
</button>
|
|
128
|
-
<button
|
|
129
|
-
onClick={() => setActiveTab("annotations")}
|
|
130
|
-
className={`px-2.5 py-1 rounded text-[11px] font-medium font-body transition-colors flex items-center gap-1.5 ${
|
|
131
|
-
activeTab === "annotations"
|
|
132
|
-
? "bg-bg-elevated text-text-primary shadow-sm"
|
|
133
|
-
: "text-text-tertiary hover:text-text-secondary"
|
|
134
|
-
}`}
|
|
135
|
-
>
|
|
136
|
-
Annotations
|
|
137
|
-
<span className="inline-flex items-center justify-center min-w-[16px] h-[16px] px-1 rounded-full bg-border-subtle text-[10px] font-medium">{annotations.length}</span>
|
|
138
|
-
</button>
|
|
139
|
-
</div>
|
|
140
|
-
{collapseButton}
|
|
141
|
-
</div>
|
|
142
|
-
|
|
143
|
-
{waitingForUpdate && <WaitingBanner feedbackConsumed={feedbackConsumed} agentWatching={agentWatching} />}
|
|
144
|
-
|
|
145
|
-
{activeTab === "feedback" ? (
|
|
146
|
-
<FeedbackDisplayContent sessionId={sessionId} revision={revision} />
|
|
147
|
-
) : (
|
|
148
|
-
<ReadOnlyAnnotationList
|
|
149
|
-
annotations={annotations}
|
|
150
|
-
generalNote={generalNote}
|
|
151
|
-
activeAnnotationId={activeAnnotationId}
|
|
152
|
-
setActiveAnnotationId={setActiveAnnotationId}
|
|
153
|
-
setActiveView={setActiveView}
|
|
154
|
-
/>
|
|
155
|
-
)}
|
|
156
|
-
</div>
|
|
157
|
-
);
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
function FeedbackDisplayContent({ sessionId, revision }: { sessionId: string; revision: number }) {
|
|
161
|
-
const [feedback, setFeedback] = useState<string | null>(null);
|
|
162
|
-
const [loading, setLoading] = useState(true);
|
|
163
|
-
|
|
164
|
-
useEffect(() => {
|
|
165
|
-
setLoading(true);
|
|
166
|
-
// Shared mode has no prior local feedback to fetch — shared canvases
|
|
167
|
-
// are always one-off snapshots. Skip the call entirely.
|
|
168
|
-
if (!FS_AVAILABLE) { setFeedback(null); setLoading(false); return; }
|
|
169
|
-
fetch(`/api/session/${sessionId}/revision/${revision}/feedback`)
|
|
170
|
-
.then((r) => r.json())
|
|
171
|
-
.then((data: any) => { setFeedback(data.feedback || null); setLoading(false); })
|
|
172
|
-
.catch(() => { setFeedback(null); setLoading(false); });
|
|
173
|
-
}, [sessionId, revision]);
|
|
174
|
-
|
|
175
|
-
return (
|
|
176
|
-
<div className="flex-1 overflow-y-auto px-4">
|
|
177
|
-
{loading ? (
|
|
178
|
-
<p className="text-[12px] text-text-tertiary font-body py-4">Loading...</p>
|
|
179
|
-
) : feedback ? (
|
|
180
|
-
<MarkdownPreview text={feedback} />
|
|
181
|
-
) : (
|
|
182
|
-
<p className="text-[12px] text-text-tertiary font-body py-4">No feedback was submitted for this revision.</p>
|
|
183
|
-
)}
|
|
184
|
-
</div>
|
|
185
|
-
);
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
function ReadOnlyAnnotationList({ annotations, generalNote, activeAnnotationId, setActiveAnnotationId, setActiveView }: {
|
|
189
|
-
annotations: Annotation[];
|
|
190
|
-
generalNote: string;
|
|
191
|
-
activeAnnotationId: string | null;
|
|
192
|
-
setActiveAnnotationId: (id: string | null) => void;
|
|
193
|
-
setActiveView: (view: ActiveView) => void;
|
|
194
|
-
}) {
|
|
195
|
-
const responseAnnotations = annotations.filter((a) => a.filePath === RESPONSE_ANNOTATION_PATH);
|
|
196
|
-
const planAnnotations = annotations.filter((a) => !a.filePath);
|
|
197
|
-
const fileAnnotations = annotations.filter((a) => a.filePath && a.filePath !== RESPONSE_ANNOTATION_PATH);
|
|
198
|
-
const fileGroups: Record<string, Annotation[]> = {};
|
|
199
|
-
for (const ann of fileAnnotations) {
|
|
200
|
-
const key = ann.filePath!;
|
|
201
|
-
if (!fileGroups[key]) fileGroups[key] = [];
|
|
202
|
-
fileGroups[key].push(ann);
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
const handleMouseEnter = (annId: string) => {
|
|
206
|
-
setActiveAnnotationId(annId);
|
|
207
|
-
setMarkActive(annId, true);
|
|
208
|
-
};
|
|
209
|
-
|
|
210
|
-
const handleMouseLeave = (annId: string) => {
|
|
211
|
-
setActiveAnnotationId(null);
|
|
212
|
-
setMarkActive(annId, false);
|
|
213
|
-
};
|
|
214
|
-
|
|
215
|
-
const handleClick = (ann: Annotation) => {
|
|
216
|
-
if (ann.id === activeAnnotationId) {
|
|
217
|
-
setActiveAnnotationId(null);
|
|
218
|
-
} else {
|
|
219
|
-
setActiveAnnotationId(ann.id);
|
|
220
|
-
if (ann.filePath) {
|
|
221
|
-
setActiveView({ type: "file", path: ann.filePath });
|
|
222
|
-
setTimeout(() => {
|
|
223
|
-
const el = findAnnotationElement(ann);
|
|
224
|
-
if (el) el.scrollIntoView({ behavior: "smooth", block: "center" });
|
|
225
|
-
}, 150);
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
};
|
|
229
|
-
|
|
230
|
-
const renderAnnotation = (ann: Annotation) => (
|
|
231
|
-
<div
|
|
232
|
-
key={ann.id}
|
|
233
|
-
className={`group/ann relative px-3 py-2.5 transition-colors duration-150 cursor-pointer ${
|
|
234
|
-
activeAnnotationId === ann.id
|
|
235
|
-
? "bg-highlight-selected"
|
|
236
|
-
: "odd:bg-bg-elevated-half hover:bg-bg-input"
|
|
237
|
-
}`}
|
|
238
|
-
onMouseEnter={() => handleMouseEnter(ann.id)}
|
|
239
|
-
onMouseLeave={() => handleMouseLeave(ann.id)}
|
|
240
|
-
onClick={() => handleClick(ann)}
|
|
241
|
-
>
|
|
242
|
-
<div className="text-[11px] text-text-tertiary italic line-clamp-2 mb-1.5 leading-snug font-body border-l-2 border-border-medium pl-2">
|
|
243
|
-
{ann.snippet.length > 80 ? ann.snippet.slice(0, 80) + "..." : ann.snippet}
|
|
244
|
-
</div>
|
|
245
|
-
{ann.note.trim() && (
|
|
246
|
-
<div className="text-[13px] font-body text-text-primary leading-relaxed">
|
|
247
|
-
{ann.note}
|
|
248
|
-
</div>
|
|
249
|
-
)}
|
|
250
|
-
<ImageThumbnails images={ann.images || []} />
|
|
251
|
-
<div className="absolute top-1.5 right-1.5 opacity-0 group-hover/ann:opacity-100 transition-opacity duration-100">
|
|
252
|
-
<button
|
|
253
|
-
onClick={(e) => { e.stopPropagation(); scrollToAnnotation(ann, setActiveView); }}
|
|
254
|
-
className="w-5 h-5 flex items-center justify-center rounded text-text-tertiary hover:text-text-secondary hover:bg-bg-input transition-colors"
|
|
255
|
-
title="Scroll to annotation"
|
|
256
|
-
>
|
|
257
|
-
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
258
|
-
<circle cx="12" cy="12" r="3" /><path d="M12 2v4M12 18v4M2 12h4M18 12h4" />
|
|
259
|
-
</svg>
|
|
260
|
-
</button>
|
|
261
|
-
</div>
|
|
262
|
-
</div>
|
|
263
|
-
);
|
|
264
|
-
|
|
265
|
-
const hasMultipleGroups = [responseAnnotations, planAnnotations, fileAnnotations].filter((g) => g.length > 0).length > 1;
|
|
266
|
-
|
|
267
|
-
return (
|
|
268
|
-
<>
|
|
269
|
-
<div className="flex-1 overflow-y-auto">
|
|
270
|
-
{responseAnnotations.length > 0 && (
|
|
271
|
-
<>
|
|
272
|
-
{hasMultipleGroups && (
|
|
273
|
-
<div className="text-[10px] uppercase tracking-widest text-text-tertiary font-body px-3 mb-1 mt-1">Agent Response</div>
|
|
274
|
-
)}
|
|
275
|
-
{responseAnnotations.map(renderAnnotation)}
|
|
276
|
-
</>
|
|
277
|
-
)}
|
|
278
|
-
|
|
279
|
-
{planAnnotations.length > 0 && (
|
|
280
|
-
<>
|
|
281
|
-
{hasMultipleGroups && (
|
|
282
|
-
<div className="text-[10px] uppercase tracking-widest text-text-tertiary font-body px-3 mb-1 mt-1">Canvas</div>
|
|
283
|
-
)}
|
|
284
|
-
{planAnnotations.map(renderAnnotation)}
|
|
285
|
-
</>
|
|
286
|
-
)}
|
|
287
|
-
|
|
288
|
-
{Object.entries(fileGroups).map(([filePath, anns]) => (
|
|
289
|
-
<div key={filePath}>
|
|
290
|
-
<button
|
|
291
|
-
onClick={() => setActiveView({ type: "file", path: filePath })}
|
|
292
|
-
className="flex items-center gap-1.5 text-[10px] uppercase tracking-widest text-text-tertiary hover:text-text-secondary font-body px-3 pt-3 pb-1 truncate transition-colors w-full text-left"
|
|
293
|
-
title={filePath}
|
|
294
|
-
>
|
|
295
|
-
<FileIcon name={filePath.split("/").pop() || filePath} type="file" />
|
|
296
|
-
<span className="truncate">{filePath}</span>
|
|
297
|
-
</button>
|
|
298
|
-
{anns.map(renderAnnotation)}
|
|
299
|
-
</div>
|
|
300
|
-
))}
|
|
301
|
-
</div>
|
|
302
|
-
|
|
303
|
-
{generalNote.trim() && (
|
|
304
|
-
<div className="border-t border-border-subtle px-4 py-3 flex-shrink-0">
|
|
305
|
-
<div className="text-[13px] font-body text-text-primary leading-relaxed">
|
|
306
|
-
{generalNote}
|
|
307
|
-
</div>
|
|
308
|
-
</div>
|
|
309
|
-
)}
|
|
310
|
-
</>
|
|
311
|
-
);
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
function AnnotationSidebarInner({ onSubmit, agentWatching, collapseButton }: Omit<AnnotationSidebarProps, "onPreview"> & { agentWatching: boolean }) {
|
|
315
|
-
const [previewOpen, setPreviewOpen] = useState(false);
|
|
316
|
-
const {
|
|
317
|
-
annotations, updateAnnotation, removeAnnotation,
|
|
318
|
-
addAnnotationImage, removeAnnotationImage,
|
|
319
|
-
generalNote, setGeneralNote,
|
|
320
|
-
activeAnnotationId, setActiveAnnotationId,
|
|
321
|
-
responses, feedbackEntries,
|
|
322
|
-
} = useAnnotations();
|
|
323
|
-
const sessionId = useContext(SessionContext);
|
|
324
|
-
const { setActiveView } = React.useContext(ActiveViewContext);
|
|
325
|
-
|
|
326
|
-
const listRef = useRef<HTMLDivElement>(null);
|
|
327
|
-
const annRefs = useRef<Map<string, HTMLDivElement>>(new Map());
|
|
328
|
-
|
|
329
|
-
const responseAnnotations = annotations.filter((a) => a.filePath === RESPONSE_ANNOTATION_PATH);
|
|
330
|
-
const planAnnotations = annotations.filter((a) => !a.filePath);
|
|
331
|
-
const fileAnnotations = annotations.filter((a) => a.filePath && a.filePath !== RESPONSE_ANNOTATION_PATH);
|
|
332
|
-
const fileGroups: Record<string, Annotation[]> = {};
|
|
333
|
-
for (const ann of fileAnnotations) {
|
|
334
|
-
const key = ann.filePath!;
|
|
335
|
-
if (!fileGroups[key]) fileGroups[key] = [];
|
|
336
|
-
fileGroups[key].push(ann);
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
// Scroll to active annotation in sidebar
|
|
340
|
-
useEffect(() => {
|
|
341
|
-
if (!activeAnnotationId) return;
|
|
342
|
-
const el = annRefs.current.get(activeAnnotationId);
|
|
343
|
-
if (el) {
|
|
344
|
-
el.scrollIntoView({ behavior: "smooth", block: "nearest" });
|
|
345
|
-
}
|
|
346
|
-
}, [activeAnnotationId]);
|
|
347
|
-
|
|
348
|
-
// Hover on sidebar card → highlight inline mark + block
|
|
349
|
-
const handleMouseEnter = useCallback((annId: string) => {
|
|
350
|
-
setActiveAnnotationId(annId);
|
|
351
|
-
setMarkActive(annId, true);
|
|
352
|
-
}, [setActiveAnnotationId]);
|
|
353
|
-
|
|
354
|
-
const handleMouseLeave = useCallback((annId: string) => {
|
|
355
|
-
setActiveAnnotationId(null);
|
|
356
|
-
setMarkActive(annId, false);
|
|
357
|
-
}, [activeAnnotationId]);
|
|
358
|
-
|
|
359
|
-
const remoteAnnotations = useMemo(() => annotations.filter((a) => a.source === "remote"), [annotations]);
|
|
360
|
-
const [includedRemoteIds, setIncludedRemoteIds] = useState<Set<string>>(() => new Set());
|
|
361
|
-
|
|
362
|
-
// Auto-include new remote annotations as they arrive
|
|
363
|
-
useEffect(() => {
|
|
364
|
-
if (remoteAnnotations.length === 0) return;
|
|
365
|
-
setIncludedRemoteIds((prev) => {
|
|
366
|
-
const next = new Set(prev);
|
|
367
|
-
for (const a of remoteAnnotations) next.add(a.id);
|
|
368
|
-
return next.size === prev.size ? prev : next;
|
|
369
|
-
});
|
|
370
|
-
}, [remoteAnnotations]);
|
|
371
|
-
|
|
372
|
-
const toggleRemoteId = useCallback((id: string) => {
|
|
373
|
-
setIncludedRemoteIds((prev) => {
|
|
374
|
-
const next = new Set(prev);
|
|
375
|
-
if (next.has(id)) next.delete(id); else next.add(id);
|
|
376
|
-
return next;
|
|
377
|
-
});
|
|
378
|
-
}, []);
|
|
379
|
-
|
|
380
|
-
const hasResponses = Array.from(responses.values()).some(hasValue);
|
|
381
|
-
const hasFeedback = feedbackEntries.size > 0;
|
|
382
|
-
const hasContent = annotations.length > 0 || generalNote.trim().length > 0 || hasResponses || hasFeedback;
|
|
383
|
-
const [validationError, setValidationError] = useState<string | null>(null);
|
|
384
|
-
|
|
385
|
-
const renderAnnotation = (ann: Annotation) => {
|
|
386
|
-
const isRemote = ann.source === "remote";
|
|
387
|
-
return (
|
|
388
|
-
<div
|
|
389
|
-
key={ann.id}
|
|
390
|
-
ref={(el) => { if (el) annRefs.current.set(ann.id, el); else annRefs.current.delete(ann.id); }}
|
|
391
|
-
className={`group/ann relative px-3 py-2.5 transition-colors duration-150 ${
|
|
392
|
-
activeAnnotationId === ann.id
|
|
393
|
-
? "bg-highlight-selected"
|
|
394
|
-
: isRemote
|
|
395
|
-
? "bg-accent-purple-muted/30 hover:bg-accent-purple-muted/50"
|
|
396
|
-
: "odd:bg-bg-elevated-half hover:bg-bg-input"
|
|
397
|
-
}`}
|
|
398
|
-
onMouseEnter={() => handleMouseEnter(ann.id)}
|
|
399
|
-
onMouseLeave={() => handleMouseLeave(ann.id)}
|
|
400
|
-
onClick={() => {
|
|
401
|
-
if (ann.id === activeAnnotationId) {
|
|
402
|
-
setActiveAnnotationId(null);
|
|
403
|
-
} else {
|
|
404
|
-
setActiveAnnotationId(ann.id);
|
|
405
|
-
if (ann.filePath) {
|
|
406
|
-
setActiveView({ type: "file", path: ann.filePath });
|
|
407
|
-
setTimeout(() => {
|
|
408
|
-
const el = findAnnotationElement(ann);
|
|
409
|
-
if (el) el.scrollIntoView({ behavior: "smooth", block: "center" });
|
|
410
|
-
}, 150);
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
}}
|
|
414
|
-
>
|
|
415
|
-
{isRemote && ann.author && (
|
|
416
|
-
<div className="flex items-center gap-1.5 mb-1.5">
|
|
417
|
-
<input
|
|
418
|
-
type="checkbox"
|
|
419
|
-
checked={includedRemoteIds.has(ann.id)}
|
|
420
|
-
onChange={(e) => { e.stopPropagation(); toggleRemoteId(ann.id); }}
|
|
421
|
-
onClick={(e) => e.stopPropagation()}
|
|
422
|
-
className="accent-accent-purple flex-shrink-0"
|
|
423
|
-
title="Include in submission"
|
|
424
|
-
/>
|
|
425
|
-
<span className="inline-flex items-center justify-center w-4 h-4 rounded-full bg-accent-purple text-white text-[9px] font-medium uppercase">
|
|
426
|
-
{ann.author.name.charAt(0)}
|
|
427
|
-
</span>
|
|
428
|
-
<span className="text-[11px] font-medium text-accent-purple font-body">
|
|
429
|
-
{ann.author.name}
|
|
430
|
-
</span>
|
|
431
|
-
<span className="text-[10px] text-text-tertiary font-body uppercase tracking-widest">
|
|
432
|
-
Remote
|
|
433
|
-
</span>
|
|
434
|
-
</div>
|
|
435
|
-
)}
|
|
436
|
-
|
|
437
|
-
{/* Snippet quote */}
|
|
438
|
-
<div className="text-[11px] text-text-tertiary italic line-clamp-2 mb-1.5 leading-snug font-body border-l-2 border-border-medium pl-2">
|
|
439
|
-
{ann.snippet.length > 80 ? ann.snippet.slice(0, 80) + "..." : ann.snippet}
|
|
440
|
-
</div>
|
|
441
|
-
|
|
442
|
-
{/* Editable note + images — only for local annotations. Remote are read-only. */}
|
|
443
|
-
{isRemote ? (
|
|
444
|
-
<div className="text-[13px] font-body text-text-primary leading-relaxed whitespace-pre-wrap">
|
|
445
|
-
{ann.note}
|
|
446
|
-
</div>
|
|
447
|
-
) : (
|
|
448
|
-
<div onClick={(e) => e.stopPropagation()}>
|
|
449
|
-
<AnnotationEditor
|
|
450
|
-
note={ann.note}
|
|
451
|
-
onNoteChange={(note) => updateAnnotation(ann.id, note)}
|
|
452
|
-
images={ann.images || []}
|
|
453
|
-
onAddImage={(path) => addAnnotationImage(ann.id, path)}
|
|
454
|
-
onRemoveImage={(path) => removeAnnotationImage(ann.id, path)}
|
|
455
|
-
sessionId={sessionId}
|
|
456
|
-
autoResize
|
|
457
|
-
minHeight={20}
|
|
458
|
-
textareaClassName="w-full bg-transparent text-[13px] font-body text-text-primary resize-none focus:outline-none leading-relaxed p-0 border-none min-h-[20px]"
|
|
459
|
-
textareaStyle={{ height: "auto", overflow: "hidden" }}
|
|
460
|
-
placeholder="Add your note..."
|
|
461
|
-
attachButton="on-focus"
|
|
462
|
-
/>
|
|
463
|
-
</div>
|
|
464
|
-
)}
|
|
465
|
-
|
|
466
|
-
{/* Actions — top right on hover */}
|
|
467
|
-
<div className="absolute top-1.5 right-1.5 opacity-0 group-hover/ann:opacity-100 transition-opacity duration-100 flex items-center gap-0.5">
|
|
468
|
-
<button
|
|
469
|
-
onClick={(e) => {
|
|
470
|
-
e.stopPropagation();
|
|
471
|
-
scrollToAnnotation(ann, setActiveView);
|
|
472
|
-
}}
|
|
473
|
-
className="w-5 h-5 flex items-center justify-center rounded text-text-tertiary hover:text-text-secondary hover:bg-bg-input transition-colors"
|
|
474
|
-
title="Scroll to annotation"
|
|
475
|
-
>
|
|
476
|
-
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
477
|
-
<circle cx="12" cy="12" r="3" /><path d="M12 2v4M12 18v4M2 12h4M18 12h4" />
|
|
478
|
-
</svg>
|
|
479
|
-
</button>
|
|
480
|
-
{!isRemote && (
|
|
481
|
-
<button
|
|
482
|
-
onClick={(e) => { e.stopPropagation(); removeAnnotation(ann.id); }}
|
|
483
|
-
className="w-5 h-5 flex items-center justify-center rounded text-text-tertiary hover:text-accent-red hover:bg-accent-red-muted transition-colors"
|
|
484
|
-
title="Delete annotation"
|
|
485
|
-
>
|
|
486
|
-
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
487
|
-
<polyline points="3 6 5 6 21 6"/>
|
|
488
|
-
<path d="M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2"/>
|
|
489
|
-
</svg>
|
|
490
|
-
</button>
|
|
491
|
-
)}
|
|
492
|
-
</div>
|
|
493
|
-
</div>
|
|
494
|
-
);
|
|
495
|
-
};
|
|
496
|
-
|
|
497
|
-
return (
|
|
498
|
-
<div className="flex flex-col h-full">
|
|
499
|
-
{/* Header */}
|
|
500
|
-
<div className="px-4 py-3 flex items-center justify-between flex-shrink-0">
|
|
501
|
-
<span className="text-[11px] font-medium uppercase tracking-widest text-text-tertiary font-body flex items-center gap-2">
|
|
502
|
-
Annotations
|
|
503
|
-
{annotations.length > 0 && (
|
|
504
|
-
<span className="inline-flex items-center justify-center min-w-[18px] h-[18px] px-1 rounded-full bg-border-subtle text-[10px] font-medium text-text-secondary">{annotations.length}</span>
|
|
505
|
-
)}
|
|
506
|
-
{!MODE.isShared && (
|
|
507
|
-
<span
|
|
508
|
-
className={`w-1.5 h-1.5 rounded-full flex-shrink-0 ${agentWatching ? "bg-accent-green" : "bg-accent-amber"}`}
|
|
509
|
-
title={agentWatching ? "Agent connected" : "Agent disconnected"}
|
|
510
|
-
/>
|
|
511
|
-
)}
|
|
512
|
-
</span>
|
|
513
|
-
{collapseButton}
|
|
514
|
-
</div>
|
|
515
|
-
|
|
516
|
-
{/* Annotation list */}
|
|
517
|
-
<div ref={listRef} className="flex-1 overflow-y-auto">
|
|
518
|
-
{annotations.length === 0 && (
|
|
519
|
-
<p className="text-[12px] text-text-tertiary px-3 py-4 leading-relaxed font-body">
|
|
520
|
-
Select text in the canvas or in files to add annotations.
|
|
521
|
-
</p>
|
|
522
|
-
)}
|
|
523
|
-
|
|
524
|
-
{responseAnnotations.length > 0 && (
|
|
525
|
-
<>
|
|
526
|
-
{(planAnnotations.length > 0 || fileAnnotations.length > 0) && (
|
|
527
|
-
<div className="text-[10px] uppercase tracking-widest text-text-tertiary font-body px-3 mb-1 mt-1">Agent Response</div>
|
|
528
|
-
)}
|
|
529
|
-
{responseAnnotations.map(renderAnnotation)}
|
|
530
|
-
</>
|
|
531
|
-
)}
|
|
532
|
-
|
|
533
|
-
{planAnnotations.length > 0 && (responseAnnotations.length > 0 || fileAnnotations.length > 0) && (
|
|
534
|
-
<div className="text-[10px] uppercase tracking-widest text-text-tertiary font-body px-3 mb-1 mt-1">Canvas</div>
|
|
535
|
-
)}
|
|
536
|
-
{planAnnotations.map(renderAnnotation)}
|
|
537
|
-
|
|
538
|
-
{Object.entries(fileGroups).map(([filePath, anns]) => (
|
|
539
|
-
<div key={filePath}>
|
|
540
|
-
<button
|
|
541
|
-
onClick={() => setActiveView({ type: "file", path: filePath })}
|
|
542
|
-
className="flex items-center gap-1.5 text-[10px] uppercase tracking-widest text-text-tertiary hover:text-text-secondary font-body px-3 pt-3 pb-1 truncate transition-colors w-full text-left"
|
|
543
|
-
title={filePath}
|
|
544
|
-
>
|
|
545
|
-
<FileIcon name={filePath.split("/").pop() || filePath} type="file" />
|
|
546
|
-
<span className="truncate">{filePath}</span>
|
|
547
|
-
</button>
|
|
548
|
-
{anns.map(renderAnnotation)}
|
|
549
|
-
</div>
|
|
550
|
-
))}
|
|
551
|
-
</div>
|
|
552
|
-
|
|
553
|
-
{/* General note — seamless */}
|
|
554
|
-
<div className="border-t border-border-subtle px-4 py-3 flex-shrink-0">
|
|
555
|
-
<textarea
|
|
556
|
-
value={generalNote}
|
|
557
|
-
onChange={(e) => setGeneralNote(e.target.value)}
|
|
558
|
-
className="w-full bg-transparent text-[13px] font-body text-text-primary resize-none leading-relaxed p-0 border-none ring-0 shadow-none outline-none focus:outline-none focus:ring-0 focus:border-none placeholder:text-text-disabled min-h-[40px]"
|
|
559
|
-
placeholder="General notes..."
|
|
560
|
-
onInput={(e) => autoResizeTextarea(e.target as HTMLTextAreaElement, 40)}
|
|
561
|
-
ref={(el) => { if (el) autoResizeTextarea(el, 40); }}
|
|
562
|
-
/>
|
|
563
|
-
</div>
|
|
564
|
-
|
|
565
|
-
{/* Validation error */}
|
|
566
|
-
{validationError && (
|
|
567
|
-
<div className="px-4 py-2 text-[12px] text-accent-red font-body border-t border-border-subtle flex-shrink-0 flex items-center justify-between gap-2">
|
|
568
|
-
<span>{validationError}</span>
|
|
569
|
-
<button
|
|
570
|
-
onClick={() => {
|
|
571
|
-
setValidationError(null);
|
|
572
|
-
const md = generateMarkdown(annotations, generalNote, responses, feedbackEntries, includedRemoteIds);
|
|
573
|
-
onSubmit(md);
|
|
574
|
-
}}
|
|
575
|
-
className="text-[11px] text-text-tertiary hover:text-text-secondary font-body whitespace-nowrap underline"
|
|
576
|
-
>
|
|
577
|
-
Submit anyway
|
|
578
|
-
</button>
|
|
579
|
-
</div>
|
|
580
|
-
)}
|
|
581
|
-
|
|
582
|
-
{/* Action buttons */}
|
|
583
|
-
<div className="px-4 py-3 border-t border-border-subtle flex-shrink-0">
|
|
584
|
-
{hasContent ? (
|
|
585
|
-
<div className="flex gap-2">
|
|
586
|
-
<button
|
|
587
|
-
onClick={() => setPreviewOpen(true)}
|
|
588
|
-
className="flex-1 py-2 rounded-lg font-body text-[13px] font-medium transition-all bg-border-subtle text-text-secondary hover:bg-border-medium hover:text-text-primary"
|
|
589
|
-
>
|
|
590
|
-
Preview
|
|
591
|
-
</button>
|
|
592
|
-
<button
|
|
593
|
-
onClick={() => {
|
|
594
|
-
const allMissing = getMissingRequiredLabels(responses, feedbackEntries);
|
|
595
|
-
if (allMissing.length > 0) {
|
|
596
|
-
setValidationError(`Please answer: ${allMissing.join(", ")}`);
|
|
597
|
-
return;
|
|
598
|
-
}
|
|
599
|
-
setValidationError(null);
|
|
600
|
-
const md = generateMarkdown(annotations, generalNote, responses, feedbackEntries, includedRemoteIds);
|
|
601
|
-
onSubmit(md);
|
|
602
|
-
}}
|
|
603
|
-
className="flex-1 py-2 rounded-lg font-body text-[13px] font-medium transition-all bg-btn-primary text-btn-primary-text hover:opacity-90 hover:-translate-y-px shadow-sm"
|
|
604
|
-
>
|
|
605
|
-
Submit
|
|
606
|
-
</button>
|
|
607
|
-
</div>
|
|
608
|
-
) : (
|
|
609
|
-
<button
|
|
610
|
-
onClick={() => onSubmit("No feedback — looks good.")}
|
|
611
|
-
className="w-full py-2 rounded-lg font-body text-[13px] font-medium border border-border-medium text-text-secondary hover:text-text-primary hover:border-border-hover hover:bg-bg-input transition-all"
|
|
612
|
-
>
|
|
613
|
-
Submit without feedback
|
|
614
|
-
</button>
|
|
615
|
-
)}
|
|
616
|
-
</div>
|
|
617
|
-
<ResponsePreview
|
|
618
|
-
open={previewOpen}
|
|
619
|
-
onClose={() => setPreviewOpen(false)}
|
|
620
|
-
onSubmit={onSubmit}
|
|
621
|
-
includedRemoteIds={includedRemoteIds}
|
|
622
|
-
onToggleRemoteId={toggleRemoteId}
|
|
623
|
-
/>
|
|
624
|
-
</div>
|
|
625
|
-
);
|
|
626
|
-
}
|
|
627
|
-
|