@jkwd/inbase 0.1.3 → 0.1.5

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
 
@@ -123,20 +126,488 @@ function AddIntentRow({
123
126
  )
124
127
  }
125
128
 
129
+ function PanelChrome({
130
+ title,
131
+ minimized = false,
132
+ onMinimize,
133
+ onClose,
134
+ }: {
135
+ title: ReactNode
136
+ minimized?: boolean
137
+ onMinimize?: () => void
138
+ onClose?: () => void
139
+ }) {
140
+ return (
141
+ <div className="hud-panel-chrome">
142
+ <div className="hud-panel-chrome-title">{title}</div>
143
+ <div className="hud-panel-controls">
144
+ {onMinimize && (
145
+ <button
146
+ className="hud-button hud-icon-button hud-panel-control"
147
+ type="button"
148
+ aria-label={minimized ? 'Restore' : 'Minimize'}
149
+ onClick={onMinimize}
150
+ >
151
+ {minimized ? '+' : '−'}
152
+ </button>
153
+ )}
154
+ {onClose && (
155
+ <button
156
+ className="hud-button hud-icon-button hud-panel-control"
157
+ type="button"
158
+ aria-label="Close"
159
+ onClick={onClose}
160
+ >
161
+ ×
162
+ </button>
163
+ )}
164
+ </div>
165
+ </div>
166
+ )
167
+ }
168
+
169
+ type SessionPanelProps = {
170
+ intent: AgentIntent
171
+ focused: boolean
172
+ naming: boolean
173
+ onFocus: () => void
174
+ onWorkflowAction: (
175
+ sessionId: string,
176
+ action: WorkflowAction,
177
+ options?: { instruction?: string; step?: number },
178
+ ) => void
179
+ onNavigateDiff: (sessionId: string, diffId: string) => void
180
+ }
181
+
182
+ function SessionPanel({
183
+ intent,
184
+ focused,
185
+ naming,
186
+ onFocus,
187
+ onWorkflowAction,
188
+ onNavigateDiff,
189
+ }: SessionPanelProps) {
190
+ const [minimized, setMinimized] = useState(false)
191
+ const [instruction, setInstruction] = useState('')
192
+ const sessionId = intent.sessionId
193
+ const pending = intent.status === 'pending' && intent.isActiveDiff
194
+ const askingBlueprint = intent.status === 'blueprint_ask'
195
+ const creatingBlueprint = intent.creationMode || intent.status === 'blueprint'
196
+ const preparing = intent.status === 'preparing'
197
+ const planReady = intent.status === 'planned'
198
+ const working = intent.status === 'working' || intent.status === 'replanning'
199
+ const previewing = intent.preview
200
+ const chainIndex = intent.chainIndex ?? 0
201
+ const previousDiff = intent.chain[chainIndex - 1]
202
+ const nextDiff = intent.chain[chainIndex + 1]
203
+ const stepLabel =
204
+ intent.step && intent.steps?.length > 0
205
+ ? `Step ${intent.step} of ${intent.steps.length}`
206
+ : 'Patch'
207
+ const lastStep =
208
+ typeof intent.step === 'number' &&
209
+ intent.steps.length > 0 &&
210
+ intent.step >= intent.steps.length
211
+ const addedFunctions = intent.addedFunctions ?? []
212
+ const addedVariables = intent.addedVariables ?? []
213
+ const addedImports = intent.addedImports ?? []
214
+ const doneSteps = new Set(
215
+ intent.status === 'finished'
216
+ ? intent.steps.map((step) => step.index)
217
+ : intent.chain
218
+ .filter(
219
+ (entry) =>
220
+ entry.status === 'applied' || entry.status === 'extended',
221
+ )
222
+ .map((entry) => entry.step),
223
+ )
224
+ if (intent.status === 'approved' && typeof intent.step === 'number') {
225
+ doneSteps.add(intent.step)
226
+ }
227
+ if (pending && typeof intent.step === 'number' && !lastStep) {
228
+ doneSteps.add(intent.step)
229
+ }
230
+ const nextStep =
231
+ intent.status === 'finished'
232
+ ? null
233
+ : (intent.steps.find((step) => !doneSteps.has(step.index)) ?? null)
234
+ const canRunNext =
235
+ Boolean(nextStep) && (planReady || pending) && !working
236
+ const canComplete = pending && lastStep
237
+ const panelDone =
238
+ intent.status === 'finished' ||
239
+ intent.status === 'approved' ||
240
+ doneSteps.size > 0
241
+
242
+ useEffect(() => {
243
+ setInstruction('')
244
+ }, [intent.diffId])
245
+
246
+ if (!sessionId || !isReviewingIntent(intent.status)) return null
247
+
248
+ const act = (
249
+ action: WorkflowAction,
250
+ options?: { instruction?: string; step?: number },
251
+ ) => onWorkflowAction(sessionId, action, options)
252
+
253
+ return (
254
+ <aside
255
+ className={
256
+ panelDone
257
+ ? 'hud-panel hud-panel-planned hud-panel-done'
258
+ : 'hud-panel hud-panel-planned'
259
+ }
260
+ data-minimized={minimized}
261
+ data-focused={focused}
262
+ onPointerDown={onFocus}
263
+ >
264
+ <PanelChrome
265
+ title={reviewTitle(intent.status)}
266
+ minimized={minimized}
267
+ onMinimize={() => setMinimized((current) => !current)}
268
+ />
269
+ {!minimized && (
270
+ <>
271
+ {intent.feature && <p className="hud-feature">{intent.feature}</p>}
272
+ {askingBlueprint ? (
273
+ <>
274
+ <p>
275
+ {intent.canEnterBlueprint
276
+ ? 'Place files and folders for this chat, then send them as a blueprint for the LLM?'
277
+ : `Blueprint edit mode is active in another chat (${intent.blueprintSessionId}). Finish or stop it before starting here.`}
278
+ </p>
279
+ <div className="hud-decide">
280
+ <button
281
+ className="hud-button hud-button-approve"
282
+ type="button"
283
+ disabled={!intent.canEnterBlueprint}
284
+ onClick={() => act('blueprint_yes')}
285
+ >
286
+ Create blueprint
287
+ </button>
288
+ <button
289
+ className="hud-button hud-button-approve"
290
+ type="button"
291
+ onClick={() => act('blueprint_no')}
292
+ >
293
+ Let LLM continue
294
+ </button>
295
+ <button
296
+ className="hud-button hud-button-reject"
297
+ type="button"
298
+ onClick={() => act('stop')}
299
+ >
300
+ Stop
301
+ </button>
302
+ </div>
303
+ </>
304
+ ) : creatingBlueprint ? (
305
+ <>
306
+ <p>
307
+ Walk the map, press <kbd>Space</kbd> for a file and{' '}
308
+ <kbd>B</kbd> for an island. Send the blueprint when the layout
309
+ is ready.
310
+ </p>
311
+ <div className="hud-decide">
312
+ <button
313
+ className="hud-button hud-button-approve"
314
+ type="button"
315
+ disabled={naming}
316
+ onClick={() => act('blueprint_send')}
317
+ >
318
+ Send blueprint
319
+ </button>
320
+ <button
321
+ className="hud-button hud-button-reject"
322
+ type="button"
323
+ onClick={() => act('stop')}
324
+ >
325
+ Stop
326
+ </button>
327
+ </div>
328
+ </>
329
+ ) : intent.status === 'finished' ? (
330
+ <p>All plan steps were applied.</p>
331
+ ) : preparing ? (
332
+ <div className="hud-working">
333
+ <span className="hud-spinner" aria-hidden="true" />
334
+ <span>LLM preparing…</span>
335
+ <button
336
+ className="hud-button hud-button-reject"
337
+ type="button"
338
+ onClick={() => act('stop')}
339
+ >
340
+ Stop
341
+ </button>
342
+ </div>
343
+ ) : working ? (
344
+ <div className="hud-working">
345
+ <span className="hud-spinner" aria-hidden="true" />
346
+ <span>
347
+ {intent.status === 'replanning'
348
+ ? 'Updating the remaining plan from your instruction…'
349
+ : `Implementing ${stepLabel.toLowerCase()}…`}
350
+ </span>
351
+ <button
352
+ className="hud-button hud-button-reject"
353
+ type="button"
354
+ onClick={() => act('stop')}
355
+ >
356
+ Stop
357
+ </button>
358
+ </div>
359
+ ) : (
360
+ <p>
361
+ {stepLabel}
362
+ {intent.reason ? ` · ${intent.reason}` : ''}
363
+ </p>
364
+ )}
365
+ {!askingBlueprint && !creatingBlueprint && (
366
+ <>
367
+ {intent.steps?.length > 0 && (
368
+ <ol className="hud-steps">
369
+ {intent.steps.map((step) => (
370
+ <li
371
+ key={step.index}
372
+ data-current={
373
+ intent.status !== 'finished' &&
374
+ step.index === intent.step &&
375
+ !doneSteps.has(step.index)
376
+ }
377
+ data-done={doneSteps.has(step.index)}
378
+ data-next={
379
+ nextStep?.index === step.index &&
380
+ !doneSteps.has(step.index)
381
+ }
382
+ >
383
+ <span className="hud-step-index">{step.index}.</span>
384
+ <span className="hud-step-main">
385
+ <span className="hud-step-title">{step.title}</span>
386
+ {((canRunNext && nextStep?.index === step.index) ||
387
+ (canComplete && step.index === intent.step)) && (
388
+ <button
389
+ className="hud-button hud-button-approve hud-run-step"
390
+ type="button"
391
+ onClick={() =>
392
+ canComplete && step.index === intent.step
393
+ ? act('continue')
394
+ : act('invoke', {
395
+ step: step.index,
396
+ })
397
+ }
398
+ >
399
+ {canComplete && step.index === intent.step
400
+ ? 'Complete'
401
+ : 'Run step'}
402
+ </button>
403
+ )}
404
+ </span>
405
+ </li>
406
+ ))}
407
+ </ol>
408
+ )}
409
+ {intent.chain.length > 0 && (
410
+ <div className="hud-chain">
411
+ <button
412
+ className="hud-button"
413
+ type="button"
414
+ disabled={!previousDiff}
415
+ onClick={() =>
416
+ previousDiff && onNavigateDiff(sessionId, previousDiff.id)
417
+ }
418
+ >
419
+ Previous
420
+ </button>
421
+ <span>
422
+ Diff {chainIndex + 1} of {intent.chain.length}
423
+ </span>
424
+ <button
425
+ className="hud-button"
426
+ type="button"
427
+ disabled={!nextDiff}
428
+ onClick={() =>
429
+ nextDiff && onNavigateDiff(sessionId, nextDiff.id)
430
+ }
431
+ >
432
+ Next
433
+ </button>
434
+ </div>
435
+ )}
436
+ <MutationFold
437
+ hasContent={
438
+ previewing &&
439
+ (intent.files.length > 0 ||
440
+ (intent.createFolders ?? []).length > 0 ||
441
+ intent.creates.length > 0 ||
442
+ intent.deletes.length > 0 ||
443
+ addedFunctions.length > 0 ||
444
+ addedVariables.length > 0 ||
445
+ addedImports.length > 0 ||
446
+ (intent.imports ?? []).length > 0)
447
+ }
448
+ >
449
+ {intent.files.length > 0 && (
450
+ <>
451
+ <div className="hud-section-title hud-section-title-edit">
452
+ Changed
453
+ </div>
454
+ <ul>
455
+ {intent.files.map((id) => (
456
+ <li className="hud-file-edit" key={id}>
457
+ {id}
458
+ </li>
459
+ ))}
460
+ </ul>
461
+ </>
462
+ )}
463
+ {(intent.createFolders ?? []).length > 0 && (
464
+ <>
465
+ <div className="hud-section-title hud-section-title-add">
466
+ Added islands
467
+ </div>
468
+ <ul>
469
+ {intent.createFolders.map((id) => (
470
+ <li className="hud-file-add" key={id}>
471
+ {id}/
472
+ </li>
473
+ ))}
474
+ </ul>
475
+ </>
476
+ )}
477
+ {intent.creates.length > 0 && (
478
+ <>
479
+ <div className="hud-section-title hud-section-title-add">
480
+ Added
481
+ </div>
482
+ <ul>
483
+ {intent.creates.map((id) => (
484
+ <li className="hud-file-add" key={id}>
485
+ {id}
486
+ </li>
487
+ ))}
488
+ </ul>
489
+ </>
490
+ )}
491
+ {intent.deletes.length > 0 && (
492
+ <>
493
+ <div className="hud-section-title hud-section-title-remove">
494
+ Removed
495
+ </div>
496
+ <ul>
497
+ {intent.deletes.map((id) => (
498
+ <li className="hud-file-remove" key={id}>
499
+ {id}
500
+ </li>
501
+ ))}
502
+ </ul>
503
+ </>
504
+ )}
505
+ <PanelList
506
+ title="Functions"
507
+ items={symbolLabels(addedFunctions)}
508
+ />
509
+ <PanelList
510
+ title="Variables"
511
+ items={symbolLabels(addedVariables)}
512
+ />
513
+ {addedImports.length > 0 && (
514
+ <PanelList title="Imports" items={importLabels(addedImports)} />
515
+ )}
516
+ {addedImports.length === 0 && (intent.imports ?? []).length > 0 && (
517
+ <>
518
+ <div className="hud-section-title">Imports</div>
519
+ <ul>
520
+ {intent.imports.map((edge) => (
521
+ <li key={`${edge.from}->${edge.to}`}>
522
+ {edge.from.split('/').pop()} → {edge.to.split('/').pop()}
523
+ </li>
524
+ ))}
525
+ </ul>
526
+ </>
527
+ )}
528
+ </MutationFold>
529
+ {planReady && (
530
+ <div className="hud-decide">
531
+ <button
532
+ className="hud-button hud-button-reject"
533
+ type="button"
534
+ onClick={() => act('stop')}
535
+ >
536
+ Stop
537
+ </button>
538
+ </div>
539
+ )}
540
+ {pending && (
541
+ <>
542
+ <label className="hud-instruction">
543
+ <span>Alternative instruction for the LLM</span>
544
+ <textarea
545
+ value={instruction}
546
+ maxLength={4000}
547
+ rows={3}
548
+ placeholder="Describe what should change in the next diff…"
549
+ onChange={(event) => setInstruction(event.target.value)}
550
+ />
551
+ </label>
552
+ <div className="hud-decide">
553
+ {lastStep && (
554
+ <button
555
+ className="hud-button hud-button-approve"
556
+ type="button"
557
+ onClick={() => act('continue')}
558
+ >
559
+ Complete
560
+ </button>
561
+ )}
562
+ <button
563
+ className="hud-button hud-button-extend"
564
+ type="button"
565
+ disabled={!instruction.trim()}
566
+ onClick={() =>
567
+ act('instruct', { instruction })
568
+ }
569
+ >
570
+ Send instruction
571
+ </button>
572
+ <button
573
+ className="hud-button hud-button-reject"
574
+ type="button"
575
+ onClick={() => act('stop')}
576
+ >
577
+ Stop
578
+ </button>
579
+ </div>
580
+ </>
581
+ )}
582
+ </>
583
+ )}
584
+ </>
585
+ )}
586
+ </aside>
587
+ )
588
+ }
589
+
126
590
  type HUDProps = {
127
591
  graph: CodebaseGraph
592
+ layout: WorldLayout
128
593
  mode: ViewMode
129
594
  locked: boolean
130
595
  selectedId: string | null
596
+ selectedTick?: number
131
597
  selectedFolder?: string | null
598
+ landAt: [number, number]
132
599
  aimedRelation: AimedRelation | null
133
600
  currentFolder: string
134
601
  intent: AgentIntent
602
+ intents?: AgentIntent[]
603
+ focusedSessionId?: string | null
604
+ onFocusSession?: (sessionId: string) => void
135
605
  onWorkflowAction: (
606
+ sessionId: string,
136
607
  action: WorkflowAction,
137
608
  options?: { instruction?: string; step?: number },
138
609
  ) => void
139
- onNavigateDiff: (diffId: string) => void
610
+ onNavigateDiff: (sessionId: string, diffId: string) => void
140
611
  onOpenMap: () => void
141
612
  onWalk: () => void
142
613
  followLook: boolean
@@ -162,17 +633,27 @@ type HUDProps = {
162
633
  ) => void
163
634
  onMapAddFile?: (folderPath: string) => void
164
635
  onMapAddFolder?: (folderPath: string) => void
636
+ onInspectFile?: (fileId: string) => void
637
+ plannedIds?: string[]
638
+ createdIds?: string[]
639
+ deletedIds?: string[]
165
640
  }
166
641
 
167
642
  export function HUD({
168
643
  graph,
644
+ layout,
169
645
  mode,
170
646
  locked,
171
647
  selectedId,
648
+ selectedTick = 0,
172
649
  selectedFolder = null,
650
+ landAt,
173
651
  aimedRelation,
174
652
  currentFolder,
175
653
  intent,
654
+ intents,
655
+ focusedSessionId = null,
656
+ onFocusSession,
176
657
  onWorkflowAction,
177
658
  onNavigateDiff,
178
659
  onOpenMap,
@@ -196,6 +677,10 @@ export function HUD({
196
677
  onRemoveBlueprintImport,
197
678
  onMapAddFile,
198
679
  onMapAddFolder,
680
+ onInspectFile,
681
+ plannedIds = [],
682
+ createdIds = [],
683
+ deletedIds = [],
199
684
  }: HUDProps) {
200
685
  const selected = graph.files.find((file) => file.id === selectedId)
201
686
  const selectedFolderNode = graph.folders.find(
@@ -213,31 +698,21 @@ export function HUD({
213
698
  ? graph.files.filter((file) => file.imports.includes(selected.id))
214
699
  : []
215
700
  const mapping = mode === 'map'
216
- const sessionMode = Boolean(intent.sessionId)
701
+ const sessions = (intents ?? [intent]).filter(
702
+ (item) => item.sessionId && isReviewingIntent(item.status),
703
+ )
704
+ const canStop = canStopSession(intent)
217
705
  const [walkIntro, setWalkIntro] = useState(false)
218
706
  const walkIntroSeen = useRef(false)
219
- const [instruction, setInstruction] = useState('')
220
707
  const [infoVisible, setInfoVisible] = useState(false)
221
- const infoPanelRef = useRef<HTMLElement>(null)
222
- const pending = intent.status === 'pending' && intent.isActiveDiff
223
- const askingBlueprint = intent.status === 'blueprint_ask'
224
- const creatingBlueprint = intent.creationMode || intent.status === 'blueprint'
225
- const preparing = intent.status === 'preparing'
226
- const planReady = intent.status === 'planned'
227
- const working = intent.status === 'working' || intent.status === 'replanning'
708
+ const [infoMinimized, setInfoMinimized] = useState(false)
709
+ const [thumbnailVisible, setThumbnailVisible] = useState(true)
710
+ const [thumbnailMinimized, setThumbnailMinimized] = useState(false)
711
+ const infoPanelRef = useRef<HTMLDivElement>(null)
712
+ const creatingBlueprint = sessions.some(
713
+ (item) => item.creationMode || item.status === 'blueprint',
714
+ )
228
715
  const previewing = intent.preview
229
- const reviewing = isReviewingIntent(intent.status) || intent.chain.length > 0
230
- const chainIndex = intent.chainIndex ?? 0
231
- const previousDiff = intent.chain[chainIndex - 1]
232
- const nextDiff = intent.chain[chainIndex + 1]
233
- const stepLabel =
234
- intent.step && intent.steps?.length > 0
235
- ? `Step ${intent.step} of ${intent.steps.length}`
236
- : 'Patch'
237
- const lastStep =
238
- typeof intent.step === 'number' &&
239
- intent.steps.length > 0 &&
240
- intent.step >= intent.steps.length
241
716
  const addedFunctions = intent.addedFunctions ?? []
242
717
  const addedVariables = intent.addedVariables ?? []
243
718
  const addedImports = intent.addedImports ?? []
@@ -276,33 +751,11 @@ export function HUD({
276
751
  )
277
752
  const canEditBlueprint =
278
753
  creatingBlueprint && Boolean(selected) && !selected?.id.startsWith('draft:')
279
- const doneSteps = new Set(
280
- intent.status === 'finished'
281
- ? intent.steps.map((step) => step.index)
282
- : intent.chain
283
- .filter(
284
- (entry) =>
285
- entry.status === 'applied' || entry.status === 'extended',
286
- )
287
- .map((entry) => entry.step),
288
- )
289
- if (intent.status === 'approved' && typeof intent.step === 'number') {
290
- doneSteps.add(intent.step)
291
- }
292
- if (pending && typeof intent.step === 'number' && !lastStep) {
293
- doneSteps.add(intent.step)
294
- }
295
- const nextStep =
296
- intent.status === 'finished'
297
- ? null
298
- : (intent.steps.find((step) => !doneSteps.has(step.index)) ?? null)
299
- const canRunNext =
300
- Boolean(nextStep) && (planReady || pending) && !working
301
- const canComplete = pending && lastStep
302
- const panelDone =
303
- intent.status === 'finished' ||
304
- intent.status === 'approved' ||
305
- doneSteps.size > 0
754
+ const canInspectFile = (fileId: string, userCreated = false) =>
755
+ Boolean(onInspectFile) &&
756
+ !fileId.startsWith('draft:') &&
757
+ !(previewing && (intent.deletes ?? []).includes(fileId)) &&
758
+ (!userCreated || (intent.creates ?? []).includes(fileId))
306
759
 
307
760
  useEffect(() => {
308
761
  if (mode !== 'walk') {
@@ -323,17 +776,13 @@ export function HUD({
323
776
  return () => window.clearTimeout(timer)
324
777
  }, [locked, mode, naming])
325
778
 
326
- useEffect(() => {
327
- setInstruction('')
328
- }, [intent.diffId])
329
-
330
779
  useEffect(() => {
331
780
  infoPanelRef.current?.scrollTo({ top: 0 })
332
781
  }, [selectedId, selectedFolder])
333
782
 
334
783
  useEffect(() => {
335
- if (sessionMode && selectedId) setInfoVisible(true)
336
- }, [selectedId, sessionMode])
784
+ if (selectedId) setInfoVisible(true)
785
+ }, [selectedId, selectedTick])
337
786
 
338
787
  useEffect(() => {
339
788
  if (selectedFolder) setInfoVisible(true)
@@ -353,12 +802,39 @@ export function HUD({
353
802
  return
354
803
  }
355
804
  event.preventDefault()
356
- setInfoVisible((visible) => !visible)
805
+ setInfoVisible((visible) => {
806
+ if (!visible) setInfoMinimized(false)
807
+ return !visible
808
+ })
357
809
  }
358
810
  window.addEventListener('keydown', onKey)
359
811
  return () => window.removeEventListener('keydown', onKey)
360
812
  }, [])
361
813
 
814
+ useEffect(() => {
815
+ if (!mapping) return
816
+ const onKey = (event: KeyboardEvent) => {
817
+ if (event.repeat || event.code !== 'KeyT') return
818
+ const target = event.target
819
+ if (
820
+ target instanceof HTMLElement &&
821
+ (target.tagName === 'TEXTAREA' ||
822
+ target.tagName === 'INPUT' ||
823
+ target.tagName === 'SELECT' ||
824
+ target.isContentEditable)
825
+ ) {
826
+ return
827
+ }
828
+ event.preventDefault()
829
+ setThumbnailVisible((visible) => {
830
+ if (!visible) setThumbnailMinimized(false)
831
+ return !visible
832
+ })
833
+ }
834
+ window.addEventListener('keydown', onKey)
835
+ return () => window.removeEventListener('keydown', onKey)
836
+ }, [mapping])
837
+
362
838
  useEffect(() => {
363
839
  if (!infoVisible || (!selectedId && !selectedFolder)) return
364
840
  const onKey = (event: KeyboardEvent) => {
@@ -391,8 +867,9 @@ export function HUD({
391
867
  <div className="hud-gate-card">
392
868
  <h1>Walk</h1>
393
869
  <p>
394
- Click to look around. Press <kbd>M</kbd> to open the map, press{' '}
395
- <kbd>M</kbd> again to return here.
870
+ Click to look around. Double-click to release the mouse. Press{' '}
871
+ <kbd>M</kbd> to open the map, press <kbd>M</kbd> again to return
872
+ here.
396
873
  </p>
397
874
  <p>
398
875
  <kbd>W</kbd> <kbd>A</kbd> <kbd>S</kbd> <kbd>D</kbd> walk,{' '}
@@ -402,7 +879,7 @@ export function HUD({
402
879
  , <kbd>Space</kbd> place a file, <kbd>B</kbd> place an island
403
880
  </>
404
881
  ) : null}
405
- , click a block for imports.
882
+ , click a block for info.
406
883
  </p>
407
884
  </div>
408
885
  </div>
@@ -446,317 +923,70 @@ export function HUD({
446
923
  >
447
924
  Walk
448
925
  </button>
926
+ {canStop && (
927
+ <button
928
+ className="hud-button hud-button-reject"
929
+ type="button"
930
+ aria-label="Stop LLM session"
931
+ onClick={() =>
932
+ intent.sessionId && onWorkflowAction(intent.sessionId, 'stop')
933
+ }
934
+ >
935
+ Stop
936
+ </button>
937
+ )}
449
938
  </div>
450
939
  {selected && <div className="hud-chip">{selected.path}</div>}
451
940
  </div>
452
941
 
453
- {reviewing && (
454
- <aside
455
- className={
456
- panelDone
457
- ? 'hud-panel hud-panel-planned hud-panel-done'
458
- : 'hud-panel hud-panel-planned'
459
- }
460
- >
461
- <div className="hud-section-title">
462
- {reviewTitle(intent.status)}
463
- </div>
464
- {intent.feature && <p className="hud-feature">{intent.feature}</p>}
465
- {askingBlueprint ? (
466
- <>
467
- <p>
468
- {intent.canEnterBlueprint
469
- ? 'Place files and islands for this chat, then send them as a blueprint for the LLM?'
470
- : `Blueprint edit mode is active in another chat (${intent.blueprintSessionId}). Finish or stop it before starting here.`}
471
- </p>
472
- <div className="hud-decide">
473
- <button
474
- className="hud-button hud-button-approve"
475
- type="button"
476
- disabled={!intent.canEnterBlueprint}
477
- onClick={() => onWorkflowAction('blueprint_yes')}
478
- >
479
- Yes
480
- </button>
481
- <button
482
- className="hud-button"
483
- type="button"
484
- onClick={() => onWorkflowAction('blueprint_no')}
485
- >
486
- No
487
- </button>
488
- <button
489
- className="hud-button hud-button-reject"
490
- type="button"
491
- onClick={() => onWorkflowAction('stop')}
492
- >
493
- Stop
494
- </button>
495
- </div>
496
- </>
497
- ) : creatingBlueprint ? (
498
- <>
499
- <p>
500
- Walk the map, press <kbd>Space</kbd> for a file and{' '}
501
- <kbd>B</kbd> for an island. Send the blueprint when the layout
502
- is ready.
503
- </p>
504
- <div className="hud-decide">
505
- <button
506
- className="hud-button hud-button-approve"
507
- type="button"
508
- disabled={naming}
509
- onClick={() => onWorkflowAction('blueprint_send')}
510
- >
511
- Send blueprint
512
- </button>
513
- <button
514
- className="hud-button hud-button-reject"
515
- type="button"
516
- onClick={() => onWorkflowAction('stop')}
517
- >
518
- Stop
519
- </button>
520
- </div>
521
- </>
522
- ) : intent.status === 'finished' ? (
523
- <p>All plan steps were applied.</p>
524
- ) : preparing ? (
525
- <div className="hud-working">
526
- <span className="hud-spinner" aria-hidden="true" />
527
- <span>LLM preparing…</span>
528
- </div>
529
- ) : working ? (
530
- <div className="hud-working">
531
- <span className="hud-spinner" aria-hidden="true" />
532
- <span>
533
- {intent.status === 'replanning'
534
- ? 'Updating the remaining plan from your instruction…'
535
- : `Implementing ${stepLabel.toLowerCase()}…`}
536
- </span>
537
- </div>
538
- ) : (
539
- <p>
540
- {stepLabel}
541
- {intent.reason ? ` · ${intent.reason}` : ''}
542
- </p>
543
- )}
544
- {!askingBlueprint && !creatingBlueprint && (
545
- <>
546
- {intent.steps?.length > 0 && (
547
- <ol className="hud-steps">
548
- {intent.steps.map((step) => (
549
- <li
550
- key={step.index}
551
- data-current={
552
- intent.status !== 'finished' &&
553
- step.index === intent.step &&
554
- !doneSteps.has(step.index)
555
- }
556
- data-done={doneSteps.has(step.index)}
557
- data-next={
558
- nextStep?.index === step.index && !doneSteps.has(step.index)
559
- }
560
- >
561
- <span className="hud-step-index">{step.index}.</span>
562
- <span className="hud-step-main">
563
- <span className="hud-step-title">{step.title}</span>
564
- {((canRunNext && nextStep?.index === step.index) ||
565
- (canComplete && step.index === intent.step)) && (
566
- <button
567
- className="hud-button hud-button-approve hud-run-step"
568
- type="button"
569
- onClick={() =>
570
- canComplete && step.index === intent.step
571
- ? onWorkflowAction('continue')
572
- : onWorkflowAction('invoke', {
573
- step: step.index,
574
- })
575
- }
576
- >
577
- {canComplete && step.index === intent.step
578
- ? 'Complete'
579
- : 'Run step'}
580
- </button>
581
- )}
582
- </span>
583
- </li>
584
- ))}
585
- </ol>
586
- )}
587
- {intent.chain.length > 0 && (
588
- <div className="hud-chain">
589
- <button
590
- className="hud-button"
591
- type="button"
592
- disabled={!previousDiff}
593
- onClick={() => previousDiff && onNavigateDiff(previousDiff.id)}
594
- >
595
- Previous
596
- </button>
597
- <span>
598
- Diff {chainIndex + 1} of {intent.chain.length}
599
- </span>
600
- <button
601
- className="hud-button"
602
- type="button"
603
- disabled={!nextDiff}
604
- onClick={() => nextDiff && onNavigateDiff(nextDiff.id)}
605
- >
606
- Next
607
- </button>
608
- </div>
609
- )}
610
- <MutationFold
611
- hasContent={
612
- previewing &&
613
- (intent.files.length > 0 ||
614
- (intent.createFolders ?? []).length > 0 ||
615
- intent.creates.length > 0 ||
616
- intent.deletes.length > 0 ||
617
- addedFunctions.length > 0 ||
618
- addedVariables.length > 0 ||
619
- addedImports.length > 0 ||
620
- (intent.imports ?? []).length > 0)
621
- }
622
- >
623
- {intent.files.length > 0 && (
624
- <>
625
- <div className="hud-section-title hud-section-title-edit">Changed</div>
626
- <ul>
627
- {intent.files.map((id) => (
628
- <li className="hud-file-edit" key={id}>
629
- {id}
630
- </li>
631
- ))}
632
- </ul>
633
- </>
634
- )}
635
- {(intent.createFolders ?? []).length > 0 && (
636
- <>
637
- <div className="hud-section-title hud-section-title-add">Added islands</div>
638
- <ul>
639
- {intent.createFolders.map((id) => (
640
- <li className="hud-file-add" key={id}>
641
- {id}/
642
- </li>
643
- ))}
644
- </ul>
645
- </>
646
- )}
647
- {intent.creates.length > 0 && (
648
- <>
649
- <div className="hud-section-title hud-section-title-add">Added</div>
650
- <ul>
651
- {intent.creates.map((id) => (
652
- <li className="hud-file-add" key={id}>
653
- {id}
654
- </li>
655
- ))}
656
- </ul>
657
- </>
658
- )}
659
- {intent.deletes.length > 0 && (
660
- <>
661
- <div className="hud-section-title hud-section-title-remove">Removed</div>
662
- <ul>
663
- {intent.deletes.map((id) => (
664
- <li className="hud-file-remove" key={id}>
665
- {id}
666
- </li>
667
- ))}
668
- </ul>
669
- </>
670
- )}
671
- <PanelList
672
- title="Functions"
673
- items={symbolLabels(addedFunctions)}
942
+ {sessions.length > 0 && (
943
+ <div className="hud-left-stack">
944
+ {sessions.map((session) => (
945
+ <SessionPanel
946
+ key={session.sessionId}
947
+ intent={session}
948
+ focused={session.sessionId === (focusedSessionId ?? intent.sessionId)}
949
+ naming={naming}
950
+ onFocus={() => {
951
+ if (session.sessionId) onFocusSession?.(session.sessionId)
952
+ }}
953
+ onWorkflowAction={onWorkflowAction}
954
+ onNavigateDiff={onNavigateDiff}
674
955
  />
675
- <PanelList
676
- title="Variables"
677
- items={symbolLabels(addedVariables)}
678
- />
679
- {addedImports.length > 0 && (
680
- <PanelList title="Imports" items={importLabels(addedImports)} />
681
- )}
682
- {addedImports.length === 0 && (intent.imports ?? []).length > 0 && (
683
- <>
684
- <div className="hud-section-title">Imports</div>
685
- <ul>
686
- {intent.imports.map((edge) => (
687
- <li key={`${edge.from}->${edge.to}`}>
688
- {edge.from.split('/').pop()} → {edge.to.split('/').pop()}
689
- </li>
690
- ))}
691
- </ul>
692
- </>
693
- )}
694
- </MutationFold>
695
- {(planReady || preparing) && (
696
- <div className="hud-decide">
697
- <button
698
- className="hud-button hud-button-reject"
699
- type="button"
700
- onClick={() => onWorkflowAction('stop')}
701
- >
702
- Stop
703
- </button>
704
- </div>
705
- )}
706
- {pending && (
707
- <>
708
- <label className="hud-instruction">
709
- <span>Alternative instruction for the LLM</span>
710
- <textarea
711
- value={instruction}
712
- maxLength={4000}
713
- rows={3}
714
- placeholder="Describe what should change in the next diff…"
715
- onChange={(event) => setInstruction(event.target.value)}
716
- />
717
- </label>
718
- <div className="hud-decide">
719
- {lastStep && (
720
- <button
721
- className="hud-button hud-button-approve"
722
- type="button"
723
- onClick={() => onWorkflowAction('continue')}
724
- >
725
- Complete
726
- </button>
727
- )}
728
- <button
729
- className="hud-button hud-button-extend"
730
- type="button"
731
- disabled={!instruction.trim()}
732
- onClick={() =>
733
- onWorkflowAction('instruct', { instruction })
734
- }
735
- >
736
- Send instruction
737
- </button>
738
- <button
739
- className="hud-button hud-button-reject"
740
- type="button"
741
- onClick={() => onWorkflowAction('stop')}
742
- >
743
- Stop
744
- </button>
745
- </div>
746
- </>
747
- )}
748
- </>
749
- )}
750
- </aside>
956
+ ))}
957
+ </div>
751
958
  )}
752
959
 
960
+ <div className="hud-right-stack">
753
961
  {selected && infoVisible && (
754
- <aside ref={infoPanelRef} className="hud-panel hud-panel-info">
755
- <h2>{selected.name}</h2>
962
+ <aside
963
+ className="hud-panel hud-panel-info"
964
+ data-minimized={infoMinimized}
965
+ >
966
+ <PanelChrome
967
+ title={selected.name}
968
+ minimized={infoMinimized}
969
+ onMinimize={() => setInfoMinimized((current) => !current)}
970
+ onClose={() => {
971
+ setInfoVisible(false)
972
+ setInfoMinimized(false)
973
+ }}
974
+ />
975
+ {!infoMinimized && (
976
+ <div ref={infoPanelRef} className="hud-panel-body">
756
977
  <p className="path">{selected.path}</p>
757
978
  <p>
758
979
  {selected.lines} lines · {selected.language}
759
980
  </p>
981
+ {canInspectFile(selected.id, selected.userCreated) && (
982
+ <button
983
+ className="hud-button hud-inspect"
984
+ type="button"
985
+ onClick={() => onInspectFile?.(selected.id)}
986
+ >
987
+ Inspect file
988
+ </button>
989
+ )}
760
990
  {selectedClasses.length > 0 && (
761
991
  <>
762
992
  <div className="hud-section-title">Classes</div>
@@ -935,12 +1165,27 @@ export function HUD({
935
1165
  onAdd={(raw) => onAddBlueprintImport(selected.id, raw)}
936
1166
  />
937
1167
  )}
1168
+ </div>
1169
+ )}
938
1170
  </aside>
939
1171
  )}
940
1172
 
941
1173
  {!selected && selectedFolderNode && infoVisible && (
942
- <aside ref={infoPanelRef} className="hud-panel hud-panel-info">
943
- <h2>{selectedFolderNode.name}</h2>
1174
+ <aside
1175
+ className="hud-panel hud-panel-info"
1176
+ data-minimized={infoMinimized}
1177
+ >
1178
+ <PanelChrome
1179
+ title={selectedFolderNode.name}
1180
+ minimized={infoMinimized}
1181
+ onMinimize={() => setInfoMinimized((current) => !current)}
1182
+ onClose={() => {
1183
+ setInfoVisible(false)
1184
+ setInfoMinimized(false)
1185
+ }}
1186
+ />
1187
+ {!infoMinimized && (
1188
+ <div ref={infoPanelRef} className="hud-panel-body">
944
1189
  <p className="path">
945
1190
  {selectedFolderNode.path === '.'
946
1191
  ? graph.targetName
@@ -955,7 +1200,18 @@ export function HUD({
955
1200
  ) : (
956
1201
  <ul>
957
1202
  {folderFiles.map((file) => (
958
- <li key={file.id}>{file.path || file.id}</li>
1203
+ <li key={file.id}>
1204
+ <span>{file.path || file.id}</span>
1205
+ {canInspectFile(file.id, file.userCreated) && (
1206
+ <button
1207
+ className="hud-item-inspect"
1208
+ type="button"
1209
+ onClick={() => onInspectFile?.(file.id)}
1210
+ >
1211
+ Inspect
1212
+ </button>
1213
+ )}
1214
+ </li>
959
1215
  ))}
960
1216
  </ul>
961
1217
  )}
@@ -979,20 +1235,39 @@ export function HUD({
979
1235
  </button>
980
1236
  </div>
981
1237
  )}
1238
+ </div>
1239
+ )}
982
1240
  </aside>
983
1241
  )}
984
1242
 
1243
+ {mapping && thumbnailVisible && (
1244
+ <SelectionThumbnail
1245
+ graph={graph}
1246
+ layout={layout}
1247
+ selectedId={selectedId}
1248
+ selectedFolder={selectedFolder}
1249
+ landAt={landAt}
1250
+ importedBy={importedBy}
1251
+ minimized={thumbnailMinimized}
1252
+ plannedIds={plannedIds}
1253
+ createdIds={createdIds}
1254
+ deletedIds={deletedIds}
1255
+ onMinimize={() => setThumbnailMinimized((current) => !current)}
1256
+ onHide={() => {
1257
+ setThumbnailVisible(false)
1258
+ setThumbnailMinimized(false)
1259
+ }}
1260
+ />
1261
+ )}
1262
+ </div>
1263
+
985
1264
  <div className="hud-bottom">
986
1265
  <div className="hud-hints">
987
1266
  {mapping ? (
988
1267
  <>
989
1268
  <span>Scroll zoom</span>
990
1269
  <span>Drag pan</span>
991
- <span>
992
- {sessionMode
993
- ? 'Click a block for info'
994
- : 'Click a block for relations'}
995
- </span>
1270
+ <span>Click a block for info</span>
996
1271
  <span>Click an island for its files</span>
997
1272
  {creatingBlueprint && (
998
1273
  <>
@@ -1001,24 +1276,26 @@ export function HUD({
1001
1276
  </>
1002
1277
  )}
1003
1278
  <span>Click a line to fly there</span>
1004
- <span>Shift-click ground to walk</span>
1279
+ <span>Ctrl-click an island to walk</span>
1005
1280
  {selected?.userCreated && creatingBlueprint && (
1006
1281
  <span>Backspace delete</span>
1007
1282
  )}
1283
+ <span>{infoVisible ? 'I hide info' : 'I show info'}</span>
1284
+ {infoVisible && <span>↑↓ scroll info</span>}
1008
1285
  <span>
1009
- {sessionMode
1010
- ? infoVisible
1011
- ? 'I hide info'
1012
- : 'Click a block for info'
1013
- : infoVisible
1014
- ? 'I hide info'
1015
- : 'I show info'}
1286
+ {thumbnailVisible ? 'T hide 3D view' : 'T show 3D view'}
1016
1287
  </span>
1017
- {infoVisible && <span>↑↓ scroll info</span>}
1018
1288
  <span>
1019
1289
  {importedBy ? 'K show imports' : 'K show imported by'}
1020
1290
  </span>
1021
1291
  <span>M back to walk</span>
1292
+ {canStop && (
1293
+ <span>
1294
+ {sessions.length > 1
1295
+ ? 'Stop ends the focused LLM session'
1296
+ : 'Stop ends this LLM session'}
1297
+ </span>
1298
+ )}
1022
1299
  </>
1023
1300
  ) : (
1024
1301
  <>
@@ -1034,31 +1311,63 @@ export function HUD({
1034
1311
  {selected?.userCreated && creatingBlueprint && (
1035
1312
  <span>Backspace delete</span>
1036
1313
  )}
1037
- <span>
1038
- {sessionMode
1039
- ? 'Click a block for info'
1040
- : 'Click block for relations'}
1041
- </span>
1314
+ <span>Click a block for info</span>
1042
1315
  <span>Aim a line to fly</span>
1043
- <span>
1044
- {sessionMode
1045
- ? infoVisible
1046
- ? 'I hide info'
1047
- : 'Click a block for info'
1048
- : infoVisible
1049
- ? 'I hide info'
1050
- : 'I show info'}
1051
- </span>
1316
+ <span>{infoVisible ? 'I hide info' : 'I show info'}</span>
1052
1317
  {infoVisible && <span>↑↓ scroll info</span>}
1053
1318
  <span>
1054
1319
  {importedBy ? 'K show imports' : 'K show imported by'}
1055
1320
  </span>
1056
1321
  <span>M toggle map</span>
1057
- <span>Esc release mouse</span>
1322
+ <span>Double-click or Esc release mouse</span>
1323
+ {canStop && (
1324
+ <span>
1325
+ {sessions.length > 1
1326
+ ? 'Stop ends the focused LLM session'
1327
+ : 'Stop ends this LLM session'}
1328
+ </span>
1329
+ )}
1058
1330
  </>
1059
1331
  )}
1060
1332
  </div>
1061
1333
  <div className="hud-icon-row">
1334
+ {mapping && (
1335
+ <button
1336
+ className="hud-button hud-icon-button"
1337
+ data-active={thumbnailVisible}
1338
+ aria-label={
1339
+ thumbnailVisible ? 'Hide 3D view' : 'Show 3D view'
1340
+ }
1341
+ aria-keyshortcuts="T"
1342
+ aria-pressed={thumbnailVisible}
1343
+ type="button"
1344
+ onClick={() => {
1345
+ setThumbnailVisible((visible) => {
1346
+ if (!visible) setThumbnailMinimized(false)
1347
+ return !visible
1348
+ })
1349
+ }}
1350
+ >
1351
+ <svg
1352
+ viewBox="0 0 24 24"
1353
+ width="18"
1354
+ height="18"
1355
+ fill="none"
1356
+ stroke="currentColor"
1357
+ strokeWidth="2"
1358
+ strokeLinecap="round"
1359
+ strokeLinejoin="round"
1360
+ aria-hidden="true"
1361
+ >
1362
+ <rect x="3" y="5" width="11" height="14" rx="1.5" />
1363
+ <path d="M16 8h5v11H9v-3" />
1364
+ <path d="M6.5 16.5 9 12l2 2.5 1.5-2L15 16.5" />
1365
+ </svg>
1366
+ <span className="hud-tooltip">
1367
+ {thumbnailVisible ? 'T hide 3D view' : 'T show 3D view'}
1368
+ </span>
1369
+ </button>
1370
+ )}
1062
1371
  <button
1063
1372
  className="hud-button hud-icon-button"
1064
1373
  data-active={importedBy}