@principal-ai/principal-view-react 0.16.44 → 0.16.46
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.
- package/dist/components/session-events/SessionEventFeed.d.ts +5 -4
- package/dist/components/session-events/SessionEventFeed.d.ts.map +1 -1
- package/dist/components/session-events/SessionEventFeed.js +102 -5
- package/dist/components/session-events/SessionEventFeed.js.map +1 -1
- package/dist/subsystem/ComponentDeclaration.d.ts +3 -1
- package/dist/subsystem/ComponentDeclaration.d.ts.map +1 -1
- package/dist/subsystem/ComponentDeclaration.js +49 -75
- package/dist/subsystem/ComponentDeclaration.js.map +1 -1
- package/dist/subsystem/formatDeclaration.d.ts +11 -0
- package/dist/subsystem/formatDeclaration.d.ts.map +1 -0
- package/dist/subsystem/formatDeclaration.js +96 -0
- package/dist/subsystem/formatDeclaration.js.map +1 -0
- package/dist/subsystem/model.d.ts +11 -0
- package/dist/subsystem/model.d.ts.map +1 -1
- package/dist/subsystem/model.js.map +1 -1
- package/dist/subsystem/tokenizeComponent.d.ts +19 -0
- package/dist/subsystem/tokenizeComponent.d.ts.map +1 -0
- package/dist/subsystem/tokenizeComponent.js +61 -0
- package/dist/subsystem/tokenizeComponent.js.map +1 -0
- package/dist/subsystem/tokenizeFormatted.d.ts +15 -0
- package/dist/subsystem/tokenizeFormatted.d.ts.map +1 -0
- package/dist/subsystem/tokenizeFormatted.js +84 -0
- package/dist/subsystem/tokenizeFormatted.js.map +1 -0
- package/package.json +3 -2
- package/src/components/session-events/SessionEventFeed.tsx +97 -6
- package/src/stories/ComponentDeclarationAudit.stories.tsx +290 -0
- package/src/subsystem/ComponentDeclaration.tsx +61 -269
- package/src/subsystem/formatDeclaration.ts +116 -0
- package/src/subsystem/model.ts +24 -0
- package/src/subsystem/tokenizeComponent.ts +70 -0
- package/src/subsystem/tokenizeFormatted.ts +92 -0
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import '@xyflow/react/dist/style.css';
|
|
3
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
4
|
+
import { ThemeProvider, defaultEditorTheme } from '@principal-ade/industry-theme';
|
|
5
|
+
import { ComponentDeclaration } from '../subsystem/ComponentDeclaration';
|
|
6
|
+
import type { SubsystemComponent } from '../subsystem/model';
|
|
7
|
+
import type { GraphifyComponentDetail } from '../graphify';
|
|
8
|
+
|
|
9
|
+
const meta = {
|
|
10
|
+
title: 'Subsystem/Component Declaration Audit',
|
|
11
|
+
component: ComponentDeclaration,
|
|
12
|
+
parameters: { layout: 'padded' },
|
|
13
|
+
tags: ['autodocs'],
|
|
14
|
+
argTypes: {
|
|
15
|
+
constrained: { control: 'boolean', description: 'Toggle 80ch max-width wrapping' },
|
|
16
|
+
},
|
|
17
|
+
args: {
|
|
18
|
+
constrained: true,
|
|
19
|
+
},
|
|
20
|
+
decorators: [
|
|
21
|
+
(Story, ctx) => (
|
|
22
|
+
<ThemeProvider theme={defaultEditorTheme}>
|
|
23
|
+
<div style={{ maxWidth: ctx.args.constrained ? '80ch' : 'none', margin: '0 auto' }}>
|
|
24
|
+
<Story />
|
|
25
|
+
</div>
|
|
26
|
+
</ThemeProvider>
|
|
27
|
+
),
|
|
28
|
+
],
|
|
29
|
+
} satisfies Meta<typeof ComponentDeclaration>;
|
|
30
|
+
export default meta;
|
|
31
|
+
type Story = StoryObj<typeof ComponentDeclaration>;
|
|
32
|
+
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
// All cases — one SubsystemComponent per shape, rendered stacked
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
|
|
37
|
+
const cases: { label: string; component: SubsystemComponent }[] = [
|
|
38
|
+
{
|
|
39
|
+
label: 'function — no params',
|
|
40
|
+
component: {
|
|
41
|
+
id: 'fn-void',
|
|
42
|
+
name: 'flush',
|
|
43
|
+
kind: 'function',
|
|
44
|
+
file: 'src/event-processing/sink.ts',
|
|
45
|
+
purl: 'pkg:github/principal-ai/agent-monitoring',
|
|
46
|
+
symbol: 'flush',
|
|
47
|
+
detail: {
|
|
48
|
+
kind: 'function',
|
|
49
|
+
parameters: [],
|
|
50
|
+
} satisfies GraphifyComponentDetail,
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
label: 'function — single param',
|
|
55
|
+
component: {
|
|
56
|
+
id: 'fn-one',
|
|
57
|
+
name: 'normalize',
|
|
58
|
+
kind: 'function',
|
|
59
|
+
file: 'src/session/SessionReader.ts',
|
|
60
|
+
purl: 'pkg:github/principal-ai/agent-monitoring',
|
|
61
|
+
symbol: 'SessionReader.normalize',
|
|
62
|
+
detail: {
|
|
63
|
+
kind: 'function',
|
|
64
|
+
parameters: [{ name: 'session', type: 'SessionRecord' }],
|
|
65
|
+
returnType: 'SessionEvent[]',
|
|
66
|
+
} satisfies GraphifyComponentDetail,
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
label: 'function — two params',
|
|
71
|
+
component: {
|
|
72
|
+
id: 'fn-two',
|
|
73
|
+
name: 'normalizeSession',
|
|
74
|
+
kind: 'function',
|
|
75
|
+
file: 'src/event-processing/normalize.ts',
|
|
76
|
+
purl: 'pkg:github/principal-ai/agent-monitoring',
|
|
77
|
+
symbol: 'normalizeSession',
|
|
78
|
+
detail: {
|
|
79
|
+
kind: 'function',
|
|
80
|
+
parameters: [
|
|
81
|
+
{ name: 'session', type: 'SessionRecord' },
|
|
82
|
+
{ name: 'limit', type: 'number' },
|
|
83
|
+
],
|
|
84
|
+
returnType: 'SessionEvent[]',
|
|
85
|
+
} satisfies GraphifyComponentDetail,
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
label: 'function — rich params (positional, union, callback, generic return)',
|
|
90
|
+
component: {
|
|
91
|
+
id: 'fn-rich',
|
|
92
|
+
name: 'mergeSessions',
|
|
93
|
+
kind: 'function',
|
|
94
|
+
file: 'src/session/merge.ts',
|
|
95
|
+
purl: 'pkg:github/principal-ai/agent-monitoring',
|
|
96
|
+
symbol: 'mergeSessions',
|
|
97
|
+
detail: {
|
|
98
|
+
kind: 'function',
|
|
99
|
+
parameters: [
|
|
100
|
+
{ name: 'sessions', type: 'SessionRecord[]' },
|
|
101
|
+
{ type: 'MergeOptions' },
|
|
102
|
+
{ name: 'strategy', type: "'append' | 'replace' | 'skip'" },
|
|
103
|
+
{ name: 'onConflict', type: '((a: SessionRecord, b: SessionRecord) => SessionRecord)' },
|
|
104
|
+
],
|
|
105
|
+
returnType: 'Promise<Map<string, SessionEvent[]>>',
|
|
106
|
+
} satisfies GraphifyComponentDetail,
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
label: 'class — no body',
|
|
111
|
+
component: {
|
|
112
|
+
id: 'class-empty',
|
|
113
|
+
name: 'EmptyClass',
|
|
114
|
+
kind: 'class',
|
|
115
|
+
file: 'src/empty.ts',
|
|
116
|
+
purl: 'pkg:github/principal-ai/agent-monitoring',
|
|
117
|
+
symbol: 'EmptyClass',
|
|
118
|
+
detail: {
|
|
119
|
+
kind: 'class',
|
|
120
|
+
} satisfies GraphifyComponentDetail,
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
label: 'class — properties only',
|
|
125
|
+
component: {
|
|
126
|
+
id: 'class-props',
|
|
127
|
+
name: 'Config',
|
|
128
|
+
kind: 'class',
|
|
129
|
+
file: 'src/config.ts',
|
|
130
|
+
purl: 'pkg:github/principal-ai/agent-monitoring',
|
|
131
|
+
symbol: 'Config',
|
|
132
|
+
detail: {
|
|
133
|
+
kind: 'class',
|
|
134
|
+
properties: [
|
|
135
|
+
{ name: 'host', type: 'string' },
|
|
136
|
+
{ name: 'port', type: 'number' },
|
|
137
|
+
{ name: 'tls', type: 'boolean' },
|
|
138
|
+
],
|
|
139
|
+
} satisfies GraphifyComponentDetail,
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
label: 'class — methods + properties + extends + implements',
|
|
144
|
+
component: {
|
|
145
|
+
id: 'class-rich',
|
|
146
|
+
name: 'EventProcessor',
|
|
147
|
+
kind: 'class',
|
|
148
|
+
file: 'src/event-processing/EventProcessor.ts',
|
|
149
|
+
purl: 'pkg:github/principal-ai/agent-monitoring',
|
|
150
|
+
symbol: 'EventProcessor',
|
|
151
|
+
detail: {
|
|
152
|
+
kind: 'class',
|
|
153
|
+
methods: [
|
|
154
|
+
{ name: 'process', parameters: [{ type: 'RawEvent' }, { type: 'ProcessingOptions' }], returnType: 'ProcessedEvent' },
|
|
155
|
+
{ name: 'batch', parameters: [{ type: 'RawEvent[]' }], returnType: 'Promise<ProcessedEvent[]>' },
|
|
156
|
+
{ name: 'onError', parameters: [{ type: 'Error' }] },
|
|
157
|
+
{ name: 'dispose' },
|
|
158
|
+
],
|
|
159
|
+
properties: [
|
|
160
|
+
{ name: 'queue', type: 'RawEvent[]' },
|
|
161
|
+
{ name: 'options', type: 'Required<ProcessingOptions>' },
|
|
162
|
+
{ name: 'retryLimit', type: 'number' },
|
|
163
|
+
],
|
|
164
|
+
extends: ['BaseProcessor'],
|
|
165
|
+
implements: ['Disposable', 'EventEmitterLike'],
|
|
166
|
+
} satisfies GraphifyComponentDetail,
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
label: 'type — properties',
|
|
171
|
+
component: {
|
|
172
|
+
id: 'type-props',
|
|
173
|
+
name: 'SessionRecord',
|
|
174
|
+
kind: 'type',
|
|
175
|
+
file: 'src/session/transcript.ts',
|
|
176
|
+
purl: 'pkg:github/principal-ai/agent-monitoring',
|
|
177
|
+
symbol: 'SessionRecord',
|
|
178
|
+
detail: {
|
|
179
|
+
kind: 'type',
|
|
180
|
+
properties: [
|
|
181
|
+
{ name: 'id', type: 'string' },
|
|
182
|
+
{ name: 'admittedSeq', type: 'number' },
|
|
183
|
+
],
|
|
184
|
+
} satisfies GraphifyComponentDetail,
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
label: 'type — no properties',
|
|
189
|
+
component: {
|
|
190
|
+
id: 'type-empty',
|
|
191
|
+
name: 'BrandedId',
|
|
192
|
+
kind: 'type',
|
|
193
|
+
file: 'src/types.ts',
|
|
194
|
+
purl: 'pkg:github/principal-ai/agent-monitoring',
|
|
195
|
+
symbol: 'BrandedId',
|
|
196
|
+
detail: {
|
|
197
|
+
kind: 'type',
|
|
198
|
+
} satisfies GraphifyComponentDetail,
|
|
199
|
+
},
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
label: 'module — imports + exports',
|
|
203
|
+
component: {
|
|
204
|
+
id: 'mod-full',
|
|
205
|
+
name: 'index',
|
|
206
|
+
kind: 'module',
|
|
207
|
+
file: 'src/index.ts',
|
|
208
|
+
purl: 'pkg:github/principal-ai/agent-monitoring',
|
|
209
|
+
symbol: 'index',
|
|
210
|
+
detail: {
|
|
211
|
+
kind: 'module',
|
|
212
|
+
imports: [
|
|
213
|
+
{ name: './session/SessionReader' },
|
|
214
|
+
{ name: './event-processing/EventProcessor' },
|
|
215
|
+
],
|
|
216
|
+
exports: ['SessionReader', 'EventProcessor'],
|
|
217
|
+
symbols: ['SessionReader', 'EventProcessor', 'normalizeSession'],
|
|
218
|
+
} satisfies GraphifyComponentDetail,
|
|
219
|
+
},
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
label: 'module — no detail (fallback)',
|
|
223
|
+
component: {
|
|
224
|
+
id: 'mod-plain',
|
|
225
|
+
name: 'utils',
|
|
226
|
+
kind: 'module',
|
|
227
|
+
file: 'src/utils.ts',
|
|
228
|
+
purl: 'pkg:github/principal-ai/agent-monitoring',
|
|
229
|
+
symbol: 'utils',
|
|
230
|
+
},
|
|
231
|
+
},
|
|
232
|
+
{
|
|
233
|
+
label: 'external',
|
|
234
|
+
component: {
|
|
235
|
+
id: 'ext',
|
|
236
|
+
name: 'trail-viewer',
|
|
237
|
+
kind: 'external',
|
|
238
|
+
file: '',
|
|
239
|
+
purl: 'pkg:npm/@principal-ai/trail-viewer',
|
|
240
|
+
symbol: '',
|
|
241
|
+
detail: {
|
|
242
|
+
kind: 'external',
|
|
243
|
+
label: 'pkg:npm/@principal-ai/trail-viewer',
|
|
244
|
+
} satisfies GraphifyComponentDetail,
|
|
245
|
+
},
|
|
246
|
+
},
|
|
247
|
+
{
|
|
248
|
+
label: 'function — no symbol (uses name fallback)',
|
|
249
|
+
component: {
|
|
250
|
+
id: 'fn-no-symbol',
|
|
251
|
+
name: 'anonymousHelper',
|
|
252
|
+
kind: 'function',
|
|
253
|
+
file: 'src/helpers.ts',
|
|
254
|
+
purl: 'pkg:github/principal-ai/agent-monitoring',
|
|
255
|
+
detail: {
|
|
256
|
+
kind: 'function',
|
|
257
|
+
parameters: [{ name: 'input', type: 'string' }],
|
|
258
|
+
returnType: 'void',
|
|
259
|
+
} satisfies GraphifyComponentDetail,
|
|
260
|
+
},
|
|
261
|
+
},
|
|
262
|
+
];
|
|
263
|
+
|
|
264
|
+
// ---------------------------------------------------------------------------
|
|
265
|
+
// Story — all cases stacked
|
|
266
|
+
// ---------------------------------------------------------------------------
|
|
267
|
+
|
|
268
|
+
export const AllCases: StoryObj<{ constrained: boolean }> = {
|
|
269
|
+
render: ({ constrained }) => (
|
|
270
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 24 }}>
|
|
271
|
+
{cases.map((c) => (
|
|
272
|
+
<div key={c.label}>
|
|
273
|
+
<div
|
|
274
|
+
style={{
|
|
275
|
+
fontFamily: 'monospace',
|
|
276
|
+
fontSize: 11,
|
|
277
|
+
color: '#888',
|
|
278
|
+
marginBottom: 4,
|
|
279
|
+
textTransform: 'uppercase',
|
|
280
|
+
letterSpacing: '0.05em',
|
|
281
|
+
}}
|
|
282
|
+
>
|
|
283
|
+
{c.label}
|
|
284
|
+
</div>
|
|
285
|
+
<ComponentDeclaration component={c.component} maxWidth={constrained ? '80ch' : undefined} />
|
|
286
|
+
</div>
|
|
287
|
+
))}
|
|
288
|
+
</div>
|
|
289
|
+
),
|
|
290
|
+
};
|
|
@@ -6,15 +6,16 @@
|
|
|
6
6
|
* comments. File content lives in the bottom FileDrawer, not here.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import {
|
|
9
|
+
import { useEffect, useState, type ReactNode } from 'react';
|
|
10
10
|
import { AlignLeft, FileText } from 'lucide-react';
|
|
11
11
|
import { useTheme } from '@principal-ade/industry-theme';
|
|
12
12
|
import {
|
|
13
13
|
KIND_COLOR,
|
|
14
|
-
deriveNameFromSymbol,
|
|
15
14
|
formatPurl,
|
|
16
15
|
type SubsystemComponent,
|
|
16
|
+
type SubsystemDeclTokenKind,
|
|
17
17
|
} from './model';
|
|
18
|
+
import { tokenizeComponent } from './tokenizeComponent';
|
|
18
19
|
|
|
19
20
|
/** Shared affordance for clickable text pieces. */
|
|
20
21
|
const clickableStyle = {
|
|
@@ -54,10 +55,12 @@ export interface ComponentDeclarationProps {
|
|
|
54
55
|
/** Related-name click (types, callers/callees, implementors, …) → select
|
|
55
56
|
* that component if one matches; unmatched refs no-op. */
|
|
56
57
|
onRelatedSelect?: (ref: string) => void;
|
|
58
|
+
/** Max width of the declaration panel (CSS value). Defaults to none. */
|
|
59
|
+
maxWidth?: string | number;
|
|
57
60
|
}
|
|
58
61
|
|
|
59
62
|
/** Declaration panel for the selected component — its definition, code-style. */
|
|
60
|
-
export function ComponentDeclaration({ component, onOpenFile, onRelatedSelect }: ComponentDeclarationProps) {
|
|
63
|
+
export function ComponentDeclaration({ component, onOpenFile, onRelatedSelect: _onRelatedSelect, maxWidth }: ComponentDeclarationProps) {
|
|
61
64
|
const { theme } = useTheme();
|
|
62
65
|
const [fileHovered, setFileHovered] = useState(false);
|
|
63
66
|
// Header visibility toggles — off by default so the panel opens as pure
|
|
@@ -66,26 +69,26 @@ export function ComponentDeclaration({ component, onOpenFile, onRelatedSelect }:
|
|
|
66
69
|
const [showPurpose, setShowPurpose] = useState(false);
|
|
67
70
|
const muted = theme.colors.textMuted ?? theme.colors.textSecondary;
|
|
68
71
|
const color = KIND_COLOR[component.kind] ?? '#888';
|
|
69
|
-
const displayName = deriveNameFromSymbol(component.symbol, component.kind, component.name, component.file);
|
|
70
72
|
|
|
71
|
-
// Token
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
const str = (text: string) => <span style={{ color: theme.colors.success }}>{text}</span>;
|
|
82
|
-
const pn = (text: string) => <span style={{ color: muted }}>{text}</span>;
|
|
73
|
+
// Token-kind → theme-color map (used by the token iterator below).
|
|
74
|
+
const tokenColor: Record<SubsystemDeclTokenKind, string> = {
|
|
75
|
+
keyword: theme.colors.secondary,
|
|
76
|
+
name: color,
|
|
77
|
+
member: theme.colors.info,
|
|
78
|
+
type: theme.colors.accent,
|
|
79
|
+
punctuation: muted,
|
|
80
|
+
string: theme.colors.success,
|
|
81
|
+
newline: '', // not rendered — drives line breaks
|
|
82
|
+
};
|
|
83
83
|
|
|
84
|
-
const line = (children: ReactNode, key?: string, indent?: boolean) =>
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
84
|
+
const line = (children: ReactNode, key?: string, indent?: boolean | number) => {
|
|
85
|
+
const paddingLeft = typeof indent === 'number' ? indent : indent ? 16 : 0;
|
|
86
|
+
return (
|
|
87
|
+
<div key={key} style={{ display: 'flex', alignItems: 'center', whiteSpace: 'pre', minHeight: 18, ...(paddingLeft ? { paddingLeft } : {}) }}>
|
|
88
|
+
{children}
|
|
89
|
+
</div>
|
|
90
|
+
);
|
|
91
|
+
};
|
|
89
92
|
const commentStyle = { color: muted, fontStyle: 'italic' } as const;
|
|
90
93
|
const commentLine = (text: string, key?: string, indent?: boolean, onClick?: () => void) =>
|
|
91
94
|
line(
|
|
@@ -109,15 +112,6 @@ export function ComponentDeclaration({ component, onOpenFile, onRelatedSelect }:
|
|
|
109
112
|
// `// implemented by:`, …) are deliberately not rendered — the graph's
|
|
110
113
|
// edges carry interactions, and duplicated name lists read as clutter. The
|
|
111
114
|
// data stays on the Graphify detail types for other surfaces.
|
|
112
|
-
// Comma-join rendered tokens without raw-array key churn.
|
|
113
|
-
const list = (items: ReactNode[]) =>
|
|
114
|
-
items.map((item, i) => (
|
|
115
|
-
<Fragment key={i}>
|
|
116
|
-
{i > 0 ? ', ' : null}
|
|
117
|
-
{item}
|
|
118
|
-
</Fragment>
|
|
119
|
-
));
|
|
120
|
-
|
|
121
115
|
const lines: ReactNode[] = [];
|
|
122
116
|
// Repo identity — owner avatar + name for GitHub-hosted purls; anything
|
|
123
117
|
// else falls back to the formatted purl as a quiet comment. The avatar
|
|
@@ -207,246 +201,42 @@ export function ComponentDeclaration({ component, onOpenFile, onRelatedSelect }:
|
|
|
207
201
|
),
|
|
208
202
|
);
|
|
209
203
|
|
|
210
|
-
|
|
211
|
-
//
|
|
212
|
-
//
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
if (detail?.kind === 'class') {
|
|
219
|
-
(detail.methods ?? []).forEach((m, i) =>
|
|
220
|
-
members.push(
|
|
221
|
-
line(
|
|
222
|
-
<>
|
|
223
|
-
{mb(m.name)}
|
|
224
|
-
{pn('(')}
|
|
225
|
-
{(m.parameters ?? []).length > 0 &&
|
|
226
|
-
list(
|
|
227
|
-
(m.parameters ?? []).map((p, j) => (
|
|
228
|
-
<Fragment key={j}>
|
|
229
|
-
{p.name ? (
|
|
230
|
-
<>
|
|
231
|
-
{mb(p.name)}
|
|
232
|
-
{pn(': ')}
|
|
233
|
-
</>
|
|
234
|
-
) : null}
|
|
235
|
-
{ty(p.type)}
|
|
236
|
-
</Fragment>
|
|
237
|
-
)),
|
|
238
|
-
)}
|
|
239
|
-
{pn(')')}
|
|
240
|
-
{m.returnType ? (
|
|
241
|
-
<>
|
|
242
|
-
{pn(': ')}
|
|
243
|
-
{ty(m.returnType)}
|
|
244
|
-
</>
|
|
245
|
-
) : null}
|
|
246
|
-
</>,
|
|
247
|
-
`m${i}`,
|
|
248
|
-
true,
|
|
249
|
-
),
|
|
250
|
-
),
|
|
251
|
-
);
|
|
252
|
-
(detail.properties ?? []).forEach((prop, i) =>
|
|
253
|
-
members.push(
|
|
254
|
-
line(
|
|
255
|
-
<>
|
|
256
|
-
{mb(prop.name)}
|
|
257
|
-
{prop.type ? (
|
|
258
|
-
<>
|
|
259
|
-
{pn(': ')}
|
|
260
|
-
{ty(prop.type)}
|
|
261
|
-
</>
|
|
262
|
-
) : null}
|
|
263
|
-
</>,
|
|
264
|
-
`p${i}`,
|
|
265
|
-
true,
|
|
266
|
-
),
|
|
267
|
-
),
|
|
268
|
-
);
|
|
269
|
-
}
|
|
270
|
-
const cls = detail?.kind === 'class' ? detail : undefined;
|
|
271
|
-
const hasBody = members.length > 0;
|
|
272
|
-
lines.push(
|
|
273
|
-
line(
|
|
274
|
-
<>
|
|
275
|
-
{kw('class')}
|
|
276
|
-
{' '}
|
|
277
|
-
{nm(displayName)}
|
|
278
|
-
{cls && (cls.extends ?? []).length > 0 && (
|
|
279
|
-
<>
|
|
280
|
-
{' '}
|
|
281
|
-
{kw('extends')}
|
|
282
|
-
{' '}
|
|
283
|
-
{list((cls.extends ?? []).map((e, i) => <Fragment key={i}>{ty(e)}</Fragment>))}
|
|
284
|
-
</>
|
|
285
|
-
)}
|
|
286
|
-
{cls && (cls.implements ?? []).length > 0 && (
|
|
287
|
-
<>
|
|
288
|
-
{' '}
|
|
289
|
-
{kw('implements')}
|
|
290
|
-
{' '}
|
|
291
|
-
{list((cls.implements ?? []).map((e, i) => <Fragment key={i}>{ty(e)}</Fragment>))}
|
|
292
|
-
</>
|
|
293
|
-
)}
|
|
294
|
-
{hasBody ? pn(' {') : null}
|
|
295
|
-
</>,
|
|
296
|
-
'decl',
|
|
297
|
-
),
|
|
298
|
-
);
|
|
299
|
-
lines.push(...members);
|
|
300
|
-
if (hasBody) lines.push(line(pn('}'), 'end'));
|
|
301
|
-
break;
|
|
204
|
+
// --- Declaration tokens ------------------------------------------------
|
|
205
|
+
// Tokenize asynchronously via Prettier + highlight.js. Pre-tokenized
|
|
206
|
+
// tokens from the wire skip the pipeline entirely.
|
|
207
|
+
const [tokens, setTokens] = useState(component.tokens ?? []);
|
|
208
|
+
useEffect(() => {
|
|
209
|
+
if (component.tokens) {
|
|
210
|
+
setTokens(component.tokens);
|
|
211
|
+
return;
|
|
302
212
|
}
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
<Fragment key={i}>
|
|
317
|
-
{param.name ? (
|
|
318
|
-
<>
|
|
319
|
-
{mb(param.name)}
|
|
320
|
-
{pn(': ')}
|
|
321
|
-
</>
|
|
322
|
-
) : null}
|
|
323
|
-
{ty(param.type)}
|
|
324
|
-
</Fragment>
|
|
325
|
-
)),
|
|
326
|
-
)}
|
|
327
|
-
{pn(')')}
|
|
328
|
-
{fn?.returnType ? (
|
|
329
|
-
<>
|
|
330
|
-
{pn(': ')}
|
|
331
|
-
{ty(fn.returnType)}
|
|
332
|
-
</>
|
|
333
|
-
) : null}
|
|
334
|
-
{pn(';')}
|
|
335
|
-
</>,
|
|
336
|
-
'decl',
|
|
337
|
-
),
|
|
338
|
-
);
|
|
339
|
-
// Caller/callee relationship comments (`// called by:` / `// calls:`)
|
|
340
|
-
// are deliberately not rendered here — the graph's edges carry
|
|
341
|
-
// interactions, and duplicated call lists read as clutter. The data
|
|
342
|
-
// stays on `GraphifyFunctionDetail` for other surfaces.
|
|
343
|
-
break;
|
|
344
|
-
}
|
|
345
|
-
case 'type': {
|
|
346
|
-
const tpe = detail?.kind === 'type' ? detail : undefined;
|
|
347
|
-
const hasBody = ((tpe?.properties ?? []).length) > 0;
|
|
348
|
-
lines.push(
|
|
349
|
-
line(
|
|
350
|
-
<>
|
|
351
|
-
{kw('interface')}
|
|
352
|
-
{' '}
|
|
353
|
-
{nm(displayName)}
|
|
354
|
-
{hasBody ? pn(' {') : null}
|
|
355
|
-
</>,
|
|
356
|
-
'decl',
|
|
357
|
-
),
|
|
358
|
-
);
|
|
359
|
-
if (tpe) {
|
|
360
|
-
(tpe.properties ?? []).forEach((prop, i) =>
|
|
361
|
-
lines.push(
|
|
362
|
-
line(
|
|
363
|
-
<>
|
|
364
|
-
{mb(prop.name)}
|
|
365
|
-
{prop.type ? (
|
|
366
|
-
<>
|
|
367
|
-
{pn(': ')}
|
|
368
|
-
{ty(prop.type)}
|
|
369
|
-
</>
|
|
370
|
-
) : null}
|
|
371
|
-
{pn(';')}
|
|
372
|
-
</>,
|
|
373
|
-
`p${i}`,
|
|
374
|
-
true,
|
|
375
|
-
),
|
|
376
|
-
),
|
|
377
|
-
);
|
|
378
|
-
}
|
|
379
|
-
if (hasBody) lines.push(line(pn('}'), 'end'));
|
|
380
|
-
break;
|
|
381
|
-
}
|
|
382
|
-
case 'module': {
|
|
383
|
-
const mod = detail?.kind === 'module' ? detail : undefined;
|
|
384
|
-
if (mod) {
|
|
385
|
-
(mod.imports ?? []).forEach((imp, i) =>
|
|
386
|
-
lines.push(
|
|
387
|
-
line(
|
|
388
|
-
<>
|
|
389
|
-
{kw('import')}
|
|
390
|
-
{' '}
|
|
391
|
-
{pn("'")}
|
|
392
|
-
{str(imp.name)}
|
|
393
|
-
{pn("';")}
|
|
394
|
-
</>,
|
|
395
|
-
`imp${i}`,
|
|
396
|
-
),
|
|
397
|
-
),
|
|
398
|
-
);
|
|
399
|
-
if ((mod.exports ?? []).length > 0)
|
|
400
|
-
lines.push(
|
|
401
|
-
line(
|
|
402
|
-
<>
|
|
403
|
-
{kw('export')}
|
|
404
|
-
{' '}
|
|
405
|
-
{pn('{ ')}
|
|
406
|
-
{list((mod.exports ?? []).map((e, i) => <Fragment key={i}>{mb(e)}</Fragment>))}
|
|
407
|
-
{pn(' }')}
|
|
408
|
-
{pn(';')}
|
|
409
|
-
</>,
|
|
410
|
-
'exports',
|
|
411
|
-
),
|
|
412
|
-
);
|
|
413
|
-
if ((mod.symbols ?? []).length > 0)
|
|
414
|
-
lines.push(commentLine(`// defines: ${(mod.symbols ?? []).join(', ')}`, 'symbols'));
|
|
415
|
-
} else {
|
|
416
|
-
lines.push(
|
|
417
|
-
line(
|
|
418
|
-
<>
|
|
419
|
-
{kw('module')}
|
|
420
|
-
{' '}
|
|
421
|
-
{nm(displayName)}
|
|
422
|
-
</>,
|
|
423
|
-
'decl',
|
|
424
|
-
),
|
|
425
|
-
);
|
|
426
|
-
}
|
|
427
|
-
break;
|
|
428
|
-
}
|
|
429
|
-
case 'external': {
|
|
430
|
-
lines.push(
|
|
431
|
-
line(
|
|
432
|
-
<>
|
|
433
|
-
{kw('external')}
|
|
434
|
-
{' '}
|
|
435
|
-
{detail?.kind === 'external' ? (
|
|
436
|
-
<>
|
|
437
|
-
{pn("'")}
|
|
438
|
-
{str(detail.label)}
|
|
439
|
-
{pn("'")}
|
|
440
|
-
</>
|
|
441
|
-
) : (
|
|
442
|
-
nm(displayName)
|
|
443
|
-
)}
|
|
444
|
-
</>,
|
|
445
|
-
'decl',
|
|
446
|
-
),
|
|
447
|
-
);
|
|
448
|
-
break;
|
|
213
|
+
let cancelled = false;
|
|
214
|
+
tokenizeComponent(component).then((t) => {
|
|
215
|
+
if (!cancelled) setTokens(t);
|
|
216
|
+
});
|
|
217
|
+
return () => { cancelled = true; };
|
|
218
|
+
}, [component]);
|
|
219
|
+
const declLines: ReactNode[][] = [[]];
|
|
220
|
+
let di = 0;
|
|
221
|
+
for (const tok of tokens) {
|
|
222
|
+
if (tok.kind === 'newline') {
|
|
223
|
+
declLines.push([]);
|
|
224
|
+
di++;
|
|
225
|
+
continue;
|
|
449
226
|
}
|
|
227
|
+
declLines[di].push(
|
|
228
|
+
<span key={`${di}-${declLines[di].length}`} style={{ color: tokenColor[tok.kind] }}>
|
|
229
|
+
{tok.text}
|
|
230
|
+
</span>,
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
for (let li = 0; li < declLines.length; li++) {
|
|
234
|
+
if (declLines[li].length === 0) continue;
|
|
235
|
+
lines.push(
|
|
236
|
+
<div key={`decl-${li}`} style={{ display: 'flex', alignItems: 'center', whiteSpace: 'pre', minHeight: 18 }}>
|
|
237
|
+
{declLines[li]}
|
|
238
|
+
</div>,
|
|
239
|
+
);
|
|
450
240
|
}
|
|
451
241
|
|
|
452
242
|
// Purpose prose follows the declaration — identity and syntax first, the
|
|
@@ -474,7 +264,9 @@ export function ComponentDeclaration({ component, onOpenFile, onRelatedSelect }:
|
|
|
474
264
|
fontFamily: theme.fonts.monospace,
|
|
475
265
|
fontSize: theme.fontSizes[1],
|
|
476
266
|
lineHeight: 1.7,
|
|
477
|
-
|
|
267
|
+
...(maxWidth != null
|
|
268
|
+
? { maxWidth, wordBreak: 'break-word' }
|
|
269
|
+
: { overflowX: 'auto' }),
|
|
478
270
|
}}
|
|
479
271
|
>
|
|
480
272
|
{lines}
|