agent-canvas 0.1.28 → 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.
@@ -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({ onPreview, onSubmit, collapseButton }: AnnotationSidebarProps) {
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 onPreview={onPreview} onSubmit={onSubmit} agentWatching={agentWatching} collapseButton={collapseButton} />;
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({ onPreview, onSubmit, agentWatching, collapseButton }: AnnotationSidebarProps & { agentWatching: boolean }) {
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={onPreview}
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
-
@@ -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
- setPreviewOpen(false);
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 onPreview={() => setPreviewOpen(true)} onSubmit={handleSubmit} />
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">&#x2715;</button>
607
605
  </div>
608
606
  <div className="flex-1 overflow-hidden">
609
- <AnnotationSidebar onPreview={() => setPreviewOpen(true)} onSubmit={handleSubmit} />
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 responseAnns = annotations.filter((a) => a.filePath === RESPONSE_ANNOTATION_PATH);
16
- const canvasAnns = annotations.filter((a) => !a.filePath);
17
- const fileAnns = annotations.filter((a) => a.filePath && a.filePath !== RESPONSE_ANNOTATION_PATH);
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())