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
@@ -87,13 +87,16 @@ export function SnapshotPanel({
87
87
  }
88
88
  }, [nameInput]);
89
89
 
90
- const handleRestore = useCallback(async (id: string) => {
91
- setConfirming(null);
92
- setRestoringId(id);
93
- const result = await restoreSnapshot(id);
94
- setRestoringId(null);
95
- if (result.ok) onClose();
96
- }, [onClose]);
90
+ const handleRestore = useCallback(
91
+ async (id: string) => {
92
+ setConfirming(null);
93
+ setRestoringId(id);
94
+ const result = await restoreSnapshot(id);
95
+ setRestoringId(null);
96
+ if (result.ok) onClose();
97
+ },
98
+ [onClose],
99
+ );
97
100
 
98
101
  const handleDelete = useCallback(async (id: string) => {
99
102
  const result = await deleteSnapshot(id);
@@ -111,20 +114,11 @@ export function SnapshotPanel({
111
114
  const top = anchorRect ? anchorRect.bottom + 8 : 48;
112
115
 
113
116
  return (
114
- <div
115
- ref={panelRef}
116
- class="snapshot-panel"
117
- style={{ left: `${left}px`, top: `${top}px` }}
118
- >
117
+ <div ref={panelRef} class="snapshot-panel" style={{ left: `${left}px`, top: `${top}px` }}>
119
118
  {/* Header */}
120
119
  <div class="snapshot-panel-header">
121
120
  <span class="snapshot-panel-title">Snapshots</span>
122
- <button
123
- type="button"
124
- class="snapshot-panel-close"
125
- onClick={onClose}
126
- title="Close"
127
- >
121
+ <button type="button" class="snapshot-panel-close" onClick={onClose} title="Close">
128
122
  {'\u00d7'}
129
123
  </button>
130
124
  </div>
@@ -137,99 +131,87 @@ export function SnapshotPanel({
137
131
  class="snapshot-name-input"
138
132
  value={nameInput}
139
133
  onInput={(e) => setNameInput((e.target as HTMLInputElement).value)}
140
- onKeyDown={(e) => { if (e.key === 'Enter') handleSave(); }}
134
+ onKeyDown={(e) => {
135
+ if (e.key === 'Enter') handleSave();
136
+ }}
141
137
  placeholder="Snapshot name..."
142
138
  maxLength={80}
143
139
  disabled={saving}
144
140
  />
145
- <button
146
- type="button"
147
- class="snapshot-save-btn"
148
- onClick={handleSave}
149
- disabled={!nameInput.trim() || saving}
150
- >
141
+ <button type="button" class="snapshot-save-btn" onClick={handleSave} disabled={!nameInput.trim() || saving}>
151
142
  {saving ? '...' : 'Save'}
152
143
  </button>
153
144
  </div>
154
145
 
155
- <div class="snapshot-restore-note">
156
- Restoring replaces the current canvas. You can undo it if needed.
157
- </div>
146
+ <div class="snapshot-restore-note">Restoring replaces the current canvas. You can undo it if needed.</div>
158
147
 
159
148
  {/* Snapshot list */}
160
149
  <div class="snapshot-list">
161
- {loading && (
162
- <div class="snapshot-empty">Loading...</div>
163
- )}
150
+ {loading && <div class="snapshot-empty">Loading...</div>}
164
151
 
165
152
  {!loading && snapshots.length === 0 && (
166
- <div class="snapshot-empty">
167
- No snapshots yet. Save one to capture the current canvas state.
168
- </div>
153
+ <div class="snapshot-empty">No snapshots yet. Save one to capture the current canvas state.</div>
169
154
  )}
170
155
 
171
- {!loading && snapshots.map((snap) => (
172
- <div key={snap.id} class="snapshot-item">
173
- <div class="snapshot-item-info">
174
- <span class="snapshot-item-name">{snap.name}</span>
175
- <span class="snapshot-item-meta">
176
- {snap.nodeCount} node{snap.nodeCount !== 1 ? 's' : ''}
177
- {snap.edgeCount > 0 ? ` \u00b7 ${snap.edgeCount} edge${snap.edgeCount !== 1 ? 's' : ''}` : ''}
178
- {' \u00b7 '}
179
- {timeAgo(snap.createdAt)}
180
- </span>
181
- </div>
182
- <div class="snapshot-item-actions">
183
- {confirming?.id === snap.id ? (
184
- <>
185
- <button
186
- type="button"
187
- class={`snapshot-action-btn ${confirming.action === 'delete' ? 'snapshot-action-confirm' : 'snapshot-action-restore'}`}
188
- onClick={() => confirming.action === 'delete' ? handleDelete(snap.id) : handleRestore(snap.id)}
189
- title={confirming.action === 'delete' ? 'Confirm delete' : 'Confirm restore'}
190
- disabled={restoringId !== null}
191
- >
192
- {confirming.action === 'delete'
193
- ? 'Delete'
194
- : restoringId === snap.id
195
- ? 'Restoring...'
196
- : 'Confirm'}
197
- </button>
198
- <button
199
- type="button"
200
- class="snapshot-action-btn"
201
- onClick={() => setConfirming(null)}
202
- title="Cancel"
203
- disabled={restoringId !== null}
204
- >
205
- Cancel
206
- </button>
207
- </>
208
- ) : (
209
- <>
210
- <button
211
- type="button"
212
- class="snapshot-action-btn snapshot-action-restore"
213
- onClick={() => setConfirming({ id: snap.id, action: 'restore' })}
214
- title="Restore this snapshot"
215
- disabled={restoringId !== null}
216
- >
217
- {restoringId === snap.id ? 'Restoring...' : 'Restore'}
218
- </button>
219
- <button
220
- type="button"
221
- class="snapshot-action-btn snapshot-action-delete"
222
- onClick={() => setConfirming({ id: snap.id, action: 'delete' })}
223
- title="Delete this snapshot"
224
- disabled={restoringId !== null}
225
- >
226
- {'\u2715'}
227
- </button>
228
- </>
229
- )}
156
+ {!loading &&
157
+ snapshots.map((snap) => (
158
+ <div key={snap.id} class="snapshot-item">
159
+ <div class="snapshot-item-info">
160
+ <span class="snapshot-item-name">{snap.name}</span>
161
+ <span class="snapshot-item-meta">
162
+ {snap.nodeCount} node{snap.nodeCount !== 1 ? 's' : ''}
163
+ {snap.edgeCount > 0 ? ` \u00b7 ${snap.edgeCount} edge${snap.edgeCount !== 1 ? 's' : ''}` : ''}
164
+ {' \u00b7 '}
165
+ {timeAgo(snap.createdAt)}
166
+ </span>
167
+ </div>
168
+ <div class="snapshot-item-actions">
169
+ {confirming?.id === snap.id ? (
170
+ <>
171
+ <button
172
+ type="button"
173
+ class={`snapshot-action-btn ${confirming.action === 'delete' ? 'snapshot-action-confirm' : 'snapshot-action-restore'}`}
174
+ onClick={() => (confirming.action === 'delete' ? handleDelete(snap.id) : handleRestore(snap.id))}
175
+ title={confirming.action === 'delete' ? 'Confirm delete' : 'Confirm restore'}
176
+ disabled={restoringId !== null}
177
+ >
178
+ {confirming.action === 'delete' ? 'Delete' : restoringId === snap.id ? 'Restoring...' : 'Confirm'}
179
+ </button>
180
+ <button
181
+ type="button"
182
+ class="snapshot-action-btn"
183
+ onClick={() => setConfirming(null)}
184
+ title="Cancel"
185
+ disabled={restoringId !== null}
186
+ >
187
+ Cancel
188
+ </button>
189
+ </>
190
+ ) : (
191
+ <>
192
+ <button
193
+ type="button"
194
+ class="snapshot-action-btn snapshot-action-restore"
195
+ onClick={() => setConfirming({ id: snap.id, action: 'restore' })}
196
+ title="Restore this snapshot"
197
+ disabled={restoringId !== null}
198
+ >
199
+ {restoringId === snap.id ? 'Restoring...' : 'Restore'}
200
+ </button>
201
+ <button
202
+ type="button"
203
+ class="snapshot-action-btn snapshot-action-delete"
204
+ onClick={() => setConfirming({ id: snap.id, action: 'delete' })}
205
+ title="Delete this snapshot"
206
+ disabled={restoringId !== null}
207
+ >
208
+ {'\u2715'}
209
+ </button>
210
+ </>
211
+ )}
212
+ </div>
230
213
  </div>
231
- </div>
232
- ))}
214
+ ))}
233
215
  </div>
234
216
  </div>
235
217
  );
@@ -19,11 +19,13 @@ export const AUTO_FIT_BODY_PADDING = 24;
19
19
  * height bridge (use-iframe-content-height) instead; webpage intentionally
20
20
  * scrolls. Excluding them from the DOM path is behaviour-neutral. */
21
21
  function isIframeNode(node: CanvasNodeState): boolean {
22
- return node.type === 'html'
23
- || node.type === 'json-render'
24
- || node.type === 'graph'
25
- || node.type === 'mcp-app'
26
- || node.type === 'webpage';
22
+ return (
23
+ node.type === 'html' ||
24
+ node.type === 'json-render' ||
25
+ node.type === 'graph' ||
26
+ node.type === 'mcp-app' ||
27
+ node.type === 'webpage'
28
+ );
27
29
  }
28
30
 
29
31
  /** Authored iframe surfaces whose content has a bounded natural height — they may
@@ -39,11 +41,13 @@ function isContentFitSurface(node: CanvasNodeState): boolean {
39
41
  /** Shared exemptions: never auto-size a node the user/agent has fixed or a node
40
42
  * whose height is controlled elsewhere. */
41
43
  function isAutoSizeExempt(node: CanvasNodeState): boolean {
42
- return node.collapsed === true
43
- || node.dockPosition != null
44
- || node.data.strictSize === true
45
- || node.data.userResized === true
46
- || node.type === 'group';
44
+ return (
45
+ node.collapsed === true ||
46
+ node.dockPosition != null ||
47
+ node.data.strictSize === true ||
48
+ node.data.userResized === true ||
49
+ node.type === 'group'
50
+ );
47
51
  }
48
52
 
49
53
  /** DOM-content nodes (markdown/status/file/…) whose body scrollHeight is directly
@@ -71,9 +75,6 @@ export function shouldContentFitIframeNode(node: CanvasNodeState): boolean {
71
75
  */
72
76
  export function computeContentGrowHeight(node: CanvasNodeState, contentHeight: number): number | null {
73
77
  if (!shouldContentFitIframeNode(node) || contentHeight <= 0) return null;
74
- const want = Math.min(
75
- contentHeight + AUTO_FIT_TITLEBAR_HEIGHT + AUTO_FIT_BODY_PADDING,
76
- AUTO_FIT_MAX_HEIGHT_IFRAME,
77
- );
78
+ const want = Math.min(contentHeight + AUTO_FIT_TITLEBAR_HEIGHT + AUTO_FIT_BODY_PADDING, AUTO_FIT_MAX_HEIGHT_IFRAME);
78
79
  return want > node.size.height + 8 ? want : null;
79
80
  }
@@ -19,8 +19,16 @@ export interface SnapResult {
19
19
  /** Active guide lines to render. Null when not dragging. */
20
20
  export const activeGuides = signal<GuideLine[] | null>(null);
21
21
 
22
- interface RefEdgeX { val: number; minY: number; maxY: number }
23
- interface RefEdgeY { val: number; minX: number; maxX: number }
22
+ interface RefEdgeX {
23
+ val: number;
24
+ minY: number;
25
+ maxY: number;
26
+ }
27
+ interface RefEdgeY {
28
+ val: number;
29
+ minX: number;
30
+ maxX: number;
31
+ }
24
32
 
25
33
  /** Cached reference edges — built once at drag-start, reused on every pointermove. */
26
34
  let cachedRefX: RefEdgeX[] = [];
@@ -37,10 +45,7 @@ function getChildIds(node: CanvasNodeState | undefined): string[] {
37
45
  return node.data.children.filter((childId): childId is string => typeof childId === 'string');
38
46
  }
39
47
 
40
- function collectExcludedReferenceIds(
41
- dragId: string,
42
- allNodes: CanvasNodeState[],
43
- ): Set<string> {
48
+ function collectExcludedReferenceIds(dragId: string, allNodes: CanvasNodeState[]): Set<string> {
44
49
  const nodeMap = new Map(allNodes.map((node) => [node.id, node]));
45
50
  const excluded = new Set<string>([dragId]);
46
51
  const dragNode = nodeMap.get(dragId);
@@ -105,12 +110,7 @@ export function clearSnapCache(): void {
105
110
  * Snap a dragging node's proposed position to cached reference edges.
106
111
  * Must call buildSnapCache() before the first call in a drag session.
107
112
  */
108
- export function snapToGuides(
109
- proposedX: number,
110
- proposedY: number,
111
- nodeW: number,
112
- nodeH: number,
113
- ): SnapResult {
113
+ export function snapToGuides(proposedX: number, proposedY: number, nodeW: number, nodeH: number): SnapResult {
114
114
  const dragEdgesX = [proposedX, proposedX + nodeW / 2, proposedX + nodeW];
115
115
  const dragEdgesY = [proposedY, proposedY + nodeH / 2, proposedY + nodeH];
116
116
  const offX = [0, nodeW / 2, nodeW];
@@ -41,11 +41,7 @@ export function useNodeResize({ nodeId, viewport, onResize, onResizeEnd }: NodeR
41
41
  const scale = viewport.value.scale;
42
42
  const dw = (pointer.x - startPointer.current.x) / scale;
43
43
  const dh = (pointer.y - startPointer.current.y) / scale;
44
- onResize(
45
- nodeId,
46
- Math.max(MIN_WIDTH, startSize.current.w + dw),
47
- Math.max(MIN_HEIGHT, startSize.current.h + dh),
48
- );
44
+ onResize(nodeId, Math.max(MIN_WIDTH, startSize.current.w + dw), Math.max(MIN_HEIGHT, startSize.current.h + dh));
49
45
  };
50
46
 
51
47
  const onPointerMove = (ev: PointerEvent) => {
@@ -31,15 +31,18 @@ export function usePanZoom({ viewport, onViewportChange, onViewportCommit, disab
31
31
  const lastPinchDist = useRef(0);
32
32
  const wheelCommitTimer = useRef<number | null>(null);
33
33
 
34
- const scheduleViewportCommit = useCallback((next: ViewportState) => {
35
- if (wheelCommitTimer.current !== null) {
36
- window.clearTimeout(wheelCommitTimer.current);
37
- }
38
- wheelCommitTimer.current = window.setTimeout(() => {
39
- wheelCommitTimer.current = null;
40
- onViewportCommit(next);
41
- }, 140);
42
- }, [onViewportCommit]);
34
+ const scheduleViewportCommit = useCallback(
35
+ (next: ViewportState) => {
36
+ if (wheelCommitTimer.current !== null) {
37
+ window.clearTimeout(wheelCommitTimer.current);
38
+ }
39
+ wheelCommitTimer.current = window.setTimeout(() => {
40
+ wheelCommitTimer.current = null;
41
+ onViewportCommit(next);
42
+ }, 140);
43
+ },
44
+ [onViewportCommit],
45
+ );
43
46
 
44
47
  const handleWheel = useCallback(
45
48
  (e: WheelEvent) => {
@@ -79,15 +82,18 @@ export function usePanZoom({ viewport, onViewportChange, onViewportCommit, disab
79
82
  [disabled, viewport, onViewportChange, scheduleViewportCommit],
80
83
  );
81
84
 
82
- const handlePointerDown = useCallback((e: PointerEvent) => {
83
- if (disabled) return;
84
- // Only pan when clicking the canvas background (not nodes)
85
- const container = containerRef.current;
86
- if (!container || e.target !== container) return;
87
- isPanning.current = true;
88
- lastPointer.current = { x: e.clientX, y: e.clientY };
89
- container.setPointerCapture(e.pointerId);
90
- }, [disabled]);
85
+ const handlePointerDown = useCallback(
86
+ (e: PointerEvent) => {
87
+ if (disabled) return;
88
+ // Only pan when clicking the canvas background (not nodes)
89
+ const container = containerRef.current;
90
+ if (!container || e.target !== container) return;
91
+ isPanning.current = true;
92
+ lastPointer.current = { x: e.clientX, y: e.clientY };
93
+ container.setPointerCapture(e.pointerId);
94
+ },
95
+ [disabled],
96
+ );
91
97
 
92
98
  const handlePointerMove = useCallback(
93
99
  (e: PointerEvent) => {
@@ -183,14 +189,7 @@ export function usePanZoom({ viewport, onViewportChange, onViewportCommit, disab
183
189
  el.removeEventListener('touchmove', handleTouchMove);
184
190
  el.removeEventListener('touchend', handleTouchEnd);
185
191
  };
186
- }, [
187
- handleWheel,
188
- handlePointerDown,
189
- handlePointerMove,
190
- handlePointerUp,
191
- handleTouchMove,
192
- handleTouchEnd,
193
- ]);
192
+ }, [handleWheel, handlePointerDown, handlePointerMove, handlePointerUp, handleTouchMove, handleTouchEnd]);
194
193
 
195
194
  return containerRef;
196
195
  }
@@ -32,10 +32,7 @@ interface JsonRpcErrorResponse extends JsonRpcBase {
32
32
  }
33
33
 
34
34
  type JsonRpcResponseMessage = JsonRpcSuccessResponse | JsonRpcErrorResponse;
35
- type JsonRpcMessage =
36
- | JsonRpcRequestMessage
37
- | JsonRpcNotificationMessage
38
- | JsonRpcResponseMessage;
35
+ type JsonRpcMessage = JsonRpcRequestMessage | JsonRpcNotificationMessage | JsonRpcResponseMessage;
39
36
 
40
37
  type DisplayMode = 'inline' | 'fullscreen' | 'pip';
41
38
 
@@ -256,10 +253,7 @@ export class AppBridge {
256
253
  this.notificationHandlers.set(method, (params) => callback(params as TParams));
257
254
  }
258
255
 
259
- private setRequestHandler<TParams, TResult>(
260
- method: string,
261
- callback: RequestHandler<TParams, TResult>,
262
- ): void {
256
+ private setRequestHandler<TParams, TResult>(method: string, callback: RequestHandler<TParams, TResult>): void {
263
257
  this.requestHandlers.set(method, (params, extra) => callback(params as TParams, extra));
264
258
  }
265
259
 
@@ -447,10 +441,7 @@ export class AppBridge {
447
441
  }
448
442
  }
449
443
 
450
- private async handleRequest(
451
- message: JsonRpcRequestMessage,
452
- extra: RequestExtra,
453
- ): Promise<unknown> {
444
+ private async handleRequest(message: JsonRpcRequestMessage, extra: RequestExtra): Promise<unknown> {
454
445
  if (message.method === 'ui/initialize') {
455
446
  return this.handleInitialize(message.params);
456
447
  }
@@ -5,7 +5,13 @@ interface IconProps {
5
5
  class?: string;
6
6
  }
7
7
 
8
- const defaults = { fill: 'none', stroke: 'currentColor', 'stroke-width': '1.5', 'stroke-linecap': 'round', 'stroke-linejoin': 'round' } as const;
8
+ const defaults = {
9
+ fill: 'none',
10
+ stroke: 'currentColor',
11
+ 'stroke-width': '1.5',
12
+ 'stroke-linecap': 'round',
13
+ 'stroke-linejoin': 'round',
14
+ } as const;
9
15
 
10
16
  function Icon({ size = 16, children, ...rest }: IconProps & { children: JSX.Element | JSX.Element[] }): JSX.Element {
11
17
  return (
@@ -240,8 +246,28 @@ export function IconLogo({ size = 22, class: className }: IconProps): JSX.Elemen
240
246
  class={className}
241
247
  aria-hidden="true"
242
248
  >
243
- <rect x="8" y="8" width="48" height="48" rx="7" fill="none" stroke="currentColor" stroke-width="2.2" opacity="0.35" />
244
- <rect x="16" y="16" width="32" height="32" rx="5" fill="none" stroke="currentColor" stroke-width="2.2" opacity="0.6" />
249
+ <rect
250
+ x="8"
251
+ y="8"
252
+ width="48"
253
+ height="48"
254
+ rx="7"
255
+ fill="none"
256
+ stroke="currentColor"
257
+ stroke-width="2.2"
258
+ opacity="0.35"
259
+ />
260
+ <rect
261
+ x="16"
262
+ y="16"
263
+ width="32"
264
+ height="32"
265
+ rx="5"
266
+ fill="none"
267
+ stroke="currentColor"
268
+ stroke-width="2.2"
269
+ opacity="0.6"
270
+ />
245
271
  <rect x="24" y="24" width="16" height="16" rx="3" fill="none" stroke="currentColor" stroke-width="2.2" />
246
272
  <rect x="29" y="29" width="6" height="6" rx="1" fill="currentColor" />
247
273
  </svg>
@@ -439,22 +465,39 @@ export function IconNodeGraph(p: IconProps): JSX.Element {
439
465
  /** Map a node type → its Focus Field icon component. */
440
466
  export function getNodeIcon(type: string): (p: IconProps) => JSX.Element {
441
467
  switch (type) {
442
- case 'markdown': return IconNodeMarkdown;
443
- case 'prompt': return IconNodePrompt;
444
- case 'response': return IconNodeResponse;
445
- case 'file': return IconNodeFile;
446
- case 'image': return IconNodeImage;
447
- case 'webpage': return IconNodeWebpage;
448
- case 'context': return IconNodeContext;
449
- case 'group': return IconNodeGroup;
450
- case 'status': return IconNodeStatus;
451
- case 'trace': return IconNodeTrace;
452
- case 'ledger': return IconNodeLedger;
453
- case 'mcp-app': return IconNodeMcpApp;
454
- case 'ext-app': return IconNodeExtApp;
455
- case 'json-render': return IconNodeJsonRender;
456
- case 'graph': return IconNodeGraph;
457
- case 'html': return IconNodeWebpage;
458
- default: return IconNodeMarkdown;
468
+ case 'markdown':
469
+ return IconNodeMarkdown;
470
+ case 'prompt':
471
+ return IconNodePrompt;
472
+ case 'response':
473
+ return IconNodeResponse;
474
+ case 'file':
475
+ return IconNodeFile;
476
+ case 'image':
477
+ return IconNodeImage;
478
+ case 'webpage':
479
+ return IconNodeWebpage;
480
+ case 'context':
481
+ return IconNodeContext;
482
+ case 'group':
483
+ return IconNodeGroup;
484
+ case 'status':
485
+ return IconNodeStatus;
486
+ case 'trace':
487
+ return IconNodeTrace;
488
+ case 'ledger':
489
+ return IconNodeLedger;
490
+ case 'mcp-app':
491
+ return IconNodeMcpApp;
492
+ case 'ext-app':
493
+ return IconNodeExtApp;
494
+ case 'json-render':
495
+ return IconNodeJsonRender;
496
+ case 'graph':
497
+ return IconNodeGraph;
498
+ case 'html':
499
+ return IconNodeWebpage;
500
+ default:
501
+ return IconNodeMarkdown;
459
502
  }
460
503
  }
@@ -82,9 +82,7 @@ function stripHtmlToText(value: string): string {
82
82
  .trim();
83
83
  }
84
84
 
85
- export function normalizeContextNodeFallback(
86
- nodeData: Record<string, unknown>,
87
- ): ContextNodeFallbackDisplay | null {
85
+ export function normalizeContextNodeFallback(nodeData: Record<string, unknown>): ContextNodeFallbackDisplay | null {
88
86
  const title = asTrimmedString(nodeData.title);
89
87
  const content = asTrimmedString(nodeData.content);
90
88
  const rendered = stripHtmlToText(asTrimmedString(nodeData.rendered));
@@ -135,24 +133,23 @@ export function ContextNode({
135
133
  node,
136
134
  expanded = false,
137
135
  pinnedNodes = [],
138
- }: { node: CanvasNodeState; expanded?: boolean; pinnedNodes?: CanvasNodeState[] }) {
136
+ }: {
137
+ node: CanvasNodeState;
138
+ expanded?: boolean;
139
+ pinnedNodes?: CanvasNodeState[];
140
+ }) {
139
141
  const cards = (node.data.cards as ContextCard[]) ?? [];
140
142
  const auxTabs = (node.data.auxTabs as Array<{ id: string; url: string; reason?: string }>) ?? [];
141
143
  const pinnedContext = pinnedNodes.map(normalizePinnedContextDisplay);
142
144
  const hasPinnedContext = pinnedContext.length > 0;
143
- const currentTokens =
144
- typeof node.data.currentTokens === 'number' ? node.data.currentTokens : null;
145
+ const currentTokens = typeof node.data.currentTokens === 'number' ? node.data.currentTokens : null;
145
146
  const tokenLimit = typeof node.data.tokenLimit === 'number' ? node.data.tokenLimit : null;
146
147
  const utilization = typeof node.data.utilization === 'number' ? node.data.utilization : null;
147
- const messagesLength =
148
- typeof node.data.messagesLength === 'number' ? node.data.messagesLength : null;
149
- const percent =
150
- utilization !== null ? Math.max(0, Math.min(100, Math.round(utilization * 100))) : null;
148
+ const messagesLength = typeof node.data.messagesLength === 'number' ? node.data.messagesLength : null;
149
+ const percent = utilization !== null ? Math.max(0, Math.min(100, Math.round(utilization * 100))) : null;
151
150
  const barColor = usageBarColor(utilization ?? 0);
152
151
  const fallback =
153
- !hasPinnedContext && cards.length === 0 && auxTabs.length === 0
154
- ? normalizeContextNodeFallback(node.data)
155
- : null;
152
+ !hasPinnedContext && cards.length === 0 && auxTabs.length === 0 ? normalizeContextNodeFallback(node.data) : null;
156
153
 
157
154
  const openCard = async (card: ContextCard): Promise<void> => {
158
155
  const path = typeof card.path === 'string' ? card.path.trim() : '';
@@ -254,9 +251,7 @@ export function ContextNode({
254
251
  borderLeft: '2px solid var(--c-accent)',
255
252
  }}
256
253
  >
257
- <div style={{ fontWeight: 600, color: 'var(--c-text)', marginBottom: '2px' }}>
258
- {display.title}
259
- </div>
254
+ <div style={{ fontWeight: 600, color: 'var(--c-text)', marginBottom: '2px' }}>{display.title}</div>
260
255
  {display.summary && (
261
256
  <div
262
257
  style={{
@@ -347,9 +342,7 @@ export function ContextNode({
347
342
  borderLeft: '2px solid var(--c-accent)',
348
343
  }}
349
344
  >
350
- <div style={{ fontWeight: 600, color: 'var(--c-text)', marginBottom: '2px' }}>
351
- {display.title}
352
- </div>
345
+ <div style={{ fontWeight: 600, color: 'var(--c-text)', marginBottom: '2px' }}>{display.title}</div>
353
346
  <div
354
347
  style={{
355
348
  color: 'var(--c-muted)',
@@ -478,9 +471,7 @@ export function ContextNode({
478
471
  >
479
472
  {tab.url}
480
473
  {tab.reason && (
481
- <span style={{ color: 'var(--c-dim)', fontSize: '10px', marginLeft: '6px' }}>
482
- ({tab.reason})
483
- </span>
474
+ <span style={{ color: 'var(--c-dim)', fontSize: '10px', marginLeft: '6px' }}>({tab.reason})</span>
484
475
  )}
485
476
  </a>
486
477
  ))}
@@ -496,9 +487,7 @@ export function ContextNode({
496
487
  borderLeft: '2px solid var(--c-accent)',
497
488
  }}
498
489
  >
499
- <div style={{ fontWeight: 600, color: 'var(--c-text)', marginBottom: '4px' }}>
500
- {fallback.title}
501
- </div>
490
+ <div style={{ fontWeight: 600, color: 'var(--c-text)', marginBottom: '4px' }}>{fallback.title}</div>
502
491
  {fallback.summary && (
503
492
  <div
504
493
  style={{