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,224 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Wraps the current browser Range in <mark> elements with the given annotation ID.
|
|
3
|
-
* Works for multiline / cross-element selections.
|
|
4
|
-
*/
|
|
5
|
-
export function wrapRangeWithMark(range: Range, annotationId: string): HTMLElement[] {
|
|
6
|
-
const marks: HTMLElement[] = [];
|
|
7
|
-
|
|
8
|
-
// Collect all text nodes within the range
|
|
9
|
-
const textNodes = getTextNodesInRange(range);
|
|
10
|
-
|
|
11
|
-
for (const tn of textNodes) {
|
|
12
|
-
const nodeRange = document.createRange();
|
|
13
|
-
nodeRange.selectNodeContents(tn);
|
|
14
|
-
|
|
15
|
-
// Clamp to the actual selection range
|
|
16
|
-
if (tn === range.startContainer) {
|
|
17
|
-
nodeRange.setStart(tn, range.startOffset);
|
|
18
|
-
}
|
|
19
|
-
if (tn === range.endContainer) {
|
|
20
|
-
nodeRange.setEnd(tn, range.endOffset);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
const text = nodeRange.toString();
|
|
24
|
-
if (!text || !text.trim()) continue;
|
|
25
|
-
|
|
26
|
-
// Wrap this portion in a <mark>
|
|
27
|
-
const mark = document.createElement("mark");
|
|
28
|
-
mark.setAttribute("data-annotation-id", annotationId);
|
|
29
|
-
|
|
30
|
-
try {
|
|
31
|
-
nodeRange.surroundContents(mark);
|
|
32
|
-
} catch {
|
|
33
|
-
// surroundContents fails if range crosses element boundaries within a single text node
|
|
34
|
-
// (shouldn't happen for a single text node, but handle gracefully)
|
|
35
|
-
const fragment = nodeRange.extractContents();
|
|
36
|
-
mark.appendChild(fragment);
|
|
37
|
-
nodeRange.insertNode(mark);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
marks.push(mark);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
return marks;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function getTextNodesInRange(range: Range): Text[] {
|
|
47
|
-
const nodes: Text[] = [];
|
|
48
|
-
const ancestor = range.commonAncestorContainer;
|
|
49
|
-
|
|
50
|
-
if (ancestor.nodeType === Node.TEXT_NODE) {
|
|
51
|
-
nodes.push(ancestor as Text);
|
|
52
|
-
return nodes;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
const walker = document.createTreeWalker(ancestor, NodeFilter.SHOW_TEXT);
|
|
56
|
-
let node: Text | null;
|
|
57
|
-
while ((node = walker.nextNode() as Text | null)) {
|
|
58
|
-
// Skip line numbers and existing marks
|
|
59
|
-
const parent = node.parentElement;
|
|
60
|
-
if (parent?.classList.contains("select-none")) continue;
|
|
61
|
-
if (parent?.closest("[data-annotation-id]")) continue;
|
|
62
|
-
|
|
63
|
-
if (range.intersectsNode(node)) {
|
|
64
|
-
nodes.push(node);
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
return nodes;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* Update the visual state of marks for a given annotation ID.
|
|
73
|
-
*/
|
|
74
|
-
export function setMarkActive(annotationId: string, active: boolean) {
|
|
75
|
-
const marks = document.querySelectorAll(`[data-annotation-id="${annotationId}"]`);
|
|
76
|
-
for (const mark of marks) {
|
|
77
|
-
(mark as HTMLElement).classList.toggle("mark-active", active);
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Update active state for annotation marks.
|
|
83
|
-
* Only touches marks for the previous and current active IDs to avoid full DOM scans.
|
|
84
|
-
*/
|
|
85
|
-
export function updateAllMarkStates(activeId: string | null, prevActiveId?: string | null) {
|
|
86
|
-
if (prevActiveId != null && prevActiveId !== activeId) {
|
|
87
|
-
setMarkActive(prevActiveId, false);
|
|
88
|
-
}
|
|
89
|
-
if (activeId != null) {
|
|
90
|
-
setMarkActive(activeId, true);
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* Rename annotation ID on existing marks (e.g. temp → real ID).
|
|
96
|
-
*/
|
|
97
|
-
export function renameMarkId(oldId: string, newId: string) {
|
|
98
|
-
const marks = document.querySelectorAll(`[data-annotation-id="${oldId}"]`);
|
|
99
|
-
for (const mark of marks) {
|
|
100
|
-
mark.setAttribute("data-annotation-id", newId);
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* Restore marks for persisted annotations by finding their snippet text in the DOM.
|
|
106
|
-
* Uses before/after context for disambiguation when the same text appears multiple times.
|
|
107
|
-
*/
|
|
108
|
-
export function restoreMarks(
|
|
109
|
-
container: HTMLElement,
|
|
110
|
-
annotations: { id: string; snippet: string; filePath?: string; context?: { before: string; after: string; hierarchy: string[] } }[]
|
|
111
|
-
) {
|
|
112
|
-
for (const ann of annotations) {
|
|
113
|
-
// A canvas can be rendered more than once (overview, compare, hidden
|
|
114
|
-
// source panes), so restore decisions must be scoped to this container.
|
|
115
|
-
if (container.querySelector(`[data-annotation-id="${ann.id}"]`)) continue;
|
|
116
|
-
|
|
117
|
-
const snippet = ann.snippet;
|
|
118
|
-
if (!snippet) continue;
|
|
119
|
-
|
|
120
|
-
// Rebuild after every inserted mark. Wrapping text splits and moves text
|
|
121
|
-
// nodes, so a cached offset map can point at stale DOM after the first hit.
|
|
122
|
-
const { fullText, textNodes } = buildTextIndex(container);
|
|
123
|
-
|
|
124
|
-
// Find all occurrences of the snippet in the full text
|
|
125
|
-
const occurrences: number[] = [];
|
|
126
|
-
let searchFrom = 0;
|
|
127
|
-
while (true) {
|
|
128
|
-
const idx = fullText.indexOf(snippet, searchFrom);
|
|
129
|
-
if (idx === -1) break;
|
|
130
|
-
occurrences.push(idx);
|
|
131
|
-
searchFrom = idx + 1;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
if (occurrences.length === 0) continue;
|
|
135
|
-
|
|
136
|
-
// Pick the best occurrence using context
|
|
137
|
-
let bestIdx = occurrences[0];
|
|
138
|
-
if (occurrences.length > 1 && ann.context) {
|
|
139
|
-
let bestScore = -1;
|
|
140
|
-
for (const idx of occurrences) {
|
|
141
|
-
let score = 0;
|
|
142
|
-
if (ann.context.before) {
|
|
143
|
-
const preceding = fullText.slice(Math.max(0, idx - 80), idx);
|
|
144
|
-
if (preceding.includes(ann.context.before)) score += 2;
|
|
145
|
-
else if (ann.context.before.length > 10 && preceding.includes(ann.context.before.slice(-10))) score += 1;
|
|
146
|
-
}
|
|
147
|
-
if (ann.context.after) {
|
|
148
|
-
const following = fullText.slice(idx + snippet.length, idx + snippet.length + 80);
|
|
149
|
-
if (following.includes(ann.context.after)) score += 2;
|
|
150
|
-
else if (ann.context.after.length > 10 && following.includes(ann.context.after.slice(0, 10))) score += 1;
|
|
151
|
-
}
|
|
152
|
-
if (score > bestScore) {
|
|
153
|
-
bestScore = score;
|
|
154
|
-
bestIdx = idx;
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
// Map text offsets to DOM positions
|
|
160
|
-
const startOffset = bestIdx;
|
|
161
|
-
const endOffset = bestIdx + snippet.length;
|
|
162
|
-
|
|
163
|
-
const startPos = offsetToNode(textNodes, startOffset);
|
|
164
|
-
const endPos = offsetToNode(textNodes, endOffset);
|
|
165
|
-
if (!startPos || !endPos) continue;
|
|
166
|
-
|
|
167
|
-
try {
|
|
168
|
-
const range = document.createRange();
|
|
169
|
-
range.setStart(startPos.node, startPos.offset);
|
|
170
|
-
range.setEnd(endPos.node, endPos.offset);
|
|
171
|
-
wrapRangeWithMark(range, ann.id);
|
|
172
|
-
} catch {}
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
function buildTextIndex(container: HTMLElement): {
|
|
177
|
-
fullText: string;
|
|
178
|
-
textNodes: { node: Text; start: number }[];
|
|
179
|
-
} {
|
|
180
|
-
const textNodes: { node: Text; start: number }[] = [];
|
|
181
|
-
let fullText = "";
|
|
182
|
-
const walker = document.createTreeWalker(container, NodeFilter.SHOW_TEXT);
|
|
183
|
-
let tn: Text | null;
|
|
184
|
-
while ((tn = walker.nextNode() as Text | null)) {
|
|
185
|
-
const parent = tn.parentElement;
|
|
186
|
-
if (parent?.classList.contains("select-none")) continue;
|
|
187
|
-
if (parent?.closest("[data-annotation-id]")) continue;
|
|
188
|
-
textNodes.push({ node: tn, start: fullText.length });
|
|
189
|
-
fullText += tn.textContent || "";
|
|
190
|
-
}
|
|
191
|
-
return { fullText, textNodes };
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
function offsetToNode(
|
|
195
|
-
textNodes: { node: Text; start: number }[],
|
|
196
|
-
offset: number
|
|
197
|
-
): { node: Text; offset: number } | null {
|
|
198
|
-
for (let i = textNodes.length - 1; i >= 0; i--) {
|
|
199
|
-
if (textNodes[i].start <= offset) {
|
|
200
|
-
const localOffset = offset - textNodes[i].start;
|
|
201
|
-
const nodeLen = textNodes[i].node.textContent?.length ?? 0;
|
|
202
|
-
if (localOffset <= nodeLen) {
|
|
203
|
-
return { node: textNodes[i].node, offset: localOffset };
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
return null;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
/**
|
|
211
|
-
* Remove marks for a given annotation ID, restoring the original text nodes.
|
|
212
|
-
*/
|
|
213
|
-
export function unwrapMarks(annotationId: string) {
|
|
214
|
-
const marks = document.querySelectorAll(`[data-annotation-id="${annotationId}"]`);
|
|
215
|
-
for (const mark of marks) {
|
|
216
|
-
const parent = mark.parentNode;
|
|
217
|
-
if (!parent) continue;
|
|
218
|
-
while (mark.firstChild) {
|
|
219
|
-
parent.insertBefore(mark.firstChild, mark);
|
|
220
|
-
}
|
|
221
|
-
parent.removeChild(mark);
|
|
222
|
-
parent.normalize();
|
|
223
|
-
}
|
|
224
|
-
}
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Calculate popover position relative to an anchor element.
|
|
3
|
-
* Returns absolute position within the scroll container (or body).
|
|
4
|
-
*/
|
|
5
|
-
export function getPopoverPosition(
|
|
6
|
-
anchor: HTMLElement,
|
|
7
|
-
scrollContainer?: HTMLElement | null,
|
|
8
|
-
): { style: Record<string, string>; parent: HTMLElement } {
|
|
9
|
-
const popoverWidth = 280;
|
|
10
|
-
const gap = 8;
|
|
11
|
-
const parent = scrollContainer || document.body;
|
|
12
|
-
|
|
13
|
-
// Use getBoundingClientRect for both anchor and parent,
|
|
14
|
-
// then add scroll offsets to get absolute position within parent
|
|
15
|
-
const anchorRect = anchor.getBoundingClientRect();
|
|
16
|
-
const parentRect = parent.getBoundingClientRect();
|
|
17
|
-
|
|
18
|
-
// Vertical: anchor bottom relative to parent top + any scroll
|
|
19
|
-
const scrollTop = parent === document.body ? window.scrollY : parent.scrollTop;
|
|
20
|
-
const top = anchorRect.bottom - parentRect.top + scrollTop + gap;
|
|
21
|
-
|
|
22
|
-
// Horizontal: clamp to parent width
|
|
23
|
-
const left = Math.max(0, Math.min(anchorRect.left - parentRect.left, parentRect.width - popoverWidth - 12));
|
|
24
|
-
|
|
25
|
-
parent.style.position = "relative";
|
|
26
|
-
|
|
27
|
-
return {
|
|
28
|
-
style: { position: "absolute", top: `${top}px`, left: `${left}px` },
|
|
29
|
-
parent,
|
|
30
|
-
};
|
|
31
|
-
}
|
package/daemon/client/runtime.ts
DELETED
|
@@ -1,123 +0,0 @@
|
|
|
1
|
-
// #canvas/runtime — shared context between client app and components bundle
|
|
2
|
-
// The React.createContext call MUST live here (not in AnnotationProvider)
|
|
3
|
-
// so that both bundles reference the same context object.
|
|
4
|
-
|
|
5
|
-
import { createContext, useContext, useEffect } from "react";
|
|
6
|
-
|
|
7
|
-
export interface AnnotationContext {
|
|
8
|
-
before: string;
|
|
9
|
-
after: string;
|
|
10
|
-
hierarchy: string[];
|
|
11
|
-
lineStart?: number;
|
|
12
|
-
lineEnd?: number;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
/** Identifies the person who created an annotation. For local annotations
|
|
16
|
-
* this is the daemon owner (unset). For remote annotations it carries the
|
|
17
|
-
* reviewer's self-reported name + stable anonymous id assigned by the worker. */
|
|
18
|
-
export interface AnnotationAuthor {
|
|
19
|
-
id: string;
|
|
20
|
-
name: string;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/** An image or other file attached to an annotation. Replaces the legacy
|
|
24
|
-
* `images: string[]` pattern with a richer shape that can point to either
|
|
25
|
-
* a local upload (daemon) or a remote blob (CF Worker R2). */
|
|
26
|
-
export interface AnnotationAttachment {
|
|
27
|
-
url: string;
|
|
28
|
-
mime?: string;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export interface Annotation {
|
|
32
|
-
id: string;
|
|
33
|
-
snippet: string;
|
|
34
|
-
note: string;
|
|
35
|
-
createdAt: string;
|
|
36
|
-
filePath?: string;
|
|
37
|
-
canvasFile?: string;
|
|
38
|
-
context?: AnnotationContext;
|
|
39
|
-
/** @deprecated use `attachments` — kept for localStorage backward compat */
|
|
40
|
-
images?: string[];
|
|
41
|
-
attachments?: AnnotationAttachment[];
|
|
42
|
-
/** "remote" annotations come from a shared view and are read-only locally. */
|
|
43
|
-
source?: "local" | "remote";
|
|
44
|
-
/** Only set for source === "remote". */
|
|
45
|
-
author?: AnnotationAuthor;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export interface PlanResponse {
|
|
49
|
-
id: string;
|
|
50
|
-
type: "select" | "radio" | "checkbox" | "text" | "range";
|
|
51
|
-
label: string;
|
|
52
|
-
value: any;
|
|
53
|
-
options?: string[];
|
|
54
|
-
required?: boolean;
|
|
55
|
-
note?: string;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export interface FeedbackEntry {
|
|
59
|
-
id: string;
|
|
60
|
-
markdown: string;
|
|
61
|
-
label?: string;
|
|
62
|
-
required?: boolean;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export interface AnnotationContextValue {
|
|
66
|
-
annotations: Annotation[];
|
|
67
|
-
addAnnotation: (snippet: string, note: string, filePath?: string) => void;
|
|
68
|
-
addAnnotationWithId: (id: string, snippet: string, note: string, filePath?: string, context?: AnnotationContext, images?: string[], canvasFile?: string) => void;
|
|
69
|
-
updateAnnotation: (id: string, note: string) => void;
|
|
70
|
-
removeAnnotation: (id: string) => void;
|
|
71
|
-
addAnnotationImage: (id: string, imagePath: string) => void;
|
|
72
|
-
removeAnnotationImage: (id: string, imagePath: string) => void;
|
|
73
|
-
generalNote: string;
|
|
74
|
-
setGeneralNote: (text: string) => void;
|
|
75
|
-
clearAll: () => void;
|
|
76
|
-
activeAnnotationId: string | null;
|
|
77
|
-
setActiveAnnotationId: (id: string | null) => void;
|
|
78
|
-
responses: Map<string, PlanResponse>;
|
|
79
|
-
setResponse: (id: string, response: PlanResponse) => void;
|
|
80
|
-
feedbackEntries: Map<string, FeedbackEntry>;
|
|
81
|
-
setFeedbackEntry: (id: string, entry: FeedbackEntry) => void;
|
|
82
|
-
removeFeedbackEntry: (id: string) => void;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
export const AnnotationCtx = createContext<AnnotationContextValue>(null!);
|
|
86
|
-
|
|
87
|
-
export function useAnnotations(): AnnotationContextValue {
|
|
88
|
-
return useContext(AnnotationCtx);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
export function useFeedback(
|
|
92
|
-
id: string,
|
|
93
|
-
markdown: string,
|
|
94
|
-
options?: { label?: string; required?: boolean },
|
|
95
|
-
): void {
|
|
96
|
-
const { setFeedbackEntry, removeFeedbackEntry } = useAnnotations();
|
|
97
|
-
useEffect(() => {
|
|
98
|
-
setFeedbackEntry(id, {
|
|
99
|
-
id,
|
|
100
|
-
markdown,
|
|
101
|
-
label: options?.label,
|
|
102
|
-
required: options?.required,
|
|
103
|
-
});
|
|
104
|
-
return () => removeFeedbackEntry(id);
|
|
105
|
-
}, [id, markdown, options?.label, options?.required]);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
export { SessionContext } from "./SessionContext";
|
|
109
|
-
|
|
110
|
-
// CanvasFile context — tells components which canvas file they belong to
|
|
111
|
-
export const CanvasFileCtx = createContext<string>("");
|
|
112
|
-
export function useCanvasFile(): string {
|
|
113
|
-
return useContext(CanvasFileCtx);
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// ActiveView navigation — allows components (e.g. FilePreview) to open files
|
|
117
|
-
export type ActiveView = { type: "overview" } | { type: "canvas"; filename: string } | { type: "file"; path: string };
|
|
118
|
-
|
|
119
|
-
export const ActiveViewCtx = createContext<{
|
|
120
|
-
setActiveView: (v: ActiveView) => void;
|
|
121
|
-
}>({
|
|
122
|
-
setActiveView: () => {},
|
|
123
|
-
});
|
package/daemon/client/theme.css
DELETED
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
@theme {
|
|
2
|
-
/* Colors — initial values, overridden by [data-theme] in styles.css */
|
|
3
|
-
--color-bg-base: #1a1915;
|
|
4
|
-
--color-bg-surface: #1f1e18;
|
|
5
|
-
--color-bg-elevated: #25241d;
|
|
6
|
-
--color-bg-code: #1c1b16;
|
|
7
|
-
--color-bg-input: rgba(255, 248, 220, 0.03);
|
|
8
|
-
--color-bg-elevated-half: rgba(37, 36, 29, 0.5);
|
|
9
|
-
|
|
10
|
-
--color-text-primary: #e8e4df;
|
|
11
|
-
--color-text-secondary: #a09a92;
|
|
12
|
-
--color-text-tertiary: #847d75;
|
|
13
|
-
--color-text-code: #c4beb7;
|
|
14
|
-
--color-text-disabled: #3a3530;
|
|
15
|
-
--color-text-inverse: #1a1a1a;
|
|
16
|
-
|
|
17
|
-
--color-border-subtle: rgba(255, 248, 240, 0.06);
|
|
18
|
-
--color-border-medium: rgba(255, 248, 240, 0.08);
|
|
19
|
-
--color-border-hover: rgba(255, 248, 240, 0.12);
|
|
20
|
-
--color-border-strong: rgba(255, 248, 240, 0.15);
|
|
21
|
-
|
|
22
|
-
--color-accent-green: #4a9e6d;
|
|
23
|
-
--color-accent-green-muted: rgba(74, 158, 109, 0.12);
|
|
24
|
-
--color-accent-amber: #c49a3a;
|
|
25
|
-
--color-accent-amber-muted: rgba(196, 154, 58, 0.12);
|
|
26
|
-
--color-accent-red: #c45a5a;
|
|
27
|
-
--color-accent-red-muted: rgba(196, 90, 90, 0.12);
|
|
28
|
-
--color-accent-blue: #5a8ec4;
|
|
29
|
-
--color-accent-blue-muted: rgba(90, 142, 196, 0.10);
|
|
30
|
-
|
|
31
|
-
--color-highlight-bg: rgba(255, 220, 100, 0.15);
|
|
32
|
-
--color-highlight-border: rgba(255, 220, 100, 0.35);
|
|
33
|
-
--color-highlight-annotation: rgba(255, 220, 100, 0.18);
|
|
34
|
-
--color-highlight-active: rgba(255, 220, 100, 0.35);
|
|
35
|
-
--color-highlight-selected: rgba(255, 220, 100, 0.10);
|
|
36
|
-
|
|
37
|
-
--color-badge-bg: rgba(196, 154, 58, 0.20);
|
|
38
|
-
--color-btn-primary: #e8e4df;
|
|
39
|
-
--color-btn-primary-text: #1a1a1a;
|
|
40
|
-
|
|
41
|
-
/* Fonts */
|
|
42
|
-
--font-heading: 'Instrument Serif', Georgia, 'Times New Roman', serif;
|
|
43
|
-
--font-body: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
|
|
44
|
-
--font-mono: 'JetBrains Mono', 'Fira Code', Consolas, monospace;
|
|
45
|
-
|
|
46
|
-
/* Font sizes */
|
|
47
|
-
--text-section: 1.75rem;
|
|
48
|
-
--text-section--line-height: 1.25;
|
|
49
|
-
--text-section--letter-spacing: -0.01em;
|
|
50
|
-
--text-task-label: 0.9375rem;
|
|
51
|
-
--text-task-label--line-height: 1.4;
|
|
52
|
-
--text-body: 0.875rem;
|
|
53
|
-
--text-body--line-height: 1.65;
|
|
54
|
-
--text-code: 0.8125rem;
|
|
55
|
-
--text-code--line-height: 1.6;
|
|
56
|
-
--text-meta: 0.75rem;
|
|
57
|
-
--text-meta--line-height: 1.4;
|
|
58
|
-
--text-meta--letter-spacing: 0.02em;
|
|
59
|
-
--text-tiny: 0.6875rem;
|
|
60
|
-
--text-tiny--line-height: 1.4;
|
|
61
|
-
|
|
62
|
-
/* Shadows */
|
|
63
|
-
--shadow-sm: 0 1px 2px var(--color-shadow);
|
|
64
|
-
--shadow-md: 0 4px 12px var(--color-shadow);
|
|
65
|
-
--shadow-lg: 0 8px 24px var(--color-shadow);
|
|
66
|
-
}
|
|
@@ -1,188 +0,0 @@
|
|
|
1
|
-
import React, { useEffect, useState, useRef, useCallback } from "react";
|
|
2
|
-
import type { AnnotationContext, Annotation } from "./AnnotationProvider";
|
|
3
|
-
import { useAnnotations } from "./AnnotationProvider";
|
|
4
|
-
import { wrapRangeWithMark, updateAllMarkStates, renameMarkId, unwrapMarks, restoreMarks } from "./highlightRange";
|
|
5
|
-
import { AnnotationCreatePopover, AnnotationEditPopover } from "./Popover";
|
|
6
|
-
import { generateAnnotationId } from "./utils";
|
|
7
|
-
|
|
8
|
-
interface UseTextAnnotationOptions {
|
|
9
|
-
/** Ref to the container element where text can be selected and marks are placed */
|
|
10
|
-
containerRef: React.RefObject<HTMLElement | null>;
|
|
11
|
-
/** Trigger for mark restoration — when this value changes, marks are restored */
|
|
12
|
-
restoreKey: unknown;
|
|
13
|
-
/** Subset of annotations to restore in this container */
|
|
14
|
-
restoreAnnotations: Annotation[];
|
|
15
|
-
/** Build AnnotationContext from a selection Range */
|
|
16
|
-
extractContext: (range: Range) => AnnotationContext | undefined;
|
|
17
|
-
/** Optional filePath to associate with created annotations */
|
|
18
|
-
filePath?: string;
|
|
19
|
-
/** Optional canvasFile to associate with created annotations */
|
|
20
|
-
canvasFile?: string;
|
|
21
|
-
/** Scroll container for popover positioning */
|
|
22
|
-
scrollContainer?: HTMLElement | null;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export function useTextAnnotation(options: UseTextAnnotationOptions) {
|
|
26
|
-
const { containerRef, restoreKey, restoreAnnotations, extractContext, filePath, canvasFile, scrollContainer } = options;
|
|
27
|
-
const { annotations: allAnnotations, addAnnotationWithId, removeAnnotation, updateAnnotation, addAnnotationImage, removeAnnotationImage, activeAnnotationId, setActiveAnnotationId } = useAnnotations();
|
|
28
|
-
|
|
29
|
-
const [editPopover, setEditPopover] = useState<{ anchorEl: HTMLElement; annId: string } | null>(null);
|
|
30
|
-
const [createPopover, setCreatePopover] = useState<{ anchorEl: HTMLElement; tempId: string; snippet: string; ctx?: AnnotationContext } | null>(null);
|
|
31
|
-
|
|
32
|
-
// Refs for stable handlers (registered with empty or minimal deps)
|
|
33
|
-
const extractContextRef = useRef(extractContext);
|
|
34
|
-
extractContextRef.current = extractContext;
|
|
35
|
-
const filePathRef = useRef(filePath);
|
|
36
|
-
filePathRef.current = filePath;
|
|
37
|
-
const canvasFileRef = useRef(canvasFile);
|
|
38
|
-
canvasFileRef.current = canvasFile;
|
|
39
|
-
const addAnnotationRef = useRef(addAnnotationWithId);
|
|
40
|
-
addAnnotationRef.current = addAnnotationWithId;
|
|
41
|
-
const activeAnnotationIdRef = useRef(activeAnnotationId);
|
|
42
|
-
activeAnnotationIdRef.current = activeAnnotationId;
|
|
43
|
-
const setActiveAnnotationIdRef = useRef(setActiveAnnotationId);
|
|
44
|
-
setActiveAnnotationIdRef.current = setActiveAnnotationId;
|
|
45
|
-
|
|
46
|
-
// Mark restoration after content renders or annotations change
|
|
47
|
-
const restoreAnnotationsLen = restoreAnnotations.length;
|
|
48
|
-
useEffect(() => {
|
|
49
|
-
if (!containerRef.current) return;
|
|
50
|
-
const timer = setTimeout(() => {
|
|
51
|
-
if (containerRef.current) {
|
|
52
|
-
restoreMarks(containerRef.current, restoreAnnotations);
|
|
53
|
-
}
|
|
54
|
-
}, 50);
|
|
55
|
-
return () => clearTimeout(timer);
|
|
56
|
-
}, [restoreKey, restoreAnnotationsLen]);
|
|
57
|
-
|
|
58
|
-
// Mark active state tracking
|
|
59
|
-
const prevActiveRef = useRef<string | null>(null);
|
|
60
|
-
useEffect(() => {
|
|
61
|
-
updateAllMarkStates(activeAnnotationId, prevActiveRef.current);
|
|
62
|
-
prevActiveRef.current = activeAnnotationId;
|
|
63
|
-
}, [activeAnnotationId]);
|
|
64
|
-
|
|
65
|
-
// Mark click/hover/out handlers on the container (uses refs to avoid re-registration on every hover)
|
|
66
|
-
useEffect(() => {
|
|
67
|
-
const container = containerRef.current;
|
|
68
|
-
if (!container) return;
|
|
69
|
-
|
|
70
|
-
const handleClick = (e: MouseEvent) => {
|
|
71
|
-
const mark = (e.target as HTMLElement).closest("[data-annotation-id]") as HTMLElement | null;
|
|
72
|
-
if (!mark) return;
|
|
73
|
-
e.stopPropagation();
|
|
74
|
-
const annId = mark.getAttribute("data-annotation-id")!;
|
|
75
|
-
if (annId === activeAnnotationIdRef.current) {
|
|
76
|
-
setEditPopover({ anchorEl: mark, annId });
|
|
77
|
-
} else {
|
|
78
|
-
setActiveAnnotationIdRef.current(annId);
|
|
79
|
-
}
|
|
80
|
-
};
|
|
81
|
-
|
|
82
|
-
const handleMouseOver = (e: MouseEvent) => {
|
|
83
|
-
const mark = (e.target as HTMLElement).closest("[data-annotation-id]") as HTMLElement | null;
|
|
84
|
-
if (mark) setActiveAnnotationIdRef.current(mark.getAttribute("data-annotation-id")!);
|
|
85
|
-
};
|
|
86
|
-
|
|
87
|
-
const handleMouseOut = (e: MouseEvent) => {
|
|
88
|
-
const mark = (e.target as HTMLElement).closest("[data-annotation-id]");
|
|
89
|
-
const related = (e.relatedTarget as HTMLElement | null)?.closest?.("[data-annotation-id]");
|
|
90
|
-
if (mark && !related && !document.getElementById("ann-inline-popover")) setActiveAnnotationIdRef.current(null);
|
|
91
|
-
};
|
|
92
|
-
|
|
93
|
-
container.addEventListener("click", handleClick);
|
|
94
|
-
container.addEventListener("mouseover", handleMouseOver);
|
|
95
|
-
container.addEventListener("mouseout", handleMouseOut);
|
|
96
|
-
return () => {
|
|
97
|
-
container.removeEventListener("click", handleClick);
|
|
98
|
-
container.removeEventListener("mouseover", handleMouseOver);
|
|
99
|
-
container.removeEventListener("mouseout", handleMouseOut);
|
|
100
|
-
};
|
|
101
|
-
}, [restoreKey]);
|
|
102
|
-
|
|
103
|
-
// Document-level mouseup for text selection → mark → popover
|
|
104
|
-
useEffect(() => {
|
|
105
|
-
const handler = () => handleMouseUp();
|
|
106
|
-
document.addEventListener("mouseup", handler);
|
|
107
|
-
return () => document.removeEventListener("mouseup", handler);
|
|
108
|
-
}, []);
|
|
109
|
-
|
|
110
|
-
const handleMouseUp = useCallback(() => {
|
|
111
|
-
const sel = window.getSelection();
|
|
112
|
-
if (!sel || sel.isCollapsed || !sel.toString().trim()) return;
|
|
113
|
-
if (!containerRef.current) return;
|
|
114
|
-
const range = sel.getRangeAt(0);
|
|
115
|
-
if (!containerRef.current.contains(range.startContainer)) return;
|
|
116
|
-
if ((range.startContainer.parentElement as HTMLElement)?.closest?.("[data-annotation-id]")) return;
|
|
117
|
-
const snippet = sel.toString().trim();
|
|
118
|
-
if (snippet.length < 2) return;
|
|
119
|
-
|
|
120
|
-
// Extract context before wrapping disturbs the DOM
|
|
121
|
-
const ctx = extractContextRef.current(range);
|
|
122
|
-
const savedRange = range.cloneRange();
|
|
123
|
-
const tempId = `__pending_${Date.now()}`;
|
|
124
|
-
try { wrapRangeWithMark(savedRange, tempId); } catch {}
|
|
125
|
-
window.getSelection()?.removeAllRanges();
|
|
126
|
-
|
|
127
|
-
const marks = document.querySelectorAll(`[data-annotation-id="${tempId}"]`);
|
|
128
|
-
const lastMark = marks[marks.length - 1] as HTMLElement | undefined;
|
|
129
|
-
if (!lastMark) return;
|
|
130
|
-
|
|
131
|
-
setCreatePopover({ anchorEl: lastMark, tempId, snippet, ctx });
|
|
132
|
-
}, []);
|
|
133
|
-
|
|
134
|
-
const openCreatePopover = useCallback((anchorEl: HTMLElement, tempId: string, snippet: string, ctx?: AnnotationContext) => {
|
|
135
|
-
setCreatePopover({ anchorEl, tempId, snippet, ctx });
|
|
136
|
-
}, []);
|
|
137
|
-
|
|
138
|
-
const popovers = (
|
|
139
|
-
<>
|
|
140
|
-
{editPopover && (() => {
|
|
141
|
-
const ann = allAnnotations.find((a) => a.id === editPopover.annId);
|
|
142
|
-
if (!ann) return null;
|
|
143
|
-
return (
|
|
144
|
-
<AnnotationEditPopover
|
|
145
|
-
anchorEl={editPopover.anchorEl}
|
|
146
|
-
scrollContainer={scrollContainer}
|
|
147
|
-
initialNote={ann.note}
|
|
148
|
-
initialImages={ann.images}
|
|
149
|
-
onUpdate={(note, images) => {
|
|
150
|
-
updateAnnotation(editPopover.annId, note);
|
|
151
|
-
// Sync images
|
|
152
|
-
const current = ann.images || [];
|
|
153
|
-
for (const img of images) {
|
|
154
|
-
if (!current.includes(img)) addAnnotationImage(editPopover.annId, img);
|
|
155
|
-
}
|
|
156
|
-
for (const img of current) {
|
|
157
|
-
if (!images.includes(img)) removeAnnotationImage(editPopover.annId, img);
|
|
158
|
-
}
|
|
159
|
-
}}
|
|
160
|
-
onDelete={() => { removeAnnotation(editPopover.annId); setActiveAnnotationId(null); }}
|
|
161
|
-
onClose={() => setEditPopover(null)}
|
|
162
|
-
/>
|
|
163
|
-
);
|
|
164
|
-
})()}
|
|
165
|
-
|
|
166
|
-
{createPopover && (
|
|
167
|
-
<AnnotationCreatePopover
|
|
168
|
-
anchorEl={createPopover.anchorEl}
|
|
169
|
-
scrollContainer={scrollContainer}
|
|
170
|
-
snippet={createPopover.snippet}
|
|
171
|
-
truncateAt={80}
|
|
172
|
-
onAdd={(note, images) => {
|
|
173
|
-
const id = generateAnnotationId();
|
|
174
|
-
renameMarkId(createPopover.tempId, id);
|
|
175
|
-
addAnnotationRef.current(id, createPopover.snippet, note, filePathRef.current, createPopover.ctx, images, canvasFileRef.current);
|
|
176
|
-
setCreatePopover(null);
|
|
177
|
-
}}
|
|
178
|
-
onCancel={() => {
|
|
179
|
-
unwrapMarks(createPopover.tempId);
|
|
180
|
-
setCreatePopover(null);
|
|
181
|
-
}}
|
|
182
|
-
/>
|
|
183
|
-
)}
|
|
184
|
-
</>
|
|
185
|
-
);
|
|
186
|
-
|
|
187
|
-
return { popovers, openCreatePopover, isPopoverOpen: !!(editPopover || createPopover) };
|
|
188
|
-
}
|
package/daemon/client/utils.ts
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
/** Sentinel filePath for annotations on the agent response banner */
|
|
2
|
-
export const RESPONSE_ANNOTATION_PATH = "__agent-response__";
|
|
3
|
-
|
|
4
|
-
/** Generate a unique annotation ID */
|
|
5
|
-
export function generateAnnotationId(): string {
|
|
6
|
-
return `ann-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
/** Auto-resize a textarea to fit its content */
|
|
10
|
-
export function autoResizeTextarea(el: HTMLTextAreaElement, minHeight?: number) {
|
|
11
|
-
el.style.height = "auto";
|
|
12
|
-
el.style.height = (minHeight ? Math.max(minHeight, el.scrollHeight) : el.scrollHeight) + "px";
|
|
13
|
-
}
|