@fabrika/canvas-kernel 0.1.45 → 0.3.1
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 +33 -67
- package/index.ts +20 -83
- package/package.json +69 -72
- package/src/client/AnnotationProvider.tsx +218 -255
- package/src/client/AnnotationSidebar.tsx +393 -338
- package/src/client/FileIcon.tsx +105 -66
- package/src/client/PlanRenderer.tsx +607 -551
- package/src/client/RenderErrorContext.ts +7 -7
- package/src/client/ResponsePreview.tsx +192 -173
- package/src/client/annotationDom.ts +43 -97
- package/src/client/annotationDraft.ts +106 -96
- package/src/client/appContext.tsx +40 -40
- package/src/client/components/Callout.tsx +20 -20
- package/src/client/components/Checklist.tsx +27 -29
- package/src/client/components/Choice.tsx +178 -159
- package/src/client/components/CodeBlock.tsx +27 -26
- package/src/client/components/Diff.tsx +16 -17
- package/src/client/components/FilePreview.tsx +102 -74
- package/src/client/components/Image.tsx +41 -39
- package/src/client/components/Markdown.tsx +33 -32
- package/src/client/components/Mermaid.tsx +593 -542
- package/src/client/components/Note.tsx +8 -8
- package/src/client/components/Priority.tsx +10 -12
- package/src/client/components/ResponseNote.tsx +16 -15
- package/src/client/components/Section.tsx +28 -26
- package/src/client/components/Table.tsx +33 -30
- package/src/client/components/Task.tsx +58 -60
- package/src/client/components/UserInput.tsx +169 -152
- package/src/client/components/index.ts +16 -16
- package/src/client/components/renderMarkdownChildren.tsx +86 -68
- package/src/client/diffLines.ts +51 -39
- package/src/client/generateMarkdown.ts +197 -191
- package/src/client/hostContext.tsx +39 -34
- package/src/client/index.ts +44 -125
- package/src/client/pageGlobals.ts +25 -0
- package/src/client/persistenceSchemas.ts +41 -0
- package/src/client/preact-all.ts +4 -4
- package/src/client/runtime.ts +58 -111
- package/src/client/styles.css +1 -74
- package/src/engine/compiler.ts +411 -323
- package/src/engine/file-admission.ts +25 -0
- package/src/engine/paths.ts +44 -44
- package/src/engine/session.ts +515 -626
- package/src/engine/sessionSchemas.ts +71 -0
- package/src/engine/watcher.ts +70 -60
- package/src/engine/websocket.ts +134 -132
- package/annotate.ts +0 -94
- package/daemon.ts +0 -46
- package/src/client/AnnotationDraftFooter.tsx +0 -76
- package/src/client/AnnotationEditor.tsx +0 -320
- package/src/client/AnnotationList.tsx +0 -190
- package/src/client/Popover.tsx +0 -361
- package/src/client/SessionContext.tsx +0 -3
- package/src/client/annotationContext.ts +0 -130
- package/src/client/annotationMarkdown.ts +0 -102
- package/src/client/annotationTarget.ts +0 -73
- package/src/client/blockTarget.ts +0 -127
- package/src/client/highlightRange.ts +0 -262
- package/src/client/popoverPosition.ts +0 -46
- package/src/client/regionTarget.ts +0 -180
- package/src/client/testing/dom.ts +0 -74
- package/src/client/textTarget.ts +0 -31
- package/src/client/useRegionAnnotation.tsx +0 -188
- package/src/client/useTextAnnotation.tsx +0 -194
- package/src/client/utils.ts +0 -33
- package/src/engine/channel.ts +0 -278
- package/src/engine/http-utils.ts +0 -14
- package/src/engine/lifecycle.ts +0 -302
- package/src/engine/router.ts +0 -16
- package/src/engine/waiter.ts +0 -188
|
@@ -1,17 +1,16 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import { FileIcon } from
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import { ResponsePreview } from "./ResponsePreview";
|
|
1
|
+
import { SessionContext } from '#canvas/runtime'
|
|
2
|
+
import { AnnotationDraftFooter, AnnotationList, fileAnnotationPath, RESPONSE_ANNOTATION_PATH } from '@fabrika/annotations'
|
|
3
|
+
import React, { useCallback, useContext, useEffect, useMemo, useState } from 'react'
|
|
4
|
+
import { findAnnotationElement, scrollToAnnotation } from './annotationDom'
|
|
5
|
+
import { useAnnotations } from './AnnotationProvider'
|
|
6
|
+
import type { Annotation } from './AnnotationProvider'
|
|
7
|
+
import { ActiveViewContext, RevisionContext } from './appContext'
|
|
8
|
+
import { FileIcon } from './FileIcon'
|
|
9
|
+
import { generateMarkdown, getMissingRequiredLabels, hasValue } from './generateMarkdown'
|
|
10
|
+
import { useCanvasHost } from './hostContext'
|
|
11
|
+
import { feedbackResponseSchema } from './persistenceSchemas'
|
|
12
|
+
import { MarkdownPreview } from './ResponsePreview'
|
|
13
|
+
import { ResponsePreview } from './ResponsePreview'
|
|
15
14
|
|
|
16
15
|
/**
|
|
17
16
|
* The canvas host's sidebar: the reusable annotation list and draft footer,
|
|
@@ -20,355 +19,411 @@ import { ResponsePreview } from "./ResponsePreview";
|
|
|
20
19
|
*/
|
|
21
20
|
|
|
22
21
|
interface AnnotationSidebarProps {
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
onSubmit: (feedback: string) => void
|
|
23
|
+
collapseButton?: React.ReactNode
|
|
25
24
|
}
|
|
26
25
|
|
|
27
26
|
export function AnnotationSidebar({ onSubmit, collapseButton }: AnnotationSidebarProps) {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
27
|
+
const { isReadOnly, selectedRevision, currentRevision, revisions, agentWatching } = useContext(RevisionContext)
|
|
28
|
+
const sessionId = useContext(SessionContext)
|
|
29
|
+
const selectedRevInfo = revisions.find((r) => r.revision === selectedRevision)
|
|
30
|
+
const isCurrentButSubmitted = isReadOnly && selectedRevision === currentRevision
|
|
31
|
+
const feedbackConsumed = !!selectedRevInfo?.feedbackConsumed
|
|
32
|
+
const roundLabel = selectedRevInfo?.label || `Round ${selectedRevision}`
|
|
33
|
+
|
|
34
|
+
if (isReadOnly) {
|
|
35
|
+
return (
|
|
36
|
+
<ReadOnlyAnnotationSidebar
|
|
37
|
+
sessionId={sessionId}
|
|
38
|
+
revision={selectedRevision}
|
|
39
|
+
label={roundLabel}
|
|
40
|
+
waitingForUpdate={isCurrentButSubmitted}
|
|
41
|
+
feedbackConsumed={feedbackConsumed}
|
|
42
|
+
agentWatching={agentWatching}
|
|
43
|
+
collapseButton={collapseButton}
|
|
44
|
+
/>
|
|
45
|
+
)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return <AnnotationSidebarInner onSubmit={onSubmit} agentWatching={agentWatching} collapseButton={collapseButton} />
|
|
40
49
|
}
|
|
41
50
|
|
|
42
|
-
function FeedbackDisplay(
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
51
|
+
function FeedbackDisplay(
|
|
52
|
+
{ sessionId, revision, label, waitingForUpdate, feedbackConsumed, agentWatching, collapseButton }: {
|
|
53
|
+
sessionId: string
|
|
54
|
+
revision: number
|
|
55
|
+
label: string
|
|
56
|
+
waitingForUpdate?: boolean
|
|
57
|
+
feedbackConsumed?: boolean
|
|
58
|
+
agentWatching?: boolean
|
|
59
|
+
collapseButton?: React.ReactNode
|
|
60
|
+
},
|
|
61
|
+
) {
|
|
62
|
+
const host = useCanvasHost()
|
|
63
|
+
const isSharedMode = host.isShared
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<div className="flex flex-col h-full">
|
|
67
|
+
<div className="px-4 py-3 flex items-center justify-between flex-shrink-0">
|
|
68
|
+
<span className="text-[11px] font-medium uppercase tracking-widest text-text-tertiary font-body">
|
|
69
|
+
{waitingForUpdate ? 'Feedback sent' : `Feedback — ${label}`}
|
|
70
|
+
</span>
|
|
71
|
+
{collapseButton}
|
|
72
|
+
</div>
|
|
73
|
+
|
|
74
|
+
{waitingForUpdate && !isSharedMode && <WaitingBanner feedbackConsumed={feedbackConsumed} agentWatching={agentWatching} />}
|
|
75
|
+
|
|
76
|
+
<FeedbackDisplayContent sessionId={sessionId} revision={revision} />
|
|
77
|
+
</div>
|
|
78
|
+
)
|
|
60
79
|
}
|
|
61
80
|
|
|
62
81
|
function WaitingBanner({ feedbackConsumed, agentWatching }: { feedbackConsumed?: boolean; agentWatching?: boolean }) {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
82
|
+
if (feedbackConsumed) {
|
|
83
|
+
return (
|
|
84
|
+
<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">
|
|
85
|
+
<span className="w-2 h-2 rounded-full bg-accent-green animate-pulse" />
|
|
86
|
+
<span className="text-[12px] font-body text-accent-green">Feedback received — waiting for next revision...</span>
|
|
87
|
+
</div>
|
|
88
|
+
)
|
|
89
|
+
}
|
|
90
|
+
if (agentWatching) {
|
|
91
|
+
return (
|
|
92
|
+
<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">
|
|
93
|
+
<span className="w-2 h-2 rounded-full bg-accent-blue animate-pulse" />
|
|
94
|
+
<span className="text-[12px] font-body text-accent-blue">Waiting for agent to pick up feedback...</span>
|
|
95
|
+
</div>
|
|
96
|
+
)
|
|
97
|
+
}
|
|
98
|
+
return (
|
|
99
|
+
<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">
|
|
100
|
+
<span className="w-2 h-2 rounded-full bg-accent-amber" />
|
|
101
|
+
<span className="text-[12px] font-body text-accent-amber">Agent disconnected — tell Claude to check feedback</span>
|
|
102
|
+
</div>
|
|
103
|
+
)
|
|
85
104
|
}
|
|
86
105
|
|
|
87
|
-
type ReadOnlyTab =
|
|
88
|
-
|
|
89
|
-
function ReadOnlyAnnotationSidebar(
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
106
|
+
type ReadOnlyTab = 'feedback' | 'annotations'
|
|
107
|
+
|
|
108
|
+
function ReadOnlyAnnotationSidebar(
|
|
109
|
+
{ sessionId, revision, label, waitingForUpdate, feedbackConsumed, agentWatching, collapseButton }: {
|
|
110
|
+
sessionId: string
|
|
111
|
+
revision: number
|
|
112
|
+
label: string
|
|
113
|
+
waitingForUpdate?: boolean
|
|
114
|
+
feedbackConsumed?: boolean
|
|
115
|
+
agentWatching?: boolean
|
|
116
|
+
collapseButton?: React.ReactNode
|
|
117
|
+
},
|
|
118
|
+
) {
|
|
119
|
+
const { annotations, generalNote } = useAnnotations()
|
|
120
|
+
const hasAnnotations = annotations.length > 0 || generalNote.trim().length > 0
|
|
121
|
+
const [activeTab, setActiveTab] = useState<ReadOnlyTab>('feedback')
|
|
122
|
+
|
|
123
|
+
// If no annotations in localStorage, just show feedback
|
|
124
|
+
if (!hasAnnotations) {
|
|
125
|
+
return (
|
|
126
|
+
<FeedbackDisplay
|
|
127
|
+
sessionId={sessionId}
|
|
128
|
+
revision={revision}
|
|
129
|
+
label={label}
|
|
130
|
+
waitingForUpdate={waitingForUpdate}
|
|
131
|
+
feedbackConsumed={feedbackConsumed}
|
|
132
|
+
agentWatching={agentWatching}
|
|
133
|
+
collapseButton={collapseButton}
|
|
134
|
+
/>
|
|
135
|
+
)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return (
|
|
139
|
+
<div className="flex flex-col h-full">
|
|
140
|
+
{/* Header with tab switcher */}
|
|
141
|
+
<div className="px-4 py-3 flex items-center justify-between flex-shrink-0">
|
|
142
|
+
<div className="flex items-center gap-1 bg-bg-input rounded-md p-0.5">
|
|
143
|
+
<button
|
|
144
|
+
onClick={() => setActiveTab('feedback')}
|
|
145
|
+
className={`px-2.5 py-1 rounded text-[11px] font-medium font-body transition-colors ${
|
|
146
|
+
activeTab === 'feedback'
|
|
147
|
+
? 'bg-bg-elevated text-text-primary shadow-sm'
|
|
148
|
+
: 'text-text-tertiary hover:text-text-secondary'
|
|
149
|
+
}`}
|
|
150
|
+
>
|
|
151
|
+
Feedback
|
|
152
|
+
</button>
|
|
153
|
+
<button
|
|
154
|
+
onClick={() => setActiveTab('annotations')}
|
|
155
|
+
className={`px-2.5 py-1 rounded text-[11px] font-medium font-body transition-colors flex items-center gap-1.5 ${
|
|
156
|
+
activeTab === 'annotations'
|
|
157
|
+
? 'bg-bg-elevated text-text-primary shadow-sm'
|
|
158
|
+
: 'text-text-tertiary hover:text-text-secondary'
|
|
159
|
+
}`}
|
|
160
|
+
>
|
|
161
|
+
Annotations
|
|
162
|
+
<span className="inline-flex items-center justify-center min-w-[16px] h-[16px] px-1 rounded-full bg-border-subtle text-[10px] font-medium">
|
|
163
|
+
{annotations.length}
|
|
164
|
+
</span>
|
|
165
|
+
</button>
|
|
166
|
+
</div>
|
|
167
|
+
{collapseButton}
|
|
168
|
+
</div>
|
|
169
|
+
|
|
170
|
+
{waitingForUpdate && <WaitingBanner feedbackConsumed={feedbackConsumed} agentWatching={agentWatching} />}
|
|
171
|
+
|
|
172
|
+
{activeTab === 'feedback' ? <FeedbackDisplayContent sessionId={sessionId} revision={revision} /> : (
|
|
173
|
+
<>
|
|
174
|
+
<div className="flex-1 overflow-y-auto">
|
|
175
|
+
<CanvasAnnotationGroups annotations={annotations} readOnly />
|
|
176
|
+
</div>
|
|
177
|
+
{generalNote.trim() && (
|
|
178
|
+
<div className="border-t border-border-subtle px-4 py-3 flex-shrink-0">
|
|
179
|
+
<div className="text-[13px] font-body text-text-primary leading-relaxed">
|
|
180
|
+
{generalNote}
|
|
181
|
+
</div>
|
|
182
|
+
</div>
|
|
183
|
+
)}
|
|
184
|
+
</>
|
|
185
|
+
)}
|
|
186
|
+
</div>
|
|
187
|
+
)
|
|
149
188
|
}
|
|
150
189
|
|
|
151
190
|
function FeedbackDisplayContent({ sessionId, revision }: { sessionId: string; revision: number }) {
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
191
|
+
const host = useCanvasHost()
|
|
192
|
+
const [feedback, setFeedback] = useState<string | null>(null)
|
|
193
|
+
const [loading, setLoading] = useState(true)
|
|
194
|
+
|
|
195
|
+
useEffect(() => {
|
|
196
|
+
setLoading(true)
|
|
197
|
+
// Shared mode has no prior local feedback to fetch — shared canvases
|
|
198
|
+
// are always one-off snapshots. Skip the call entirely.
|
|
199
|
+
if (!host.fsAvailable) {
|
|
200
|
+
setFeedback(null)
|
|
201
|
+
setLoading(false)
|
|
202
|
+
return
|
|
203
|
+
}
|
|
204
|
+
fetch(`/api/session/${sessionId}/revision/${revision}/feedback`)
|
|
205
|
+
.then((r) => r.json())
|
|
206
|
+
.then((data: unknown) => {
|
|
207
|
+
setFeedback(feedbackResponseSchema.parse(data).feedback || null)
|
|
208
|
+
setLoading(false)
|
|
209
|
+
})
|
|
210
|
+
.catch(() => {
|
|
211
|
+
setFeedback(null)
|
|
212
|
+
setLoading(false)
|
|
213
|
+
})
|
|
214
|
+
}, [sessionId, revision])
|
|
215
|
+
|
|
216
|
+
return (
|
|
217
|
+
<div className="flex-1 overflow-y-auto px-4">
|
|
218
|
+
{loading
|
|
219
|
+
? <p className="text-[12px] text-text-tertiary font-body py-4">Loading...</p>
|
|
220
|
+
: feedback
|
|
221
|
+
? <MarkdownPreview text={feedback} />
|
|
222
|
+
: <p className="text-[12px] text-text-tertiary font-body py-4">No feedback was submitted for this revision.</p>}
|
|
223
|
+
</div>
|
|
224
|
+
)
|
|
178
225
|
}
|
|
179
226
|
|
|
180
227
|
/** How a canvas splits its annotations: the agent's response, the canvas
|
|
181
228
|
* itself, then one group per file the reader opened. */
|
|
182
229
|
function groupAnnotations(annotations: Annotation[]) {
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
230
|
+
const response = annotations.filter((a) => a.filePath === RESPONSE_ANNOTATION_PATH)
|
|
231
|
+
const plan = annotations.filter((a) => !a.filePath)
|
|
232
|
+
const files = annotations.filter((a) => fileAnnotationPath(a) !== undefined)
|
|
233
|
+
const fileGroups: Record<string, Annotation[]> = {}
|
|
234
|
+
for (const ann of files) {
|
|
235
|
+
const key = fileAnnotationPath(ann)
|
|
236
|
+
if (!key) continue
|
|
237
|
+
const group = fileGroups[key]
|
|
238
|
+
if (group) group.push(ann)
|
|
239
|
+
else fileGroups[key] = [ann]
|
|
240
|
+
}
|
|
241
|
+
const hasMultipleGroups = [response, plan, files].filter((g) => g.length > 0).length > 1
|
|
242
|
+
return { response, plan, fileGroups, hasMultipleGroups }
|
|
196
243
|
}
|
|
197
244
|
|
|
198
245
|
function GroupHeading({ children }: { children: React.ReactNode }) {
|
|
199
|
-
|
|
246
|
+
return <div className="text-[10px] uppercase tracking-widest text-text-tertiary font-body px-3 mb-1 mt-1">{children}</div>
|
|
200
247
|
}
|
|
201
248
|
|
|
202
249
|
function CanvasAnnotationGroups({ annotations, readOnly, includedRemoteIds, onToggleRemoteId }: {
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
250
|
+
annotations: Annotation[]
|
|
251
|
+
readOnly?: boolean
|
|
252
|
+
includedRemoteIds?: Set<string>
|
|
253
|
+
onToggleRemoteId?: (id: string) => void
|
|
207
254
|
}) {
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
255
|
+
const { setActiveView } = useContext(ActiveViewContext)
|
|
256
|
+
const { response, plan, fileGroups, hasMultipleGroups } = groupAnnotations(annotations)
|
|
257
|
+
|
|
258
|
+
// Selecting a file annotation means opening its file first; the element only
|
|
259
|
+
// exists once that view is mounted.
|
|
260
|
+
const onSelect = useCallback((ann: Annotation) => {
|
|
261
|
+
const filePath = fileAnnotationPath(ann)
|
|
262
|
+
if (!filePath) return
|
|
263
|
+
setActiveView({ type: 'file', path: filePath })
|
|
264
|
+
setTimeout(() => {
|
|
265
|
+
const el = findAnnotationElement(ann)
|
|
266
|
+
if (el) el.scrollIntoView({ behavior: 'smooth', block: 'center' })
|
|
267
|
+
}, 150)
|
|
268
|
+
}, [setActiveView])
|
|
269
|
+
|
|
270
|
+
const onReveal = useCallback((ann: Annotation) => scrollToAnnotation(ann, setActiveView), [setActiveView])
|
|
271
|
+
const listProps = { readOnly, onSelect, onReveal, includedRemoteIds, onToggleRemoteId }
|
|
272
|
+
|
|
273
|
+
return (
|
|
274
|
+
<>
|
|
275
|
+
{response.length > 0 && (
|
|
276
|
+
<>
|
|
277
|
+
{hasMultipleGroups && <GroupHeading>Agent Response</GroupHeading>}
|
|
278
|
+
<AnnotationList annotations={response} {...listProps} />
|
|
279
|
+
</>
|
|
280
|
+
)}
|
|
281
|
+
|
|
282
|
+
{plan.length > 0 && (
|
|
283
|
+
<>
|
|
284
|
+
{hasMultipleGroups && <GroupHeading>Canvas</GroupHeading>}
|
|
285
|
+
<AnnotationList annotations={plan} {...listProps} />
|
|
286
|
+
</>
|
|
287
|
+
)}
|
|
288
|
+
|
|
289
|
+
{Object.entries(fileGroups).map(([filePath, anns]) => (
|
|
290
|
+
<div key={filePath}>
|
|
291
|
+
<button
|
|
292
|
+
onClick={() => setActiveView({ type: 'file', path: filePath })}
|
|
293
|
+
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"
|
|
294
|
+
title={filePath}
|
|
295
|
+
>
|
|
296
|
+
<FileIcon name={filePath.split('/').pop() || filePath} type="file" />
|
|
297
|
+
<span className="truncate">{filePath}</span>
|
|
298
|
+
</button>
|
|
299
|
+
<AnnotationList annotations={anns} {...listProps} />
|
|
300
|
+
</div>
|
|
301
|
+
))}
|
|
302
|
+
</>
|
|
303
|
+
)
|
|
257
304
|
}
|
|
258
305
|
|
|
259
|
-
function AnnotationSidebarInner({ onSubmit, agentWatching, collapseButton }: Omit<AnnotationSidebarProps,
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
306
|
+
function AnnotationSidebarInner({ onSubmit, agentWatching, collapseButton }: Omit<AnnotationSidebarProps, 'onPreview'> & { agentWatching: boolean }) {
|
|
307
|
+
const host = useCanvasHost()
|
|
308
|
+
const [previewOpen, setPreviewOpen] = useState(false)
|
|
309
|
+
const {
|
|
310
|
+
annotations,
|
|
311
|
+
generalNote,
|
|
312
|
+
submittableResponses,
|
|
313
|
+
feedbackEntries,
|
|
314
|
+
} = useAnnotations()
|
|
315
|
+
|
|
316
|
+
const remoteAnnotations = useMemo(() => annotations.filter((a) => a.source === 'remote'), [annotations])
|
|
317
|
+
const [includedRemoteIds, setIncludedRemoteIds] = useState<Set<string>>(() => new Set())
|
|
318
|
+
|
|
319
|
+
// Auto-include new remote annotations as they arrive
|
|
320
|
+
useEffect(() => {
|
|
321
|
+
if (remoteAnnotations.length === 0) return
|
|
322
|
+
setIncludedRemoteIds((prev) => {
|
|
323
|
+
const next = new Set(prev)
|
|
324
|
+
for (const a of remoteAnnotations) next.add(a.id)
|
|
325
|
+
return next.size === prev.size ? prev : next
|
|
326
|
+
})
|
|
327
|
+
}, [remoteAnnotations])
|
|
328
|
+
|
|
329
|
+
const toggleRemoteId = useCallback((id: string) => {
|
|
330
|
+
setIncludedRemoteIds((prev) => {
|
|
331
|
+
const next = new Set(prev)
|
|
332
|
+
if (next.has(id)) next.delete(id)
|
|
333
|
+
else next.add(id)
|
|
334
|
+
return next
|
|
335
|
+
})
|
|
336
|
+
}, [])
|
|
337
|
+
|
|
338
|
+
const hasResponses = Array.from(submittableResponses.values()).some(hasValue)
|
|
339
|
+
const hasFeedback = feedbackEntries.size > 0
|
|
340
|
+
const hasContent = annotations.length > 0 || generalNote.trim().length > 0 || hasResponses || hasFeedback
|
|
341
|
+
const [validationError, setValidationError] = useState<string | null>(null)
|
|
342
|
+
|
|
343
|
+
const submitMarkdown = () => {
|
|
344
|
+
const md = generateMarkdown(annotations, generalNote, submittableResponses, feedbackEntries, includedRemoteIds)
|
|
345
|
+
onSubmit(md)
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
return (
|
|
349
|
+
<div className="flex flex-col h-full">
|
|
350
|
+
{/* Header */}
|
|
351
|
+
<div className="px-4 py-3 flex items-center justify-between flex-shrink-0">
|
|
352
|
+
<span className="text-[11px] font-medium uppercase tracking-widest text-text-tertiary font-body flex items-center gap-2">
|
|
353
|
+
Annotations
|
|
354
|
+
{annotations.length > 0 && (
|
|
355
|
+
<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">
|
|
356
|
+
{annotations.length}
|
|
357
|
+
</span>
|
|
358
|
+
)}
|
|
359
|
+
{!host.isShared && (
|
|
360
|
+
<span
|
|
361
|
+
className={`w-1.5 h-1.5 rounded-full flex-shrink-0 ${agentWatching ? 'bg-accent-green' : 'bg-accent-amber'}`}
|
|
362
|
+
title={agentWatching ? 'Agent connected' : 'Agent disconnected'}
|
|
363
|
+
/>
|
|
364
|
+
)}
|
|
365
|
+
</span>
|
|
366
|
+
{collapseButton}
|
|
367
|
+
</div>
|
|
368
|
+
|
|
369
|
+
{/* Annotation list */}
|
|
370
|
+
<div className="flex-1 overflow-y-auto">
|
|
371
|
+
{annotations.length === 0 && (
|
|
372
|
+
<p className="text-[12px] text-text-tertiary px-3 py-4 leading-relaxed font-body">
|
|
373
|
+
Select text in the canvas or in files to add annotations.
|
|
374
|
+
</p>
|
|
375
|
+
)}
|
|
376
|
+
|
|
377
|
+
<CanvasAnnotationGroups
|
|
378
|
+
annotations={annotations}
|
|
379
|
+
includedRemoteIds={includedRemoteIds}
|
|
380
|
+
onToggleRemoteId={toggleRemoteId}
|
|
381
|
+
/>
|
|
382
|
+
</div>
|
|
383
|
+
|
|
384
|
+
<AnnotationDraftFooter
|
|
385
|
+
hasContent={hasContent}
|
|
386
|
+
onSubmit={() => {
|
|
387
|
+
const allMissing = getMissingRequiredLabels(submittableResponses, feedbackEntries)
|
|
388
|
+
if (allMissing.length > 0) {
|
|
389
|
+
setValidationError(`Please answer: ${allMissing.join(', ')}`)
|
|
390
|
+
return
|
|
391
|
+
}
|
|
392
|
+
setValidationError(null)
|
|
393
|
+
submitMarkdown()
|
|
394
|
+
}}
|
|
395
|
+
onSubmitEmpty={() => onSubmit('No feedback — looks good.')}
|
|
396
|
+
notice={validationError && (
|
|
397
|
+
<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">
|
|
398
|
+
<span>{validationError}</span>
|
|
399
|
+
<button
|
|
400
|
+
onClick={() => {
|
|
401
|
+
setValidationError(null)
|
|
402
|
+
submitMarkdown()
|
|
403
|
+
}}
|
|
404
|
+
className="text-[11px] text-text-tertiary hover:text-text-secondary font-body whitespace-nowrap underline"
|
|
405
|
+
>
|
|
406
|
+
Submit anyway
|
|
407
|
+
</button>
|
|
408
|
+
</div>
|
|
409
|
+
)}
|
|
410
|
+
secondaryAction={
|
|
411
|
+
<button
|
|
412
|
+
onClick={() => setPreviewOpen(true)}
|
|
413
|
+
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"
|
|
414
|
+
>
|
|
415
|
+
Preview
|
|
416
|
+
</button>
|
|
417
|
+
}
|
|
418
|
+
/>
|
|
419
|
+
|
|
420
|
+
<ResponsePreview
|
|
421
|
+
open={previewOpen}
|
|
422
|
+
onClose={() => setPreviewOpen(false)}
|
|
423
|
+
onSubmit={onSubmit}
|
|
424
|
+
includedRemoteIds={includedRemoteIds}
|
|
425
|
+
onToggleRemoteId={toggleRemoteId}
|
|
426
|
+
/>
|
|
427
|
+
</div>
|
|
428
|
+
)
|
|
374
429
|
}
|