@jkwd/inbase 0.1.4 → 0.1.6

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.
@@ -1,6 +1,8 @@
1
1
  import { useEffect, useRef, useState, type ReactNode } from 'react'
2
2
  import { NameInput } from './NameInput'
3
+ import { SelectionThumbnail } from '../scene/SelectionThumbnail'
3
4
  import {
5
+ canStopSession,
4
6
  isReviewingIntent,
5
7
  type AgentIntent,
6
8
  type AgentIntentStatus,
@@ -9,6 +11,7 @@ import {
9
11
  type PatchImportAddition,
10
12
  type PatchSymbolAddition,
11
13
  type ViewMode,
14
+ type WorldLayout,
12
15
  type WorkflowAction,
13
16
  } from '../types'
14
17
 
@@ -55,17 +58,83 @@ function importLabels(items: PatchImportAddition[]) {
55
58
  function PanelList({
56
59
  title,
57
60
  items,
61
+ tone,
58
62
  }: {
59
63
  title: string
60
64
  items: Array<{ key: string; label: string }>
65
+ tone?: 'add' | 'edit' | 'remove'
61
66
  }) {
62
67
  if (items.length === 0) return null
68
+ const titleClass =
69
+ tone === 'add'
70
+ ? 'hud-section-title hud-section-title-add'
71
+ : tone === 'edit'
72
+ ? 'hud-section-title hud-section-title-edit'
73
+ : tone === 'remove'
74
+ ? 'hud-section-title hud-section-title-remove'
75
+ : 'hud-section-title'
76
+ const itemClass =
77
+ tone === 'add'
78
+ ? 'hud-file-add'
79
+ : tone === 'edit'
80
+ ? 'hud-file-edit'
81
+ : tone === 'remove'
82
+ ? 'hud-file-remove'
83
+ : undefined
63
84
  return (
64
85
  <>
65
- <div className="hud-section-title">{title}</div>
86
+ <div className={titleClass}>{title}</div>
66
87
  <ul>
67
88
  {items.map((item) => (
68
- <li key={item.key}>{item.label}</li>
89
+ <li className={itemClass} key={item.key}>
90
+ {item.label}
91
+ </li>
92
+ ))}
93
+ </ul>
94
+ </>
95
+ )
96
+ }
97
+
98
+ function symbolChangeClass(kind: 'add' | 'edit' | null | undefined) {
99
+ if (kind === 'add') return 'hud-file-add'
100
+ if (kind === 'edit') return 'hud-file-edit'
101
+ return undefined
102
+ }
103
+
104
+ function extraAddedSymbols(
105
+ existing: Array<{ name: string }>,
106
+ added: PatchSymbolAddition[],
107
+ ) {
108
+ const have = new Set(existing.map((item) => item.name))
109
+ return added.filter((item) => !have.has(item.name))
110
+ }
111
+
112
+ function PatchSymbolChanges({
113
+ title,
114
+ added,
115
+ changed,
116
+ }: {
117
+ title: string
118
+ added: PatchSymbolAddition[]
119
+ changed: PatchSymbolAddition[]
120
+ }) {
121
+ if (added.length === 0 && changed.length === 0) return null
122
+ const addedNames = new Set(added.map((item) => item.name))
123
+ return (
124
+ <>
125
+ <div className="hud-section-title">{title}</div>
126
+ <ul>
127
+ {changed
128
+ .filter((item) => !addedNames.has(item.name))
129
+ .map((item) => (
130
+ <li className="hud-file-edit" key={`edit-${item.file}:${item.name}`}>
131
+ {item.name}
132
+ </li>
133
+ ))}
134
+ {added.map((item) => (
135
+ <li className="hud-file-add" key={`add-${item.file}:${item.name}`}>
136
+ {item.name}
137
+ </li>
69
138
  ))}
70
139
  </ul>
71
140
  </>
@@ -123,27 +192,538 @@ function AddIntentRow({
123
192
  )
124
193
  }
125
194
 
195
+ function PanelChrome({
196
+ title,
197
+ minimized = false,
198
+ onMinimize,
199
+ onClose,
200
+ }: {
201
+ title: ReactNode
202
+ minimized?: boolean
203
+ onMinimize?: () => void
204
+ onClose?: () => void
205
+ }) {
206
+ return (
207
+ <div className="hud-panel-chrome">
208
+ <div className="hud-panel-chrome-title">{title}</div>
209
+ <div className="hud-panel-controls">
210
+ {onMinimize && (
211
+ <button
212
+ className="hud-button hud-icon-button hud-panel-control"
213
+ type="button"
214
+ aria-label={minimized ? 'Restore' : 'Minimize'}
215
+ onClick={onMinimize}
216
+ >
217
+ {minimized ? '+' : '−'}
218
+ </button>
219
+ )}
220
+ {onClose && (
221
+ <button
222
+ className="hud-button hud-icon-button hud-panel-control"
223
+ type="button"
224
+ aria-label="Close"
225
+ onClick={onClose}
226
+ >
227
+ ×
228
+ </button>
229
+ )}
230
+ </div>
231
+ </div>
232
+ )
233
+ }
234
+
235
+ type SessionPanelProps = {
236
+ intent: AgentIntent
237
+ focused: boolean
238
+ naming: boolean
239
+ onFocus: () => void
240
+ onWorkflowAction: (
241
+ sessionId: string,
242
+ action: WorkflowAction,
243
+ options?: { instruction?: string; step?: number; stepByStep?: boolean },
244
+ ) => void
245
+ onNavigateDiff: (sessionId: string, diffId: string) => void
246
+ }
247
+
248
+ function SessionPanel({
249
+ intent,
250
+ focused,
251
+ naming,
252
+ onFocus,
253
+ onWorkflowAction,
254
+ onNavigateDiff,
255
+ }: SessionPanelProps) {
256
+ const [minimized, setMinimized] = useState(false)
257
+ const [instruction, setInstruction] = useState('')
258
+ const sessionId = intent.sessionId
259
+ const pending = intent.status === 'pending' && intent.isActiveDiff
260
+ const askingBlueprint = intent.status === 'blueprint_ask'
261
+ const creatingBlueprint = intent.creationMode || intent.status === 'blueprint'
262
+ const preparing = intent.status === 'preparing'
263
+ const planReady = intent.status === 'planned'
264
+ const working = intent.status === 'working' || intent.status === 'replanning'
265
+ const previewing = intent.preview
266
+ const chainIndex = intent.chainIndex ?? 0
267
+ const previousDiff = intent.chain[chainIndex - 1]
268
+ const nextDiff = intent.chain[chainIndex + 1]
269
+ const stepLabel =
270
+ intent.step && intent.steps?.length > 0
271
+ ? `Step ${intent.step} of ${intent.steps.length}`
272
+ : 'Patch'
273
+ const lastStep =
274
+ typeof intent.step === 'number' &&
275
+ intent.steps.length > 0 &&
276
+ intent.step >= intent.steps.length
277
+ const stepByStep = intent.stepByStep !== false
278
+ const addedFunctions = intent.addedFunctions ?? []
279
+ const addedVariables = intent.addedVariables ?? []
280
+ const addedImports = intent.addedImports ?? []
281
+ const changedFunctions = intent.changedFunctions ?? []
282
+ const changedVariables = intent.changedVariables ?? []
283
+ const doneSteps = new Set(
284
+ intent.status === 'finished'
285
+ ? intent.steps.map((step) => step.index)
286
+ : intent.chain
287
+ .filter(
288
+ (entry) =>
289
+ entry.status === 'applied' || entry.status === 'extended',
290
+ )
291
+ .map((entry) => entry.step),
292
+ )
293
+ if (intent.status === 'approved' && typeof intent.step === 'number') {
294
+ doneSteps.add(intent.step)
295
+ }
296
+ if (pending && typeof intent.step === 'number' && !lastStep) {
297
+ doneSteps.add(intent.step)
298
+ }
299
+ const nextStep =
300
+ intent.status === 'finished'
301
+ ? null
302
+ : (intent.steps.find((step) => !doneSteps.has(step.index)) ?? null)
303
+ const canRunNext =
304
+ Boolean(nextStep) && (planReady || pending) && !working
305
+ const canComplete = pending && lastStep
306
+ const panelDone =
307
+ intent.status === 'finished' ||
308
+ intent.status === 'approved' ||
309
+ doneSteps.size > 0
310
+
311
+ useEffect(() => {
312
+ setInstruction('')
313
+ }, [intent.diffId])
314
+
315
+ if (!sessionId || !isReviewingIntent(intent.status)) return null
316
+
317
+ const act = (
318
+ action: WorkflowAction,
319
+ options?: { instruction?: string; step?: number; stepByStep?: boolean },
320
+ ) => onWorkflowAction(sessionId, action, options)
321
+
322
+ return (
323
+ <aside
324
+ className={
325
+ panelDone
326
+ ? 'hud-panel hud-panel-planned hud-panel-done'
327
+ : 'hud-panel hud-panel-planned'
328
+ }
329
+ data-minimized={minimized}
330
+ data-focused={focused}
331
+ onPointerDown={onFocus}
332
+ >
333
+ <PanelChrome
334
+ title={reviewTitle(intent.status)}
335
+ minimized={minimized}
336
+ onMinimize={() => setMinimized((current) => !current)}
337
+ />
338
+ {!minimized && (
339
+ <>
340
+ <label className="hud-mode-switch">
341
+ <span>Step by step</span>
342
+ <button
343
+ className="hud-switch"
344
+ type="button"
345
+ role="switch"
346
+ aria-checked={stepByStep}
347
+ aria-label="Step by step"
348
+ onKeyDown={(event) => event.stopPropagation()}
349
+ onClick={() => act('set_step_by_step', { stepByStep: !stepByStep })}
350
+ />
351
+ </label>
352
+ {!stepByStep && (
353
+ <p className="hud-mode-hint">
354
+ LLM implements the full plan. You can still walk the diffs, then
355
+ Complete.
356
+ </p>
357
+ )}
358
+ {intent.feature && <p className="hud-feature">{intent.feature}</p>}
359
+ {askingBlueprint ? (
360
+ <>
361
+ <p>
362
+ {intent.canEnterBlueprint
363
+ ? 'Place files and folders for this chat, then send them as a blueprint for the LLM?'
364
+ : `Blueprint edit mode is active in another chat (${intent.blueprintSessionId}). Finish or stop it before starting here.`}
365
+ </p>
366
+ <div className="hud-decide">
367
+ <button
368
+ className="hud-button hud-button-approve"
369
+ type="button"
370
+ disabled={!intent.canEnterBlueprint}
371
+ onClick={() => act('blueprint_yes')}
372
+ >
373
+ Create blueprint
374
+ </button>
375
+ <button
376
+ className="hud-button hud-button-approve"
377
+ type="button"
378
+ onClick={() => act('blueprint_no')}
379
+ >
380
+ Let LLM continue
381
+ </button>
382
+ <button
383
+ className="hud-button hud-button-reject"
384
+ type="button"
385
+ onClick={() => act('stop')}
386
+ >
387
+ Stop
388
+ </button>
389
+ </div>
390
+ </>
391
+ ) : creatingBlueprint ? (
392
+ <>
393
+ <p>
394
+ Walk the map, press <kbd>Space</kbd> for a file and{' '}
395
+ <kbd>B</kbd> for an island. Send the blueprint when the layout
396
+ is ready.
397
+ </p>
398
+ <div className="hud-decide">
399
+ <button
400
+ className="hud-button hud-button-approve"
401
+ type="button"
402
+ disabled={naming}
403
+ onClick={() => act('blueprint_send')}
404
+ >
405
+ Send blueprint
406
+ </button>
407
+ <button
408
+ className="hud-button hud-button-reject"
409
+ type="button"
410
+ onClick={() => act('stop')}
411
+ >
412
+ Stop
413
+ </button>
414
+ </div>
415
+ </>
416
+ ) : intent.status === 'finished' ? (
417
+ <p>All plan steps were applied.</p>
418
+ ) : preparing ? (
419
+ <div className="hud-working">
420
+ <span className="hud-spinner" aria-hidden="true" />
421
+ <span>LLM preparing…</span>
422
+ <button
423
+ className="hud-button hud-button-reject"
424
+ type="button"
425
+ onClick={() => act('stop')}
426
+ >
427
+ Stop
428
+ </button>
429
+ </div>
430
+ ) : working ? (
431
+ <div className="hud-working">
432
+ <span className="hud-spinner" aria-hidden="true" />
433
+ <span>
434
+ {intent.status === 'replanning'
435
+ ? 'Updating the remaining plan from your instruction…'
436
+ : intent.stalledWait
437
+ ? 'The LLM is still waiting on this step…'
438
+ : `Implementing ${stepLabel.toLowerCase()}…`}
439
+ </span>
440
+ <button
441
+ className="hud-button hud-button-reject"
442
+ type="button"
443
+ onClick={() => act('stop')}
444
+ >
445
+ Stop
446
+ </button>
447
+ </div>
448
+ ) : (
449
+ <p>
450
+ {stepLabel}
451
+ {intent.reason ? ` · ${intent.reason}` : ''}
452
+ </p>
453
+ )}
454
+ {!askingBlueprint && !creatingBlueprint && (
455
+ <>
456
+ {intent.steps?.length > 0 && (
457
+ <ol className="hud-steps">
458
+ {intent.steps.map((step) => (
459
+ <li
460
+ key={step.index}
461
+ data-current={
462
+ intent.status !== 'finished' &&
463
+ step.index === intent.step &&
464
+ !doneSteps.has(step.index)
465
+ }
466
+ data-done={doneSteps.has(step.index)}
467
+ data-next={
468
+ nextStep?.index === step.index &&
469
+ !doneSteps.has(step.index)
470
+ }
471
+ >
472
+ <span className="hud-step-index">{step.index}.</span>
473
+ <span className="hud-step-main">
474
+ <span className="hud-step-title">{step.title}</span>
475
+ {((stepByStep &&
476
+ canRunNext &&
477
+ nextStep?.index === step.index) ||
478
+ (canComplete && step.index === intent.step)) && (
479
+ <button
480
+ className="hud-button hud-button-approve hud-run-step"
481
+ type="button"
482
+ onClick={() =>
483
+ canComplete && step.index === intent.step
484
+ ? act('continue')
485
+ : act('invoke', {
486
+ step: step.index,
487
+ })
488
+ }
489
+ >
490
+ {canComplete && step.index === intent.step
491
+ ? 'Complete'
492
+ : 'Run step'}
493
+ </button>
494
+ )}
495
+ </span>
496
+ </li>
497
+ ))}
498
+ </ol>
499
+ )}
500
+ {intent.chain.length > 0 && (
501
+ <div className="hud-chain">
502
+ <button
503
+ className="hud-button"
504
+ type="button"
505
+ disabled={!previousDiff}
506
+ onClick={() =>
507
+ previousDiff && onNavigateDiff(sessionId, previousDiff.id)
508
+ }
509
+ >
510
+ Previous
511
+ </button>
512
+ <span>
513
+ Diff {chainIndex + 1} of {intent.chain.length}
514
+ </span>
515
+ <button
516
+ className="hud-button"
517
+ type="button"
518
+ disabled={!nextDiff}
519
+ onClick={() =>
520
+ nextDiff && onNavigateDiff(sessionId, nextDiff.id)
521
+ }
522
+ >
523
+ Next
524
+ </button>
525
+ </div>
526
+ )}
527
+ <MutationFold
528
+ hasContent={
529
+ previewing &&
530
+ (intent.files.length > 0 ||
531
+ (intent.createFolders ?? []).length > 0 ||
532
+ intent.creates.length > 0 ||
533
+ intent.deletes.length > 0 ||
534
+ addedFunctions.length > 0 ||
535
+ addedVariables.length > 0 ||
536
+ addedImports.length > 0 ||
537
+ changedFunctions.length > 0 ||
538
+ changedVariables.length > 0 ||
539
+ (intent.imports ?? []).length > 0)
540
+ }
541
+ >
542
+ {intent.files.length > 0 && (
543
+ <>
544
+ <div className="hud-section-title hud-section-title-edit">
545
+ Changed
546
+ </div>
547
+ <ul>
548
+ {intent.files.map((id) => (
549
+ <li className="hud-file-edit" key={id}>
550
+ {id}
551
+ </li>
552
+ ))}
553
+ </ul>
554
+ </>
555
+ )}
556
+ {(intent.createFolders ?? []).length > 0 && (
557
+ <>
558
+ <div className="hud-section-title hud-section-title-add">
559
+ Added islands
560
+ </div>
561
+ <ul>
562
+ {intent.createFolders.map((id) => (
563
+ <li className="hud-file-add" key={id}>
564
+ {id}/
565
+ </li>
566
+ ))}
567
+ </ul>
568
+ </>
569
+ )}
570
+ {intent.creates.length > 0 && (
571
+ <>
572
+ <div className="hud-section-title hud-section-title-add">
573
+ Added
574
+ </div>
575
+ <ul>
576
+ {intent.creates.map((id) => (
577
+ <li className="hud-file-add" key={id}>
578
+ {id}
579
+ </li>
580
+ ))}
581
+ </ul>
582
+ </>
583
+ )}
584
+ {intent.deletes.length > 0 && (
585
+ <>
586
+ <div className="hud-section-title hud-section-title-remove">
587
+ Removed
588
+ </div>
589
+ <ul>
590
+ {intent.deletes.map((id) => (
591
+ <li className="hud-file-remove" key={id}>
592
+ {id}
593
+ </li>
594
+ ))}
595
+ </ul>
596
+ </>
597
+ )}
598
+ <PanelList
599
+ title="Changed functions"
600
+ items={symbolLabels(changedFunctions)}
601
+ tone="edit"
602
+ />
603
+ <PanelList
604
+ title="Added functions"
605
+ items={symbolLabels(addedFunctions)}
606
+ tone="add"
607
+ />
608
+ <PanelList
609
+ title="Changed variables"
610
+ items={symbolLabels(changedVariables)}
611
+ tone="edit"
612
+ />
613
+ <PanelList
614
+ title="Added variables"
615
+ items={symbolLabels(addedVariables)}
616
+ tone="add"
617
+ />
618
+ {addedImports.length > 0 && (
619
+ <PanelList title="Imports" items={importLabels(addedImports)} />
620
+ )}
621
+ {addedImports.length === 0 && (intent.imports ?? []).length > 0 && (
622
+ <>
623
+ <div className="hud-section-title">Imports</div>
624
+ <ul>
625
+ {intent.imports.map((edge) => (
626
+ <li key={`${edge.from}->${edge.to}`}>
627
+ {edge.from.split('/').pop()} → {edge.to.split('/').pop()}
628
+ </li>
629
+ ))}
630
+ </ul>
631
+ </>
632
+ )}
633
+ </MutationFold>
634
+ {planReady && (
635
+ <div className="hud-decide">
636
+ <button
637
+ className="hud-button hud-button-reject"
638
+ type="button"
639
+ onClick={() => act('stop')}
640
+ >
641
+ Stop
642
+ </button>
643
+ </div>
644
+ )}
645
+ {pending && (
646
+ <>
647
+ <label className="hud-instruction">
648
+ <span>Alternative instruction for the LLM</span>
649
+ <textarea
650
+ value={instruction}
651
+ maxLength={4000}
652
+ rows={3}
653
+ placeholder="Describe what should change in the next diff…"
654
+ onChange={(event) => setInstruction(event.target.value)}
655
+ />
656
+ </label>
657
+ <div className="hud-decide">
658
+ {lastStep && (
659
+ <button
660
+ className="hud-button hud-button-approve"
661
+ type="button"
662
+ onClick={() => act('continue')}
663
+ >
664
+ Complete
665
+ </button>
666
+ )}
667
+ <button
668
+ className="hud-button hud-button-extend"
669
+ type="button"
670
+ disabled={!instruction.trim()}
671
+ onClick={() =>
672
+ act('instruct', { instruction })
673
+ }
674
+ >
675
+ Send instruction
676
+ </button>
677
+ <button
678
+ className="hud-button hud-button-reject"
679
+ type="button"
680
+ onClick={() => act('stop')}
681
+ >
682
+ Stop
683
+ </button>
684
+ </div>
685
+ </>
686
+ )}
687
+ </>
688
+ )}
689
+ </>
690
+ )}
691
+ </aside>
692
+ )
693
+ }
694
+
126
695
  type HUDProps = {
127
696
  graph: CodebaseGraph
697
+ layout: WorldLayout
128
698
  mode: ViewMode
129
699
  locked: boolean
130
700
  selectedId: string | null
131
701
  selectedTick?: number
702
+ inspectTick?: number
132
703
  selectedFolder?: string | null
704
+ landAt: [number, number]
133
705
  aimedRelation: AimedRelation | null
706
+ aimedFileId?: string | null
134
707
  currentFolder: string
135
708
  intent: AgentIntent
709
+ intents?: AgentIntent[]
710
+ focusedSessionId?: string | null
711
+ onFocusSession?: (sessionId: string) => void
136
712
  onWorkflowAction: (
713
+ sessionId: string,
137
714
  action: WorkflowAction,
138
- options?: { instruction?: string; step?: number },
715
+ options?: { instruction?: string; step?: number; stepByStep?: boolean },
139
716
  ) => void
140
- onNavigateDiff: (diffId: string) => void
717
+ onNavigateDiff: (sessionId: string, diffId: string) => void
141
718
  onOpenMap: () => void
142
719
  onWalk: () => void
143
720
  followLook: boolean
144
721
  onToggleFollowLook: () => void
145
722
  importedBy: boolean
146
723
  onToggleImportedBy: () => void
724
+ changePathsOnly?: boolean
725
+ hasChangeSet?: boolean
726
+ onToggleChangePathsOnly?: () => void
147
727
  naming?: boolean
148
728
  namingIsland?: boolean
149
729
  onCommitIslandName?: (name: string) => void
@@ -164,18 +744,29 @@ type HUDProps = {
164
744
  onMapAddFile?: (folderPath: string) => void
165
745
  onMapAddFolder?: (folderPath: string) => void
166
746
  onInspectFile?: (fileId: string) => void
747
+ onInspectBlock?: (fileId: string) => void
748
+ plannedIds?: string[]
749
+ createdIds?: string[]
750
+ deletedIds?: string[]
167
751
  }
168
752
 
169
753
  export function HUD({
170
754
  graph,
755
+ layout,
171
756
  mode,
172
757
  locked,
173
758
  selectedId,
174
759
  selectedTick = 0,
760
+ inspectTick = 0,
175
761
  selectedFolder = null,
762
+ landAt,
176
763
  aimedRelation,
764
+ aimedFileId = null,
177
765
  currentFolder,
178
766
  intent,
767
+ intents,
768
+ focusedSessionId = null,
769
+ onFocusSession,
179
770
  onWorkflowAction,
180
771
  onNavigateDiff,
181
772
  onOpenMap,
@@ -184,6 +775,9 @@ export function HUD({
184
775
  onToggleFollowLook,
185
776
  importedBy,
186
777
  onToggleImportedBy,
778
+ changePathsOnly = false,
779
+ hasChangeSet = false,
780
+ onToggleChangePathsOnly,
187
781
  naming = false,
188
782
  namingIsland = false,
189
783
  onCommitIslandName,
@@ -200,6 +794,10 @@ export function HUD({
200
794
  onMapAddFile,
201
795
  onMapAddFolder,
202
796
  onInspectFile,
797
+ onInspectBlock,
798
+ plannedIds = [],
799
+ createdIds = [],
800
+ deletedIds = [],
203
801
  }: HUDProps) {
204
802
  const selected = graph.files.find((file) => file.id === selectedId)
205
803
  const selectedFolderNode = graph.folders.find(
@@ -217,34 +815,26 @@ export function HUD({
217
815
  ? graph.files.filter((file) => file.imports.includes(selected.id))
218
816
  : []
219
817
  const mapping = mode === 'map'
220
- const sessionMode = Boolean(intent.sessionId)
818
+ const sessions = (intents ?? [intent]).filter(
819
+ (item) => item.sessionId && isReviewingIntent(item.status),
820
+ )
821
+ const canStop = canStopSession(intent)
221
822
  const [walkIntro, setWalkIntro] = useState(false)
222
823
  const walkIntroSeen = useRef(false)
223
- const [instruction, setInstruction] = useState('')
224
824
  const [infoVisible, setInfoVisible] = useState(false)
225
- const infoPanelRef = useRef<HTMLElement>(null)
226
- const pending = intent.status === 'pending' && intent.isActiveDiff
227
- const askingBlueprint = intent.status === 'blueprint_ask'
228
- const creatingBlueprint = intent.creationMode || intent.status === 'blueprint'
229
- const preparing = intent.status === 'preparing'
230
- const planReady = intent.status === 'planned'
231
- const working = intent.status === 'working' || intent.status === 'replanning'
825
+ const [infoMinimized, setInfoMinimized] = useState(false)
826
+ const [thumbnailVisible, setThumbnailVisible] = useState(true)
827
+ const [thumbnailMinimized, setThumbnailMinimized] = useState(false)
828
+ const infoPanelRef = useRef<HTMLDivElement>(null)
829
+ const creatingBlueprint = sessions.some(
830
+ (item) => item.creationMode || item.status === 'blueprint',
831
+ )
232
832
  const previewing = intent.preview
233
- const reviewing = isReviewingIntent(intent.status) || intent.chain.length > 0
234
- const chainIndex = intent.chainIndex ?? 0
235
- const previousDiff = intent.chain[chainIndex - 1]
236
- const nextDiff = intent.chain[chainIndex + 1]
237
- const stepLabel =
238
- intent.step && intent.steps?.length > 0
239
- ? `Step ${intent.step} of ${intent.steps.length}`
240
- : 'Patch'
241
- const lastStep =
242
- typeof intent.step === 'number' &&
243
- intent.steps.length > 0 &&
244
- intent.step >= intent.steps.length
245
833
  const addedFunctions = intent.addedFunctions ?? []
246
834
  const addedVariables = intent.addedVariables ?? []
247
835
  const addedImports = intent.addedImports ?? []
836
+ const changedFunctions = intent.changedFunctions ?? []
837
+ const changedVariables = intent.changedVariables ?? []
248
838
  const selectedAddedFunctions = selected
249
839
  ? addedFunctions.filter((item) => item.file === selected.id)
250
840
  : []
@@ -254,6 +844,12 @@ export function HUD({
254
844
  const selectedAddedImports = selected
255
845
  ? addedImports.filter((item) => item.file === selected.id)
256
846
  : []
847
+ const selectedChangedFunctions = selected
848
+ ? changedFunctions.filter((item) => item.file === selected.id)
849
+ : []
850
+ const selectedChangedVariables = selected
851
+ ? changedVariables.filter((item) => item.file === selected.id)
852
+ : []
257
853
  const selectedClasses = selected
258
854
  ? selected.symbols.filter((symbol) => symbol.kind === 'class')
259
855
  : []
@@ -263,6 +859,26 @@ export function HUD({
263
859
  const selectedVariables = selected
264
860
  ? selected.symbols.filter((symbol) => symbol.kind === 'variable')
265
861
  : []
862
+ const functionChange = new Map<string, 'add' | 'edit'>([
863
+ ...selectedChangedFunctions.map(
864
+ (item) => [item.name, 'edit'] as const,
865
+ ),
866
+ ...selectedAddedFunctions.map((item) => [item.name, 'add'] as const),
867
+ ])
868
+ const variableChange = new Map<string, 'add' | 'edit'>([
869
+ ...selectedChangedVariables.map(
870
+ (item) => [item.name, 'edit'] as const,
871
+ ),
872
+ ...selectedAddedVariables.map((item) => [item.name, 'add'] as const),
873
+ ])
874
+ const extraAddedFunctions = extraAddedSymbols(
875
+ selectedFunctions,
876
+ selectedAddedFunctions,
877
+ )
878
+ const extraAddedVariables = extraAddedSymbols(
879
+ selectedVariables,
880
+ selectedAddedVariables,
881
+ )
266
882
  const selectedBlueprintFunctions = selected
267
883
  ? blueprintFunctions.filter((item) => item.file === selected.id)
268
884
  : []
@@ -280,38 +896,11 @@ export function HUD({
280
896
  )
281
897
  const canEditBlueprint =
282
898
  creatingBlueprint && Boolean(selected) && !selected?.id.startsWith('draft:')
283
- const doneSteps = new Set(
284
- intent.status === 'finished'
285
- ? intent.steps.map((step) => step.index)
286
- : intent.chain
287
- .filter(
288
- (entry) =>
289
- entry.status === 'applied' || entry.status === 'extended',
290
- )
291
- .map((entry) => entry.step),
292
- )
293
- if (intent.status === 'approved' && typeof intent.step === 'number') {
294
- doneSteps.add(intent.step)
295
- }
296
- if (pending && typeof intent.step === 'number' && !lastStep) {
297
- doneSteps.add(intent.step)
298
- }
299
- const nextStep =
300
- intent.status === 'finished'
301
- ? null
302
- : (intent.steps.find((step) => !doneSteps.has(step.index)) ?? null)
303
- const canRunNext =
304
- Boolean(nextStep) && (planReady || pending) && !working
305
- const canComplete = pending && lastStep
306
899
  const canInspectFile = (fileId: string, userCreated = false) =>
307
900
  Boolean(onInspectFile) &&
308
901
  !fileId.startsWith('draft:') &&
309
902
  !(previewing && (intent.deletes ?? []).includes(fileId)) &&
310
903
  (!userCreated || (intent.creates ?? []).includes(fileId))
311
- const panelDone =
312
- intent.status === 'finished' ||
313
- intent.status === 'approved' ||
314
- doneSteps.size > 0
315
904
 
316
905
  useEffect(() => {
317
906
  if (mode !== 'walk') {
@@ -332,22 +921,38 @@ export function HUD({
332
921
  return () => window.clearTimeout(timer)
333
922
  }, [locked, mode, naming])
334
923
 
335
- useEffect(() => {
336
- setInstruction('')
337
- }, [intent.diffId])
338
-
339
924
  useEffect(() => {
340
925
  infoPanelRef.current?.scrollTo({ top: 0 })
341
926
  }, [selectedId, selectedFolder])
342
927
 
343
928
  useEffect(() => {
929
+ if (mode === 'walk') return
344
930
  if (selectedId) setInfoVisible(true)
345
- }, [selectedId, selectedTick])
931
+ }, [mode, selectedId, selectedTick])
932
+
933
+ useEffect(() => {
934
+ if (inspectTick > 0) {
935
+ setInfoVisible(true)
936
+ setInfoMinimized(false)
937
+ }
938
+ }, [inspectTick])
346
939
 
347
940
  useEffect(() => {
348
941
  if (selectedFolder) setInfoVisible(true)
349
942
  }, [selectedFolder])
350
943
 
944
+ useEffect(() => {
945
+ if (!locked) return
946
+ setInfoVisible(false)
947
+ setInfoMinimized(false)
948
+ }, [locked])
949
+
950
+ const infoOpen = infoVisible && Boolean(selected || selectedFolderNode)
951
+
952
+ useEffect(() => {
953
+ if (infoOpen) document.exitPointerLock()
954
+ }, [infoOpen])
955
+
351
956
  useEffect(() => {
352
957
  const onKey = (event: KeyboardEvent) => {
353
958
  if (event.repeat || event.code !== 'KeyI') return
@@ -362,11 +967,46 @@ export function HUD({
362
967
  return
363
968
  }
364
969
  event.preventDefault()
365
- setInfoVisible((visible) => !visible)
970
+ if (infoVisible) {
971
+ setInfoVisible(false)
972
+ setInfoMinimized(false)
973
+ return
974
+ }
975
+ const blockId = aimedFileId ?? selectedId
976
+ if (mode === 'walk') {
977
+ if (!blockId) return
978
+ onInspectBlock?.(blockId)
979
+ }
980
+ setInfoMinimized(false)
981
+ setInfoVisible(true)
982
+ }
983
+ window.addEventListener('keydown', onKey)
984
+ return () => window.removeEventListener('keydown', onKey)
985
+ }, [aimedFileId, infoVisible, mode, onInspectBlock, selectedId])
986
+
987
+ useEffect(() => {
988
+ if (!mapping) return
989
+ const onKey = (event: KeyboardEvent) => {
990
+ if (event.repeat || event.code !== 'KeyT') return
991
+ const target = event.target
992
+ if (
993
+ target instanceof HTMLElement &&
994
+ (target.tagName === 'TEXTAREA' ||
995
+ target.tagName === 'INPUT' ||
996
+ target.tagName === 'SELECT' ||
997
+ target.isContentEditable)
998
+ ) {
999
+ return
1000
+ }
1001
+ event.preventDefault()
1002
+ setThumbnailVisible((visible) => {
1003
+ if (!visible) setThumbnailMinimized(false)
1004
+ return !visible
1005
+ })
366
1006
  }
367
1007
  window.addEventListener('keydown', onKey)
368
1008
  return () => window.removeEventListener('keydown', onKey)
369
- }, [])
1009
+ }, [mapping])
370
1010
 
371
1011
  useEffect(() => {
372
1012
  if (!infoVisible || (!selectedId && !selectedFolder)) return
@@ -400,8 +1040,9 @@ export function HUD({
400
1040
  <div className="hud-gate-card">
401
1041
  <h1>Walk</h1>
402
1042
  <p>
403
- Click to look around. Press <kbd>M</kbd> to open the map, press{' '}
404
- <kbd>M</kbd> again to return here.
1043
+ Click to look around. Double-click to release the mouse. Press{' '}
1044
+ <kbd>M</kbd> to open the map, press <kbd>M</kbd> again to return
1045
+ here.
405
1046
  </p>
406
1047
  <p>
407
1048
  <kbd>W</kbd> <kbd>A</kbd> <kbd>S</kbd> <kbd>D</kbd> walk,{' '}
@@ -411,7 +1052,7 @@ export function HUD({
411
1052
  , <kbd>Space</kbd> place a file, <kbd>B</kbd> place an island
412
1053
  </>
413
1054
  ) : null}
414
- , click a block for info.
1055
+ , double-click a block or press <kbd>I</kbd> for info.
415
1056
  </p>
416
1057
  </div>
417
1058
  </div>
@@ -455,313 +1096,57 @@ export function HUD({
455
1096
  >
456
1097
  Walk
457
1098
  </button>
1099
+ {canStop && (
1100
+ <button
1101
+ className="hud-button hud-button-reject"
1102
+ type="button"
1103
+ aria-label="Stop LLM session"
1104
+ onClick={() =>
1105
+ intent.sessionId && onWorkflowAction(intent.sessionId, 'stop')
1106
+ }
1107
+ >
1108
+ Stop
1109
+ </button>
1110
+ )}
458
1111
  </div>
459
1112
  {selected && <div className="hud-chip">{selected.path}</div>}
460
1113
  </div>
461
1114
 
462
- {reviewing && (
463
- <aside
464
- className={
465
- panelDone
466
- ? 'hud-panel hud-panel-planned hud-panel-done'
467
- : 'hud-panel hud-panel-planned'
468
- }
469
- >
470
- <div className="hud-section-title">
471
- {reviewTitle(intent.status)}
472
- </div>
473
- {intent.feature && <p className="hud-feature">{intent.feature}</p>}
474
- {askingBlueprint ? (
475
- <>
476
- <p>
477
- {intent.canEnterBlueprint
478
- ? 'Place files and islands for this chat, then send them as a blueprint for the LLM?'
479
- : `Blueprint edit mode is active in another chat (${intent.blueprintSessionId}). Finish or stop it before starting here.`}
480
- </p>
481
- <div className="hud-decide">
482
- <button
483
- className="hud-button hud-button-approve"
484
- type="button"
485
- disabled={!intent.canEnterBlueprint}
486
- onClick={() => onWorkflowAction('blueprint_yes')}
487
- >
488
- Yes
489
- </button>
490
- <button
491
- className="hud-button"
492
- type="button"
493
- onClick={() => onWorkflowAction('blueprint_no')}
494
- >
495
- No
496
- </button>
497
- <button
498
- className="hud-button hud-button-reject"
499
- type="button"
500
- onClick={() => onWorkflowAction('stop')}
501
- >
502
- Stop
503
- </button>
504
- </div>
505
- </>
506
- ) : creatingBlueprint ? (
507
- <>
508
- <p>
509
- Walk the map, press <kbd>Space</kbd> for a file and{' '}
510
- <kbd>B</kbd> for an island. Send the blueprint when the layout
511
- is ready.
512
- </p>
513
- <div className="hud-decide">
514
- <button
515
- className="hud-button hud-button-approve"
516
- type="button"
517
- disabled={naming}
518
- onClick={() => onWorkflowAction('blueprint_send')}
519
- >
520
- Send blueprint
521
- </button>
522
- <button
523
- className="hud-button hud-button-reject"
524
- type="button"
525
- onClick={() => onWorkflowAction('stop')}
526
- >
527
- Stop
528
- </button>
529
- </div>
530
- </>
531
- ) : intent.status === 'finished' ? (
532
- <p>All plan steps were applied.</p>
533
- ) : preparing ? (
534
- <div className="hud-working">
535
- <span className="hud-spinner" aria-hidden="true" />
536
- <span>LLM preparing…</span>
537
- </div>
538
- ) : working ? (
539
- <div className="hud-working">
540
- <span className="hud-spinner" aria-hidden="true" />
541
- <span>
542
- {intent.status === 'replanning'
543
- ? 'Updating the remaining plan from your instruction…'
544
- : `Implementing ${stepLabel.toLowerCase()}…`}
545
- </span>
546
- </div>
547
- ) : (
548
- <p>
549
- {stepLabel}
550
- {intent.reason ? ` · ${intent.reason}` : ''}
551
- </p>
552
- )}
553
- {!askingBlueprint && !creatingBlueprint && (
554
- <>
555
- {intent.steps?.length > 0 && (
556
- <ol className="hud-steps">
557
- {intent.steps.map((step) => (
558
- <li
559
- key={step.index}
560
- data-current={
561
- intent.status !== 'finished' &&
562
- step.index === intent.step &&
563
- !doneSteps.has(step.index)
564
- }
565
- data-done={doneSteps.has(step.index)}
566
- data-next={
567
- nextStep?.index === step.index && !doneSteps.has(step.index)
568
- }
569
- >
570
- <span className="hud-step-index">{step.index}.</span>
571
- <span className="hud-step-main">
572
- <span className="hud-step-title">{step.title}</span>
573
- {((canRunNext && nextStep?.index === step.index) ||
574
- (canComplete && step.index === intent.step)) && (
575
- <button
576
- className="hud-button hud-button-approve hud-run-step"
577
- type="button"
578
- onClick={() =>
579
- canComplete && step.index === intent.step
580
- ? onWorkflowAction('continue')
581
- : onWorkflowAction('invoke', {
582
- step: step.index,
583
- })
584
- }
585
- >
586
- {canComplete && step.index === intent.step
587
- ? 'Complete'
588
- : 'Run step'}
589
- </button>
590
- )}
591
- </span>
592
- </li>
593
- ))}
594
- </ol>
595
- )}
596
- {intent.chain.length > 0 && (
597
- <div className="hud-chain">
598
- <button
599
- className="hud-button"
600
- type="button"
601
- disabled={!previousDiff}
602
- onClick={() => previousDiff && onNavigateDiff(previousDiff.id)}
603
- >
604
- Previous
605
- </button>
606
- <span>
607
- Diff {chainIndex + 1} of {intent.chain.length}
608
- </span>
609
- <button
610
- className="hud-button"
611
- type="button"
612
- disabled={!nextDiff}
613
- onClick={() => nextDiff && onNavigateDiff(nextDiff.id)}
614
- >
615
- Next
616
- </button>
617
- </div>
618
- )}
619
- <MutationFold
620
- hasContent={
621
- previewing &&
622
- (intent.files.length > 0 ||
623
- (intent.createFolders ?? []).length > 0 ||
624
- intent.creates.length > 0 ||
625
- intent.deletes.length > 0 ||
626
- addedFunctions.length > 0 ||
627
- addedVariables.length > 0 ||
628
- addedImports.length > 0 ||
629
- (intent.imports ?? []).length > 0)
630
- }
631
- >
632
- {intent.files.length > 0 && (
633
- <>
634
- <div className="hud-section-title hud-section-title-edit">Changed</div>
635
- <ul>
636
- {intent.files.map((id) => (
637
- <li className="hud-file-edit" key={id}>
638
- {id}
639
- </li>
640
- ))}
641
- </ul>
642
- </>
643
- )}
644
- {(intent.createFolders ?? []).length > 0 && (
645
- <>
646
- <div className="hud-section-title hud-section-title-add">Added islands</div>
647
- <ul>
648
- {intent.createFolders.map((id) => (
649
- <li className="hud-file-add" key={id}>
650
- {id}/
651
- </li>
652
- ))}
653
- </ul>
654
- </>
655
- )}
656
- {intent.creates.length > 0 && (
657
- <>
658
- <div className="hud-section-title hud-section-title-add">Added</div>
659
- <ul>
660
- {intent.creates.map((id) => (
661
- <li className="hud-file-add" key={id}>
662
- {id}
663
- </li>
664
- ))}
665
- </ul>
666
- </>
667
- )}
668
- {intent.deletes.length > 0 && (
669
- <>
670
- <div className="hud-section-title hud-section-title-remove">Removed</div>
671
- <ul>
672
- {intent.deletes.map((id) => (
673
- <li className="hud-file-remove" key={id}>
674
- {id}
675
- </li>
676
- ))}
677
- </ul>
678
- </>
679
- )}
680
- <PanelList
681
- title="Functions"
682
- items={symbolLabels(addedFunctions)}
1115
+ {sessions.length > 0 && (
1116
+ <div className="hud-left-stack">
1117
+ {sessions.map((session) => (
1118
+ <SessionPanel
1119
+ key={session.sessionId}
1120
+ intent={session}
1121
+ focused={session.sessionId === (focusedSessionId ?? intent.sessionId)}
1122
+ naming={naming}
1123
+ onFocus={() => {
1124
+ if (session.sessionId) onFocusSession?.(session.sessionId)
1125
+ }}
1126
+ onWorkflowAction={onWorkflowAction}
1127
+ onNavigateDiff={onNavigateDiff}
683
1128
  />
684
- <PanelList
685
- title="Variables"
686
- items={symbolLabels(addedVariables)}
687
- />
688
- {addedImports.length > 0 && (
689
- <PanelList title="Imports" items={importLabels(addedImports)} />
690
- )}
691
- {addedImports.length === 0 && (intent.imports ?? []).length > 0 && (
692
- <>
693
- <div className="hud-section-title">Imports</div>
694
- <ul>
695
- {intent.imports.map((edge) => (
696
- <li key={`${edge.from}->${edge.to}`}>
697
- {edge.from.split('/').pop()} → {edge.to.split('/').pop()}
698
- </li>
699
- ))}
700
- </ul>
701
- </>
702
- )}
703
- </MutationFold>
704
- {(planReady || preparing) && (
705
- <div className="hud-decide">
706
- <button
707
- className="hud-button hud-button-reject"
708
- type="button"
709
- onClick={() => onWorkflowAction('stop')}
710
- >
711
- Stop
712
- </button>
713
- </div>
714
- )}
715
- {pending && (
716
- <>
717
- <label className="hud-instruction">
718
- <span>Alternative instruction for the LLM</span>
719
- <textarea
720
- value={instruction}
721
- maxLength={4000}
722
- rows={3}
723
- placeholder="Describe what should change in the next diff…"
724
- onChange={(event) => setInstruction(event.target.value)}
725
- />
726
- </label>
727
- <div className="hud-decide">
728
- {lastStep && (
729
- <button
730
- className="hud-button hud-button-approve"
731
- type="button"
732
- onClick={() => onWorkflowAction('continue')}
733
- >
734
- Complete
735
- </button>
736
- )}
737
- <button
738
- className="hud-button hud-button-extend"
739
- type="button"
740
- disabled={!instruction.trim()}
741
- onClick={() =>
742
- onWorkflowAction('instruct', { instruction })
743
- }
744
- >
745
- Send instruction
746
- </button>
747
- <button
748
- className="hud-button hud-button-reject"
749
- type="button"
750
- onClick={() => onWorkflowAction('stop')}
751
- >
752
- Stop
753
- </button>
754
- </div>
755
- </>
756
- )}
757
- </>
758
- )}
759
- </aside>
1129
+ ))}
1130
+ </div>
760
1131
  )}
761
1132
 
1133
+ <div className="hud-right-stack">
762
1134
  {selected && infoVisible && (
763
- <aside ref={infoPanelRef} className="hud-panel hud-panel-info">
764
- <h2>{selected.name}</h2>
1135
+ <aside
1136
+ className="hud-panel hud-panel-info"
1137
+ data-minimized={infoMinimized}
1138
+ >
1139
+ <PanelChrome
1140
+ title={selected.name}
1141
+ minimized={infoMinimized}
1142
+ onMinimize={() => setInfoMinimized((current) => !current)}
1143
+ onClose={() => {
1144
+ setInfoVisible(false)
1145
+ setInfoMinimized(false)
1146
+ }}
1147
+ />
1148
+ {!infoMinimized && (
1149
+ <div ref={infoPanelRef} className="hud-panel-body">
765
1150
  <p className="path">{selected.path}</p>
766
1151
  <p>
767
1152
  {selected.lines} lines · {selected.language}
@@ -775,13 +1160,45 @@ export function HUD({
775
1160
  Inspect file
776
1161
  </button>
777
1162
  )}
1163
+ {previewing &&
1164
+ (selectedChangedFunctions.length > 0 ||
1165
+ selectedAddedFunctions.length > 0 ||
1166
+ selectedChangedVariables.length > 0 ||
1167
+ selectedAddedVariables.length > 0 ||
1168
+ selectedAddedImports.length > 0) && (
1169
+ <>
1170
+ <div className="hud-section-title hud-section-title-edit">
1171
+ LLM changes
1172
+ </div>
1173
+ <PatchSymbolChanges
1174
+ title="Functions"
1175
+ added={selectedAddedFunctions}
1176
+ changed={selectedChangedFunctions}
1177
+ />
1178
+ <PatchSymbolChanges
1179
+ title="Vars"
1180
+ added={selectedAddedVariables}
1181
+ changed={selectedChangedVariables}
1182
+ />
1183
+ <PanelList
1184
+ title="Imports"
1185
+ items={importLabels(selectedAddedImports)}
1186
+ tone="add"
1187
+ />
1188
+ </>
1189
+ )}
778
1190
  {selectedClasses.length > 0 && (
779
1191
  <>
780
1192
  <div className="hud-section-title">Classes</div>
781
1193
  <ul>
782
1194
  {selectedClasses.map((symbol) => (
783
1195
  <li key={`class-${symbol.name}`}>
784
- <span className={symbol.intended ? 'hud-intended' : undefined}>
1196
+ <span
1197
+ className={
1198
+ symbolChangeClass(functionChange.get(symbol.name)) ??
1199
+ (symbol.intended ? 'hud-intended' : undefined)
1200
+ }
1201
+ >
785
1202
  {symbol.name}
786
1203
  </span>
787
1204
  </li>
@@ -791,13 +1208,19 @@ export function HUD({
791
1208
  )}
792
1209
  <div className="hud-section-title">Functions</div>
793
1210
  {selectedFunctions.length === 0 &&
1211
+ extraAddedFunctions.length === 0 &&
794
1212
  selectedBlueprintFunctions.length === 0 ? (
795
1213
  <p>No functions</p>
796
1214
  ) : (
797
1215
  <ul>
798
1216
  {selectedFunctions.map((symbol) => (
799
1217
  <li key={`fn-${symbol.name}`}>
800
- <span className={symbol.intended ? 'hud-intended' : undefined}>
1218
+ <span
1219
+ className={
1220
+ symbolChangeClass(functionChange.get(symbol.name)) ??
1221
+ (symbol.intended ? 'hud-intended' : undefined)
1222
+ }
1223
+ >
801
1224
  {symbol.name}
802
1225
  </span>
803
1226
  {canEditBlueprint && symbol.intended && (
@@ -814,6 +1237,11 @@ export function HUD({
814
1237
  )}
815
1238
  </li>
816
1239
  ))}
1240
+ {extraAddedFunctions.map((item) => (
1241
+ <li key={`fn-add-${item.name}`}>
1242
+ <span className="hud-file-add">{item.name}</span>
1243
+ </li>
1244
+ ))}
817
1245
  </ul>
818
1246
  )}
819
1247
  {canEditBlueprint && onAddBlueprintFunction && (
@@ -824,13 +1252,19 @@ export function HUD({
824
1252
  )}
825
1253
  <div className="hud-section-title">Vars</div>
826
1254
  {selectedVariables.length === 0 &&
1255
+ extraAddedVariables.length === 0 &&
827
1256
  selectedBlueprintVariables.length === 0 ? (
828
1257
  <p>No vars</p>
829
1258
  ) : (
830
1259
  <ul>
831
1260
  {selectedVariables.map((symbol) => (
832
1261
  <li key={`var-${symbol.name}`}>
833
- <span className={symbol.intended ? 'hud-intended' : undefined}>
1262
+ <span
1263
+ className={
1264
+ symbolChangeClass(variableChange.get(symbol.name)) ??
1265
+ (symbol.intended ? 'hud-intended' : undefined)
1266
+ }
1267
+ >
834
1268
  {symbol.name}
835
1269
  </span>
836
1270
  {canEditBlueprint && symbol.intended && (
@@ -847,6 +1281,11 @@ export function HUD({
847
1281
  )}
848
1282
  </li>
849
1283
  ))}
1284
+ {extraAddedVariables.map((item) => (
1285
+ <li key={`var-add-${item.name}`}>
1286
+ <span className="hud-file-add">{item.name}</span>
1287
+ </li>
1288
+ ))}
850
1289
  </ul>
851
1290
  )}
852
1291
  {canEditBlueprint && onAddBlueprintVariable && (
@@ -855,22 +1294,6 @@ export function HUD({
855
1294
  onAdd={(name) => onAddBlueprintVariable(selected.id, name)}
856
1295
  />
857
1296
  )}
858
- {previewing && (
859
- <>
860
- <PanelList
861
- title="Added functions"
862
- items={symbolLabels(selectedAddedFunctions)}
863
- />
864
- <PanelList
865
- title="Added variables"
866
- items={symbolLabels(selectedAddedVariables)}
867
- />
868
- <PanelList
869
- title="Added imports"
870
- items={importLabels(selectedAddedImports)}
871
- />
872
- </>
873
- )}
874
1297
  <div className="hud-section-title">
875
1298
  {importedBy ? 'Imported by' : 'Imports'}
876
1299
  </div>
@@ -953,12 +1376,27 @@ export function HUD({
953
1376
  onAdd={(raw) => onAddBlueprintImport(selected.id, raw)}
954
1377
  />
955
1378
  )}
1379
+ </div>
1380
+ )}
956
1381
  </aside>
957
1382
  )}
958
1383
 
959
1384
  {!selected && selectedFolderNode && infoVisible && (
960
- <aside ref={infoPanelRef} className="hud-panel hud-panel-info">
961
- <h2>{selectedFolderNode.name}</h2>
1385
+ <aside
1386
+ className="hud-panel hud-panel-info"
1387
+ data-minimized={infoMinimized}
1388
+ >
1389
+ <PanelChrome
1390
+ title={selectedFolderNode.name}
1391
+ minimized={infoMinimized}
1392
+ onMinimize={() => setInfoMinimized((current) => !current)}
1393
+ onClose={() => {
1394
+ setInfoVisible(false)
1395
+ setInfoMinimized(false)
1396
+ }}
1397
+ />
1398
+ {!infoMinimized && (
1399
+ <div ref={infoPanelRef} className="hud-panel-body">
962
1400
  <p className="path">
963
1401
  {selectedFolderNode.path === '.'
964
1402
  ? graph.targetName
@@ -974,7 +1412,7 @@ export function HUD({
974
1412
  <ul>
975
1413
  {folderFiles.map((file) => (
976
1414
  <li key={file.id}>
977
- <span>{file.path || file.id}</span>
1415
+ <span>{file.name}</span>
978
1416
  {canInspectFile(file.id, file.userCreated) && (
979
1417
  <button
980
1418
  className="hud-item-inspect"
@@ -1008,9 +1446,32 @@ export function HUD({
1008
1446
  </button>
1009
1447
  </div>
1010
1448
  )}
1449
+ </div>
1450
+ )}
1011
1451
  </aside>
1012
1452
  )}
1013
1453
 
1454
+ {mapping && thumbnailVisible && (
1455
+ <SelectionThumbnail
1456
+ graph={graph}
1457
+ layout={layout}
1458
+ selectedId={selectedId}
1459
+ selectedFolder={selectedFolder}
1460
+ landAt={landAt}
1461
+ importedBy={importedBy}
1462
+ minimized={thumbnailMinimized}
1463
+ plannedIds={plannedIds}
1464
+ createdIds={createdIds}
1465
+ deletedIds={deletedIds}
1466
+ onMinimize={() => setThumbnailMinimized((current) => !current)}
1467
+ onHide={() => {
1468
+ setThumbnailVisible(false)
1469
+ setThumbnailMinimized(false)
1470
+ }}
1471
+ />
1472
+ )}
1473
+ </div>
1474
+
1014
1475
  <div className="hud-bottom">
1015
1476
  <div className="hud-hints">
1016
1477
  {mapping ? (
@@ -1027,15 +1488,32 @@ export function HUD({
1027
1488
  )}
1028
1489
  <span>Click a line to fly there</span>
1029
1490
  <span>Ctrl-click an island to walk</span>
1491
+ {hasChangeSet && (
1492
+ <span>
1493
+ {changePathsOnly
1494
+ ? 'C show all paths'
1495
+ : 'C show only changed paths'}
1496
+ </span>
1497
+ )}
1030
1498
  {selected?.userCreated && creatingBlueprint && (
1031
1499
  <span>Backspace delete</span>
1032
1500
  )}
1033
1501
  <span>{infoVisible ? 'I hide info' : 'I show info'}</span>
1034
1502
  {infoVisible && <span>↑↓ scroll info</span>}
1503
+ <span>
1504
+ {thumbnailVisible ? 'T hide 3D view' : 'T show 3D view'}
1505
+ </span>
1035
1506
  <span>
1036
1507
  {importedBy ? 'K show imports' : 'K show imported by'}
1037
1508
  </span>
1038
1509
  <span>M back to walk</span>
1510
+ {canStop && (
1511
+ <span>
1512
+ {sessions.length > 1
1513
+ ? 'Stop ends the focused LLM session'
1514
+ : 'Stop ends this LLM session'}
1515
+ </span>
1516
+ )}
1039
1517
  </>
1040
1518
  ) : (
1041
1519
  <>
@@ -1051,7 +1529,7 @@ export function HUD({
1051
1529
  {selected?.userCreated && creatingBlueprint && (
1052
1530
  <span>Backspace delete</span>
1053
1531
  )}
1054
- <span>Click a block for info</span>
1532
+ <span>Double-click a block for info</span>
1055
1533
  <span>Aim a line to fly</span>
1056
1534
  <span>{infoVisible ? 'I hide info' : 'I show info'}</span>
1057
1535
  {infoVisible && <span>↑↓ scroll info</span>}
@@ -1059,11 +1537,94 @@ export function HUD({
1059
1537
  {importedBy ? 'K show imports' : 'K show imported by'}
1060
1538
  </span>
1061
1539
  <span>M toggle map</span>
1062
- <span>Esc release mouse</span>
1540
+ <span>Double-click or Esc release mouse</span>
1541
+ {canStop && (
1542
+ <span>
1543
+ {sessions.length > 1
1544
+ ? 'Stop ends the focused LLM session'
1545
+ : 'Stop ends this LLM session'}
1546
+ </span>
1547
+ )}
1063
1548
  </>
1064
1549
  )}
1065
1550
  </div>
1066
1551
  <div className="hud-icon-row">
1552
+ {mapping && hasChangeSet && onToggleChangePathsOnly && (
1553
+ <button
1554
+ className="hud-button hud-icon-button"
1555
+ data-active={changePathsOnly}
1556
+ aria-label={
1557
+ changePathsOnly
1558
+ ? 'Show all folder paths'
1559
+ : 'Show only changed paths'
1560
+ }
1561
+ aria-keyshortcuts="C"
1562
+ aria-pressed={changePathsOnly}
1563
+ type="button"
1564
+ onClick={onToggleChangePathsOnly}
1565
+ >
1566
+ <svg
1567
+ viewBox="0 0 24 24"
1568
+ width="18"
1569
+ height="18"
1570
+ fill="none"
1571
+ stroke="currentColor"
1572
+ strokeWidth="2"
1573
+ strokeLinecap="round"
1574
+ strokeLinejoin="round"
1575
+ aria-hidden="true"
1576
+ >
1577
+ <circle cx="12" cy="5" r="2.4" />
1578
+ <path d="M12 7.4v4.2" />
1579
+ <path d="M12 11.6 6.2 16" />
1580
+ <path d="M12 11.6 17.8 16" />
1581
+ <circle cx="6.2" cy="18" r="2.1" />
1582
+ <circle cx="17.8" cy="18" r="2.1" />
1583
+ </svg>
1584
+ <span className="hud-tooltip">
1585
+ {changePathsOnly
1586
+ ? 'C show all paths'
1587
+ : 'C show only changed paths'}
1588
+ </span>
1589
+ </button>
1590
+ )}
1591
+ {mapping && (
1592
+ <button
1593
+ className="hud-button hud-icon-button"
1594
+ data-active={thumbnailVisible}
1595
+ aria-label={
1596
+ thumbnailVisible ? 'Hide 3D view' : 'Show 3D view'
1597
+ }
1598
+ aria-keyshortcuts="T"
1599
+ aria-pressed={thumbnailVisible}
1600
+ type="button"
1601
+ onClick={() => {
1602
+ setThumbnailVisible((visible) => {
1603
+ if (!visible) setThumbnailMinimized(false)
1604
+ return !visible
1605
+ })
1606
+ }}
1607
+ >
1608
+ <svg
1609
+ viewBox="0 0 24 24"
1610
+ width="18"
1611
+ height="18"
1612
+ fill="none"
1613
+ stroke="currentColor"
1614
+ strokeWidth="2"
1615
+ strokeLinecap="round"
1616
+ strokeLinejoin="round"
1617
+ aria-hidden="true"
1618
+ >
1619
+ <rect x="3" y="5" width="11" height="14" rx="1.5" />
1620
+ <path d="M16 8h5v11H9v-3" />
1621
+ <path d="M6.5 16.5 9 12l2 2.5 1.5-2L15 16.5" />
1622
+ </svg>
1623
+ <span className="hud-tooltip">
1624
+ {thumbnailVisible ? 'T hide 3D view' : 'T show 3D view'}
1625
+ </span>
1626
+ </button>
1627
+ )}
1067
1628
  <button
1068
1629
  className="hud-button hud-icon-button"
1069
1630
  data-active={importedBy}