@jkwd/inbase 0.1.0

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 (44) hide show
  1. package/README.md +76 -0
  2. package/apps/explorer/index.html +12 -0
  3. package/apps/explorer/package.json +28 -0
  4. package/apps/explorer/scripts/js-source.mjs +188 -0
  5. package/apps/explorer/scripts/patch-lib.d.ts +115 -0
  6. package/apps/explorer/scripts/patch-lib.mjs +472 -0
  7. package/apps/explorer/scripts/scan-target.mjs +188 -0
  8. package/apps/explorer/scripts/session-store.d.ts +156 -0
  9. package/apps/explorer/scripts/session-store.mjs +809 -0
  10. package/apps/explorer/scripts/target-config.d.ts +8 -0
  11. package/apps/explorer/scripts/target-config.mjs +42 -0
  12. package/apps/explorer/src/App.tsx +941 -0
  13. package/apps/explorer/src/agentIntent.ts +182 -0
  14. package/apps/explorer/src/codebase.ts +15 -0
  15. package/apps/explorer/src/index.css +632 -0
  16. package/apps/explorer/src/layout.ts +508 -0
  17. package/apps/explorer/src/main.tsx +16 -0
  18. package/apps/explorer/src/scene/BlockPlacer.tsx +87 -0
  19. package/apps/explorer/src/scene/Bridge.tsx +290 -0
  20. package/apps/explorer/src/scene/FileBlock.tsx +256 -0
  21. package/apps/explorer/src/scene/FolderArea.tsx +96 -0
  22. package/apps/explorer/src/scene/IslandPlacer.tsx +40 -0
  23. package/apps/explorer/src/scene/MapSelectBorder.tsx +57 -0
  24. package/apps/explorer/src/scene/MapView.tsx +247 -0
  25. package/apps/explorer/src/scene/Player.tsx +245 -0
  26. package/apps/explorer/src/scene/RelationLines.tsx +223 -0
  27. package/apps/explorer/src/scene/SelectionController.tsx +89 -0
  28. package/apps/explorer/src/scene/UserContextTracker.tsx +152 -0
  29. package/apps/explorer/src/scene/World.tsx +323 -0
  30. package/apps/explorer/src/theme.ts +111 -0
  31. package/apps/explorer/src/types.ts +245 -0
  32. package/apps/explorer/src/ui/CanvasErrorBoundary.tsx +38 -0
  33. package/apps/explorer/src/ui/HUD.tsx +1090 -0
  34. package/apps/explorer/src/ui/NameInput.tsx +45 -0
  35. package/apps/explorer/src/userContext.ts +73 -0
  36. package/apps/explorer/src/userCreated.ts +354 -0
  37. package/apps/explorer/src/vite-env.d.ts +1 -0
  38. package/apps/explorer/tsconfig.json +21 -0
  39. package/apps/explorer/vite.config.ts +295 -0
  40. package/bin/inbase.mjs +170 -0
  41. package/bin/project.mjs +94 -0
  42. package/bin/session.mjs +241 -0
  43. package/package.json +63 -0
  44. package/skill/inbase/SKILL.md +167 -0
@@ -0,0 +1,941 @@
1
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
2
+ import { Canvas } from '@react-three/fiber'
3
+ import { emptyIntent, fetchAgentIntent, performAgentAction, persistSessionBlueprint } from './agentIntent'
4
+ import { fetchCodebase } from './codebase'
5
+ import {
6
+ layoutWorld,
7
+ markCreatedFolders,
8
+ standInFront,
9
+ relationTravelTarget,
10
+ withPreviewGraph,
11
+ } from './layout'
12
+ import { World } from './scene/World'
13
+ import { HUD } from './ui/HUD'
14
+ import {
15
+ fetchUserContext,
16
+ persistFollowLook,
17
+ persistUserContext,
18
+ } from './userContext'
19
+ import {
20
+ defaultBlockSpot,
21
+ isBlueprintSymbolName,
22
+ namedCreatedBlocks,
23
+ namedCreatedIslands,
24
+ parseBlueprintImport,
25
+ parseUserCreatedBlocks,
26
+ parseUserCreatedIslands,
27
+ resolveCreatedFile,
28
+ resolveCreatedIsland,
29
+ withBlueprintIntent,
30
+ withUserCreatedGraph,
31
+ withUserCreatedLayout,
32
+ } from './userCreated'
33
+ import {
34
+ isPatchPreview,
35
+ type AgentIntent,
36
+ type AimedRelation,
37
+ type CodebaseGraph,
38
+ type FlyTo,
39
+ type PatchImportAddition,
40
+ type PatchSymbolAddition,
41
+ type UserCreatedBlock,
42
+ type UserCreatedIsland,
43
+ type ViewMode,
44
+ type WorkflowAction,
45
+ } from './types'
46
+
47
+ function intentSignature(intent: AgentIntent) {
48
+ return JSON.stringify({
49
+ updatedAt: intent.updatedAt,
50
+ status: intent.status,
51
+ phase: intent.phase,
52
+ sessionId: intent.sessionId,
53
+ creationMode: intent.creationMode,
54
+ diffId: intent.diffId,
55
+ chain: intent.chain,
56
+ files: intent.files,
57
+ creates: intent.creates,
58
+ deletes: intent.deletes,
59
+ createFolders: intent.createFolders,
60
+ imports: intent.imports,
61
+ addedFunctions: intent.addedFunctions,
62
+ addedVariables: intent.addedVariables,
63
+ addedImports: intent.addedImports,
64
+ })
65
+ }
66
+
67
+ export default function App() {
68
+ const [graph, setGraph] = useState<CodebaseGraph | null>(null)
69
+ const [loadError, setLoadError] = useState<string | null>(null)
70
+
71
+ const refreshGraph = useCallback(async () => {
72
+ const next = await fetchCodebase()
73
+ if (next) {
74
+ setGraph(next)
75
+ setLoadError(null)
76
+ return
77
+ }
78
+ setLoadError((current) => current ?? 'Could not load the project map.')
79
+ }, [])
80
+
81
+ useEffect(() => {
82
+ void refreshGraph()
83
+ }, [refreshGraph])
84
+
85
+ if (!graph) {
86
+ return (
87
+ <div className="boot">
88
+ <p>{loadError ?? 'Loading map…'}</p>
89
+ </div>
90
+ )
91
+ }
92
+
93
+ return <Explorer graph={graph} onRefreshGraph={refreshGraph} />
94
+ }
95
+
96
+ function Explorer({
97
+ graph,
98
+ onRefreshGraph,
99
+ }: {
100
+ graph: CodebaseGraph
101
+ onRefreshGraph: () => Promise<void>
102
+ }) {
103
+ const [intent, setIntent] = useState<AgentIntent>(emptyIntent)
104
+ const previewing = intent.preview || isPatchPreview(intent.status)
105
+ const plannedCreates = previewing ? intent.creates : []
106
+ const [userBlocks, setUserBlocks] = useState<UserCreatedBlock[]>([])
107
+ const [userIslands, setUserIslands] = useState<UserCreatedIsland[]>([])
108
+ const [blueprintFunctions, setBlueprintFunctions] = useState<
109
+ PatchSymbolAddition[]
110
+ >([])
111
+ const [blueprintVariables, setBlueprintVariables] = useState<
112
+ PatchSymbolAddition[]
113
+ >([])
114
+ const [blueprintImports, setBlueprintImports] = useState<
115
+ PatchImportAddition[]
116
+ >([])
117
+ const namingId = userBlocks.find((block) => block.naming)?.id ?? null
118
+ const namingIslandId = userIslands.find((island) => island.naming)?.id ?? null
119
+ const naming = Boolean(namingId || namingIslandId)
120
+ const previewGraph = useMemo(() => {
121
+ if (!previewing) return graph
122
+ return withPreviewGraph(
123
+ graph,
124
+ plannedCreates,
125
+ intent.createLines ?? {},
126
+ intent.createFolders ?? [],
127
+ intent.imports ?? [],
128
+ )
129
+ }, [
130
+ graph,
131
+ intent.createFolders,
132
+ intent.createLines,
133
+ intent.imports,
134
+ plannedCreates,
135
+ previewing,
136
+ ])
137
+ const displayGraph = useMemo(
138
+ () =>
139
+ withBlueprintIntent(
140
+ withUserCreatedGraph(previewGraph, userBlocks, userIslands),
141
+ blueprintFunctions,
142
+ blueprintVariables,
143
+ blueprintImports,
144
+ ),
145
+ [
146
+ blueprintFunctions,
147
+ blueprintImports,
148
+ blueprintVariables,
149
+ previewGraph,
150
+ userBlocks,
151
+ userIslands,
152
+ ],
153
+ )
154
+ const layout = useMemo(() => {
155
+ const world = layoutWorld(previewGraph)
156
+ if (previewing) markCreatedFolders(world, intent.createFolders ?? [])
157
+ return withUserCreatedLayout(world, userBlocks, userIslands)
158
+ }, [intent.createFolders, previewGraph, previewing, userBlocks, userIslands])
159
+ const [mode, setMode] = useState<ViewMode>('map')
160
+ const [landAt, setLandAt] = useState<[number, number]>([
161
+ layout.spawn[0],
162
+ layout.spawn[2],
163
+ ])
164
+ const walkPos = useRef<[number, number]>([layout.spawn[0], layout.spawn[2]])
165
+ const [selectedId, setSelectedId] = useState<string | null>(null)
166
+ const [selectedFolder, setSelectedFolder] = useState<string | null>(null)
167
+ const [aimedRelation, setAimedRelation] = useState<AimedRelation | null>(null)
168
+ const [flyTo, setFlyTo] = useState<FlyTo | null>(null)
169
+ const [locked, setLocked] = useState(false)
170
+ const [currentFolder, setCurrentFolder] = useState(graph.targetName)
171
+ const [followLook, setFollowLook] = useState(false)
172
+ const [importedBy, setImportedBy] = useState(false)
173
+ const lastIntentSig = useRef<string | null>(null)
174
+ const viewedDiffId = useRef<string | null>(null)
175
+ const browsingHistory = useRef(false)
176
+
177
+ const applyIntent = useCallback((next: AgentIntent) => {
178
+ setIntent(next)
179
+ viewedDiffId.current = next.diffId
180
+ }, [])
181
+
182
+ const rememberWalk = useCallback((x: number, z: number) => {
183
+ walkPos.current = [x, z]
184
+ }, [])
185
+
186
+ const openMap = useCallback(() => {
187
+ setLandAt(walkPos.current)
188
+ setLocked(false)
189
+ document.exitPointerLock()
190
+ setMode('map')
191
+ }, [])
192
+
193
+ const openWalk = useCallback(() => {
194
+ setMode('walk')
195
+ }, [])
196
+
197
+ const toggleMap = useCallback(() => {
198
+ if (mode === 'walk') {
199
+ setLandAt(walkPos.current)
200
+ setLocked(false)
201
+ document.exitPointerLock()
202
+ setMode('map')
203
+ return
204
+ }
205
+ setMode('walk')
206
+ }, [mode])
207
+
208
+ const land = useCallback((x: number, z: number) => {
209
+ walkPos.current = [x, z]
210
+ setFlyTo(null)
211
+ setLandAt([x, z])
212
+ setMode('walk')
213
+ }, [])
214
+
215
+ const travelToFile = useCallback(
216
+ (fileId: string, fly: boolean) => {
217
+ const placed = layout.files[fileId]
218
+ if (!placed) return
219
+ const [x, z] = standInFront(placed)
220
+ walkPos.current = [x, z]
221
+ setAimedRelation(null)
222
+ setLandAt([x, z])
223
+ setFlyTo(
224
+ fly
225
+ ? {
226
+ nonce: Date.now(),
227
+ lookAt: [placed.position[0], placed.position[1], placed.position[2]],
228
+ }
229
+ : null,
230
+ )
231
+ setMode('walk')
232
+ },
233
+ [layout.files],
234
+ )
235
+
236
+ const flyAlongRelation = useCallback(
237
+ (fromId: string, toId: string) => {
238
+ const [x, z] = walkPos.current
239
+ travelToFile(relationTravelTarget(fromId, toId, x, z, layout.files), true)
240
+ },
241
+ [layout.files, travelToFile],
242
+ )
243
+
244
+ const runWorkflowAction = useCallback(
245
+ async (
246
+ action: WorkflowAction,
247
+ options: { instruction?: string; step?: number } = {},
248
+ ) => {
249
+ if (!intent.sessionId) return
250
+ if (
251
+ (action === 'continue' || action === 'instruct') &&
252
+ (!intent.diffId || !intent.isActiveDiff)
253
+ ) {
254
+ return
255
+ }
256
+ try {
257
+ const next = await performAgentAction(
258
+ action,
259
+ intent.sessionId,
260
+ {
261
+ ...options,
262
+ diffId: intent.diffId ?? undefined,
263
+ ...(action === 'blueprint_send'
264
+ ? {
265
+ userCreatedBlocks: namedCreatedBlocks(userBlocks),
266
+ userCreatedIslands: namedCreatedIslands(userIslands),
267
+ addedFunctions: blueprintFunctions,
268
+ addedVariables: blueprintVariables,
269
+ addedImports: blueprintImports,
270
+ }
271
+ : {}),
272
+ },
273
+ )
274
+ browsingHistory.current = false
275
+ lastIntentSig.current = intentSignature(next)
276
+ applyIntent(next)
277
+ if (action === 'invoke' || action === 'continue') {
278
+ await onRefreshGraph()
279
+ }
280
+ } catch {
281
+ // Keep the pending patch visible if apply failed.
282
+ }
283
+ },
284
+ [
285
+ applyIntent,
286
+ blueprintFunctions,
287
+ blueprintImports,
288
+ blueprintVariables,
289
+ intent.diffId,
290
+ intent.isActiveDiff,
291
+ intent.sessionId,
292
+ onRefreshGraph,
293
+ userBlocks,
294
+ userIslands,
295
+ ],
296
+ )
297
+
298
+ const navigateDiff = useCallback(
299
+ async (diffId: string) => {
300
+ try {
301
+ const latest = intent.chain.at(-1)?.id
302
+ browsingHistory.current = diffId !== latest
303
+ const next = await fetchAgentIntent(diffId)
304
+ lastIntentSig.current = intentSignature(next)
305
+ applyIntent(next)
306
+ } catch {
307
+ // Keep the current chain position if navigation failed.
308
+ }
309
+ },
310
+ [applyIntent, intent.chain],
311
+ )
312
+
313
+ useEffect(() => {
314
+ const onKey = (event: KeyboardEvent) => {
315
+ if (event.repeat || event.code !== 'KeyM') return
316
+ const target = event.target
317
+ if (
318
+ target instanceof HTMLElement &&
319
+ (target.tagName === 'TEXTAREA' ||
320
+ target.tagName === 'INPUT' ||
321
+ target.tagName === 'SELECT' ||
322
+ target.isContentEditable)
323
+ ) {
324
+ return
325
+ }
326
+ event.preventDefault()
327
+ toggleMap()
328
+ }
329
+ window.addEventListener('keydown', onKey)
330
+ return () => window.removeEventListener('keydown', onKey)
331
+ }, [toggleMap])
332
+
333
+ useEffect(() => {
334
+ let cancelled = false
335
+ void fetchUserContext().then((context) => {
336
+ if (cancelled) return
337
+ if (typeof context?.followLook === 'boolean') {
338
+ setFollowLook(context.followLook)
339
+ }
340
+ })
341
+ return () => {
342
+ cancelled = true
343
+ }
344
+ }, [])
345
+
346
+ useEffect(() => {
347
+ if (!intent.sessionId) {
348
+ setUserBlocks([])
349
+ setUserIslands([])
350
+ setBlueprintFunctions([])
351
+ setBlueprintVariables([])
352
+ setBlueprintImports([])
353
+ return
354
+ }
355
+ const knownFiles = new Set(graph.files.map((file) => file.id))
356
+ const knownFolders = new Set(graph.folders.map((folder) => folder.path))
357
+ const nextBlocks = parseUserCreatedBlocks(intent.userCreatedBlocks).filter(
358
+ (block) => !knownFiles.has(block.id),
359
+ )
360
+ const nextIslands = parseUserCreatedIslands(intent.userCreatedIslands).filter(
361
+ (island) => !knownFolders.has(island.path),
362
+ )
363
+ if (intent.creationMode) {
364
+ setUserBlocks((current) =>
365
+ current.some((block) => block.naming) || current.length > 0
366
+ ? current
367
+ : nextBlocks,
368
+ )
369
+ setUserIslands((current) =>
370
+ current.some((island) => island.naming) || current.length > 0
371
+ ? current
372
+ : nextIslands,
373
+ )
374
+ setBlueprintFunctions((current) =>
375
+ current.length > 0 ? current : intent.blueprintFunctions,
376
+ )
377
+ setBlueprintVariables((current) =>
378
+ current.length > 0 ? current : intent.blueprintVariables,
379
+ )
380
+ setBlueprintImports((current) =>
381
+ current.length > 0 ? current : intent.blueprintImports,
382
+ )
383
+ return
384
+ }
385
+ setUserBlocks(nextBlocks)
386
+ setUserIslands(nextIslands)
387
+ setBlueprintFunctions(intent.blueprintFunctions)
388
+ setBlueprintVariables(intent.blueprintVariables)
389
+ setBlueprintImports(intent.blueprintImports)
390
+ }, [
391
+ intent.blueprintFunctions,
392
+ intent.blueprintImports,
393
+ intent.blueprintVariables,
394
+ intent.creationMode,
395
+ intent.userCreatedBlocks,
396
+ intent.userCreatedIslands,
397
+ graph,
398
+ ])
399
+
400
+ const persistBlueprint = useCallback(
401
+ (
402
+ blocks: UserCreatedBlock[] = userBlocks,
403
+ islands: UserCreatedIsland[] = userIslands,
404
+ functions: PatchSymbolAddition[] = blueprintFunctions,
405
+ variables: PatchSymbolAddition[] = blueprintVariables,
406
+ imports: PatchImportAddition[] = blueprintImports,
407
+ ) => {
408
+ if (!intent.sessionId || !intent.creationMode) return
409
+ persistSessionBlueprint(intent.sessionId, {
410
+ userCreatedBlocks: namedCreatedBlocks(blocks),
411
+ userCreatedIslands: namedCreatedIslands(islands),
412
+ addedFunctions: functions,
413
+ addedVariables: variables,
414
+ addedImports: imports,
415
+ })
416
+ },
417
+ [
418
+ blueprintFunctions,
419
+ blueprintImports,
420
+ blueprintVariables,
421
+ intent.creationMode,
422
+ intent.sessionId,
423
+ userBlocks,
424
+ userIslands,
425
+ ],
426
+ )
427
+
428
+ const placeBlock = useCallback(
429
+ (spot: { x: number; z: number; folder: string }) => {
430
+ if (!intent.creationMode) return
431
+ setUserBlocks((current) => {
432
+ if (current.some((block) => block.naming)) return current
433
+ return [
434
+ ...current,
435
+ {
436
+ id: `draft:${Date.now()}`,
437
+ name: '',
438
+ path: '',
439
+ folder: spot.folder,
440
+ x: spot.x,
441
+ z: spot.z,
442
+ naming: true,
443
+ },
444
+ ]
445
+ })
446
+ document.exitPointerLock()
447
+ },
448
+ [intent.creationMode],
449
+ )
450
+
451
+ const placeBlockOnFolder = useCallback(
452
+ (folderPath: string) => {
453
+ if (!intent.creationMode) return
454
+ const fileCount = displayGraph.files.filter(
455
+ (file) => file.folder === folderPath,
456
+ ).length
457
+ const spot = defaultBlockSpot(layout, folderPath, fileCount)
458
+ if (!spot) return
459
+ placeBlock(spot)
460
+ },
461
+ [displayGraph.files, intent.creationMode, layout, placeBlock],
462
+ )
463
+
464
+ const commitBlockName = useCallback(
465
+ (id: string, rawName: string) => {
466
+ const draft = userBlocks.find((block) => block.id === id)
467
+ if (!draft) return false
468
+ const resolved = resolveCreatedFile(rawName, draft.folder)
469
+ if (!resolved) return false
470
+ const taken =
471
+ graph.files.some((file) => file.id === resolved.id) ||
472
+ userBlocks.some(
473
+ (block) => block.id === resolved.id && block.id !== id,
474
+ )
475
+ if (taken) return false
476
+ const next = userBlocks.map((block) =>
477
+ block.id === id
478
+ ? {
479
+ ...resolved,
480
+ x: draft.x,
481
+ z: draft.z,
482
+ }
483
+ : block,
484
+ )
485
+ setUserBlocks(next)
486
+ persistBlueprint(next, userIslands)
487
+ return true
488
+ },
489
+ [graph.files, persistBlueprint, userBlocks, userIslands],
490
+ )
491
+
492
+ const cancelBlockName = useCallback((id: string) => {
493
+ setUserBlocks((current) => current.filter((block) => block.id !== id))
494
+ }, [])
495
+
496
+ const placeIsland = useCallback(
497
+ (parent: string) => {
498
+ if (!intent.creationMode) return
499
+ setUserIslands((current) => {
500
+ if (current.some((island) => island.naming)) return current
501
+ return [
502
+ ...current,
503
+ {
504
+ id: `draft:${Date.now()}`,
505
+ name: '',
506
+ path: '',
507
+ parent,
508
+ naming: true,
509
+ },
510
+ ]
511
+ })
512
+ document.exitPointerLock()
513
+ },
514
+ [intent.creationMode],
515
+ )
516
+
517
+ const placeIslandOnFolder = useCallback(
518
+ (parent: string) => {
519
+ if (!intent.creationMode) return
520
+ placeIsland(parent)
521
+ },
522
+ [intent.creationMode, placeIsland],
523
+ )
524
+
525
+ const commitIslandName = useCallback(
526
+ (id: string, rawName: string) => {
527
+ const draft = userIslands.find((island) => island.id === id)
528
+ if (!draft) return false
529
+ const resolved = resolveCreatedIsland(rawName, draft.parent)
530
+ if (!resolved) return false
531
+ const taken =
532
+ graph.folders.some((folder) => folder.path === resolved.path) ||
533
+ userIslands.some(
534
+ (island) => island.path === resolved.path && island.id !== id,
535
+ )
536
+ if (taken) return false
537
+ const next = userIslands.map((island) =>
538
+ island.id === id
539
+ ? {
540
+ ...resolved,
541
+ }
542
+ : island,
543
+ )
544
+ setUserIslands(next)
545
+ persistBlueprint(userBlocks, next)
546
+ return true
547
+ },
548
+ [graph.folders, persistBlueprint, userBlocks, userIslands],
549
+ )
550
+
551
+ const cancelIslandName = useCallback((id: string) => {
552
+ setUserIslands((current) => current.filter((island) => island.id !== id))
553
+ }, [])
554
+
555
+ const deleteSelectedCreatedBlock = useCallback(() => {
556
+ if (!intent.creationMode || !selectedId) return false
557
+ const selected = userBlocks.find((block) => block.id === selectedId)
558
+ if (!selected || selected.naming) return false
559
+ const next = userBlocks.filter((block) => block.id !== selectedId)
560
+ setUserBlocks(next)
561
+ persistBlueprint(next, userIslands)
562
+ setSelectedId(null)
563
+ return true
564
+ }, [intent.creationMode, persistBlueprint, selectedId, userBlocks, userIslands])
565
+
566
+ const selectFile = useCallback(
567
+ (fileId: string | null) => {
568
+ setSelectedId(fileId)
569
+ if (fileId) setSelectedFolder(null)
570
+ if (fileId && intent.creationMode) document.exitPointerLock()
571
+ },
572
+ [intent.creationMode],
573
+ )
574
+
575
+ const selectFolder = useCallback((folderPath: string | null) => {
576
+ setSelectedFolder(folderPath)
577
+ if (folderPath) setSelectedId(null)
578
+ }, [])
579
+
580
+ const addBlueprintFunction = useCallback(
581
+ (fileId: string, rawName: string) => {
582
+ if (!intent.creationMode || fileId.startsWith('draft:')) return false
583
+ const name = rawName.trim()
584
+ if (!isBlueprintSymbolName(name)) return false
585
+ const exists =
586
+ displayGraph.files
587
+ .find((file) => file.id === fileId)
588
+ ?.symbols.some(
589
+ (symbol) => symbol.kind === 'function' && symbol.name === name,
590
+ ) ||
591
+ blueprintFunctions.some(
592
+ (item) => item.file === fileId && item.name === name,
593
+ )
594
+ if (exists) return false
595
+ const next = [...blueprintFunctions, { name, file: fileId }]
596
+ setBlueprintFunctions(next)
597
+ persistBlueprint(userBlocks, userIslands, next, blueprintVariables, blueprintImports)
598
+ return true
599
+ },
600
+ [
601
+ blueprintFunctions,
602
+ blueprintImports,
603
+ blueprintVariables,
604
+ displayGraph.files,
605
+ intent.creationMode,
606
+ persistBlueprint,
607
+ userBlocks,
608
+ userIslands,
609
+ ],
610
+ )
611
+
612
+ const addBlueprintVariable = useCallback(
613
+ (fileId: string, rawName: string) => {
614
+ if (!intent.creationMode || fileId.startsWith('draft:')) return false
615
+ const name = rawName.trim()
616
+ if (!isBlueprintSymbolName(name)) return false
617
+ const exists =
618
+ displayGraph.files
619
+ .find((file) => file.id === fileId)
620
+ ?.symbols.some(
621
+ (symbol) => symbol.kind === 'variable' && symbol.name === name,
622
+ ) ||
623
+ blueprintVariables.some(
624
+ (item) => item.file === fileId && item.name === name,
625
+ )
626
+ if (exists) return false
627
+ const next = [...blueprintVariables, { name, file: fileId }]
628
+ setBlueprintVariables(next)
629
+ persistBlueprint(userBlocks, userIslands, blueprintFunctions, next, blueprintImports)
630
+ return true
631
+ },
632
+ [
633
+ blueprintFunctions,
634
+ blueprintImports,
635
+ blueprintVariables,
636
+ displayGraph.files,
637
+ intent.creationMode,
638
+ persistBlueprint,
639
+ userBlocks,
640
+ userIslands,
641
+ ],
642
+ )
643
+
644
+ const addBlueprintImport = useCallback(
645
+ (fileId: string, raw: string) => {
646
+ if (!intent.creationMode || fileId.startsWith('draft:')) return false
647
+ const parsed = parseBlueprintImport(
648
+ raw,
649
+ fileId,
650
+ displayGraph.files.map((file) => file.id),
651
+ )
652
+ if (!parsed) return false
653
+ const exists = blueprintImports.some(
654
+ (item) =>
655
+ item.file === fileId &&
656
+ item.name === parsed.name &&
657
+ item.from === parsed.from,
658
+ )
659
+ if (exists) return false
660
+ const next = [...blueprintImports, parsed]
661
+ setBlueprintImports(next)
662
+ persistBlueprint(
663
+ userBlocks,
664
+ userIslands,
665
+ blueprintFunctions,
666
+ blueprintVariables,
667
+ next,
668
+ )
669
+ return true
670
+ },
671
+ [
672
+ blueprintFunctions,
673
+ blueprintImports,
674
+ blueprintVariables,
675
+ displayGraph.files,
676
+ intent.creationMode,
677
+ persistBlueprint,
678
+ userBlocks,
679
+ userIslands,
680
+ ],
681
+ )
682
+
683
+ const removeBlueprintFunction = useCallback(
684
+ (fileId: string, name: string) => {
685
+ if (!intent.creationMode) return
686
+ const next = blueprintFunctions.filter(
687
+ (item) => !(item.file === fileId && item.name === name),
688
+ )
689
+ setBlueprintFunctions(next)
690
+ persistBlueprint(userBlocks, userIslands, next, blueprintVariables, blueprintImports)
691
+ },
692
+ [
693
+ blueprintFunctions,
694
+ blueprintImports,
695
+ blueprintVariables,
696
+ intent.creationMode,
697
+ persistBlueprint,
698
+ userBlocks,
699
+ userIslands,
700
+ ],
701
+ )
702
+
703
+ const removeBlueprintVariable = useCallback(
704
+ (fileId: string, name: string) => {
705
+ if (!intent.creationMode) return
706
+ const next = blueprintVariables.filter(
707
+ (item) => !(item.file === fileId && item.name === name),
708
+ )
709
+ setBlueprintVariables(next)
710
+ persistBlueprint(userBlocks, userIslands, blueprintFunctions, next, blueprintImports)
711
+ },
712
+ [
713
+ blueprintFunctions,
714
+ blueprintImports,
715
+ blueprintVariables,
716
+ intent.creationMode,
717
+ persistBlueprint,
718
+ userBlocks,
719
+ userIslands,
720
+ ],
721
+ )
722
+
723
+ const removeBlueprintImport = useCallback(
724
+ (fileId: string, name: string, from: string) => {
725
+ if (!intent.creationMode) return
726
+ const next = blueprintImports.filter(
727
+ (item) =>
728
+ !(item.file === fileId && item.name === name && item.from === from),
729
+ )
730
+ setBlueprintImports(next)
731
+ persistBlueprint(
732
+ userBlocks,
733
+ userIslands,
734
+ blueprintFunctions,
735
+ blueprintVariables,
736
+ next,
737
+ )
738
+ },
739
+ [
740
+ blueprintFunctions,
741
+ blueprintImports,
742
+ blueprintVariables,
743
+ intent.creationMode,
744
+ persistBlueprint,
745
+ userBlocks,
746
+ userIslands,
747
+ ],
748
+ )
749
+
750
+ useEffect(() => {
751
+ if (!namingId && !namingIslandId) return
752
+ const onKey = (event: KeyboardEvent) => {
753
+ if (event.code !== 'Escape') return
754
+ event.preventDefault()
755
+ if (namingId) cancelBlockName(namingId)
756
+ if (namingIslandId) cancelIslandName(namingIslandId)
757
+ }
758
+ window.addEventListener('keydown', onKey)
759
+ return () => window.removeEventListener('keydown', onKey)
760
+ }, [cancelBlockName, cancelIslandName, namingId, namingIslandId])
761
+
762
+ useEffect(() => {
763
+ const onKey = (event: KeyboardEvent) => {
764
+ if (event.repeat || event.code !== 'Backspace') return
765
+ const target = event.target
766
+ if (
767
+ target instanceof HTMLElement &&
768
+ (target.tagName === 'TEXTAREA' ||
769
+ target.tagName === 'INPUT' ||
770
+ target.tagName === 'SELECT' ||
771
+ target.isContentEditable)
772
+ ) {
773
+ return
774
+ }
775
+ if (!deleteSelectedCreatedBlock()) return
776
+ event.preventDefault()
777
+ }
778
+ window.addEventListener('keydown', onKey)
779
+ return () => window.removeEventListener('keydown', onKey)
780
+ }, [deleteSelectedCreatedBlock])
781
+
782
+ const toggleFollowLook = useCallback(() => {
783
+ setFollowLook((current) => {
784
+ const next = !current
785
+ persistFollowLook(next)
786
+ return next
787
+ })
788
+ }, [])
789
+
790
+ const toggleImportedBy = useCallback(() => {
791
+ setImportedBy((current) => !current)
792
+ }, [])
793
+
794
+ useEffect(() => {
795
+ const onKey = (event: KeyboardEvent) => {
796
+ if (event.repeat || event.code !== 'KeyK') return
797
+ const target = event.target
798
+ if (
799
+ target instanceof HTMLElement &&
800
+ (target.tagName === 'TEXTAREA' ||
801
+ target.tagName === 'INPUT' ||
802
+ target.tagName === 'SELECT' ||
803
+ target.isContentEditable)
804
+ ) {
805
+ return
806
+ }
807
+ event.preventDefault()
808
+ toggleImportedBy()
809
+ }
810
+ window.addEventListener('keydown', onKey)
811
+ return () => window.removeEventListener('keydown', onKey)
812
+ }, [toggleImportedBy])
813
+
814
+ useEffect(() => {
815
+ let cancelled = false
816
+ const poll = async () => {
817
+ try {
818
+ const next = await fetchAgentIntent(
819
+ browsingHistory.current ? viewedDiffId.current ?? undefined : undefined,
820
+ )
821
+ const signature = intentSignature(next)
822
+ if (cancelled || signature === lastIntentSig.current) {
823
+ return
824
+ }
825
+ lastIntentSig.current = signature
826
+ applyIntent(next)
827
+ } catch {
828
+ // Explorer may be running without the intent endpoint yet.
829
+ }
830
+ }
831
+ void poll()
832
+ const timer = window.setInterval(() => {
833
+ void poll()
834
+ }, 700)
835
+ return () => {
836
+ cancelled = true
837
+ window.clearInterval(timer)
838
+ }
839
+ }, [applyIntent])
840
+
841
+ const plannedIds = previewing ? [...intent.files, ...intent.creates] : []
842
+ const blueprintImportEdges = blueprintImports.flatMap((item) => {
843
+ if (!displayGraph.files.some((file) => file.id === item.from)) return []
844
+ return [{ from: item.file, to: item.from }]
845
+ })
846
+ const plannedImports = [
847
+ ...(previewing ? (intent.imports ?? []) : []),
848
+ ...blueprintImportEdges,
849
+ ]
850
+ const deletedIds = previewing ? intent.deletes : []
851
+
852
+ return (
853
+ <>
854
+ <div className="stage">
855
+ <Canvas
856
+ shadows={false}
857
+ gl={{ antialias: true, toneMappingExposure: 1.25 }}
858
+ camera={{
859
+ position: layout.spawn,
860
+ fov: 70,
861
+ near: 0.1,
862
+ far: 400,
863
+ }}
864
+ >
865
+ <World
866
+ graph={displayGraph}
867
+ layout={layout}
868
+ mode={mode}
869
+ landAt={landAt}
870
+ selectedId={selectedId}
871
+ selectedFolder={selectedFolder}
872
+ locked={locked}
873
+ onSelect={selectFile}
874
+ onSelectFolder={selectFolder}
875
+ onLockedChange={setLocked}
876
+ onFolderChange={setCurrentFolder}
877
+ onLand={land}
878
+ onWalkPosition={rememberWalk}
879
+ onContext={persistUserContext}
880
+ plannedIds={plannedIds}
881
+ previewFiles={{}}
882
+ plannedImports={plannedImports}
883
+ createdIds={plannedCreates}
884
+ deletedIds={deletedIds}
885
+ createLines={intent.createLines ?? {}}
886
+ flyTo={flyTo}
887
+ aimedRelation={aimedRelation}
888
+ onAimRelation={setAimedRelation}
889
+ onTravelTo={flyAlongRelation}
890
+ importedBy={importedBy}
891
+ namingId={namingId}
892
+ namingIslandId={namingIslandId}
893
+ onPlaceBlock={intent.creationMode ? placeBlock : undefined}
894
+ onPlaceIsland={intent.creationMode ? placeIsland : undefined}
895
+ onCommitName={commitBlockName}
896
+ onCancelName={cancelBlockName}
897
+ userCreatedBlocks={userBlocks}
898
+ userCreatedIslands={userIslands}
899
+ />
900
+ </Canvas>
901
+ </div>
902
+ <HUD
903
+ graph={displayGraph}
904
+ mode={mode}
905
+ locked={locked}
906
+ selectedId={selectedId}
907
+ selectedFolder={selectedFolder}
908
+ aimedRelation={aimedRelation}
909
+ currentFolder={currentFolder}
910
+ intent={intent}
911
+ onWorkflowAction={runWorkflowAction}
912
+ onNavigateDiff={navigateDiff}
913
+ onOpenMap={openMap}
914
+ onWalk={openWalk}
915
+ followLook={followLook}
916
+ onToggleFollowLook={toggleFollowLook}
917
+ importedBy={importedBy}
918
+ onToggleImportedBy={toggleImportedBy}
919
+ naming={naming}
920
+ namingIsland={Boolean(namingIslandId)}
921
+ onCommitIslandName={(name) => {
922
+ if (namingIslandId) commitIslandName(namingIslandId, name)
923
+ }}
924
+ onCancelIslandName={() => {
925
+ if (namingIslandId) cancelIslandName(namingIslandId)
926
+ }}
927
+ blueprintFunctions={blueprintFunctions}
928
+ blueprintVariables={blueprintVariables}
929
+ blueprintImports={blueprintImports}
930
+ onAddBlueprintFunction={addBlueprintFunction}
931
+ onAddBlueprintVariable={addBlueprintVariable}
932
+ onAddBlueprintImport={addBlueprintImport}
933
+ onRemoveBlueprintFunction={removeBlueprintFunction}
934
+ onRemoveBlueprintVariable={removeBlueprintVariable}
935
+ onRemoveBlueprintImport={removeBlueprintImport}
936
+ onMapAddFile={placeBlockOnFolder}
937
+ onMapAddFolder={placeIslandOnFolder}
938
+ />
939
+ </>
940
+ )
941
+ }