@jkwd/inbase 0.1.20 → 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 (59) 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-ignore.mjs +9 -0
  7. package/apps/explorer/scripts/scan-target.mjs +22 -5
  8. package/apps/explorer/scripts/session-store.d.ts +86 -11
  9. package/apps/explorer/scripts/session-store.mjs +371 -58
  10. package/apps/explorer/scripts/target-config.d.ts +38 -3
  11. package/apps/explorer/scripts/target-config.mjs +147 -3
  12. package/apps/explorer/src/App.tsx +1080 -158
  13. package/apps/explorer/src/agentIntent.ts +61 -7
  14. package/apps/explorer/src/codebase.ts +1 -1
  15. package/apps/explorer/src/devTargets.ts +66 -0
  16. package/apps/explorer/src/explain.ts +312 -0
  17. package/apps/explorer/src/index.css +884 -223
  18. package/apps/explorer/src/layout.ts +55 -0
  19. package/apps/explorer/src/scene/DistantFileBlocks.tsx +4 -2
  20. package/apps/explorer/src/scene/FileBlock.tsx +95 -72
  21. package/apps/explorer/src/scene/FolderArea.tsx +55 -32
  22. package/apps/explorer/src/scene/MapView.tsx +506 -33
  23. package/apps/explorer/src/scene/RelationLines.tsx +7 -0
  24. package/apps/explorer/src/scene/World.tsx +146 -32
  25. package/apps/explorer/src/speech.ts +228 -0
  26. package/apps/explorer/src/theme.ts +29 -0
  27. package/apps/explorer/src/types.ts +98 -8
  28. package/apps/explorer/src/ui/CanvasErrorBoundary.tsx +3 -1
  29. package/apps/explorer/src/ui/ExplainAskCard.tsx +142 -0
  30. package/apps/explorer/src/ui/ExplainHud.tsx +524 -0
  31. package/apps/explorer/src/ui/ExplainInfoPanel.tsx +135 -0
  32. package/apps/explorer/src/ui/ExplainPointer.tsx +73 -0
  33. package/apps/explorer/src/ui/EyeIcon.tsx +38 -1
  34. package/apps/explorer/src/ui/HUD.tsx +1067 -792
  35. package/apps/explorer/src/ui/NameInput.tsx +114 -5
  36. package/apps/explorer/src/userContext.ts +0 -11
  37. package/apps/explorer/src/userCreated.ts +54 -1
  38. package/apps/explorer/vite.config.ts +173 -26
  39. package/bin/inbase.mjs +11 -2
  40. package/bin/project.mjs +1 -1
  41. package/bin/session.mjs +287 -38
  42. package/package.json +4 -1
  43. package/skill/commands/amber.md +23 -0
  44. package/skill/commands/blue.md +13 -0
  45. package/skill/commands/coral.md +23 -0
  46. package/skill/commands/explain.md +77 -0
  47. package/skill/commands/green.md +23 -0
  48. package/skill/commands/inbase.md +7 -5
  49. package/skill/commands/lime.md +23 -0
  50. package/skill/commands/orange.md +23 -0
  51. package/skill/commands/purple.md +23 -0
  52. package/skill/commands/red.md +23 -0
  53. package/skill/commands/skipinbase.md +1 -1
  54. package/skill/commands/violet.md +23 -0
  55. package/skill/commands/yellow.md +23 -0
  56. package/skill/inbase/SKILL.md +124 -76
  57. package/apps/explorer/src/scene/BlockPlacer.tsx +0 -78
  58. package/apps/explorer/src/scene/IslandPlacer.tsx +0 -31
  59. package/apps/explorer/src/scene/SelectionThumbnail.tsx +0 -1050
@@ -1,28 +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'
1
+ import { useEffect, useLayoutEffect, useRef, useState, type CSSProperties, type ReactNode } from 'react'
2
+ import { createPortal } from 'react-dom'
3
+ import { InfoNameField, NameInput } from './NameInput'
5
4
  import {
6
- canStopSession,
7
5
  isReviewingIntent,
8
6
  type AgentIntent,
9
7
  type AgentIntentStatus,
10
8
  type AimedRelation,
11
9
  type BlueprintNote,
12
10
  type BlueprintNoteKind,
11
+ GLOBAL_BLUEPRINT_COLOR,
12
+ type BlueprintOption,
13
13
  type BlueprintPointer,
14
14
  type BlueprintPointerKind,
15
15
  type BranchChanges,
16
16
  type CodebaseGraph,
17
+ type ExplainTargetKind,
17
18
  type PatchImportAddition,
18
19
  type PatchSymbolAddition,
19
20
  type ViewMode,
20
- type WorldLayout,
21
21
  type WorkflowAction,
22
22
  } from '../types'
23
23
  import { findBlueprintNote, findBlueprintPointer } from '../userCreated'
24
24
  import { EyeIcon } from './EyeIcon'
25
25
  import { beginKeyboardIsolation, shouldIgnoreShortcut } from '../keyboard'
26
+ import type { DevTargetsState } from '../devTargets'
26
27
 
27
28
  function reviewTitle(status: AgentIntentStatus) {
28
29
  if (status === 'blueprint_ask') return 'Setup blueprint'
@@ -42,14 +43,270 @@ function sessionLabel(intent: AgentIntent) {
42
43
  return intent.name?.trim() || intent.feature?.trim() || ''
43
44
  }
44
45
 
45
- function sessionTabLabel(intent: AgentIntent, sessions: AgentIntent[]) {
46
- const title = sessionLabel(intent) || reviewTitle(intent.status)
47
- const same = sessions.filter(
48
- (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>
49
309
  )
50
- if (same.length < 2) return title
51
- const id = intent.sessionId ?? ''
52
- return `${title} · ${id.slice(-4)}`
53
310
  }
54
311
 
55
312
  function fileBase(id: string) {
@@ -192,9 +449,15 @@ function MutationFold({
192
449
  function AddIntentRow({
193
450
  placeholder,
194
451
  onAdd,
452
+ pickLabel,
453
+ pickActive = false,
454
+ onTogglePick,
195
455
  }: {
196
456
  placeholder: string
197
457
  onAdd: (value: string) => boolean
458
+ pickLabel?: string
459
+ pickActive?: boolean
460
+ onTogglePick?: () => void
198
461
  }) {
199
462
  const [value, setValue] = useState('')
200
463
  return (
@@ -217,6 +480,17 @@ function AddIntentRow({
217
480
  <button className="hud-button" type="submit">
218
481
  Add
219
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
+ )}
220
494
  </form>
221
495
  )
222
496
  }
@@ -314,54 +588,71 @@ function BlueprintSymbolRow({
314
588
  className,
315
589
  hasNote,
316
590
  noteOpen,
317
- pointed,
318
591
  canEdit,
319
592
  canRemove,
320
593
  onRemove,
321
594
  onOpenNote,
595
+ onExplain,
596
+ pointerTarget,
597
+ colorPointers,
598
+ currentColorId,
322
599
  onTogglePoint,
323
600
  }: {
324
601
  name: string
325
602
  className?: string
326
603
  hasNote: boolean
327
604
  noteOpen?: boolean
328
- pointed?: boolean
329
605
  canEdit: boolean
330
606
  canRemove?: boolean
331
607
  onRemove?: () => void
332
608
  onOpenNote: () => void
333
- 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
334
618
  }) {
619
+ const showActions = Boolean(onExplain) || canEdit
335
620
  return (
336
621
  <li>
337
622
  <span className={className}>{name}</span>
338
- {canEdit && (
623
+ {showActions && (
339
624
  <div className="hud-item-actions">
340
- {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 && (
341
644
  <button
342
- className="hud-item-point"
645
+ className="hud-item-note"
343
646
  type="button"
344
- data-pointed={pointed ? 'true' : 'false'}
345
- aria-label={
346
- pointed ? `Stop pointing to ${name}` : `Point to ${name}`
347
- }
348
- aria-pressed={Boolean(pointed)}
349
- 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}
350
651
  >
351
- <EyeIcon size={13} />
652
+ Note
352
653
  </button>
353
654
  )}
354
- <button
355
- className="hud-item-note"
356
- type="button"
357
- data-has-note={hasNote ? 'true' : 'false'}
358
- data-open={noteOpen ? 'true' : 'false'}
359
- aria-label={`Edit note for ${name}`}
360
- onClick={onOpenNote}
361
- >
362
- Note
363
- </button>
364
- {canRemove && (
655
+ {canEdit && canRemove && (
365
656
  <button
366
657
  className="hud-item-remove"
367
658
  type="button"
@@ -390,6 +681,35 @@ function AttachStateBadge({ attached }: { attached: boolean }) {
390
681
  )
391
682
  }
392
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
+
393
713
  function PanelChrome({
394
714
  title,
395
715
  subtitle,
@@ -397,8 +717,8 @@ function PanelChrome({
397
717
  minimized = false,
398
718
  onMinimize,
399
719
  onClose,
400
- closeLabel = 'Close',
401
- closeReject = false,
720
+ onExplain,
721
+ explainLabel,
402
722
  }: {
403
723
  title: ReactNode
404
724
  subtitle?: ReactNode
@@ -406,14 +726,22 @@ function PanelChrome({
406
726
  minimized?: boolean
407
727
  onMinimize?: () => void
408
728
  onClose?: () => void
409
- closeLabel?: string
410
- closeReject?: boolean
729
+ onExplain?: () => void
730
+ explainLabel?: string
411
731
  }) {
412
732
  return (
413
733
  <div className="hud-panel-chrome">
414
734
  <div className="hud-panel-chrome-heading">
415
735
  <div className="hud-panel-chrome-title-row">
416
- <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>
417
745
  {badge}
418
746
  </div>
419
747
  {subtitle ? (
@@ -433,13 +761,9 @@ function PanelChrome({
433
761
  )}
434
762
  {onClose && (
435
763
  <button
436
- className={
437
- closeReject
438
- ? 'hud-button hud-icon-button hud-panel-control hud-button-reject'
439
- : 'hud-button hud-icon-button hud-panel-control'
440
- }
764
+ className="hud-button hud-icon-button hud-panel-control"
441
765
  type="button"
442
- aria-label={closeLabel}
766
+ aria-label="Close"
443
767
  onClick={onClose}
444
768
  >
445
769
  ×
@@ -450,142 +774,9 @@ function PanelChrome({
450
774
  )
451
775
  }
452
776
 
453
- function InitialInstructionField({
454
- value,
455
- onChange,
456
- }: {
457
- value: string
458
- onChange: (value: string) => void
459
- }) {
460
- return (
461
- <label className="hud-instruction">
462
- <textarea
463
- value={value}
464
- maxLength={4000}
465
- rows={4}
466
- placeholder="What should the LLM build?"
467
- onChange={(event) => onChange(event.target.value)}
468
- onKeyDown={(event) => event.stopPropagation()}
469
- />
470
- </label>
471
- )
472
- }
473
-
474
- function formatFileSize(bytes: number) {
475
- if (bytes < 1024) return `${bytes} B`
476
- if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`
477
- return `${(bytes / (1024 * 1024)).toFixed(1)} MB`
478
- }
479
-
480
- async function fileToBase64(file: File) {
481
- const bytes = new Uint8Array(await file.arrayBuffer())
482
- const chunk = 0x8000
483
- let binary = ''
484
- for (let offset = 0; offset < bytes.length; offset += chunk) {
485
- binary += String.fromCharCode(...bytes.subarray(offset, offset + chunk))
486
- }
487
- return btoa(binary)
488
- }
489
-
490
- type ContextFileInfo = {
491
- id: string
492
- name: string
493
- mimeType: string
494
- size: number
495
- }
496
-
497
- function ContextFileDrop({
498
- files,
499
- busy,
500
- error,
501
- onAdd,
502
- onRemove,
503
- }: {
504
- files: ContextFileInfo[]
505
- busy: boolean
506
- error: string | null
507
- onAdd: (files: File[]) => void
508
- onRemove: (fileId: string) => void
509
- }) {
510
- const inputRef = useRef<HTMLInputElement>(null)
511
- const [over, setOver] = useState(false)
512
-
513
- const takeFiles = (list: FileList | File[] | null) => {
514
- if (!list || busy) return
515
- const next = [...list].filter((file) => file.size > 0)
516
- if (next.length > 0) onAdd(next)
517
- }
518
-
519
- return (
520
- <div className="hud-context">
521
- <button
522
- className="hud-context-drop"
523
- type="button"
524
- data-over={over}
525
- data-busy={busy}
526
- disabled={busy}
527
- aria-label="Attach files for the LLM"
528
- onDragEnter={(event) => {
529
- event.preventDefault()
530
- if (event.dataTransfer.types.includes('Files')) setOver(true)
531
- }}
532
- onDragOver={(event) => {
533
- event.preventDefault()
534
- event.dataTransfer.dropEffect = 'copy'
535
- }}
536
- onDragLeave={(event) => {
537
- if (event.currentTarget.contains(event.relatedTarget as Node)) return
538
- setOver(false)
539
- }}
540
- onDrop={(event) => {
541
- event.preventDefault()
542
- event.stopPropagation()
543
- setOver(false)
544
- takeFiles(event.dataTransfer.files)
545
- }}
546
- onKeyDown={(event) => event.stopPropagation()}
547
- onClick={() => inputRef.current?.click()}
548
- >
549
- <input
550
- ref={inputRef}
551
- type="file"
552
- multiple
553
- hidden
554
- onChange={(event) => {
555
- takeFiles(event.target.files)
556
- event.target.value = ''
557
- }}
558
- />
559
- {busy ? 'Attaching…' : 'Drop files or click to attach'}
560
- </button>
561
- {files.length > 0 && (
562
- <ul className="hud-context-list">
563
- {files.map((file) => (
564
- <li key={file.id}>
565
- <span className="hud-context-name" title={file.name}>
566
- {file.name}
567
- </span>
568
- <span className="hud-context-size">{formatFileSize(file.size)}</span>
569
- <button
570
- className="hud-item-remove"
571
- type="button"
572
- aria-label={`Remove ${file.name}`}
573
- disabled={busy}
574
- onClick={() => onRemove(file.id)}
575
- >
576
- ×
577
- </button>
578
- </li>
579
- ))}
580
- </ul>
581
- )}
582
- {error ? <p className="hud-context-error">{error}</p> : null}
583
- </div>
584
- )
585
- }
586
-
587
777
  function blueprintIsDefined(intent: AgentIntent) {
588
778
  return (
779
+ Boolean(intent.localBlueprintEnabled) ||
589
780
  (intent.userCreatedBlocks?.length ?? 0) > 0 ||
590
781
  (intent.userCreatedIslands?.length ?? 0) > 0 ||
591
782
  (intent.blueprintFunctions?.length ?? 0) > 0 ||
@@ -597,56 +788,18 @@ function blueprintIsDefined(intent: AgentIntent) {
597
788
  }
598
789
 
599
790
  function HandshakeSetup({
600
- instruction,
601
- onInstructionChange,
602
- contextFiles,
603
- contextBusy,
604
- contextError,
605
- onAddContextFiles,
606
- onRemoveContextFile,
607
791
  blueprintDefined,
608
792
  awaitingAttach,
609
793
  nextAttachLabel,
794
+ colorCommand,
610
795
  }: {
611
- instruction: string
612
- onInstructionChange: (value: string) => void
613
- contextFiles: ContextFileInfo[]
614
- contextBusy: boolean
615
- contextError: string | null
616
- onAddContextFiles: (files: File[]) => void
617
- onRemoveContextFile: (fileId: string) => void
618
796
  blueprintDefined: boolean
619
797
  awaitingAttach: boolean
620
798
  nextAttachLabel: string | null
799
+ colorCommand?: string | null
621
800
  }) {
622
801
  return (
623
802
  <div className="hud-setup">
624
- <section
625
- className="hud-setup-section"
626
- onDragOver={(event) => {
627
- if (event.dataTransfer.types.includes('Files')) event.preventDefault()
628
- }}
629
- onDrop={(event) => {
630
- event.preventDefault()
631
- const dropped = [...(event.dataTransfer.files ?? [])].filter(
632
- (file) => file.size > 0,
633
- )
634
- if (dropped.length > 0) onAddContextFiles(dropped)
635
- }}
636
- >
637
- <h2 className="hud-setup-heading">Instructions</h2>
638
- <InitialInstructionField
639
- value={instruction}
640
- onChange={onInstructionChange}
641
- />
642
- <ContextFileDrop
643
- files={contextFiles}
644
- busy={contextBusy}
645
- error={contextError}
646
- onAdd={onAddContextFiles}
647
- onRemove={onRemoveContextFile}
648
- />
649
- </section>
650
803
  <section className="hud-setup-section">
651
804
  <h2 className="hud-setup-heading">
652
805
  Blueprint
@@ -658,27 +811,32 @@ function HandshakeSetup({
658
811
  </span>
659
812
  </h2>
660
813
  <p>
661
- Press <kbd>Space</kbd> for a file and <kbd>B</kbd> for an island.
662
- On the map, right-click to add a file or folder. Open a file's info
663
- panel to add functions, vars, and notes (instructions or pseudo
664
- 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.
665
818
  </p>
666
819
  </section>
667
820
  <section className="hud-setup-section">
668
821
  <h2 className="hud-setup-heading">Start</h2>
669
822
  {awaitingAttach && nextAttachLabel ? (
670
- <p>
671
- <kbd>/inbase</kbd> attaches {nextAttachLabel} first. This session
672
- stays in the queue.
673
- </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
+ </>
674
830
  ) : awaitingAttach ? (
675
- <p>
676
- Run <kbd>/inbase</kbd> in a Cursor chat to connect and start.
677
- </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
+ </>
678
838
  ) : (
679
- <p>
680
- This window is attached. Starting from <kbd>/inbase</kbd>…
681
- </p>
839
+ <p>This window is attached. Starting from the Cursor chat…</p>
682
840
  )}
683
841
  </section>
684
842
  </div>
@@ -700,7 +858,16 @@ function sessionLiveStatus(intent: AgentIntent) {
700
858
  return { text: 'LLM wait timed out', busy: false }
701
859
  }
702
860
  if (intent.awaitingAttach) {
703
- 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 }
704
871
  }
705
872
  if (intent.status === 'pending') {
706
873
  return intent.listening
@@ -742,9 +909,6 @@ function sessionLiveStatus(intent: AgentIntent) {
742
909
  ) {
743
910
  return { text: 'LLM attached', busy: true }
744
911
  }
745
- if (intent.llmIdle) {
746
- return { text: 'LLM is idle', busy: false }
747
- }
748
912
  return { text: 'LLM connected', busy: true }
749
913
  }
750
914
 
@@ -752,12 +916,16 @@ function LiveStatus({
752
916
  intent,
753
917
  showStop = false,
754
918
  onStop,
919
+ startingExplain = false,
755
920
  }: {
756
921
  intent: AgentIntent
757
922
  showStop?: boolean
758
923
  onStop?: () => void
924
+ startingExplain?: boolean
759
925
  }) {
760
- const status = sessionLiveStatus(intent)
926
+ const status = startingExplain && !intent.pendingExplain
927
+ ? { text: 'Starting an explanation…', busy: true }
928
+ : sessionLiveStatus(intent)
761
929
  const [flash, setFlash] = useState(false)
762
930
  const lastAt = intent.lastAck?.at
763
931
 
@@ -785,6 +953,12 @@ function LiveStatus({
785
953
  )
786
954
  }
787
955
 
956
+ function PlaceFilesHint() {
957
+ return (
958
+ <p className="hud-place-hint">Right-click to create files and folders.</p>
959
+ )
960
+ }
961
+
788
962
  type SessionPanelProps = {
789
963
  intent: AgentIntent
790
964
  focused: boolean
@@ -810,14 +984,7 @@ function SessionPanel({
810
984
  }: SessionPanelProps) {
811
985
  const [minimized, setMinimized] = useState(false)
812
986
  const [instruction, setInstruction] = useState('')
813
- const [initialInstruction, setInitialInstruction] = useState(
814
- () => intent.initialInstruction ?? '',
815
- )
816
- const [contextFiles, setContextFiles] = useState<ContextFileInfo[]>(
817
- () => intent.contextFiles ?? [],
818
- )
819
- const [contextBusy, setContextBusy] = useState(false)
820
- const [contextError, setContextError] = useState<string | null>(null)
987
+ const [startingExplain, setStartingExplain] = useState(false)
821
988
  const sessionId = intent.sessionId
822
989
  const latestEntry = intent.isActiveDiff ? intent.chain.at(-1) : null
823
990
  const pending =
@@ -862,8 +1029,10 @@ function SessionPanel({
862
1029
  planReady && !working && proposalStep === null
863
1030
  ? (intent.steps.find((step) => !acceptedSteps.has(step.index)) ?? null)
864
1031
  : null
865
- const canRunNext = stepByStep && Boolean(invokeStep)
866
- 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
867
1036
  const lastStep =
868
1037
  typeof proposalStep === 'number' &&
869
1038
  intent.steps.length > 0 &&
@@ -876,75 +1045,29 @@ function SessionPanel({
876
1045
 
877
1046
  useEffect(() => {
878
1047
  setInstruction('')
879
- }, [intent.diffId])
880
-
881
- useEffect(() => {
882
- setInitialInstruction(intent.initialInstruction ?? '')
883
- setContextFiles(intent.contextFiles ?? [])
884
- setContextError(null)
885
- }, [sessionId])
1048
+ setStartingExplain(false)
1049
+ }, [intent.diffId, sessionId])
886
1050
 
887
1051
  useEffect(() => {
888
- if (contextBusy) return
889
- setContextFiles(intent.contextFiles ?? [])
890
- }, [contextBusy, intent.contextFiles])
1052
+ if (intent.pendingExplain) setStartingExplain(false)
1053
+ }, [intent.pendingExplain])
891
1054
 
892
1055
  if (!sessionId || !isReviewingIntent(intent.status)) return null
893
1056
 
894
- const updateInitialInstruction = (value: string) => {
895
- setInitialInstruction(value)
896
- persistInitialInstruction(sessionId, value)
897
- }
898
-
899
- const addContextFiles = (files: File[]) => {
900
- setContextBusy(true)
901
- setContextError(null)
902
- void Promise.all(
903
- files.map(async (file) => ({
904
- name: file.name,
905
- mimeType: file.type || 'application/octet-stream',
906
- contentBase64: await fileToBase64(file),
907
- })),
908
- )
909
- .then((payload) => persistAddContextFiles(sessionId, payload))
910
- .then((next) => {
911
- setContextFiles(next.contextFiles ?? [])
912
- })
913
- .catch((caught) => {
914
- setContextError(
915
- caught instanceof Error ? caught.message : 'Could not attach files',
916
- )
917
- })
918
- .finally(() => setContextBusy(false))
919
- }
920
-
921
- const removeContextFile = (fileId: string) => {
922
- setContextBusy(true)
923
- setContextError(null)
924
- void persistRemoveContextFile(sessionId, fileId)
925
- .then((next) => {
926
- setContextFiles(next.contextFiles ?? [])
927
- })
928
- .catch((caught) => {
929
- setContextError(
930
- caught instanceof Error ? caught.message : 'Could not remove file',
931
- )
932
- })
933
- .finally(() => setContextBusy(false))
934
- }
935
-
936
- const showInitialInstruction =
1057
+ const handshakeSetup =
937
1058
  Boolean(intent.awaitingAttach) &&
938
1059
  (askingBlueprint || sendingBlueprint || preparing)
939
- const handshakeSetup = showInitialInstruction
940
1060
  const llmConnected = intent.awaitingAttach === false
941
1061
  const showConnectedProgress =
942
- 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
943
1066
  const queuedBehind =
944
1067
  intent.awaitingAttach &&
945
1068
  nextAttachSession &&
946
1069
  nextAttachSession.sessionId !== sessionId
947
- ? sessionLabel(nextAttachSession) || 'an earlier session'
1070
+ ? sessionDisplayName(nextAttachSession) || 'an earlier session'
948
1071
  : null
949
1072
 
950
1073
  const act = (
@@ -961,50 +1084,64 @@ function SessionPanel({
961
1084
  }
962
1085
  data-minimized={minimized}
963
1086
  data-focused={focused}
964
- data-attached={llmConnected}
1087
+ data-attached={llmConnected && !llmDisconnected}
965
1088
  onPointerDown={onFocus}
966
1089
  >
967
1090
  <PanelChrome
968
1091
  title={
969
- sessionLabel(intent) ||
970
- (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
+ </>
971
1101
  }
972
1102
  subtitle={
973
1103
  sessionLabel(intent)
974
1104
  ? showConnectedProgress
975
1105
  ? 'LLM connected'
976
1106
  : reviewTitle(intent.status)
977
- : undefined
1107
+ : sessionColorName(intent)
1108
+ ? showConnectedProgress
1109
+ ? 'LLM connected'
1110
+ : reviewTitle(intent.status)
1111
+ : undefined
978
1112
  }
979
- badge={<AttachStateBadge attached={llmConnected} />}
1113
+ badge={<AttachStateBadge attached={llmConnected && !llmDisconnected} />}
980
1114
  minimized={minimized}
981
1115
  onMinimize={() => setMinimized((current) => !current)}
982
- onClose={() => act('stop')}
983
- closeLabel="Stop session"
984
- closeReject
985
1116
  />
986
1117
  {!minimized && (
987
1118
  <>
988
1119
  {!intent.awaitingAttach && (
989
- <LiveStatus
990
- intent={intent}
991
- showStop={showConnectedProgress || working}
992
- onStop={() => act('stop')}
993
- />
1120
+ <>
1121
+ {showPlaceHint && !planReady && !pending && <PlaceFilesHint />}
1122
+ <LiveStatus
1123
+ intent={intent}
1124
+ showStop={liveHasStop}
1125
+ startingExplain={startingExplain}
1126
+ onStop={() => act('stop')}
1127
+ />
1128
+ </>
994
1129
  )}
995
- <label className="hud-mode-switch">
996
- <span>Step by step</span>
997
- <button
998
- className="hud-switch"
999
- type="button"
1000
- role="switch"
1001
- aria-checked={stepByStep}
1002
- aria-label="Step by step"
1003
- onKeyDown={(event) => event.stopPropagation()}
1004
- onClick={() => act('set_step_by_step', { stepByStep: !stepByStep })}
1005
- />
1006
- </label>
1007
- {!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 && (
1008
1145
  <p className="hud-mode-hint">
1009
1146
  LLM implements the full plan. You can still walk the diffs, then
1010
1147
  Accept proposal.
@@ -1012,41 +1149,49 @@ function SessionPanel({
1012
1149
  )}
1013
1150
  {handshakeSetup ? (
1014
1151
  <HandshakeSetup
1015
- instruction={initialInstruction}
1016
- onInstructionChange={updateInitialInstruction}
1017
- contextFiles={contextFiles}
1018
- contextBusy={contextBusy}
1019
- contextError={contextError}
1020
- onAddContextFiles={addContextFiles}
1021
- onRemoveContextFile={removeContextFile}
1022
1152
  blueprintDefined={blueprintIsDefined(intent)}
1023
1153
  awaitingAttach={Boolean(intent.awaitingAttach)}
1024
1154
  nextAttachLabel={queuedBehind}
1155
+ colorCommand={sessionSlashCommand(intent)}
1025
1156
  />
1026
1157
  ) : intent.awaitingAttach ? (
1027
1158
  queuedBehind ? (
1028
- <p className="hud-mode-hint">
1029
- <kbd>/inbase</kbd> attaches {queuedBehind} first. This session
1030
- stays in the queue.
1031
- </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>
1032
1169
  ) : (
1033
- <p className="hud-mode-hint">
1034
- No LLM is attached. Open a Cursor chat and run{' '}
1035
- <kbd>/inbase</kbd>. It connects to the next waiting session.
1036
- </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>
1037
1177
  )
1038
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}
1039
1184
  {intent.feature &&
1040
1185
  !handshakeSetup &&
1041
1186
  intent.feature.trim() !== sessionLabel(intent) && (
1042
1187
  <p className="hud-feature">{intent.feature}</p>
1043
1188
  )}
1044
- {askingBlueprint && !handshakeSetup && !showConnectedProgress ? (
1189
+ {askingBlueprint && !handshakeSetup && !showConnectedProgress && !llmDisconnected ? (
1045
1190
  <>
1046
1191
  <p>
1047
- A shared blueprint is already available on the map. Send this
1048
- session to the LLM, or skip and let it continue with the current
1049
- 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.
1050
1195
  </p>
1051
1196
  <div className="hud-decide">
1052
1197
  <button
@@ -1075,20 +1220,13 @@ function SessionPanel({
1075
1220
  ) : handshakeSetup ? null : intent.status === 'finished' ? (
1076
1221
  <p>All plan steps were applied.</p>
1077
1222
  ) : showConnectedProgress || preparing ? null : (
1078
- <p>
1223
+ <p className="hud-step-label">
1079
1224
  {stepLabel}
1080
1225
  {intent.reason ? ` · ${intent.reason}` : ''}
1081
1226
  </p>
1082
1227
  )}
1083
1228
  {!askingBlueprint && !sendingBlueprint && (
1084
1229
  <>
1085
- {canPlace && !intent.working && (
1086
- <p>
1087
- On the map, right-click to add a file or folder.{' '}
1088
- <kbd>Space</kbd> places a file, <kbd>B</kbd> an island while
1089
- walking.
1090
- </p>
1091
- )}
1092
1230
  {intent.steps?.length > 0 && (
1093
1231
  <ol className="hud-steps">
1094
1232
  {intent.steps.map((step) => {
@@ -1096,6 +1234,8 @@ function SessionPanel({
1096
1234
  const processing = processingStep === step.index
1097
1235
  const accepted = acceptedSteps.has(step.index) && !proposed
1098
1236
  const creating = processing && !proposed
1237
+ const explaining =
1238
+ startingExplain || Boolean(intent.pendingExplain)
1099
1239
  const showStepAction =
1100
1240
  creating ||
1101
1241
  (canRunNext && invokeStep?.index === step.index) ||
@@ -1110,29 +1250,54 @@ function SessionPanel({
1110
1250
  <span className="hud-step-main">
1111
1251
  <span className="hud-step-title">{step.title}</span>
1112
1252
  {showStepAction && (
1113
- <button
1114
- className="hud-button hud-button-approve hud-run-step"
1115
- type="button"
1116
- disabled={creating}
1117
- aria-busy={creating}
1118
- onClick={() =>
1119
- proposed
1120
- ? lastStep
1121
- ? 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
+ })
1122
1266
  : act('invoke', {
1123
- step: step.index + 1,
1267
+ step: step.index,
1124
1268
  })
1125
- : act('invoke', {
1126
- 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)
1127
1292
  })
1128
- }
1129
- >
1130
- {proposed
1131
- ? 'Accept proposal'
1132
- : creating
1133
- ? 'Creating proposal…'
1134
- : 'Create proposal'}
1135
- </button>
1293
+ }}
1294
+ >
1295
+ {explaining
1296
+ ? 'Starting explanation…'
1297
+ : 'Explain proposal'}
1298
+ </button>
1299
+ )}
1300
+ </span>
1136
1301
  )}
1137
1302
  </span>
1138
1303
  </li>
@@ -1199,7 +1364,7 @@ function SessionPanel({
1199
1364
  {(intent.createFolders ?? []).length > 0 && (
1200
1365
  <>
1201
1366
  <div className="hud-section-title hud-section-title-add">
1202
- Added islands
1367
+ Added folders
1203
1368
  </div>
1204
1369
  <ul>
1205
1370
  {intent.createFolders.map((id) => (
@@ -1275,17 +1440,20 @@ function SessionPanel({
1275
1440
  )}
1276
1441
  </MutationFold>
1277
1442
  {planReady && (
1278
- <div className="hud-decide">
1279
- <button
1280
- className="hud-button hud-button-reject"
1281
- type="button"
1282
- onClick={() => act('stop')}
1283
- >
1284
- Stop
1285
- </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>
1286
1454
  </div>
1287
1455
  )}
1288
- {pending && (
1456
+ {pending && !llmDisconnected && (
1289
1457
  <>
1290
1458
  <label className="hud-instruction">
1291
1459
  <span>Alternative instruction for the LLM</span>
@@ -1298,24 +1466,27 @@ function SessionPanel({
1298
1466
  onKeyDown={(event) => event.stopPropagation()}
1299
1467
  />
1300
1468
  </label>
1301
- <div className="hud-decide">
1302
- <button
1303
- className="hud-button hud-button-extend"
1304
- type="button"
1305
- disabled={!instruction.trim()}
1306
- onClick={() =>
1307
- act('instruct', { instruction })
1308
- }
1309
- >
1310
- Send instruction
1311
- </button>
1312
- <button
1313
- className="hud-button hud-button-reject"
1314
- type="button"
1315
- onClick={() => act('stop')}
1316
- >
1317
- Stop
1318
- </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>
1319
1490
  </div>
1320
1491
  </>
1321
1492
  )}
@@ -1390,7 +1561,7 @@ function BranchChangesPanel({
1390
1561
  {(changes.createFolders ?? []).length > 0 && (
1391
1562
  <>
1392
1563
  <div className="hud-section-title hud-section-title-add">
1393
- Added islands
1564
+ Added folders
1394
1565
  </div>
1395
1566
  <ul>
1396
1567
  {changes.createFolders.map((id) => (
@@ -1458,7 +1629,7 @@ type ExplorerInstruction = {
1458
1629
  label: string
1459
1630
  }
1460
1631
 
1461
- type InstructionView = 'walk' | '3dview' | 'map'
1632
+ type InstructionView = 'walk' | 'map'
1462
1633
 
1463
1634
  type ExplorerInstructionSection = {
1464
1635
  id: InstructionView
@@ -1491,10 +1662,7 @@ function explorerInstructions({
1491
1662
  changePathsOnly,
1492
1663
  selectedUserCreated,
1493
1664
  infoVisible,
1494
- thumbnailVisible,
1495
1665
  importedBy,
1496
- canStop,
1497
- sessionCount,
1498
1666
  showBranchChanges,
1499
1667
  canShowBranchChanges,
1500
1668
  }: {
@@ -1503,10 +1671,7 @@ function explorerInstructions({
1503
1671
  changePathsOnly: boolean
1504
1672
  selectedUserCreated: boolean
1505
1673
  infoVisible: boolean
1506
- thumbnailVisible: boolean
1507
1674
  importedBy: boolean
1508
- canStop: boolean
1509
- sessionCount: number
1510
1675
  showBranchChanges: boolean
1511
1676
  canShowBranchChanges: boolean
1512
1677
  }): ExplorerInstructionSection[] {
@@ -1540,23 +1705,6 @@ function explorerInstructions({
1540
1705
  },
1541
1706
  ]
1542
1707
  : []
1543
- const stop: ExplorerInstruction[] = canStop
1544
- ? [
1545
- {
1546
- id: 'stop',
1547
- keys: ['Stop'],
1548
- label:
1549
- sessionCount > 1
1550
- ? 'Ends the focused LLM session'
1551
- : 'Ends this LLM session',
1552
- },
1553
- ]
1554
- : []
1555
- const thumbnail: ExplorerInstruction = {
1556
- id: 'thumbnail',
1557
- keys: ['T'],
1558
- label: thumbnailVisible ? 'Hide 3D view' : 'Show 3D view',
1559
- }
1560
1708
  return [
1561
1709
  {
1562
1710
  id: 'walk',
@@ -1567,8 +1715,6 @@ function explorerInstructions({
1567
1715
  { id: 'shift', keys: ['Shift'], label: 'Sprint' },
1568
1716
  ...(canPlace
1569
1717
  ? [
1570
- { id: 'space', keys: ['Space'], label: 'Place file' },
1571
- { id: 'b-island', keys: ['B'], label: 'Place island' },
1572
1718
  {
1573
1719
  id: 'point-to',
1574
1720
  keys: ['Point to'],
@@ -1580,7 +1726,7 @@ function explorerInstructions({
1580
1726
  {
1581
1727
  id: 'dblclick-info',
1582
1728
  keys: ['Double-click'],
1583
- label: 'A block for info',
1729
+ label: 'A file or folder for info',
1584
1730
  },
1585
1731
  { id: 'aim-line', keys: ['Click'], label: 'Aim a line to fly' },
1586
1732
  ...info,
@@ -1589,26 +1735,33 @@ function explorerInstructions({
1589
1735
  {
1590
1736
  id: 'update-model',
1591
1737
  keys: ['Update model'],
1592
- 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',
1593
1745
  },
1594
1746
  {
1595
- id: 'setup-session',
1596
- keys: ['Setup LLM session'],
1597
- 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',
1598
1751
  },
1599
1752
  {
1600
1753
  id: 'blueprint-toggle',
1601
- keys: ['Hide/Show blueprint'],
1602
- label: 'Toggle the shared blueprint overlay',
1754
+ keys: ['Hide/Show'],
1755
+ label: 'Hide this color; other blueprint colors stay visible',
1603
1756
  },
1604
1757
  {
1605
1758
  id: 'blueprint-clear',
1606
- keys: ['Clear blueprint'],
1759
+ keys: ['Clear'],
1607
1760
  label: 'Remove every planned file and folder',
1608
1761
  },
1609
1762
  {
1610
1763
  id: 'blueprint-cleanup',
1611
- keys: ['Cleanup blueprint'],
1764
+ keys: ['Cleanup'],
1612
1765
  label: 'Drop blueprint files and folders that already exist',
1613
1766
  },
1614
1767
  { id: 'toggle-map', keys: ['M'], label: 'Toggle map' },
@@ -1617,26 +1770,6 @@ function explorerInstructions({
1617
1770
  keys: ['Double-click', 'Esc'],
1618
1771
  label: 'Release mouse',
1619
1772
  },
1620
- ...stop,
1621
- ],
1622
- },
1623
- {
1624
- id: '3dview',
1625
- title: '3D view',
1626
- items: [
1627
- { id: 'scroll-zoom', keys: ['Scroll'], label: 'Zoom' },
1628
- { id: 'drag-pan', keys: ['Drag'], label: 'Pan' },
1629
- {
1630
- id: 'cmd-drag-rotate',
1631
- keys: ['⌘', 'Ctrl', 'Drag'],
1632
- label: 'Rotate',
1633
- },
1634
- {
1635
- id: 'dblclick-reset',
1636
- keys: ['Double-click'],
1637
- label: 'Reset view',
1638
- },
1639
- thumbnail,
1640
1773
  ],
1641
1774
  },
1642
1775
  {
@@ -1645,26 +1778,26 @@ function explorerInstructions({
1645
1778
  items: [
1646
1779
  { id: 'scroll-zoom', keys: ['Scroll'], label: 'Zoom' },
1647
1780
  { id: 'drag-pan', keys: ['Drag'], label: 'Pan' },
1648
- { id: 'click-block', keys: ['Click'], label: 'A block for info' },
1781
+ { id: 'click-block', keys: ['Click'], label: 'A file for info' },
1649
1782
  {
1650
1783
  id: 'click-island',
1651
1784
  keys: ['Click'],
1652
- label: 'An island for its files',
1785
+ label: 'A folder for its files',
1653
1786
  },
1654
1787
  ...(canPlace
1655
1788
  ? [
1656
- { id: 'select-island', keys: ['Click'], label: 'Select an island' },
1789
+ { id: 'select-island', keys: ['Click'], label: 'Select a folder' },
1657
1790
  {
1658
1791
  id: 'add-file-folder',
1659
1792
  keys: ['Right-click'],
1660
- label: 'Add file or folder, or point to a folder',
1793
+ label: 'Create a file or folder, or point to a folder',
1661
1794
  },
1662
1795
  ]
1663
1796
  : []),
1664
1797
  {
1665
1798
  id: 'option-click-walk',
1666
1799
  keys: ['Option', 'Click'],
1667
- label: 'An island to walk',
1800
+ label: 'A folder to walk',
1668
1801
  },
1669
1802
  {
1670
1803
  id: 'gold-pin',
@@ -1684,36 +1817,41 @@ function explorerInstructions({
1684
1817
  : []),
1685
1818
  ...backspace,
1686
1819
  ...info,
1687
- thumbnail,
1688
1820
  imported,
1689
1821
  ...branch,
1690
1822
  {
1691
1823
  id: 'update-model',
1692
1824
  keys: ['Update model'],
1693
- label: 'Rescan and rebuild the map',
1825
+ label: 'Rescan files and folders',
1694
1826
  },
1695
1827
  {
1696
- id: 'setup-session',
1697
- keys: ['Setup LLM session'],
1698
- 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',
1699
1838
  },
1700
1839
  {
1701
1840
  id: 'blueprint-toggle',
1702
- keys: ['Hide/Show blueprint'],
1703
- label: 'Toggle the shared blueprint overlay',
1841
+ keys: ['Hide/Show'],
1842
+ label: 'Hide this color; other blueprint colors stay visible',
1704
1843
  },
1705
1844
  {
1706
1845
  id: 'blueprint-clear',
1707
- keys: ['Clear blueprint'],
1846
+ keys: ['Clear'],
1708
1847
  label: 'Remove every planned file and folder',
1709
1848
  },
1710
1849
  {
1711
1850
  id: 'blueprint-cleanup',
1712
- keys: ['Cleanup blueprint'],
1851
+ keys: ['Cleanup'],
1713
1852
  label: 'Drop blueprint files and folders that already exist',
1714
1853
  },
1715
1854
  { id: 'map-walk', keys: ['M'], label: 'Back to walk' },
1716
- ...stop,
1717
1855
  ],
1718
1856
  },
1719
1857
  ]
@@ -1721,14 +1859,12 @@ function explorerInstructions({
1721
1859
 
1722
1860
  type HUDProps = {
1723
1861
  graph: CodebaseGraph
1724
- layout: WorldLayout
1725
1862
  mode: ViewMode
1726
1863
  locked: boolean
1727
1864
  selectedId: string | null
1728
1865
  selectedTick?: number
1729
1866
  inspectTick?: number
1730
1867
  selectedFolder?: string | null
1731
- landAt: [number, number]
1732
1868
  aimedRelation: AimedRelation | null
1733
1869
  aimedFileId?: string | null
1734
1870
  intent: AgentIntent
@@ -1736,7 +1872,6 @@ type HUDProps = {
1736
1872
  focusedSessionId?: string | null
1737
1873
  nextAttachSessionId?: string | null
1738
1874
  onFocusSession?: (sessionId: string) => void
1739
- onSetupSession?: () => Promise<unknown>
1740
1875
  onWorkflowAction: (
1741
1876
  sessionId: string,
1742
1877
  action: WorkflowAction,
@@ -1745,8 +1880,6 @@ type HUDProps = {
1745
1880
  onNavigateDiff: (sessionId: string, diffId: string) => void
1746
1881
  onOpenMap: () => void
1747
1882
  onWalk: () => void
1748
- followLook: boolean
1749
- onToggleFollowLook: () => void
1750
1883
  showBranchChanges?: boolean
1751
1884
  branchChanges?: BranchChanges
1752
1885
  canShowBranchChanges?: boolean
@@ -1771,6 +1904,9 @@ type HUDProps = {
1771
1904
  onAddBlueprintFunction?: (fileId: string, name: string) => boolean
1772
1905
  onAddBlueprintVariable?: (fileId: string, name: string) => boolean
1773
1906
  onAddBlueprintImport?: (fileId: string, raw: string) => boolean
1907
+ importPickActive?: boolean
1908
+ onToggleImportPick?: () => void
1909
+ onCancelImportPick?: () => void
1774
1910
  onRemoveBlueprintFunction?: (fileId: string, name: string) => void
1775
1911
  onRemoveBlueprintVariable?: (fileId: string, name: string) => void
1776
1912
  onRemoveBlueprintImport?: (
@@ -1788,32 +1924,40 @@ type HUDProps = {
1788
1924
  kind: BlueprintPointerKind
1789
1925
  path: string
1790
1926
  name?: string
1927
+ color?: string
1791
1928
  }) => void
1792
1929
  onMapAddFile?: (folderPath: string) => void
1793
1930
  onMapAddFolder?: (folderPath: string) => void
1931
+ onRenameCreatedFile?: (fileId: string, name: string) => string | null
1794
1932
  onInspectFile?: (fileId: string) => void
1795
1933
  onInspectBlock?: (fileId: string) => void
1796
- plannedIds?: string[]
1797
- createdIds?: string[]
1798
- deletedIds?: string[]
1934
+ onExplainTarget?: (input: {
1935
+ kind: ExplainTargetKind
1936
+ path: string
1937
+ name?: string
1938
+ }) => void
1799
1939
  blueprintHidden?: boolean
1800
1940
  blueprintHasContent?: boolean
1801
1941
  blueprintCanCleanup?: boolean
1942
+ blueprintColor?: string | null
1943
+ blueprintOptions?: BlueprintOption[]
1944
+ blueprintColorPointers?: BlueprintColorOption[]
1945
+ onSelectBlueprintColor?: (color: string) => void
1802
1946
  onToggleBlueprintHidden?: () => void
1803
1947
  onClearBlueprint?: () => void
1804
1948
  onCleanupBlueprint?: () => void
1949
+ devTargets?: DevTargetsState
1950
+ onSelectDevTarget?: (id: string) => void
1805
1951
  }
1806
1952
 
1807
1953
  export function HUD({
1808
1954
  graph,
1809
- layout,
1810
1955
  mode,
1811
1956
  locked,
1812
1957
  selectedId,
1813
1958
  selectedTick = 0,
1814
1959
  inspectTick = 0,
1815
1960
  selectedFolder = null,
1816
- landAt,
1817
1961
  aimedRelation,
1818
1962
  aimedFileId = null,
1819
1963
  intent,
@@ -1821,13 +1965,10 @@ export function HUD({
1821
1965
  focusedSessionId = null,
1822
1966
  nextAttachSessionId = null,
1823
1967
  onFocusSession,
1824
- onSetupSession,
1825
1968
  onWorkflowAction,
1826
1969
  onNavigateDiff,
1827
1970
  onOpenMap,
1828
1971
  onWalk,
1829
- followLook,
1830
- onToggleFollowLook,
1831
1972
  showBranchChanges = false,
1832
1973
  branchChanges,
1833
1974
  canShowBranchChanges = false,
@@ -1848,10 +1989,12 @@ export function HUD({
1848
1989
  blueprintVariables = [],
1849
1990
  blueprintImports = [],
1850
1991
  blueprintNotes = [],
1851
- blueprintPointers = [],
1852
1992
  onAddBlueprintFunction,
1853
1993
  onAddBlueprintVariable,
1854
1994
  onAddBlueprintImport,
1995
+ importPickActive = false,
1996
+ onToggleImportPick,
1997
+ onCancelImportPick,
1855
1998
  onRemoveBlueprintFunction,
1856
1999
  onRemoveBlueprintVariable,
1857
2000
  onRemoveBlueprintImport,
@@ -1859,17 +2002,22 @@ export function HUD({
1859
2002
  onToggleBlueprintPointer,
1860
2003
  onMapAddFile,
1861
2004
  onMapAddFolder,
2005
+ onRenameCreatedFile,
1862
2006
  onInspectFile,
1863
2007
  onInspectBlock,
1864
- plannedIds = [],
1865
- createdIds = [],
1866
- deletedIds = [],
2008
+ onExplainTarget,
1867
2009
  blueprintHidden = false,
1868
2010
  blueprintHasContent = false,
1869
2011
  blueprintCanCleanup = false,
2012
+ blueprintColor = null,
2013
+ blueprintOptions = [],
2014
+ blueprintColorPointers = [],
2015
+ onSelectBlueprintColor,
1870
2016
  onToggleBlueprintHidden,
1871
2017
  onClearBlueprint,
1872
2018
  onCleanupBlueprint,
2019
+ devTargets,
2020
+ onSelectDevTarget,
1873
2021
  }: HUDProps) {
1874
2022
  const selected = graph.files.find((file) => file.id === selectedId)
1875
2023
  const selectedFolderNode = graph.folders.find(
@@ -1890,7 +2038,6 @@ export function HUD({
1890
2038
  const sessions = (intents ?? [intent]).filter(
1891
2039
  (item) => item.sessionId && isReviewingIntent(item.status),
1892
2040
  )
1893
- const canStop = canStopSession(intent)
1894
2041
  const nextAttachSession =
1895
2042
  sessions.find((session) => session.sessionId === nextAttachSessionId) ??
1896
2043
  [...sessions].reverse().find((session) => session.awaitingAttach) ??
@@ -1898,6 +2045,9 @@ export function HUD({
1898
2045
  const [walkIntro, setWalkIntro] = useState(false)
1899
2046
  const walkIntroSeen = useRef(false)
1900
2047
  const [instructionsOpen, setInstructionsOpen] = useState(false)
2048
+ const [actionsMenuOpen, setActionsMenuOpen] = useState(false)
2049
+ const [actionsMenuPosition, setActionsMenuPosition] = useState<CSSProperties>()
2050
+ const actionsMenuRef = useRef<HTMLDivElement>(null)
1901
2051
  const [noteEditor, setNoteEditor] = useState<{
1902
2052
  file: string
1903
2053
  kind: BlueprintNoteKind
@@ -1906,13 +2056,8 @@ export function HUD({
1906
2056
  subtitle: string
1907
2057
  placeholder: string
1908
2058
  } | null>(null)
1909
- const [setupError, setSetupError] = useState<string | null>(null)
1910
- const [setupBusy, setSetupBusy] = useState(false)
1911
2059
  const [infoVisible, setInfoVisible] = useState(false)
1912
2060
  const [infoMinimized, setInfoMinimized] = useState(false)
1913
- const [thumbnailVisible, setThumbnailVisible] = useState(true)
1914
- const [thumbnailMinimized, setThumbnailMinimized] = useState(false)
1915
- const [thumbnailMaximized, setThumbnailMaximized] = useState(false)
1916
2061
  const infoPanelRef = useRef<HTMLDivElement>(null)
1917
2062
  const canPlace = true
1918
2063
  const overlay = showBranchChanges && branchChanges ? branchChanges : intent
@@ -1983,19 +2128,23 @@ export function HUD({
1983
2128
  )
1984
2129
  const canEditBlueprint =
1985
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
+ }
1986
2145
  const selectedFileNote = selected
1987
2146
  ? findBlueprintNote(blueprintNotes, selected.id, 'file')
1988
2147
  : ''
1989
- const selectedFilePointed = selected
1990
- ? findBlueprintPointer(blueprintPointers, 'file', selected.id)
1991
- : false
1992
- const selectedFolderPointed = selectedFolderNode
1993
- ? findBlueprintPointer(
1994
- blueprintPointers,
1995
- 'folder',
1996
- selectedFolderNode.path,
1997
- )
1998
- : false
1999
2148
  const openFileNote = () => {
2000
2149
  if (!selected || !onSetBlueprintNote) return
2001
2150
  setInstructionsOpen(false)
@@ -2073,6 +2222,11 @@ export function HUD({
2073
2222
  setInfoMinimized(false)
2074
2223
  }, [locked])
2075
2224
 
2225
+ useEffect(() => {
2226
+ if (infoVisible || !importPickActive) return
2227
+ onCancelImportPick?.()
2228
+ }, [importPickActive, infoVisible, onCancelImportPick])
2229
+
2076
2230
  const infoOpen = infoVisible && Boolean(selected || selectedFolderNode)
2077
2231
 
2078
2232
  useEffect(() => {
@@ -2101,26 +2255,6 @@ export function HUD({
2101
2255
  return () => window.removeEventListener('keydown', onKey)
2102
2256
  }, [aimedFileId, infoVisible, mode, onInspectBlock, selectedId])
2103
2257
 
2104
- useEffect(() => {
2105
- if (!mapping) return
2106
- const onKey = (event: KeyboardEvent) => {
2107
- if (event.repeat || event.code !== 'KeyT') return
2108
- if (shouldIgnoreShortcut(event)) return
2109
- event.preventDefault()
2110
- setThumbnailVisible((visible) => {
2111
- if (!visible) {
2112
- setThumbnailMinimized(false)
2113
- setThumbnailMaximized(false)
2114
- } else {
2115
- setThumbnailMaximized(false)
2116
- }
2117
- return !visible
2118
- })
2119
- }
2120
- window.addEventListener('keydown', onKey)
2121
- return () => window.removeEventListener('keydown', onKey)
2122
- }, [mapping])
2123
-
2124
2258
  useEffect(() => {
2125
2259
  if (!infoVisible || (!selectedId && !selectedFolder)) return
2126
2260
  const onKey = (event: KeyboardEvent) => {
@@ -2140,6 +2274,7 @@ export function HUD({
2140
2274
  useEffect(() => {
2141
2275
  if (!instructionsOpen) return
2142
2276
  document.exitPointerLock()
2277
+ setActionsMenuOpen(false)
2143
2278
  const onKey = (event: KeyboardEvent) => {
2144
2279
  if (event.code !== 'Escape') return
2145
2280
  if (shouldIgnoreShortcut(event)) return
@@ -2151,24 +2286,61 @@ export function HUD({
2151
2286
  return () => window.removeEventListener('keydown', onKey, true)
2152
2287
  }, [instructionsOpen])
2153
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
+
2154
2333
  const instructionSections = explorerInstructions({
2155
2334
  canPlace,
2156
2335
  hasChangeSet,
2157
2336
  changePathsOnly,
2158
2337
  selectedUserCreated: Boolean(selected?.userCreated),
2159
2338
  infoVisible,
2160
- thumbnailVisible,
2161
2339
  importedBy,
2162
- canStop,
2163
- sessionCount: sessions.length,
2164
2340
  showBranchChanges,
2165
2341
  canShowBranchChanges,
2166
2342
  })
2167
- const currentInstructionView: InstructionView = mapping
2168
- ? thumbnailMaximized
2169
- ? '3dview'
2170
- : 'map'
2171
- : 'walk'
2343
+ const currentInstructionView: InstructionView = mapping ? 'map' : 'walk'
2172
2344
 
2173
2345
  return (
2174
2346
  <div className="hud">
@@ -2183,13 +2355,8 @@ export function HUD({
2183
2355
  </p>
2184
2356
  <p>
2185
2357
  <kbd>W</kbd> <kbd>A</kbd> <kbd>S</kbd> <kbd>D</kbd> walk,{' '}
2186
- <kbd>Shift</kbd> sprint
2187
- {canPlace ? (
2188
- <>
2189
- , <kbd>Space</kbd> place a file, <kbd>B</kbd> place an island
2190
- </>
2191
- ) : null}
2192
- , 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.
2193
2360
  </p>
2194
2361
  </div>
2195
2362
  </div>
@@ -2202,6 +2369,7 @@ export function HUD({
2202
2369
  <div className="hud-name-gate">
2203
2370
  <NameInput
2204
2371
  placeholder="Folder name"
2372
+ fallbackName="New folder"
2205
2373
  onCommit={onCommitIslandName}
2206
2374
  onCancel={onCancelIslandName}
2207
2375
  />
@@ -2233,20 +2401,40 @@ export function HUD({
2233
2401
  >
2234
2402
  Walk
2235
2403
  </button>
2236
- {canStop && (
2237
- <button
2238
- className="hud-button hud-button-reject"
2239
- type="button"
2240
- aria-label="Stop LLM session"
2241
- onClick={() =>
2242
- intent.sessionId && onWorkflowAction(intent.sessionId, 'stop')
2243
- }
2244
- >
2245
- Stop
2246
- </button>
2247
- )}
2248
2404
  </div>
2249
- {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
+ )}
2250
2438
  </div>
2251
2439
 
2252
2440
  {(sessions.length > 0 || showBranchChanges) && (
@@ -2257,23 +2445,33 @@ export function HUD({
2257
2445
  const active =
2258
2446
  session.sessionId === (focusedSessionId ?? intent.sessionId)
2259
2447
  const attached = session.awaitingAttach === false
2260
- const label = sessionTabLabel(session, sessions)
2448
+ const label = sessionDisplayName(session) || 'Session'
2261
2449
  return (
2262
2450
  <button
2263
2451
  className="hud-button hud-session-tab"
2264
2452
  type="button"
2265
2453
  role="tab"
2266
2454
  aria-selected={active}
2455
+ aria-label={
2456
+ attached ? `${label}, attached` : `${label}, waiting`
2457
+ }
2267
2458
  data-active={active}
2268
2459
  data-attached={attached}
2269
2460
  key={session.sessionId}
2270
2461
  title={attached ? `${label} · Attached` : `${label} · Waiting`}
2462
+ style={
2463
+ session.colorHex
2464
+ ? ({
2465
+ '--session-color': session.colorHex,
2466
+ } as CSSProperties)
2467
+ : undefined
2468
+ }
2271
2469
  onClick={() => {
2272
2470
  if (session.sessionId) onFocusSession?.(session.sessionId)
2471
+ if (session.color) onSelectBlueprintColor?.(session.color)
2273
2472
  }}
2274
2473
  >
2275
- <span className="hud-attach-dot" aria-hidden="true" />
2276
- <span className="hud-session-tab-label">{label}</span>
2474
+ <SessionSwatch colorHex={session.colorHex} />
2277
2475
  </button>
2278
2476
  )
2279
2477
  })}
@@ -2305,7 +2503,16 @@ export function HUD({
2305
2503
  data-minimized={infoMinimized}
2306
2504
  >
2307
2505
  <PanelChrome
2308
- title={selected.name}
2506
+ title={
2507
+ canRenameSelected ? (
2508
+ <InfoNameField
2509
+ name={selected.name}
2510
+ onRename={renameSelectedFile}
2511
+ />
2512
+ ) : (
2513
+ selected.name
2514
+ )
2515
+ }
2309
2516
  minimized={infoMinimized}
2310
2517
  onMinimize={() => setInfoMinimized((current) => !current)}
2311
2518
  onClose={() => {
@@ -2319,28 +2526,43 @@ export function HUD({
2319
2526
  <p>
2320
2527
  {selected.lines} lines · {selected.language}
2321
2528
  </p>
2322
- {canInspectFile(selected.id, selected.userCreated) && (
2323
- <button
2324
- className="hud-button hud-inspect"
2325
- type="button"
2326
- onClick={() => onInspectFile?.(selected.id)}
2327
- >
2328
- Inspect file
2329
- </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>
2330
2550
  )}
2331
2551
  {canEditBlueprint && onToggleBlueprintPointer && (
2332
- <button
2333
- className="hud-button hud-inspect hud-point"
2334
- type="button"
2335
- data-pointed={selectedFilePointed ? 'true' : 'false'}
2336
- aria-pressed={selectedFilePointed}
2337
- onClick={() =>
2338
- 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
+ })
2339
2564
  }
2340
- >
2341
- <EyeIcon size={15} />
2342
- {selectedFilePointed ? 'Stop pointing' : 'Point to file'}
2343
- </button>
2565
+ />
2344
2566
  )}
2345
2567
  {canEditBlueprint && onSetBlueprintNote && (
2346
2568
  <button
@@ -2398,6 +2620,21 @@ export function HUD({
2398
2620
  >
2399
2621
  {symbol.name}
2400
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}
2401
2638
  </li>
2402
2639
  ))}
2403
2640
  </ul>
@@ -2433,23 +2670,35 @@ export function HUD({
2433
2670
  }
2434
2671
  canEdit={Boolean(canEditBlueprint && onSetBlueprintNote)}
2435
2672
  canRemove={Boolean(canEditBlueprint && symbol.intended)}
2436
- pointed={findBlueprintPointer(
2437
- blueprintPointers,
2438
- 'function',
2439
- selected.id,
2440
- symbol.name,
2441
- )}
2673
+ pointerTarget={{
2674
+ kind: 'function',
2675
+ path: selected.id,
2676
+ name: symbol.name,
2677
+ }}
2678
+ colorPointers={blueprintColorPointers}
2679
+ currentColorId={blueprintColor}
2442
2680
  onRemove={() =>
2443
2681
  onRemoveBlueprintFunction?.(selected.id, symbol.name)
2444
2682
  }
2445
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
+ }
2446
2694
  onTogglePoint={
2447
2695
  onToggleBlueprintPointer
2448
- ? () =>
2696
+ ? (color) =>
2449
2697
  onToggleBlueprintPointer({
2450
2698
  kind: 'function',
2451
2699
  path: selected.id,
2452
2700
  name: symbol.name,
2701
+ color,
2453
2702
  })
2454
2703
  : undefined
2455
2704
  }
@@ -2474,20 +2723,32 @@ export function HUD({
2474
2723
  noteEditor.name === item.name
2475
2724
  }
2476
2725
  canEdit={Boolean(canEditBlueprint && onSetBlueprintNote)}
2477
- pointed={findBlueprintPointer(
2478
- blueprintPointers,
2479
- 'function',
2480
- selected.id,
2481
- item.name,
2482
- )}
2726
+ pointerTarget={{
2727
+ kind: 'function',
2728
+ path: selected.id,
2729
+ name: item.name,
2730
+ }}
2731
+ colorPointers={blueprintColorPointers}
2732
+ currentColorId={blueprintColor}
2483
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
+ }
2484
2744
  onTogglePoint={
2485
2745
  onToggleBlueprintPointer
2486
- ? () =>
2746
+ ? (color) =>
2487
2747
  onToggleBlueprintPointer({
2488
2748
  kind: 'function',
2489
2749
  path: selected.id,
2490
2750
  name: item.name,
2751
+ color,
2491
2752
  })
2492
2753
  : undefined
2493
2754
  }
@@ -2531,23 +2792,35 @@ export function HUD({
2531
2792
  }
2532
2793
  canEdit={Boolean(canEditBlueprint && onSetBlueprintNote)}
2533
2794
  canRemove={Boolean(canEditBlueprint && symbol.intended)}
2534
- pointed={findBlueprintPointer(
2535
- blueprintPointers,
2536
- 'variable',
2537
- selected.id,
2538
- symbol.name,
2539
- )}
2795
+ pointerTarget={{
2796
+ kind: 'variable',
2797
+ path: selected.id,
2798
+ name: symbol.name,
2799
+ }}
2800
+ colorPointers={blueprintColorPointers}
2801
+ currentColorId={blueprintColor}
2540
2802
  onRemove={() =>
2541
2803
  onRemoveBlueprintVariable?.(selected.id, symbol.name)
2542
2804
  }
2543
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
+ }
2544
2816
  onTogglePoint={
2545
2817
  onToggleBlueprintPointer
2546
- ? () =>
2818
+ ? (color) =>
2547
2819
  onToggleBlueprintPointer({
2548
2820
  kind: 'variable',
2549
2821
  path: selected.id,
2550
2822
  name: symbol.name,
2823
+ color,
2551
2824
  })
2552
2825
  : undefined
2553
2826
  }
@@ -2572,20 +2845,32 @@ export function HUD({
2572
2845
  noteEditor.name === item.name
2573
2846
  }
2574
2847
  canEdit={Boolean(canEditBlueprint && onSetBlueprintNote)}
2575
- pointed={findBlueprintPointer(
2576
- blueprintPointers,
2577
- 'variable',
2578
- selected.id,
2579
- item.name,
2580
- )}
2848
+ pointerTarget={{
2849
+ kind: 'variable',
2850
+ path: selected.id,
2851
+ name: item.name,
2852
+ }}
2853
+ colorPointers={blueprintColorPointers}
2854
+ currentColorId={blueprintColor}
2581
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
+ }
2582
2866
  onTogglePoint={
2583
2867
  onToggleBlueprintPointer
2584
- ? () =>
2868
+ ? (color) =>
2585
2869
  onToggleBlueprintPointer({
2586
2870
  kind: 'variable',
2587
2871
  path: selected.id,
2588
2872
  name: item.name,
2873
+ color,
2589
2874
  })
2590
2875
  : undefined
2591
2876
  }
@@ -2677,10 +2962,20 @@ export function HUD({
2677
2962
  </ul>
2678
2963
  )}
2679
2964
  {canEditBlueprint && !importedBy && onAddBlueprintImport && (
2680
- <AddIntentRow
2681
- placeholder="Clock from src/components/Clock.tsx"
2682
- onAdd={(raw) => onAddBlueprintImport(selected.id, raw)}
2683
- />
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
+ </>
2684
2979
  )}
2685
2980
  </div>
2686
2981
  )}
@@ -2700,6 +2995,16 @@ export function HUD({
2700
2995
  setInfoVisible(false)
2701
2996
  setInfoMinimized(false)
2702
2997
  }}
2998
+ onExplain={
2999
+ onExplainTarget
3000
+ ? () =>
3001
+ onExplainTarget({
3002
+ kind: 'folder',
3003
+ path: selectedFolderNode.path,
3004
+ })
3005
+ : undefined
3006
+ }
3007
+ explainLabel={`Explain ${selectedFolderNode.name}`}
2703
3008
  />
2704
3009
  {!infoMinimized && (
2705
3010
  <div ref={infoPanelRef} className="hud-panel-body">
@@ -2713,42 +3018,55 @@ export function HUD({
2713
3018
  </p>
2714
3019
  <div className="hud-section-title">Files</div>
2715
3020
  {folderFiles.length === 0 ? (
2716
- <p>No files on this island</p>
3021
+ <p>No files in this folder</p>
2717
3022
  ) : (
2718
3023
  <ul>
2719
3024
  {folderFiles.map((file) => (
2720
3025
  <li key={file.id}>
2721
3026
  <span>{file.name}</span>
2722
- {canInspectFile(file.id, file.userCreated) && (
2723
- <button
2724
- className="hud-item-inspect"
2725
- type="button"
2726
- onClick={() => onInspectFile?.(file.id)}
2727
- >
2728
- Inspect
2729
- </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>
2730
3049
  )}
2731
3050
  </li>
2732
3051
  ))}
2733
3052
  </ul>
2734
3053
  )}
2735
3054
  {canPlace && onToggleBlueprintPointer && (
2736
- <button
2737
- className="hud-button hud-inspect hud-point"
2738
- type="button"
2739
- data-pointed={selectedFolderPointed ? 'true' : 'false'}
2740
- 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"
2741
3061
  disabled={naming || selectedFolderNode.path.startsWith('draft:')}
2742
- onClick={() =>
3062
+ onToggle={(color) =>
2743
3063
  onToggleBlueprintPointer({
2744
3064
  kind: 'folder',
2745
3065
  path: selectedFolderNode.path,
3066
+ color,
2746
3067
  })
2747
3068
  }
2748
- >
2749
- <EyeIcon size={15} />
2750
- {selectedFolderPointed ? 'Stop pointing' : 'Point to folder'}
2751
- </button>
3069
+ />
2752
3070
  )}
2753
3071
  {canPlace && mapping && onMapAddFile && onMapAddFolder && (
2754
3072
  <div className="hud-decide hud-map-blueprint">
@@ -2774,49 +3092,6 @@ export function HUD({
2774
3092
  )}
2775
3093
  </aside>
2776
3094
  )}
2777
-
2778
- {mapping && thumbnailVisible && (
2779
- <SelectionThumbnail
2780
- graph={graph}
2781
- layout={layout}
2782
- selectedId={selectedId}
2783
- selectedFolder={selectedFolder}
2784
- landAt={landAt}
2785
- importedBy={importedBy}
2786
- minimized={thumbnailMinimized}
2787
- maximized={thumbnailMaximized}
2788
- plannedIds={plannedIds}
2789
- createdIds={createdIds}
2790
- deletedIds={deletedIds}
2791
- pointedFileIds={
2792
- blueprintHidden
2793
- ? []
2794
- : blueprintPointers.flatMap((item) =>
2795
- item.kind === 'folder' ? [] : [item.path],
2796
- )
2797
- }
2798
- pointedFolderPaths={
2799
- blueprintHidden
2800
- ? []
2801
- : blueprintPointers.flatMap((item) =>
2802
- item.kind === 'folder' ? [item.path] : [],
2803
- )
2804
- }
2805
- onMinimize={() => {
2806
- setThumbnailMaximized(false)
2807
- setThumbnailMinimized((current) => !current)
2808
- }}
2809
- onMaximize={() => {
2810
- setThumbnailMinimized(false)
2811
- setThumbnailMaximized((current) => !current)
2812
- }}
2813
- onHide={() => {
2814
- setThumbnailVisible(false)
2815
- setThumbnailMinimized(false)
2816
- setThumbnailMaximized(false)
2817
- }}
2818
- />
2819
- )}
2820
3095
  </div>
2821
3096
 
2822
3097
  {noteEditor && onSetBlueprintNote && (
@@ -2889,52 +3164,54 @@ export function HUD({
2889
3164
 
2890
3165
  <div className="hud-bottom">
2891
3166
  <div className="hud-bottom-actions">
2892
- <button
2893
- className="hud-button"
2894
- data-active={instructionsOpen}
2895
- type="button"
2896
- aria-haspopup="dialog"
2897
- aria-expanded={instructionsOpen}
2898
- onClick={() => {
2899
- setNoteEditor(null)
2900
- setInstructionsOpen((open) => !open)
2901
- }}
2902
- >
2903
- Instructions
2904
- </button>
2905
- <button
2906
- className="hud-button"
2907
- type="button"
2908
- aria-label="Rescan the project and rebuild the map"
2909
- disabled={updatingModel}
2910
- onClick={onUpdateModel}
2911
- >
2912
- {updatingModel ? 'Updating…' : 'Update model'}
2913
- </button>
2914
- {onSetupSession && (
2915
- <button
2916
- className="hud-button"
2917
- type="button"
2918
- aria-label="Start an LLM session without attaching a chat yet"
2919
- title={setupError ?? 'Start an LLM session without attaching a chat yet'}
2920
- disabled={setupBusy}
2921
- onClick={() => {
2922
- setInstructionsOpen(false)
2923
- setSetupError(null)
2924
- setSetupBusy(true)
2925
- void onSetupSession()
2926
- .catch((caught) => {
2927
- setSetupError(
2928
- caught instanceof Error
2929
- ? caught.message
2930
- : 'Could not set up the session',
2931
- )
2932
- })
2933
- .finally(() => setSetupBusy(false))
2934
- }}
3167
+ {onSelectBlueprintColor && blueprintOptions.length > 0 && (
3168
+ <div
3169
+ className="hud-blueprint-select"
3170
+ role="radiogroup"
3171
+ aria-label="Blueprint"
2935
3172
  >
2936
- {setupBusy ? 'Starting…' : 'Setup LLM session'}
2937
- </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>
2938
3215
  )}
2939
3216
  {onToggleBlueprintHidden && (
2940
3217
  <button
@@ -2944,12 +3221,12 @@ export function HUD({
2944
3221
  aria-label={blueprintHidden ? 'Show blueprint' : 'Hide blueprint'}
2945
3222
  title={
2946
3223
  blueprintHidden
2947
- ? 'Show the shared blueprint overlay'
2948
- : 'Hide the shared blueprint overlay'
3224
+ ? 'Show this blueprint overlay'
3225
+ : 'Hide this blueprint overlay; other colors stay visible'
2949
3226
  }
2950
3227
  onClick={onToggleBlueprintHidden}
2951
3228
  >
2952
- {blueprintHidden ? 'Show blueprint' : 'Hide blueprint'}
3229
+ {blueprintHidden ? 'Show' : 'Hide'}
2953
3230
  </button>
2954
3231
  )}
2955
3232
  {onClearBlueprint && (
@@ -2961,7 +3238,7 @@ export function HUD({
2961
3238
  disabled={!blueprintHasContent}
2962
3239
  onClick={onClearBlueprint}
2963
3240
  >
2964
- Clear blueprint
3241
+ Clear
2965
3242
  </button>
2966
3243
  )}
2967
3244
  {onCleanupBlueprint && (
@@ -2973,7 +3250,7 @@ export function HUD({
2973
3250
  disabled={!blueprintCanCleanup}
2974
3251
  onClick={onCleanupBlueprint}
2975
3252
  >
2976
- Cleanup blueprint
3253
+ Cleanup
2977
3254
  </button>
2978
3255
  )}
2979
3256
  </div>
@@ -3017,48 +3294,6 @@ export function HUD({
3017
3294
  </span>
3018
3295
  </button>
3019
3296
  )}
3020
- {mapping && (
3021
- <button
3022
- className="hud-button hud-icon-button"
3023
- data-active={thumbnailVisible}
3024
- aria-label={
3025
- thumbnailVisible ? 'Hide 3D view' : 'Show 3D view'
3026
- }
3027
- aria-keyshortcuts="T"
3028
- aria-pressed={thumbnailVisible}
3029
- type="button"
3030
- onClick={() => {
3031
- setThumbnailVisible((visible) => {
3032
- if (!visible) {
3033
- setThumbnailMinimized(false)
3034
- setThumbnailMaximized(false)
3035
- } else {
3036
- setThumbnailMaximized(false)
3037
- }
3038
- return !visible
3039
- })
3040
- }}
3041
- >
3042
- <svg
3043
- viewBox="0 0 24 24"
3044
- width="18"
3045
- height="18"
3046
- fill="none"
3047
- stroke="currentColor"
3048
- strokeWidth="2"
3049
- strokeLinecap="round"
3050
- strokeLinejoin="round"
3051
- aria-hidden="true"
3052
- >
3053
- <rect x="3" y="5" width="11" height="14" rx="1.5" />
3054
- <path d="M16 8h5v11H9v-3" />
3055
- <path d="M6.5 16.5 9 12l2 2.5 1.5-2L15 16.5" />
3056
- </svg>
3057
- <span className="hud-tooltip">
3058
- {thumbnailVisible ? 'T hide 3D view' : 'T show 3D view'}
3059
- </span>
3060
- </button>
3061
- )}
3062
3297
  <button
3063
3298
  className="hud-button hud-icon-button"
3064
3299
  data-active={importedBy}
@@ -3136,30 +3371,70 @@ export function HUD({
3136
3371
  : 'G show branch changes'}
3137
3372
  </span>
3138
3373
  </button>
3139
- <button
3140
- className="hud-button hud-icon-button"
3141
- data-active={followLook}
3142
- aria-label="Make LLM look where I look"
3143
- aria-pressed={followLook}
3144
- type="button"
3145
- onClick={onToggleFollowLook}
3146
- >
3147
- <svg
3148
- viewBox="0 0 24 24"
3149
- width="18"
3150
- height="18"
3151
- fill="none"
3152
- stroke="currentColor"
3153
- strokeWidth="2"
3154
- strokeLinecap="round"
3155
- strokeLinejoin="round"
3156
- 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)}
3157
3383
  >
3158
- <path d="M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7-10-7-10-7Z" />
3159
- <circle cx="12" cy="12" r="3" />
3160
- </svg>
3161
- <span className="hud-tooltip">Make LLM look where I look</span>
3162
- </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>
3163
3438
  </div>
3164
3439
  </div>
3165
3440
  </div>