aihand 0.0.1 → 0.1.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 (113) hide show
  1. package/README.md +136 -2
  2. package/dist/chunk-2NTK7H4W.js +10 -0
  3. package/dist/chunk-3X4FTHLC.cjs +369 -0
  4. package/dist/chunk-BXVNR4E2.js +399 -0
  5. package/dist/chunk-C7DGE6MY.cjs +1456 -0
  6. package/dist/chunk-DUUCVLC3.cjs +254 -0
  7. package/dist/chunk-FAHI53KO.cjs +125 -0
  8. package/dist/chunk-G7KVJ7NF.js +369 -0
  9. package/dist/chunk-GNEUSRGP.js +52 -0
  10. package/dist/chunk-IGNEAOLT.cjs +130 -0
  11. package/dist/chunk-IS5XFUDB.js +125 -0
  12. package/dist/chunk-JLYC76XL.js +2448 -0
  13. package/dist/chunk-KQOABC2O.cjs +52 -0
  14. package/dist/chunk-OVMK33AC.cjs +104 -0
  15. package/dist/chunk-OWYK2IGV.js +250 -0
  16. package/dist/chunk-PQSQN4CN.js +126 -0
  17. package/dist/chunk-QF6AG3M5.cjs +410 -0
  18. package/dist/chunk-QSAMLXML.js +1456 -0
  19. package/dist/chunk-VEKYRKPF.cjs +399 -0
  20. package/dist/chunk-Y6H7W7PI.cjs +2451 -0
  21. package/dist/chunk-YKSYW77R.js +410 -0
  22. package/dist/chunk-Z2Y65YOY.cjs +7 -0
  23. package/dist/chunk-ZJQRNIK7.js +104 -0
  24. package/dist/cli-FDS2C2CZ.cjs +651 -0
  25. package/dist/cli-HHRGYPSM.js +649 -0
  26. package/dist/cli-JQEIE7RQ.js +120 -0
  27. package/dist/cli-K3OS2QQH.cjs +122 -0
  28. package/dist/cli-OSYG6LJD.cjs +89 -0
  29. package/dist/cli-TXRW5PG6.js +89 -0
  30. package/dist/cli.cjs +81 -0
  31. package/dist/cli.js +81 -0
  32. package/dist/config-5KEQLN6L.cjs +13 -0
  33. package/dist/config-PJPYKDLQ.js +13 -0
  34. package/dist/graph-IH56SCPK.js +8 -0
  35. package/dist/graph-ZUXXCJ5A.cjs +8 -0
  36. package/dist/index.cjs +481 -0
  37. package/dist/index.d.cts +461 -0
  38. package/dist/index.d.ts +461 -0
  39. package/dist/index.js +479 -0
  40. package/dist/locate-5XFSXJ5J.cjs +15 -0
  41. package/dist/locate-NKSUGL3A.js +15 -0
  42. package/dist/refactor-5FWSZIBN.cjs +19 -0
  43. package/dist/refactor-BOB3SZSA.js +19 -0
  44. package/dist/scan-4R7GQG2W.cjs +9 -0
  45. package/dist/scan-VF54GAAX.js +9 -0
  46. package/dist/ui/probe/server.cjs +505 -0
  47. package/dist/ui/probe/server.js +507 -0
  48. package/dist/vite.cjs +12 -0
  49. package/dist/vite.d.cts +12 -0
  50. package/dist/vite.d.ts +12 -0
  51. package/dist/vite.js +12 -0
  52. package/package.json +82 -9
  53. package/src/cli.ts +107 -0
  54. package/src/index.ts +54 -0
  55. package/src/read/cli.ts +650 -0
  56. package/src/read/compact.ts +286 -0
  57. package/src/read/config.ts +62 -0
  58. package/src/read/graph.ts +182 -0
  59. package/src/read/index.ts +12 -0
  60. package/src/read/inject.ts +121 -0
  61. package/src/read/locate.ts +104 -0
  62. package/src/read/panel.ts +335 -0
  63. package/src/read/pipeline.ts +78 -0
  64. package/src/read/refactor.ts +576 -0
  65. package/src/read/render.ts +1118 -0
  66. package/src/read/scan.ts +61 -0
  67. package/src/read/seam.ts +0 -0
  68. package/src/read/security.ts +171 -0
  69. package/src/read/signals.ts +333 -0
  70. package/src/read/state.ts +71 -0
  71. package/src/read/stategraph.ts +205 -0
  72. package/src/read/types.ts +162 -0
  73. package/src/read/vite.ts +77 -0
  74. package/src/ui/babel/line-profiler.ts +197 -0
  75. package/src/ui/babel/source-loc.ts +68 -0
  76. package/src/ui/bridge/cdp-bridge.ts +138 -0
  77. package/src/ui/bridge/compile-probe.ts +80 -0
  78. package/src/ui/bridge/transport.ts +26 -0
  79. package/src/ui/bridge/vite-bridge.ts +116 -0
  80. package/src/ui/client/client-patch.ts +899 -0
  81. package/src/ui/client/client.ts +2562 -0
  82. package/src/ui/core/action.ts +747 -0
  83. package/src/ui/core/candidates.ts +348 -0
  84. package/src/ui/core/canvas.ts +305 -0
  85. package/src/ui/core/check.ts +34 -0
  86. package/src/ui/core/compact.ts +314 -0
  87. package/src/ui/core/detail.ts +244 -0
  88. package/src/ui/core/diff.ts +253 -0
  89. package/src/ui/core/emit.ts +198 -0
  90. package/src/ui/core/knob-exec.ts +137 -0
  91. package/src/ui/core/perf.ts +254 -0
  92. package/src/ui/core/types.ts +164 -0
  93. package/src/ui/core/util.ts +221 -0
  94. package/src/ui/index.ts +5 -0
  95. package/src/ui/probe/cli.ts +139 -0
  96. package/src/ui/probe/server.ts +468 -0
  97. package/src/ui/self/act.ts +47 -0
  98. package/src/ui/self/discover.ts +101 -0
  99. package/src/ui/self/grow.ts +121 -0
  100. package/src/ui/self/install.ts +100 -0
  101. package/src/ui/self/probe.ts +105 -0
  102. package/src/ui/self/screen-hook.ts +44 -0
  103. package/src/ui/self/self.ts +48 -0
  104. package/src/ui/self/store-refs.ts +123 -0
  105. package/src/ui/self/store-schema.ts +65 -0
  106. package/src/ui/self/synth.ts +37 -0
  107. package/src/ui/server/cli.ts +102 -0
  108. package/src/ui/server/dispatch.ts +276 -0
  109. package/src/ui/server/help-text.ts +237 -0
  110. package/src/ui/server/knob-schema.ts +87 -0
  111. package/src/ui/server/plugin.ts +1151 -0
  112. package/src/vite.ts +39 -0
  113. package/index.js +0 -2
@@ -0,0 +1,34 @@
1
+ import type { CheckResult, RawState } from './types'
2
+
3
+ export function check(raw: RawState): CheckResult {
4
+ const consoleErrors = raw.console.filter(l => l.level === 'error')
5
+ const failedRequests = raw.network.filter(r => r.status >= 400 || r.failed)
6
+
7
+ const assertions = [
8
+ {
9
+ name: 'no-console-errors',
10
+ pass: consoleErrors.length === 0,
11
+ ...consoleErrors.length && { detail: consoleErrors[0].text },
12
+ },
13
+ {
14
+ name: 'no-uncaught-errors',
15
+ pass: raw.errors.length === 0,
16
+ ...raw.errors.length && { detail: raw.errors[0].message },
17
+ },
18
+ {
19
+ name: 'no-failed-requests',
20
+ pass: failedRequests.length === 0,
21
+ ...failedRequests.length && { detail: `${failedRequests[0].method} ${failedRequests[0].url} ${failedRequests[0].status}` },
22
+ },
23
+ {
24
+ name: 'ui-not-empty',
25
+ pass: raw.ui.trim().length > 0,
26
+ ...!raw.ui.trim().length && { detail: 'UI tree is empty' },
27
+ },
28
+ ]
29
+
30
+ return {
31
+ pass: assertions.every(a => a.pass),
32
+ assertions,
33
+ }
34
+ }
@@ -0,0 +1,314 @@
1
+ import type { CompactState, ErrorEntry, LogEntry, NetworkRequest, RawState } from './types'
2
+ import { detailPerformance } from './perf'
3
+ import { appStackFrames, compactUrl, formatValue, truncate } from './util'
4
+
5
+ const SLOW_THRESHOLD = 1000
6
+
7
+ // --- UI (merged component tree + DOM semantics) ---
8
+
9
+ const MAX_UI_DEPTH = 6
10
+ const UI_PRIMITIVES = new Set([
11
+ 'Button',
12
+ 'Input',
13
+ 'Label',
14
+ 'Badge',
15
+ 'Checkbox',
16
+ 'Skeleton',
17
+ 'Spinner',
18
+ 'Switch',
19
+ 'Tabs',
20
+ 'Tooltip',
21
+ 'Popover',
22
+ 'Dialog',
23
+ 'Select',
24
+ 'Card',
25
+ 'Table',
26
+ 'Slider',
27
+ 'Progress',
28
+ 'RadioGroup',
29
+ 'HoverCard',
30
+ 'DropdownMenu',
31
+ 'ContextMenu',
32
+ 'Command',
33
+ 'Form',
34
+ 'Alert',
35
+ 'Pagination',
36
+ 'Textarea',
37
+ 'TooltipProvider',
38
+ 'DialogPortal',
39
+ 'Router',
40
+ 'RenderErrorBoundary',
41
+ 'RouterProvider',
42
+ 'RouterProvider2',
43
+ 'PanelGroup',
44
+ 'Panel',
45
+ ])
46
+
47
+ // component name = leading run before the first space, '[', or '—' separator
48
+ export function nameOf(line: string): string {
49
+ let end = 0
50
+ while (end < line.length) {
51
+ const c = line[end]
52
+ if (c === ' ' || c === '\t' || c === '[' || c === '—')
53
+ break
54
+ end++
55
+ }
56
+ return line.slice(0, end)
57
+ }
58
+
59
+ export function compactUI(tree: string): string {
60
+ if (!tree)
61
+ return ''
62
+
63
+ const lines = tree.split('\n')
64
+ const result: string[] = []
65
+ const repeatTracker = new Map<string, { count: number, lastIndex: number }>()
66
+
67
+ for (let i = 0; i < lines.length; i++) {
68
+ const line = lines[i]
69
+ const trimmed = line.trimStart()
70
+ if (!trimmed)
71
+ continue
72
+
73
+ const indent = line.length - trimmed.length
74
+ const depth = Math.floor(indent / 2)
75
+ if (depth > MAX_UI_DEPTH)
76
+ continue
77
+
78
+ const componentName = nameOf(trimmed)
79
+ if (UI_PRIMITIVES.has(componentName))
80
+ continue
81
+
82
+ // fold repeated siblings
83
+ const key = `${depth}:${componentName}`
84
+ const tracker = repeatTracker.get(key)
85
+ if (tracker && i - tracker.lastIndex <= 2) {
86
+ tracker.count++
87
+ tracker.lastIndex = i
88
+ continue
89
+ }
90
+
91
+ // flush previous repeat groups
92
+ for (const [k, t] of repeatTracker) {
93
+ if (t.count > 1) {
94
+ const d = Number.parseInt(k.split(':')[0])
95
+ const name = k.split(':').slice(1).join(':')
96
+ result.push(`${' '.repeat(d)}${name} ×${t.count}`)
97
+ }
98
+ if (t.count > 1 || i - t.lastIndex > 2) {
99
+ repeatTracker.delete(k)
100
+ }
101
+ }
102
+
103
+ repeatTracker.set(key, { count: 1, lastIndex: i })
104
+ result.push(line)
105
+ }
106
+
107
+ // flush remaining
108
+ for (const [k, t] of repeatTracker) {
109
+ if (t.count > 1) {
110
+ const d = Number.parseInt(k.split(':')[0])
111
+ const name = k.split(':').slice(1).join(':')
112
+ result.push(`${' '.repeat(d)}${name} ×${t.count}`)
113
+ }
114
+ }
115
+
116
+ return result.join('\n')
117
+ }
118
+
119
+ // --- Console ---
120
+
121
+ // case-insensitive substrings that mark a log line as dev-tooling noise
122
+ const NOISE_SUBSTRINGS = [
123
+ '[hmr]',
124
+ '[vite]',
125
+ 'hot module',
126
+ 'react-devtools',
127
+ 'download the react devtools',
128
+ 'warning: react does not recognize',
129
+ 'source map',
130
+ 'favicon.ico',
131
+ 'webpack',
132
+ ]
133
+
134
+ export function compactConsole(logs: LogEntry[]): string {
135
+ if (!logs.length)
136
+ return ''
137
+
138
+ // filter noise
139
+ const filtered = logs.filter((l) => {
140
+ const lower = l.text.toLowerCase()
141
+ return !NOISE_SUBSTRINGS.some(s => lower.includes(s))
142
+ })
143
+ if (!filtered.length)
144
+ return ''
145
+
146
+ // dedup consecutive same messages
147
+ const deduped: { entry: LogEntry, count: number }[] = []
148
+ for (const log of filtered) {
149
+ const last = deduped[deduped.length - 1]
150
+ if (last && last.entry.text === log.text && last.entry.level === log.level) {
151
+ last.count++
152
+ }
153
+ else {
154
+ deduped.push({ entry: log, count: 1 })
155
+ }
156
+ }
157
+
158
+ // prioritize: errors first, then warns, then recent info/debug
159
+ const errors = deduped.filter(d => d.entry.level === 'error')
160
+ const warns = deduped.filter(d => d.entry.level === 'warn')
161
+ const rest = deduped.filter(d => d.entry.level !== 'error' && d.entry.level !== 'warn')
162
+
163
+ // keep last N info/debug entries
164
+ const recentRest = rest.slice(-10)
165
+
166
+ const lines: string[] = []
167
+ for (const group of [...errors, ...warns, ...recentRest]) {
168
+ const prefix = `[${group.entry.level}]`
169
+ const count = group.count > 1 ? ` ×${group.count}` : ''
170
+ const source = group.entry.source ? ` (${group.entry.source})` : ''
171
+ const text = truncate(group.entry.text, 200)
172
+ lines.push(`${prefix}${count} ${text}${source}`)
173
+ }
174
+
175
+ return lines.join('\n')
176
+ }
177
+
178
+ // --- Network ---
179
+
180
+ export function compactNetwork(requests: NetworkRequest[]): string {
181
+ if (!requests.length)
182
+ return ''
183
+
184
+ // only fetch/XHR
185
+ const relevant = requests.filter(r =>
186
+ r.resourceType === 'fetch' || r.resourceType === 'xhr' || r.resourceType === 'websocket'
187
+ || r.resourceType === 'eventsource' || isApiUrl(r.url),
188
+ )
189
+ if (!relevant.length)
190
+ return ''
191
+
192
+ const lines: string[] = []
193
+ for (const req of relevant) {
194
+ const duration = req.duration > 0 ? ` ${formatDuration(req.duration)}` : ''
195
+ const slow = req.duration >= SLOW_THRESHOLD ? ' [SLOW]' : ''
196
+ const url = compactUrl(req.url, 50)
197
+ const headers = diagnosticHeaders(req)
198
+
199
+ if (req.failed || req.status >= 400) {
200
+ // failed: show more detail
201
+ const body = req.responseBody ? ` "${truncate(req.responseBody, 100)}"` : ''
202
+ const failure = req.failureText ? ` (${req.failureText})` : ''
203
+ lines.push(`${req.method} ${url} ${req.status}${failure}${body}${headers}${duration}${slow}`)
204
+ }
205
+ else {
206
+ lines.push(`${req.method} ${url} ${req.status}${headers}${duration}${slow}`)
207
+ }
208
+ }
209
+
210
+ return lines.join('\n')
211
+ }
212
+
213
+ function isApiUrl(url: string): boolean {
214
+ try {
215
+ const u = new URL(url)
216
+ return u.pathname.startsWith('/api') || u.pathname.includes('/graphql')
217
+ }
218
+ catch {
219
+ return false
220
+ }
221
+ }
222
+
223
+ const DIAGNOSTIC_HEADERS = ['content-type', 'x-error', 'www-authenticate', 'access-control-allow-origin']
224
+
225
+ function diagnosticHeaders(req: NetworkRequest): string {
226
+ const h = req.responseHeaders
227
+ if (!h)
228
+ return ''
229
+ const parts: string[] = []
230
+ for (const key of DIAGNOSTIC_HEADERS) {
231
+ const val = h[key]
232
+ if (!val)
233
+ continue
234
+ // skip common json content-type on success — not diagnostic
235
+ if (key === 'content-type' && req.status < 400 && val.includes('application/json'))
236
+ continue
237
+ parts.push(`${key}: ${truncate(val, 60)}`)
238
+ }
239
+ if (!parts.length)
240
+ return ''
241
+ return ` [${parts.join(', ')}]`
242
+ }
243
+
244
+ function formatDuration(ms: number): string {
245
+ return ms >= 1000 ? `${(ms / 1000).toFixed(1)}s` : `${Math.round(ms)}ms`
246
+ }
247
+
248
+ // --- Errors ---
249
+
250
+ export function compactErrors(errors: ErrorEntry[]): string {
251
+ if (!errors.length)
252
+ return ''
253
+
254
+ // dedup by message
255
+ const seen = new Map<string, ErrorEntry>()
256
+ for (const err of errors) {
257
+ if (!seen.has(err.message)) {
258
+ seen.set(err.message, err)
259
+ }
260
+ }
261
+
262
+ const lines: string[] = []
263
+ for (const err of seen.values()) {
264
+ lines.push(err.message)
265
+ if (err.stack) {
266
+ for (const frame of appStackFrames(err.stack, 5))
267
+ lines.push(` ${frame}`)
268
+ }
269
+ }
270
+
271
+ return lines.join('\n')
272
+ }
273
+
274
+ // --- State ---
275
+
276
+ export function compactState(state: Record<string, unknown>): string {
277
+ if (!state || !Object.keys(state).length)
278
+ return ''
279
+
280
+ const lines: string[] = []
281
+ for (const [name, value] of Object.entries(state)) {
282
+ lines.push(`${name}:`)
283
+ if (typeof value === 'object' && value !== null) {
284
+ for (const [k, v] of Object.entries(value as Record<string, unknown>)) {
285
+ lines.push(` ${k}: ${truncate(formatValue(v), 120)}`)
286
+ }
287
+ }
288
+ else {
289
+ lines.push(` ${String(value)}`)
290
+ }
291
+ }
292
+ return lines.join('\n')
293
+ }
294
+
295
+ // --- Main ---
296
+
297
+ export function compact(raw: RawState): CompactState {
298
+ return {
299
+ url: raw.url,
300
+ ui: compactUI(raw.ui),
301
+ console: compactConsole(raw.console),
302
+ network: compactNetwork(raw.network),
303
+ errors: compactErrors(raw.errors),
304
+ state: compactState(raw.state),
305
+ performance: raw.performance ? detailPerformance(raw.performance) : undefined,
306
+ timestamp: raw.timestamp,
307
+ counts: {
308
+ console: raw.console.length,
309
+ network: raw.network.length,
310
+ errors: raw.errors.length,
311
+ state: Object.keys(raw.state).length,
312
+ },
313
+ }
314
+ }
@@ -0,0 +1,244 @@
1
+ import type { ErrorEntry, LogEntry, NetworkRequest, PerformanceData, RawState } from './types'
2
+ import { isSecretKey, redactSecretValue } from './candidates'
3
+ import { compactUI } from './compact'
4
+ import { detailPerformance } from './perf'
5
+ import { appStackFrames, formatValue, truncate } from './util'
6
+
7
+ // A header line for the full dump: `Authorization`/`Cookie`/`X-Api-Key` carry credentials,
8
+ // so mask the value by header name (same key-level chokepoint as domain fields). Bodies can
9
+ // also embed keys but have no name to key off — left as-is; the credential surface is headers.
10
+ const headerLine = (k: string, v: string) => ` ${k}: ${isSecretKey(k) ? redactSecretValue(v) : v}`
11
+
12
+ export function detail(raw: RawState, section: string, index: string | undefined, full: boolean): string | null {
13
+ switch (section) {
14
+ case 'ui': return full ? (raw.ui || null) : (compactUI(raw.ui) || null)
15
+ case 'console': return detailConsole(raw.console, index, full)
16
+ case 'network': return detailNetwork(raw.network, index, full)
17
+ case 'errors': return detailError(raw.errors, index, full)
18
+ case 'state': return detailState(raw.state, index, full)
19
+ case 'profile': return raw.performance ? detailPerformance(raw.performance) : '(no perf data — is the tab foreground?)'
20
+ default: return null
21
+ }
22
+ }
23
+
24
+ // --- Console ---
25
+
26
+ function detailConsole(logs: LogEntry[], index: string | undefined, full: boolean): string | null {
27
+ if (index === undefined) {
28
+ if (!logs.length)
29
+ return '(empty)'
30
+ return logs.map((log, i) => `[${i}] [${log.level}] ${truncate(log.text, 120)}`).join('\n')
31
+ }
32
+ const i = Number.parseInt(index)
33
+ if (Number.isNaN(i) || i < 0 || i >= logs.length)
34
+ return null
35
+ const log = logs[i]
36
+
37
+ if (full) {
38
+ const parts = [`[${log.level}] ${log.text}`]
39
+ if (log.timestamp)
40
+ parts.push(`timestamp: ${new Date(log.timestamp).toISOString()}`)
41
+ if (log.source)
42
+ parts.push(`source: ${log.source}`)
43
+ return parts.join('\n')
44
+ }
45
+
46
+ return `[${log.level}] ${truncate(log.text, 200)}`
47
+ }
48
+
49
+ // --- Network ---
50
+
51
+ function detailNetwork(requests: NetworkRequest[], index: string | undefined, full: boolean): string | null {
52
+ if (index === undefined) {
53
+ if (!requests.length)
54
+ return '(empty)'
55
+ return requests.map((req, i) => `[${i}] ${req.method} ${req.status} ${truncate(req.url, 80)} ${req.duration}ms${req.failed ? ' FAILED' : ''}`).join('\n')
56
+ }
57
+ const i = Number.parseInt(index)
58
+ if (Number.isNaN(i) || i < 0 || i >= requests.length)
59
+ return null
60
+ const req = requests[i]
61
+
62
+ const lines = [
63
+ `${req.method} ${req.url}`,
64
+ `status: ${req.status}`,
65
+ `duration: ${req.duration}ms`,
66
+ `type: ${req.resourceType}`,
67
+ ]
68
+ if (req.failed)
69
+ lines.push(`failed: ${req.failureText || 'true'}`)
70
+
71
+ if (full) {
72
+ if (req.requestHeaders && Object.keys(req.requestHeaders).length) {
73
+ lines.push('request-headers:')
74
+ for (const [k, v] of Object.entries(req.requestHeaders)) lines.push(headerLine(k, v))
75
+ }
76
+ if (req.requestBody)
77
+ lines.push(`request-body:\n${req.requestBody}`)
78
+ if (req.responseHeaders && Object.keys(req.responseHeaders).length) {
79
+ lines.push('response-headers:')
80
+ for (const [k, v] of Object.entries(req.responseHeaders)) lines.push(headerLine(k, v))
81
+ }
82
+ if (req.responseBody)
83
+ lines.push(`response-body:\n${req.responseBody}`)
84
+ }
85
+ else {
86
+ if (req.requestBody) {
87
+ lines.push(`request-body: ${byteSize(req.requestBody)}`)
88
+ if (req.requestSample) {
89
+ const schema = jsonSchema(req.requestSample)
90
+ if (schema)
91
+ lines.push(schema)
92
+ }
93
+ }
94
+ if (req.responseBody) {
95
+ if (req.status >= 400) {
96
+ lines.push(`response-body: ${byteSize(req.responseBody)} "${truncate(req.responseBody, 100)}"`)
97
+ }
98
+ else {
99
+ if (req.responseSample) {
100
+ lines.push(`response-body: ${byteSize(req.responseBody)}`)
101
+ const schema = jsonSchema(req.responseSample)
102
+ if (schema)
103
+ lines.push(schema)
104
+ }
105
+ else {
106
+ lines.push(`response-body: ${byteSize(req.responseBody)} ${truncate(req.responseBody, 100)}`)
107
+ }
108
+ }
109
+ }
110
+ }
111
+
112
+ return lines.join('\n')
113
+ }
114
+
115
+ // --- Errors ---
116
+
117
+ function detailError(errors: ErrorEntry[], index: string | undefined, full: boolean): string | null {
118
+ if (index === undefined) {
119
+ if (!errors.length)
120
+ return '(empty)'
121
+ return errors.map((err, i) => `[${i}] ${truncate(err.message, 120)}`).join('\n')
122
+ }
123
+ const i = Number.parseInt(index)
124
+ if (Number.isNaN(i) || i < 0 || i >= errors.length)
125
+ return null
126
+ const err = errors[i]
127
+
128
+ if (full) {
129
+ const lines = [err.message]
130
+ if (err.stack)
131
+ lines.push(err.stack)
132
+ if (err.source)
133
+ lines.push(`source: ${err.source}`)
134
+ if (err.line != null)
135
+ lines.push(`location: ${err.source || ''}:${err.line}:${err.column ?? 0}`)
136
+ return lines.join('\n')
137
+ }
138
+
139
+ const lines = [err.message]
140
+ if (err.stack) {
141
+ const all = appStackFrames(err.stack, Infinity)
142
+ lines.push(...all.slice(0, 3))
143
+ if (all.length > 3)
144
+ lines.push(` ... ${all.length - 3} more app frames`)
145
+ }
146
+ if (err.line != null)
147
+ lines.push(`location: ${err.source || ''}:${err.line}:${err.column ?? 0}`)
148
+ return lines.join('\n')
149
+ }
150
+
151
+ // --- State ---
152
+
153
+ function detailState(state: Record<string, unknown>, name: string | undefined, full: boolean): string | null {
154
+ if (!name) {
155
+ const keys = Object.keys(state)
156
+ if (!keys.length)
157
+ return '(empty)'
158
+ return keys.map(k => `${k}: ${formatSummaryValue(state[k])}`).join('\n')
159
+ }
160
+ if (!(name in state))
161
+ return null
162
+ const value = state[name]
163
+ if (full) {
164
+ try {
165
+ return JSON.stringify(value, null, 2) ?? formatValue(value)
166
+ }
167
+ catch {
168
+ return formatValue(value) // 循环引用 / Error / Map → 不再 {}
169
+ }
170
+ }
171
+ if (typeof value !== 'object' || value === null)
172
+ return `${name}: ${typeof value}`
173
+ const lines: string[] = []
174
+ for (const [k, v] of Object.entries(value as Record<string, unknown>)) {
175
+ lines.push(`${k}: ${formatSummaryValue(v)}`)
176
+ }
177
+ return lines.join('\n')
178
+ }
179
+
180
+ function isArraySentinel(v: string): boolean {
181
+ if (!v.startsWith('Array(') || !v.endsWith(')'))
182
+ return false
183
+ const digits = v.slice(6, -1)
184
+ return digits.length > 0 && [...digits].every(c => c >= '0' && c <= '9')
185
+ }
186
+
187
+ function formatSummaryValue(v: unknown): string {
188
+ if (typeof v === 'string' && isArraySentinel(v)) // "Array(N)" from boundedSnapshot — leave untruncated
189
+ return v
190
+ return truncate(formatValue(v), 80)
191
+ }
192
+
193
+ // --- JSON schema fallback ---
194
+
195
+ function jsonSchema(sample: string): string | null {
196
+ try {
197
+ return schemaOf(JSON.parse(sample), 0)
198
+ }
199
+ catch {
200
+ return null
201
+ }
202
+ }
203
+
204
+ function schemaOf(v: unknown, d: number): string {
205
+ if (v === null)
206
+ return 'null'
207
+ if (typeof v === 'string')
208
+ return 'string'
209
+ if (typeof v === 'number')
210
+ return 'number'
211
+ if (typeof v === 'boolean')
212
+ return 'boolean'
213
+ if (Array.isArray(v)) {
214
+ if (!v.length)
215
+ return '[]'
216
+ if (d >= 3)
217
+ return '[…]'
218
+ return `${schemaOf(v[0], d + 1)}[]`
219
+ }
220
+ if (typeof v === 'object') {
221
+ if (d >= 3)
222
+ return '{…}'
223
+ const entries = Object.entries(v as Record<string, unknown>)
224
+ if (!entries.length)
225
+ return '{}'
226
+ const max = d === 0 ? 12 : 6
227
+ const fields = entries.slice(0, max).map(([k, val]) => `${k}: ${schemaOf(val, d + 1)}`)
228
+ if (entries.length > max)
229
+ fields.push(`… ${entries.length - max} more`)
230
+ return `{ ${fields.join(', ')} }`
231
+ }
232
+ return typeof v
233
+ }
234
+
235
+ // --- Utils ---
236
+
237
+ function byteSize(s: string): string {
238
+ const bytes = new TextEncoder().encode(s).length
239
+ if (bytes < 1024)
240
+ return `${bytes}B`
241
+ if (bytes < 1024 * 1024)
242
+ return `${(bytes / 1024).toFixed(1)}KB`
243
+ return `${(bytes / (1024 * 1024)).toFixed(1)}MB`
244
+ }