@jkwd/inbase 0.1.13 → 0.1.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,7 @@
1
1
  import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
2
2
  import { Canvas } from '@react-three/fiber'
3
- import { emptyIntent, fetchAgentIntent, fetchAgentIntents, inspectTargetFile, performAgentAction, persistSessionBlueprint, persistSessionFocus, setupVisualizerSession } from './agentIntent'
3
+ import { shouldIgnoreShortcut, isKeyboardIsolated } from './keyboard'
4
+ import { emptyIntent, fetchAgentIntent, fetchAgentIntents, inspectTargetFile, performAgentAction, persistBlueprintCleanup, persistBlueprintClear, persistBlueprintHidden, persistSessionBlueprint, persistSessionFocus, setupVisualizerSession } from './agentIntent'
4
5
  import { emptyBranchChanges, fetchBranchChanges } from './branchChanges'
5
6
  import { fetchCodebase, updateCodebase } from './codebase'
6
7
  import {
@@ -14,6 +15,7 @@ import {
14
15
  } from './layout'
15
16
  import { World } from './scene/World'
16
17
  import { HUD } from './ui/HUD'
18
+ import { CanvasErrorBoundary } from './ui/CanvasErrorBoundary'
17
19
  import {
18
20
  MapContextMenu,
19
21
  type MapContextMenuState,
@@ -26,14 +28,18 @@ import {
26
28
  } from './userContext'
27
29
  import {
28
30
  defaultBlockSpot,
31
+ dropBlueprintFileNotes,
32
+ dropBlueprintSymbolNote,
29
33
  isBlueprintSymbolName,
30
34
  namedCreatedBlocks,
31
35
  namedCreatedIslands,
32
36
  parseBlueprintImport,
37
+ parseBlueprintNotes,
33
38
  parseUserCreatedBlocks,
34
39
  parseUserCreatedIslands,
35
40
  resolveCreatedFile,
36
41
  resolveCreatedIsland,
42
+ setBlueprintNote,
37
43
  withBlueprintIntent,
38
44
  withUserCreatedGraph,
39
45
  withUserCreatedLayout,
@@ -43,6 +49,8 @@ import {
43
49
  llmIsMakingChanges,
44
50
  type AgentIntent,
45
51
  type AimedRelation,
52
+ type BlueprintNote,
53
+ type BlueprintNoteKind,
46
54
  type CodebaseGraph,
47
55
  type FlyTo,
48
56
  type PatchImportAddition,
@@ -165,7 +173,7 @@ function Explorer({
165
173
  intents.find((item) => item.sessionId === focusedSessionId) ??
166
174
  intents[0] ??
167
175
  emptyIntent
168
- const canPlace = Boolean(intent.sessionId && intent.creationMode)
176
+ const canPlace = true
169
177
  const llmBusy = intents.some(llmIsMakingChanges)
170
178
  const llmPreviewing = intent.preview || isPatchPreview(intent.status)
171
179
  const showingBranchChanges =
@@ -191,6 +199,23 @@ function Explorer({
191
199
  const [blueprintImports, setBlueprintImports] = useState<
192
200
  PatchImportAddition[]
193
201
  >([])
202
+ const [blueprintNotes, setBlueprintNotes] = useState<BlueprintNote[]>([])
203
+ const [blueprintHidden, setBlueprintHidden] = useState(false)
204
+ const userBlocksRef = useRef(userBlocks)
205
+ const userIslandsRef = useRef(userIslands)
206
+ const blueprintFunctionsRef = useRef(blueprintFunctions)
207
+ const blueprintVariablesRef = useRef(blueprintVariables)
208
+ const blueprintImportsRef = useRef(blueprintImports)
209
+ const blueprintNotesRef = useRef(blueprintNotes)
210
+ const notesDirty = useRef(false)
211
+ const blueprintPersistGen = useRef(0)
212
+ const notePersistTimer = useRef<number | null>(null)
213
+ userBlocksRef.current = userBlocks
214
+ userIslandsRef.current = userIslands
215
+ blueprintFunctionsRef.current = blueprintFunctions
216
+ blueprintVariablesRef.current = blueprintVariables
217
+ blueprintImportsRef.current = blueprintImports
218
+ blueprintNotesRef.current = blueprintNotes
194
219
  const namingId = userBlocks.find((block) => block.naming)?.id ?? null
195
220
  const namingIslandId = userIslands.find((island) => island.naming)?.id ?? null
196
221
  const naming = Boolean(namingId || namingIslandId)
@@ -211,10 +236,30 @@ function Explorer({
211
236
  plannedCreates,
212
237
  previewing,
213
238
  ])
239
+ const knownFileIds = useMemo(
240
+ () => new Set(previewGraph.files.map((file) => file.id)),
241
+ [previewGraph],
242
+ )
243
+ const knownFolderPaths = useMemo(
244
+ () => new Set(previewGraph.folders.map((folder) => folder.path)),
245
+ [previewGraph],
246
+ )
247
+ const visibleBlocks = blueprintHidden
248
+ ? userBlocks.filter((block) => block.naming)
249
+ : userBlocks
250
+ const visibleIslands = blueprintHidden
251
+ ? userIslands.filter((island) => island.naming)
252
+ : userIslands
253
+ const pendingBlocks = visibleBlocks.filter(
254
+ (block) => block.naming || !knownFileIds.has(block.id),
255
+ )
256
+ const overlayBlocks = visibleBlocks.filter(
257
+ (block) => !block.naming && knownFileIds.has(block.id),
258
+ )
214
259
  const displayGraph = useMemo(
215
260
  () =>
216
261
  withBlueprintIntent(
217
- withUserCreatedGraph(previewGraph, userBlocks, userIslands),
262
+ withUserCreatedGraph(previewGraph, pendingBlocks, visibleIslands),
218
263
  blueprintFunctions,
219
264
  blueprintVariables,
220
265
  blueprintImports,
@@ -223,23 +268,23 @@ function Explorer({
223
268
  blueprintFunctions,
224
269
  blueprintImports,
225
270
  blueprintVariables,
271
+ pendingBlocks,
226
272
  previewGraph,
227
- userBlocks,
228
- userIslands,
273
+ visibleIslands,
229
274
  ],
230
275
  )
231
276
  const layout = useMemo(() => {
232
277
  const world = layoutWorld(
233
- withUserCreatedGraph(previewGraph, [], userIslands),
278
+ withUserCreatedGraph(previewGraph, [], visibleIslands),
234
279
  )
235
280
  if (previewing) markCreatedFolders(world, changeSet.createFolders ?? [])
236
- return withUserCreatedLayout(world, userBlocks, userIslands)
281
+ return withUserCreatedLayout(world, pendingBlocks, visibleIslands)
237
282
  }, [
238
283
  changeSet.createFolders,
284
+ pendingBlocks,
239
285
  previewGraph,
240
286
  previewing,
241
- userBlocks,
242
- userIslands,
287
+ visibleIslands,
243
288
  ])
244
289
  const changeFileIds = useMemo(() => {
245
290
  const ids = new Set<string>()
@@ -250,10 +295,12 @@ function Explorer({
250
295
  for (const item of blueprintFunctions) ids.add(item.file)
251
296
  for (const item of blueprintVariables) ids.add(item.file)
252
297
  for (const item of blueprintImports) ids.add(item.file)
298
+ for (const note of blueprintNotes) ids.add(note.file)
253
299
  return [...ids]
254
300
  }, [
255
301
  blueprintFunctions,
256
302
  blueprintImports,
303
+ blueprintNotes,
257
304
  blueprintVariables,
258
305
  changeSet.creates,
259
306
  changeSet.deletes,
@@ -315,7 +362,6 @@ function Explorer({
315
362
  const lastIntentSig = useRef<string | null>(null)
316
363
  const viewedDiffId = useRef<Record<string, string | null>>({})
317
364
  const browsingHistory = useRef<Record<string, boolean>>({})
318
- const loadedBlueprintSession = useRef<string | null>(null)
319
365
  const seenSessionIds = useRef<Set<string>>(new Set())
320
366
 
321
367
  const applyIntent = useCallback((next: AgentIntent, sessionId?: string) => {
@@ -463,6 +509,7 @@ function Explorer({
463
509
  addedFunctions: blueprintFunctions,
464
510
  addedVariables: blueprintVariables,
465
511
  addedImports: blueprintImports,
512
+ notes: blueprintNotes,
466
513
  }
467
514
  : {}),
468
515
  },
@@ -485,6 +532,7 @@ function Explorer({
485
532
  applyIntent,
486
533
  blueprintFunctions,
487
534
  blueprintImports,
535
+ blueprintNotes,
488
536
  blueprintVariables,
489
537
  intents,
490
538
  onRefreshGraph,
@@ -537,16 +585,7 @@ function Explorer({
537
585
  useEffect(() => {
538
586
  const onKey = (event: KeyboardEvent) => {
539
587
  if (event.repeat || event.code !== 'KeyM') return
540
- const target = event.target
541
- if (
542
- target instanceof HTMLElement &&
543
- (target.tagName === 'TEXTAREA' ||
544
- target.tagName === 'INPUT' ||
545
- target.tagName === 'SELECT' ||
546
- target.isContentEditable)
547
- ) {
548
- return
549
- }
588
+ if (shouldIgnoreShortcut(event)) return
550
589
  event.preventDefault()
551
590
  toggleMap()
552
591
  }
@@ -570,98 +609,78 @@ function Explorer({
570
609
  }
571
610
  }, [])
572
611
 
573
- useEffect(() => {
574
- if (!intent.sessionId) {
575
- loadedBlueprintSession.current = null
576
- setUserBlocks([])
577
- setUserIslands([])
578
- setBlueprintFunctions([])
579
- setBlueprintVariables([])
580
- setBlueprintImports([])
581
- return
582
- }
583
- const knownFiles = new Set(graph.files.map((file) => file.id))
584
- const knownFolders = new Set(graph.folders.map((folder) => folder.path))
585
- const nextBlocks = parseUserCreatedBlocks(intent.userCreatedBlocks).filter(
586
- (block) => !knownFiles.has(block.id),
587
- )
588
- const nextIslands = parseUserCreatedIslands(intent.userCreatedIslands).filter(
589
- (island) => !knownFolders.has(island.path),
590
- )
591
- const switched = loadedBlueprintSession.current !== intent.sessionId
592
- if (switched) {
593
- loadedBlueprintSession.current = intent.sessionId
594
- setUserBlocks(nextBlocks)
595
- setUserIslands(nextIslands)
596
- setBlueprintFunctions(intent.blueprintFunctions)
597
- setBlueprintVariables(intent.blueprintVariables)
598
- setBlueprintImports(intent.blueprintImports)
599
- return
600
- }
601
- if (intent.creationMode) {
602
- setUserBlocks((current) =>
603
- current.some((block) => block.naming) || current.length > 0
604
- ? current
605
- : nextBlocks,
606
- )
607
- setUserIslands((current) =>
608
- current.some((island) => island.naming) || current.length > 0
609
- ? current
610
- : nextIslands,
611
- )
612
- setBlueprintFunctions((current) =>
613
- current.length > 0 ? current : intent.blueprintFunctions,
614
- )
615
- setBlueprintVariables((current) =>
616
- current.length > 0 ? current : intent.blueprintVariables,
617
- )
618
- setBlueprintImports((current) =>
619
- current.length > 0 ? current : intent.blueprintImports,
620
- )
621
- return
622
- }
623
- setUserBlocks(nextBlocks)
624
- setUserIslands(nextIslands)
625
- setBlueprintFunctions(intent.blueprintFunctions)
626
- setBlueprintVariables(intent.blueprintVariables)
627
- setBlueprintImports(intent.blueprintImports)
628
- }, [
629
- intent.blueprintFunctions,
630
- intent.blueprintImports,
631
- intent.blueprintVariables,
632
- intent.creationMode,
633
- intent.sessionId,
634
- intent.userCreatedBlocks,
635
- intent.userCreatedIslands,
636
- graph,
637
- ])
638
-
639
612
  const persistBlueprint = useCallback(
640
613
  (
641
- blocks: UserCreatedBlock[] = userBlocks,
642
- islands: UserCreatedIsland[] = userIslands,
643
- functions: PatchSymbolAddition[] = blueprintFunctions,
644
- variables: PatchSymbolAddition[] = blueprintVariables,
645
- imports: PatchImportAddition[] = blueprintImports,
614
+ blocks: UserCreatedBlock[] = userBlocksRef.current,
615
+ islands: UserCreatedIsland[] = userIslandsRef.current,
616
+ functions: PatchSymbolAddition[] = blueprintFunctionsRef.current,
617
+ variables: PatchSymbolAddition[] = blueprintVariablesRef.current,
618
+ imports: PatchImportAddition[] = blueprintImportsRef.current,
619
+ notes: BlueprintNote[] = blueprintNotesRef.current,
646
620
  ) => {
647
- if (!intent.sessionId || !intent.creationMode) return
648
- persistSessionBlueprint(intent.sessionId, {
621
+ if (notePersistTimer.current != null) {
622
+ window.clearTimeout(notePersistTimer.current)
623
+ notePersistTimer.current = null
624
+ }
625
+ const gen = ++blueprintPersistGen.current
626
+ notesDirty.current = true
627
+ void persistSessionBlueprint(intent.sessionId, {
649
628
  userCreatedBlocks: namedCreatedBlocks(blocks),
650
629
  userCreatedIslands: namedCreatedIslands(islands),
651
630
  addedFunctions: functions,
652
631
  addedVariables: variables,
653
632
  addedImports: imports,
633
+ notes,
634
+ }).finally(() => {
635
+ if (gen !== blueprintPersistGen.current) return
636
+ if (notePersistTimer.current != null) return
637
+ notesDirty.current = false
654
638
  })
655
639
  },
656
- [
657
- blueprintFunctions,
658
- blueprintImports,
659
- blueprintVariables,
660
- intent.creationMode,
661
- intent.sessionId,
662
- userBlocks,
663
- userIslands,
664
- ],
640
+ [intent.sessionId],
641
+ )
642
+
643
+ const persistBlueprintSoon = useCallback(() => {
644
+ notesDirty.current = true
645
+ if (notePersistTimer.current != null) {
646
+ window.clearTimeout(notePersistTimer.current)
647
+ }
648
+ notePersistTimer.current = window.setTimeout(() => {
649
+ persistBlueprint()
650
+ }, 400)
651
+ }, [persistBlueprint])
652
+
653
+ useEffect(() => {
654
+ return () => {
655
+ if (notePersistTimer.current == null) return
656
+ window.clearTimeout(notePersistTimer.current)
657
+ notePersistTimer.current = null
658
+ if (!notesDirty.current) return
659
+ persistSessionBlueprint(intent.sessionId, {
660
+ userCreatedBlocks: namedCreatedBlocks(userBlocksRef.current),
661
+ userCreatedIslands: namedCreatedIslands(userIslandsRef.current),
662
+ addedFunctions: blueprintFunctionsRef.current,
663
+ addedVariables: blueprintVariablesRef.current,
664
+ addedImports: blueprintImportsRef.current,
665
+ notes: blueprintNotesRef.current,
666
+ })
667
+ notesDirty.current = false
668
+ }
669
+ }, [intent.sessionId])
670
+
671
+ const applyBlueprintNote = useCallback(
672
+ (next: {
673
+ file: string
674
+ kind: BlueprintNoteKind
675
+ name?: string
676
+ note: string
677
+ }) => {
678
+ const notes = setBlueprintNote(blueprintNotesRef.current, next)
679
+ blueprintNotesRef.current = notes
680
+ setBlueprintNotes(notes)
681
+ persistBlueprintSoon()
682
+ },
683
+ [persistBlueprintSoon],
665
684
  )
666
685
 
667
686
  const placeBlock = useCallback(
@@ -811,8 +830,11 @@ function Explorer({
811
830
  const selected = userBlocks.find((block) => block.id === selectedId)
812
831
  if (!selected || selected.naming) return false
813
832
  const next = userBlocks.filter((block) => block.id !== selectedId)
833
+ const notes = dropBlueprintFileNotes(blueprintNotesRef.current, [selectedId])
834
+ blueprintNotesRef.current = notes
814
835
  setUserBlocks(next)
815
- persistBlueprint(next, userIslands)
836
+ setBlueprintNotes(notes)
837
+ persistBlueprint(next, userIslands, undefined, undefined, undefined, notes)
816
838
  setSelectedId(null)
817
839
  return true
818
840
  }, [canPlace, persistBlueprint, selectedId, userBlocks, userIslands])
@@ -971,8 +993,23 @@ function Explorer({
971
993
  const next = blueprintFunctions.filter(
972
994
  (item) => !(item.file === fileId && item.name === name),
973
995
  )
996
+ const notes = dropBlueprintSymbolNote(
997
+ blueprintNotesRef.current,
998
+ fileId,
999
+ 'function',
1000
+ name,
1001
+ )
1002
+ blueprintNotesRef.current = notes
974
1003
  setBlueprintFunctions(next)
975
- persistBlueprint(userBlocks, userIslands, next, blueprintVariables, blueprintImports)
1004
+ setBlueprintNotes(notes)
1005
+ persistBlueprint(
1006
+ userBlocks,
1007
+ userIslands,
1008
+ next,
1009
+ blueprintVariables,
1010
+ blueprintImports,
1011
+ notes,
1012
+ )
976
1013
  },
977
1014
  [
978
1015
  blueprintFunctions,
@@ -991,8 +1028,23 @@ function Explorer({
991
1028
  const next = blueprintVariables.filter(
992
1029
  (item) => !(item.file === fileId && item.name === name),
993
1030
  )
1031
+ const notes = dropBlueprintSymbolNote(
1032
+ blueprintNotesRef.current,
1033
+ fileId,
1034
+ 'variable',
1035
+ name,
1036
+ )
1037
+ blueprintNotesRef.current = notes
994
1038
  setBlueprintVariables(next)
995
- persistBlueprint(userBlocks, userIslands, blueprintFunctions, next, blueprintImports)
1039
+ setBlueprintNotes(notes)
1040
+ persistBlueprint(
1041
+ userBlocks,
1042
+ userIslands,
1043
+ blueprintFunctions,
1044
+ next,
1045
+ blueprintImports,
1046
+ notes,
1047
+ )
996
1048
  },
997
1049
  [
998
1050
  blueprintFunctions,
@@ -1036,6 +1088,7 @@ function Explorer({
1036
1088
  if (!namingId && !namingIslandId) return
1037
1089
  const onKey = (event: KeyboardEvent) => {
1038
1090
  if (event.code !== 'Escape') return
1091
+ if (shouldIgnoreShortcut(event)) return
1039
1092
  event.preventDefault()
1040
1093
  if (namingId) cancelBlockName(namingId)
1041
1094
  if (namingIslandId) cancelIslandName(namingIslandId)
@@ -1051,16 +1104,7 @@ function Explorer({
1051
1104
  useEffect(() => {
1052
1105
  const onKey = (event: KeyboardEvent) => {
1053
1106
  if (event.repeat || event.code !== 'Backspace') return
1054
- const target = event.target
1055
- if (
1056
- target instanceof HTMLElement &&
1057
- (target.tagName === 'TEXTAREA' ||
1058
- target.tagName === 'INPUT' ||
1059
- target.tagName === 'SELECT' ||
1060
- target.isContentEditable)
1061
- ) {
1062
- return
1063
- }
1107
+ if (shouldIgnoreShortcut(event)) return
1064
1108
  if (!deleteSelectedCreatedBlock()) return
1065
1109
  event.preventDefault()
1066
1110
  }
@@ -1100,16 +1144,7 @@ function Explorer({
1100
1144
  useEffect(() => {
1101
1145
  const onKey = (event: KeyboardEvent) => {
1102
1146
  if (event.repeat || event.code !== 'KeyK') return
1103
- const target = event.target
1104
- if (
1105
- target instanceof HTMLElement &&
1106
- (target.tagName === 'TEXTAREA' ||
1107
- target.tagName === 'INPUT' ||
1108
- target.tagName === 'SELECT' ||
1109
- target.isContentEditable)
1110
- ) {
1111
- return
1112
- }
1147
+ if (shouldIgnoreShortcut(event)) return
1113
1148
  event.preventDefault()
1114
1149
  toggleImportedBy()
1115
1150
  }
@@ -1120,16 +1155,7 @@ function Explorer({
1120
1155
  useEffect(() => {
1121
1156
  const onKey = (event: KeyboardEvent) => {
1122
1157
  if (event.repeat || event.code !== 'KeyG') return
1123
- const target = event.target
1124
- if (
1125
- target instanceof HTMLElement &&
1126
- (target.tagName === 'TEXTAREA' ||
1127
- target.tagName === 'INPUT' ||
1128
- target.tagName === 'SELECT' ||
1129
- target.isContentEditable)
1130
- ) {
1131
- return
1132
- }
1158
+ if (shouldIgnoreShortcut(event)) return
1133
1159
  if (!canToggleBranchChanges) return
1134
1160
  event.preventDefault()
1135
1161
  toggleShowBranchChanges()
@@ -1163,16 +1189,7 @@ function Explorer({
1163
1189
  if (mode !== 'map' || !hasChangeSet) return
1164
1190
  const onKey = (event: KeyboardEvent) => {
1165
1191
  if (event.repeat || event.code !== 'KeyC') return
1166
- const target = event.target
1167
- if (
1168
- target instanceof HTMLElement &&
1169
- (target.tagName === 'TEXTAREA' ||
1170
- target.tagName === 'INPUT' ||
1171
- target.tagName === 'SELECT' ||
1172
- target.isContentEditable)
1173
- ) {
1174
- return
1175
- }
1192
+ if (shouldIgnoreShortcut(event)) return
1176
1193
  event.preventDefault()
1177
1194
  toggleChangePathsOnly()
1178
1195
  }
@@ -1186,6 +1203,29 @@ function Explorer({
1186
1203
  try {
1187
1204
  const bundle = await fetchAgentIntents()
1188
1205
  if (cancelled) return
1206
+ setBlueprintHidden(Boolean(bundle.blueprint.hidden))
1207
+ const nextBlocks = parseUserCreatedBlocks(bundle.blueprint.userCreatedBlocks)
1208
+ const nextIslands = parseUserCreatedIslands(bundle.blueprint.userCreatedIslands)
1209
+ setUserBlocks((current) => {
1210
+ const drafts = current.filter((block) => block.naming)
1211
+ if (drafts.length === 0) return nextBlocks
1212
+ const namedIds = new Set(drafts.map((block) => block.id))
1213
+ return [...nextBlocks.filter((block) => !namedIds.has(block.id)), ...drafts]
1214
+ })
1215
+ setUserIslands((current) => {
1216
+ const drafts = current.filter((island) => island.naming)
1217
+ if (drafts.length === 0) return nextIslands
1218
+ const namedIds = new Set(drafts.map((island) => island.id))
1219
+ return [...nextIslands.filter((island) => !namedIds.has(island.id)), ...drafts]
1220
+ })
1221
+ setBlueprintFunctions(bundle.blueprint.addedFunctions)
1222
+ setBlueprintVariables(bundle.blueprint.addedVariables)
1223
+ setBlueprintImports(bundle.blueprint.addedImports)
1224
+ if (!notesDirty.current && !isKeyboardIsolated()) {
1225
+ const nextNotes = parseBlueprintNotes(bundle.blueprint.notes)
1226
+ blueprintNotesRef.current = nextNotes
1227
+ setBlueprintNotes(nextNotes)
1228
+ }
1189
1229
  setNextAttachSessionId(bundle.nextAttachSessionId)
1190
1230
  const merged: AgentIntent[] = []
1191
1231
  for (const next of bundle.intents) {
@@ -1258,10 +1298,69 @@ function Explorer({
1258
1298
  ...blueprintImportEdges,
1259
1299
  ]
1260
1300
  const deletedIds = previewing ? changeSet.deletes : []
1301
+ const blueprintHasContent =
1302
+ namedCreatedBlocks(userBlocks).length > 0 ||
1303
+ namedCreatedIslands(userIslands).length > 0 ||
1304
+ blueprintFunctions.length > 0 ||
1305
+ blueprintVariables.length > 0 ||
1306
+ blueprintImports.length > 0 ||
1307
+ blueprintNotes.length > 0
1308
+ const blueprintCanCleanup =
1309
+ userBlocks.some((block) => !block.naming && knownFileIds.has(block.id)) ||
1310
+ userIslands.some(
1311
+ (island) => !island.naming && knownFolderPaths.has(island.path),
1312
+ )
1313
+
1314
+ const toggleBlueprintHidden = useCallback(() => {
1315
+ const next = !blueprintHidden
1316
+ setBlueprintHidden(next)
1317
+ void persistBlueprintHidden(next).catch(() => setBlueprintHidden(!next))
1318
+ }, [blueprintHidden])
1319
+
1320
+ const clearSharedBlueprint = useCallback(() => {
1321
+ setUserBlocks([])
1322
+ setUserIslands([])
1323
+ setBlueprintFunctions([])
1324
+ setBlueprintVariables([])
1325
+ setBlueprintImports([])
1326
+ blueprintNotesRef.current = []
1327
+ setBlueprintNotes([])
1328
+ void persistBlueprintClear()
1329
+ }, [])
1330
+
1331
+ const cleanupSharedBlueprint = useCallback(() => {
1332
+ const files = knownFileIds
1333
+ const folders = knownFolderPaths
1334
+ const removed = new Set(
1335
+ userBlocks
1336
+ .filter((block) => !block.naming && files.has(block.id))
1337
+ .map((block) => block.id),
1338
+ )
1339
+ setUserBlocks((current) =>
1340
+ current.filter((block) => block.naming || !files.has(block.id)),
1341
+ )
1342
+ setUserIslands((current) =>
1343
+ current.filter((island) => island.naming || !folders.has(island.path)),
1344
+ )
1345
+ setBlueprintFunctions((current) =>
1346
+ current.filter((item) => !removed.has(item.file)),
1347
+ )
1348
+ setBlueprintVariables((current) =>
1349
+ current.filter((item) => !removed.has(item.file)),
1350
+ )
1351
+ setBlueprintImports((current) =>
1352
+ current.filter((item) => !removed.has(item.file)),
1353
+ )
1354
+ const notes = dropBlueprintFileNotes(blueprintNotesRef.current, removed)
1355
+ blueprintNotesRef.current = notes
1356
+ setBlueprintNotes(notes)
1357
+ void persistBlueprintCleanup()
1358
+ }, [knownFileIds, knownFolderPaths, userBlocks])
1261
1359
 
1262
1360
  return (
1263
1361
  <>
1264
1362
  <div className="stage">
1363
+ <CanvasErrorBoundary>
1265
1364
  <Canvas
1266
1365
  shadows={false}
1267
1366
  dpr={[1, 1.5]}
@@ -1307,8 +1406,11 @@ function Explorer({
1307
1406
  onBlueprintMenu={canPlace ? setMapMenu : undefined}
1308
1407
  onCommitName={commitBlockName}
1309
1408
  onCancelName={cancelBlockName}
1310
- userCreatedBlocks={userBlocks}
1311
- userCreatedIslands={userIslands}
1409
+ onCommitIslandName={commitIslandName}
1410
+ onCancelIslandName={cancelIslandName}
1411
+ userCreatedBlocks={visibleBlocks}
1412
+ userCreatedIslands={visibleIslands}
1413
+ overlayBlocks={overlayBlocks}
1312
1414
  mapGraph={
1313
1415
  changePathsOnly && hasChangeSet ? changePathGraph : null
1314
1416
  }
@@ -1317,6 +1419,7 @@ function Explorer({
1317
1419
  }
1318
1420
  />
1319
1421
  </Canvas>
1422
+ </CanvasErrorBoundary>
1320
1423
  </div>
1321
1424
  <HUD
1322
1425
  graph={displayGraph}
@@ -1365,12 +1468,14 @@ function Explorer({
1365
1468
  blueprintFunctions={blueprintFunctions}
1366
1469
  blueprintVariables={blueprintVariables}
1367
1470
  blueprintImports={blueprintImports}
1471
+ blueprintNotes={blueprintNotes}
1368
1472
  onAddBlueprintFunction={addBlueprintFunction}
1369
1473
  onAddBlueprintVariable={addBlueprintVariable}
1370
1474
  onAddBlueprintImport={addBlueprintImport}
1371
1475
  onRemoveBlueprintFunction={removeBlueprintFunction}
1372
1476
  onRemoveBlueprintVariable={removeBlueprintVariable}
1373
1477
  onRemoveBlueprintImport={removeBlueprintImport}
1478
+ onSetBlueprintNote={applyBlueprintNote}
1374
1479
  onMapAddFile={placeBlockOnFolder}
1375
1480
  onMapAddFolder={placeIslandOnFolder}
1376
1481
  onInspectFile={inspectFile}
@@ -1378,6 +1483,12 @@ function Explorer({
1378
1483
  plannedIds={plannedIds}
1379
1484
  createdIds={plannedCreates}
1380
1485
  deletedIds={deletedIds}
1486
+ blueprintHidden={blueprintHidden}
1487
+ blueprintHasContent={blueprintHasContent}
1488
+ blueprintCanCleanup={blueprintCanCleanup}
1489
+ onToggleBlueprintHidden={toggleBlueprintHidden}
1490
+ onClearBlueprint={clearSharedBlueprint}
1491
+ onCleanupBlueprint={cleanupSharedBlueprint}
1381
1492
  />
1382
1493
  <MapContextMenu
1383
1494
  menu={mapMenu}