pmx-canvas 0.3.0 → 0.3.2

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 (161) hide show
  1. package/CHANGELOG.md +73 -0
  2. package/dist/canvas/index.js +65 -65
  3. package/dist/types/cli/agent.d.ts +9 -1
  4. package/dist/types/cli/daemon.d.ts +74 -0
  5. package/dist/types/cli/watch.d.ts +2 -2
  6. package/dist/types/client/canvas/CanvasViewport.d.ts +1 -1
  7. package/dist/types/client/canvas/CommandPalette.d.ts +1 -1
  8. package/dist/types/client/canvas/Minimap.d.ts +1 -1
  9. package/dist/types/client/nodes/ExtAppFrame.d.ts +9 -1
  10. package/dist/types/client/nodes/FileNode.d.ts +1 -1
  11. package/dist/types/client/nodes/ImageNode.d.ts +1 -1
  12. package/dist/types/client/nodes/InlineFormatBar.d.ts +1 -1
  13. package/dist/types/client/nodes/MarkdownNode.d.ts +1 -1
  14. package/dist/types/client/nodes/PromptNode.d.ts +1 -1
  15. package/dist/types/client/nodes/ResponseNode.d.ts +1 -1
  16. package/dist/types/server/canvas-schema.d.ts +1 -1
  17. package/dist/types/server/html-primitives.d.ts +1 -1
  18. package/dist/types/server/index.d.ts +4 -4
  19. package/dist/types/server/operations/index.d.ts +1 -1
  20. package/dist/types/server/operations/ops/ax-read.d.ts +2 -0
  21. package/dist/types/server/operations/ops/canvas-wire.d.ts +2 -0
  22. package/dist/types/server/operations/ops/ext-app.d.ts +2 -0
  23. package/dist/types/server/server.d.ts +8 -0
  24. package/docs/cli.md +10 -1
  25. package/docs/http-api.md +28 -0
  26. package/docs/plans/plan-009-tech-debt-backlog.md +5 -5
  27. package/docs/screenshot.png +0 -0
  28. package/docs/tech-debt-assessment-2026-07.md +2 -2
  29. package/package.json +5 -2
  30. package/skills/pmx-canvas/SKILL.md +15 -10
  31. package/skills/pmx-canvas/references/full-reference.md +17 -3
  32. package/src/cli/agent.ts +1951 -1571
  33. package/src/cli/daemon.ts +460 -0
  34. package/src/cli/index.ts +80 -323
  35. package/src/cli/watch.ts +2 -10
  36. package/src/client/App.tsx +48 -46
  37. package/src/client/canvas/AttentionHistory.tsx +11 -1
  38. package/src/client/canvas/CanvasNode.tsx +41 -29
  39. package/src/client/canvas/CanvasViewport.tsx +101 -66
  40. package/src/client/canvas/CommandPalette.tsx +61 -27
  41. package/src/client/canvas/ContextMenu.tsx +13 -20
  42. package/src/client/canvas/ContextPinBar.tsx +1 -5
  43. package/src/client/canvas/ContextPinHud.tsx +1 -6
  44. package/src/client/canvas/DockedNode.tsx +4 -4
  45. package/src/client/canvas/EdgeLayer.tsx +37 -36
  46. package/src/client/canvas/ExpandedNodeOverlay.tsx +69 -45
  47. package/src/client/canvas/FocusFieldLayer.tsx +20 -22
  48. package/src/client/canvas/IntentLayer.tsx +31 -14
  49. package/src/client/canvas/Minimap.tsx +11 -16
  50. package/src/client/canvas/SelectionBar.tsx +4 -11
  51. package/src/client/canvas/ShortcutOverlay.tsx +3 -1
  52. package/src/client/canvas/SnapshotPanel.tsx +77 -95
  53. package/src/client/canvas/auto-fit.ts +15 -14
  54. package/src/client/canvas/snap-guides.ts +12 -12
  55. package/src/client/canvas/use-node-resize.ts +1 -5
  56. package/src/client/canvas/use-pan-zoom.ts +25 -26
  57. package/src/client/ext-app/bridge.ts +3 -12
  58. package/src/client/icons.tsx +63 -20
  59. package/src/client/nodes/ContextNode.tsx +14 -25
  60. package/src/client/nodes/ExtAppFrame.tsx +194 -80
  61. package/src/client/nodes/FileNode.tsx +74 -62
  62. package/src/client/nodes/GroupNode.tsx +4 -6
  63. package/src/client/nodes/HtmlNode.tsx +76 -46
  64. package/src/client/nodes/ImageNode.tsx +18 -27
  65. package/src/client/nodes/InlineFormatBar.tsx +4 -21
  66. package/src/client/nodes/InlineMarkdownEditor.tsx +0 -1
  67. package/src/client/nodes/LedgerNode.tsx +10 -4
  68. package/src/client/nodes/MarkdownNode.tsx +3 -10
  69. package/src/client/nodes/McpAppNode.tsx +26 -22
  70. package/src/client/nodes/MdFormatBar.tsx +10 -7
  71. package/src/client/nodes/PromptNode.tsx +23 -51
  72. package/src/client/nodes/ResponseNode.tsx +3 -13
  73. package/src/client/nodes/StatusNode.tsx +5 -9
  74. package/src/client/nodes/StatusSummary.tsx +2 -8
  75. package/src/client/nodes/WebpageNode.tsx +20 -14
  76. package/src/client/nodes/iframe-document-url.ts +25 -16
  77. package/src/client/nodes/image-warnings.ts +1 -7
  78. package/src/client/nodes/md-format.ts +20 -5
  79. package/src/client/state/attention-bridge.ts +4 -9
  80. package/src/client/state/attention-store.ts +1 -7
  81. package/src/client/state/canvas-store.ts +52 -36
  82. package/src/client/state/intent-bridge.ts +176 -112
  83. package/src/client/state/intent-store.ts +4 -1
  84. package/src/client/state/sse-bridge.ts +53 -70
  85. package/src/json-render/catalog.ts +12 -16
  86. package/src/json-render/charts/components.tsx +16 -20
  87. package/src/json-render/charts/extra-components.tsx +8 -16
  88. package/src/json-render/charts/extra-definitions.ts +1 -2
  89. package/src/json-render/charts/tufte-components.tsx +37 -20
  90. package/src/json-render/charts/tufte-definitions.ts +8 -2
  91. package/src/json-render/renderer/index.tsx +42 -22
  92. package/src/json-render/schema.ts +6 -3
  93. package/src/json-render/server.ts +33 -39
  94. package/src/mcp/canvas-access.ts +35 -21
  95. package/src/mcp/server.ts +132 -70
  96. package/src/server/agent-context.ts +63 -36
  97. package/src/server/ax-context.ts +7 -5
  98. package/src/server/ax-interaction.ts +176 -43
  99. package/src/server/ax-state-manager.ts +182 -39
  100. package/src/server/ax-state.ts +142 -47
  101. package/src/server/canvas-db.ts +213 -95
  102. package/src/server/canvas-operations.ts +180 -123
  103. package/src/server/canvas-provenance.ts +1 -4
  104. package/src/server/canvas-schema.ts +454 -73
  105. package/src/server/canvas-serialization.ts +27 -35
  106. package/src/server/canvas-state.ts +150 -58
  107. package/src/server/chart-template.ts +4 -5
  108. package/src/server/code-graph.ts +19 -6
  109. package/src/server/diagram-presets.ts +28 -29
  110. package/src/server/ext-app-lookup.ts +3 -12
  111. package/src/server/html-node-summary.ts +19 -10
  112. package/src/server/html-primitives.ts +326 -97
  113. package/src/server/html-surface.ts +6 -9
  114. package/src/server/image-source.ts +6 -4
  115. package/src/server/index.ts +320 -217
  116. package/src/server/intent-registry.ts +2 -5
  117. package/src/server/mcp-app-candidate.ts +5 -10
  118. package/src/server/mcp-app-host.ts +14 -38
  119. package/src/server/mcp-app-runtime.ts +12 -20
  120. package/src/server/mutation-history.ts +15 -5
  121. package/src/server/operations/composites.ts +1 -3
  122. package/src/server/operations/http.ts +2 -3
  123. package/src/server/operations/index.ts +7 -1
  124. package/src/server/operations/invoker.ts +4 -3
  125. package/src/server/operations/mcp.ts +22 -30
  126. package/src/server/operations/ops/annotation.ts +122 -10
  127. package/src/server/operations/ops/app.ts +98 -73
  128. package/src/server/operations/ops/ax-await.ts +17 -10
  129. package/src/server/operations/ops/ax-read.ts +347 -0
  130. package/src/server/operations/ops/ax-shared.ts +2 -7
  131. package/src/server/operations/ops/ax-state.ts +32 -14
  132. package/src/server/operations/ops/ax-timeline.ts +32 -19
  133. package/src/server/operations/ops/ax-work.ts +54 -37
  134. package/src/server/operations/ops/batch.ts +41 -15
  135. package/src/server/operations/ops/canvas-wire.ts +91 -0
  136. package/src/server/operations/ops/edges.ts +37 -25
  137. package/src/server/operations/ops/ext-app.ts +346 -0
  138. package/src/server/operations/ops/groups.ts +49 -20
  139. package/src/server/operations/ops/intent.ts +18 -12
  140. package/src/server/operations/ops/json-render.ts +239 -98
  141. package/src/server/operations/ops/nodes.ts +298 -109
  142. package/src/server/operations/ops/query.ts +46 -28
  143. package/src/server/operations/ops/snapshots.ts +35 -26
  144. package/src/server/operations/ops/validate.ts +2 -1
  145. package/src/server/operations/ops/viewport.ts +60 -16
  146. package/src/server/operations/ops/webview.ts +44 -18
  147. package/src/server/operations/registry.ts +2 -3
  148. package/src/server/operations/types.ts +7 -5
  149. package/src/server/operations/webview-runner.ts +1 -3
  150. package/src/server/placement.ts +8 -18
  151. package/src/server/server.ts +122 -1028
  152. package/src/server/spatial-analysis.ts +39 -25
  153. package/src/server/trace-manager.ts +3 -8
  154. package/src/server/web-artifacts.ts +23 -27
  155. package/src/server/webpage-node.ts +5 -13
  156. package/src/shared/auto-arrange.ts +12 -5
  157. package/src/shared/content-height-reporter.ts +8 -6
  158. package/src/shared/ext-app-tool-result.ts +2 -6
  159. package/src/shared/placement.ts +1 -4
  160. package/src/shared/semantic-attention.ts +39 -37
  161. package/src/shared/surface.ts +8 -4
@@ -32,10 +32,7 @@ function RenderedMarkdown({ html }: { html: string }) {
32
32
  return <div ref={containerRef} />;
33
33
  }
34
34
 
35
- export function ResponseNode({
36
- node,
37
- expanded = false,
38
- }: { node: CanvasNodeState; expanded?: boolean }) {
35
+ export function ResponseNode({ node, expanded = false }: { node: CanvasNodeState; expanded?: boolean }) {
39
36
  const content = (node.data.content as string) || '';
40
37
  const status = (node.data.status as string) || 'streaming';
41
38
  const [rendered, setRendered] = useState('');
@@ -59,18 +56,11 @@ export function ResponseNode({
59
56
  }, [content]);
60
57
 
61
58
  const handleReply = useCallback(() => {
62
- submitCanvasPrompt(
63
- '',
64
- { x: node.position.x, y: node.position.y + node.size.height + 24 },
65
- node.id,
66
- );
59
+ submitCanvasPrompt('', { x: node.position.x, y: node.position.y + node.size.height + 24 }, node.id);
67
60
  }, [node]);
68
61
 
69
62
  return (
70
- <div
71
- class="response-node-inner"
72
- style={{ display: 'flex', flexDirection: 'column', height: '100%' }}
73
- >
63
+ <div class="response-node-inner" style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
74
64
  {/* Streaming indicator */}
75
65
  {isStreaming && (
76
66
  <div
@@ -2,17 +2,13 @@ import { PHASE_COLORS } from '../theme/tokens';
2
2
  import type { CanvasNodeState } from '../types';
3
3
 
4
4
  export function getStatusDisplayPhase(node: CanvasNodeState): string {
5
- const phase = typeof node.data.phase === 'string' && node.data.phase.trim().length > 0
6
- ? node.data.phase.trim()
7
- : '';
5
+ const phase = typeof node.data.phase === 'string' && node.data.phase.trim().length > 0 ? node.data.phase.trim() : '';
8
6
  if (phase) return phase;
9
- const content = typeof node.data.content === 'string' && node.data.content.trim().length > 0
10
- ? node.data.content.trim()
11
- : '';
7
+ const content =
8
+ typeof node.data.content === 'string' && node.data.content.trim().length > 0 ? node.data.content.trim() : '';
12
9
  if (content) return content;
13
- const status = typeof node.data.status === 'string' && node.data.status.trim().length > 0
14
- ? node.data.status.trim()
15
- : '';
10
+ const status =
11
+ typeof node.data.status === 'string' && node.data.status.trim().length > 0 ? node.data.status.trim() : '';
16
12
  return status || 'idle';
17
13
  }
18
14
 
@@ -21,15 +21,9 @@ export function StatusSummary({ node }: { node: CanvasNodeState }) {
21
21
  flexShrink: 0,
22
22
  }}
23
23
  />
24
- <span
25
- style={{ color: phaseColor, fontSize: '10px', textTransform: 'uppercase', fontWeight: 600 }}
26
- >
27
- {phase}
28
- </span>
24
+ <span style={{ color: phaseColor, fontSize: '10px', textTransform: 'uppercase', fontWeight: 600 }}>{phase}</span>
29
25
  {activeTool && (
30
- <span style={{ color: 'var(--c-warn)', fontSize: '10px', fontFamily: 'var(--mono)' }}>
31
- ⚙ {activeTool}
32
- </span>
26
+ <span style={{ color: 'var(--c-warn)', fontSize: '10px', fontFamily: 'var(--mono)' }}>⚙ {activeTool}</span>
33
27
  )}
34
28
  {subagent && subagent.state !== 'completed' && (
35
29
  <span style={{ color: 'var(--c-subagent)', fontSize: '10px' }}>⠉ {subagent.name}</span>
@@ -20,11 +20,12 @@ export function WebpageNode({ node, expanded = false }: { node: CanvasNodeState;
20
20
  const url = typeof node.data.url === 'string' ? node.data.url : '';
21
21
  const pageTitle = typeof node.data.pageTitle === 'string' ? node.data.pageTitle : '';
22
22
  const description = typeof node.data.description === 'string' ? node.data.description : '';
23
- const excerpt = typeof node.data.excerpt === 'string'
24
- ? node.data.excerpt
25
- : typeof node.data.content === 'string'
26
- ? node.data.content
27
- : '';
23
+ const excerpt =
24
+ typeof node.data.excerpt === 'string'
25
+ ? node.data.excerpt
26
+ : typeof node.data.content === 'string'
27
+ ? node.data.content
28
+ : '';
28
29
  const status = typeof node.data.status === 'string' ? node.data.status : 'idle';
29
30
  const error = typeof node.data.error === 'string' ? node.data.error : '';
30
31
  const fetchedAt = formatFetchedAt(typeof node.data.fetchedAt === 'string' ? node.data.fetchedAt : undefined);
@@ -55,12 +56,7 @@ export function WebpageNode({ node, expanded = false }: { node: CanvasNodeState;
55
56
  return <div style={{ color: 'var(--c-dim)', fontStyle: 'italic', padding: '12px' }}>No webpage URL set</div>;
56
57
  }
57
58
 
58
- const statusTone =
59
- status === 'ready'
60
- ? 'var(--c-ok)'
61
- : status === 'error'
62
- ? 'var(--c-danger)'
63
- : 'var(--c-warn)';
59
+ const statusTone = status === 'ready' ? 'var(--c-ok)' : status === 'error' ? 'var(--c-danger)' : 'var(--c-warn)';
64
60
 
65
61
  return (
66
62
  <div style={{ display: 'flex', flexDirection: 'column', gap: '12px', height: '100%' }}>
@@ -123,7 +119,9 @@ export function WebpageNode({ node, expanded = false }: { node: CanvasNodeState;
123
119
  />
124
120
  )}
125
121
  {description && (
126
- <p style={{ margin: 0, color: 'var(--c-text-soft)', lineHeight: 1.5, fontSize: expanded ? '14px' : '12px' }}>
122
+ <p
123
+ style={{ margin: 0, color: 'var(--c-text-soft)', lineHeight: 1.5, fontSize: expanded ? '14px' : '12px' }}
124
+ >
127
125
  {description}
128
126
  </p>
129
127
  )}
@@ -232,7 +230,8 @@ export function WebpageNode({ node, expanded = false }: { node: CanvasNodeState;
232
230
  background: 'rgba(255,255,255,0.03)',
233
231
  }}
234
232
  >
235
- Live preview (best effort). If this stays blank, the site likely blocks framing. The cached text snapshot below still works.
233
+ Live preview (best effort). If this stays blank, the site likely blocks framing. The cached text snapshot
234
+ below still works.
236
235
  </div>
237
236
  <iframe
238
237
  class="webpage-node-iframe"
@@ -274,7 +273,14 @@ export function WebpageNode({ node, expanded = false }: { node: CanvasNodeState;
274
273
  }}
275
274
  >
276
275
  {excerpt ? (
277
- <div style={{ whiteSpace: 'pre-wrap', lineHeight: 1.55, color: 'var(--c-text)', fontSize: expanded ? '14px' : '12px' }}>
276
+ <div
277
+ style={{
278
+ whiteSpace: 'pre-wrap',
279
+ lineHeight: 1.55,
280
+ color: 'var(--c-text)',
281
+ fontSize: expanded ? '14px' : '12px',
282
+ }}
283
+ >
278
284
  {excerpt}
279
285
  </div>
280
286
  ) : (
@@ -7,12 +7,14 @@ interface FrameDocumentCreateResponse {
7
7
  }
8
8
 
9
9
  function isFrameDocumentCreateResponse(value: unknown): value is FrameDocumentCreateResponse {
10
- return Boolean(value)
11
- && typeof value === 'object'
12
- && value !== null
13
- && !Array.isArray(value)
14
- && 'ok' in value
15
- && typeof (value as { ok: unknown }).ok === 'boolean';
10
+ return (
11
+ Boolean(value) &&
12
+ typeof value === 'object' &&
13
+ value !== null &&
14
+ !Array.isArray(value) &&
15
+ 'ok' in value &&
16
+ typeof (value as { ok: unknown }).ok === 'boolean'
17
+ );
16
18
  }
17
19
 
18
20
  export async function createIframeDocumentUrl(html: string, sandbox: string): Promise<string> {
@@ -21,17 +23,21 @@ export async function createIframeDocumentUrl(html: string, sandbox: string): Pr
21
23
  headers: { 'Content-Type': 'application/json' },
22
24
  body: JSON.stringify({ html, sandbox }),
23
25
  });
24
- const json = await response.json() as unknown;
26
+ const json = (await response.json()) as unknown;
25
27
  if (!response.ok || !isFrameDocumentCreateResponse(json) || !json.ok || typeof json.url !== 'string') {
26
- const message = isFrameDocumentCreateResponse(json) && json.error
27
- ? json.error
28
- : `Frame document request failed with HTTP ${response.status}`;
28
+ const message =
29
+ isFrameDocumentCreateResponse(json) && json.error
30
+ ? json.error
31
+ : `Frame document request failed with HTTP ${response.status}`;
29
32
  throw new Error(message);
30
33
  }
31
34
  return json.url;
32
35
  }
33
36
 
34
- export function useIframeDocument(html: string, sandbox: string): { attributes: { src?: string }; ready: boolean; key: string } {
37
+ export function useIframeDocument(
38
+ html: string,
39
+ sandbox: string,
40
+ ): { attributes: { src?: string }; ready: boolean; key: string } {
35
41
  const [src, setSrc] = useState<string | null>(null);
36
42
 
37
43
  useEffect(() => {
@@ -50,9 +56,12 @@ export function useIframeDocument(html: string, sandbox: string): { attributes:
50
56
  };
51
57
  }, [html, sandbox]);
52
58
 
53
- return useMemo(() => ({
54
- attributes: src ? { src } : {},
55
- ready: Boolean(src),
56
- key: src ?? '',
57
- }), [src]);
59
+ return useMemo(
60
+ () => ({
61
+ attributes: src ? { src } : {},
62
+ ready: Boolean(src),
63
+ key: src ?? '',
64
+ }),
65
+ [src],
66
+ );
58
67
  }
@@ -52,13 +52,7 @@ function metadataWarnings(node: CanvasNodeState): ImageNodeWarning[] {
52
52
  }
53
53
 
54
54
  function looksLikeLoginCapture(node: CanvasNodeState): boolean {
55
- const haystack = [
56
- node.data.title,
57
- node.data.caption,
58
- node.data.alt,
59
- node.data.src,
60
- node.data.path,
61
- ]
55
+ const haystack = [node.data.title, node.data.caption, node.data.alt, node.data.src, node.data.path]
62
56
  .map((value) => readTrimmedString(value)?.toLowerCase() ?? '')
63
57
  .join(' ');
64
58
 
@@ -168,11 +168,26 @@ export function getSelectionRect(ta: HTMLTextAreaElement): { top: number; left:
168
168
  const mirror = document.createElement('div');
169
169
  const style = getComputedStyle(ta);
170
170
  const props = [
171
- 'fontFamily', 'fontSize', 'fontWeight', 'lineHeight', 'letterSpacing',
172
- 'wordSpacing', 'textIndent', 'whiteSpace', 'wordWrap', 'overflowWrap',
173
- 'paddingTop', 'paddingRight', 'paddingBottom', 'paddingLeft',
174
- 'borderTopWidth', 'borderRightWidth', 'borderBottomWidth', 'borderLeftWidth',
175
- 'boxSizing', 'width',
171
+ 'fontFamily',
172
+ 'fontSize',
173
+ 'fontWeight',
174
+ 'lineHeight',
175
+ 'letterSpacing',
176
+ 'wordSpacing',
177
+ 'textIndent',
178
+ 'whiteSpace',
179
+ 'wordWrap',
180
+ 'overflowWrap',
181
+ 'paddingTop',
182
+ 'paddingRight',
183
+ 'paddingBottom',
184
+ 'paddingLeft',
185
+ 'borderTopWidth',
186
+ 'borderRightWidth',
187
+ 'borderBottomWidth',
188
+ 'borderLeftWidth',
189
+ 'boxSizing',
190
+ 'width',
176
191
  ] as const;
177
192
  for (const p of props) {
178
193
  mirror.style[p] = style[p];
@@ -106,10 +106,7 @@ function entryFromContextPin(event: ContextPinWatchEvent): AttentionEntry | null
106
106
  'context',
107
107
  'Context updated',
108
108
  detail,
109
- [
110
- ...event.added.map((node) => node.id),
111
- ...event.removed.map((node) => node.id),
112
- ],
109
+ [...event.added.map((node) => node.id), ...event.removed.map((node) => node.id)],
113
110
  event.timestamp ? Date.parse(event.timestamp) || Date.now() : Date.now(),
114
111
  );
115
112
  }
@@ -148,10 +145,7 @@ function entryFromRemove(event: RemoveWatchEvent): AttentionEntry | null {
148
145
  'remove',
149
146
  'Items removed',
150
147
  parts.join(' · '),
151
- [
152
- ...event.nodes.map((node) => node.id),
153
- ...event.edges.flatMap((edge) => [edge.fromId, edge.toId]),
154
- ],
148
+ [...event.nodes.map((node) => node.id), ...event.edges.flatMap((edge) => [edge.fromId, edge.toId])],
155
149
  event.timestamp ? Date.parse(event.timestamp) || Date.now() : Date.now(),
156
150
  );
157
151
  }
@@ -318,7 +312,8 @@ export function resetAttentionBridge(): void {
318
312
  }
319
313
 
320
314
  export function syncAttentionFromSse(message: SseMessage): void {
321
- const entries = reducer.handleMessage(message)
315
+ const entries = reducer
316
+ .handleMessage(message)
322
317
  .map((event) => entryFromEvent(event))
323
318
  .filter((entry): entry is AttentionEntry => entry !== null);
324
319
 
@@ -1,13 +1,7 @@
1
1
  import { signal } from '@preact/signals';
2
2
  import type { SemanticAttentionRegion } from '../../shared/semantic-attention.js';
3
3
 
4
- export type AttentionTone =
5
- | 'context'
6
- | 'relationship'
7
- | 'group'
8
- | 'cluster'
9
- | 'neighborhood'
10
- | 'remove';
4
+ export type AttentionTone = 'context' | 'relationship' | 'group' | 'cluster' | 'neighborhood' | 'remove';
11
5
 
12
6
  export interface AttentionEntry {
13
7
  id: string;
@@ -1,5 +1,13 @@
1
1
  import { batch, computed, signal } from '@preact/signals';
2
- import { isExcalidrawNode, type CanvasAnnotation, type CanvasEdge, type CanvasLayout, type CanvasNodeState, type ConnectionStatus, type ViewportState } from '../types';
2
+ import {
3
+ isExcalidrawNode,
4
+ type CanvasAnnotation,
5
+ type CanvasEdge,
6
+ type CanvasLayout,
7
+ type CanvasNodeState,
8
+ type ConnectionStatus,
9
+ type ViewportState,
10
+ } from '../types';
3
11
  import { computeAutoArrange } from '../../shared/auto-arrange';
4
12
  import { pushCanvasUpdate, updateViewportFromClient } from './intent-bridge';
5
13
 
@@ -29,7 +37,7 @@ export const axSurfaceState = signal<unknown>(null);
29
37
  export const expandedNodeId = signal<string | null>(null);
30
38
  export const pendingExpandedNodeCloseId = signal<string | null>(null);
31
39
  let expandedCloseTimer: ReturnType<typeof setTimeout> | null = null;
32
- let pendingCloseInitialCheckpointAt: unknown = undefined;
40
+ let pendingCloseInitialCheckpointAt: unknown;
33
41
  const EXCALIDRAW_CLOSE_POLL_MS = 100;
34
42
  const EXCALIDRAW_CLOSE_MAX_WAIT_MS = 2500;
35
43
 
@@ -54,10 +62,7 @@ export const selectedNodeIds = signal<Set<string>>(new Set());
54
62
  // ── Context pins (persistent context for agent queries) ──────
55
63
  export const contextPinnedNodeIds = signal<Set<string>>(new Set());
56
64
 
57
- export function getNeighborNodeIds(
58
- nodeId: string | null,
59
- edgeMap: Map<string, CanvasEdge>,
60
- ): Set<string> {
65
+ export function getNeighborNodeIds(nodeId: string | null, edgeMap: Map<string, CanvasEdge>): Set<string> {
61
66
  if (!nodeId) return new Set();
62
67
 
63
68
  const neighborIds = new Set<string>();
@@ -392,10 +397,7 @@ export function commitViewport(next: ViewportState): void {
392
397
  commitViewportWithOptions(next);
393
398
  }
394
399
 
395
- function commitViewportWithOptions(
396
- next: ViewportState,
397
- options: { recordHistory?: boolean } = {},
398
- ): void {
400
+ function commitViewportWithOptions(next: ViewportState, options: { recordHistory?: boolean } = {}): void {
399
401
  viewport.value = next;
400
402
  persistLayout(options);
401
403
  void updateViewportFromClient(next, options);
@@ -412,9 +414,7 @@ export function applyServerCanvasLayout(
412
414
  if (node.zIndex >= nextMaxZ) nextMaxZ = node.zIndex + 1;
413
415
  }
414
416
 
415
- const edgeSource = layout.edges.filter(
416
- (edge) => nextNodes.has(edge.from) && nextNodes.has(edge.to),
417
- );
417
+ const edgeSource = layout.edges.filter((edge) => nextNodes.has(edge.from) && nextNodes.has(edge.to));
418
418
  const nextEdges = new Map<string, CanvasEdge>();
419
419
  for (const edge of edgeSource) {
420
420
  nextEdges.set(edge.id, edge);
@@ -424,8 +424,7 @@ export function applyServerCanvasLayout(
424
424
  nextAnnotations.set(annotation.id, annotation);
425
425
  }
426
426
 
427
- const nextActiveNodeId =
428
- activeNodeId.value !== null && nextNodes.has(activeNodeId.value) ? activeNodeId.value : null;
427
+ const nextActiveNodeId = activeNodeId.value !== null && nextNodes.has(activeNodeId.value) ? activeNodeId.value : null;
429
428
  const nextExpandedNodeId =
430
429
  expandedNodeId.value !== null && nextNodes.has(expandedNodeId.value) ? expandedNodeId.value : null;
431
430
  const nextSelectedNodeIds = filterNodeIdSet(selectedNodeIds.value, nextNodes);
@@ -618,11 +617,15 @@ export function focusNode(id: string, options: { recordHistory?: boolean } = {})
618
617
  const v = viewport.value;
619
618
  const cx = node.position.x + node.size.width / 2;
620
619
  const cy = node.position.y + node.size.height / 2;
621
- animateViewport({
622
- x: window.innerWidth / 2 - cx * v.scale,
623
- y: window.innerHeight / 2 - cy * v.scale,
624
- scale: v.scale,
625
- }, 300, options);
620
+ animateViewport(
621
+ {
622
+ x: window.innerWidth / 2 - cx * v.scale,
623
+ y: window.innerHeight / 2 - cy * v.scale,
624
+ scale: v.scale,
625
+ },
626
+ 300,
627
+ options,
628
+ );
626
629
  bringToFront(id);
627
630
  }
628
631
 
@@ -669,10 +672,18 @@ export function walkGraph(direction: 'up' | 'down' | 'left' | 'right'): void {
669
672
  // Dot product with direction vector, normalized by distance
670
673
  let dot: number;
671
674
  switch (direction) {
672
- case 'up': dot = -dy; break;
673
- case 'down': dot = dy; break;
674
- case 'left': dot = -dx; break;
675
- case 'right': dot = dx; break;
675
+ case 'up':
676
+ dot = -dy;
677
+ break;
678
+ case 'down':
679
+ dot = dy;
680
+ break;
681
+ case 'left':
682
+ dot = -dx;
683
+ break;
684
+ case 'right':
685
+ dot = dx;
686
+ break;
676
687
  }
677
688
 
678
689
  // Only consider nodes that are at least somewhat in the right direction
@@ -727,10 +738,7 @@ export function collapseExpandedNode(): void {
727
738
  const pollForSave = () => {
728
739
  const latestNode = nodes.value.get(closingNodeId);
729
740
  const latestCheckpointAt = (latestNode?.data.appCheckpoint as { updatedAt?: unknown } | undefined)?.updatedAt;
730
- if (
731
- latestCheckpointAt !== undefined &&
732
- latestCheckpointAt !== pendingCloseInitialCheckpointAt
733
- ) {
741
+ if (latestCheckpointAt !== undefined && latestCheckpointAt !== pendingCloseInitialCheckpointAt) {
734
742
  finishExpandedNodeClose(closingNodeId);
735
743
  return;
736
744
  }
@@ -762,10 +770,14 @@ export function autoArrange(): void {
762
770
  updateNode(id, { position });
763
771
  }
764
772
  for (const [groupId, bounds] of result.groupBounds.entries()) {
765
- updateNodeWithOptions(groupId, {
766
- position: { x: bounds.x, y: bounds.y },
767
- size: { width: bounds.width, height: bounds.height },
768
- }, { skipGroupChildTranslation: true });
773
+ updateNodeWithOptions(
774
+ groupId,
775
+ {
776
+ position: { x: bounds.x, y: bounds.y },
777
+ size: { width: bounds.width, height: bounds.height },
778
+ },
779
+ { skipGroupChildTranslation: true },
780
+ );
769
781
  }
770
782
  });
771
783
  persistLayout();
@@ -780,10 +792,14 @@ export function forceDirectedArrange(): void {
780
792
  updateNode(id, { position });
781
793
  }
782
794
  for (const [groupId, bounds] of result.groupBounds.entries()) {
783
- updateNodeWithOptions(groupId, {
784
- position: { x: bounds.x, y: bounds.y },
785
- size: { width: bounds.width, height: bounds.height },
786
- }, { skipGroupChildTranslation: true });
795
+ updateNodeWithOptions(
796
+ groupId,
797
+ {
798
+ position: { x: bounds.x, y: bounds.y },
799
+ size: { width: bounds.width, height: bounds.height },
800
+ },
801
+ { skipGroupChildTranslation: true },
802
+ );
787
803
  }
788
804
  });
789
805
  persistLayout();