@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,223 @@
1
+ import { useMemo } from 'react'
2
+ import * as THREE from 'three'
3
+ import type { AimedRelation, FileNode, PlacedFile, PatchImport, WorldLayout } from '../types'
4
+
5
+ const SELECTED_COLOR = '#6ad2ff'
6
+ const SELECTED_AIM = '#e7f7ff'
7
+ const IMPORTED_BY_COLOR = '#b57bff'
8
+ const IMPORTED_BY_AIM = '#ecd9ff'
9
+ const PATCH_COLOR = '#f0d24a'
10
+ const PATCH_AIM = '#fff4c2'
11
+ const UP = new THREE.Vector3(0, 1, 0)
12
+
13
+ type RelationLinesProps = {
14
+ selectedId: string | null
15
+ aimedRelation: AimedRelation | null
16
+ files: FileNode[]
17
+ layout: WorldLayout
18
+ extras?: Record<string, PlacedFile>
19
+ plannedEdges?: PatchImport[]
20
+ fromAbove?: boolean
21
+ importedBy?: boolean
22
+ }
23
+
24
+ type ArrowHead = {
25
+ position: THREE.Vector3
26
+ quaternion: THREE.Quaternion
27
+ radius: number
28
+ height: number
29
+ }
30
+
31
+ type LineMesh = {
32
+ id: string
33
+ from: string
34
+ to: string
35
+ planned: boolean
36
+ geometry: THREE.TubeGeometry
37
+ arrows: ArrowHead[]
38
+ }
39
+
40
+ function fileTop(file: PlacedFile) {
41
+ return new THREE.Vector3(
42
+ file.position[0],
43
+ file.position[1] + file.size[1] / 2,
44
+ file.position[2],
45
+ )
46
+ }
47
+
48
+ function arcCurve(start: THREE.Vector3, end: THREE.Vector3) {
49
+ const mid = start.clone().lerp(end, 0.5)
50
+ const dx = end.x - start.x
51
+ const dz = end.z - start.z
52
+ const length = Math.hypot(dx, dz) || 1
53
+ const bow = Math.min(5.5, length * 0.14)
54
+ mid.x += (-dz / length) * bow
55
+ mid.z += (dx / length) * bow
56
+ mid.y = Math.max(start.y, end.y) + 2.4 + length * 0.06
57
+ return new THREE.QuadraticBezierCurve3(start, mid, end)
58
+ }
59
+
60
+ function arrowAt(
61
+ curve: THREE.QuadraticBezierCurve3,
62
+ t: number,
63
+ radius: number,
64
+ height: number,
65
+ towardImported: boolean,
66
+ ): ArrowHead {
67
+ const tangent = curve.getTangentAt(t)
68
+ if (tangent.lengthSq() < 1e-8) tangent.set(0, 1, 0)
69
+ else tangent.normalize()
70
+ if (!towardImported) tangent.negate()
71
+ return {
72
+ position: curve.getPointAt(t),
73
+ quaternion: new THREE.Quaternion().setFromUnitVectors(UP, tangent),
74
+ radius,
75
+ height,
76
+ }
77
+ }
78
+
79
+ function arrowHeads(
80
+ curve: THREE.QuadraticBezierCurve3,
81
+ radius: number,
82
+ fromAbove: boolean,
83
+ towardImported: boolean,
84
+ ): ArrowHead[] {
85
+ const height = Math.max(radius * 6.5, fromAbove ? 0.9 : 0.42)
86
+ const coneRadius = Math.max(radius * 2.8, fromAbove ? 0.32 : 0.15)
87
+ return [
88
+ arrowAt(curve, 0.18, coneRadius, height, towardImported),
89
+ arrowAt(curve, 0.82, coneRadius, height, towardImported),
90
+ ]
91
+ }
92
+
93
+ function edgeMesh(
94
+ from: PlacedFile,
95
+ to: PlacedFile,
96
+ toId: string,
97
+ planned: boolean,
98
+ radius: number,
99
+ fromAbove: boolean,
100
+ towardImported: boolean,
101
+ ): LineMesh {
102
+ const curve = arcCurve(fileTop(from), fileTop(to))
103
+ return {
104
+ id: `${from.id}->${toId}`,
105
+ from: from.id,
106
+ to: toId,
107
+ planned,
108
+ geometry: new THREE.TubeGeometry(curve, 28, radius, 6, false),
109
+ arrows: arrowHeads(curve, radius, fromAbove, towardImported),
110
+ }
111
+ }
112
+
113
+ export function RelationLines({
114
+ selectedId,
115
+ aimedRelation,
116
+ files,
117
+ layout,
118
+ extras = {},
119
+ plannedEdges = [],
120
+ fromAbove = false,
121
+ importedBy = false,
122
+ }: RelationLinesProps) {
123
+ const meshes = useMemo(() => {
124
+ const placed = { ...layout.files, ...extras }
125
+ const selectedRadius = fromAbove ? 0.2 : 0.07
126
+ const plannedRadius = fromAbove ? 0.26 : 0.1
127
+ const lines: LineMesh[] = []
128
+ const plannedKeys = new Set<string>()
129
+
130
+ for (const edge of plannedEdges) {
131
+ const from = placed[edge.from]
132
+ const to = placed[edge.to]
133
+ if (!from || !to) continue
134
+ const key = `${edge.from}->${edge.to}`
135
+ if (plannedKeys.has(key)) continue
136
+ plannedKeys.add(key)
137
+ lines.push(edgeMesh(from, to, edge.to, true, plannedRadius, fromAbove, false))
138
+ }
139
+
140
+ if (selectedId) {
141
+ const selected = placed[selectedId]
142
+ if (selected) {
143
+ if (importedBy) {
144
+ for (const file of files) {
145
+ if (file.id === selectedId || !file.imports.includes(selectedId)) continue
146
+ if (plannedKeys.has(`${file.id}->${selectedId}`)) continue
147
+ const importer = placed[file.id]
148
+ if (!importer) continue
149
+ lines.push(
150
+ edgeMesh(importer, selected, selectedId, false, selectedRadius, fromAbove, false),
151
+ )
152
+ }
153
+ } else {
154
+ const file = files.find((item) => item.id === selectedId)
155
+ if (file) {
156
+ for (const importId of file.imports) {
157
+ if (plannedKeys.has(`${file.id}->${importId}`)) continue
158
+ const target = placed[importId]
159
+ if (!target) continue
160
+ lines.push(
161
+ edgeMesh(selected, target, importId, false, selectedRadius, fromAbove, false),
162
+ )
163
+ }
164
+ }
165
+ }
166
+ }
167
+ }
168
+
169
+ return lines
170
+ }, [extras, files, fromAbove, importedBy, layout.files, plannedEdges, selectedId])
171
+
172
+ if (meshes.length === 0) return null
173
+
174
+ return (
175
+ <group>
176
+ {meshes.map((mesh) => {
177
+ const aimed =
178
+ aimedRelation?.from === mesh.from && aimedRelation?.to === mesh.to
179
+ const color = mesh.planned
180
+ ? aimed
181
+ ? PATCH_AIM
182
+ : PATCH_COLOR
183
+ : importedBy
184
+ ? aimed
185
+ ? IMPORTED_BY_AIM
186
+ : IMPORTED_BY_COLOR
187
+ : aimed
188
+ ? SELECTED_AIM
189
+ : SELECTED_COLOR
190
+ const glow = aimed ? 2.4 : mesh.planned ? 1.7 : 1.4
191
+ const relation = { relationFrom: mesh.from, relationTo: mesh.to }
192
+ return (
193
+ <group key={mesh.id}>
194
+ <mesh geometry={mesh.geometry} userData={relation}>
195
+ <meshStandardMaterial
196
+ color={color}
197
+ emissive={color}
198
+ emissiveIntensity={glow}
199
+ roughness={0.3}
200
+ />
201
+ </mesh>
202
+ {mesh.arrows.map((arrow, index) => (
203
+ <mesh
204
+ key={`${mesh.id}-arrow-${index}`}
205
+ position={arrow.position}
206
+ quaternion={arrow.quaternion}
207
+ userData={relation}
208
+ >
209
+ <coneGeometry args={[arrow.radius, arrow.height, 10]} />
210
+ <meshStandardMaterial
211
+ color={color}
212
+ emissive={color}
213
+ emissiveIntensity={glow}
214
+ roughness={0.3}
215
+ />
216
+ </mesh>
217
+ ))}
218
+ </group>
219
+ )
220
+ })}
221
+ </group>
222
+ )
223
+ }
@@ -0,0 +1,89 @@
1
+ import { useEffect, useRef } from 'react'
2
+ import { useFrame, useThree } from '@react-three/fiber'
3
+ import * as THREE from 'three'
4
+ import { relationTravelTarget } from '../layout'
5
+ import type { AimedRelation, PlacedFile } from '../types'
6
+
7
+ type SelectionControllerProps = {
8
+ locked: boolean
9
+ files: Record<string, PlacedFile>
10
+ onSelect: (fileId: string | null) => void
11
+ onAimRelation?: (aim: AimedRelation | null) => void
12
+ onTravelTo: (fromId: string, toId: string) => void
13
+ }
14
+
15
+ const ndc = new THREE.Vector2(0, 0)
16
+ const raycaster = new THREE.Raycaster()
17
+
18
+ function aimedRelation(
19
+ hits: THREE.Intersection[],
20
+ origin: THREE.Vector3,
21
+ files: Record<string, PlacedFile>,
22
+ ): AimedRelation | null {
23
+ const relation = hits.find((item) => item.object.userData.relationTo)
24
+ const file = hits.find((item) => item.object.userData.fileId)
25
+ if (!relation || (file && relation.distance > file.distance + 0.4)) return null
26
+ const fromId = relation.object.userData.relationFrom as string
27
+ const toId = relation.object.userData.relationTo as string
28
+ return {
29
+ from: fromId,
30
+ to: toId,
31
+ flyTo: relationTravelTarget(fromId, toId, origin.x, origin.z, files),
32
+ }
33
+ }
34
+
35
+ function aimKey(aim: AimedRelation | null) {
36
+ return aim ? `${aim.from}->${aim.to}:${aim.flyTo}` : ''
37
+ }
38
+
39
+ export function SelectionController({
40
+ locked,
41
+ files,
42
+ onSelect,
43
+ onAimRelation,
44
+ onTravelTo,
45
+ }: SelectionControllerProps) {
46
+ const { camera, scene } = useThree()
47
+ const lastAim = useRef('')
48
+
49
+ useFrame(() => {
50
+ if (!onAimRelation) return
51
+ if (!locked) {
52
+ if (lastAim.current) {
53
+ lastAim.current = ''
54
+ onAimRelation(null)
55
+ }
56
+ return
57
+ }
58
+ raycaster.setFromCamera(ndc, camera)
59
+ const next = aimedRelation(
60
+ raycaster.intersectObjects(scene.children, true),
61
+ camera.position,
62
+ files,
63
+ )
64
+ const key = aimKey(next)
65
+ if (key === lastAim.current) return
66
+ lastAim.current = key
67
+ onAimRelation(next)
68
+ })
69
+
70
+ useEffect(() => {
71
+ const onClick = () => {
72
+ if (!locked) return
73
+ raycaster.setFromCamera(ndc, camera)
74
+ const hits = raycaster.intersectObjects(scene.children, true)
75
+ const aim = aimedRelation(hits, camera.position, files)
76
+ if (aim) {
77
+ onTravelTo(aim.from, aim.to)
78
+ return
79
+ }
80
+ const file = hits.find((item) => item.object.userData.fileId)
81
+ onSelect(file ? (file.object.userData.fileId as string) : null)
82
+ }
83
+
84
+ window.addEventListener('click', onClick)
85
+ return () => window.removeEventListener('click', onClick)
86
+ }, [camera, files, locked, onSelect, onTravelTo, scene])
87
+
88
+ return null
89
+ }
@@ -0,0 +1,152 @@
1
+ import { useRef } from 'react'
2
+ import { useFrame, useThree } from '@react-three/fiber'
3
+ import * as THREE from 'three'
4
+ import { folderAt } from '../layout'
5
+ import { fileById, toFileRef } from '../userContext'
6
+ import { namedCreatedBlocks, namedCreatedIslands } from '../userCreated'
7
+ import type { CodebaseGraph, UserContext, UserCreatedBlock, UserCreatedIsland, UserFileRef, ViewMode, WorldLayout } from '../types'
8
+
9
+ type UserContextTrackerProps = {
10
+ graph: CodebaseGraph
11
+ layout: WorldLayout
12
+ mode: ViewMode
13
+ selectedId: string | null
14
+ userCreatedBlocks?: UserCreatedBlock[]
15
+ userCreatedIslands?: UserCreatedIsland[]
16
+ onContext: (context: UserContext) => void
17
+ }
18
+
19
+ const ndc = new THREE.Vector2(0, 0)
20
+ const raycaster = new THREE.Raycaster()
21
+ const look = new THREE.Vector3()
22
+ const toward = new THREE.Vector3()
23
+
24
+ function filesOnIsland(graph: CodebaseGraph, islandPath: string | null): UserFileRef[] {
25
+ if (!islandPath) return []
26
+ const folder = graph.folders.find((item) => item.path === islandPath)
27
+ if (!folder) return []
28
+ return folder.files
29
+ .map((id) => fileById(graph, id))
30
+ .filter((file): file is NonNullable<typeof file> => Boolean(file))
31
+ .map(toFileRef)
32
+ }
33
+
34
+ function filesInLookDirection(
35
+ graph: CodebaseGraph,
36
+ layout: WorldLayout,
37
+ origin: THREE.Vector3,
38
+ direction: THREE.Vector3,
39
+ ): UserFileRef[] {
40
+ const seen: { score: number; file: UserFileRef }[] = []
41
+
42
+ for (const file of graph.files) {
43
+ const placed = layout.files[file.id]
44
+ if (!placed) continue
45
+ toward.set(placed.position[0], placed.position[1], placed.position[2]).sub(origin)
46
+ const distance = toward.length()
47
+ if (distance > 24 || distance < 0.35) continue
48
+ toward.y = 0
49
+ if (toward.lengthSq() === 0) continue
50
+ toward.normalize()
51
+ const alignment = toward.dot(direction)
52
+ if (alignment < 0.62) continue
53
+ seen.push({
54
+ score: alignment * 8 - distance * 0.08,
55
+ file: toFileRef(file),
56
+ })
57
+ }
58
+
59
+ seen.sort((a, b) => b.score - a.score)
60
+ return seen.slice(0, 8).map((item) => item.file)
61
+ }
62
+
63
+ export function UserContextTracker({
64
+ graph,
65
+ layout,
66
+ mode,
67
+ selectedId,
68
+ userCreatedBlocks = [],
69
+ userCreatedIslands = [],
70
+ onContext,
71
+ }: UserContextTrackerProps) {
72
+ const { camera, scene } = useThree()
73
+ const lastKey = useRef('')
74
+
75
+ const lastIsland = useRef<ReturnType<typeof folderAt>>(null)
76
+
77
+ useFrame(() => {
78
+ try {
79
+ const walking = mode === 'walk'
80
+ const island = walking
81
+ ? folderAt(camera.position.x, camera.position.z, layout)
82
+ : lastIsland.current
83
+ if (walking && island) lastIsland.current = island
84
+
85
+ let lookingAtId: string | null = selectedId
86
+ if (walking) {
87
+ raycaster.setFromCamera(ndc, camera)
88
+ const hit = raycaster
89
+ .intersectObjects(scene.children, true)
90
+ .find((item) => item.object.userData.fileId)
91
+ lookingAtId = hit ? (hit.object.userData.fileId as string) : null
92
+ }
93
+
94
+ camera.getWorldDirection(look)
95
+ look.y = 0
96
+ if (look.lengthSq() > 0) look.normalize()
97
+
98
+ const gazed = fileById(graph, lookingAtId)
99
+ const lookingAtFiles = walking
100
+ ? filesInLookDirection(graph, layout, camera.position, look)
101
+ : gazed
102
+ ? [toFileRef(gazed)]
103
+ : []
104
+
105
+ const lookingAt = gazed ?? fileById(graph, lookingAtFiles[0]?.id ?? null)
106
+ const selected = fileById(graph, selectedId)
107
+ const islandPath = island?.path ?? selected?.folder ?? null
108
+ const islandName =
109
+ island?.name ??
110
+ graph.folders.find((folder) => folder.path === islandPath)?.name ??
111
+ 'open ground'
112
+
113
+ const namedBlocks = namedCreatedBlocks(userCreatedBlocks)
114
+ const namedIslands = namedCreatedIslands(userCreatedIslands)
115
+ const key = [
116
+ mode,
117
+ islandPath ?? '',
118
+ lookingAt?.id ?? '',
119
+ selected?.id ?? '',
120
+ lookingAtFiles.map((file) => file.id).join(','),
121
+ namedBlocks.map((block) => block.id).join(','),
122
+ namedIslands.map((island) => island.id).join(','),
123
+ ].join('|')
124
+
125
+ if (key === lastKey.current) return
126
+ lastKey.current = key
127
+
128
+ onContext({
129
+ updatedAt: new Date().toISOString(),
130
+ mode,
131
+ island: {
132
+ path: islandPath,
133
+ name: islandName,
134
+ },
135
+ lookingAt: lookingAt ? toFileRef(lookingAt) : null,
136
+ lookingAtFiles,
137
+ selected: selected ? toFileRef(selected) : null,
138
+ filesOnIsland: filesOnIsland(graph, islandPath),
139
+ position: {
140
+ x: Number(camera.position.x.toFixed(2)),
141
+ z: Number(camera.position.z.toFixed(2)),
142
+ },
143
+ userCreatedBlocks: namedBlocks,
144
+ userCreatedIslands: namedIslands,
145
+ })
146
+ } catch {
147
+ // Context tracking must never stop the render loop.
148
+ }
149
+ })
150
+
151
+ return null
152
+ }