@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,130 +1,140 @@
1
+ import { z } from 'zod'
2
+
3
+ // Carry old and host-extended drafts intact; hydration owns validation of individual entries.
4
+ const carriedDraftSchema = z.object({}).passthrough()
5
+ const draftContentSchema = z.object({
6
+ annotations: z.array(z.unknown()).catch([]),
7
+ generalNote: z.string().catch(''),
8
+ responses: z.array(z.unknown()).catch([]),
9
+ })
10
+ const responseContentSchema = z.tuple([
11
+ z.unknown(),
12
+ z.object({ note: z.unknown(), value: z.unknown() }),
13
+ ])
14
+
1
15
  interface RevisionState {
2
- revision: number;
3
- hasFeedback: boolean;
16
+ revision: number
17
+ hasFeedback: boolean
4
18
  }
5
19
 
6
20
  interface AnnotationStorage {
7
- getItem(key: string): string | null;
8
- setItem(key: string, value: string): void;
9
- removeItem(key: string): void;
21
+ getItem(key: string): string | null
22
+ setItem(key: string, value: string): void
23
+ removeItem(key: string): void
10
24
  }
11
25
 
12
26
  interface RemovableAnnotationStorage {
13
- removeItem(key: string): void;
27
+ removeItem(key: string): void
14
28
  }
15
29
 
16
- export type AnnotationDraftPhase = "current" | "next";
30
+ export type AnnotationDraftPhase = 'current' | 'next'
17
31
 
18
32
  export function annotationDraftKey(
19
- sessionId: string,
20
- revision: number,
21
- phase: AnnotationDraftPhase = "current",
33
+ sessionId: string,
34
+ revision: number,
35
+ phase: AnnotationDraftPhase = 'current',
22
36
  ): string {
23
- const base = `canvas:${sessionId}:rev:${revision}`;
24
- return phase === "next" ? `${base}:next-draft` : base;
37
+ const base = `canvas:${sessionId}:rev:${revision}`
38
+ return phase === 'next' ? `${base}:next-draft` : base
25
39
  }
26
40
 
27
41
  function handledKey(sessionId: string, revision: number): string {
28
- return `${annotationDraftKey(sessionId, revision)}:draft-handled`;
42
+ return `${annotationDraftKey(sessionId, revision)}:draft-handled`
29
43
  }
30
44
 
31
45
  export function clearPersistedDraft(
32
- storage: RemovableAnnotationStorage,
33
- sessionId: string,
34
- revision: number,
46
+ storage: RemovableAnnotationStorage,
47
+ sessionId: string,
48
+ revision: number,
35
49
  ): void {
36
- try {
37
- storage.removeItem(annotationDraftKey(sessionId, revision));
38
- storage.removeItem(handledKey(sessionId, revision));
39
- } catch {}
50
+ try {
51
+ storage.removeItem(annotationDraftKey(sessionId, revision))
52
+ storage.removeItem(handledKey(sessionId, revision))
53
+ } catch {}
40
54
  }
41
55
 
42
56
  function parseState(raw: string | null): object | null {
43
- if (!raw) return null;
44
- try {
45
- const value: unknown = JSON.parse(raw);
46
- return typeof value === "object" && value !== null && !Array.isArray(value)
47
- ? value
48
- : null;
49
- } catch {
50
- return null;
51
- }
57
+ if (!raw) return null
58
+ try {
59
+ const value: unknown = JSON.parse(raw)
60
+ const result = carriedDraftSchema.safeParse(value)
61
+ return result.success ? result.data : null
62
+ } catch {
63
+ return null
64
+ }
52
65
  }
53
66
 
54
67
  function hasResponseValue(entry: unknown): boolean {
55
- if (!Array.isArray(entry) || entry.length < 2) return false;
56
- const response: unknown = entry[1];
57
- if (typeof response !== "object" || response === null || Array.isArray(response)) return false;
58
-
59
- const note = Reflect.get(response, "note");
60
- if (typeof note === "string" && note.trim().length > 0) return true;
61
-
62
- const value = Reflect.get(response, "value");
63
- if (typeof value === "string") return value.trim().length > 0;
64
- if (Array.isArray(value)) return value.length > 0;
65
- return value !== null && value !== undefined;
68
+ const parsed = responseContentSchema.safeParse(entry)
69
+ if (!parsed.success) return false
70
+ const { note, value } = parsed.data[1]
71
+ if (typeof note === 'string' && note.trim().length > 0) return true
72
+
73
+ if (typeof value === 'string') return value.trim().length > 0
74
+ if (Array.isArray(value)) return value.length > 0
75
+ return value !== null && value !== undefined
66
76
  }
67
77
 
68
78
  function hasDraftContent(state: object | null): boolean {
69
- if (!state) return false;
70
- const annotations = Reflect.get(state, "annotations");
71
- if (Array.isArray(annotations) && annotations.length > 0) return true;
72
-
73
- const generalNote = Reflect.get(state, "generalNote");
74
- if (typeof generalNote === "string" && generalNote.trim().length > 0) return true;
75
-
76
- const responses = Reflect.get(state, "responses");
77
- return Array.isArray(responses) && responses.some(hasResponseValue);
79
+ if (!state) return false
80
+ const { annotations, generalNote, responses } = draftContentSchema.parse(state)
81
+ return annotations.length > 0 || generalNote.trim().length > 0 || responses.some(hasResponseValue)
78
82
  }
79
83
 
80
84
  /** Carry an unsent draft forward without crossing submitted revisions. */
81
85
  export function carryUnsubmittedDraft(
82
- storage: AnnotationStorage,
83
- sessionId: string,
84
- targetRevision: number,
85
- revisions: RevisionState[],
86
+ storage: AnnotationStorage,
87
+ sessionId: string,
88
+ targetRevision: number,
89
+ revisions: RevisionState[],
86
90
  ): number | null {
87
- try {
88
- const targetKey = annotationDraftKey(sessionId, targetRevision);
89
- if (hasDraftContent(parseState(storage.getItem(targetKey)))) {
90
- storage.setItem(handledKey(sessionId, targetRevision), "1");
91
- return null;
92
- }
93
- if (storage.getItem(handledKey(sessionId, targetRevision)) !== null) return null;
94
-
95
- const previous = revisions
96
- .filter((revision) => revision.revision < targetRevision)
97
- .sort((left, right) => right.revision - left.revision);
98
-
99
- for (const revision of previous) {
100
- if (revision.hasFeedback) {
101
- const sourceKey = annotationDraftKey(sessionId, revision.revision, "next");
102
- const source = parseState(storage.getItem(sourceKey));
103
- if (!hasDraftContent(source)) return null;
104
-
105
- storage.setItem(targetKey, JSON.stringify({
106
- ...source,
107
- feedbackEntries: [],
108
- }));
109
- storage.setItem(handledKey(sessionId, targetRevision), "1");
110
- storage.removeItem(sourceKey);
111
- return revision.revision;
112
- }
113
-
114
- const source = parseState(storage.getItem(annotationDraftKey(sessionId, revision.revision)));
115
- if (!hasDraftContent(source)) {
116
- if (storage.getItem(handledKey(sessionId, revision.revision)) !== null) return null;
117
- continue;
118
- }
119
-
120
- storage.setItem(targetKey, JSON.stringify({
121
- ...source,
122
- feedbackEntries: [],
123
- }));
124
- storage.setItem(handledKey(sessionId, targetRevision), "1");
125
- return revision.revision;
126
- }
127
- } catch {}
128
-
129
- return null;
91
+ try {
92
+ const targetKey = annotationDraftKey(sessionId, targetRevision)
93
+ if (hasDraftContent(parseState(storage.getItem(targetKey)))) {
94
+ storage.setItem(handledKey(sessionId, targetRevision), '1')
95
+ return null
96
+ }
97
+ if (storage.getItem(handledKey(sessionId, targetRevision)) !== null) return null
98
+
99
+ const previous = revisions
100
+ .filter((revision) => revision.revision < targetRevision)
101
+ .sort((left, right) => right.revision - left.revision)
102
+
103
+ for (const revision of previous) {
104
+ if (revision.hasFeedback) {
105
+ const sourceKey = annotationDraftKey(sessionId, revision.revision, 'next')
106
+ const source = parseState(storage.getItem(sourceKey))
107
+ if (!hasDraftContent(source)) return null
108
+
109
+ storage.setItem(
110
+ targetKey,
111
+ JSON.stringify({
112
+ ...source,
113
+ feedbackEntries: [],
114
+ }),
115
+ )
116
+ storage.setItem(handledKey(sessionId, targetRevision), '1')
117
+ storage.removeItem(sourceKey)
118
+ return revision.revision
119
+ }
120
+
121
+ const source = parseState(storage.getItem(annotationDraftKey(sessionId, revision.revision)))
122
+ if (!hasDraftContent(source)) {
123
+ if (storage.getItem(handledKey(sessionId, revision.revision)) !== null) return null
124
+ continue
125
+ }
126
+
127
+ storage.setItem(
128
+ targetKey,
129
+ JSON.stringify({
130
+ ...source,
131
+ feedbackEntries: [],
132
+ }),
133
+ )
134
+ storage.setItem(handledKey(sessionId, targetRevision), '1')
135
+ return revision.revision
136
+ }
137
+ } catch {}
138
+
139
+ return null
130
140
  }
@@ -1,5 +1,5 @@
1
- import { createContext } from "react";
2
- import type { ActiveView } from "#canvas/runtime";
1
+ import type { ActiveView } from '#canvas/runtime'
2
+ import { createContext } from 'react'
3
3
 
4
4
  /**
5
5
  * Shell state the annotation surface reads but does not own.
@@ -9,53 +9,53 @@ import type { ActiveView } from "#canvas/runtime";
9
9
  * and how to navigate. The host provides both contexts around the surface.
10
10
  */
11
11
 
12
- export type { ActiveView };
12
+ export type { ActiveView }
13
13
 
14
14
  export interface CanvasFileInfo {
15
- filename: string;
16
- diffStats?: { added: number; removed: number };
15
+ filename: string
16
+ diffStats?: { added: number; removed: number }
17
17
  }
18
18
 
19
19
  export interface RevisionInfo {
20
- revision: number;
21
- label?: string;
22
- canvasFiles: CanvasFileInfo[];
23
- createdAt: string;
24
- hasFeedback: boolean;
25
- feedbackConsumed: boolean;
26
- response?: string;
20
+ revision: number
21
+ label?: string
22
+ canvasFiles: CanvasFileInfo[]
23
+ createdAt: string
24
+ hasFeedback: boolean
25
+ feedbackConsumed: boolean
26
+ response?: string
27
27
  }
28
28
 
29
29
  export const ActiveViewContext = createContext<{
30
- activeView: ActiveView;
31
- setActiveView: (v: ActiveView) => void;
32
- openFiles: string[];
33
- closeFile: (path: string) => void;
34
- canvasFiles: string[];
30
+ activeView: ActiveView
31
+ setActiveView: (v: ActiveView) => void
32
+ openFiles: string[]
33
+ closeFile: (path: string) => void
34
+ canvasFiles: string[]
35
35
  }>({
36
- activeView: { type: "overview" },
37
- setActiveView: () => {},
38
- openFiles: [],
39
- closeFile: () => {},
40
- canvasFiles: [],
41
- });
36
+ activeView: { type: 'overview' },
37
+ setActiveView: () => {},
38
+ openFiles: [],
39
+ closeFile: () => {},
40
+ canvasFiles: [],
41
+ })
42
42
 
43
43
  export const RevisionContext = createContext<{
44
- currentRevision: number;
45
- selectedRevision: number;
46
- revisions: RevisionInfo[];
47
- setSelectedRevision: (rev: number) => void;
48
- isReadOnly: boolean;
49
- compareRevision: { left: number; right: number } | null;
50
- setCompareRevision: (rev: { left: number; right: number } | null) => void;
51
- agentWatching: boolean;
44
+ currentRevision: number
45
+ selectedRevision: number
46
+ revisions: RevisionInfo[]
47
+ setSelectedRevision: (rev: number) => void
48
+ isReadOnly: boolean
49
+ compareRevision: { left: number; right: number } | null
50
+ setCompareRevision: (rev: { left: number; right: number } | null) => void
51
+ agentWatching: boolean
52
52
  }>({
53
- currentRevision: 1,
54
- selectedRevision: 1,
55
- revisions: [],
56
- setSelectedRevision: () => {},
57
- isReadOnly: false,
58
- compareRevision: null,
59
- setCompareRevision: () => {},
60
- agentWatching: false,
61
- });
53
+ currentRevision: 1,
54
+ selectedRevision: 1,
55
+ revisions: [],
56
+ setSelectedRevision: () => {},
57
+ isReadOnly: false,
58
+ compareRevision: null,
59
+ setCompareRevision: () => {},
60
+ agentWatching: false,
61
+ })
@@ -1,27 +1,27 @@
1
- import React from "react";
2
- import { renderMarkdownChildren } from "./renderMarkdownChildren";
1
+ import React from 'react'
2
+ import { renderMarkdownChildren } from './renderMarkdownChildren'
3
3
 
4
4
  interface CalloutProps {
5
- type?: "info" | "warning" | "danger" | "tip";
6
- children?: React.ReactNode;
5
+ type?: 'info' | 'warning' | 'danger' | 'tip'
6
+ children?: React.ReactNode
7
7
  }
8
8
 
9
9
  const config = {
10
- info: { bg: "bg-accent-blue-muted", icon: "i", iconColor: "text-accent-blue" },
11
- warning: { bg: "bg-accent-amber-muted", icon: "!", iconColor: "text-accent-amber" },
12
- danger: { bg: "bg-accent-red-muted", icon: "\u2715", iconColor: "text-accent-red" },
13
- tip: { bg: "bg-accent-green-muted", icon: "\u2713", iconColor: "text-accent-green" },
14
- };
10
+ info: { bg: 'bg-accent-blue-muted', icon: 'i', iconColor: 'text-accent-blue' },
11
+ warning: { bg: 'bg-accent-amber-muted', icon: '!', iconColor: 'text-accent-amber' },
12
+ danger: { bg: 'bg-accent-red-muted', icon: '\u2715', iconColor: 'text-accent-red' },
13
+ tip: { bg: 'bg-accent-green-muted', icon: '\u2713', iconColor: 'text-accent-green' },
14
+ }
15
15
 
16
- export function Callout({ type = "info", children }: CalloutProps) {
17
- const cfg = config[type];
18
- return (
19
- <div className={`${cfg.bg} relative rounded-lg px-5 py-4 mt-3 flex gap-3 items-start`} data-md="callout" data-md-type={type}>
20
- {/* Decorative: the type it stands for is already in data-md-type. */}
21
- <span aria-hidden="true" className={`${cfg.iconColor} opacity-70 text-base leading-relaxed flex-shrink-0 mt-px`}>
22
- {cfg.icon}
23
- </span>
24
- <div className="text-body text-text-secondary leading-relaxed">{renderMarkdownChildren(children)}</div>
25
- </div>
26
- );
16
+ export function Callout({ type = 'info', children }: CalloutProps) {
17
+ const cfg = config[type]
18
+ return (
19
+ <div className={`${cfg.bg} relative rounded-lg px-5 py-4 mt-3 flex gap-3 items-start`} data-md="callout" data-md-type={type}>
20
+ {/* Decorative: the type it stands for is already in data-md-type. */}
21
+ <span aria-hidden="true" className={`${cfg.iconColor} opacity-70 text-base leading-relaxed flex-shrink-0 mt-px`}>
22
+ {cfg.icon}
23
+ </span>
24
+ <div className="text-body text-text-secondary leading-relaxed">{renderMarkdownChildren(children)}</div>
25
+ </div>
26
+ )
27
27
  }
@@ -1,37 +1,35 @@
1
- import React from "react";
2
-
3
1
  interface ChecklistItem {
4
- label: string;
5
- checked: boolean;
2
+ label: string
3
+ checked: boolean
6
4
  }
7
5
 
8
6
  interface ChecklistProps {
9
- items: ChecklistItem[];
7
+ items: ChecklistItem[]
10
8
  }
11
9
 
12
10
  export function Checklist({ items }: ChecklistProps) {
13
- return (
14
- <ul className="space-y-1 mt-3" data-md="checklist">
15
- {items.map((item, i) => (
16
- <li key={i} data-md="checklist-item" data-md-label={item.label} className="relative flex items-start gap-2.5 py-1">
17
- <span className={`w-4 h-4 rounded-full flex items-center justify-center flex-shrink-0 mt-0.5 transition-all ${
18
- item.checked
19
- ? "bg-accent-green"
20
- : "border-[1.5px] border-border-hover"
21
- }`}>
22
- {item.checked && (
23
- <svg width="10" height="10" viewBox="0 0 10 10" fill="none">
24
- <path d="M2.5 5L4.5 7L7.5 3" style={{ stroke: "var(--color-text-inverse)" }} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
25
- </svg>
26
- )}
27
- </span>
28
- <span className={`text-body font-body ${
29
- item.checked ? "text-text-tertiary line-through decoration-border-hover" : "text-text-secondary"
30
- }`}>
31
- {item.label}
32
- </span>
33
- </li>
34
- ))}
35
- </ul>
36
- );
11
+ return (
12
+ <ul className="space-y-1 mt-3" data-md="checklist">
13
+ {items.map((item, i) => (
14
+ <li key={i} data-md="checklist-item" data-md-label={item.label} className="relative flex items-start gap-2.5 py-1">
15
+ <span
16
+ className={`w-4 h-4 rounded-full flex items-center justify-center flex-shrink-0 mt-0.5 transition-all ${
17
+ item.checked
18
+ ? 'bg-accent-green'
19
+ : 'border-[1.5px] border-border-hover'
20
+ }`}
21
+ >
22
+ {item.checked && (
23
+ <svg width="10" height="10" viewBox="0 0 10 10" fill="none">
24
+ <path d="M2.5 5L4.5 7L7.5 3" style={{ stroke: 'var(--color-text-inverse)' }} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
25
+ </svg>
26
+ )}
27
+ </span>
28
+ <span className={`text-body font-body ${item.checked ? 'text-text-tertiary line-through decoration-border-hover' : 'text-text-secondary'}`}>
29
+ {item.label}
30
+ </span>
31
+ </li>
32
+ ))}
33
+ </ul>
34
+ )
37
35
  }