@jkwd/inbase 0.1.21 → 0.1.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/README.md +13 -7
  2. package/apps/explorer/package.json +1 -0
  3. package/apps/explorer/scripts/explain-store.d.ts +135 -0
  4. package/apps/explorer/scripts/explain-store.mjs +666 -0
  5. package/apps/explorer/scripts/patch-lib.mjs +4 -0
  6. package/apps/explorer/scripts/scan-target.mjs +22 -5
  7. package/apps/explorer/scripts/session-store.d.ts +86 -11
  8. package/apps/explorer/scripts/session-store.mjs +371 -58
  9. package/apps/explorer/scripts/target-config.d.ts +38 -3
  10. package/apps/explorer/scripts/target-config.mjs +147 -3
  11. package/apps/explorer/src/App.tsx +1073 -158
  12. package/apps/explorer/src/agentIntent.ts +61 -7
  13. package/apps/explorer/src/codebase.ts +1 -1
  14. package/apps/explorer/src/devTargets.ts +66 -0
  15. package/apps/explorer/src/explain.ts +312 -0
  16. package/apps/explorer/src/index.css +874 -222
  17. package/apps/explorer/src/layout.ts +55 -0
  18. package/apps/explorer/src/scene/DistantFileBlocks.tsx +4 -2
  19. package/apps/explorer/src/scene/FileBlock.tsx +95 -72
  20. package/apps/explorer/src/scene/FolderArea.tsx +55 -32
  21. package/apps/explorer/src/scene/MapView.tsx +506 -33
  22. package/apps/explorer/src/scene/RelationLines.tsx +7 -0
  23. package/apps/explorer/src/scene/World.tsx +146 -32
  24. package/apps/explorer/src/speech.ts +228 -0
  25. package/apps/explorer/src/theme.ts +29 -0
  26. package/apps/explorer/src/types.ts +98 -8
  27. package/apps/explorer/src/ui/CanvasErrorBoundary.tsx +1 -1
  28. package/apps/explorer/src/ui/ExplainAskCard.tsx +142 -0
  29. package/apps/explorer/src/ui/ExplainHud.tsx +524 -0
  30. package/apps/explorer/src/ui/ExplainInfoPanel.tsx +135 -0
  31. package/apps/explorer/src/ui/ExplainPointer.tsx +73 -0
  32. package/apps/explorer/src/ui/EyeIcon.tsx +38 -1
  33. package/apps/explorer/src/ui/HUD.tsx +1067 -795
  34. package/apps/explorer/src/ui/NameInput.tsx +114 -5
  35. package/apps/explorer/src/userContext.ts +0 -11
  36. package/apps/explorer/src/userCreated.ts +54 -1
  37. package/apps/explorer/vite.config.ts +173 -26
  38. package/bin/inbase.mjs +11 -2
  39. package/bin/project.mjs +1 -1
  40. package/bin/session.mjs +287 -38
  41. package/package.json +4 -1
  42. package/skill/commands/amber.md +23 -0
  43. package/skill/commands/blue.md +13 -0
  44. package/skill/commands/coral.md +23 -0
  45. package/skill/commands/explain.md +77 -0
  46. package/skill/commands/green.md +23 -0
  47. package/skill/commands/inbase.md +7 -5
  48. package/skill/commands/lime.md +23 -0
  49. package/skill/commands/orange.md +23 -0
  50. package/skill/commands/purple.md +23 -0
  51. package/skill/commands/red.md +23 -0
  52. package/skill/commands/skipinbase.md +1 -1
  53. package/skill/commands/violet.md +23 -0
  54. package/skill/commands/yellow.md +23 -0
  55. package/skill/inbase/SKILL.md +124 -76
  56. package/apps/explorer/src/scene/BlockPlacer.tsx +0 -78
  57. package/apps/explorer/src/scene/IslandPlacer.tsx +0 -31
  58. package/apps/explorer/src/scene/SelectionThumbnail.tsx +0 -1069
@@ -1,29 +1,29 @@
1
- import { useEffect, useRef, useState, type ReactNode } from 'react'
2
- import { persistAddContextFiles, persistInitialInstruction, persistRemoveContextFile } from '../agentIntent'
3
- import { NameInput } from './NameInput'
4
- import { SelectionThumbnail } from '../scene/SelectionThumbnail'
5
- import { CanvasErrorBoundary } from './CanvasErrorBoundary'
1
+ import { useEffect, useLayoutEffect, useRef, useState, type CSSProperties, type ReactNode } from 'react'
2
+ import { createPortal } from 'react-dom'
3
+ import { InfoNameField, NameInput } from './NameInput'
6
4
  import {
7
- canStopSession,
8
5
  isReviewingIntent,
9
6
  type AgentIntent,
10
7
  type AgentIntentStatus,
11
8
  type AimedRelation,
12
9
  type BlueprintNote,
13
10
  type BlueprintNoteKind,
11
+ GLOBAL_BLUEPRINT_COLOR,
12
+ type BlueprintOption,
14
13
  type BlueprintPointer,
15
14
  type BlueprintPointerKind,
16
15
  type BranchChanges,
17
16
  type CodebaseGraph,
17
+ type ExplainTargetKind,
18
18
  type PatchImportAddition,
19
19
  type PatchSymbolAddition,
20
20
  type ViewMode,
21
- type WorldLayout,
22
21
  type WorkflowAction,
23
22
  } from '../types'
24
23
  import { findBlueprintNote, findBlueprintPointer } from '../userCreated'
25
24
  import { EyeIcon } from './EyeIcon'
26
25
  import { beginKeyboardIsolation, shouldIgnoreShortcut } from '../keyboard'
26
+ import type { DevTargetsState } from '../devTargets'
27
27
 
28
28
  function reviewTitle(status: AgentIntentStatus) {
29
29
  if (status === 'blueprint_ask') return 'Setup blueprint'
@@ -43,14 +43,270 @@ function sessionLabel(intent: AgentIntent) {
43
43
  return intent.name?.trim() || intent.feature?.trim() || ''
44
44
  }
45
45
 
46
- function sessionTabLabel(intent: AgentIntent, sessions: AgentIntent[]) {
47
- const title = sessionLabel(intent) || reviewTitle(intent.status)
48
- const same = sessions.filter(
49
- (item) => (sessionLabel(item) || reviewTitle(item.status)) === title,
46
+ function sessionColorName(intent: AgentIntent) {
47
+ return intent.colorName?.trim() || ''
48
+ }
49
+
50
+ function sessionSlashCommand(intent: Pick<AgentIntent, 'color'>) {
51
+ const color = intent.color?.trim()
52
+ if (!color || color === 'blue') return null
53
+ return `/${color}`
54
+ }
55
+
56
+ function ColorConnectHint({
57
+ colorCommand,
58
+ queued,
59
+ }: {
60
+ colorCommand?: string | null
61
+ queued?: boolean
62
+ }) {
63
+ return (
64
+ <p>
65
+ {queued && colorCommand ? (
66
+ <>
67
+ Type <kbd>{colorCommand}</kbd> in a Cursor chat to skip the queue and
68
+ connect here.{' '}
69
+ </>
70
+ ) : null}
71
+ Use <kbd>/coral</kbd>, <kbd>/amber</kbd>, <kbd>/lime</kbd>,{' '}
72
+ <kbd>/orange</kbd>, or <kbd>/violet</kbd> to connect to that color
73
+ {!queued && colorCommand ? (
74
+ <>
75
+ {' '}
76
+ — this session is <kbd>{colorCommand}</kbd>
77
+ </>
78
+ ) : null}
79
+ . Aliases: <kbd>/red</kbd>, <kbd>/yellow</kbd>, <kbd>/green</kbd>,{' '}
80
+ <kbd>/purple</kbd>. <kbd>/blue</kbd> is the global blueprint, not a chat.
81
+ </p>
82
+ )
83
+ }
84
+
85
+ function sessionDisplayName(intent: AgentIntent) {
86
+ return sessionLabel(intent) || sessionColorName(intent)
87
+ }
88
+
89
+ function SessionSwatch({
90
+ colorHex,
91
+ className = 'hud-session-swatch',
92
+ }: {
93
+ colorHex?: string | null
94
+ className?: string
95
+ }) {
96
+ return (
97
+ <span
98
+ className={className}
99
+ aria-hidden="true"
100
+ style={
101
+ colorHex
102
+ ? ({ '--session-color': colorHex } as CSSProperties)
103
+ : undefined
104
+ }
105
+ />
106
+ )
107
+ }
108
+
109
+ type BlueprintColorOption = BlueprintOption & { pointers: BlueprintPointer[] }
110
+
111
+ function optionPointed(
112
+ option: BlueprintColorOption,
113
+ target: { kind: BlueprintPointerKind; path: string; name?: string },
114
+ ) {
115
+ return findBlueprintPointer(
116
+ option.pointers,
117
+ target.kind,
118
+ target.path,
119
+ target.name,
120
+ )
121
+ }
122
+
123
+ function placeAnchoredMenu(
124
+ trigger: DOMRect,
125
+ menuHeight: number,
126
+ width: number,
127
+ alignEnd: boolean,
128
+ ) {
129
+ const gap = 4
130
+ const pad = 8
131
+ const left = Math.min(
132
+ Math.max(pad, alignEnd ? trigger.right - width : trigger.left),
133
+ window.innerWidth - width - pad,
134
+ )
135
+ const spaceBelow = window.innerHeight - trigger.bottom - pad
136
+ const spaceAbove = trigger.top - pad
137
+ const openAbove =
138
+ menuHeight > 0 &&
139
+ menuHeight + gap > spaceBelow &&
140
+ spaceAbove > spaceBelow
141
+ const maxHeight = Math.max(0, (openAbove ? spaceAbove : spaceBelow) - gap)
142
+ const usedHeight = menuHeight > 0 ? Math.min(menuHeight, maxHeight) : 0
143
+ const top = openAbove
144
+ ? Math.max(pad, trigger.top - usedHeight - gap)
145
+ : trigger.bottom + gap
146
+ return { top, left, width, maxHeight }
147
+ }
148
+
149
+ function PointColorControl({
150
+ target,
151
+ colorPointers = [],
152
+ currentColorId,
153
+ onToggle,
154
+ compact = false,
155
+ idleLabel,
156
+ pointedLabel,
157
+ disabled = false,
158
+ }: {
159
+ target: { kind: BlueprintPointerKind; path: string; name?: string }
160
+ colorPointers?: BlueprintColorOption[]
161
+ currentColorId?: string | null
162
+ onToggle: (color?: string) => void
163
+ compact?: boolean
164
+ idleLabel: string
165
+ pointedLabel: string
166
+ disabled?: boolean
167
+ }) {
168
+ const triggerRef = useRef<HTMLDivElement>(null)
169
+ const menuRef = useRef<HTMLDivElement>(null)
170
+ const [open, setOpen] = useState(false)
171
+ const current =
172
+ colorPointers.find((option) => option.id === (currentColorId ?? 'global')) ??
173
+ colorPointers[0]
174
+ const currentHex = current?.hex ?? GLOBAL_BLUEPRINT_COLOR.hex
175
+ const pointed = current ? optionPointed(current, target) : false
176
+ const showMenu = colorPointers.length > 0
177
+
178
+ useLayoutEffect(() => {
179
+ if (!open) return
180
+ const place = () => {
181
+ const trigger = triggerRef.current
182
+ const menu = menuRef.current
183
+ if (!trigger || !menu) return
184
+ const rect = trigger.getBoundingClientRect()
185
+ const width = compact ? 168 : Math.max(rect.width, 168)
186
+ menu.style.maxHeight = 'none'
187
+ const { top, left, maxHeight } = placeAnchoredMenu(
188
+ rect,
189
+ menu.offsetHeight,
190
+ width,
191
+ compact,
192
+ )
193
+ menu.style.top = `${top}px`
194
+ menu.style.left = `${left}px`
195
+ menu.style.width = `${width}px`
196
+ menu.style.maxHeight = `${maxHeight}px`
197
+ menu.style.visibility = 'visible'
198
+ }
199
+ place()
200
+ window.addEventListener('resize', place)
201
+ window.addEventListener('scroll', place, true)
202
+ return () => {
203
+ window.removeEventListener('resize', place)
204
+ window.removeEventListener('scroll', place, true)
205
+ }
206
+ }, [compact, open, colorPointers.length])
207
+
208
+ useEffect(() => {
209
+ if (!open) return
210
+ const onKey = (event: KeyboardEvent) => {
211
+ if (event.code !== 'Escape') return
212
+ if (shouldIgnoreShortcut(event)) return
213
+ event.preventDefault()
214
+ setOpen(false)
215
+ }
216
+ const onPointerDown = (event: PointerEvent) => {
217
+ const node = event.target
218
+ if (
219
+ node instanceof Element &&
220
+ (triggerRef.current?.contains(node) ||
221
+ node.closest('.hud-point-dropdown'))
222
+ ) {
223
+ return
224
+ }
225
+ setOpen(false)
226
+ }
227
+ window.addEventListener('keydown', onKey, true)
228
+ window.addEventListener('pointerdown', onPointerDown, true)
229
+ return () => {
230
+ window.removeEventListener('keydown', onKey, true)
231
+ window.removeEventListener('pointerdown', onPointerDown, true)
232
+ }
233
+ }, [open])
234
+
235
+ return (
236
+ <div
237
+ ref={triggerRef}
238
+ className={
239
+ compact ? 'hud-point-menu hud-point-menu-compact' : 'hud-point-menu'
240
+ }
241
+ >
242
+ <button
243
+ className={
244
+ compact ? 'hud-item-point' : 'hud-button hud-inspect hud-point'
245
+ }
246
+ type="button"
247
+ data-pointed={pointed ? 'true' : 'false'}
248
+ data-active={open}
249
+ aria-pressed={pointed}
250
+ aria-haspopup={showMenu ? 'menu' : undefined}
251
+ aria-expanded={showMenu ? open : undefined}
252
+ aria-label={compact ? (pointed ? pointedLabel : idleLabel) : undefined}
253
+ disabled={disabled}
254
+ style={
255
+ {
256
+ '--session-color': currentHex,
257
+ } as CSSProperties
258
+ }
259
+ onClick={() => {
260
+ if (!showMenu || disabled) return
261
+ setOpen((currentOpen) => !currentOpen)
262
+ }}
263
+ >
264
+ <EyeIcon size={compact ? 13 : 15} />
265
+ {compact ? null : pointed ? pointedLabel : idleLabel}
266
+ </button>
267
+ {open &&
268
+ showMenu &&
269
+ createPortal(
270
+ <div ref={menuRef} className="hud-point-dropdown" role="menu">
271
+ <button
272
+ type="button"
273
+ role="menuitem"
274
+ className="hud-point-dropdown-cancel"
275
+ onClick={() => setOpen(false)}
276
+ >
277
+ Cancel
278
+ </button>
279
+ {colorPointers.map((option) => {
280
+ const selected = optionPointed(option, target)
281
+ return (
282
+ <button
283
+ key={option.id}
284
+ type="button"
285
+ role="menuitem"
286
+ data-pointed={selected ? 'true' : 'false'}
287
+ aria-pressed={selected}
288
+ style={
289
+ {
290
+ '--session-color': option.hex,
291
+ } as CSSProperties
292
+ }
293
+ onClick={() => {
294
+ onToggle(option.id)
295
+ setOpen(false)
296
+ }}
297
+ >
298
+ <EyeIcon size={15} />
299
+ <span>
300
+ {option.kind === 'global' ? 'Global' : option.name}
301
+ </span>
302
+ </button>
303
+ )
304
+ })}
305
+ </div>,
306
+ document.body,
307
+ )}
308
+ </div>
50
309
  )
51
- if (same.length < 2) return title
52
- const id = intent.sessionId ?? ''
53
- return `${title} · ${id.slice(-4)}`
54
310
  }
55
311
 
56
312
  function fileBase(id: string) {
@@ -193,9 +449,15 @@ function MutationFold({
193
449
  function AddIntentRow({
194
450
  placeholder,
195
451
  onAdd,
452
+ pickLabel,
453
+ pickActive = false,
454
+ onTogglePick,
196
455
  }: {
197
456
  placeholder: string
198
457
  onAdd: (value: string) => boolean
458
+ pickLabel?: string
459
+ pickActive?: boolean
460
+ onTogglePick?: () => void
199
461
  }) {
200
462
  const [value, setValue] = useState('')
201
463
  return (
@@ -218,6 +480,17 @@ function AddIntentRow({
218
480
  <button className="hud-button" type="submit">
219
481
  Add
220
482
  </button>
483
+ {onTogglePick && pickLabel && (
484
+ <button
485
+ className="hud-button"
486
+ type="button"
487
+ data-active={pickActive ? 'true' : undefined}
488
+ aria-pressed={pickActive}
489
+ onClick={onTogglePick}
490
+ >
491
+ {pickLabel}
492
+ </button>
493
+ )}
221
494
  </form>
222
495
  )
223
496
  }
@@ -315,54 +588,71 @@ function BlueprintSymbolRow({
315
588
  className,
316
589
  hasNote,
317
590
  noteOpen,
318
- pointed,
319
591
  canEdit,
320
592
  canRemove,
321
593
  onRemove,
322
594
  onOpenNote,
595
+ onExplain,
596
+ pointerTarget,
597
+ colorPointers,
598
+ currentColorId,
323
599
  onTogglePoint,
324
600
  }: {
325
601
  name: string
326
602
  className?: string
327
603
  hasNote: boolean
328
604
  noteOpen?: boolean
329
- pointed?: boolean
330
605
  canEdit: boolean
331
606
  canRemove?: boolean
332
607
  onRemove?: () => void
333
608
  onOpenNote: () => void
334
- onTogglePoint?: () => void
609
+ onExplain?: () => void
610
+ pointerTarget?: {
611
+ kind: BlueprintPointerKind
612
+ path: string
613
+ name: string
614
+ }
615
+ colorPointers?: BlueprintColorOption[]
616
+ currentColorId?: string | null
617
+ onTogglePoint?: (color?: string) => void
335
618
  }) {
619
+ const showActions = Boolean(onExplain) || canEdit
336
620
  return (
337
621
  <li>
338
622
  <span className={className}>{name}</span>
339
- {canEdit && (
623
+ {showActions && (
340
624
  <div className="hud-item-actions">
341
- {onTogglePoint && (
625
+ {onExplain ? (
626
+ <ExplainButton
627
+ compact
628
+ label={`Explain ${name}`}
629
+ onClick={onExplain}
630
+ />
631
+ ) : null}
632
+ {canEdit && onTogglePoint && pointerTarget && (
633
+ <PointColorControl
634
+ compact
635
+ target={pointerTarget}
636
+ colorPointers={colorPointers}
637
+ currentColorId={currentColorId}
638
+ idleLabel={`Point to ${name}`}
639
+ pointedLabel={`Stop pointing to ${name}`}
640
+ onToggle={onTogglePoint}
641
+ />
642
+ )}
643
+ {canEdit && (
342
644
  <button
343
- className="hud-item-point"
645
+ className="hud-item-note"
344
646
  type="button"
345
- data-pointed={pointed ? 'true' : 'false'}
346
- aria-label={
347
- pointed ? `Stop pointing to ${name}` : `Point to ${name}`
348
- }
349
- aria-pressed={Boolean(pointed)}
350
- onClick={onTogglePoint}
647
+ data-has-note={hasNote ? 'true' : 'false'}
648
+ data-open={noteOpen ? 'true' : 'false'}
649
+ aria-label={`Edit note for ${name}`}
650
+ onClick={onOpenNote}
351
651
  >
352
- <EyeIcon size={13} />
652
+ Note
353
653
  </button>
354
654
  )}
355
- <button
356
- className="hud-item-note"
357
- type="button"
358
- data-has-note={hasNote ? 'true' : 'false'}
359
- data-open={noteOpen ? 'true' : 'false'}
360
- aria-label={`Edit note for ${name}`}
361
- onClick={onOpenNote}
362
- >
363
- Note
364
- </button>
365
- {canRemove && (
655
+ {canEdit && canRemove && (
366
656
  <button
367
657
  className="hud-item-remove"
368
658
  type="button"
@@ -391,6 +681,35 @@ function AttachStateBadge({ attached }: { attached: boolean }) {
391
681
  )
392
682
  }
393
683
 
684
+ function ExplainButton({
685
+ label,
686
+ onClick,
687
+ compact = false,
688
+ }: {
689
+ label: string
690
+ onClick: () => void
691
+ compact?: boolean
692
+ }) {
693
+ return (
694
+ <button
695
+ className={
696
+ compact
697
+ ? 'hud-explain-button hud-explain-inline'
698
+ : 'hud-explain-button'
699
+ }
700
+ type="button"
701
+ aria-label={label}
702
+ title={label}
703
+ onClick={(event) => {
704
+ event.stopPropagation()
705
+ onClick()
706
+ }}
707
+ >
708
+ ?
709
+ </button>
710
+ )
711
+ }
712
+
394
713
  function PanelChrome({
395
714
  title,
396
715
  subtitle,
@@ -398,8 +717,8 @@ function PanelChrome({
398
717
  minimized = false,
399
718
  onMinimize,
400
719
  onClose,
401
- closeLabel = 'Close',
402
- closeReject = false,
720
+ onExplain,
721
+ explainLabel,
403
722
  }: {
404
723
  title: ReactNode
405
724
  subtitle?: ReactNode
@@ -407,14 +726,22 @@ function PanelChrome({
407
726
  minimized?: boolean
408
727
  onMinimize?: () => void
409
728
  onClose?: () => void
410
- closeLabel?: string
411
- closeReject?: boolean
729
+ onExplain?: () => void
730
+ explainLabel?: string
412
731
  }) {
413
732
  return (
414
733
  <div className="hud-panel-chrome">
415
734
  <div className="hud-panel-chrome-heading">
416
735
  <div className="hud-panel-chrome-title-row">
417
- <div className="hud-panel-chrome-title">{title}</div>
736
+ <div className="hud-panel-chrome-title">
737
+ {title}
738
+ {onExplain ? (
739
+ <ExplainButton
740
+ label={explainLabel ?? 'Explain'}
741
+ onClick={onExplain}
742
+ />
743
+ ) : null}
744
+ </div>
418
745
  {badge}
419
746
  </div>
420
747
  {subtitle ? (
@@ -434,13 +761,9 @@ function PanelChrome({
434
761
  )}
435
762
  {onClose && (
436
763
  <button
437
- className={
438
- closeReject
439
- ? 'hud-button hud-icon-button hud-panel-control hud-button-reject'
440
- : 'hud-button hud-icon-button hud-panel-control'
441
- }
764
+ className="hud-button hud-icon-button hud-panel-control"
442
765
  type="button"
443
- aria-label={closeLabel}
766
+ aria-label="Close"
444
767
  onClick={onClose}
445
768
  >
446
769
  ×
@@ -451,142 +774,9 @@ function PanelChrome({
451
774
  )
452
775
  }
453
776
 
454
- function InitialInstructionField({
455
- value,
456
- onChange,
457
- }: {
458
- value: string
459
- onChange: (value: string) => void
460
- }) {
461
- return (
462
- <label className="hud-instruction">
463
- <textarea
464
- value={value}
465
- maxLength={4000}
466
- rows={4}
467
- placeholder="What should the LLM build?"
468
- onChange={(event) => onChange(event.target.value)}
469
- onKeyDown={(event) => event.stopPropagation()}
470
- />
471
- </label>
472
- )
473
- }
474
-
475
- function formatFileSize(bytes: number) {
476
- if (bytes < 1024) return `${bytes} B`
477
- if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`
478
- return `${(bytes / (1024 * 1024)).toFixed(1)} MB`
479
- }
480
-
481
- async function fileToBase64(file: File) {
482
- const bytes = new Uint8Array(await file.arrayBuffer())
483
- const chunk = 0x8000
484
- let binary = ''
485
- for (let offset = 0; offset < bytes.length; offset += chunk) {
486
- binary += String.fromCharCode(...bytes.subarray(offset, offset + chunk))
487
- }
488
- return btoa(binary)
489
- }
490
-
491
- type ContextFileInfo = {
492
- id: string
493
- name: string
494
- mimeType: string
495
- size: number
496
- }
497
-
498
- function ContextFileDrop({
499
- files,
500
- busy,
501
- error,
502
- onAdd,
503
- onRemove,
504
- }: {
505
- files: ContextFileInfo[]
506
- busy: boolean
507
- error: string | null
508
- onAdd: (files: File[]) => void
509
- onRemove: (fileId: string) => void
510
- }) {
511
- const inputRef = useRef<HTMLInputElement>(null)
512
- const [over, setOver] = useState(false)
513
-
514
- const takeFiles = (list: FileList | File[] | null) => {
515
- if (!list || busy) return
516
- const next = [...list].filter((file) => file.size > 0)
517
- if (next.length > 0) onAdd(next)
518
- }
519
-
520
- return (
521
- <div className="hud-context">
522
- <button
523
- className="hud-context-drop"
524
- type="button"
525
- data-over={over}
526
- data-busy={busy}
527
- disabled={busy}
528
- aria-label="Attach files for the LLM"
529
- onDragEnter={(event) => {
530
- event.preventDefault()
531
- if (event.dataTransfer.types.includes('Files')) setOver(true)
532
- }}
533
- onDragOver={(event) => {
534
- event.preventDefault()
535
- event.dataTransfer.dropEffect = 'copy'
536
- }}
537
- onDragLeave={(event) => {
538
- if (event.currentTarget.contains(event.relatedTarget as Node)) return
539
- setOver(false)
540
- }}
541
- onDrop={(event) => {
542
- event.preventDefault()
543
- event.stopPropagation()
544
- setOver(false)
545
- takeFiles(event.dataTransfer.files)
546
- }}
547
- onKeyDown={(event) => event.stopPropagation()}
548
- onClick={() => inputRef.current?.click()}
549
- >
550
- <input
551
- ref={inputRef}
552
- type="file"
553
- multiple
554
- hidden
555
- onChange={(event) => {
556
- takeFiles(event.target.files)
557
- event.target.value = ''
558
- }}
559
- />
560
- {busy ? 'Attaching…' : 'Drop files or click to attach'}
561
- </button>
562
- {files.length > 0 && (
563
- <ul className="hud-context-list">
564
- {files.map((file) => (
565
- <li key={file.id}>
566
- <span className="hud-context-name" title={file.name}>
567
- {file.name}
568
- </span>
569
- <span className="hud-context-size">{formatFileSize(file.size)}</span>
570
- <button
571
- className="hud-item-remove"
572
- type="button"
573
- aria-label={`Remove ${file.name}`}
574
- disabled={busy}
575
- onClick={() => onRemove(file.id)}
576
- >
577
- ×
578
- </button>
579
- </li>
580
- ))}
581
- </ul>
582
- )}
583
- {error ? <p className="hud-context-error">{error}</p> : null}
584
- </div>
585
- )
586
- }
587
-
588
777
  function blueprintIsDefined(intent: AgentIntent) {
589
778
  return (
779
+ Boolean(intent.localBlueprintEnabled) ||
590
780
  (intent.userCreatedBlocks?.length ?? 0) > 0 ||
591
781
  (intent.userCreatedIslands?.length ?? 0) > 0 ||
592
782
  (intent.blueprintFunctions?.length ?? 0) > 0 ||
@@ -598,56 +788,18 @@ function blueprintIsDefined(intent: AgentIntent) {
598
788
  }
599
789
 
600
790
  function HandshakeSetup({
601
- instruction,
602
- onInstructionChange,
603
- contextFiles,
604
- contextBusy,
605
- contextError,
606
- onAddContextFiles,
607
- onRemoveContextFile,
608
791
  blueprintDefined,
609
792
  awaitingAttach,
610
793
  nextAttachLabel,
794
+ colorCommand,
611
795
  }: {
612
- instruction: string
613
- onInstructionChange: (value: string) => void
614
- contextFiles: ContextFileInfo[]
615
- contextBusy: boolean
616
- contextError: string | null
617
- onAddContextFiles: (files: File[]) => void
618
- onRemoveContextFile: (fileId: string) => void
619
796
  blueprintDefined: boolean
620
797
  awaitingAttach: boolean
621
798
  nextAttachLabel: string | null
799
+ colorCommand?: string | null
622
800
  }) {
623
801
  return (
624
802
  <div className="hud-setup">
625
- <section
626
- className="hud-setup-section"
627
- onDragOver={(event) => {
628
- if (event.dataTransfer.types.includes('Files')) event.preventDefault()
629
- }}
630
- onDrop={(event) => {
631
- event.preventDefault()
632
- const dropped = [...(event.dataTransfer.files ?? [])].filter(
633
- (file) => file.size > 0,
634
- )
635
- if (dropped.length > 0) onAddContextFiles(dropped)
636
- }}
637
- >
638
- <h2 className="hud-setup-heading">Instructions</h2>
639
- <InitialInstructionField
640
- value={instruction}
641
- onChange={onInstructionChange}
642
- />
643
- <ContextFileDrop
644
- files={contextFiles}
645
- busy={contextBusy}
646
- error={contextError}
647
- onAdd={onAddContextFiles}
648
- onRemove={onRemoveContextFile}
649
- />
650
- </section>
651
803
  <section className="hud-setup-section">
652
804
  <h2 className="hud-setup-heading">
653
805
  Blueprint
@@ -659,27 +811,32 @@ function HandshakeSetup({
659
811
  </span>
660
812
  </h2>
661
813
  <p>
662
- Press <kbd>Space</kbd> for a file and <kbd>B</kbd> for an island.
663
- On the map, right-click to add a file or folder. Open a file's info
664
- panel to add functions, vars, and notes (instructions or pseudo
665
- code). The blueprint is shared across sessions.
814
+ Right-click to create files and folders. Open a file's
815
+ info panel to add functions, vars, and notes (instructions or
816
+ pseudo code). Every chat receives the global (blue) blueprint plus
817
+ this session's color.
666
818
  </p>
667
819
  </section>
668
820
  <section className="hud-setup-section">
669
821
  <h2 className="hud-setup-heading">Start</h2>
670
822
  {awaitingAttach && nextAttachLabel ? (
671
- <p>
672
- <kbd>/inbase</kbd> attaches {nextAttachLabel} first. This session
673
- stays in the queue.
674
- </p>
823
+ <>
824
+ <p>
825
+ The next Cursor chat connects to {nextAttachLabel} first. This
826
+ session stays in the queue.
827
+ </p>
828
+ <ColorConnectHint colorCommand={colorCommand} queued />
829
+ </>
675
830
  ) : awaitingAttach ? (
676
- <p>
677
- Run <kbd>/inbase</kbd> in a Cursor chat to connect and start.
678
- </p>
831
+ <>
832
+ <p>
833
+ Open a Cursor chat to connect and start. A regular chat takes
834
+ the next empty slot.
835
+ </p>
836
+ <ColorConnectHint colorCommand={colorCommand} />
837
+ </>
679
838
  ) : (
680
- <p>
681
- This window is attached. Starting from <kbd>/inbase</kbd>…
682
- </p>
839
+ <p>This window is attached. Starting from the Cursor chat…</p>
683
840
  )}
684
841
  </section>
685
842
  </div>
@@ -701,7 +858,16 @@ function sessionLiveStatus(intent: AgentIntent) {
701
858
  return { text: 'LLM wait timed out', busy: false }
702
859
  }
703
860
  if (intent.awaitingAttach) {
704
- return { text: 'Waiting for /inbase in Cursor', busy: false }
861
+ return { text: 'Waiting for a Cursor chat', busy: false }
862
+ }
863
+ if (intent.llmIdle) {
864
+ return { text: 'LLM disconnected', busy: false }
865
+ }
866
+ if (intent.pendingExplain) {
867
+ return { text: 'Starting an explanation…', busy: true }
868
+ }
869
+ if (kind === 'explain' && !intent.listening) {
870
+ return { text: 'LLM is explaining this proposal on the map', busy: true }
705
871
  }
706
872
  if (intent.status === 'pending') {
707
873
  return intent.listening
@@ -743,9 +909,6 @@ function sessionLiveStatus(intent: AgentIntent) {
743
909
  ) {
744
910
  return { text: 'LLM attached', busy: true }
745
911
  }
746
- if (intent.llmIdle) {
747
- return { text: 'LLM is idle', busy: false }
748
- }
749
912
  return { text: 'LLM connected', busy: true }
750
913
  }
751
914
 
@@ -753,12 +916,16 @@ function LiveStatus({
753
916
  intent,
754
917
  showStop = false,
755
918
  onStop,
919
+ startingExplain = false,
756
920
  }: {
757
921
  intent: AgentIntent
758
922
  showStop?: boolean
759
923
  onStop?: () => void
924
+ startingExplain?: boolean
760
925
  }) {
761
- const status = sessionLiveStatus(intent)
926
+ const status = startingExplain && !intent.pendingExplain
927
+ ? { text: 'Starting an explanation…', busy: true }
928
+ : sessionLiveStatus(intent)
762
929
  const [flash, setFlash] = useState(false)
763
930
  const lastAt = intent.lastAck?.at
764
931
 
@@ -786,6 +953,12 @@ function LiveStatus({
786
953
  )
787
954
  }
788
955
 
956
+ function PlaceFilesHint() {
957
+ return (
958
+ <p className="hud-place-hint">Right-click to create files and folders.</p>
959
+ )
960
+ }
961
+
789
962
  type SessionPanelProps = {
790
963
  intent: AgentIntent
791
964
  focused: boolean
@@ -811,14 +984,7 @@ function SessionPanel({
811
984
  }: SessionPanelProps) {
812
985
  const [minimized, setMinimized] = useState(false)
813
986
  const [instruction, setInstruction] = useState('')
814
- const [initialInstruction, setInitialInstruction] = useState(
815
- () => intent.initialInstruction ?? '',
816
- )
817
- const [contextFiles, setContextFiles] = useState<ContextFileInfo[]>(
818
- () => intent.contextFiles ?? [],
819
- )
820
- const [contextBusy, setContextBusy] = useState(false)
821
- const [contextError, setContextError] = useState<string | null>(null)
987
+ const [startingExplain, setStartingExplain] = useState(false)
822
988
  const sessionId = intent.sessionId
823
989
  const latestEntry = intent.isActiveDiff ? intent.chain.at(-1) : null
824
990
  const pending =
@@ -863,8 +1029,10 @@ function SessionPanel({
863
1029
  planReady && !working && proposalStep === null
864
1030
  ? (intent.steps.find((step) => !acceptedSteps.has(step.index)) ?? null)
865
1031
  : null
866
- const canRunNext = stepByStep && Boolean(invokeStep)
867
- const canAcceptProposal = proposalStep !== null
1032
+ const llmDisconnected =
1033
+ Boolean(intent.llmIdle) && intent.awaitingAttach === false
1034
+ const canRunNext = stepByStep && Boolean(invokeStep) && !llmDisconnected
1035
+ const canAcceptProposal = proposalStep !== null && !llmDisconnected
868
1036
  const lastStep =
869
1037
  typeof proposalStep === 'number' &&
870
1038
  intent.steps.length > 0 &&
@@ -877,75 +1045,29 @@ function SessionPanel({
877
1045
 
878
1046
  useEffect(() => {
879
1047
  setInstruction('')
880
- }, [intent.diffId])
881
-
882
- useEffect(() => {
883
- setInitialInstruction(intent.initialInstruction ?? '')
884
- setContextFiles(intent.contextFiles ?? [])
885
- setContextError(null)
886
- }, [sessionId])
1048
+ setStartingExplain(false)
1049
+ }, [intent.diffId, sessionId])
887
1050
 
888
1051
  useEffect(() => {
889
- if (contextBusy) return
890
- setContextFiles(intent.contextFiles ?? [])
891
- }, [contextBusy, intent.contextFiles])
1052
+ if (intent.pendingExplain) setStartingExplain(false)
1053
+ }, [intent.pendingExplain])
892
1054
 
893
1055
  if (!sessionId || !isReviewingIntent(intent.status)) return null
894
1056
 
895
- const updateInitialInstruction = (value: string) => {
896
- setInitialInstruction(value)
897
- persistInitialInstruction(sessionId, value)
898
- }
899
-
900
- const addContextFiles = (files: File[]) => {
901
- setContextBusy(true)
902
- setContextError(null)
903
- void Promise.all(
904
- files.map(async (file) => ({
905
- name: file.name,
906
- mimeType: file.type || 'application/octet-stream',
907
- contentBase64: await fileToBase64(file),
908
- })),
909
- )
910
- .then((payload) => persistAddContextFiles(sessionId, payload))
911
- .then((next) => {
912
- setContextFiles(next.contextFiles ?? [])
913
- })
914
- .catch((caught) => {
915
- setContextError(
916
- caught instanceof Error ? caught.message : 'Could not attach files',
917
- )
918
- })
919
- .finally(() => setContextBusy(false))
920
- }
921
-
922
- const removeContextFile = (fileId: string) => {
923
- setContextBusy(true)
924
- setContextError(null)
925
- void persistRemoveContextFile(sessionId, fileId)
926
- .then((next) => {
927
- setContextFiles(next.contextFiles ?? [])
928
- })
929
- .catch((caught) => {
930
- setContextError(
931
- caught instanceof Error ? caught.message : 'Could not remove file',
932
- )
933
- })
934
- .finally(() => setContextBusy(false))
935
- }
936
-
937
- const showInitialInstruction =
1057
+ const handshakeSetup =
938
1058
  Boolean(intent.awaitingAttach) &&
939
1059
  (askingBlueprint || sendingBlueprint || preparing)
940
- const handshakeSetup = showInitialInstruction
941
1060
  const llmConnected = intent.awaitingAttach === false
942
1061
  const showConnectedProgress =
943
- llmConnected && (askingBlueprint || sendingBlueprint || preparing)
1062
+ llmConnected && !llmDisconnected && (askingBlueprint || sendingBlueprint || preparing)
1063
+ const showPlaceHint =
1064
+ canPlace && !intent.working && !askingBlueprint && !sendingBlueprint
1065
+ const liveHasStop = showConnectedProgress || working || llmDisconnected
944
1066
  const queuedBehind =
945
1067
  intent.awaitingAttach &&
946
1068
  nextAttachSession &&
947
1069
  nextAttachSession.sessionId !== sessionId
948
- ? sessionLabel(nextAttachSession) || 'an earlier session'
1070
+ ? sessionDisplayName(nextAttachSession) || 'an earlier session'
949
1071
  : null
950
1072
 
951
1073
  const act = (
@@ -962,50 +1084,64 @@ function SessionPanel({
962
1084
  }
963
1085
  data-minimized={minimized}
964
1086
  data-focused={focused}
965
- data-attached={llmConnected}
1087
+ data-attached={llmConnected && !llmDisconnected}
966
1088
  onPointerDown={onFocus}
967
1089
  >
968
1090
  <PanelChrome
969
1091
  title={
970
- sessionLabel(intent) ||
971
- (showConnectedProgress ? 'LLM connected' : reviewTitle(intent.status))
1092
+ <>
1093
+ <SessionSwatch colorHex={intent.colorHex} />
1094
+ <span className="hud-panel-chrome-title-text">
1095
+ {sessionDisplayName(intent) ||
1096
+ (showConnectedProgress
1097
+ ? 'LLM connected'
1098
+ : reviewTitle(intent.status))}
1099
+ </span>
1100
+ </>
972
1101
  }
973
1102
  subtitle={
974
1103
  sessionLabel(intent)
975
1104
  ? showConnectedProgress
976
1105
  ? 'LLM connected'
977
1106
  : reviewTitle(intent.status)
978
- : undefined
1107
+ : sessionColorName(intent)
1108
+ ? showConnectedProgress
1109
+ ? 'LLM connected'
1110
+ : reviewTitle(intent.status)
1111
+ : undefined
979
1112
  }
980
- badge={<AttachStateBadge attached={llmConnected} />}
1113
+ badge={<AttachStateBadge attached={llmConnected && !llmDisconnected} />}
981
1114
  minimized={minimized}
982
1115
  onMinimize={() => setMinimized((current) => !current)}
983
- onClose={() => act('stop')}
984
- closeLabel="Stop session"
985
- closeReject
986
1116
  />
987
1117
  {!minimized && (
988
1118
  <>
989
1119
  {!intent.awaitingAttach && (
990
- <LiveStatus
991
- intent={intent}
992
- showStop={showConnectedProgress || working}
993
- onStop={() => act('stop')}
994
- />
1120
+ <>
1121
+ {showPlaceHint && !planReady && !pending && <PlaceFilesHint />}
1122
+ <LiveStatus
1123
+ intent={intent}
1124
+ showStop={liveHasStop}
1125
+ startingExplain={startingExplain}
1126
+ onStop={() => act('stop')}
1127
+ />
1128
+ </>
995
1129
  )}
996
- <label className="hud-mode-switch">
997
- <span>Step by step</span>
998
- <button
999
- className="hud-switch"
1000
- type="button"
1001
- role="switch"
1002
- aria-checked={stepByStep}
1003
- aria-label="Step by step"
1004
- onKeyDown={(event) => event.stopPropagation()}
1005
- onClick={() => act('set_step_by_step', { stepByStep: !stepByStep })}
1006
- />
1007
- </label>
1008
- {!stepByStep && (
1130
+ {!llmDisconnected && (
1131
+ <label className="hud-mode-switch">
1132
+ <span>Step by step</span>
1133
+ <button
1134
+ className="hud-switch"
1135
+ type="button"
1136
+ role="switch"
1137
+ aria-checked={stepByStep}
1138
+ aria-label="Step by step"
1139
+ onKeyDown={(event) => event.stopPropagation()}
1140
+ onClick={() => act('set_step_by_step', { stepByStep: !stepByStep })}
1141
+ />
1142
+ </label>
1143
+ )}
1144
+ {!llmDisconnected && !stepByStep && (
1009
1145
  <p className="hud-mode-hint">
1010
1146
  LLM implements the full plan. You can still walk the diffs, then
1011
1147
  Accept proposal.
@@ -1013,41 +1149,49 @@ function SessionPanel({
1013
1149
  )}
1014
1150
  {handshakeSetup ? (
1015
1151
  <HandshakeSetup
1016
- instruction={initialInstruction}
1017
- onInstructionChange={updateInitialInstruction}
1018
- contextFiles={contextFiles}
1019
- contextBusy={contextBusy}
1020
- contextError={contextError}
1021
- onAddContextFiles={addContextFiles}
1022
- onRemoveContextFile={removeContextFile}
1023
1152
  blueprintDefined={blueprintIsDefined(intent)}
1024
1153
  awaitingAttach={Boolean(intent.awaitingAttach)}
1025
1154
  nextAttachLabel={queuedBehind}
1155
+ colorCommand={sessionSlashCommand(intent)}
1026
1156
  />
1027
1157
  ) : intent.awaitingAttach ? (
1028
1158
  queuedBehind ? (
1029
- <p className="hud-mode-hint">
1030
- <kbd>/inbase</kbd> attaches {queuedBehind} first. This session
1031
- stays in the queue.
1032
- </p>
1159
+ <div className="hud-mode-hint">
1160
+ <p>
1161
+ The next Cursor chat connects to {queuedBehind} first. This
1162
+ session stays in the queue.
1163
+ </p>
1164
+ <ColorConnectHint
1165
+ colorCommand={sessionSlashCommand(intent)}
1166
+ queued
1167
+ />
1168
+ </div>
1033
1169
  ) : (
1034
- <p className="hud-mode-hint">
1035
- No LLM is attached. Open a Cursor chat and run{' '}
1036
- <kbd>/inbase</kbd>. It connects to the next waiting session.
1037
- </p>
1170
+ <div className="hud-mode-hint">
1171
+ <p>
1172
+ No LLM is attached. Open a Cursor chat — it connects to the
1173
+ next waiting session.
1174
+ </p>
1175
+ <ColorConnectHint colorCommand={sessionSlashCommand(intent)} />
1176
+ </div>
1038
1177
  )
1039
1178
  ) : null}
1179
+ {llmDisconnected ? (
1180
+ <p className="hud-mode-hint">
1181
+ This chat is no longer connected. The session will reset.
1182
+ </p>
1183
+ ) : null}
1040
1184
  {intent.feature &&
1041
1185
  !handshakeSetup &&
1042
1186
  intent.feature.trim() !== sessionLabel(intent) && (
1043
1187
  <p className="hud-feature">{intent.feature}</p>
1044
1188
  )}
1045
- {askingBlueprint && !handshakeSetup && !showConnectedProgress ? (
1189
+ {askingBlueprint && !handshakeSetup && !showConnectedProgress && !llmDisconnected ? (
1046
1190
  <>
1047
1191
  <p>
1048
- A shared blueprint is already available on the map. Send this
1049
- session to the LLM, or skip and let it continue with the current
1050
- layout.
1192
+ This chat receives the global (blue) blueprint and this
1193
+ session's color. Send it to the LLM, or skip and let it
1194
+ continue with the current layout.
1051
1195
  </p>
1052
1196
  <div className="hud-decide">
1053
1197
  <button
@@ -1076,20 +1220,13 @@ function SessionPanel({
1076
1220
  ) : handshakeSetup ? null : intent.status === 'finished' ? (
1077
1221
  <p>All plan steps were applied.</p>
1078
1222
  ) : showConnectedProgress || preparing ? null : (
1079
- <p>
1223
+ <p className="hud-step-label">
1080
1224
  {stepLabel}
1081
1225
  {intent.reason ? ` · ${intent.reason}` : ''}
1082
1226
  </p>
1083
1227
  )}
1084
1228
  {!askingBlueprint && !sendingBlueprint && (
1085
1229
  <>
1086
- {canPlace && !intent.working && (
1087
- <p>
1088
- On the map, right-click to add a file or folder.{' '}
1089
- <kbd>Space</kbd> places a file, <kbd>B</kbd> an island while
1090
- walking.
1091
- </p>
1092
- )}
1093
1230
  {intent.steps?.length > 0 && (
1094
1231
  <ol className="hud-steps">
1095
1232
  {intent.steps.map((step) => {
@@ -1097,6 +1234,8 @@ function SessionPanel({
1097
1234
  const processing = processingStep === step.index
1098
1235
  const accepted = acceptedSteps.has(step.index) && !proposed
1099
1236
  const creating = processing && !proposed
1237
+ const explaining =
1238
+ startingExplain || Boolean(intent.pendingExplain)
1100
1239
  const showStepAction =
1101
1240
  creating ||
1102
1241
  (canRunNext && invokeStep?.index === step.index) ||
@@ -1111,29 +1250,54 @@ function SessionPanel({
1111
1250
  <span className="hud-step-main">
1112
1251
  <span className="hud-step-title">{step.title}</span>
1113
1252
  {showStepAction && (
1114
- <button
1115
- className="hud-button hud-button-approve hud-run-step"
1116
- type="button"
1117
- disabled={creating}
1118
- aria-busy={creating}
1119
- onClick={() =>
1120
- proposed
1121
- ? lastStep
1122
- ? act('continue')
1253
+ <span className="hud-step-actions">
1254
+ <button
1255
+ className="hud-button hud-button-approve hud-run-step"
1256
+ type="button"
1257
+ disabled={creating}
1258
+ aria-busy={creating}
1259
+ onClick={() =>
1260
+ proposed
1261
+ ? lastStep
1262
+ ? act('continue')
1263
+ : act('invoke', {
1264
+ step: step.index + 1,
1265
+ })
1123
1266
  : act('invoke', {
1124
- step: step.index + 1,
1267
+ step: step.index,
1125
1268
  })
1126
- : act('invoke', {
1127
- step: step.index,
1269
+ }
1270
+ >
1271
+ {proposed
1272
+ ? 'Accept proposal'
1273
+ : creating
1274
+ ? 'Creating proposal…'
1275
+ : 'Create proposal'}
1276
+ </button>
1277
+ {!creating && (
1278
+ <button
1279
+ className="hud-button hud-button-approve hud-run-step"
1280
+ type="button"
1281
+ disabled={explaining}
1282
+ aria-busy={explaining}
1283
+ onClick={() => {
1284
+ setStartingExplain(true)
1285
+ void Promise.resolve(
1286
+ onWorkflowAction(
1287
+ sessionId,
1288
+ 'explain_proposal',
1289
+ ),
1290
+ ).then((ok) => {
1291
+ if (ok === false) setStartingExplain(false)
1128
1292
  })
1129
- }
1130
- >
1131
- {proposed
1132
- ? 'Accept proposal'
1133
- : creating
1134
- ? 'Creating proposal…'
1135
- : 'Create proposal'}
1136
- </button>
1293
+ }}
1294
+ >
1295
+ {explaining
1296
+ ? 'Starting explanation…'
1297
+ : 'Explain proposal'}
1298
+ </button>
1299
+ )}
1300
+ </span>
1137
1301
  )}
1138
1302
  </span>
1139
1303
  </li>
@@ -1200,7 +1364,7 @@ function SessionPanel({
1200
1364
  {(intent.createFolders ?? []).length > 0 && (
1201
1365
  <>
1202
1366
  <div className="hud-section-title hud-section-title-add">
1203
- Added islands
1367
+ Added folders
1204
1368
  </div>
1205
1369
  <ul>
1206
1370
  {intent.createFolders.map((id) => (
@@ -1276,17 +1440,20 @@ function SessionPanel({
1276
1440
  )}
1277
1441
  </MutationFold>
1278
1442
  {planReady && (
1279
- <div className="hud-decide">
1280
- <button
1281
- className="hud-button hud-button-reject"
1282
- type="button"
1283
- onClick={() => act('stop')}
1284
- >
1285
- Stop
1286
- </button>
1443
+ <div className="hud-session-actions">
1444
+ {showPlaceHint && <PlaceFilesHint />}
1445
+ <div className="hud-decide">
1446
+ <button
1447
+ className="hud-button hud-button-reject"
1448
+ type="button"
1449
+ onClick={() => act('stop')}
1450
+ >
1451
+ Stop
1452
+ </button>
1453
+ </div>
1287
1454
  </div>
1288
1455
  )}
1289
- {pending && (
1456
+ {pending && !llmDisconnected && (
1290
1457
  <>
1291
1458
  <label className="hud-instruction">
1292
1459
  <span>Alternative instruction for the LLM</span>
@@ -1299,24 +1466,27 @@ function SessionPanel({
1299
1466
  onKeyDown={(event) => event.stopPropagation()}
1300
1467
  />
1301
1468
  </label>
1302
- <div className="hud-decide">
1303
- <button
1304
- className="hud-button hud-button-extend"
1305
- type="button"
1306
- disabled={!instruction.trim()}
1307
- onClick={() =>
1308
- act('instruct', { instruction })
1309
- }
1310
- >
1311
- Send instruction
1312
- </button>
1313
- <button
1314
- className="hud-button hud-button-reject"
1315
- type="button"
1316
- onClick={() => act('stop')}
1317
- >
1318
- Stop
1319
- </button>
1469
+ <div className="hud-session-actions">
1470
+ {showPlaceHint && <PlaceFilesHint />}
1471
+ <div className="hud-decide">
1472
+ <button
1473
+ className="hud-button hud-button-extend"
1474
+ type="button"
1475
+ disabled={!instruction.trim()}
1476
+ onClick={() =>
1477
+ act('instruct', { instruction })
1478
+ }
1479
+ >
1480
+ Send instruction
1481
+ </button>
1482
+ <button
1483
+ className="hud-button hud-button-reject"
1484
+ type="button"
1485
+ onClick={() => act('stop')}
1486
+ >
1487
+ Stop
1488
+ </button>
1489
+ </div>
1320
1490
  </div>
1321
1491
  </>
1322
1492
  )}
@@ -1391,7 +1561,7 @@ function BranchChangesPanel({
1391
1561
  {(changes.createFolders ?? []).length > 0 && (
1392
1562
  <>
1393
1563
  <div className="hud-section-title hud-section-title-add">
1394
- Added islands
1564
+ Added folders
1395
1565
  </div>
1396
1566
  <ul>
1397
1567
  {changes.createFolders.map((id) => (
@@ -1459,7 +1629,7 @@ type ExplorerInstruction = {
1459
1629
  label: string
1460
1630
  }
1461
1631
 
1462
- type InstructionView = 'walk' | '3dview' | 'map'
1632
+ type InstructionView = 'walk' | 'map'
1463
1633
 
1464
1634
  type ExplorerInstructionSection = {
1465
1635
  id: InstructionView
@@ -1492,10 +1662,7 @@ function explorerInstructions({
1492
1662
  changePathsOnly,
1493
1663
  selectedUserCreated,
1494
1664
  infoVisible,
1495
- thumbnailVisible,
1496
1665
  importedBy,
1497
- canStop,
1498
- sessionCount,
1499
1666
  showBranchChanges,
1500
1667
  canShowBranchChanges,
1501
1668
  }: {
@@ -1504,10 +1671,7 @@ function explorerInstructions({
1504
1671
  changePathsOnly: boolean
1505
1672
  selectedUserCreated: boolean
1506
1673
  infoVisible: boolean
1507
- thumbnailVisible: boolean
1508
1674
  importedBy: boolean
1509
- canStop: boolean
1510
- sessionCount: number
1511
1675
  showBranchChanges: boolean
1512
1676
  canShowBranchChanges: boolean
1513
1677
  }): ExplorerInstructionSection[] {
@@ -1541,23 +1705,6 @@ function explorerInstructions({
1541
1705
  },
1542
1706
  ]
1543
1707
  : []
1544
- const stop: ExplorerInstruction[] = canStop
1545
- ? [
1546
- {
1547
- id: 'stop',
1548
- keys: ['Stop'],
1549
- label:
1550
- sessionCount > 1
1551
- ? 'Ends the focused LLM session'
1552
- : 'Ends this LLM session',
1553
- },
1554
- ]
1555
- : []
1556
- const thumbnail: ExplorerInstruction = {
1557
- id: 'thumbnail',
1558
- keys: ['T'],
1559
- label: thumbnailVisible ? 'Hide 3D view' : 'Show 3D view',
1560
- }
1561
1708
  return [
1562
1709
  {
1563
1710
  id: 'walk',
@@ -1568,8 +1715,6 @@ function explorerInstructions({
1568
1715
  { id: 'shift', keys: ['Shift'], label: 'Sprint' },
1569
1716
  ...(canPlace
1570
1717
  ? [
1571
- { id: 'space', keys: ['Space'], label: 'Place file' },
1572
- { id: 'b-island', keys: ['B'], label: 'Place island' },
1573
1718
  {
1574
1719
  id: 'point-to',
1575
1720
  keys: ['Point to'],
@@ -1581,7 +1726,7 @@ function explorerInstructions({
1581
1726
  {
1582
1727
  id: 'dblclick-info',
1583
1728
  keys: ['Double-click'],
1584
- label: 'A block for info',
1729
+ label: 'A file or folder for info',
1585
1730
  },
1586
1731
  { id: 'aim-line', keys: ['Click'], label: 'Aim a line to fly' },
1587
1732
  ...info,
@@ -1590,26 +1735,33 @@ function explorerInstructions({
1590
1735
  {
1591
1736
  id: 'update-model',
1592
1737
  keys: ['Update model'],
1593
- label: 'Rescan and rebuild the map',
1738
+ label: 'Rescan files and folders',
1739
+ },
1740
+ {
1741
+ id: 'cursor-chat',
1742
+ keys: ['Cursor chat'],
1743
+ label:
1744
+ 'Connects to the next empty session, or /coral /amber /lime /orange /violet for that color; /explain for explain mode; 5 chats at once',
1594
1745
  },
1595
1746
  {
1596
- id: 'setup-session',
1597
- keys: ['Setup LLM session'],
1598
- label: 'Open a session; /inbase attaches the oldest waiting one',
1747
+ id: 'blueprint-select',
1748
+ keys: ['Blueprint colors'],
1749
+ label:
1750
+ 'All colors stay visible; the selected color is where new files go',
1599
1751
  },
1600
1752
  {
1601
1753
  id: 'blueprint-toggle',
1602
- keys: ['Hide/Show blueprint'],
1603
- label: 'Toggle the shared blueprint overlay',
1754
+ keys: ['Hide/Show'],
1755
+ label: 'Hide this color; other blueprint colors stay visible',
1604
1756
  },
1605
1757
  {
1606
1758
  id: 'blueprint-clear',
1607
- keys: ['Clear blueprint'],
1759
+ keys: ['Clear'],
1608
1760
  label: 'Remove every planned file and folder',
1609
1761
  },
1610
1762
  {
1611
1763
  id: 'blueprint-cleanup',
1612
- keys: ['Cleanup blueprint'],
1764
+ keys: ['Cleanup'],
1613
1765
  label: 'Drop blueprint files and folders that already exist',
1614
1766
  },
1615
1767
  { id: 'toggle-map', keys: ['M'], label: 'Toggle map' },
@@ -1618,26 +1770,6 @@ function explorerInstructions({
1618
1770
  keys: ['Double-click', 'Esc'],
1619
1771
  label: 'Release mouse',
1620
1772
  },
1621
- ...stop,
1622
- ],
1623
- },
1624
- {
1625
- id: '3dview',
1626
- title: '3D view',
1627
- items: [
1628
- { id: 'scroll-zoom', keys: ['Scroll'], label: 'Zoom' },
1629
- { id: 'drag-pan', keys: ['Drag'], label: 'Pan' },
1630
- {
1631
- id: 'cmd-drag-rotate',
1632
- keys: ['⌘', 'Ctrl', 'Drag'],
1633
- label: 'Rotate',
1634
- },
1635
- {
1636
- id: 'dblclick-reset',
1637
- keys: ['Double-click'],
1638
- label: 'Reset view',
1639
- },
1640
- thumbnail,
1641
1773
  ],
1642
1774
  },
1643
1775
  {
@@ -1646,26 +1778,26 @@ function explorerInstructions({
1646
1778
  items: [
1647
1779
  { id: 'scroll-zoom', keys: ['Scroll'], label: 'Zoom' },
1648
1780
  { id: 'drag-pan', keys: ['Drag'], label: 'Pan' },
1649
- { id: 'click-block', keys: ['Click'], label: 'A block for info' },
1781
+ { id: 'click-block', keys: ['Click'], label: 'A file for info' },
1650
1782
  {
1651
1783
  id: 'click-island',
1652
1784
  keys: ['Click'],
1653
- label: 'An island for its files',
1785
+ label: 'A folder for its files',
1654
1786
  },
1655
1787
  ...(canPlace
1656
1788
  ? [
1657
- { id: 'select-island', keys: ['Click'], label: 'Select an island' },
1789
+ { id: 'select-island', keys: ['Click'], label: 'Select a folder' },
1658
1790
  {
1659
1791
  id: 'add-file-folder',
1660
1792
  keys: ['Right-click'],
1661
- label: 'Add file or folder, or point to a folder',
1793
+ label: 'Create a file or folder, or point to a folder',
1662
1794
  },
1663
1795
  ]
1664
1796
  : []),
1665
1797
  {
1666
1798
  id: 'option-click-walk',
1667
1799
  keys: ['Option', 'Click'],
1668
- label: 'An island to walk',
1800
+ label: 'A folder to walk',
1669
1801
  },
1670
1802
  {
1671
1803
  id: 'gold-pin',
@@ -1685,36 +1817,41 @@ function explorerInstructions({
1685
1817
  : []),
1686
1818
  ...backspace,
1687
1819
  ...info,
1688
- thumbnail,
1689
1820
  imported,
1690
1821
  ...branch,
1691
1822
  {
1692
1823
  id: 'update-model',
1693
1824
  keys: ['Update model'],
1694
- label: 'Rescan and rebuild the map',
1825
+ label: 'Rescan files and folders',
1695
1826
  },
1696
1827
  {
1697
- id: 'setup-session',
1698
- keys: ['Setup LLM session'],
1699
- label: 'Open a session; /inbase attaches the oldest waiting one',
1828
+ id: 'cursor-chat',
1829
+ keys: ['Cursor chat'],
1830
+ label:
1831
+ 'Connects to the next empty session, or /coral /amber /lime /orange /violet for that color; /explain for explain mode; 5 chats at once',
1832
+ },
1833
+ {
1834
+ id: 'blueprint-select',
1835
+ keys: ['Blueprint colors'],
1836
+ label:
1837
+ 'All colors stay visible; the selected color is where new files go',
1700
1838
  },
1701
1839
  {
1702
1840
  id: 'blueprint-toggle',
1703
- keys: ['Hide/Show blueprint'],
1704
- label: 'Toggle the shared blueprint overlay',
1841
+ keys: ['Hide/Show'],
1842
+ label: 'Hide this color; other blueprint colors stay visible',
1705
1843
  },
1706
1844
  {
1707
1845
  id: 'blueprint-clear',
1708
- keys: ['Clear blueprint'],
1846
+ keys: ['Clear'],
1709
1847
  label: 'Remove every planned file and folder',
1710
1848
  },
1711
1849
  {
1712
1850
  id: 'blueprint-cleanup',
1713
- keys: ['Cleanup blueprint'],
1851
+ keys: ['Cleanup'],
1714
1852
  label: 'Drop blueprint files and folders that already exist',
1715
1853
  },
1716
1854
  { id: 'map-walk', keys: ['M'], label: 'Back to walk' },
1717
- ...stop,
1718
1855
  ],
1719
1856
  },
1720
1857
  ]
@@ -1722,14 +1859,12 @@ function explorerInstructions({
1722
1859
 
1723
1860
  type HUDProps = {
1724
1861
  graph: CodebaseGraph
1725
- layout: WorldLayout
1726
1862
  mode: ViewMode
1727
1863
  locked: boolean
1728
1864
  selectedId: string | null
1729
1865
  selectedTick?: number
1730
1866
  inspectTick?: number
1731
1867
  selectedFolder?: string | null
1732
- landAt: [number, number]
1733
1868
  aimedRelation: AimedRelation | null
1734
1869
  aimedFileId?: string | null
1735
1870
  intent: AgentIntent
@@ -1737,7 +1872,6 @@ type HUDProps = {
1737
1872
  focusedSessionId?: string | null
1738
1873
  nextAttachSessionId?: string | null
1739
1874
  onFocusSession?: (sessionId: string) => void
1740
- onSetupSession?: () => Promise<unknown>
1741
1875
  onWorkflowAction: (
1742
1876
  sessionId: string,
1743
1877
  action: WorkflowAction,
@@ -1746,8 +1880,6 @@ type HUDProps = {
1746
1880
  onNavigateDiff: (sessionId: string, diffId: string) => void
1747
1881
  onOpenMap: () => void
1748
1882
  onWalk: () => void
1749
- followLook: boolean
1750
- onToggleFollowLook: () => void
1751
1883
  showBranchChanges?: boolean
1752
1884
  branchChanges?: BranchChanges
1753
1885
  canShowBranchChanges?: boolean
@@ -1772,6 +1904,9 @@ type HUDProps = {
1772
1904
  onAddBlueprintFunction?: (fileId: string, name: string) => boolean
1773
1905
  onAddBlueprintVariable?: (fileId: string, name: string) => boolean
1774
1906
  onAddBlueprintImport?: (fileId: string, raw: string) => boolean
1907
+ importPickActive?: boolean
1908
+ onToggleImportPick?: () => void
1909
+ onCancelImportPick?: () => void
1775
1910
  onRemoveBlueprintFunction?: (fileId: string, name: string) => void
1776
1911
  onRemoveBlueprintVariable?: (fileId: string, name: string) => void
1777
1912
  onRemoveBlueprintImport?: (
@@ -1789,32 +1924,40 @@ type HUDProps = {
1789
1924
  kind: BlueprintPointerKind
1790
1925
  path: string
1791
1926
  name?: string
1927
+ color?: string
1792
1928
  }) => void
1793
1929
  onMapAddFile?: (folderPath: string) => void
1794
1930
  onMapAddFolder?: (folderPath: string) => void
1931
+ onRenameCreatedFile?: (fileId: string, name: string) => string | null
1795
1932
  onInspectFile?: (fileId: string) => void
1796
1933
  onInspectBlock?: (fileId: string) => void
1797
- plannedIds?: string[]
1798
- createdIds?: string[]
1799
- deletedIds?: string[]
1934
+ onExplainTarget?: (input: {
1935
+ kind: ExplainTargetKind
1936
+ path: string
1937
+ name?: string
1938
+ }) => void
1800
1939
  blueprintHidden?: boolean
1801
1940
  blueprintHasContent?: boolean
1802
1941
  blueprintCanCleanup?: boolean
1942
+ blueprintColor?: string | null
1943
+ blueprintOptions?: BlueprintOption[]
1944
+ blueprintColorPointers?: BlueprintColorOption[]
1945
+ onSelectBlueprintColor?: (color: string) => void
1803
1946
  onToggleBlueprintHidden?: () => void
1804
1947
  onClearBlueprint?: () => void
1805
1948
  onCleanupBlueprint?: () => void
1949
+ devTargets?: DevTargetsState
1950
+ onSelectDevTarget?: (id: string) => void
1806
1951
  }
1807
1952
 
1808
1953
  export function HUD({
1809
1954
  graph,
1810
- layout,
1811
1955
  mode,
1812
1956
  locked,
1813
1957
  selectedId,
1814
1958
  selectedTick = 0,
1815
1959
  inspectTick = 0,
1816
1960
  selectedFolder = null,
1817
- landAt,
1818
1961
  aimedRelation,
1819
1962
  aimedFileId = null,
1820
1963
  intent,
@@ -1822,13 +1965,10 @@ export function HUD({
1822
1965
  focusedSessionId = null,
1823
1966
  nextAttachSessionId = null,
1824
1967
  onFocusSession,
1825
- onSetupSession,
1826
1968
  onWorkflowAction,
1827
1969
  onNavigateDiff,
1828
1970
  onOpenMap,
1829
1971
  onWalk,
1830
- followLook,
1831
- onToggleFollowLook,
1832
1972
  showBranchChanges = false,
1833
1973
  branchChanges,
1834
1974
  canShowBranchChanges = false,
@@ -1849,10 +1989,12 @@ export function HUD({
1849
1989
  blueprintVariables = [],
1850
1990
  blueprintImports = [],
1851
1991
  blueprintNotes = [],
1852
- blueprintPointers = [],
1853
1992
  onAddBlueprintFunction,
1854
1993
  onAddBlueprintVariable,
1855
1994
  onAddBlueprintImport,
1995
+ importPickActive = false,
1996
+ onToggleImportPick,
1997
+ onCancelImportPick,
1856
1998
  onRemoveBlueprintFunction,
1857
1999
  onRemoveBlueprintVariable,
1858
2000
  onRemoveBlueprintImport,
@@ -1860,17 +2002,22 @@ export function HUD({
1860
2002
  onToggleBlueprintPointer,
1861
2003
  onMapAddFile,
1862
2004
  onMapAddFolder,
2005
+ onRenameCreatedFile,
1863
2006
  onInspectFile,
1864
2007
  onInspectBlock,
1865
- plannedIds = [],
1866
- createdIds = [],
1867
- deletedIds = [],
2008
+ onExplainTarget,
1868
2009
  blueprintHidden = false,
1869
2010
  blueprintHasContent = false,
1870
2011
  blueprintCanCleanup = false,
2012
+ blueprintColor = null,
2013
+ blueprintOptions = [],
2014
+ blueprintColorPointers = [],
2015
+ onSelectBlueprintColor,
1871
2016
  onToggleBlueprintHidden,
1872
2017
  onClearBlueprint,
1873
2018
  onCleanupBlueprint,
2019
+ devTargets,
2020
+ onSelectDevTarget,
1874
2021
  }: HUDProps) {
1875
2022
  const selected = graph.files.find((file) => file.id === selectedId)
1876
2023
  const selectedFolderNode = graph.folders.find(
@@ -1891,7 +2038,6 @@ export function HUD({
1891
2038
  const sessions = (intents ?? [intent]).filter(
1892
2039
  (item) => item.sessionId && isReviewingIntent(item.status),
1893
2040
  )
1894
- const canStop = canStopSession(intent)
1895
2041
  const nextAttachSession =
1896
2042
  sessions.find((session) => session.sessionId === nextAttachSessionId) ??
1897
2043
  [...sessions].reverse().find((session) => session.awaitingAttach) ??
@@ -1899,6 +2045,9 @@ export function HUD({
1899
2045
  const [walkIntro, setWalkIntro] = useState(false)
1900
2046
  const walkIntroSeen = useRef(false)
1901
2047
  const [instructionsOpen, setInstructionsOpen] = useState(false)
2048
+ const [actionsMenuOpen, setActionsMenuOpen] = useState(false)
2049
+ const [actionsMenuPosition, setActionsMenuPosition] = useState<CSSProperties>()
2050
+ const actionsMenuRef = useRef<HTMLDivElement>(null)
1902
2051
  const [noteEditor, setNoteEditor] = useState<{
1903
2052
  file: string
1904
2053
  kind: BlueprintNoteKind
@@ -1907,13 +2056,8 @@ export function HUD({
1907
2056
  subtitle: string
1908
2057
  placeholder: string
1909
2058
  } | null>(null)
1910
- const [setupError, setSetupError] = useState<string | null>(null)
1911
- const [setupBusy, setSetupBusy] = useState(false)
1912
2059
  const [infoVisible, setInfoVisible] = useState(false)
1913
2060
  const [infoMinimized, setInfoMinimized] = useState(false)
1914
- const [thumbnailVisible, setThumbnailVisible] = useState(true)
1915
- const [thumbnailMinimized, setThumbnailMinimized] = useState(false)
1916
- const [thumbnailMaximized, setThumbnailMaximized] = useState(false)
1917
2061
  const infoPanelRef = useRef<HTMLDivElement>(null)
1918
2062
  const canPlace = true
1919
2063
  const overlay = showBranchChanges && branchChanges ? branchChanges : intent
@@ -1984,19 +2128,23 @@ export function HUD({
1984
2128
  )
1985
2129
  const canEditBlueprint =
1986
2130
  canPlace && Boolean(selected) && !selected?.id.startsWith('draft:')
2131
+ const canRenameSelected =
2132
+ Boolean(onRenameCreatedFile) &&
2133
+ Boolean(selected?.userCreated) &&
2134
+ !selected?.id.startsWith('draft:')
2135
+ const renameSelectedFile = (nextName: string) => {
2136
+ if (!selected || !onRenameCreatedFile) return false
2137
+ const previousId = selected.id
2138
+ const nextId = onRenameCreatedFile(previousId, nextName)
2139
+ if (!nextId) return false
2140
+ setNoteEditor((current) =>
2141
+ current?.file === previousId ? { ...current, file: nextId } : current,
2142
+ )
2143
+ return true
2144
+ }
1987
2145
  const selectedFileNote = selected
1988
2146
  ? findBlueprintNote(blueprintNotes, selected.id, 'file')
1989
2147
  : ''
1990
- const selectedFilePointed = selected
1991
- ? findBlueprintPointer(blueprintPointers, 'file', selected.id)
1992
- : false
1993
- const selectedFolderPointed = selectedFolderNode
1994
- ? findBlueprintPointer(
1995
- blueprintPointers,
1996
- 'folder',
1997
- selectedFolderNode.path,
1998
- )
1999
- : false
2000
2148
  const openFileNote = () => {
2001
2149
  if (!selected || !onSetBlueprintNote) return
2002
2150
  setInstructionsOpen(false)
@@ -2074,6 +2222,11 @@ export function HUD({
2074
2222
  setInfoMinimized(false)
2075
2223
  }, [locked])
2076
2224
 
2225
+ useEffect(() => {
2226
+ if (infoVisible || !importPickActive) return
2227
+ onCancelImportPick?.()
2228
+ }, [importPickActive, infoVisible, onCancelImportPick])
2229
+
2077
2230
  const infoOpen = infoVisible && Boolean(selected || selectedFolderNode)
2078
2231
 
2079
2232
  useEffect(() => {
@@ -2102,26 +2255,6 @@ export function HUD({
2102
2255
  return () => window.removeEventListener('keydown', onKey)
2103
2256
  }, [aimedFileId, infoVisible, mode, onInspectBlock, selectedId])
2104
2257
 
2105
- useEffect(() => {
2106
- if (!mapping) return
2107
- const onKey = (event: KeyboardEvent) => {
2108
- if (event.repeat || event.code !== 'KeyT') return
2109
- if (shouldIgnoreShortcut(event)) return
2110
- event.preventDefault()
2111
- setThumbnailVisible((visible) => {
2112
- if (!visible) {
2113
- setThumbnailMinimized(false)
2114
- setThumbnailMaximized(false)
2115
- } else {
2116
- setThumbnailMaximized(false)
2117
- }
2118
- return !visible
2119
- })
2120
- }
2121
- window.addEventListener('keydown', onKey)
2122
- return () => window.removeEventListener('keydown', onKey)
2123
- }, [mapping])
2124
-
2125
2258
  useEffect(() => {
2126
2259
  if (!infoVisible || (!selectedId && !selectedFolder)) return
2127
2260
  const onKey = (event: KeyboardEvent) => {
@@ -2141,6 +2274,7 @@ export function HUD({
2141
2274
  useEffect(() => {
2142
2275
  if (!instructionsOpen) return
2143
2276
  document.exitPointerLock()
2277
+ setActionsMenuOpen(false)
2144
2278
  const onKey = (event: KeyboardEvent) => {
2145
2279
  if (event.code !== 'Escape') return
2146
2280
  if (shouldIgnoreShortcut(event)) return
@@ -2152,24 +2286,61 @@ export function HUD({
2152
2286
  return () => window.removeEventListener('keydown', onKey, true)
2153
2287
  }, [instructionsOpen])
2154
2288
 
2289
+ useLayoutEffect(() => {
2290
+ if (!actionsMenuOpen) return
2291
+ const updatePosition = () => {
2292
+ const trigger = actionsMenuRef.current
2293
+ if (!trigger) return
2294
+ const rect = trigger.getBoundingClientRect()
2295
+ setActionsMenuPosition({
2296
+ right: window.innerWidth - rect.right,
2297
+ bottom: window.innerHeight - rect.top + 8,
2298
+ })
2299
+ }
2300
+ updatePosition()
2301
+ window.addEventListener('resize', updatePosition)
2302
+ return () => window.removeEventListener('resize', updatePosition)
2303
+ }, [actionsMenuOpen])
2304
+
2305
+ useEffect(() => {
2306
+ if (!actionsMenuOpen) return
2307
+ const onKey = (event: KeyboardEvent) => {
2308
+ if (event.code !== 'Escape') return
2309
+ if (shouldIgnoreShortcut(event)) return
2310
+ event.preventDefault()
2311
+ event.stopPropagation()
2312
+ setActionsMenuOpen(false)
2313
+ }
2314
+ const onPointerDown = (event: PointerEvent) => {
2315
+ const target = event.target
2316
+ if (
2317
+ target instanceof Element &&
2318
+ (actionsMenuRef.current?.contains(target) ||
2319
+ target.closest('.hud-actions-menu-list'))
2320
+ ) {
2321
+ return
2322
+ }
2323
+ setActionsMenuOpen(false)
2324
+ }
2325
+ window.addEventListener('keydown', onKey, true)
2326
+ window.addEventListener('pointerdown', onPointerDown, true)
2327
+ return () => {
2328
+ window.removeEventListener('keydown', onKey, true)
2329
+ window.removeEventListener('pointerdown', onPointerDown, true)
2330
+ }
2331
+ }, [actionsMenuOpen])
2332
+
2155
2333
  const instructionSections = explorerInstructions({
2156
2334
  canPlace,
2157
2335
  hasChangeSet,
2158
2336
  changePathsOnly,
2159
2337
  selectedUserCreated: Boolean(selected?.userCreated),
2160
2338
  infoVisible,
2161
- thumbnailVisible,
2162
2339
  importedBy,
2163
- canStop,
2164
- sessionCount: sessions.length,
2165
2340
  showBranchChanges,
2166
2341
  canShowBranchChanges,
2167
2342
  })
2168
- const currentInstructionView: InstructionView = mapping
2169
- ? thumbnailMaximized
2170
- ? '3dview'
2171
- : 'map'
2172
- : 'walk'
2343
+ const currentInstructionView: InstructionView = mapping ? 'map' : 'walk'
2173
2344
 
2174
2345
  return (
2175
2346
  <div className="hud">
@@ -2184,13 +2355,8 @@ export function HUD({
2184
2355
  </p>
2185
2356
  <p>
2186
2357
  <kbd>W</kbd> <kbd>A</kbd> <kbd>S</kbd> <kbd>D</kbd> walk,{' '}
2187
- <kbd>Shift</kbd> sprint
2188
- {canPlace ? (
2189
- <>
2190
- , <kbd>Space</kbd> place a file, <kbd>B</kbd> place an island
2191
- </>
2192
- ) : null}
2193
- , double-click a block or press <kbd>I</kbd> for info.
2358
+ <kbd>Shift</kbd> sprint, double-click a file or folder or press{' '}
2359
+ <kbd>I</kbd> for info.
2194
2360
  </p>
2195
2361
  </div>
2196
2362
  </div>
@@ -2203,6 +2369,7 @@ export function HUD({
2203
2369
  <div className="hud-name-gate">
2204
2370
  <NameInput
2205
2371
  placeholder="Folder name"
2372
+ fallbackName="New folder"
2206
2373
  onCommit={onCommitIslandName}
2207
2374
  onCancel={onCancelIslandName}
2208
2375
  />
@@ -2234,20 +2401,40 @@ export function HUD({
2234
2401
  >
2235
2402
  Walk
2236
2403
  </button>
2237
- {canStop && (
2238
- <button
2239
- className="hud-button hud-button-reject"
2240
- type="button"
2241
- aria-label="Stop LLM session"
2242
- onClick={() =>
2243
- intent.sessionId && onWorkflowAction(intent.sessionId, 'stop')
2244
- }
2245
- >
2246
- Stop
2247
- </button>
2248
- )}
2249
2404
  </div>
2250
- {selected && <div className="hud-chip">{selected.path}</div>}
2405
+ {(selected ||
2406
+ (devTargets?.enabled &&
2407
+ onSelectDevTarget &&
2408
+ devTargets.targets.length > 0)) && (
2409
+ <div className="hud-top-end">
2410
+ {selected && <div className="hud-chip">{selected.path}</div>}
2411
+ {devTargets?.enabled &&
2412
+ onSelectDevTarget &&
2413
+ devTargets.targets.length > 0 && (
2414
+ <label className="hud-target-select">
2415
+ <span>Look at</span>
2416
+ <select
2417
+ className="hud-button hud-target-select-control"
2418
+ aria-label="Look at"
2419
+ title="Choose which project the map scans. Only available while developing Inbase."
2420
+ value={devTargets.currentId ?? ''}
2421
+ disabled={updatingModel}
2422
+ onChange={(event) => {
2423
+ const next = event.target.value
2424
+ if (!next || next === devTargets.currentId) return
2425
+ onSelectDevTarget(next)
2426
+ }}
2427
+ >
2428
+ {devTargets.targets.map((target) => (
2429
+ <option key={target.id} value={target.id}>
2430
+ {target.label}
2431
+ </option>
2432
+ ))}
2433
+ </select>
2434
+ </label>
2435
+ )}
2436
+ </div>
2437
+ )}
2251
2438
  </div>
2252
2439
 
2253
2440
  {(sessions.length > 0 || showBranchChanges) && (
@@ -2258,23 +2445,33 @@ export function HUD({
2258
2445
  const active =
2259
2446
  session.sessionId === (focusedSessionId ?? intent.sessionId)
2260
2447
  const attached = session.awaitingAttach === false
2261
- const label = sessionTabLabel(session, sessions)
2448
+ const label = sessionDisplayName(session) || 'Session'
2262
2449
  return (
2263
2450
  <button
2264
2451
  className="hud-button hud-session-tab"
2265
2452
  type="button"
2266
2453
  role="tab"
2267
2454
  aria-selected={active}
2455
+ aria-label={
2456
+ attached ? `${label}, attached` : `${label}, waiting`
2457
+ }
2268
2458
  data-active={active}
2269
2459
  data-attached={attached}
2270
2460
  key={session.sessionId}
2271
2461
  title={attached ? `${label} · Attached` : `${label} · Waiting`}
2462
+ style={
2463
+ session.colorHex
2464
+ ? ({
2465
+ '--session-color': session.colorHex,
2466
+ } as CSSProperties)
2467
+ : undefined
2468
+ }
2272
2469
  onClick={() => {
2273
2470
  if (session.sessionId) onFocusSession?.(session.sessionId)
2471
+ if (session.color) onSelectBlueprintColor?.(session.color)
2274
2472
  }}
2275
2473
  >
2276
- <span className="hud-attach-dot" aria-hidden="true" />
2277
- <span className="hud-session-tab-label">{label}</span>
2474
+ <SessionSwatch colorHex={session.colorHex} />
2278
2475
  </button>
2279
2476
  )
2280
2477
  })}
@@ -2306,7 +2503,16 @@ export function HUD({
2306
2503
  data-minimized={infoMinimized}
2307
2504
  >
2308
2505
  <PanelChrome
2309
- title={selected.name}
2506
+ title={
2507
+ canRenameSelected ? (
2508
+ <InfoNameField
2509
+ name={selected.name}
2510
+ onRename={renameSelectedFile}
2511
+ />
2512
+ ) : (
2513
+ selected.name
2514
+ )
2515
+ }
2310
2516
  minimized={infoMinimized}
2311
2517
  onMinimize={() => setInfoMinimized((current) => !current)}
2312
2518
  onClose={() => {
@@ -2320,28 +2526,43 @@ export function HUD({
2320
2526
  <p>
2321
2527
  {selected.lines} lines · {selected.language}
2322
2528
  </p>
2323
- {canInspectFile(selected.id, selected.userCreated) && (
2324
- <button
2325
- className="hud-button hud-inspect"
2326
- type="button"
2327
- onClick={() => onInspectFile?.(selected.id)}
2328
- >
2329
- Inspect file
2330
- </button>
2529
+ {(onExplainTarget ||
2530
+ canInspectFile(selected.id, selected.userCreated)) && (
2531
+ <div className="hud-file-actions">
2532
+ {onExplainTarget ? (
2533
+ <ExplainButton
2534
+ label={`Explain ${selected.name}`}
2535
+ onClick={() =>
2536
+ onExplainTarget({ kind: 'file', path: selected.id })
2537
+ }
2538
+ />
2539
+ ) : null}
2540
+ {canInspectFile(selected.id, selected.userCreated) && (
2541
+ <button
2542
+ className="hud-button hud-inspect"
2543
+ type="button"
2544
+ onClick={() => onInspectFile?.(selected.id)}
2545
+ >
2546
+ Inspect file
2547
+ </button>
2548
+ )}
2549
+ </div>
2331
2550
  )}
2332
2551
  {canEditBlueprint && onToggleBlueprintPointer && (
2333
- <button
2334
- className="hud-button hud-inspect hud-point"
2335
- type="button"
2336
- data-pointed={selectedFilePointed ? 'true' : 'false'}
2337
- aria-pressed={selectedFilePointed}
2338
- onClick={() =>
2339
- onToggleBlueprintPointer({ kind: 'file', path: selected.id })
2552
+ <PointColorControl
2553
+ target={{ kind: 'file', path: selected.id }}
2554
+ colorPointers={blueprintColorPointers}
2555
+ currentColorId={blueprintColor}
2556
+ idleLabel="Point to file"
2557
+ pointedLabel="Stop pointing"
2558
+ onToggle={(color) =>
2559
+ onToggleBlueprintPointer({
2560
+ kind: 'file',
2561
+ path: selected.id,
2562
+ color,
2563
+ })
2340
2564
  }
2341
- >
2342
- <EyeIcon size={15} />
2343
- {selectedFilePointed ? 'Stop pointing' : 'Point to file'}
2344
- </button>
2565
+ />
2345
2566
  )}
2346
2567
  {canEditBlueprint && onSetBlueprintNote && (
2347
2568
  <button
@@ -2399,6 +2620,21 @@ export function HUD({
2399
2620
  >
2400
2621
  {symbol.name}
2401
2622
  </span>
2623
+ {onExplainTarget ? (
2624
+ <div className="hud-item-actions">
2625
+ <ExplainButton
2626
+ compact
2627
+ label={`Explain ${symbol.name}`}
2628
+ onClick={() =>
2629
+ onExplainTarget({
2630
+ kind: 'class',
2631
+ path: selected.id,
2632
+ name: symbol.name,
2633
+ })
2634
+ }
2635
+ />
2636
+ </div>
2637
+ ) : null}
2402
2638
  </li>
2403
2639
  ))}
2404
2640
  </ul>
@@ -2434,23 +2670,35 @@ export function HUD({
2434
2670
  }
2435
2671
  canEdit={Boolean(canEditBlueprint && onSetBlueprintNote)}
2436
2672
  canRemove={Boolean(canEditBlueprint && symbol.intended)}
2437
- pointed={findBlueprintPointer(
2438
- blueprintPointers,
2439
- 'function',
2440
- selected.id,
2441
- symbol.name,
2442
- )}
2673
+ pointerTarget={{
2674
+ kind: 'function',
2675
+ path: selected.id,
2676
+ name: symbol.name,
2677
+ }}
2678
+ colorPointers={blueprintColorPointers}
2679
+ currentColorId={blueprintColor}
2443
2680
  onRemove={() =>
2444
2681
  onRemoveBlueprintFunction?.(selected.id, symbol.name)
2445
2682
  }
2446
2683
  onOpenNote={() => openSymbolNote('function', symbol.name)}
2684
+ onExplain={
2685
+ onExplainTarget
2686
+ ? () =>
2687
+ onExplainTarget({
2688
+ kind: 'function',
2689
+ path: selected.id,
2690
+ name: symbol.name,
2691
+ })
2692
+ : undefined
2693
+ }
2447
2694
  onTogglePoint={
2448
2695
  onToggleBlueprintPointer
2449
- ? () =>
2696
+ ? (color) =>
2450
2697
  onToggleBlueprintPointer({
2451
2698
  kind: 'function',
2452
2699
  path: selected.id,
2453
2700
  name: symbol.name,
2701
+ color,
2454
2702
  })
2455
2703
  : undefined
2456
2704
  }
@@ -2475,20 +2723,32 @@ export function HUD({
2475
2723
  noteEditor.name === item.name
2476
2724
  }
2477
2725
  canEdit={Boolean(canEditBlueprint && onSetBlueprintNote)}
2478
- pointed={findBlueprintPointer(
2479
- blueprintPointers,
2480
- 'function',
2481
- selected.id,
2482
- item.name,
2483
- )}
2726
+ pointerTarget={{
2727
+ kind: 'function',
2728
+ path: selected.id,
2729
+ name: item.name,
2730
+ }}
2731
+ colorPointers={blueprintColorPointers}
2732
+ currentColorId={blueprintColor}
2484
2733
  onOpenNote={() => openSymbolNote('function', item.name)}
2734
+ onExplain={
2735
+ onExplainTarget
2736
+ ? () =>
2737
+ onExplainTarget({
2738
+ kind: 'function',
2739
+ path: selected.id,
2740
+ name: item.name,
2741
+ })
2742
+ : undefined
2743
+ }
2485
2744
  onTogglePoint={
2486
2745
  onToggleBlueprintPointer
2487
- ? () =>
2746
+ ? (color) =>
2488
2747
  onToggleBlueprintPointer({
2489
2748
  kind: 'function',
2490
2749
  path: selected.id,
2491
2750
  name: item.name,
2751
+ color,
2492
2752
  })
2493
2753
  : undefined
2494
2754
  }
@@ -2532,23 +2792,35 @@ export function HUD({
2532
2792
  }
2533
2793
  canEdit={Boolean(canEditBlueprint && onSetBlueprintNote)}
2534
2794
  canRemove={Boolean(canEditBlueprint && symbol.intended)}
2535
- pointed={findBlueprintPointer(
2536
- blueprintPointers,
2537
- 'variable',
2538
- selected.id,
2539
- symbol.name,
2540
- )}
2795
+ pointerTarget={{
2796
+ kind: 'variable',
2797
+ path: selected.id,
2798
+ name: symbol.name,
2799
+ }}
2800
+ colorPointers={blueprintColorPointers}
2801
+ currentColorId={blueprintColor}
2541
2802
  onRemove={() =>
2542
2803
  onRemoveBlueprintVariable?.(selected.id, symbol.name)
2543
2804
  }
2544
2805
  onOpenNote={() => openSymbolNote('variable', symbol.name)}
2806
+ onExplain={
2807
+ onExplainTarget
2808
+ ? () =>
2809
+ onExplainTarget({
2810
+ kind: 'variable',
2811
+ path: selected.id,
2812
+ name: symbol.name,
2813
+ })
2814
+ : undefined
2815
+ }
2545
2816
  onTogglePoint={
2546
2817
  onToggleBlueprintPointer
2547
- ? () =>
2818
+ ? (color) =>
2548
2819
  onToggleBlueprintPointer({
2549
2820
  kind: 'variable',
2550
2821
  path: selected.id,
2551
2822
  name: symbol.name,
2823
+ color,
2552
2824
  })
2553
2825
  : undefined
2554
2826
  }
@@ -2573,20 +2845,32 @@ export function HUD({
2573
2845
  noteEditor.name === item.name
2574
2846
  }
2575
2847
  canEdit={Boolean(canEditBlueprint && onSetBlueprintNote)}
2576
- pointed={findBlueprintPointer(
2577
- blueprintPointers,
2578
- 'variable',
2579
- selected.id,
2580
- item.name,
2581
- )}
2848
+ pointerTarget={{
2849
+ kind: 'variable',
2850
+ path: selected.id,
2851
+ name: item.name,
2852
+ }}
2853
+ colorPointers={blueprintColorPointers}
2854
+ currentColorId={blueprintColor}
2582
2855
  onOpenNote={() => openSymbolNote('variable', item.name)}
2856
+ onExplain={
2857
+ onExplainTarget
2858
+ ? () =>
2859
+ onExplainTarget({
2860
+ kind: 'variable',
2861
+ path: selected.id,
2862
+ name: item.name,
2863
+ })
2864
+ : undefined
2865
+ }
2583
2866
  onTogglePoint={
2584
2867
  onToggleBlueprintPointer
2585
- ? () =>
2868
+ ? (color) =>
2586
2869
  onToggleBlueprintPointer({
2587
2870
  kind: 'variable',
2588
2871
  path: selected.id,
2589
2872
  name: item.name,
2873
+ color,
2590
2874
  })
2591
2875
  : undefined
2592
2876
  }
@@ -2678,10 +2962,20 @@ export function HUD({
2678
2962
  </ul>
2679
2963
  )}
2680
2964
  {canEditBlueprint && !importedBy && onAddBlueprintImport && (
2681
- <AddIntentRow
2682
- placeholder="Clock from src/components/Clock.tsx"
2683
- onAdd={(raw) => onAddBlueprintImport(selected.id, raw)}
2684
- />
2965
+ <>
2966
+ <AddIntentRow
2967
+ placeholder="Clock from src/components/Clock.tsx"
2968
+ onAdd={(raw) => onAddBlueprintImport(selected.id, raw)}
2969
+ pickLabel={mapping ? 'Select a file' : undefined}
2970
+ pickActive={importPickActive}
2971
+ onTogglePick={mapping ? onToggleImportPick : undefined}
2972
+ />
2973
+ {importPickActive && (
2974
+ <p className="hud-pick-hint">
2975
+ Click a file to import it. Esc to cancel.
2976
+ </p>
2977
+ )}
2978
+ </>
2685
2979
  )}
2686
2980
  </div>
2687
2981
  )}
@@ -2701,6 +2995,16 @@ export function HUD({
2701
2995
  setInfoVisible(false)
2702
2996
  setInfoMinimized(false)
2703
2997
  }}
2998
+ onExplain={
2999
+ onExplainTarget
3000
+ ? () =>
3001
+ onExplainTarget({
3002
+ kind: 'folder',
3003
+ path: selectedFolderNode.path,
3004
+ })
3005
+ : undefined
3006
+ }
3007
+ explainLabel={`Explain ${selectedFolderNode.name}`}
2704
3008
  />
2705
3009
  {!infoMinimized && (
2706
3010
  <div ref={infoPanelRef} className="hud-panel-body">
@@ -2714,42 +3018,55 @@ export function HUD({
2714
3018
  </p>
2715
3019
  <div className="hud-section-title">Files</div>
2716
3020
  {folderFiles.length === 0 ? (
2717
- <p>No files on this island</p>
3021
+ <p>No files in this folder</p>
2718
3022
  ) : (
2719
3023
  <ul>
2720
3024
  {folderFiles.map((file) => (
2721
3025
  <li key={file.id}>
2722
3026
  <span>{file.name}</span>
2723
- {canInspectFile(file.id, file.userCreated) && (
2724
- <button
2725
- className="hud-item-inspect"
2726
- type="button"
2727
- onClick={() => onInspectFile?.(file.id)}
2728
- >
2729
- Inspect
2730
- </button>
3027
+ {(onExplainTarget ||
3028
+ canInspectFile(file.id, file.userCreated)) && (
3029
+ <div className="hud-item-actions">
3030
+ {onExplainTarget ? (
3031
+ <ExplainButton
3032
+ compact
3033
+ label={`Explain ${file.name}`}
3034
+ onClick={() =>
3035
+ onExplainTarget({ kind: 'file', path: file.id })
3036
+ }
3037
+ />
3038
+ ) : null}
3039
+ {canInspectFile(file.id, file.userCreated) && (
3040
+ <button
3041
+ className="hud-item-inspect"
3042
+ type="button"
3043
+ onClick={() => onInspectFile?.(file.id)}
3044
+ >
3045
+ Inspect
3046
+ </button>
3047
+ )}
3048
+ </div>
2731
3049
  )}
2732
3050
  </li>
2733
3051
  ))}
2734
3052
  </ul>
2735
3053
  )}
2736
3054
  {canPlace && onToggleBlueprintPointer && (
2737
- <button
2738
- className="hud-button hud-inspect hud-point"
2739
- type="button"
2740
- data-pointed={selectedFolderPointed ? 'true' : 'false'}
2741
- aria-pressed={selectedFolderPointed}
3055
+ <PointColorControl
3056
+ target={{ kind: 'folder', path: selectedFolderNode.path }}
3057
+ colorPointers={blueprintColorPointers}
3058
+ currentColorId={blueprintColor}
3059
+ idleLabel="Point to folder"
3060
+ pointedLabel="Stop pointing"
2742
3061
  disabled={naming || selectedFolderNode.path.startsWith('draft:')}
2743
- onClick={() =>
3062
+ onToggle={(color) =>
2744
3063
  onToggleBlueprintPointer({
2745
3064
  kind: 'folder',
2746
3065
  path: selectedFolderNode.path,
3066
+ color,
2747
3067
  })
2748
3068
  }
2749
- >
2750
- <EyeIcon size={15} />
2751
- {selectedFolderPointed ? 'Stop pointing' : 'Point to folder'}
2752
- </button>
3069
+ />
2753
3070
  )}
2754
3071
  {canPlace && mapping && onMapAddFile && onMapAddFolder && (
2755
3072
  <div className="hud-decide hud-map-blueprint">
@@ -2775,51 +3092,6 @@ export function HUD({
2775
3092
  )}
2776
3093
  </aside>
2777
3094
  )}
2778
-
2779
- {mapping && thumbnailVisible && (
2780
- <CanvasErrorBoundary fallback={null}>
2781
- <SelectionThumbnail
2782
- graph={graph}
2783
- layout={layout}
2784
- selectedId={selectedId}
2785
- selectedFolder={selectedFolder}
2786
- landAt={landAt}
2787
- importedBy={importedBy}
2788
- minimized={thumbnailMinimized}
2789
- maximized={thumbnailMaximized}
2790
- plannedIds={plannedIds}
2791
- createdIds={createdIds}
2792
- deletedIds={deletedIds}
2793
- pointedFileIds={
2794
- blueprintHidden
2795
- ? []
2796
- : blueprintPointers.flatMap((item) =>
2797
- item.kind === 'folder' ? [] : [item.path],
2798
- )
2799
- }
2800
- pointedFolderPaths={
2801
- blueprintHidden
2802
- ? []
2803
- : blueprintPointers.flatMap((item) =>
2804
- item.kind === 'folder' ? [item.path] : [],
2805
- )
2806
- }
2807
- onMinimize={() => {
2808
- setThumbnailMaximized(false)
2809
- setThumbnailMinimized((current) => !current)
2810
- }}
2811
- onMaximize={() => {
2812
- setThumbnailMinimized(false)
2813
- setThumbnailMaximized((current) => !current)
2814
- }}
2815
- onHide={() => {
2816
- setThumbnailVisible(false)
2817
- setThumbnailMinimized(false)
2818
- setThumbnailMaximized(false)
2819
- }}
2820
- />
2821
- </CanvasErrorBoundary>
2822
- )}
2823
3095
  </div>
2824
3096
 
2825
3097
  {noteEditor && onSetBlueprintNote && (
@@ -2892,52 +3164,54 @@ export function HUD({
2892
3164
 
2893
3165
  <div className="hud-bottom">
2894
3166
  <div className="hud-bottom-actions">
2895
- <button
2896
- className="hud-button"
2897
- data-active={instructionsOpen}
2898
- type="button"
2899
- aria-haspopup="dialog"
2900
- aria-expanded={instructionsOpen}
2901
- onClick={() => {
2902
- setNoteEditor(null)
2903
- setInstructionsOpen((open) => !open)
2904
- }}
2905
- >
2906
- Instructions
2907
- </button>
2908
- <button
2909
- className="hud-button"
2910
- type="button"
2911
- aria-label="Rescan the project and rebuild the map"
2912
- disabled={updatingModel}
2913
- onClick={onUpdateModel}
2914
- >
2915
- {updatingModel ? 'Updating…' : 'Update model'}
2916
- </button>
2917
- {onSetupSession && (
2918
- <button
2919
- className="hud-button"
2920
- type="button"
2921
- aria-label="Start an LLM session without attaching a chat yet"
2922
- title={setupError ?? 'Start an LLM session without attaching a chat yet'}
2923
- disabled={setupBusy}
2924
- onClick={() => {
2925
- setInstructionsOpen(false)
2926
- setSetupError(null)
2927
- setSetupBusy(true)
2928
- void onSetupSession()
2929
- .catch((caught) => {
2930
- setSetupError(
2931
- caught instanceof Error
2932
- ? caught.message
2933
- : 'Could not set up the session',
2934
- )
2935
- })
2936
- .finally(() => setSetupBusy(false))
2937
- }}
3167
+ {onSelectBlueprintColor && blueprintOptions.length > 0 && (
3168
+ <div
3169
+ className="hud-blueprint-select"
3170
+ role="radiogroup"
3171
+ aria-label="Blueprint"
2938
3172
  >
2939
- {setupBusy ? 'Starting…' : 'Setup LLM session'}
2940
- </button>
3173
+ {blueprintOptions.map((option) => {
3174
+ const selected = (blueprintColor ?? 'global') === option.id
3175
+ return (
3176
+ <button
3177
+ key={option.id}
3178
+ className={
3179
+ option.kind === 'global'
3180
+ ? 'hud-button hud-blueprint-option'
3181
+ : 'hud-button hud-blueprint-option hud-blueprint-option-swatch'
3182
+ }
3183
+ type="button"
3184
+ role="radio"
3185
+ aria-checked={selected}
3186
+ data-active={selected}
3187
+ aria-label={
3188
+ option.kind === 'global'
3189
+ ? 'Global blueprint'
3190
+ : `${option.name} session blueprint`
3191
+ }
3192
+ title={
3193
+ option.kind === 'global'
3194
+ ? 'Place on the global blueprint. All colors stay visible.'
3195
+ : `Place on the ${option.name} blueprint. All colors stay visible.`
3196
+ }
3197
+ style={
3198
+ {
3199
+ '--session-color': option.hex,
3200
+ } as CSSProperties
3201
+ }
3202
+ onClick={() => onSelectBlueprintColor(option.id)}
3203
+ >
3204
+ <SessionSwatch
3205
+ colorHex={option.hex}
3206
+ className="hud-session-swatch hud-blueprint-swatch"
3207
+ />
3208
+ {option.kind === 'global' ? (
3209
+ <span className="hud-blueprint-select-label">Global</span>
3210
+ ) : null}
3211
+ </button>
3212
+ )
3213
+ })}
3214
+ </div>
2941
3215
  )}
2942
3216
  {onToggleBlueprintHidden && (
2943
3217
  <button
@@ -2947,12 +3221,12 @@ export function HUD({
2947
3221
  aria-label={blueprintHidden ? 'Show blueprint' : 'Hide blueprint'}
2948
3222
  title={
2949
3223
  blueprintHidden
2950
- ? 'Show the shared blueprint overlay'
2951
- : 'Hide the shared blueprint overlay'
3224
+ ? 'Show this blueprint overlay'
3225
+ : 'Hide this blueprint overlay; other colors stay visible'
2952
3226
  }
2953
3227
  onClick={onToggleBlueprintHidden}
2954
3228
  >
2955
- {blueprintHidden ? 'Show blueprint' : 'Hide blueprint'}
3229
+ {blueprintHidden ? 'Show' : 'Hide'}
2956
3230
  </button>
2957
3231
  )}
2958
3232
  {onClearBlueprint && (
@@ -2964,7 +3238,7 @@ export function HUD({
2964
3238
  disabled={!blueprintHasContent}
2965
3239
  onClick={onClearBlueprint}
2966
3240
  >
2967
- Clear blueprint
3241
+ Clear
2968
3242
  </button>
2969
3243
  )}
2970
3244
  {onCleanupBlueprint && (
@@ -2976,7 +3250,7 @@ export function HUD({
2976
3250
  disabled={!blueprintCanCleanup}
2977
3251
  onClick={onCleanupBlueprint}
2978
3252
  >
2979
- Cleanup blueprint
3253
+ Cleanup
2980
3254
  </button>
2981
3255
  )}
2982
3256
  </div>
@@ -3020,48 +3294,6 @@ export function HUD({
3020
3294
  </span>
3021
3295
  </button>
3022
3296
  )}
3023
- {mapping && (
3024
- <button
3025
- className="hud-button hud-icon-button"
3026
- data-active={thumbnailVisible}
3027
- aria-label={
3028
- thumbnailVisible ? 'Hide 3D view' : 'Show 3D view'
3029
- }
3030
- aria-keyshortcuts="T"
3031
- aria-pressed={thumbnailVisible}
3032
- type="button"
3033
- onClick={() => {
3034
- setThumbnailVisible((visible) => {
3035
- if (!visible) {
3036
- setThumbnailMinimized(false)
3037
- setThumbnailMaximized(false)
3038
- } else {
3039
- setThumbnailMaximized(false)
3040
- }
3041
- return !visible
3042
- })
3043
- }}
3044
- >
3045
- <svg
3046
- viewBox="0 0 24 24"
3047
- width="18"
3048
- height="18"
3049
- fill="none"
3050
- stroke="currentColor"
3051
- strokeWidth="2"
3052
- strokeLinecap="round"
3053
- strokeLinejoin="round"
3054
- aria-hidden="true"
3055
- >
3056
- <rect x="3" y="5" width="11" height="14" rx="1.5" />
3057
- <path d="M16 8h5v11H9v-3" />
3058
- <path d="M6.5 16.5 9 12l2 2.5 1.5-2L15 16.5" />
3059
- </svg>
3060
- <span className="hud-tooltip">
3061
- {thumbnailVisible ? 'T hide 3D view' : 'T show 3D view'}
3062
- </span>
3063
- </button>
3064
- )}
3065
3297
  <button
3066
3298
  className="hud-button hud-icon-button"
3067
3299
  data-active={importedBy}
@@ -3139,30 +3371,70 @@ export function HUD({
3139
3371
  : 'G show branch changes'}
3140
3372
  </span>
3141
3373
  </button>
3142
- <button
3143
- className="hud-button hud-icon-button"
3144
- data-active={followLook}
3145
- aria-label="Make LLM look where I look"
3146
- aria-pressed={followLook}
3147
- type="button"
3148
- onClick={onToggleFollowLook}
3149
- >
3150
- <svg
3151
- viewBox="0 0 24 24"
3152
- width="18"
3153
- height="18"
3154
- fill="none"
3155
- stroke="currentColor"
3156
- strokeWidth="2"
3157
- strokeLinecap="round"
3158
- strokeLinejoin="round"
3159
- aria-hidden="true"
3374
+ <div className="hud-actions-menu" ref={actionsMenuRef}>
3375
+ <button
3376
+ className="hud-button hud-icon-button"
3377
+ data-active={actionsMenuOpen}
3378
+ type="button"
3379
+ aria-label="More actions"
3380
+ aria-haspopup="menu"
3381
+ aria-expanded={actionsMenuOpen}
3382
+ onClick={() => setActionsMenuOpen((open) => !open)}
3160
3383
  >
3161
- <path d="M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7-10-7-10-7Z" />
3162
- <circle cx="12" cy="12" r="3" />
3163
- </svg>
3164
- <span className="hud-tooltip">Make LLM look where I look</span>
3165
- </button>
3384
+ <svg
3385
+ viewBox="0 0 24 24"
3386
+ width="18"
3387
+ height="18"
3388
+ fill="none"
3389
+ stroke="currentColor"
3390
+ strokeWidth="2"
3391
+ strokeLinecap="round"
3392
+ strokeLinejoin="round"
3393
+ aria-hidden="true"
3394
+ >
3395
+ <path d="M4 7h16" />
3396
+ <path d="M4 12h16" />
3397
+ <path d="M4 17h16" />
3398
+ </svg>
3399
+ <span className="hud-tooltip">More</span>
3400
+ </button>
3401
+ {actionsMenuOpen &&
3402
+ actionsMenuPosition &&
3403
+ createPortal(
3404
+ <div
3405
+ className="hud-actions-menu-list"
3406
+ role="menu"
3407
+ style={actionsMenuPosition}
3408
+ >
3409
+ <button
3410
+ type="button"
3411
+ role="menuitem"
3412
+ aria-haspopup="dialog"
3413
+ aria-expanded={instructionsOpen}
3414
+ onClick={() => {
3415
+ setNoteEditor(null)
3416
+ setActionsMenuOpen(false)
3417
+ setInstructionsOpen((open) => !open)
3418
+ }}
3419
+ >
3420
+ Instructions
3421
+ </button>
3422
+ <button
3423
+ type="button"
3424
+ role="menuitem"
3425
+ aria-label="Rescan files and folders"
3426
+ disabled={updatingModel}
3427
+ onClick={() => {
3428
+ setActionsMenuOpen(false)
3429
+ onUpdateModel()
3430
+ }}
3431
+ >
3432
+ {updatingModel ? 'Updating…' : 'Update model'}
3433
+ </button>
3434
+ </div>,
3435
+ document.body,
3436
+ )}
3437
+ </div>
3166
3438
  </div>
3167
3439
  </div>
3168
3440
  </div>