@jkwd/inbase 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 (44) hide show
  1. package/README.md +76 -0
  2. package/apps/explorer/index.html +12 -0
  3. package/apps/explorer/package.json +28 -0
  4. package/apps/explorer/scripts/js-source.mjs +188 -0
  5. package/apps/explorer/scripts/patch-lib.d.ts +115 -0
  6. package/apps/explorer/scripts/patch-lib.mjs +472 -0
  7. package/apps/explorer/scripts/scan-target.mjs +188 -0
  8. package/apps/explorer/scripts/session-store.d.ts +156 -0
  9. package/apps/explorer/scripts/session-store.mjs +809 -0
  10. package/apps/explorer/scripts/target-config.d.ts +8 -0
  11. package/apps/explorer/scripts/target-config.mjs +42 -0
  12. package/apps/explorer/src/App.tsx +941 -0
  13. package/apps/explorer/src/agentIntent.ts +182 -0
  14. package/apps/explorer/src/codebase.ts +15 -0
  15. package/apps/explorer/src/index.css +632 -0
  16. package/apps/explorer/src/layout.ts +508 -0
  17. package/apps/explorer/src/main.tsx +16 -0
  18. package/apps/explorer/src/scene/BlockPlacer.tsx +87 -0
  19. package/apps/explorer/src/scene/Bridge.tsx +290 -0
  20. package/apps/explorer/src/scene/FileBlock.tsx +256 -0
  21. package/apps/explorer/src/scene/FolderArea.tsx +96 -0
  22. package/apps/explorer/src/scene/IslandPlacer.tsx +40 -0
  23. package/apps/explorer/src/scene/MapSelectBorder.tsx +57 -0
  24. package/apps/explorer/src/scene/MapView.tsx +247 -0
  25. package/apps/explorer/src/scene/Player.tsx +245 -0
  26. package/apps/explorer/src/scene/RelationLines.tsx +223 -0
  27. package/apps/explorer/src/scene/SelectionController.tsx +89 -0
  28. package/apps/explorer/src/scene/UserContextTracker.tsx +152 -0
  29. package/apps/explorer/src/scene/World.tsx +323 -0
  30. package/apps/explorer/src/theme.ts +111 -0
  31. package/apps/explorer/src/types.ts +245 -0
  32. package/apps/explorer/src/ui/CanvasErrorBoundary.tsx +38 -0
  33. package/apps/explorer/src/ui/HUD.tsx +1090 -0
  34. package/apps/explorer/src/ui/NameInput.tsx +45 -0
  35. package/apps/explorer/src/userContext.ts +73 -0
  36. package/apps/explorer/src/userCreated.ts +354 -0
  37. package/apps/explorer/src/vite-env.d.ts +1 -0
  38. package/apps/explorer/tsconfig.json +21 -0
  39. package/apps/explorer/vite.config.ts +295 -0
  40. package/bin/inbase.mjs +170 -0
  41. package/bin/project.mjs +94 -0
  42. package/bin/session.mjs +241 -0
  43. package/package.json +63 -0
  44. package/skill/inbase/SKILL.md +167 -0
@@ -0,0 +1,1090 @@
1
+ import { useEffect, useRef, useState } from 'react'
2
+ import { NameInput } from './NameInput'
3
+ import {
4
+ isReviewingIntent,
5
+ type AgentIntent,
6
+ type AgentIntentStatus,
7
+ type AimedRelation,
8
+ type CodebaseGraph,
9
+ type PatchImportAddition,
10
+ type PatchSymbolAddition,
11
+ type ViewMode,
12
+ type WorkflowAction,
13
+ } from '../types'
14
+
15
+ function reviewTitle(status: AgentIntentStatus) {
16
+ if (status === 'blueprint_ask') return 'Setup blueprint'
17
+ if (status === 'blueprint') return 'Blueprint'
18
+ if (status === 'preparing') return 'LLM preparing'
19
+ if (status === 'planned') return 'Plan ready'
20
+ if (status === 'working') return 'LLM working'
21
+ if (status === 'replanning') return 'LLM revising plan'
22
+ if (status === 'pending') return 'Review this step'
23
+ if (status === 'extended') return 'Extended diff'
24
+ if (status === 'approved') return 'Completed step'
25
+ if (status === 'finished') return 'Finished'
26
+ return 'Visual workflow'
27
+ }
28
+
29
+ function fileBase(id: string) {
30
+ return id.split('/').pop() ?? id
31
+ }
32
+
33
+ function symbolLabels(items: PatchSymbolAddition[]) {
34
+ const files = new Set(items.map((item) => item.file))
35
+ const showFile = files.size > 1
36
+ return items.map((item) => ({
37
+ key: `${item.file}:${item.name}`,
38
+ label: showFile ? `${item.name} · ${fileBase(item.file)}` : item.name,
39
+ }))
40
+ }
41
+
42
+ function importLabels(items: PatchImportAddition[]) {
43
+ const files = new Set(items.map((item) => item.file))
44
+ const showFile = files.size > 1
45
+ return items.map((item) => {
46
+ const what =
47
+ item.name === item.from ? item.from : `${item.name} from ${item.from}`
48
+ return {
49
+ key: `${item.file}:${item.name}:${item.from}`,
50
+ label: showFile ? `${what} · ${fileBase(item.file)}` : what,
51
+ }
52
+ })
53
+ }
54
+
55
+ function PanelList({
56
+ title,
57
+ items,
58
+ }: {
59
+ title: string
60
+ items: Array<{ key: string; label: string }>
61
+ }) {
62
+ if (items.length === 0) return null
63
+ return (
64
+ <>
65
+ <div className="hud-section-title">{title}</div>
66
+ <ul>
67
+ {items.map((item) => (
68
+ <li key={item.key}>{item.label}</li>
69
+ ))}
70
+ </ul>
71
+ </>
72
+ )
73
+ }
74
+
75
+ function AddIntentRow({
76
+ placeholder,
77
+ onAdd,
78
+ }: {
79
+ placeholder: string
80
+ onAdd: (value: string) => boolean
81
+ }) {
82
+ const [value, setValue] = useState('')
83
+ return (
84
+ <form
85
+ className="hud-add-row"
86
+ onSubmit={(event) => {
87
+ event.preventDefault()
88
+ if (onAdd(value)) setValue('')
89
+ }}
90
+ >
91
+ <input
92
+ value={value}
93
+ onChange={(event) => setValue(event.target.value)}
94
+ placeholder={placeholder}
95
+ aria-label={placeholder}
96
+ autoComplete="off"
97
+ spellCheck={false}
98
+ onKeyDown={(event) => event.stopPropagation()}
99
+ />
100
+ <button className="hud-button" type="submit">
101
+ Add
102
+ </button>
103
+ </form>
104
+ )
105
+ }
106
+
107
+ type HUDProps = {
108
+ graph: CodebaseGraph
109
+ mode: ViewMode
110
+ locked: boolean
111
+ selectedId: string | null
112
+ selectedFolder?: string | null
113
+ aimedRelation: AimedRelation | null
114
+ currentFolder: string
115
+ intent: AgentIntent
116
+ onWorkflowAction: (
117
+ action: WorkflowAction,
118
+ options?: { instruction?: string; step?: number },
119
+ ) => void
120
+ onNavigateDiff: (diffId: string) => void
121
+ onOpenMap: () => void
122
+ onWalk: () => void
123
+ followLook: boolean
124
+ onToggleFollowLook: () => void
125
+ importedBy: boolean
126
+ onToggleImportedBy: () => void
127
+ naming?: boolean
128
+ namingIsland?: boolean
129
+ onCommitIslandName?: (name: string) => void
130
+ onCancelIslandName?: () => void
131
+ blueprintFunctions?: PatchSymbolAddition[]
132
+ blueprintVariables?: PatchSymbolAddition[]
133
+ blueprintImports?: PatchImportAddition[]
134
+ onAddBlueprintFunction?: (fileId: string, name: string) => boolean
135
+ onAddBlueprintVariable?: (fileId: string, name: string) => boolean
136
+ onAddBlueprintImport?: (fileId: string, raw: string) => boolean
137
+ onRemoveBlueprintFunction?: (fileId: string, name: string) => void
138
+ onRemoveBlueprintVariable?: (fileId: string, name: string) => void
139
+ onRemoveBlueprintImport?: (
140
+ fileId: string,
141
+ name: string,
142
+ from: string,
143
+ ) => void
144
+ onMapAddFile?: (folderPath: string) => void
145
+ onMapAddFolder?: (folderPath: string) => void
146
+ }
147
+
148
+ export function HUD({
149
+ graph,
150
+ mode,
151
+ locked,
152
+ selectedId,
153
+ selectedFolder = null,
154
+ aimedRelation,
155
+ currentFolder,
156
+ intent,
157
+ onWorkflowAction,
158
+ onNavigateDiff,
159
+ onOpenMap,
160
+ onWalk,
161
+ followLook,
162
+ onToggleFollowLook,
163
+ importedBy,
164
+ onToggleImportedBy,
165
+ naming = false,
166
+ namingIsland = false,
167
+ onCommitIslandName,
168
+ onCancelIslandName,
169
+ blueprintFunctions = [],
170
+ blueprintVariables = [],
171
+ blueprintImports = [],
172
+ onAddBlueprintFunction,
173
+ onAddBlueprintVariable,
174
+ onAddBlueprintImport,
175
+ onRemoveBlueprintFunction,
176
+ onRemoveBlueprintVariable,
177
+ onRemoveBlueprintImport,
178
+ onMapAddFile,
179
+ onMapAddFolder,
180
+ }: HUDProps) {
181
+ const selected = graph.files.find((file) => file.id === selectedId)
182
+ const selectedFolderNode = graph.folders.find(
183
+ (folder) => folder.path === selectedFolder,
184
+ )
185
+ const folderFiles = selectedFolderNode
186
+ ? graph.files.filter(
187
+ (file) =>
188
+ selectedFolderNode.files.includes(file.id) ||
189
+ file.folder === selectedFolderNode.path,
190
+ )
191
+ : []
192
+ const aimed = graph.files.find((file) => file.id === aimedRelation?.flyTo)
193
+ const importers = selected
194
+ ? graph.files.filter((file) => file.imports.includes(selected.id))
195
+ : []
196
+ const mapping = mode === 'map'
197
+ const sessionMode = Boolean(intent.sessionId)
198
+ const [walkIntro, setWalkIntro] = useState(false)
199
+ const walkIntroSeen = useRef(false)
200
+ const [instruction, setInstruction] = useState('')
201
+ const [infoVisible, setInfoVisible] = useState(false)
202
+ const infoPanelRef = useRef<HTMLElement>(null)
203
+ const pending = intent.status === 'pending' && intent.isActiveDiff
204
+ const askingBlueprint = intent.status === 'blueprint_ask'
205
+ const creatingBlueprint = intent.creationMode || intent.status === 'blueprint'
206
+ const preparing = intent.status === 'preparing'
207
+ const planReady = intent.status === 'planned'
208
+ const working = intent.status === 'working' || intent.status === 'replanning'
209
+ const previewing = intent.preview
210
+ const reviewing = isReviewingIntent(intent.status) || intent.chain.length > 0
211
+ const chainIndex = intent.chainIndex ?? 0
212
+ const previousDiff = intent.chain[chainIndex - 1]
213
+ const nextDiff = intent.chain[chainIndex + 1]
214
+ const stepLabel =
215
+ intent.step && intent.steps?.length > 0
216
+ ? `Step ${intent.step} of ${intent.steps.length}`
217
+ : 'Patch'
218
+ const lastStep =
219
+ typeof intent.step === 'number' &&
220
+ intent.steps.length > 0 &&
221
+ intent.step >= intent.steps.length
222
+ const addedFunctions = intent.addedFunctions ?? []
223
+ const addedVariables = intent.addedVariables ?? []
224
+ const addedImports = intent.addedImports ?? []
225
+ const selectedAddedFunctions = selected
226
+ ? addedFunctions.filter((item) => item.file === selected.id)
227
+ : []
228
+ const selectedAddedVariables = selected
229
+ ? addedVariables.filter((item) => item.file === selected.id)
230
+ : []
231
+ const selectedAddedImports = selected
232
+ ? addedImports.filter((item) => item.file === selected.id)
233
+ : []
234
+ const selectedClasses = selected
235
+ ? selected.symbols.filter((symbol) => symbol.kind === 'class')
236
+ : []
237
+ const selectedFunctions = selected
238
+ ? selected.symbols.filter((symbol) => symbol.kind === 'function')
239
+ : []
240
+ const selectedVariables = selected
241
+ ? selected.symbols.filter((symbol) => symbol.kind === 'variable')
242
+ : []
243
+ const selectedBlueprintFunctions = selected
244
+ ? blueprintFunctions.filter((item) => item.file === selected.id)
245
+ : []
246
+ const selectedBlueprintVariables = selected
247
+ ? blueprintVariables.filter((item) => item.file === selected.id)
248
+ : []
249
+ const selectedBlueprintImports = selected
250
+ ? blueprintImports.filter((item) => item.file === selected.id)
251
+ : []
252
+ const intendedImportFrom = new Set(
253
+ selectedBlueprintImports.map((item) => item.from),
254
+ )
255
+ const extraBlueprintImports = selectedBlueprintImports.filter(
256
+ (item) => !selected?.imports.includes(item.from),
257
+ )
258
+ const canEditBlueprint =
259
+ creatingBlueprint && Boolean(selected) && !selected?.id.startsWith('draft:')
260
+ const doneSteps = new Set(
261
+ intent.status === 'finished'
262
+ ? intent.steps.map((step) => step.index)
263
+ : intent.chain
264
+ .filter(
265
+ (entry) =>
266
+ entry.status === 'applied' || entry.status === 'extended',
267
+ )
268
+ .map((entry) => entry.step),
269
+ )
270
+ if (intent.status === 'approved' && typeof intent.step === 'number') {
271
+ doneSteps.add(intent.step)
272
+ }
273
+ if (pending && typeof intent.step === 'number' && !lastStep) {
274
+ doneSteps.add(intent.step)
275
+ }
276
+ const nextStep =
277
+ intent.status === 'finished'
278
+ ? null
279
+ : (intent.steps.find((step) => !doneSteps.has(step.index)) ?? null)
280
+ const canRunNext =
281
+ Boolean(nextStep) && (planReady || pending) && !working
282
+ const canComplete = pending && lastStep
283
+ const panelDone =
284
+ intent.status === 'finished' ||
285
+ intent.status === 'approved' ||
286
+ doneSteps.size > 0
287
+
288
+ useEffect(() => {
289
+ if (mode !== 'walk') {
290
+ walkIntroSeen.current = false
291
+ setWalkIntro(false)
292
+ return
293
+ }
294
+ if (locked || naming) {
295
+ walkIntroSeen.current = true
296
+ setWalkIntro(false)
297
+ return
298
+ }
299
+ if (walkIntroSeen.current) {
300
+ setWalkIntro(false)
301
+ return
302
+ }
303
+ const timer = window.setTimeout(() => setWalkIntro(true), 160)
304
+ return () => window.clearTimeout(timer)
305
+ }, [locked, mode, naming])
306
+
307
+ useEffect(() => {
308
+ setInstruction('')
309
+ }, [intent.diffId])
310
+
311
+ useEffect(() => {
312
+ infoPanelRef.current?.scrollTo({ top: 0 })
313
+ }, [selectedId, selectedFolder])
314
+
315
+ useEffect(() => {
316
+ if (sessionMode && selectedId) setInfoVisible(true)
317
+ }, [selectedId, sessionMode])
318
+
319
+ useEffect(() => {
320
+ if (selectedFolder) setInfoVisible(true)
321
+ }, [selectedFolder])
322
+
323
+ useEffect(() => {
324
+ const onKey = (event: KeyboardEvent) => {
325
+ if (event.repeat || event.code !== 'KeyI') return
326
+ const target = event.target
327
+ if (
328
+ target instanceof HTMLElement &&
329
+ (target.tagName === 'TEXTAREA' ||
330
+ target.tagName === 'INPUT' ||
331
+ target.tagName === 'SELECT' ||
332
+ target.isContentEditable)
333
+ ) {
334
+ return
335
+ }
336
+ event.preventDefault()
337
+ setInfoVisible((visible) => !visible)
338
+ }
339
+ window.addEventListener('keydown', onKey)
340
+ return () => window.removeEventListener('keydown', onKey)
341
+ }, [])
342
+
343
+ useEffect(() => {
344
+ if (!infoVisible || (!selectedId && !selectedFolder)) return
345
+ const onKey = (event: KeyboardEvent) => {
346
+ if (event.code !== 'ArrowUp' && event.code !== 'ArrowDown') return
347
+ const target = event.target
348
+ if (
349
+ target instanceof HTMLElement &&
350
+ (target.tagName === 'TEXTAREA' ||
351
+ target.tagName === 'INPUT' ||
352
+ target.tagName === 'SELECT' ||
353
+ target.isContentEditable)
354
+ ) {
355
+ return
356
+ }
357
+ const panel = infoPanelRef.current
358
+ if (!panel || panel.scrollHeight <= panel.clientHeight + 1) return
359
+ event.preventDefault()
360
+ event.stopImmediatePropagation()
361
+ const step = Math.max(40, Math.round(panel.clientHeight * 0.2))
362
+ panel.scrollBy({ top: event.code === 'ArrowDown' ? step : -step })
363
+ }
364
+ window.addEventListener('keydown', onKey, true)
365
+ return () => window.removeEventListener('keydown', onKey, true)
366
+ }, [infoVisible, selectedId, selectedFolder])
367
+
368
+ return (
369
+ <div className="hud">
370
+ {mode === 'walk' && !locked && walkIntro && !naming && (
371
+ <div className="hud-gate">
372
+ <div className="hud-gate-card">
373
+ <h1>Walk</h1>
374
+ <p>
375
+ Click to look around. Press <kbd>M</kbd> to open the map, press{' '}
376
+ <kbd>M</kbd> again to return here.
377
+ </p>
378
+ <p>
379
+ <kbd>W</kbd> <kbd>A</kbd> <kbd>S</kbd> <kbd>D</kbd> walk,{' '}
380
+ <kbd>Shift</kbd> sprint
381
+ {creatingBlueprint ? (
382
+ <>
383
+ , <kbd>Space</kbd> place a file, <kbd>B</kbd> place an island
384
+ </>
385
+ ) : null}
386
+ , click a block for imports.
387
+ </p>
388
+ </div>
389
+ </div>
390
+ )}
391
+
392
+ {namingIsland && onCommitIslandName && onCancelIslandName && (
393
+ <div className="hud-name-gate">
394
+ <NameInput
395
+ placeholder="Folder name"
396
+ onCommit={onCommitIslandName}
397
+ onCancel={onCancelIslandName}
398
+ />
399
+ </div>
400
+ )}
401
+
402
+ {mode === 'walk' && locked && (
403
+ <div className="crosshair" data-aim={Boolean(aimed)} />
404
+ )}
405
+ {mode === 'walk' && locked && aimed && (
406
+ <div className="hud-aim">Click to fly to {aimed.name}</div>
407
+ )}
408
+
409
+ <div className="hud-top">
410
+ <div className="hud-chip">
411
+ {mapping ? `Map of ${graph.targetName}` : `You are in ${currentFolder}`}
412
+ </div>
413
+ <div className="hud-mode">
414
+ <button
415
+ className="hud-button"
416
+ data-active={mapping}
417
+ type="button"
418
+ onClick={onOpenMap}
419
+ >
420
+ Map
421
+ </button>
422
+ <button
423
+ className="hud-button"
424
+ data-active={!mapping}
425
+ type="button"
426
+ onClick={onWalk}
427
+ >
428
+ Walk
429
+ </button>
430
+ </div>
431
+ {selected && <div className="hud-chip">{selected.path}</div>}
432
+ </div>
433
+
434
+ {reviewing && (
435
+ <aside
436
+ className={
437
+ panelDone
438
+ ? 'hud-panel hud-panel-planned hud-panel-done'
439
+ : 'hud-panel hud-panel-planned'
440
+ }
441
+ >
442
+ <div className="hud-section-title">
443
+ {reviewTitle(intent.status)}
444
+ </div>
445
+ {intent.feature && <p className="hud-feature">{intent.feature}</p>}
446
+ {askingBlueprint ? (
447
+ <>
448
+ <p>
449
+ {intent.canEnterBlueprint
450
+ ? 'Place files and islands for this chat, then send them as a blueprint for the LLM?'
451
+ : `Blueprint edit mode is active in another chat (${intent.blueprintSessionId}). Finish or stop it before starting here.`}
452
+ </p>
453
+ <div className="hud-decide">
454
+ <button
455
+ className="hud-button hud-button-approve"
456
+ type="button"
457
+ disabled={!intent.canEnterBlueprint}
458
+ onClick={() => onWorkflowAction('blueprint_yes')}
459
+ >
460
+ Yes
461
+ </button>
462
+ <button
463
+ className="hud-button"
464
+ type="button"
465
+ onClick={() => onWorkflowAction('blueprint_no')}
466
+ >
467
+ No
468
+ </button>
469
+ <button
470
+ className="hud-button hud-button-reject"
471
+ type="button"
472
+ onClick={() => onWorkflowAction('stop')}
473
+ >
474
+ Stop
475
+ </button>
476
+ </div>
477
+ </>
478
+ ) : creatingBlueprint ? (
479
+ <>
480
+ <p>
481
+ Walk the map, press <kbd>Space</kbd> for a file and{' '}
482
+ <kbd>B</kbd> for an island. Send the blueprint when the layout
483
+ is ready.
484
+ </p>
485
+ <div className="hud-decide">
486
+ <button
487
+ className="hud-button hud-button-approve"
488
+ type="button"
489
+ disabled={naming}
490
+ onClick={() => onWorkflowAction('blueprint_send')}
491
+ >
492
+ Send blueprint
493
+ </button>
494
+ <button
495
+ className="hud-button hud-button-reject"
496
+ type="button"
497
+ onClick={() => onWorkflowAction('stop')}
498
+ >
499
+ Stop
500
+ </button>
501
+ </div>
502
+ </>
503
+ ) : intent.status === 'finished' ? (
504
+ <p>All plan steps were applied.</p>
505
+ ) : preparing ? (
506
+ <div className="hud-working">
507
+ <span className="hud-spinner" aria-hidden="true" />
508
+ <span>LLM preparing…</span>
509
+ </div>
510
+ ) : working ? (
511
+ <div className="hud-working">
512
+ <span className="hud-spinner" aria-hidden="true" />
513
+ <span>
514
+ {intent.status === 'replanning'
515
+ ? 'Updating the remaining plan from your instruction…'
516
+ : `Implementing ${stepLabel.toLowerCase()}…`}
517
+ </span>
518
+ </div>
519
+ ) : (
520
+ <p>
521
+ {stepLabel}
522
+ {intent.reason ? ` · ${intent.reason}` : ''}
523
+ </p>
524
+ )}
525
+ {!askingBlueprint && !creatingBlueprint && (
526
+ <>
527
+ {intent.steps?.length > 0 && (
528
+ <ol className="hud-steps">
529
+ {intent.steps.map((step) => (
530
+ <li
531
+ key={step.index}
532
+ data-current={
533
+ intent.status !== 'finished' &&
534
+ step.index === intent.step &&
535
+ !doneSteps.has(step.index)
536
+ }
537
+ data-done={doneSteps.has(step.index)}
538
+ data-next={
539
+ nextStep?.index === step.index && !doneSteps.has(step.index)
540
+ }
541
+ >
542
+ <span className="hud-step-main">
543
+ <span className="hud-step-title">{step.title}</span>
544
+ {((canRunNext && nextStep?.index === step.index) ||
545
+ (canComplete && step.index === intent.step)) && (
546
+ <button
547
+ className="hud-button hud-button-approve hud-run-step"
548
+ type="button"
549
+ onClick={() =>
550
+ canComplete && step.index === intent.step
551
+ ? onWorkflowAction('continue')
552
+ : onWorkflowAction('invoke', {
553
+ step: step.index,
554
+ })
555
+ }
556
+ >
557
+ {canComplete && step.index === intent.step
558
+ ? 'Complete'
559
+ : 'Run step'}
560
+ </button>
561
+ )}
562
+ </span>
563
+ </li>
564
+ ))}
565
+ </ol>
566
+ )}
567
+ {intent.chain.length > 0 && (
568
+ <div className="hud-chain">
569
+ <button
570
+ className="hud-button"
571
+ type="button"
572
+ disabled={!previousDiff}
573
+ onClick={() => previousDiff && onNavigateDiff(previousDiff.id)}
574
+ >
575
+ Previous
576
+ </button>
577
+ <span>
578
+ Diff {chainIndex + 1} of {intent.chain.length}
579
+ </span>
580
+ <button
581
+ className="hud-button"
582
+ type="button"
583
+ disabled={!nextDiff}
584
+ onClick={() => nextDiff && onNavigateDiff(nextDiff.id)}
585
+ >
586
+ Next
587
+ </button>
588
+ </div>
589
+ )}
590
+ {previewing && intent.files.length > 0 && (
591
+ <>
592
+ <div className="hud-section-title hud-section-title-edit">Changed</div>
593
+ <ul>
594
+ {intent.files.map((id) => (
595
+ <li className="hud-file-edit" key={id}>
596
+ {id}
597
+ </li>
598
+ ))}
599
+ </ul>
600
+ </>
601
+ )}
602
+ {previewing && (intent.createFolders ?? []).length > 0 && (
603
+ <>
604
+ <div className="hud-section-title hud-section-title-add">Added islands</div>
605
+ <ul>
606
+ {intent.createFolders.map((id) => (
607
+ <li className="hud-file-add" key={id}>
608
+ {id}/
609
+ </li>
610
+ ))}
611
+ </ul>
612
+ </>
613
+ )}
614
+ {previewing && intent.creates.length > 0 && (
615
+ <>
616
+ <div className="hud-section-title hud-section-title-add">Added</div>
617
+ <ul>
618
+ {intent.creates.map((id) => (
619
+ <li className="hud-file-add" key={id}>
620
+ {id}
621
+ </li>
622
+ ))}
623
+ </ul>
624
+ </>
625
+ )}
626
+ {previewing && intent.deletes.length > 0 && (
627
+ <>
628
+ <div className="hud-section-title hud-section-title-remove">Removed</div>
629
+ <ul>
630
+ {intent.deletes.map((id) => (
631
+ <li className="hud-file-remove" key={id}>
632
+ {id}
633
+ </li>
634
+ ))}
635
+ </ul>
636
+ </>
637
+ )}
638
+ {previewing && (
639
+ <>
640
+ <PanelList
641
+ title="Functions"
642
+ items={symbolLabels(addedFunctions)}
643
+ />
644
+ <PanelList
645
+ title="Variables"
646
+ items={symbolLabels(addedVariables)}
647
+ />
648
+ </>
649
+ )}
650
+ {previewing && addedImports.length > 0 && (
651
+ <PanelList title="Imports" items={importLabels(addedImports)} />
652
+ )}
653
+ {previewing && addedImports.length === 0 && (intent.imports ?? []).length > 0 && (
654
+ <>
655
+ <div className="hud-section-title">Imports</div>
656
+ <ul>
657
+ {intent.imports.map((edge) => (
658
+ <li key={`${edge.from}->${edge.to}`}>
659
+ {edge.from.split('/').pop()} → {edge.to.split('/').pop()}
660
+ </li>
661
+ ))}
662
+ </ul>
663
+ </>
664
+ )}
665
+ {(planReady || preparing) && (
666
+ <div className="hud-decide">
667
+ <button
668
+ className="hud-button hud-button-reject"
669
+ type="button"
670
+ onClick={() => onWorkflowAction('stop')}
671
+ >
672
+ Stop
673
+ </button>
674
+ </div>
675
+ )}
676
+ {pending && (
677
+ <>
678
+ <label className="hud-instruction">
679
+ <span>Alternative instruction for the LLM</span>
680
+ <textarea
681
+ value={instruction}
682
+ maxLength={4000}
683
+ rows={3}
684
+ placeholder="Describe what should change in the next diff…"
685
+ onChange={(event) => setInstruction(event.target.value)}
686
+ />
687
+ </label>
688
+ <div className="hud-decide">
689
+ {lastStep && (
690
+ <button
691
+ className="hud-button hud-button-approve"
692
+ type="button"
693
+ onClick={() => onWorkflowAction('continue')}
694
+ >
695
+ Complete
696
+ </button>
697
+ )}
698
+ <button
699
+ className="hud-button hud-button-extend"
700
+ type="button"
701
+ disabled={!instruction.trim()}
702
+ onClick={() =>
703
+ onWorkflowAction('instruct', { instruction })
704
+ }
705
+ >
706
+ Send instruction
707
+ </button>
708
+ <button
709
+ className="hud-button hud-button-reject"
710
+ type="button"
711
+ onClick={() => onWorkflowAction('stop')}
712
+ >
713
+ Stop
714
+ </button>
715
+ </div>
716
+ </>
717
+ )}
718
+ </>
719
+ )}
720
+ </aside>
721
+ )}
722
+
723
+ {selected && infoVisible && (
724
+ <aside ref={infoPanelRef} className="hud-panel hud-panel-info">
725
+ <h2>{selected.name}</h2>
726
+ <p className="path">{selected.path}</p>
727
+ <p>
728
+ {selected.lines} lines · {selected.language}
729
+ </p>
730
+ {selectedClasses.length > 0 && (
731
+ <>
732
+ <div className="hud-section-title">Classes</div>
733
+ <ul>
734
+ {selectedClasses.map((symbol) => (
735
+ <li key={`class-${symbol.name}`}>
736
+ <span className={symbol.intended ? 'hud-intended' : undefined}>
737
+ {symbol.name}
738
+ </span>
739
+ </li>
740
+ ))}
741
+ </ul>
742
+ </>
743
+ )}
744
+ <div className="hud-section-title">Functions</div>
745
+ {selectedFunctions.length === 0 &&
746
+ selectedBlueprintFunctions.length === 0 ? (
747
+ <p>No functions</p>
748
+ ) : (
749
+ <ul>
750
+ {selectedFunctions.map((symbol) => (
751
+ <li key={`fn-${symbol.name}`}>
752
+ <span className={symbol.intended ? 'hud-intended' : undefined}>
753
+ {symbol.name}
754
+ </span>
755
+ {canEditBlueprint && symbol.intended && (
756
+ <button
757
+ className="hud-item-remove"
758
+ type="button"
759
+ aria-label={`Remove ${symbol.name}`}
760
+ onClick={() =>
761
+ onRemoveBlueprintFunction?.(selected.id, symbol.name)
762
+ }
763
+ >
764
+ ×
765
+ </button>
766
+ )}
767
+ </li>
768
+ ))}
769
+ </ul>
770
+ )}
771
+ {canEditBlueprint && onAddBlueprintFunction && (
772
+ <AddIntentRow
773
+ placeholder="Function name"
774
+ onAdd={(name) => onAddBlueprintFunction(selected.id, name)}
775
+ />
776
+ )}
777
+ <div className="hud-section-title">Vars</div>
778
+ {selectedVariables.length === 0 &&
779
+ selectedBlueprintVariables.length === 0 ? (
780
+ <p>No vars</p>
781
+ ) : (
782
+ <ul>
783
+ {selectedVariables.map((symbol) => (
784
+ <li key={`var-${symbol.name}`}>
785
+ <span className={symbol.intended ? 'hud-intended' : undefined}>
786
+ {symbol.name}
787
+ </span>
788
+ {canEditBlueprint && symbol.intended && (
789
+ <button
790
+ className="hud-item-remove"
791
+ type="button"
792
+ aria-label={`Remove ${symbol.name}`}
793
+ onClick={() =>
794
+ onRemoveBlueprintVariable?.(selected.id, symbol.name)
795
+ }
796
+ >
797
+ ×
798
+ </button>
799
+ )}
800
+ </li>
801
+ ))}
802
+ </ul>
803
+ )}
804
+ {canEditBlueprint && onAddBlueprintVariable && (
805
+ <AddIntentRow
806
+ placeholder="Variable name"
807
+ onAdd={(name) => onAddBlueprintVariable(selected.id, name)}
808
+ />
809
+ )}
810
+ {previewing && (
811
+ <>
812
+ <PanelList
813
+ title="Added functions"
814
+ items={symbolLabels(selectedAddedFunctions)}
815
+ />
816
+ <PanelList
817
+ title="Added variables"
818
+ items={symbolLabels(selectedAddedVariables)}
819
+ />
820
+ <PanelList
821
+ title="Added imports"
822
+ items={importLabels(selectedAddedImports)}
823
+ />
824
+ </>
825
+ )}
826
+ <div className="hud-section-title">
827
+ {importedBy ? 'Imported by' : 'Imports'}
828
+ </div>
829
+ {importedBy ? (
830
+ importers.length === 0 ? (
831
+ <p>Nothing local imports this</p>
832
+ ) : (
833
+ <ul>
834
+ {importers.map((file) => (
835
+ <li key={file.id}>{file.id}</li>
836
+ ))}
837
+ </ul>
838
+ )
839
+ ) : selected.imports.length === 0 && extraBlueprintImports.length === 0 ? (
840
+ <p>No local imports</p>
841
+ ) : (
842
+ <ul>
843
+ {selected.imports.map((id) => (
844
+ <li key={id}>
845
+ <span
846
+ className={
847
+ intendedImportFrom.has(id) ? 'hud-intended' : undefined
848
+ }
849
+ >
850
+ {id}
851
+ </span>
852
+ {canEditBlueprint && intendedImportFrom.has(id) && (
853
+ <button
854
+ className="hud-item-remove"
855
+ type="button"
856
+ aria-label={`Remove import ${id}`}
857
+ onClick={() => {
858
+ const item = selectedBlueprintImports.find(
859
+ (entry) => entry.from === id,
860
+ )
861
+ if (item) {
862
+ onRemoveBlueprintImport?.(
863
+ selected.id,
864
+ item.name,
865
+ item.from,
866
+ )
867
+ }
868
+ }}
869
+ >
870
+ ×
871
+ </button>
872
+ )}
873
+ </li>
874
+ ))}
875
+ {extraBlueprintImports.map((item) => (
876
+ <li key={`bp-${item.name}-${item.from}`}>
877
+ <span className="hud-intended">
878
+ {item.name === item.from
879
+ ? item.from
880
+ : `${item.name} from ${item.from}`}
881
+ </span>
882
+ {canEditBlueprint && (
883
+ <button
884
+ className="hud-item-remove"
885
+ type="button"
886
+ aria-label={`Remove import ${item.name}`}
887
+ onClick={() =>
888
+ onRemoveBlueprintImport?.(
889
+ selected.id,
890
+ item.name,
891
+ item.from,
892
+ )
893
+ }
894
+ >
895
+ ×
896
+ </button>
897
+ )}
898
+ </li>
899
+ ))}
900
+ </ul>
901
+ )}
902
+ {canEditBlueprint && !importedBy && onAddBlueprintImport && (
903
+ <AddIntentRow
904
+ placeholder="Clock from src/components/Clock.tsx"
905
+ onAdd={(raw) => onAddBlueprintImport(selected.id, raw)}
906
+ />
907
+ )}
908
+ </aside>
909
+ )}
910
+
911
+ {!selected && selectedFolderNode && infoVisible && (
912
+ <aside ref={infoPanelRef} className="hud-panel hud-panel-info">
913
+ <h2>{selectedFolderNode.name}</h2>
914
+ <p className="path">
915
+ {selectedFolderNode.path === '.'
916
+ ? graph.targetName
917
+ : selectedFolderNode.path}
918
+ </p>
919
+ <p>
920
+ {folderFiles.length} {folderFiles.length === 1 ? 'file' : 'files'}
921
+ </p>
922
+ <div className="hud-section-title">Files</div>
923
+ {folderFiles.length === 0 ? (
924
+ <p>No files on this island</p>
925
+ ) : (
926
+ <ul>
927
+ {folderFiles.map((file) => (
928
+ <li key={file.id}>{file.path || file.id}</li>
929
+ ))}
930
+ </ul>
931
+ )}
932
+ {creatingBlueprint && mapping && onMapAddFile && onMapAddFolder && (
933
+ <div className="hud-decide hud-map-blueprint">
934
+ <button
935
+ className="hud-button hud-button-approve"
936
+ type="button"
937
+ disabled={naming}
938
+ onClick={() => onMapAddFile(selectedFolderNode.path)}
939
+ >
940
+ Add file
941
+ </button>
942
+ <button
943
+ className="hud-button"
944
+ type="button"
945
+ disabled={naming}
946
+ onClick={() => onMapAddFolder(selectedFolderNode.path)}
947
+ >
948
+ Add folder
949
+ </button>
950
+ </div>
951
+ )}
952
+ </aside>
953
+ )}
954
+
955
+ <div className="hud-bottom">
956
+ <div className="hud-hints">
957
+ {mapping ? (
958
+ <>
959
+ <span>Scroll zoom</span>
960
+ <span>Drag pan</span>
961
+ <span>
962
+ {sessionMode
963
+ ? 'Click a block for info'
964
+ : 'Click a block for relations'}
965
+ </span>
966
+ <span>Click an island for its files</span>
967
+ {creatingBlueprint && (
968
+ <>
969
+ <span>Select an island</span>
970
+ <span>Add file / Add folder in panel</span>
971
+ </>
972
+ )}
973
+ <span>Click a line to fly there</span>
974
+ <span>Double-click ground to walk</span>
975
+ {selected?.userCreated && creatingBlueprint && (
976
+ <span>Backspace delete</span>
977
+ )}
978
+ <span>
979
+ {sessionMode
980
+ ? infoVisible
981
+ ? 'I hide info'
982
+ : 'Click a block for info'
983
+ : infoVisible
984
+ ? 'I hide info'
985
+ : 'I show info'}
986
+ </span>
987
+ {infoVisible && <span>↑↓ scroll info</span>}
988
+ <span>
989
+ {importedBy ? 'K show imports' : 'K show imported by'}
990
+ </span>
991
+ <span>M back to walk</span>
992
+ </>
993
+ ) : (
994
+ <>
995
+ <span>WASD walk</span>
996
+ <span>Mouse look</span>
997
+ <span>Shift sprint</span>
998
+ {creatingBlueprint && (
999
+ <>
1000
+ <span>Space place file</span>
1001
+ <span>B place island</span>
1002
+ </>
1003
+ )}
1004
+ {selected?.userCreated && creatingBlueprint && (
1005
+ <span>Backspace delete</span>
1006
+ )}
1007
+ <span>
1008
+ {sessionMode
1009
+ ? 'Click a block for info'
1010
+ : 'Click block for relations'}
1011
+ </span>
1012
+ <span>Aim a line to fly</span>
1013
+ <span>
1014
+ {sessionMode
1015
+ ? infoVisible
1016
+ ? 'I hide info'
1017
+ : 'Click a block for info'
1018
+ : infoVisible
1019
+ ? 'I hide info'
1020
+ : 'I show info'}
1021
+ </span>
1022
+ {infoVisible && <span>↑↓ scroll info</span>}
1023
+ <span>
1024
+ {importedBy ? 'K show imports' : 'K show imported by'}
1025
+ </span>
1026
+ <span>M toggle map</span>
1027
+ <span>Esc release mouse</span>
1028
+ </>
1029
+ )}
1030
+ </div>
1031
+ <div className="hud-icon-row">
1032
+ <button
1033
+ className="hud-button hud-icon-button"
1034
+ data-active={importedBy}
1035
+ aria-label={
1036
+ importedBy ? 'Show imports' : 'Show imported by'
1037
+ }
1038
+ aria-keyshortcuts="K"
1039
+ aria-pressed={importedBy}
1040
+ type="button"
1041
+ onClick={onToggleImportedBy}
1042
+ >
1043
+ <svg
1044
+ viewBox="0 0 24 24"
1045
+ width="18"
1046
+ height="18"
1047
+ fill="none"
1048
+ stroke="currentColor"
1049
+ strokeWidth="2"
1050
+ strokeLinecap="round"
1051
+ strokeLinejoin="round"
1052
+ aria-hidden="true"
1053
+ >
1054
+ <rect x="13" y="6" width="8" height="12" rx="1.5" />
1055
+ <path d="M3 12h8" />
1056
+ <path d="M8 8l4 4-4 4" />
1057
+ </svg>
1058
+ <span className="hud-tooltip">
1059
+ {importedBy ? 'K show imports' : 'K show imported by'}
1060
+ </span>
1061
+ </button>
1062
+ <button
1063
+ className="hud-button hud-icon-button"
1064
+ data-active={followLook}
1065
+ aria-label="Make LLM look where I look"
1066
+ aria-pressed={followLook}
1067
+ type="button"
1068
+ onClick={onToggleFollowLook}
1069
+ >
1070
+ <svg
1071
+ viewBox="0 0 24 24"
1072
+ width="18"
1073
+ height="18"
1074
+ fill="none"
1075
+ stroke="currentColor"
1076
+ strokeWidth="2"
1077
+ strokeLinecap="round"
1078
+ strokeLinejoin="round"
1079
+ aria-hidden="true"
1080
+ >
1081
+ <path d="M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7-10-7-10-7Z" />
1082
+ <circle cx="12" cy="12" r="3" />
1083
+ </svg>
1084
+ <span className="hud-tooltip">Make LLM look where I look</span>
1085
+ </button>
1086
+ </div>
1087
+ </div>
1088
+ </div>
1089
+ )
1090
+ }