@jkwd/inbase 0.1.4 → 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,21 +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
131
596
  selectedTick?: number
132
597
  selectedFolder?: string | null
598
+ landAt: [number, number]
133
599
  aimedRelation: AimedRelation | null
134
600
  currentFolder: string
135
601
  intent: AgentIntent
602
+ intents?: AgentIntent[]
603
+ focusedSessionId?: string | null
604
+ onFocusSession?: (sessionId: string) => void
136
605
  onWorkflowAction: (
606
+ sessionId: string,
137
607
  action: WorkflowAction,
138
608
  options?: { instruction?: string; step?: number },
139
609
  ) => void
140
- onNavigateDiff: (diffId: string) => void
610
+ onNavigateDiff: (sessionId: string, diffId: string) => void
141
611
  onOpenMap: () => void
142
612
  onWalk: () => void
143
613
  followLook: boolean
@@ -164,18 +634,26 @@ type HUDProps = {
164
634
  onMapAddFile?: (folderPath: string) => void
165
635
  onMapAddFolder?: (folderPath: string) => void
166
636
  onInspectFile?: (fileId: string) => void
637
+ plannedIds?: string[]
638
+ createdIds?: string[]
639
+ deletedIds?: string[]
167
640
  }
168
641
 
169
642
  export function HUD({
170
643
  graph,
644
+ layout,
171
645
  mode,
172
646
  locked,
173
647
  selectedId,
174
648
  selectedTick = 0,
175
649
  selectedFolder = null,
650
+ landAt,
176
651
  aimedRelation,
177
652
  currentFolder,
178
653
  intent,
654
+ intents,
655
+ focusedSessionId = null,
656
+ onFocusSession,
179
657
  onWorkflowAction,
180
658
  onNavigateDiff,
181
659
  onOpenMap,
@@ -200,6 +678,9 @@ export function HUD({
200
678
  onMapAddFile,
201
679
  onMapAddFolder,
202
680
  onInspectFile,
681
+ plannedIds = [],
682
+ createdIds = [],
683
+ deletedIds = [],
203
684
  }: HUDProps) {
204
685
  const selected = graph.files.find((file) => file.id === selectedId)
205
686
  const selectedFolderNode = graph.folders.find(
@@ -217,31 +698,21 @@ export function HUD({
217
698
  ? graph.files.filter((file) => file.imports.includes(selected.id))
218
699
  : []
219
700
  const mapping = mode === 'map'
220
- const sessionMode = Boolean(intent.sessionId)
701
+ const sessions = (intents ?? [intent]).filter(
702
+ (item) => item.sessionId && isReviewingIntent(item.status),
703
+ )
704
+ const canStop = canStopSession(intent)
221
705
  const [walkIntro, setWalkIntro] = useState(false)
222
706
  const walkIntroSeen = useRef(false)
223
- const [instruction, setInstruction] = useState('')
224
707
  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'
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
+ )
232
715
  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
716
  const addedFunctions = intent.addedFunctions ?? []
246
717
  const addedVariables = intent.addedVariables ?? []
247
718
  const addedImports = intent.addedImports ?? []
@@ -280,38 +751,11 @@ export function HUD({
280
751
  )
281
752
  const canEditBlueprint =
282
753
  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
754
  const canInspectFile = (fileId: string, userCreated = false) =>
307
755
  Boolean(onInspectFile) &&
308
756
  !fileId.startsWith('draft:') &&
309
757
  !(previewing && (intent.deletes ?? []).includes(fileId)) &&
310
758
  (!userCreated || (intent.creates ?? []).includes(fileId))
311
- const panelDone =
312
- intent.status === 'finished' ||
313
- intent.status === 'approved' ||
314
- doneSteps.size > 0
315
759
 
316
760
  useEffect(() => {
317
761
  if (mode !== 'walk') {
@@ -332,10 +776,6 @@ export function HUD({
332
776
  return () => window.clearTimeout(timer)
333
777
  }, [locked, mode, naming])
334
778
 
335
- useEffect(() => {
336
- setInstruction('')
337
- }, [intent.diffId])
338
-
339
779
  useEffect(() => {
340
780
  infoPanelRef.current?.scrollTo({ top: 0 })
341
781
  }, [selectedId, selectedFolder])
@@ -362,12 +802,39 @@ export function HUD({
362
802
  return
363
803
  }
364
804
  event.preventDefault()
365
- setInfoVisible((visible) => !visible)
805
+ setInfoVisible((visible) => {
806
+ if (!visible) setInfoMinimized(false)
807
+ return !visible
808
+ })
366
809
  }
367
810
  window.addEventListener('keydown', onKey)
368
811
  return () => window.removeEventListener('keydown', onKey)
369
812
  }, [])
370
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
+
371
838
  useEffect(() => {
372
839
  if (!infoVisible || (!selectedId && !selectedFolder)) return
373
840
  const onKey = (event: KeyboardEvent) => {
@@ -400,8 +867,9 @@ export function HUD({
400
867
  <div className="hud-gate-card">
401
868
  <h1>Walk</h1>
402
869
  <p>
403
- Click to look around. Press <kbd>M</kbd> to open the map, press{' '}
404
- <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.
405
873
  </p>
406
874
  <p>
407
875
  <kbd>W</kbd> <kbd>A</kbd> <kbd>S</kbd> <kbd>D</kbd> walk,{' '}
@@ -455,313 +923,57 @@ export function HUD({
455
923
  >
456
924
  Walk
457
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
+ )}
458
938
  </div>
459
939
  {selected && <div className="hud-chip">{selected.path}</div>}
460
940
  </div>
461
941
 
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)}
683
- />
684
- <PanelList
685
- title="Variables"
686
- items={symbolLabels(addedVariables)}
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}
687
955
  />
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>
956
+ ))}
957
+ </div>
760
958
  )}
761
959
 
960
+ <div className="hud-right-stack">
762
961
  {selected && infoVisible && (
763
- <aside ref={infoPanelRef} className="hud-panel hud-panel-info">
764
- <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">
765
977
  <p className="path">{selected.path}</p>
766
978
  <p>
767
979
  {selected.lines} lines · {selected.language}
@@ -953,12 +1165,27 @@ export function HUD({
953
1165
  onAdd={(raw) => onAddBlueprintImport(selected.id, raw)}
954
1166
  />
955
1167
  )}
1168
+ </div>
1169
+ )}
956
1170
  </aside>
957
1171
  )}
958
1172
 
959
1173
  {!selected && selectedFolderNode && infoVisible && (
960
- <aside ref={infoPanelRef} className="hud-panel hud-panel-info">
961
- <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">
962
1189
  <p className="path">
963
1190
  {selectedFolderNode.path === '.'
964
1191
  ? graph.targetName
@@ -1008,9 +1235,32 @@ export function HUD({
1008
1235
  </button>
1009
1236
  </div>
1010
1237
  )}
1238
+ </div>
1239
+ )}
1011
1240
  </aside>
1012
1241
  )}
1013
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
+
1014
1264
  <div className="hud-bottom">
1015
1265
  <div className="hud-hints">
1016
1266
  {mapping ? (
@@ -1032,10 +1282,20 @@ export function HUD({
1032
1282
  )}
1033
1283
  <span>{infoVisible ? 'I hide info' : 'I show info'}</span>
1034
1284
  {infoVisible && <span>↑↓ scroll info</span>}
1285
+ <span>
1286
+ {thumbnailVisible ? 'T hide 3D view' : 'T show 3D view'}
1287
+ </span>
1035
1288
  <span>
1036
1289
  {importedBy ? 'K show imports' : 'K show imported by'}
1037
1290
  </span>
1038
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
+ )}
1039
1299
  </>
1040
1300
  ) : (
1041
1301
  <>
@@ -1059,11 +1319,55 @@ export function HUD({
1059
1319
  {importedBy ? 'K show imports' : 'K show imported by'}
1060
1320
  </span>
1061
1321
  <span>M toggle map</span>
1062
- <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
+ )}
1063
1330
  </>
1064
1331
  )}
1065
1332
  </div>
1066
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
+ )}
1067
1371
  <button
1068
1372
  className="hud-button hud-icon-button"
1069
1373
  data-active={importedBy}