@xdelivered/emberflow 0.2.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) hide show
  1. package/bin/commands.ts +7 -3
  2. package/bin/init.test.ts +94 -0
  3. package/bin/init.ts +108 -2
  4. package/dist/bin/commands.d.ts +1 -0
  5. package/dist/bin/commands.js +6 -3
  6. package/dist/bin/init.d.ts +4 -0
  7. package/dist/bin/init.js +96 -2
  8. package/dist/server/agents/codexParse.js +31 -0
  9. package/dist/server/agents/detect.d.ts +18 -8
  10. package/dist/server/agents/detect.js +78 -13
  11. package/dist/server/agents/modelRejectionHint.js +1 -1
  12. package/dist/server/agents/prompt.d.ts +15 -1
  13. package/dist/server/agents/prompt.js +59 -37
  14. package/dist/server/agents/runManager.d.ts +23 -2
  15. package/dist/server/agents/runManager.js +36 -8
  16. package/dist/server/client.d.ts +1 -0
  17. package/dist/server/client.js +7 -2
  18. package/dist/server/index.js +208 -16
  19. package/dist/server/nodesPayload.d.ts +14 -1
  20. package/dist/server/nodesPayload.js +15 -2
  21. package/dist/server/projectMode.js +5 -2
  22. package/dist/server/runRegistry.d.ts +52 -1
  23. package/dist/server/runRegistry.js +170 -1
  24. package/dist/server/sourceNav.d.ts +77 -0
  25. package/dist/server/sourceNav.js +503 -0
  26. package/dist/server/subflowRunner.d.ts +48 -1
  27. package/dist/server/subflowRunner.js +34 -7
  28. package/dist/server/updateCheck.d.ts +68 -0
  29. package/dist/server/updateCheck.js +142 -0
  30. package/dist/src/engine/executor.js +4 -3
  31. package/dist/src/engine/registry.d.ts +27 -2
  32. package/dist/src/engine/registry.js +84 -2
  33. package/dist/src/nodes/index.d.ts +8 -2
  34. package/dist/src/nodes/index.js +7 -3
  35. package/dist/src/nodes/login.d.ts +3 -1
  36. package/dist/src/nodes/login.js +2 -2
  37. package/package.json +28 -7
  38. package/server/agentRoute.test.ts +20 -0
  39. package/server/agents/codexParse.test.ts +37 -0
  40. package/server/agents/codexParse.ts +34 -0
  41. package/server/agents/detect.test.ts +26 -7
  42. package/server/agents/detect.ts +82 -14
  43. package/server/agents/modelRejectionHint.ts +2 -1
  44. package/server/agents/prompt.test.ts +128 -0
  45. package/server/agents/prompt.ts +102 -3
  46. package/server/agents/runManager.test.ts +9 -0
  47. package/server/agents/runManager.ts +37 -7
  48. package/server/client.ts +10 -4
  49. package/server/index.ts +213 -15
  50. package/server/nodeRun.test.ts +189 -0
  51. package/server/nodesPayload.test.ts +38 -0
  52. package/server/nodesPayload.ts +28 -3
  53. package/server/projectMode.ts +5 -2
  54. package/server/runRegistry.ts +200 -3
  55. package/server/setupStatus.test.ts +3 -0
  56. package/server/sourceNav.test.ts +382 -0
  57. package/server/sourceNav.ts +644 -0
  58. package/server/sourceNavRoute.test.ts +163 -0
  59. package/server/stepDrill.test.ts +380 -0
  60. package/server/subflowRunner.ts +92 -11
  61. package/server/updateCheck.test.ts +209 -0
  62. package/server/updateCheck.ts +175 -0
  63. package/server/workflowTestRoute.test.ts +1 -1
  64. package/src/App.tsx +82 -2
  65. package/src/components/AgentConsole.tsx +7 -280
  66. package/src/components/AgentStream.test.tsx +88 -0
  67. package/src/components/AgentStream.tsx +303 -0
  68. package/src/components/CreateModal.tsx +43 -9
  69. package/src/components/Dock.tsx +1 -1
  70. package/src/components/EmptyState.test.tsx +49 -0
  71. package/src/components/EmptyState.tsx +61 -0
  72. package/src/components/EnvironmentDialog.tsx +4 -1
  73. package/src/components/EnvironmentPicker.tsx +8 -3
  74. package/src/components/EnvironmentsDialog.tsx +4 -1
  75. package/src/components/InfraPanel.tsx +30 -2
  76. package/src/components/InfrastructureDialog.test.tsx +97 -0
  77. package/src/components/InfrastructureDialog.tsx +111 -0
  78. package/src/components/Inspector.tsx +9 -26
  79. package/src/components/RunbookView.tsx +70 -6
  80. package/src/components/SettingsDialog.tsx +4 -59
  81. package/src/components/Sidebar.tsx +6 -26
  82. package/src/components/SourceNavigator.test.tsx +226 -0
  83. package/src/components/SourceNavigator.tsx +520 -0
  84. package/src/components/StatusBar.tsx +227 -26
  85. package/src/components/Toolbar.tsx +28 -16
  86. package/src/components/UpdateChip.test.tsx +31 -0
  87. package/src/components/WelcomeDialog.test.tsx +384 -13
  88. package/src/components/WelcomeDialog.tsx +996 -177
  89. package/src/engine/executor.ts +4 -3
  90. package/src/engine/registry.sourceRef.test.ts +112 -0
  91. package/src/engine/registry.ts +103 -2
  92. package/src/lib/guidedQuestions.test.ts +224 -0
  93. package/src/lib/guidedQuestions.ts +181 -0
  94. package/src/lib/runbookModel.ts +3 -3
  95. package/src/lib/runbookProjection.test.ts +43 -0
  96. package/src/lib/runbookProjection.ts +26 -6
  97. package/src/nodes/index.ts +10 -3
  98. package/src/nodes/login.ts +5 -2
  99. package/src/store/agentClient.ts +27 -8
  100. package/src/store/apiTree.ts +12 -0
  101. package/src/store/builderStore.test.ts +441 -99
  102. package/src/store/builderStore.ts +385 -314
  103. package/src/store/nodeMeta.ts +10 -2
  104. package/src/store/serverRunner.ts +56 -5
  105. package/src/store/setupClient.ts +7 -2
  106. package/src/store/sourceNavClient.ts +48 -0
  107. package/src/store/updateClient.ts +50 -0
  108. package/studio-dist/assets/index-BbzppDpt.js +65 -0
  109. package/studio-dist/assets/index-CLf6blcA.css +1 -0
  110. package/studio-dist/index.html +2 -2
  111. package/templates/skills/emberflow-basics/SKILL.md +29 -1
  112. package/templates/skills/emberflow-model-process/SKILL.md +92 -9
  113. package/templates/skills/emberflow-new-workflow/SKILL.md +8 -1
  114. package/templates/skills/emberflow-review-workflow/SKILL.md +64 -1
  115. package/studio-dist/assets/index-DNJwW-hM.css +0 -1
  116. package/studio-dist/assets/index-O26dKRjW.js +0 -65
@@ -0,0 +1,520 @@
1
+ /**
2
+ * Source-reference navigation for node implementations (spec:
3
+ * docs/superpowers/specs/2026-07-14-source-reference-navigation.md).
4
+ *
5
+ * When a node carries a sourceRef, the Inspector's code dialog renders this
6
+ * navigator instead of the plain toString() view: the whole registering file,
7
+ * scrolled to the register() line, with identifiers that resolve to
8
+ * project-owned files rendered as clickable links (custom
9
+ * react-syntax-highlighter renderer). Clicking pushes the target file onto a
10
+ * breadcrumb stack; a "Referenced code" panel below lists every import with a
11
+ * badge — project (link) / external / builtin / unresolved (reason) — so no
12
+ * referenced code is ever silently hidden.
13
+ */
14
+ import { useEffect, useId, useRef, useState } from 'react';
15
+ import type { CSSProperties, ElementType, ReactNode } from 'react';
16
+ import { Prism as SyntaxHighlighter, createElement } from 'react-syntax-highlighter';
17
+ import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism';
18
+ import { Badge } from '@/components/ui/badge';
19
+ import {
20
+ fetchSourceFile,
21
+ type Resolution,
22
+ type SourceFileFetchResult,
23
+ type SourceFilePayload,
24
+ type SourceImport,
25
+ type SourceReexport,
26
+ } from '../store/sourceNavClient';
27
+
28
+ // ---------------------------------------------------------------------------
29
+ // Shared highlighter chrome — identical to the Inspector's historical setup,
30
+ // so the sourceRef-less fallback view is pixel-for-pixel today's view.
31
+ // ---------------------------------------------------------------------------
32
+
33
+ const CODE_CUSTOM_STYLE: CSSProperties = {
34
+ margin: 0,
35
+ background: 'transparent',
36
+ fontSize: '12.5px',
37
+ lineHeight: 1.6,
38
+ padding: '12px 2px',
39
+ };
40
+
41
+ const CODE_TAG_PROPS = { style: { background: 'transparent', textShadow: 'none' } as CSSProperties };
42
+
43
+ const LINE_NUMBER_STYLE: CSSProperties = {
44
+ color: 'var(--muted-foreground)',
45
+ opacity: 0.35,
46
+ background: 'transparent',
47
+ minWidth: '2.75em',
48
+ paddingRight: '1em',
49
+ };
50
+
51
+ // ---------------------------------------------------------------------------
52
+ // Token linking: a custom renderer walks the highlighter's per-line rows and
53
+ // wraps whole-identifier text matches in link buttons.
54
+ // ---------------------------------------------------------------------------
55
+
56
+ /** Structural supertype of react-syntax-highlighter's (unexported) rendererNode. */
57
+ interface HastNode {
58
+ type: 'element' | 'text';
59
+ value?: string | number;
60
+ tagName?: ElementType;
61
+ properties?: { className: unknown[]; [key: string]: unknown };
62
+ children?: HastNode[];
63
+ }
64
+
65
+ interface RendererArgs {
66
+ rows: HastNode[];
67
+ stylesheet: { [key: string]: CSSProperties };
68
+ useInlineStyles: boolean;
69
+ }
70
+
71
+ /** What clicking an identifier does: open another file, or jump within this one. */
72
+ type LinkTarget = { kind: 'file'; path: string; line?: number } | { kind: 'line'; line: number };
73
+
74
+ const LINK_STYLE: CSSProperties = {
75
+ background: 'none',
76
+ border: 'none',
77
+ padding: 0,
78
+ margin: 0,
79
+ font: 'inherit',
80
+ color: 'inherit',
81
+ cursor: 'pointer',
82
+ textDecoration: 'underline',
83
+ textDecorationStyle: 'dotted',
84
+ textUnderlineOffset: '2px',
85
+ };
86
+
87
+ const IDENTIFIER = /[A-Za-z_$][A-Za-z0-9_$]*/g;
88
+
89
+ /** Split a text node into text pieces + link buttons for whole-identifier matches. */
90
+ function linkifyText(
91
+ value: string,
92
+ targets: Map<string, LinkTarget>,
93
+ navigate: (target: LinkTarget) => void,
94
+ ): HastNode[] {
95
+ const out: HastNode[] = [];
96
+ let cursor = 0;
97
+ for (const match of value.matchAll(IDENTIFIER)) {
98
+ const target = targets.get(match[0]);
99
+ if (!target) continue;
100
+ if (match.index > cursor) out.push({ type: 'text', value: value.slice(cursor, match.index) });
101
+ out.push({
102
+ type: 'element',
103
+ tagName: 'button',
104
+ properties: {
105
+ className: ['source-nav-link'],
106
+ type: 'button',
107
+ style: LINK_STYLE,
108
+ title:
109
+ target.kind === 'file'
110
+ ? `Open ${target.path}${target.line !== undefined ? `:${target.line}` : ''}`
111
+ : `Jump to line ${target.line}`,
112
+ onClick: () => navigate(target),
113
+ },
114
+ children: [{ type: 'text', value: match[0] }],
115
+ });
116
+ cursor = match.index + match[0].length;
117
+ }
118
+ if (out.length === 0) return [{ type: 'text', value }];
119
+ if (cursor < value.length) out.push({ type: 'text', value: value.slice(cursor) });
120
+ return out;
121
+ }
122
+
123
+ function isLineNumberNode(node: HastNode): boolean {
124
+ const classes = node.properties?.className ?? [];
125
+ return classes.some(
126
+ (c) => c === 'linenumber' || c === 'react-syntax-highlighter-line-number',
127
+ );
128
+ }
129
+
130
+ function linkifyNode(
131
+ node: HastNode,
132
+ targets: Map<string, LinkTarget>,
133
+ navigate: (target: LinkTarget) => void,
134
+ ): HastNode[] {
135
+ if (node.type === 'text') {
136
+ return linkifyText(String(node.value ?? ''), targets, navigate);
137
+ }
138
+ if (isLineNumberNode(node)) return [node];
139
+ return [
140
+ {
141
+ ...node,
142
+ children: (node.children ?? []).flatMap((child) => linkifyNode(child, targets, navigate)),
143
+ },
144
+ ];
145
+ }
146
+
147
+ /** DOM id for a 1-based line within one navigator instance (auto-scroll anchor). */
148
+ function lineId(uid: string, line: number): string {
149
+ return `${uid}-L${line}`;
150
+ }
151
+
152
+ // ---------------------------------------------------------------------------
153
+ // Referenced-code panel
154
+ // ---------------------------------------------------------------------------
155
+
156
+ function ResolutionRow({
157
+ label,
158
+ from,
159
+ resolution,
160
+ onOpen,
161
+ }: {
162
+ label: string;
163
+ from: string;
164
+ resolution: Resolution;
165
+ onOpen: (path: string, line?: number) => void;
166
+ }) {
167
+ return (
168
+ <li className="flex flex-wrap items-center gap-2 py-1 text-[12px]">
169
+ {resolution.kind === 'project' && (
170
+ <>
171
+ <Badge variant="highlight">project</Badge>
172
+ <span className="font-mono">{label}</span>
173
+ <button
174
+ type="button"
175
+ className="font-mono text-highlight underline decoration-dotted underline-offset-2 hover:decoration-solid"
176
+ onClick={() => onOpen(resolution.path, resolution.line)}
177
+ >
178
+ {resolution.path}
179
+ {resolution.line !== undefined ? `:${resolution.line}` : ''}
180
+ </button>
181
+ </>
182
+ )}
183
+ {resolution.kind === 'external' && (
184
+ <>
185
+ <Badge variant="outline">external</Badge>
186
+ <span className="font-mono text-muted-foreground">{label}</span>
187
+ <span className="text-muted-foreground">package: {resolution.package}</span>
188
+ </>
189
+ )}
190
+ {resolution.kind === 'builtin' && (
191
+ <>
192
+ <Badge variant="outline">builtin</Badge>
193
+ <span className="font-mono text-muted-foreground">{label}</span>
194
+ <span className="text-muted-foreground">Node.js builtin</span>
195
+ </>
196
+ )}
197
+ {resolution.kind === 'unresolved' && (
198
+ <>
199
+ <Badge variant="destructive">unresolved</Badge>
200
+ <span className="font-mono">{label}</span>
201
+ <span className="text-destructive-foreground">unresolved — {resolution.reason}</span>
202
+ </>
203
+ )}
204
+ <span className="ml-auto font-mono text-[11px] text-muted-foreground/70">{from}</span>
205
+ </li>
206
+ );
207
+ }
208
+
209
+ /** Display label for an import row; side-effect imports have no local binding. */
210
+ function importLabel(entry: SourceImport): string {
211
+ if (entry.local) return entry.local;
212
+ if (entry.name === '*') return '(side-effect import)';
213
+ return entry.name;
214
+ }
215
+
216
+ function ReferencedCodePanel({
217
+ imports,
218
+ reexports,
219
+ onOpen,
220
+ }: {
221
+ imports: SourceImport[];
222
+ reexports: SourceReexport[];
223
+ onOpen: (path: string, line?: number) => void;
224
+ }) {
225
+ if (imports.length === 0 && reexports.length === 0) return null;
226
+ return (
227
+ <div className="mt-2 rounded-md border border-border bg-background p-3">
228
+ <div className="mb-1 text-[11px] font-medium uppercase tracking-wider text-muted-foreground">
229
+ Referenced code
230
+ </div>
231
+ <ul className="divide-y divide-border/50">
232
+ {imports.map((entry, i) => (
233
+ <ResolutionRow
234
+ key={`import-${i}`}
235
+ label={importLabel(entry)}
236
+ from={entry.from}
237
+ resolution={entry.resolution}
238
+ onOpen={onOpen}
239
+ />
240
+ ))}
241
+ {reexports.map((entry, i) => (
242
+ <ResolutionRow
243
+ key={`reexport-${i}`}
244
+ label={`export ${entry.name}`}
245
+ from={entry.from}
246
+ resolution={entry.resolution}
247
+ onOpen={onOpen}
248
+ />
249
+ ))}
250
+ </ul>
251
+ </div>
252
+ );
253
+ }
254
+
255
+ // ---------------------------------------------------------------------------
256
+ // SourceNavigator
257
+ // ---------------------------------------------------------------------------
258
+
259
+ interface Crumb {
260
+ path: string;
261
+ line?: number;
262
+ }
263
+
264
+ export interface SourceNavigatorProps {
265
+ /** Repo-relative path of the node's registering module. */
266
+ entryFile: string;
267
+ /** 1-based line of the register() call, when captured. */
268
+ entryLine?: number;
269
+ /** The node type being inspected (context for headers/errors). */
270
+ nodeType: string;
271
+ /** Test seam: overrides the /source-file client. */
272
+ fetcher?: (path: string) => Promise<SourceFileFetchResult>;
273
+ /** Test seam: start at a deeper breadcrumb stack. */
274
+ initialStack?: Crumb[];
275
+ /** Test seam: pre-populate the per-file fetch cache (path → result). */
276
+ initialFiles?: Record<string, SourceFileFetchResult>;
277
+ }
278
+
279
+ function basename(path: string): string {
280
+ const i = path.lastIndexOf('/');
281
+ return i === -1 ? path : path.slice(i + 1);
282
+ }
283
+
284
+ export function SourceNavigator({
285
+ entryFile,
286
+ entryLine,
287
+ nodeType,
288
+ fetcher = fetchSourceFile,
289
+ initialStack,
290
+ initialFiles,
291
+ }: SourceNavigatorProps) {
292
+ const uid = useId();
293
+ const [stack, setStack] = useState<Crumb[]>(
294
+ () => initialStack ?? [{ path: entryFile, line: entryLine }],
295
+ );
296
+ // Per-file fetch cache: repeated and circular visits hit the cache — safe by
297
+ // construction (flat per-file views, navigation is just a stack push).
298
+ const [files, setFiles] = useState<Record<string, SourceFileFetchResult>>(
299
+ () => initialFiles ?? {},
300
+ );
301
+ const containerRef = useRef<HTMLDivElement>(null);
302
+
303
+ const current = stack[stack.length - 1];
304
+ const result = files[current.path];
305
+
306
+ useEffect(() => {
307
+ if (files[current.path]) return;
308
+ let cancelled = false;
309
+ void fetcher(current.path).then((res) => {
310
+ if (!cancelled) setFiles((prev) => ({ ...prev, [current.path]: res }));
311
+ });
312
+ return () => {
313
+ cancelled = true;
314
+ };
315
+ // eslint-disable-next-line react-hooks/exhaustive-deps
316
+ }, [current.path, files[current.path] !== undefined]);
317
+
318
+ // Auto-scroll to the crumb's line once its content is on screen.
319
+ useEffect(() => {
320
+ if (!result?.ok) return;
321
+ if (current.line === undefined) {
322
+ containerRef.current?.scrollTo?.({ top: 0 });
323
+ return;
324
+ }
325
+ scrollToLine(current.line);
326
+ // eslint-disable-next-line react-hooks/exhaustive-deps
327
+ }, [current.path, current.line, result?.ok === true, stack.length]);
328
+
329
+ function scrollToLine(line: number) {
330
+ const el = containerRef.current?.querySelector?.(`[id="${lineId(uid, line)}"]`);
331
+ if (el && 'scrollIntoView' in el) (el as HTMLElement).scrollIntoView({ block: 'center' });
332
+ }
333
+
334
+ function push(path: string, line?: number) {
335
+ if (path === current.path && line !== undefined) {
336
+ // Same-file navigation is a scroll, not a crumb.
337
+ scrollToLine(line);
338
+ return;
339
+ }
340
+ setStack((prev) => [...prev, { path, line }]);
341
+ }
342
+
343
+ function jumpTo(depth: number) {
344
+ setStack((prev) => prev.slice(0, depth + 1));
345
+ }
346
+
347
+ function back() {
348
+ setStack((prev) => (prev.length > 1 ? prev.slice(0, -1) : prev));
349
+ }
350
+
351
+ const payload: SourceFilePayload | undefined = result?.ok ? result.payload : undefined;
352
+
353
+ // Identifier → action map for the current file: project-resolved import
354
+ // locals open the target file; local declarations scroll within this one.
355
+ const targets = new Map<string, LinkTarget>();
356
+ if (payload) {
357
+ for (const decl of payload.symbols.declarations) {
358
+ targets.set(decl.name, { kind: 'line', line: decl.line });
359
+ }
360
+ for (const entry of payload.symbols.imports) {
361
+ if (entry.local && entry.resolution.kind === 'project') {
362
+ targets.set(entry.local, {
363
+ kind: 'file',
364
+ path: entry.resolution.path,
365
+ line: entry.resolution.line,
366
+ });
367
+ }
368
+ }
369
+ }
370
+
371
+ const navigate = (target: LinkTarget) => {
372
+ if (target.kind === 'file') push(target.path, target.line);
373
+ else scrollToLine(target.line);
374
+ };
375
+
376
+ const renderer = ({ rows, stylesheet, useInlineStyles }: RendererArgs): ReactNode =>
377
+ rows.map((row, i) => {
378
+ const transformed: HastNode = {
379
+ ...row,
380
+ properties: { ...(row.properties ?? { className: [] }), id: lineId(uid, i + 1) },
381
+ children: (row.children ?? []).flatMap((child) => linkifyNode(child, targets, navigate)),
382
+ };
383
+ return createElement({
384
+ node: transformed as Parameters<typeof createElement>[0]['node'],
385
+ stylesheet,
386
+ useInlineStyles,
387
+ key: `source-row-${i}`,
388
+ });
389
+ });
390
+
391
+ return (
392
+ <div className="flex min-h-0 flex-1 flex-col" data-node-type={nodeType}>
393
+ <style>{`.source-nav-link:hover { color: var(--highlight) !important; text-decoration-style: solid !important; }`}</style>
394
+
395
+ {/* Breadcrumb header */}
396
+ <div className="mb-2 flex items-center gap-1.5 text-[12px]">
397
+ {stack.length > 1 && (
398
+ <button
399
+ type="button"
400
+ className="mr-1 rounded-sm border border-border px-1.5 py-0.5 text-[11px] text-muted-foreground hover:text-foreground"
401
+ onClick={back}
402
+ >
403
+ ← Back
404
+ </button>
405
+ )}
406
+ {stack.map((crumb, depth) => {
407
+ const last = depth === stack.length - 1;
408
+ return (
409
+ <span key={`${crumb.path}-${depth}`} className="flex items-center gap-1.5">
410
+ {depth > 0 && <span className="text-muted-foreground/60">›</span>}
411
+ {last ? (
412
+ <span className="font-mono font-semibold" title={crumb.path}>
413
+ {basename(crumb.path)}
414
+ </span>
415
+ ) : (
416
+ <button
417
+ type="button"
418
+ className="font-mono text-muted-foreground underline decoration-dotted underline-offset-2 hover:text-highlight"
419
+ title={crumb.path}
420
+ onClick={() => jumpTo(depth)}
421
+ >
422
+ {basename(crumb.path)}
423
+ </button>
424
+ )}
425
+ </span>
426
+ );
427
+ })}
428
+ </div>
429
+
430
+ {payload?.resolver === 'unavailable' && (
431
+ <div className="mb-2 rounded-md border border-border bg-tertiary px-3 py-2 text-[12px] text-muted-foreground">
432
+ Symbol resolution unavailable (typescript not installed) — references below may be
433
+ incomplete
434
+ </div>
435
+ )}
436
+
437
+ <div
438
+ ref={containerRef}
439
+ className="min-h-0 flex-1 overflow-auto rounded-md border border-border bg-background"
440
+ >
441
+ {result === undefined && (
442
+ <div className="p-4 text-[13px] text-muted-foreground">Loading source…</div>
443
+ )}
444
+ {result !== undefined && !result.ok && (
445
+ <div className="p-4 text-[13px] text-destructive-foreground">{result.error}</div>
446
+ )}
447
+ {payload && (
448
+ <SyntaxHighlighter
449
+ language={payload.language === 'ts' ? 'typescript' : 'javascript'}
450
+ style={vscDarkPlus}
451
+ showLineNumbers
452
+ customStyle={CODE_CUSTOM_STYLE}
453
+ codeTagProps={CODE_TAG_PROPS}
454
+ lineNumberStyle={LINE_NUMBER_STYLE}
455
+ renderer={renderer}
456
+ >
457
+ {payload.content}
458
+ </SyntaxHighlighter>
459
+ )}
460
+ </div>
461
+
462
+ {payload && (
463
+ <ReferencedCodePanel
464
+ imports={payload.symbols.imports}
465
+ reexports={payload.symbols.reexports}
466
+ onOpen={(path, line) => push(path, line)}
467
+ />
468
+ )}
469
+ </div>
470
+ );
471
+ }
472
+
473
+ // ---------------------------------------------------------------------------
474
+ // NodeCodeView — the Inspector code dialog's body. sourceRef → navigator;
475
+ // builtin → today's view plus a quiet badge; neither → EXACTLY today's view.
476
+ // ---------------------------------------------------------------------------
477
+
478
+ export interface NodeCodeViewProps {
479
+ nodeType: string;
480
+ /** toString()/stored source — the fallback when no sourceRef exists. */
481
+ source: string;
482
+ sourceRef?: { file: string; line?: number };
483
+ builtin?: boolean;
484
+ /** Test seam, forwarded to SourceNavigator. */
485
+ fetcher?: (path: string) => Promise<SourceFileFetchResult>;
486
+ }
487
+
488
+ export function NodeCodeView({ nodeType, source, sourceRef, builtin, fetcher }: NodeCodeViewProps) {
489
+ if (sourceRef) {
490
+ return (
491
+ <SourceNavigator
492
+ entryFile={sourceRef.file}
493
+ entryLine={sourceRef.line}
494
+ nodeType={nodeType}
495
+ fetcher={fetcher}
496
+ />
497
+ );
498
+ }
499
+ return (
500
+ <>
501
+ {builtin && (
502
+ <div className="mb-1">
503
+ <Badge variant="outline">Built-in node</Badge>
504
+ </div>
505
+ )}
506
+ <div className="min-h-0 flex-1 overflow-auto rounded-md border border-border bg-background">
507
+ <SyntaxHighlighter
508
+ language="javascript"
509
+ style={vscDarkPlus}
510
+ showLineNumbers
511
+ customStyle={CODE_CUSTOM_STYLE}
512
+ codeTagProps={CODE_TAG_PROPS}
513
+ lineNumberStyle={LINE_NUMBER_STYLE}
514
+ >
515
+ {source}
516
+ </SyntaxHighlighter>
517
+ </div>
518
+ </>
519
+ );
520
+ }