agent-canvas 0.1.29 → 0.1.30
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/daemon/client/AnnotationSidebar.tsx +45 -9
- package/daemon/client/App.tsx +5 -9
- package/daemon/client/ResponsePreview.tsx +19 -3
- package/daemon/client/generateMarkdown.ts +7 -3
- package/daemon/dist/client.js +52 -52
- package/package.json +1 -1
- package/workers/canvas-share/public/assets/client.js +52 -52
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { useRef, useEffect, useCallback, useState, useContext } from "react";
|
|
1
|
+
import React, { useRef, useEffect, useCallback, useState, useContext, useMemo } from "react";
|
|
2
2
|
import { useAnnotations, Annotation } from "./AnnotationProvider";
|
|
3
3
|
import { setMarkActive } from "./highlightRange";
|
|
4
4
|
import { generateMarkdown, hasValue, getMissingRequiredLabels } from "./generateMarkdown";
|
|
@@ -10,14 +10,14 @@ import { autoResizeTextarea, RESPONSE_ANNOTATION_PATH } from "./utils";
|
|
|
10
10
|
import { AnnotationEditor, ImageThumbnails } from "./AnnotationEditor";
|
|
11
11
|
import { findAnnotationElement, scrollToAnnotation } from "./annotationDom";
|
|
12
12
|
import { MODE, FS_AVAILABLE } from "./clientApi";
|
|
13
|
+
import { ResponsePreview } from "./ResponsePreview";
|
|
13
14
|
|
|
14
15
|
interface AnnotationSidebarProps {
|
|
15
|
-
onPreview: () => void;
|
|
16
16
|
onSubmit: (feedback: string) => void;
|
|
17
17
|
collapseButton?: React.ReactNode;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
export function AnnotationSidebar({
|
|
20
|
+
export function AnnotationSidebar({ onSubmit, collapseButton }: AnnotationSidebarProps) {
|
|
21
21
|
const { isReadOnly, selectedRevision, currentRevision, revisions, agentWatching } = useContext(RevisionContext);
|
|
22
22
|
const sessionId = useContext(SessionContext);
|
|
23
23
|
const selectedRevInfo = revisions.find((r) => r.revision === selectedRevision);
|
|
@@ -29,7 +29,7 @@ export function AnnotationSidebar({ onPreview, onSubmit, collapseButton }: Annot
|
|
|
29
29
|
return <ReadOnlyAnnotationSidebar sessionId={sessionId} revision={selectedRevision} label={roundLabel} waitingForUpdate={isCurrentButSubmitted} feedbackConsumed={feedbackConsumed} agentWatching={agentWatching} collapseButton={collapseButton} />;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
return <AnnotationSidebarInner
|
|
32
|
+
return <AnnotationSidebarInner onSubmit={onSubmit} agentWatching={agentWatching} collapseButton={collapseButton} />;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
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 }) {
|
|
@@ -311,7 +311,8 @@ function ReadOnlyAnnotationList({ annotations, generalNote, activeAnnotationId,
|
|
|
311
311
|
);
|
|
312
312
|
}
|
|
313
313
|
|
|
314
|
-
function AnnotationSidebarInner({
|
|
314
|
+
function AnnotationSidebarInner({ onSubmit, agentWatching, collapseButton }: Omit<AnnotationSidebarProps, "onPreview"> & { agentWatching: boolean }) {
|
|
315
|
+
const [previewOpen, setPreviewOpen] = useState(false);
|
|
315
316
|
const {
|
|
316
317
|
annotations, updateAnnotation, removeAnnotation,
|
|
317
318
|
addAnnotationImage, removeAnnotationImage,
|
|
@@ -355,6 +356,27 @@ function AnnotationSidebarInner({ onPreview, onSubmit, agentWatching, collapseBu
|
|
|
355
356
|
setMarkActive(annId, false);
|
|
356
357
|
}, [activeAnnotationId]);
|
|
357
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
|
+
|
|
358
380
|
const hasResponses = Array.from(responses.values()).some(hasValue);
|
|
359
381
|
const hasFeedback = feedbackEntries.size > 0;
|
|
360
382
|
const hasContent = annotations.length > 0 || generalNote.trim().length > 0 || hasResponses || hasFeedback;
|
|
@@ -392,6 +414,14 @@ function AnnotationSidebarInner({ onPreview, onSubmit, agentWatching, collapseBu
|
|
|
392
414
|
>
|
|
393
415
|
{isRemote && ann.author && (
|
|
394
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
|
+
/>
|
|
395
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">
|
|
396
426
|
{ann.author.name.charAt(0)}
|
|
397
427
|
</span>
|
|
@@ -539,7 +569,7 @@ function AnnotationSidebarInner({ onPreview, onSubmit, agentWatching, collapseBu
|
|
|
539
569
|
<button
|
|
540
570
|
onClick={() => {
|
|
541
571
|
setValidationError(null);
|
|
542
|
-
const md = generateMarkdown(annotations, generalNote, responses, feedbackEntries);
|
|
572
|
+
const md = generateMarkdown(annotations, generalNote, responses, feedbackEntries, includedRemoteIds);
|
|
543
573
|
onSubmit(md);
|
|
544
574
|
}}
|
|
545
575
|
className="text-[11px] text-text-tertiary hover:text-text-secondary font-body whitespace-nowrap underline"
|
|
@@ -554,7 +584,7 @@ function AnnotationSidebarInner({ onPreview, onSubmit, agentWatching, collapseBu
|
|
|
554
584
|
{hasContent ? (
|
|
555
585
|
<div className="flex gap-2">
|
|
556
586
|
<button
|
|
557
|
-
onClick={
|
|
587
|
+
onClick={() => setPreviewOpen(true)}
|
|
558
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"
|
|
559
589
|
>
|
|
560
590
|
Preview
|
|
@@ -567,7 +597,7 @@ function AnnotationSidebarInner({ onPreview, onSubmit, agentWatching, collapseBu
|
|
|
567
597
|
return;
|
|
568
598
|
}
|
|
569
599
|
setValidationError(null);
|
|
570
|
-
const md = generateMarkdown(annotations, generalNote, responses, feedbackEntries);
|
|
600
|
+
const md = generateMarkdown(annotations, generalNote, responses, feedbackEntries, includedRemoteIds);
|
|
571
601
|
onSubmit(md);
|
|
572
602
|
}}
|
|
573
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"
|
|
@@ -584,8 +614,14 @@ function AnnotationSidebarInner({ onPreview, onSubmit, agentWatching, collapseBu
|
|
|
584
614
|
</button>
|
|
585
615
|
)}
|
|
586
616
|
</div>
|
|
617
|
+
<ResponsePreview
|
|
618
|
+
open={previewOpen}
|
|
619
|
+
onClose={() => setPreviewOpen(false)}
|
|
620
|
+
onSubmit={onSubmit}
|
|
621
|
+
includedRemoteIds={includedRemoteIds}
|
|
622
|
+
onToggleRemoteId={toggleRemoteId}
|
|
623
|
+
/>
|
|
587
624
|
</div>
|
|
588
625
|
);
|
|
589
626
|
}
|
|
590
627
|
|
|
591
|
-
|
package/daemon/client/App.tsx
CHANGED
|
@@ -5,7 +5,6 @@ import { SessionContext, ActiveViewCtx } from "#canvas/runtime";
|
|
|
5
5
|
import { AnnotationProvider, useAnnotations } from "./AnnotationProvider";
|
|
6
6
|
import { PlanRenderer } from "./PlanRenderer";
|
|
7
7
|
import { AnnotationSidebar } from "./AnnotationSidebar";
|
|
8
|
-
import { ResponsePreview } from "./ResponsePreview";
|
|
9
8
|
import { FileBrowser } from "./FileBrowser";
|
|
10
9
|
import { FileIcon } from "./FileIcon";
|
|
11
10
|
import { FileViewer } from "./FileViewer";
|
|
@@ -226,8 +225,8 @@ function App() {
|
|
|
226
225
|
const [revisions, setRevisions] = useState<RevisionInfo[]>([]);
|
|
227
226
|
const [compareRevision, setCompareRevision] = useState<{ left: number; right: number } | null>(null);
|
|
228
227
|
const [connected, setConnected] = useState(false);
|
|
228
|
+
const [sharedFeedbackSubmitted, setSharedFeedbackSubmitted] = useState(false);
|
|
229
229
|
const [agentWatching, setAgentWatching] = useState(false);
|
|
230
|
-
const [previewOpen, setPreviewOpen] = useState(false);
|
|
231
230
|
const [canvasFiles, setCanvasFiles] = useState<string[]>([]);
|
|
232
231
|
const [activeView, setActiveViewRaw] = useState<ActiveView>({ type: "overview" });
|
|
233
232
|
const [openFiles, setOpenFiles] = useState<string[]>([]);
|
|
@@ -278,7 +277,7 @@ function App() {
|
|
|
278
277
|
const scrollPositions = useRef<Map<string, number>>(new Map());
|
|
279
278
|
|
|
280
279
|
const selectedRevInfo = revisions.find((r) => r.revision === selectedRevision);
|
|
281
|
-
const isReadOnly = selectedRevision !== currentRevision || !!selectedRevInfo?.hasFeedback;
|
|
280
|
+
const isReadOnly = selectedRevision !== currentRevision || !!selectedRevInfo?.hasFeedback || sharedFeedbackSubmitted;
|
|
282
281
|
|
|
283
282
|
const setActiveView = useCallback((v: ActiveView) => {
|
|
284
283
|
// Save current scroll position before switching
|
|
@@ -446,7 +445,7 @@ function App() {
|
|
|
446
445
|
annotations: structuredAnnotations,
|
|
447
446
|
generalNote: feedback,
|
|
448
447
|
});
|
|
449
|
-
|
|
448
|
+
setSharedFeedbackSubmitted(true);
|
|
450
449
|
setToast({ kind: "success", message: "Feedback submitted. The canvas author will see it shortly." });
|
|
451
450
|
} catch (e: any) {
|
|
452
451
|
setToast({ kind: "error", message: `Failed to submit feedback: ${e.message || e}` });
|
|
@@ -469,7 +468,6 @@ function App() {
|
|
|
469
468
|
|
|
470
469
|
if (wsRef.current?.readyState === WebSocket.OPEN) {
|
|
471
470
|
wsRef.current.send(JSON.stringify({ type: "submit", feedback }));
|
|
472
|
-
setPreviewOpen(false);
|
|
473
471
|
// Server will broadcast revision-updated with hasFeedback: true
|
|
474
472
|
// which triggers isReadOnly via revisions state
|
|
475
473
|
}
|
|
@@ -497,7 +495,7 @@ function App() {
|
|
|
497
495
|
|
|
498
496
|
{/* Right panel — fixed to viewport, resizable */}
|
|
499
497
|
<ResizableSidebar collapsed={rightCollapsed} onToggle={() => setRightCollapsed((c) => !c)}>
|
|
500
|
-
<AnnotationSidebar
|
|
498
|
+
<AnnotationSidebar onSubmit={handleSubmit} />
|
|
501
499
|
</ResizableSidebar>
|
|
502
500
|
|
|
503
501
|
{/* Center content — normal document flow, browser scroll */}
|
|
@@ -606,14 +604,12 @@ function App() {
|
|
|
606
604
|
<button onClick={() => setMobileSidebar(false)} className="text-text-tertiary hover:text-text-secondary w-7 h-7 flex items-center justify-center">✕</button>
|
|
607
605
|
</div>
|
|
608
606
|
<div className="flex-1 overflow-hidden">
|
|
609
|
-
<AnnotationSidebar
|
|
607
|
+
<AnnotationSidebar onSubmit={handleSubmit} />
|
|
610
608
|
</div>
|
|
611
609
|
</div>
|
|
612
610
|
</div>
|
|
613
611
|
)}
|
|
614
612
|
|
|
615
|
-
<ResponsePreview open={previewOpen} onClose={() => setPreviewOpen(false)} onSubmit={handleSubmit} />
|
|
616
|
-
|
|
617
613
|
<ShareDialog
|
|
618
614
|
sessionId={sessionId}
|
|
619
615
|
revision={selectedRevision}
|
|
@@ -7,9 +7,11 @@ interface ResponsePreviewProps {
|
|
|
7
7
|
open: boolean;
|
|
8
8
|
onClose: () => void;
|
|
9
9
|
onSubmit: (feedback: string) => void;
|
|
10
|
+
includedRemoteIds: Set<string>;
|
|
11
|
+
onToggleRemoteId: (id: string) => void;
|
|
10
12
|
}
|
|
11
13
|
|
|
12
|
-
export function ResponsePreview({ open, onClose, onSubmit }: ResponsePreviewProps) {
|
|
14
|
+
export function ResponsePreview({ open, onClose, onSubmit, includedRemoteIds, onToggleRemoteId }: ResponsePreviewProps) {
|
|
13
15
|
const { annotations, generalNote, responses, feedbackEntries } = useAnnotations();
|
|
14
16
|
const [editedText, setEditedText] = useState("");
|
|
15
17
|
const [manuallyEdited, setManuallyEdited] = useState(false);
|
|
@@ -18,6 +20,11 @@ export function ResponsePreview({ open, onClose, onSubmit }: ResponsePreviewProp
|
|
|
18
20
|
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
|
19
21
|
const [prevOpen, setPrevOpen] = useState(false);
|
|
20
22
|
|
|
23
|
+
const remoteAnnotations = useMemo(
|
|
24
|
+
() => annotations.filter((a) => a.source === "remote"),
|
|
25
|
+
[annotations],
|
|
26
|
+
);
|
|
27
|
+
|
|
21
28
|
if (open && !prevOpen) {
|
|
22
29
|
setManuallyEdited(false);
|
|
23
30
|
setEditMode(false);
|
|
@@ -33,8 +40,8 @@ export function ResponsePreview({ open, onClose, onSubmit }: ResponsePreviewProp
|
|
|
33
40
|
}
|
|
34
41
|
|
|
35
42
|
const generatedText = useMemo(
|
|
36
|
-
() => generateMarkdown(annotations, generalNote, responses, feedbackEntries),
|
|
37
|
-
[annotations, generalNote, responses, feedbackEntries]
|
|
43
|
+
() => generateMarkdown(annotations, generalNote, responses, feedbackEntries, includedRemoteIds),
|
|
44
|
+
[annotations, generalNote, responses, feedbackEntries, includedRemoteIds],
|
|
38
45
|
);
|
|
39
46
|
const text = manuallyEdited ? editedText : generatedText;
|
|
40
47
|
|
|
@@ -111,6 +118,15 @@ export function ResponsePreview({ open, onClose, onSubmit }: ResponsePreviewProp
|
|
|
111
118
|
</div>
|
|
112
119
|
)}
|
|
113
120
|
|
|
121
|
+
{/* Remote feedback summary */}
|
|
122
|
+
{remoteAnnotations.length > 0 && (
|
|
123
|
+
<div className="px-5 py-2 border-t border-border-subtle flex-shrink-0">
|
|
124
|
+
<span className="text-[12px] text-text-secondary font-body">
|
|
125
|
+
Reviewer feedback: {includedRemoteIds.size}/{remoteAnnotations.length} included
|
|
126
|
+
</span>
|
|
127
|
+
</div>
|
|
128
|
+
)}
|
|
129
|
+
|
|
114
130
|
{/* Actions */}
|
|
115
131
|
{validationError && (
|
|
116
132
|
<div className="px-5 py-2 text-[12px] text-accent-red font-body border-t border-border-subtle flex-shrink-0 flex items-center justify-between gap-2">
|
|
@@ -10,11 +10,15 @@ export function generateMarkdown(
|
|
|
10
10
|
generalNote: string,
|
|
11
11
|
responses?: Map<string, PlanResponse>,
|
|
12
12
|
feedbackEntries?: Map<string, FeedbackEntry>,
|
|
13
|
+
includedRemoteIds?: Set<string>,
|
|
13
14
|
): string {
|
|
14
15
|
const parts: string[] = [];
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
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);
|
|
18
22
|
const resps = responses ? Array.from(responses.values()).filter((r) => hasValue(r)) : [];
|
|
19
23
|
const fbEntries = feedbackEntries
|
|
20
24
|
? Array.from(feedbackEntries.values()).filter((e) => e.markdown.trim())
|