@jkwd/inbase 0.1.18 → 0.1.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
1
  import { useEffect, useRef, useState, type ReactNode } from 'react'
2
- import { persistInitialInstruction } from '../agentIntent'
2
+ import { persistAddContextFiles, persistInitialInstruction, persistRemoveContextFile } from '../agentIntent'
3
3
  import { NameInput } from './NameInput'
4
4
  import { SelectionThumbnail } from '../scene/SelectionThumbnail'
5
5
  import {
@@ -10,6 +10,8 @@ import {
10
10
  type AimedRelation,
11
11
  type BlueprintNote,
12
12
  type BlueprintNoteKind,
13
+ type BlueprintPointer,
14
+ type BlueprintPointerKind,
13
15
  type BranchChanges,
14
16
  type CodebaseGraph,
15
17
  type PatchImportAddition,
@@ -18,7 +20,8 @@ import {
18
20
  type WorldLayout,
19
21
  type WorkflowAction,
20
22
  } from '../types'
21
- import { findBlueprintNote } from '../userCreated'
23
+ import { findBlueprintNote, findBlueprintPointer } from '../userCreated'
24
+ import { EyeIcon } from './EyeIcon'
22
25
  import { beginKeyboardIsolation, shouldIgnoreShortcut } from '../keyboard'
23
26
 
24
27
  function reviewTitle(status: AgentIntentStatus) {
@@ -311,25 +314,43 @@ function BlueprintSymbolRow({
311
314
  className,
312
315
  hasNote,
313
316
  noteOpen,
317
+ pointed,
314
318
  canEdit,
315
319
  canRemove,
316
320
  onRemove,
317
321
  onOpenNote,
322
+ onTogglePoint,
318
323
  }: {
319
324
  name: string
320
325
  className?: string
321
326
  hasNote: boolean
322
327
  noteOpen?: boolean
328
+ pointed?: boolean
323
329
  canEdit: boolean
324
330
  canRemove?: boolean
325
331
  onRemove?: () => void
326
332
  onOpenNote: () => void
333
+ onTogglePoint?: () => void
327
334
  }) {
328
335
  return (
329
336
  <li>
330
337
  <span className={className}>{name}</span>
331
338
  {canEdit && (
332
339
  <div className="hud-item-actions">
340
+ {onTogglePoint && (
341
+ <button
342
+ className="hud-item-point"
343
+ 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}
350
+ >
351
+ <EyeIcon size={13} />
352
+ </button>
353
+ )}
333
354
  <button
334
355
  className="hud-item-note"
335
356
  type="button"
@@ -450,6 +471,119 @@ function InitialInstructionField({
450
471
  )
451
472
  }
452
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
+
453
587
  function blueprintIsDefined(intent: AgentIntent) {
454
588
  return (
455
589
  (intent.userCreatedBlocks?.length ?? 0) > 0 ||
@@ -457,31 +591,61 @@ function blueprintIsDefined(intent: AgentIntent) {
457
591
  (intent.blueprintFunctions?.length ?? 0) > 0 ||
458
592
  (intent.blueprintVariables?.length ?? 0) > 0 ||
459
593
  (intent.blueprintImports?.length ?? 0) > 0 ||
460
- (intent.blueprintNotes?.length ?? 0) > 0
594
+ (intent.blueprintNotes?.length ?? 0) > 0 ||
595
+ (intent.blueprintPointers?.length ?? 0) > 0
461
596
  )
462
597
  }
463
598
 
464
599
  function HandshakeSetup({
465
600
  instruction,
466
601
  onInstructionChange,
602
+ contextFiles,
603
+ contextBusy,
604
+ contextError,
605
+ onAddContextFiles,
606
+ onRemoveContextFile,
467
607
  blueprintDefined,
468
608
  awaitingAttach,
469
609
  nextAttachLabel,
470
610
  }: {
471
611
  instruction: string
472
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
473
618
  blueprintDefined: boolean
474
619
  awaitingAttach: boolean
475
620
  nextAttachLabel: string | null
476
621
  }) {
477
622
  return (
478
623
  <div className="hud-setup">
479
- <section className="hud-setup-section">
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
+ >
480
637
  <h2 className="hud-setup-heading">Instructions</h2>
481
638
  <InitialInstructionField
482
639
  value={instruction}
483
640
  onChange={onInstructionChange}
484
641
  />
642
+ <ContextFileDrop
643
+ files={contextFiles}
644
+ busy={contextBusy}
645
+ error={contextError}
646
+ onAdd={onAddContextFiles}
647
+ onRemove={onRemoveContextFile}
648
+ />
485
649
  </section>
486
650
  <section className="hud-setup-section">
487
651
  <h2 className="hud-setup-heading">
@@ -538,6 +702,11 @@ function sessionLiveStatus(intent: AgentIntent) {
538
702
  if (intent.awaitingAttach) {
539
703
  return { text: 'Waiting for /inbase in Cursor', busy: false }
540
704
  }
705
+ if (intent.status === 'pending') {
706
+ return intent.listening
707
+ ? { text: 'LLM is listening — accept or send an instruction', busy: false }
708
+ : { text: 'Review this step', busy: false }
709
+ }
541
710
  if (kind === 'execute') {
542
711
  return { text: `LLM received ${detail}`, busy: true }
543
712
  }
@@ -566,11 +735,6 @@ function sessionLiveStatus(intent: AgentIntent) {
566
735
  ? { text: 'LLM is listening — run the next step', busy: false }
567
736
  : { text: 'Plan ready', busy: false }
568
737
  }
569
- if (intent.status === 'pending') {
570
- return intent.listening
571
- ? { text: 'LLM is listening — accept or send an instruction', busy: false }
572
- : { text: 'Review this step', busy: false }
573
- }
574
738
  if (
575
739
  kind === 'attached' ||
576
740
  intent.status === 'blueprint' ||
@@ -649,14 +813,22 @@ function SessionPanel({
649
813
  const [initialInstruction, setInitialInstruction] = useState(
650
814
  () => intent.initialInstruction ?? '',
651
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)
652
821
  const sessionId = intent.sessionId
653
- const pending = intent.status === 'pending' && intent.isActiveDiff
822
+ const latestEntry = intent.isActiveDiff ? intent.chain.at(-1) : null
823
+ const pending =
824
+ latestEntry?.status === 'pending' ||
825
+ (intent.status === 'pending' && intent.isActiveDiff)
654
826
  const askingBlueprint = intent.status === 'blueprint_ask'
655
827
  const sendingBlueprint = intent.status === 'blueprint'
656
828
  const canPlace = Boolean(intent.creationMode)
657
829
  const preparing = intent.status === 'preparing'
658
- const planReady = intent.status === 'planned'
659
830
  const working = intent.status === 'working' || intent.status === 'replanning'
831
+ const planReady = intent.status === 'planned' && !pending
660
832
  const previewing = intent.preview
661
833
  const chainIndex = intent.chainIndex ?? 0
662
834
  const previousDiff = intent.chain[chainIndex - 1]
@@ -665,10 +837,6 @@ function SessionPanel({
665
837
  intent.step && intent.steps?.length > 0
666
838
  ? `Step ${intent.step} of ${intent.steps.length}`
667
839
  : 'Patch'
668
- const lastStep =
669
- typeof intent.step === 'number' &&
670
- intent.steps.length > 0 &&
671
- intent.step >= intent.steps.length
672
840
  const stepByStep = intent.stepByStep !== false
673
841
  const addedFunctions = intent.addedFunctions ?? []
674
842
  const addedVariables = intent.addedVariables ?? []
@@ -679,25 +847,27 @@ function SessionPanel({
679
847
  intent.status === 'finished'
680
848
  ? intent.steps.map((step) => step.index)
681
849
  : intent.chain
682
- .filter(
683
- (entry) =>
684
- entry.status === 'applied' || entry.status === 'extended',
685
- )
850
+ .filter((entry) => entry.status === 'applied')
686
851
  .map((entry) => entry.step),
687
852
  )
688
853
  if (intent.status === 'approved' && typeof intent.step === 'number') {
689
854
  acceptedSteps.add(intent.step)
690
855
  }
691
- const proposalStep =
692
- pending && typeof intent.step === 'number' ? intent.step : null
856
+ const proposalStep = pending
857
+ ? (latestEntry?.status === 'pending' ? latestEntry.step : intent.step)
858
+ : null
693
859
  const processingStep =
694
860
  working && typeof intent.step === 'number' ? intent.step : null
695
861
  const invokeStep =
696
- planReady && !working
862
+ planReady && !working && proposalStep === null
697
863
  ? (intent.steps.find((step) => !acceptedSteps.has(step.index)) ?? null)
698
864
  : null
699
865
  const canRunNext = stepByStep && Boolean(invokeStep)
700
866
  const canAcceptProposal = proposalStep !== null
867
+ const lastStep =
868
+ typeof proposalStep === 'number' &&
869
+ intent.steps.length > 0 &&
870
+ proposalStep >= intent.steps.length
701
871
  const panelDone =
702
872
  intent.status === 'finished' ||
703
873
  intent.status === 'approved' ||
@@ -710,8 +880,15 @@ function SessionPanel({
710
880
 
711
881
  useEffect(() => {
712
882
  setInitialInstruction(intent.initialInstruction ?? '')
883
+ setContextFiles(intent.contextFiles ?? [])
884
+ setContextError(null)
713
885
  }, [sessionId])
714
886
 
887
+ useEffect(() => {
888
+ if (contextBusy) return
889
+ setContextFiles(intent.contextFiles ?? [])
890
+ }, [contextBusy, intent.contextFiles])
891
+
715
892
  if (!sessionId || !isReviewingIntent(intent.status)) return null
716
893
 
717
894
  const updateInitialInstruction = (value: string) => {
@@ -719,6 +896,43 @@ function SessionPanel({
719
896
  persistInitialInstruction(sessionId, value)
720
897
  }
721
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
+
722
936
  const showInitialInstruction =
723
937
  Boolean(intent.awaitingAttach) &&
724
938
  (askingBlueprint || sendingBlueprint || preparing)
@@ -800,6 +1014,11 @@ function SessionPanel({
800
1014
  <HandshakeSetup
801
1015
  instruction={initialInstruction}
802
1016
  onInstructionChange={updateInitialInstruction}
1017
+ contextFiles={contextFiles}
1018
+ contextBusy={contextBusy}
1019
+ contextError={contextError}
1020
+ onAddContextFiles={addContextFiles}
1021
+ onRemoveContextFile={removeContextFile}
803
1022
  blueprintDefined={blueprintIsDefined(intent)}
804
1023
  awaitingAttach={Boolean(intent.awaitingAttach)}
805
1024
  nextAttachLabel={queuedBehind}
@@ -875,7 +1094,7 @@ function SessionPanel({
875
1094
  {intent.steps.map((step) => {
876
1095
  const proposed = proposalStep === step.index
877
1096
  const processing = processingStep === step.index
878
- const accepted = acceptedSteps.has(step.index)
1097
+ const accepted = acceptedSteps.has(step.index) && !proposed
879
1098
  const creating = processing && !proposed
880
1099
  const showStepAction =
881
1100
  creating ||
@@ -1074,7 +1293,7 @@ function SessionPanel({
1074
1293
  value={instruction}
1075
1294
  maxLength={4000}
1076
1295
  rows={3}
1077
- placeholder="Describe what should change in the next diff…"
1296
+ placeholder="Describe what should change in this proposal…"
1078
1297
  onChange={(event) => setInstruction(event.target.value)}
1079
1298
  onKeyDown={(event) => event.stopPropagation()}
1080
1299
  />
@@ -1350,6 +1569,11 @@ function explorerInstructions({
1350
1569
  ? [
1351
1570
  { id: 'space', keys: ['Space'], label: 'Place file' },
1352
1571
  { id: 'b-island', keys: ['B'], label: 'Place island' },
1572
+ {
1573
+ id: 'point-to',
1574
+ keys: ['Point to'],
1575
+ label: 'Keep a file, folder, or function in mind',
1576
+ },
1353
1577
  ]
1354
1578
  : []),
1355
1579
  ...backspace,
@@ -1433,11 +1657,10 @@ function explorerInstructions({
1433
1657
  {
1434
1658
  id: 'add-file-folder',
1435
1659
  keys: ['Right-click'],
1436
- label: 'Add file or folder',
1660
+ label: 'Add file or folder, or point to a folder',
1437
1661
  },
1438
1662
  ]
1439
1663
  : []),
1440
- { id: 'click-line', keys: ['Click'], label: 'A line to fly there' },
1441
1664
  {
1442
1665
  id: 'option-click-walk',
1443
1666
  keys: ['Option', 'Click'],
@@ -1544,6 +1767,7 @@ type HUDProps = {
1544
1767
  blueprintVariables?: PatchSymbolAddition[]
1545
1768
  blueprintImports?: PatchImportAddition[]
1546
1769
  blueprintNotes?: BlueprintNote[]
1770
+ blueprintPointers?: BlueprintPointer[]
1547
1771
  onAddBlueprintFunction?: (fileId: string, name: string) => boolean
1548
1772
  onAddBlueprintVariable?: (fileId: string, name: string) => boolean
1549
1773
  onAddBlueprintImport?: (fileId: string, raw: string) => boolean
@@ -1560,6 +1784,11 @@ type HUDProps = {
1560
1784
  name?: string
1561
1785
  note: string
1562
1786
  }) => void
1787
+ onToggleBlueprintPointer?: (next: {
1788
+ kind: BlueprintPointerKind
1789
+ path: string
1790
+ name?: string
1791
+ }) => void
1563
1792
  onMapAddFile?: (folderPath: string) => void
1564
1793
  onMapAddFolder?: (folderPath: string) => void
1565
1794
  onInspectFile?: (fileId: string) => void
@@ -1619,6 +1848,7 @@ export function HUD({
1619
1848
  blueprintVariables = [],
1620
1849
  blueprintImports = [],
1621
1850
  blueprintNotes = [],
1851
+ blueprintPointers = [],
1622
1852
  onAddBlueprintFunction,
1623
1853
  onAddBlueprintVariable,
1624
1854
  onAddBlueprintImport,
@@ -1626,6 +1856,7 @@ export function HUD({
1626
1856
  onRemoveBlueprintVariable,
1627
1857
  onRemoveBlueprintImport,
1628
1858
  onSetBlueprintNote,
1859
+ onToggleBlueprintPointer,
1629
1860
  onMapAddFile,
1630
1861
  onMapAddFolder,
1631
1862
  onInspectFile,
@@ -1755,6 +1986,16 @@ export function HUD({
1755
1986
  const selectedFileNote = selected
1756
1987
  ? findBlueprintNote(blueprintNotes, selected.id, 'file')
1757
1988
  : ''
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
1758
1999
  const openFileNote = () => {
1759
2000
  if (!selected || !onSetBlueprintNote) return
1760
2001
  setInstructionsOpen(false)
@@ -2087,6 +2328,20 @@ export function HUD({
2087
2328
  Inspect file
2088
2329
  </button>
2089
2330
  )}
2331
+ {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 })
2339
+ }
2340
+ >
2341
+ <EyeIcon size={15} />
2342
+ {selectedFilePointed ? 'Stop pointing' : 'Point to file'}
2343
+ </button>
2344
+ )}
2090
2345
  {canEditBlueprint && onSetBlueprintNote && (
2091
2346
  <button
2092
2347
  className="hud-button hud-inspect"
@@ -2178,10 +2433,26 @@ export function HUD({
2178
2433
  }
2179
2434
  canEdit={Boolean(canEditBlueprint && onSetBlueprintNote)}
2180
2435
  canRemove={Boolean(canEditBlueprint && symbol.intended)}
2436
+ pointed={findBlueprintPointer(
2437
+ blueprintPointers,
2438
+ 'function',
2439
+ selected.id,
2440
+ symbol.name,
2441
+ )}
2181
2442
  onRemove={() =>
2182
2443
  onRemoveBlueprintFunction?.(selected.id, symbol.name)
2183
2444
  }
2184
2445
  onOpenNote={() => openSymbolNote('function', symbol.name)}
2446
+ onTogglePoint={
2447
+ onToggleBlueprintPointer
2448
+ ? () =>
2449
+ onToggleBlueprintPointer({
2450
+ kind: 'function',
2451
+ path: selected.id,
2452
+ name: symbol.name,
2453
+ })
2454
+ : undefined
2455
+ }
2185
2456
  />
2186
2457
  ))}
2187
2458
  {extraAddedFunctions.map((item) => (
@@ -2203,7 +2474,23 @@ export function HUD({
2203
2474
  noteEditor.name === item.name
2204
2475
  }
2205
2476
  canEdit={Boolean(canEditBlueprint && onSetBlueprintNote)}
2477
+ pointed={findBlueprintPointer(
2478
+ blueprintPointers,
2479
+ 'function',
2480
+ selected.id,
2481
+ item.name,
2482
+ )}
2206
2483
  onOpenNote={() => openSymbolNote('function', item.name)}
2484
+ onTogglePoint={
2485
+ onToggleBlueprintPointer
2486
+ ? () =>
2487
+ onToggleBlueprintPointer({
2488
+ kind: 'function',
2489
+ path: selected.id,
2490
+ name: item.name,
2491
+ })
2492
+ : undefined
2493
+ }
2207
2494
  />
2208
2495
  ))}
2209
2496
  </ul>
@@ -2244,10 +2531,26 @@ export function HUD({
2244
2531
  }
2245
2532
  canEdit={Boolean(canEditBlueprint && onSetBlueprintNote)}
2246
2533
  canRemove={Boolean(canEditBlueprint && symbol.intended)}
2534
+ pointed={findBlueprintPointer(
2535
+ blueprintPointers,
2536
+ 'variable',
2537
+ selected.id,
2538
+ symbol.name,
2539
+ )}
2247
2540
  onRemove={() =>
2248
2541
  onRemoveBlueprintVariable?.(selected.id, symbol.name)
2249
2542
  }
2250
2543
  onOpenNote={() => openSymbolNote('variable', symbol.name)}
2544
+ onTogglePoint={
2545
+ onToggleBlueprintPointer
2546
+ ? () =>
2547
+ onToggleBlueprintPointer({
2548
+ kind: 'variable',
2549
+ path: selected.id,
2550
+ name: symbol.name,
2551
+ })
2552
+ : undefined
2553
+ }
2251
2554
  />
2252
2555
  ))}
2253
2556
  {extraAddedVariables.map((item) => (
@@ -2269,7 +2572,23 @@ export function HUD({
2269
2572
  noteEditor.name === item.name
2270
2573
  }
2271
2574
  canEdit={Boolean(canEditBlueprint && onSetBlueprintNote)}
2575
+ pointed={findBlueprintPointer(
2576
+ blueprintPointers,
2577
+ 'variable',
2578
+ selected.id,
2579
+ item.name,
2580
+ )}
2272
2581
  onOpenNote={() => openSymbolNote('variable', item.name)}
2582
+ onTogglePoint={
2583
+ onToggleBlueprintPointer
2584
+ ? () =>
2585
+ onToggleBlueprintPointer({
2586
+ kind: 'variable',
2587
+ path: selected.id,
2588
+ name: item.name,
2589
+ })
2590
+ : undefined
2591
+ }
2273
2592
  />
2274
2593
  ))}
2275
2594
  </ul>
@@ -2413,6 +2732,24 @@ export function HUD({
2413
2732
  ))}
2414
2733
  </ul>
2415
2734
  )}
2735
+ {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}
2741
+ disabled={naming || selectedFolderNode.path.startsWith('draft:')}
2742
+ onClick={() =>
2743
+ onToggleBlueprintPointer({
2744
+ kind: 'folder',
2745
+ path: selectedFolderNode.path,
2746
+ })
2747
+ }
2748
+ >
2749
+ <EyeIcon size={15} />
2750
+ {selectedFolderPointed ? 'Stop pointing' : 'Point to folder'}
2751
+ </button>
2752
+ )}
2416
2753
  {canPlace && mapping && onMapAddFile && onMapAddFolder && (
2417
2754
  <div className="hud-decide hud-map-blueprint">
2418
2755
  <button
@@ -2451,6 +2788,20 @@ export function HUD({
2451
2788
  plannedIds={plannedIds}
2452
2789
  createdIds={createdIds}
2453
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
+ }
2454
2805
  onMinimize={() => {
2455
2806
  setThumbnailMaximized(false)
2456
2807
  setThumbnailMinimized((current) => !current)