@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.
Files changed (70) hide show
  1. package/README.md +33 -67
  2. package/index.ts +20 -83
  3. package/package.json +69 -72
  4. package/src/client/AnnotationProvider.tsx +218 -255
  5. package/src/client/AnnotationSidebar.tsx +393 -338
  6. package/src/client/FileIcon.tsx +105 -66
  7. package/src/client/PlanRenderer.tsx +607 -551
  8. package/src/client/RenderErrorContext.ts +7 -7
  9. package/src/client/ResponsePreview.tsx +192 -173
  10. package/src/client/annotationDom.ts +43 -97
  11. package/src/client/annotationDraft.ts +106 -96
  12. package/src/client/appContext.tsx +40 -40
  13. package/src/client/components/Callout.tsx +20 -20
  14. package/src/client/components/Checklist.tsx +27 -29
  15. package/src/client/components/Choice.tsx +178 -159
  16. package/src/client/components/CodeBlock.tsx +27 -26
  17. package/src/client/components/Diff.tsx +16 -17
  18. package/src/client/components/FilePreview.tsx +102 -74
  19. package/src/client/components/Image.tsx +41 -39
  20. package/src/client/components/Markdown.tsx +33 -32
  21. package/src/client/components/Mermaid.tsx +593 -542
  22. package/src/client/components/Note.tsx +8 -8
  23. package/src/client/components/Priority.tsx +10 -12
  24. package/src/client/components/ResponseNote.tsx +16 -15
  25. package/src/client/components/Section.tsx +28 -26
  26. package/src/client/components/Table.tsx +33 -30
  27. package/src/client/components/Task.tsx +58 -60
  28. package/src/client/components/UserInput.tsx +169 -152
  29. package/src/client/components/index.ts +16 -16
  30. package/src/client/components/renderMarkdownChildren.tsx +86 -68
  31. package/src/client/diffLines.ts +51 -39
  32. package/src/client/generateMarkdown.ts +197 -191
  33. package/src/client/hostContext.tsx +39 -34
  34. package/src/client/index.ts +44 -125
  35. package/src/client/pageGlobals.ts +25 -0
  36. package/src/client/persistenceSchemas.ts +41 -0
  37. package/src/client/preact-all.ts +4 -4
  38. package/src/client/runtime.ts +58 -111
  39. package/src/client/styles.css +1 -74
  40. package/src/engine/compiler.ts +411 -323
  41. package/src/engine/file-admission.ts +25 -0
  42. package/src/engine/paths.ts +44 -44
  43. package/src/engine/session.ts +515 -626
  44. package/src/engine/sessionSchemas.ts +71 -0
  45. package/src/engine/watcher.ts +70 -60
  46. package/src/engine/websocket.ts +134 -132
  47. package/annotate.ts +0 -94
  48. package/daemon.ts +0 -46
  49. package/src/client/AnnotationDraftFooter.tsx +0 -76
  50. package/src/client/AnnotationEditor.tsx +0 -320
  51. package/src/client/AnnotationList.tsx +0 -190
  52. package/src/client/Popover.tsx +0 -361
  53. package/src/client/SessionContext.tsx +0 -3
  54. package/src/client/annotationContext.ts +0 -130
  55. package/src/client/annotationMarkdown.ts +0 -102
  56. package/src/client/annotationTarget.ts +0 -73
  57. package/src/client/blockTarget.ts +0 -127
  58. package/src/client/highlightRange.ts +0 -262
  59. package/src/client/popoverPosition.ts +0 -46
  60. package/src/client/regionTarget.ts +0 -180
  61. package/src/client/testing/dom.ts +0 -74
  62. package/src/client/textTarget.ts +0 -31
  63. package/src/client/useRegionAnnotation.tsx +0 -188
  64. package/src/client/useTextAnnotation.tsx +0 -194
  65. package/src/client/utils.ts +0 -33
  66. package/src/engine/channel.ts +0 -278
  67. package/src/engine/http-utils.ts +0 -14
  68. package/src/engine/lifecycle.ts +0 -302
  69. package/src/engine/router.ts +0 -16
  70. package/src/engine/waiter.ts +0 -188
@@ -1,31 +0,0 @@
1
- import type { AnnotationTarget } from "./annotationTarget";
2
- import { restoreMarks } from "./highlightRange";
3
-
4
- /** A run of text, identified by the text itself. */
5
- export interface TextLocator {
6
- text: string;
7
- }
8
-
9
- /**
10
- * Selected text, wrapped in inline `<mark>` elements.
11
- *
12
- * The fallback target: any snippet at all is text, so this one claims
13
- * everything the others turned down and must stay last in the registry.
14
- */
15
- export const textTarget: AnnotationTarget<TextLocator> = {
16
- kind: "text",
17
-
18
- parse: (snippet) => (snippet ? { text: snippet } : null),
19
-
20
- format: (locator) => locator.text,
21
-
22
- // A run of text has no element of its own until a mark wraps it, and the mark
23
- // is found by annotation id, not by the text. Guessing the element back from
24
- // page text would resolve an annotation onto whatever happens to read the
25
- // same today, so a missing mark stays a miss.
26
- find: () => null,
27
-
28
- restore: (_locator, root, ann) => {
29
- restoreMarks(root, [ann]);
30
- },
31
- };
@@ -1,188 +0,0 @@
1
- import React, { useEffect, useRef, useState } from "react";
2
- import { useAnnotations } from "./AnnotationProvider";
3
- import { AnnotationCreatePopover } from "./Popover";
4
- import {
5
- applyRegionStyle,
6
- createRegionOverlay,
7
- describeRegion,
8
- isDrawableRegion,
9
- regionBetween,
10
- regionPointIn,
11
- regionTarget,
12
- REGION_HOST_ATTR,
13
- type RegionPoint,
14
- type RegionShape,
15
- } from "./regionTarget";
16
- import { generateAnnotationId } from "./utils";
17
-
18
- interface UseRegionAnnotationOptions {
19
- /** Container holding the image blocks a region can be drawn on. */
20
- containerRef: React.RefObject<HTMLElement | null>;
21
- /** Re-registers the drawing listener when the canvas remounts. */
22
- restoreKey: unknown;
23
- /** Shape a new region takes. */
24
- shape?: RegionShape;
25
- /** Canvas file the created annotation belongs to. */
26
- canvasFile?: string;
27
- /** Scroll container for popover positioning. */
28
- scrollContainer?: HTMLElement | null;
29
- }
30
-
31
- /** One region being drawn out, held outside React: the box follows the pointer
32
- * at pointer rate, which is no reason to re-render the canvas. */
33
- interface RegionDrag {
34
- pointerId: number;
35
- host: HTMLElement;
36
- src: string;
37
- start: RegionPoint;
38
- shape: RegionShape;
39
- overlay: HTMLElement;
40
- id: string;
41
- }
42
-
43
- /** A region drawn but not yet given a note. */
44
- interface PendingRegion {
45
- overlay: HTMLElement;
46
- id: string;
47
- snippet: string;
48
- label: string;
49
- }
50
-
51
- /**
52
- * Drag on an image block to annotate one part of it.
53
- *
54
- * An image carries no text, so a plain drag over one is unambiguous and takes
55
- * no modifier: the selection this would compete with cannot start there. It
56
- * still has to stay off the two things that already live on an image block —
57
- * the whole-image comment button and any region already drawn.
58
- */
59
- export function useRegionAnnotation(options: UseRegionAnnotationOptions) {
60
- const { containerRef, restoreKey, shape = "rect", canvasFile, scrollContainer } = options;
61
- const { addAnnotationWithId } = useAnnotations();
62
-
63
- const [pending, setPending] = useState<PendingRegion | null>(null);
64
- const [isDrawing, setIsDrawing] = useState(false);
65
-
66
- const addAnnotationRef = useRef(addAnnotationWithId);
67
- addAnnotationRef.current = addAnnotationWithId;
68
- const canvasFileRef = useRef(canvasFile);
69
- canvasFileRef.current = canvasFile;
70
- const shapeRef = useRef(shape);
71
- shapeRef.current = shape;
72
-
73
- useEffect(() => {
74
- const container = containerRef.current;
75
- if (!container) return;
76
-
77
- let drag: RegionDrag | null = null;
78
-
79
- /** Where the pointer is, on the region grid of `host`. */
80
- function pointIn(host: HTMLElement, e: PointerEvent): RegionPoint | null {
81
- const rect = host.getBoundingClientRect();
82
- return regionPointIn(rect, e.clientX - rect.left, e.clientY - rect.top);
83
- }
84
-
85
- function endDrag() {
86
- if (!drag) return;
87
- const { host, pointerId } = drag;
88
- if (host.hasPointerCapture(pointerId)) host.releasePointerCapture(pointerId);
89
- host.removeEventListener("pointermove", onMove);
90
- host.removeEventListener("pointerup", onUp);
91
- host.removeEventListener("pointercancel", onCancel);
92
- drag = null;
93
- setIsDrawing(false);
94
- }
95
-
96
- function onDown(e: PointerEvent) {
97
- if (drag || e.button !== 0) return;
98
- const target = e.target;
99
- if (!(target instanceof Element)) return;
100
- // The comment button mints the whole-image annotation, and a region
101
- // already drawn is its own click target; neither starts a new one.
102
- if (target.closest("[data-block-comment-btn]")) return;
103
- if (target.closest("[data-annotation-id]")) return;
104
-
105
- const host = target.closest(`[${REGION_HOST_ATTR}]`);
106
- if (!(host instanceof HTMLElement) || !container?.contains(host)) return;
107
- const src = host.closest("[data-md='image']")?.getAttribute("data-md-src");
108
- if (!src) return;
109
- const start = pointIn(host, e);
110
- if (!start) return;
111
-
112
- // Without this the browser starts its own image drag instead.
113
- e.preventDefault();
114
- host.setPointerCapture(e.pointerId);
115
-
116
- const id = generateAnnotationId();
117
- const overlay = createRegionOverlay({ ...start, w: 0, h: 0 }, shapeRef.current, id);
118
- overlay.classList.add("ann-region-draft");
119
- host.appendChild(overlay);
120
-
121
- drag = { pointerId: e.pointerId, host, src, start, shape: shapeRef.current, overlay, id };
122
- host.addEventListener("pointermove", onMove);
123
- host.addEventListener("pointerup", onUp);
124
- host.addEventListener("pointercancel", onCancel);
125
- setIsDrawing(true);
126
- }
127
-
128
- function onMove(e: PointerEvent) {
129
- if (!drag || drag.pointerId !== e.pointerId) return;
130
- const point = pointIn(drag.host, e);
131
- if (!point) return;
132
- applyRegionStyle(drag.overlay, regionBetween(drag.start, point));
133
- }
134
-
135
- function onUp(e: PointerEvent) {
136
- if (!drag || drag.pointerId !== e.pointerId) return;
137
- const { overlay, id, src, shape: drawnShape } = drag;
138
- const point = pointIn(drag.host, e) ?? drag.start;
139
- const box = regionBetween(drag.start, point);
140
- endDrag();
141
-
142
- if (!isDrawableRegion(box)) {
143
- overlay.remove();
144
- return;
145
- }
146
- applyRegionStyle(overlay, box);
147
- const locator = { src, shape: drawnShape, ...box };
148
- setPending({ overlay, id, snippet: regionTarget.format(locator), label: describeRegion(locator) });
149
- }
150
-
151
- function onCancel(e: PointerEvent) {
152
- if (!drag || drag.pointerId !== e.pointerId) return;
153
- drag.overlay.remove();
154
- endDrag();
155
- }
156
-
157
- container.addEventListener("pointerdown", onDown);
158
- return () => {
159
- container.removeEventListener("pointerdown", onDown);
160
- if (drag) {
161
- drag.overlay.remove();
162
- endDrag();
163
- }
164
- };
165
- }, [restoreKey]);
166
-
167
- const popovers = pending ? (
168
- <AnnotationCreatePopover
169
- anchorEl={pending.overlay}
170
- scrollContainer={scrollContainer}
171
- snippet={pending.label}
172
- truncateAt={80}
173
- onAdd={(note, images) => {
174
- // The overlay already carries the annotation's real id, so committing
175
- // is only dropping the draft look — no redraw, no flicker.
176
- pending.overlay.classList.remove("ann-region-draft");
177
- addAnnotationRef.current(pending.id, pending.snippet, note, undefined, undefined, images, canvasFileRef.current);
178
- setPending(null);
179
- }}
180
- onCancel={() => {
181
- pending.overlay.remove();
182
- setPending(null);
183
- }}
184
- />
185
- ) : null;
186
-
187
- return { popovers, isDrawing, isPopoverOpen: !!pending };
188
- }
@@ -1,194 +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, rangeIndexText } from "./highlightRange";
5
- import { restoreAnnotationTargets } from "./annotationDom";
6
- import { AnnotationCreatePopover, AnnotationEditPopover } from "./Popover";
7
- import { generateAnnotationId } from "./utils";
8
-
9
- interface UseTextAnnotationOptions {
10
- /** Ref to the container element where text can be selected and marks are placed */
11
- containerRef: React.RefObject<HTMLElement | null>;
12
- /** Trigger for mark restoration — when this value changes, marks are restored */
13
- restoreKey: unknown;
14
- /** Subset of annotations to restore in this container */
15
- restoreAnnotations: Annotation[];
16
- /** Build AnnotationContext from a selection Range */
17
- extractContext: (range: Range) => AnnotationContext | undefined;
18
- /** Optional filePath to associate with created annotations */
19
- filePath?: string;
20
- /** Optional canvasFile to associate with created annotations */
21
- canvasFile?: string;
22
- /** Scroll container for popover positioning */
23
- scrollContainer?: HTMLElement | null;
24
- }
25
-
26
- export function useTextAnnotation(options: UseTextAnnotationOptions) {
27
- const { containerRef, restoreKey, restoreAnnotations, extractContext, filePath, canvasFile, scrollContainer } = options;
28
- const { annotations: allAnnotations, addAnnotationWithId, removeAnnotation, updateAnnotation, addAnnotationImage, removeAnnotationImage, activeAnnotationId, setActiveAnnotationId } = useAnnotations();
29
-
30
- const [editPopover, setEditPopover] = useState<{ anchorEl: HTMLElement; annId: string } | null>(null);
31
- const [createPopover, setCreatePopover] = useState<{ anchorEl: HTMLElement; tempId: string; snippet: string; ctx?: AnnotationContext } | null>(null);
32
-
33
- // Refs for stable handlers (registered with empty or minimal deps)
34
- const extractContextRef = useRef(extractContext);
35
- extractContextRef.current = extractContext;
36
- const filePathRef = useRef(filePath);
37
- filePathRef.current = filePath;
38
- const canvasFileRef = useRef(canvasFile);
39
- canvasFileRef.current = canvasFile;
40
- const addAnnotationRef = useRef(addAnnotationWithId);
41
- addAnnotationRef.current = addAnnotationWithId;
42
- const activeAnnotationIdRef = useRef(activeAnnotationId);
43
- activeAnnotationIdRef.current = activeAnnotationId;
44
- const setActiveAnnotationIdRef = useRef(setActiveAnnotationId);
45
- setActiveAnnotationIdRef.current = setActiveAnnotationId;
46
-
47
- // Decoration restoration after content renders or annotations change. Not
48
- // only marks: every locator strategy puts its own decoration back here, so a
49
- // host that mounts this hook gets region overlays restored too.
50
- const restoreAnnotationsLen = restoreAnnotations.length;
51
- useEffect(() => {
52
- if (!containerRef.current) return;
53
- const timer = setTimeout(() => {
54
- if (containerRef.current) {
55
- restoreAnnotationTargets(containerRef.current, restoreAnnotations);
56
- }
57
- }, 50);
58
- return () => clearTimeout(timer);
59
- }, [restoreKey, restoreAnnotationsLen]);
60
-
61
- // Mark active state tracking
62
- const prevActiveRef = useRef<string | null>(null);
63
- useEffect(() => {
64
- updateAllMarkStates(activeAnnotationId, prevActiveRef.current);
65
- prevActiveRef.current = activeAnnotationId;
66
- }, [activeAnnotationId]);
67
-
68
- // Mark click/hover/out handlers on the container (uses refs to avoid re-registration on every hover)
69
- useEffect(() => {
70
- const container = containerRef.current;
71
- if (!container) return;
72
-
73
- const handleClick = (e: MouseEvent) => {
74
- const mark = (e.target as HTMLElement).closest("[data-annotation-id]") as HTMLElement | null;
75
- if (!mark) return;
76
- e.stopPropagation();
77
- const annId = mark.getAttribute("data-annotation-id")!;
78
- if (annId === activeAnnotationIdRef.current) {
79
- setEditPopover({ anchorEl: mark, annId });
80
- } else {
81
- setActiveAnnotationIdRef.current(annId);
82
- }
83
- };
84
-
85
- const handleMouseOver = (e: MouseEvent) => {
86
- const mark = (e.target as HTMLElement).closest("[data-annotation-id]") as HTMLElement | null;
87
- if (mark) setActiveAnnotationIdRef.current(mark.getAttribute("data-annotation-id")!);
88
- };
89
-
90
- const handleMouseOut = (e: MouseEvent) => {
91
- const mark = (e.target as HTMLElement).closest("[data-annotation-id]");
92
- const related = (e.relatedTarget as HTMLElement | null)?.closest?.("[data-annotation-id]");
93
- if (mark && !related && !document.getElementById("ann-inline-popover")) setActiveAnnotationIdRef.current(null);
94
- };
95
-
96
- container.addEventListener("click", handleClick);
97
- container.addEventListener("mouseover", handleMouseOver);
98
- container.addEventListener("mouseout", handleMouseOut);
99
- return () => {
100
- container.removeEventListener("click", handleClick);
101
- container.removeEventListener("mouseover", handleMouseOver);
102
- container.removeEventListener("mouseout", handleMouseOut);
103
- };
104
- }, [restoreKey]);
105
-
106
- // Document-level mouseup for text selection → mark → popover
107
- useEffect(() => {
108
- const handler = () => handleMouseUp();
109
- document.addEventListener("mouseup", handler);
110
- return () => document.removeEventListener("mouseup", handler);
111
- }, []);
112
-
113
- const handleMouseUp = useCallback(() => {
114
- const sel = window.getSelection();
115
- if (!sel || sel.isCollapsed || !sel.toString().trim()) return;
116
- if (!containerRef.current) return;
117
- const range = sel.getRangeAt(0);
118
- if (!containerRef.current.contains(range.startContainer)) return;
119
- if ((range.startContainer.parentElement as HTMLElement)?.closest?.("[data-annotation-id]")) return;
120
- // Not sel.toString(): a selection reaching over an existing annotation
121
- // carries text the index does not hold, and a snippet naming that text can
122
- // never be restored.
123
- const snippet = rangeIndexText(range).trim();
124
- if (snippet.length < 2) return;
125
-
126
- // Extract context before wrapping disturbs the DOM
127
- const ctx = extractContextRef.current(range);
128
- const savedRange = range.cloneRange();
129
- const tempId = `__pending_${Date.now()}`;
130
- try { wrapRangeWithMark(savedRange, tempId); } catch {}
131
- window.getSelection()?.removeAllRanges();
132
-
133
- const marks = document.querySelectorAll(`[data-annotation-id="${tempId}"]`);
134
- const lastMark = marks[marks.length - 1] as HTMLElement | undefined;
135
- if (!lastMark) return;
136
-
137
- setCreatePopover({ anchorEl: lastMark, tempId, snippet, ctx });
138
- }, []);
139
-
140
- const openCreatePopover = useCallback((anchorEl: HTMLElement, tempId: string, snippet: string, ctx?: AnnotationContext) => {
141
- setCreatePopover({ anchorEl, tempId, snippet, ctx });
142
- }, []);
143
-
144
- const popovers = (
145
- <>
146
- {editPopover && (() => {
147
- const ann = allAnnotations.find((a) => a.id === editPopover.annId);
148
- if (!ann) return null;
149
- return (
150
- <AnnotationEditPopover
151
- anchorEl={editPopover.anchorEl}
152
- scrollContainer={scrollContainer}
153
- initialNote={ann.note}
154
- initialImages={ann.images}
155
- onUpdate={(note, images) => {
156
- updateAnnotation(editPopover.annId, note);
157
- // Sync images
158
- const current = ann.images || [];
159
- for (const img of images) {
160
- if (!current.includes(img)) addAnnotationImage(editPopover.annId, img);
161
- }
162
- for (const img of current) {
163
- if (!images.includes(img)) removeAnnotationImage(editPopover.annId, img);
164
- }
165
- }}
166
- onDelete={() => { removeAnnotation(editPopover.annId); setActiveAnnotationId(null); }}
167
- onClose={() => setEditPopover(null)}
168
- />
169
- );
170
- })()}
171
-
172
- {createPopover && (
173
- <AnnotationCreatePopover
174
- anchorEl={createPopover.anchorEl}
175
- scrollContainer={scrollContainer}
176
- snippet={createPopover.snippet}
177
- truncateAt={80}
178
- onAdd={(note, images) => {
179
- const id = generateAnnotationId();
180
- renameMarkId(createPopover.tempId, id);
181
- addAnnotationRef.current(id, createPopover.snippet, note, filePathRef.current, createPopover.ctx, images, canvasFileRef.current);
182
- setCreatePopover(null);
183
- }}
184
- onCancel={() => {
185
- unwrapMarks(createPopover.tempId);
186
- setCreatePopover(null);
187
- }}
188
- />
189
- )}
190
- </>
191
- );
192
-
193
- return { popovers, openCreatePopover, isPopoverOpen: !!(editPopover || createPopover) };
194
- }
@@ -1,33 +0,0 @@
1
- /** Sentinel filePath for annotations on the agent response banner */
2
- export const RESPONSE_ANNOTATION_PATH = "__agent-response__";
3
-
4
- /**
5
- * The file this annotation lives in, or undefined if it lives in no file.
6
- * Response annotations ride in the same field under a sentinel, so a bare
7
- * `if (ann.filePath)` opens a file tab for a path that is not one.
8
- */
9
- export function fileAnnotationPath(ann: { filePath?: string }): string | undefined {
10
- return ann.filePath && ann.filePath !== RESPONSE_ANNOTATION_PATH ? ann.filePath : undefined;
11
- }
12
-
13
- /**
14
- * Whether the attach-image affordances are drawn at all.
15
- *
16
- * The button, the paste handler and the drop zone all end in one POST to the
17
- * host's upload endpoint, so a host without one gets none of the three instead
18
- * of controls that fail silently.
19
- */
20
- export function hostAcceptsUploads(host: { uploadUrl: (() => string) | null }): boolean {
21
- return host.uploadUrl !== null;
22
- }
23
-
24
- /** Generate a unique annotation ID */
25
- export function generateAnnotationId(): string {
26
- return `ann-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
27
- }
28
-
29
- /** Auto-resize a textarea to fit its content */
30
- export function autoResizeTextarea(el: HTMLTextAreaElement, minHeight?: number) {
31
- el.style.height = "auto";
32
- el.style.height = (minHeight ? Math.max(minHeight, el.scrollHeight) : el.scrollHeight) + "px";
33
- }