@jkwd/inbase 0.1.21 → 0.1.22

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 (58) hide show
  1. package/README.md +13 -7
  2. package/apps/explorer/package.json +1 -0
  3. package/apps/explorer/scripts/explain-store.d.ts +135 -0
  4. package/apps/explorer/scripts/explain-store.mjs +666 -0
  5. package/apps/explorer/scripts/patch-lib.mjs +4 -0
  6. package/apps/explorer/scripts/scan-target.mjs +22 -5
  7. package/apps/explorer/scripts/session-store.d.ts +86 -11
  8. package/apps/explorer/scripts/session-store.mjs +371 -58
  9. package/apps/explorer/scripts/target-config.d.ts +38 -3
  10. package/apps/explorer/scripts/target-config.mjs +147 -3
  11. package/apps/explorer/src/App.tsx +1073 -158
  12. package/apps/explorer/src/agentIntent.ts +61 -7
  13. package/apps/explorer/src/codebase.ts +1 -1
  14. package/apps/explorer/src/devTargets.ts +66 -0
  15. package/apps/explorer/src/explain.ts +312 -0
  16. package/apps/explorer/src/index.css +874 -222
  17. package/apps/explorer/src/layout.ts +55 -0
  18. package/apps/explorer/src/scene/DistantFileBlocks.tsx +4 -2
  19. package/apps/explorer/src/scene/FileBlock.tsx +95 -72
  20. package/apps/explorer/src/scene/FolderArea.tsx +55 -32
  21. package/apps/explorer/src/scene/MapView.tsx +506 -33
  22. package/apps/explorer/src/scene/RelationLines.tsx +7 -0
  23. package/apps/explorer/src/scene/World.tsx +146 -32
  24. package/apps/explorer/src/speech.ts +228 -0
  25. package/apps/explorer/src/theme.ts +29 -0
  26. package/apps/explorer/src/types.ts +98 -8
  27. package/apps/explorer/src/ui/CanvasErrorBoundary.tsx +1 -1
  28. package/apps/explorer/src/ui/ExplainAskCard.tsx +142 -0
  29. package/apps/explorer/src/ui/ExplainHud.tsx +524 -0
  30. package/apps/explorer/src/ui/ExplainInfoPanel.tsx +135 -0
  31. package/apps/explorer/src/ui/ExplainPointer.tsx +73 -0
  32. package/apps/explorer/src/ui/EyeIcon.tsx +38 -1
  33. package/apps/explorer/src/ui/HUD.tsx +1067 -795
  34. package/apps/explorer/src/ui/NameInput.tsx +114 -5
  35. package/apps/explorer/src/userContext.ts +0 -11
  36. package/apps/explorer/src/userCreated.ts +54 -1
  37. package/apps/explorer/vite.config.ts +173 -26
  38. package/bin/inbase.mjs +11 -2
  39. package/bin/project.mjs +1 -1
  40. package/bin/session.mjs +287 -38
  41. package/package.json +4 -1
  42. package/skill/commands/amber.md +23 -0
  43. package/skill/commands/blue.md +13 -0
  44. package/skill/commands/coral.md +23 -0
  45. package/skill/commands/explain.md +77 -0
  46. package/skill/commands/green.md +23 -0
  47. package/skill/commands/inbase.md +7 -5
  48. package/skill/commands/lime.md +23 -0
  49. package/skill/commands/orange.md +23 -0
  50. package/skill/commands/purple.md +23 -0
  51. package/skill/commands/red.md +23 -0
  52. package/skill/commands/skipinbase.md +1 -1
  53. package/skill/commands/violet.md +23 -0
  54. package/skill/commands/yellow.md +23 -0
  55. package/skill/inbase/SKILL.md +124 -76
  56. package/apps/explorer/src/scene/BlockPlacer.tsx +0 -78
  57. package/apps/explorer/src/scene/IslandPlacer.tsx +0 -31
  58. package/apps/explorer/src/scene/SelectionThumbnail.tsx +0 -1069
@@ -0,0 +1,524 @@
1
+ import { useCallback, useEffect, useRef, useState, type CSSProperties } from 'react'
2
+ import type { ExplainSession } from '../types'
3
+ import {
4
+ currentExplainStep,
5
+ explainIsPreparing,
6
+ explainNeighbor,
7
+ explainSpeechText,
8
+ explainStepDepth,
9
+ explainStepPosition,
10
+ } from '../explain'
11
+ import { shouldIgnoreShortcut } from '../keyboard'
12
+ import {
13
+ AUTO_STEP_DELAY_MS,
14
+ canSpeak,
15
+ isSpeaking,
16
+ speakText,
17
+ stopSpeaking,
18
+ subscribeSpeakStatus,
19
+ type SpeakStatus,
20
+ } from '../speech'
21
+
22
+ export function ExplainHud({
23
+ explain,
24
+ onStep,
25
+ onAsk,
26
+ onExit,
27
+ }: {
28
+ explain: ExplainSession
29
+ onStep: (step: string) => void
30
+ onAsk: (step: string, question: string) => void
31
+ onExit: () => void
32
+ }) {
33
+ const step = currentExplainStep(explain)
34
+ const count = explain.steps.length
35
+ const id = step?.index ?? explain.currentStep
36
+ const position = explainStepPosition(explain, id)
37
+ const hasSteps = count > 0
38
+ const starting = explain.active && !hasSteps
39
+ const askingAbout = explain.pendingQuestion
40
+ const preparingFollowUp = explainIsPreparing(explain) && hasSteps
41
+ const speechAvailable = canSpeak()
42
+ const [telling, setTelling] = useState(false)
43
+ const [autoStep, setAutoStep] = useState(false)
44
+ const [speakStatus, setSpeakStatus] = useState<SpeakStatus>('idle')
45
+ const [asking, setAsking] = useState(false)
46
+ const [draft, setDraft] = useState('')
47
+ const tellingRef = useRef(false)
48
+ const autoStepRef = useRef(false)
49
+ const autoTimerRef = useRef<number | null>(null)
50
+ const waitTickRef = useRef<number | null>(null)
51
+ const [autoWaitMs, setAutoWaitMs] = useState<number | null>(null)
52
+ const [autoWaitToken, setAutoWaitToken] = useState(0)
53
+ const idRef = useRef(id)
54
+ const goToRef = useRef<(stepId: string) => void>(() => {})
55
+ const neighborRef = useRef<(delta: number) => string | null>(() => null)
56
+ const activeItemRef = useRef<HTMLLIElement | null>(null)
57
+ const askInputRef = useRef<HTMLTextAreaElement | null>(null)
58
+ const [pointerTop, setPointerTop] = useState<number | null>(null)
59
+ idRef.current = id
60
+
61
+ const clearAutoTimer = useCallback(() => {
62
+ if (autoTimerRef.current != null) {
63
+ window.clearTimeout(autoTimerRef.current)
64
+ autoTimerRef.current = null
65
+ }
66
+ if (waitTickRef.current != null) {
67
+ window.clearInterval(waitTickRef.current)
68
+ waitTickRef.current = null
69
+ }
70
+ setAutoWaitMs(null)
71
+ }, [])
72
+
73
+ const neighbor = useCallback(
74
+ (delta: number) => explainNeighbor(explain, idRef.current, delta),
75
+ [explain],
76
+ )
77
+ neighborRef.current = neighbor
78
+
79
+ const scheduleAutoStep = useCallback(() => {
80
+ clearAutoTimer()
81
+ if (!autoStepRef.current || !tellingRef.current) return
82
+ if (!neighborRef.current(1)) return
83
+ const endsAt = Date.now() + AUTO_STEP_DELAY_MS
84
+ setAutoWaitMs(AUTO_STEP_DELAY_MS)
85
+ setAutoWaitToken((token) => token + 1)
86
+ waitTickRef.current = window.setInterval(() => {
87
+ const left = Math.max(0, endsAt - Date.now())
88
+ setAutoWaitMs(left)
89
+ if (left <= 0 && waitTickRef.current != null) {
90
+ window.clearInterval(waitTickRef.current)
91
+ waitTickRef.current = null
92
+ }
93
+ }, 100)
94
+ autoTimerRef.current = window.setTimeout(() => {
95
+ autoTimerRef.current = null
96
+ setAutoWaitMs(null)
97
+ if (!autoStepRef.current || !tellingRef.current) return
98
+ const next = neighborRef.current(1)
99
+ if (!next) return
100
+ goToRef.current(next)
101
+ }, AUTO_STEP_DELAY_MS)
102
+ }, [clearAutoTimer])
103
+
104
+ const speakCurrent = useCallback(
105
+ (stepId: string) => {
106
+ const item =
107
+ explain.steps.find((entry) => entry.index === stepId) ?? null
108
+ speakText(explainSpeechText(item), () => {
109
+ if (autoStepRef.current) scheduleAutoStep()
110
+ })
111
+ },
112
+ [explain.steps, scheduleAutoStep],
113
+ )
114
+
115
+ const goTo = useCallback(
116
+ (stepId: string) => {
117
+ if (!explain.steps.some((entry) => entry.index === stepId)) return
118
+ clearAutoTimer()
119
+ onStep(stepId)
120
+ if (!tellingRef.current) return
121
+ speakCurrent(stepId)
122
+ },
123
+ [clearAutoTimer, explain.steps, onStep, speakCurrent],
124
+ )
125
+ goToRef.current = goTo
126
+
127
+ const stopTelling = useCallback(() => {
128
+ tellingRef.current = false
129
+ autoStepRef.current = false
130
+ setTelling(false)
131
+ setAutoStep(false)
132
+ clearAutoTimer()
133
+ stopSpeaking()
134
+ }, [clearAutoTimer])
135
+
136
+ const startTelling = useCallback(
137
+ (withAutoStep: boolean) => {
138
+ if (!speechAvailable) return
139
+ tellingRef.current = true
140
+ autoStepRef.current = withAutoStep
141
+ setTelling(true)
142
+ setAutoStep(withAutoStep)
143
+ speakCurrent(id)
144
+ },
145
+ [id, speakCurrent, speechAvailable],
146
+ )
147
+
148
+ const tellAgain = () => {
149
+ if (!speechAvailable) return
150
+ tellingRef.current = true
151
+ setTelling(true)
152
+ clearAutoTimer()
153
+ speakCurrent(id)
154
+ }
155
+
156
+ const toggleTell = () => {
157
+ if (!speechAvailable) return
158
+ if (telling) {
159
+ stopTelling()
160
+ return
161
+ }
162
+ startTelling(false)
163
+ }
164
+
165
+ const toggleAutoStep = () => {
166
+ if (!speechAvailable) return
167
+ if (autoStep) {
168
+ autoStepRef.current = false
169
+ setAutoStep(false)
170
+ clearAutoTimer()
171
+ return
172
+ }
173
+ if (!telling) {
174
+ startTelling(true)
175
+ return
176
+ }
177
+ autoStepRef.current = true
178
+ setAutoStep(true)
179
+ if (!isSpeaking()) scheduleAutoStep()
180
+ }
181
+
182
+ const openAsk = () => {
183
+ stopTelling()
184
+ setAsking(true)
185
+ setDraft('')
186
+ }
187
+
188
+ const closeAsk = () => {
189
+ setAsking(false)
190
+ setDraft('')
191
+ }
192
+
193
+ const submitAsk = () => {
194
+ const question = draft.trim()
195
+ if (!question || !id) return
196
+ onAsk(id, question)
197
+ closeAsk()
198
+ }
199
+
200
+ useEffect(() => {
201
+ if (!asking) return
202
+ askInputRef.current?.focus()
203
+ }, [asking])
204
+
205
+ useEffect(() => {
206
+ if (!askingAbout && !explain.answering) return
207
+ closeAsk()
208
+ }, [askingAbout, explain.answering])
209
+
210
+ useEffect(() => subscribeSpeakStatus(setSpeakStatus), [])
211
+
212
+ useEffect(
213
+ () => () => {
214
+ clearAutoTimer()
215
+ stopSpeaking()
216
+ },
217
+ [clearAutoTimer],
218
+ )
219
+
220
+ useEffect(() => {
221
+ const onKey = (event: KeyboardEvent) => {
222
+ if (event.repeat) return
223
+ if (shouldIgnoreShortcut(event) && event.code !== 'Escape') return
224
+ if (event.code === 'Escape') {
225
+ event.preventDefault()
226
+ if (asking) {
227
+ closeAsk()
228
+ return
229
+ }
230
+ onExit()
231
+ return
232
+ }
233
+ if (shouldIgnoreShortcut(event)) return
234
+ if (!hasSteps) return
235
+ if (event.code === 'ArrowRight' || event.code === 'ArrowDown') {
236
+ event.preventDefault()
237
+ const next = neighbor(1)
238
+ if (next) goTo(next)
239
+ return
240
+ }
241
+ if (event.code === 'ArrowLeft' || event.code === 'ArrowUp') {
242
+ event.preventDefault()
243
+ const previous = neighbor(-1)
244
+ if (previous) goTo(previous)
245
+ }
246
+ }
247
+ window.addEventListener('keydown', onKey)
248
+ return () => window.removeEventListener('keydown', onKey)
249
+ }, [asking, goTo, hasSteps, neighbor, onExit])
250
+
251
+ useEffect(() => {
252
+ activeItemRef.current?.scrollIntoView({ block: 'nearest', behavior: 'smooth' })
253
+ }, [id])
254
+
255
+ useEffect(() => {
256
+ if (!step?.point) {
257
+ setPointerTop(null)
258
+ return
259
+ }
260
+ const frame = window.requestAnimationFrame(() => {
261
+ const item = activeItemRef.current
262
+ const hud = item?.closest('.explain-hud')
263
+ if (!item || !(hud instanceof HTMLElement)) return
264
+ const itemBox = item.getBoundingClientRect()
265
+ const hudBox = hud.getBoundingClientRect()
266
+ setPointerTop(itemBox.top + itemBox.height / 2 - hudBox.top)
267
+ })
268
+ return () => window.cancelAnimationFrame(frame)
269
+ }, [id, step?.point])
270
+
271
+ const prevId = neighbor(-1)
272
+ const nextId = neighbor(1)
273
+ const followUpParent = askingAbout?.parent ?? null
274
+
275
+ return (
276
+ <div className="hud explain-hud">
277
+ <header className="explain-header">
278
+ <p className="explain-question">
279
+ {explain.question || 'Explanation'}
280
+ </p>
281
+ <button
282
+ className="hud-button explain-exit"
283
+ type="button"
284
+ aria-label="Exit explain mode"
285
+ title="Exit explain mode"
286
+ onClick={onExit}
287
+ >
288
+ <svg
289
+ viewBox="0 0 24 24"
290
+ width="18"
291
+ height="18"
292
+ fill="none"
293
+ stroke="currentColor"
294
+ strokeWidth="2.2"
295
+ strokeLinecap="round"
296
+ aria-hidden="true"
297
+ >
298
+ <path d="M6 6l12 12" />
299
+ <path d="M18 6L6 18" />
300
+ </svg>
301
+ </button>
302
+ </header>
303
+
304
+ {starting ? (
305
+ <p className="explain-body">Preparing an explanation…</p>
306
+ ) : hasSteps ? (
307
+ <>
308
+ {preparingFollowUp && askingAbout ? (
309
+ <p className="explain-preparing" role="status">
310
+ Preparing an answer for step {askingAbout.from || askingAbout.parent}: {askingAbout.question}
311
+ </p>
312
+ ) : preparingFollowUp ? (
313
+ <p className="explain-preparing" role="status">
314
+ Preparing a closer look at this step…
315
+ </p>
316
+ ) : null}
317
+ <ol className="explain-steps" aria-label="Explanation steps">
318
+ {explain.steps.map((item) => {
319
+ const active = item.index === id
320
+ const depth = explainStepDepth(item.index)
321
+ const waitingHere =
322
+ followUpParent === item.index && preparingFollowUp
323
+ return (
324
+ <li
325
+ key={item.index}
326
+ ref={active ? activeItemRef : undefined}
327
+ data-active={active}
328
+ data-past={position >= 0 && explainStepPosition(explain, item.index) < position}
329
+ style={{ '--explain-depth': depth } as CSSProperties}
330
+ >
331
+ <button
332
+ className="hud-button explain-step"
333
+ type="button"
334
+ data-active={active}
335
+ aria-current={active ? 'step' : undefined}
336
+ onClick={() => goTo(item.index)}
337
+ >
338
+ <span className="explain-step-index">{item.index}</span>
339
+ <span className="explain-step-title">{item.title}</span>
340
+ </button>
341
+ {item.asked ? (
342
+ <p className="explain-asked">Asked: {item.asked}</p>
343
+ ) : null}
344
+ {active && item.body ? (
345
+ <p className="explain-body">{item.body}</p>
346
+ ) : null}
347
+ {waitingHere ? (
348
+ <p className="explain-preparing-step">Preparing sub-steps…</p>
349
+ ) : null}
350
+ {active && !preparingFollowUp ? (
351
+ asking ? (
352
+ <form
353
+ className="explain-ask-form"
354
+ onSubmit={(event) => {
355
+ event.preventDefault()
356
+ submitAsk()
357
+ }}
358
+ >
359
+ <label className="explain-ask-label">
360
+ <span>Question about this step</span>
361
+ <textarea
362
+ ref={askInputRef}
363
+ value={draft}
364
+ maxLength={4000}
365
+ rows={3}
366
+ placeholder="What should this step explain more closely?"
367
+ onChange={(event) => setDraft(event.target.value)}
368
+ onKeyDown={(event) => {
369
+ event.stopPropagation()
370
+ if (event.key === 'Escape') {
371
+ event.preventDefault()
372
+ closeAsk()
373
+ }
374
+ }}
375
+ />
376
+ </label>
377
+ <div className="explain-ask-actions">
378
+ <button
379
+ className="hud-button"
380
+ type="submit"
381
+ disabled={!draft.trim()}
382
+ >
383
+ Send question
384
+ </button>
385
+ <button
386
+ className="hud-button"
387
+ type="button"
388
+ onClick={closeAsk}
389
+ >
390
+ Cancel
391
+ </button>
392
+ </div>
393
+ </form>
394
+ ) : (
395
+ <button
396
+ className="hud-button explain-ask"
397
+ type="button"
398
+ onClick={openAsk}
399
+ >
400
+ Ask question
401
+ </button>
402
+ )
403
+ ) : null}
404
+ </li>
405
+ )
406
+ })}
407
+ </ol>
408
+ <div className="explain-footer">
409
+ {telling && speakStatus === 'loading' ? (
410
+ <p className="explain-voice-status">Loading a natural voice…</p>
411
+ ) : null}
412
+ <div className="explain-tell-controls">
413
+ <button
414
+ className="hud-button explain-tell"
415
+ type="button"
416
+ data-active={telling}
417
+ aria-pressed={telling}
418
+ aria-label={telling ? 'Stop telling' : 'Tell me'}
419
+ title={
420
+ speechAvailable
421
+ ? telling
422
+ ? 'Stop reading this explanation'
423
+ : 'Read this explanation aloud'
424
+ : 'Speech is not available in this browser'
425
+ }
426
+ disabled={!speechAvailable}
427
+ onClick={toggleTell}
428
+ >
429
+ {telling ? 'Stop' : 'Tell me'}
430
+ </button>
431
+ <button
432
+ className="hud-button explain-tell-again"
433
+ type="button"
434
+ aria-label="Tell again"
435
+ title={
436
+ speechAvailable
437
+ ? 'Read this step aloud again'
438
+ : 'Speech is not available in this browser'
439
+ }
440
+ disabled={!speechAvailable}
441
+ onClick={tellAgain}
442
+ >
443
+ Tell again
444
+ </button>
445
+ <button
446
+ className="hud-button explain-auto-step"
447
+ type="button"
448
+ data-active={autoStep}
449
+ aria-pressed={autoStep}
450
+ aria-label={
451
+ autoStep ? 'Turn off auto step' : 'Auto step after dictation'
452
+ }
453
+ title={
454
+ speechAvailable
455
+ ? autoStep
456
+ ? 'Stop advancing automatically'
457
+ : 'Advance to the next step 4 seconds after each dictation'
458
+ : 'Speech is not available in this browser'
459
+ }
460
+ disabled={!speechAvailable}
461
+ onClick={toggleAutoStep}
462
+ >
463
+ Auto step
464
+ </button>
465
+ </div>
466
+ {autoStep && autoWaitMs != null && nextId ? (
467
+ <div
468
+ className="explain-auto-wait"
469
+ role="status"
470
+ aria-live="polite"
471
+ aria-label={`Next step in ${Math.max(1, Math.ceil(autoWaitMs / 1000))} seconds`}
472
+ >
473
+ <div className="explain-auto-wait-track" aria-hidden="true">
474
+ <div
475
+ key={autoWaitToken}
476
+ className="explain-auto-wait-fill"
477
+ style={{ animationDuration: `${AUTO_STEP_DELAY_MS}ms` }}
478
+ />
479
+ </div>
480
+ <p className="explain-auto-wait-label">
481
+ {autoWaitMs <= 0
482
+ ? 'Next…'
483
+ : `Next in ${Math.ceil(autoWaitMs / 1000)}s`}
484
+ </p>
485
+ </div>
486
+ ) : null}
487
+ <div className="explain-nav-controls">
488
+ <button
489
+ className="hud-button"
490
+ type="button"
491
+ aria-label="Previous step"
492
+ disabled={!prevId}
493
+ onClick={() => prevId && goTo(prevId)}
494
+ >
495
+ Prev
496
+ </button>
497
+ <span className="explain-nav-position">
498
+ {id}
499
+ {count > 0 ? ` · ${position + 1}/${count}` : ''}
500
+ </span>
501
+ <button
502
+ className="hud-button"
503
+ type="button"
504
+ aria-label="Next step"
505
+ disabled={!nextId}
506
+ onClick={() => nextId && goTo(nextId)}
507
+ >
508
+ Next
509
+ </button>
510
+ </div>
511
+ </div>
512
+ </>
513
+ ) : null}
514
+ {step?.point && pointerTop != null ? (
515
+ <span
516
+ className="explain-pointer-origin"
517
+ data-explain-pointer-origin=""
518
+ aria-hidden="true"
519
+ style={{ top: pointerTop }}
520
+ />
521
+ ) : null}
522
+ </div>
523
+ )
524
+ }
@@ -0,0 +1,135 @@
1
+ import { useEffect, useRef } from 'react'
2
+ import {
3
+ explainHitsSymbol,
4
+ explainMatchesSymbol,
5
+ } from '../explain'
6
+ import type { ExplainSymbolRef, FileNode } from '../types'
7
+
8
+ function kindLabel(kind: FileNode['symbols'][number]['kind']) {
9
+ if (kind === 'variable') return 'Vars'
10
+ if (kind === 'class') return 'Classes'
11
+ return 'Functions'
12
+ }
13
+
14
+ function SymbolList({
15
+ title,
16
+ kind,
17
+ names,
18
+ highlights,
19
+ point,
20
+ }: {
21
+ title: string
22
+ kind: FileNode['symbols'][number]['kind']
23
+ names: string[]
24
+ highlights: ExplainSymbolRef[]
25
+ point: ExplainSymbolRef | null
26
+ }) {
27
+ const dimOthers = highlights.length > 0 || Boolean(point)
28
+ return (
29
+ <>
30
+ <div className="hud-section-title">{title}</div>
31
+ {names.length === 0 ? (
32
+ <p>No {title.toLowerCase()}</p>
33
+ ) : (
34
+ <ul>
35
+ {names.map((name) => {
36
+ const pointed = explainMatchesSymbol(point, kind, name)
37
+ const highlighted =
38
+ pointed || explainHitsSymbol(highlights, kind, name)
39
+ return (
40
+ <li
41
+ key={`${kind}-${name}`}
42
+ data-explain-target={pointed ? 'true' : undefined}
43
+ data-explain-highlight={highlighted ? 'true' : undefined}
44
+ data-explain-dim={dimOthers && !highlighted ? 'true' : undefined}
45
+ >
46
+ <span>{name}</span>
47
+ </li>
48
+ )
49
+ })}
50
+ </ul>
51
+ )}
52
+ </>
53
+ )
54
+ }
55
+
56
+ export function ExplainInfoPanel({
57
+ file,
58
+ highlights,
59
+ point,
60
+ }: {
61
+ file: FileNode
62
+ highlights: ExplainSymbolRef[]
63
+ point: ExplainSymbolRef | null
64
+ }) {
65
+ const bodyRef = useRef<HTMLDivElement | null>(null)
66
+ const classes = file.symbols
67
+ .filter((symbol) => symbol.kind === 'class')
68
+ .map((symbol) => symbol.name)
69
+ const functions = file.symbols
70
+ .filter((symbol) => symbol.kind === 'function')
71
+ .map((symbol) => symbol.name)
72
+ const variables = file.symbols
73
+ .filter((symbol) => symbol.kind === 'variable')
74
+ .map((symbol) => symbol.name)
75
+ const pointFile = explainMatchesSymbol(point, 'file', file.id)
76
+
77
+ useEffect(() => {
78
+ const target = bodyRef.current?.querySelector('[data-explain-target="true"]')
79
+ if (!(target instanceof HTMLElement)) return
80
+ target.scrollIntoView({ block: 'nearest', behavior: 'smooth' })
81
+ }, [file.id, point?.kind, point?.name])
82
+
83
+ return (
84
+ <div className="hud explain-info-stack">
85
+ <aside className="hud-panel hud-panel-info explain-info-panel">
86
+ <div className="hud-panel-chrome">
87
+ <div className="hud-panel-chrome-heading">
88
+ <div className="hud-panel-chrome-title-row">
89
+ <div
90
+ className="hud-panel-chrome-title"
91
+ data-explain-target={pointFile ? 'true' : undefined}
92
+ data-explain-highlight={
93
+ pointFile || explainHitsSymbol(highlights, 'file', file.id)
94
+ ? 'true'
95
+ : undefined
96
+ }
97
+ >
98
+ {file.name}
99
+ </div>
100
+ </div>
101
+ </div>
102
+ </div>
103
+ <div ref={bodyRef} className="hud-panel-body">
104
+ <p className="path">{file.path}</p>
105
+ <p>
106
+ {file.lines} lines · {file.language}
107
+ </p>
108
+ {classes.length > 0 && (
109
+ <SymbolList
110
+ title={kindLabel('class')}
111
+ kind="class"
112
+ names={classes}
113
+ highlights={highlights}
114
+ point={point}
115
+ />
116
+ )}
117
+ <SymbolList
118
+ title={kindLabel('function')}
119
+ kind="function"
120
+ names={functions}
121
+ highlights={highlights}
122
+ point={point}
123
+ />
124
+ <SymbolList
125
+ title={kindLabel('variable')}
126
+ kind="variable"
127
+ names={variables}
128
+ highlights={highlights}
129
+ point={point}
130
+ />
131
+ </div>
132
+ </aside>
133
+ </div>
134
+ )
135
+ }