@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,57 @@
1
+ import { useMemo } from 'react'
2
+ import * as THREE from 'three'
3
+ import { MAP_SELECTION } from '../theme'
4
+
5
+ type MapSelectBorderProps = {
6
+ width: number
7
+ depth: number
8
+ y?: number
9
+ stroke?: number
10
+ userData?: Record<string, unknown>
11
+ }
12
+
13
+ export function MapSelectBorder({
14
+ width,
15
+ depth,
16
+ y = 0.04,
17
+ stroke = MAP_SELECTION.blockPad,
18
+ userData,
19
+ }: MapSelectBorderProps) {
20
+ const geometry = useMemo(() => {
21
+ const innerW = width
22
+ const innerD = depth
23
+ const outerW = width + stroke * 2
24
+ const outerD = depth + stroke * 2
25
+ const shape = new THREE.Shape()
26
+ shape.moveTo(-outerW / 2, -outerD / 2)
27
+ shape.lineTo(outerW / 2, -outerD / 2)
28
+ shape.lineTo(outerW / 2, outerD / 2)
29
+ shape.lineTo(-outerW / 2, outerD / 2)
30
+ shape.closePath()
31
+ const hole = new THREE.Path()
32
+ hole.moveTo(-innerW / 2, -innerD / 2)
33
+ hole.lineTo(-innerW / 2, innerD / 2)
34
+ hole.lineTo(innerW / 2, innerD / 2)
35
+ hole.lineTo(innerW / 2, -innerD / 2)
36
+ hole.closePath()
37
+ shape.holes.push(hole)
38
+ return new THREE.ShapeGeometry(shape)
39
+ }, [depth, stroke, width])
40
+
41
+ return (
42
+ <mesh
43
+ geometry={geometry}
44
+ position={[0, y, 0]}
45
+ rotation={[-Math.PI / 2, 0, 0]}
46
+ renderOrder={2}
47
+ userData={userData}
48
+ >
49
+ <meshBasicMaterial
50
+ color={MAP_SELECTION.color}
51
+ toneMapped={false}
52
+ side={THREE.DoubleSide}
53
+ depthWrite={false}
54
+ />
55
+ </mesh>
56
+ )
57
+ }
@@ -0,0 +1,247 @@
1
+ import { useEffect, useLayoutEffect, useMemo, useRef } from 'react'
2
+ import { useThree } from '@react-three/fiber'
3
+ import { Html, MapControls, OrthographicCamera } from '@react-three/drei'
4
+ import * as THREE from 'three'
5
+ import { folderAt, worldBounds } from '../layout'
6
+ import type { ChangeKind } from '../theme'
7
+ import type { WorldLayout } from '../types'
8
+
9
+ type MapViewProps = {
10
+ layout: WorldLayout
11
+ enabled: boolean
12
+ marker: [number, number] | null
13
+ highlightedFolders?: Partial<Record<string, ChangeKind>>
14
+ selectedFolder?: string | null
15
+ onLand: (x: number, z: number) => void
16
+ onSelect: (fileId: string | null) => void
17
+ onSelectFolder: (folderPath: string | null) => void
18
+ onTravelTo: (fromId: string, toId: string) => void
19
+ }
20
+
21
+ export function MapView({
22
+ layout,
23
+ enabled,
24
+ marker,
25
+ highlightedFolders,
26
+ selectedFolder = null,
27
+ onLand,
28
+ onSelect,
29
+ onSelectFolder,
30
+ onTravelTo,
31
+ }: MapViewProps) {
32
+ const size = useThree((state) => state.size)
33
+ const { camera, gl, scene } = useThree()
34
+ const bounds = useMemo(() => worldBounds(layout), [layout])
35
+ const drag = useRef({ x: 0, y: 0, moved: false, active: false })
36
+ const sized = size.width > 16 && size.height > 16
37
+
38
+ const fitZoom = sized
39
+ ? Math.min(
40
+ size.width / Math.max(bounds.width + 24, 1),
41
+ size.height / Math.max(bounds.depth + 24, 1),
42
+ )
43
+ : 8
44
+ const zoom = Math.max(fitZoom * 0.92, 1)
45
+
46
+ useLayoutEffect(() => {
47
+ if (!enabled || !(camera instanceof THREE.OrthographicCamera)) return
48
+ camera.up.set(0, 0, -1)
49
+ camera.position.set(bounds.cx, 120, bounds.cz)
50
+ camera.lookAt(bounds.cx, 0, bounds.cz)
51
+ camera.near = 1
52
+ camera.far = 2000
53
+ camera.zoom = zoom
54
+ camera.updateProjectionMatrix()
55
+ }, [bounds.cx, bounds.cz, camera, enabled, zoom])
56
+
57
+ useEffect(() => {
58
+ if (!enabled) return
59
+ const element = gl.domElement
60
+ element.style.cursor = 'grab'
61
+
62
+ const onDown = (event: PointerEvent) => {
63
+ if (event.button !== 0) return
64
+ drag.current = { x: event.clientX, y: event.clientY, moved: false, active: true }
65
+ element.style.cursor = 'grabbing'
66
+ }
67
+
68
+ const onMove = (event: PointerEvent) => {
69
+ if (Math.hypot(event.clientX - drag.current.x, event.clientY - drag.current.y) > 5) {
70
+ drag.current.moved = true
71
+ }
72
+ }
73
+
74
+ const pickAt = (clientX: number, clientY: number) => {
75
+ const rect = element.getBoundingClientRect()
76
+ if (rect.width < 2 || rect.height < 2) return null
77
+ const ndc = new THREE.Vector2(
78
+ ((clientX - rect.left) / rect.width) * 2 - 1,
79
+ -((clientY - rect.top) / rect.height) * 2 + 1,
80
+ )
81
+ const raycaster = new THREE.Raycaster()
82
+ raycaster.setFromCamera(ndc, camera)
83
+ const hits = raycaster.intersectObjects(scene.children, true)
84
+ const relationHit = hits.find((hit) => hit.object.userData.relationTo)
85
+ const fileHit = hits.find((hit) => hit.object.userData.fileId)
86
+ return { raycaster, relationHit, fileHit }
87
+ }
88
+
89
+ const onUp = (event: PointerEvent) => {
90
+ element.style.cursor = 'grab'
91
+ const startedOnCanvas = drag.current.active
92
+ drag.current.active = false
93
+ if (!startedOnCanvas || event.button !== 0 || drag.current.moved) return
94
+
95
+ const pick = pickAt(event.clientX, event.clientY)
96
+ if (!pick) return
97
+ const { relationHit, fileHit } = pick
98
+ if (
99
+ relationHit &&
100
+ (!fileHit || relationHit.distance <= fileHit.distance + 0.4)
101
+ ) {
102
+ onTravelTo(
103
+ relationHit.object.userData.relationFrom as string,
104
+ relationHit.object.userData.relationTo as string,
105
+ )
106
+ return
107
+ }
108
+ if (fileHit) {
109
+ onSelect(fileHit.object.userData.fileId as string)
110
+ return
111
+ }
112
+
113
+ const hit = new THREE.Vector3()
114
+ const plane = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0)
115
+ if (pick.raycaster.ray.intersectPlane(plane, hit)) {
116
+ const folder = folderAt(hit.x, hit.z, layout)
117
+ if (folder) {
118
+ onSelectFolder(folder.path)
119
+ return
120
+ }
121
+ }
122
+ onSelect(null)
123
+ onSelectFolder(null)
124
+ }
125
+
126
+ const onDblClick = (event: MouseEvent) => {
127
+ if (event.button !== 0 || drag.current.moved) return
128
+ const pick = pickAt(event.clientX, event.clientY)
129
+ if (!pick) return
130
+ const { raycaster, relationHit, fileHit } = pick
131
+ if (relationHit || fileHit) return
132
+
133
+ const hit = new THREE.Vector3()
134
+ const plane = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0)
135
+ if (raycaster.ray.intersectPlane(plane, hit)) {
136
+ const lock = element.requestPointerLock()
137
+ if (lock && typeof lock.catch === 'function') {
138
+ void lock.catch(() => {})
139
+ }
140
+ onLand(hit.x, hit.z)
141
+ }
142
+ }
143
+
144
+ element.addEventListener('pointerdown', onDown)
145
+ window.addEventListener('pointermove', onMove)
146
+ window.addEventListener('pointerup', onUp)
147
+ element.addEventListener('dblclick', onDblClick)
148
+ return () => {
149
+ element.style.cursor = ''
150
+ element.removeEventListener('pointerdown', onDown)
151
+ window.removeEventListener('pointermove', onMove)
152
+ window.removeEventListener('pointerup', onUp)
153
+ element.removeEventListener('dblclick', onDblClick)
154
+ }
155
+ }, [
156
+ camera,
157
+ enabled,
158
+ gl.domElement,
159
+ layout,
160
+ onLand,
161
+ onSelect,
162
+ onSelectFolder,
163
+ onTravelTo,
164
+ scene,
165
+ ])
166
+
167
+ return (
168
+ <>
169
+ {enabled && (
170
+ <OrthographicCamera
171
+ makeDefault
172
+ position={[bounds.cx, 120, bounds.cz]}
173
+ zoom={zoom}
174
+ near={1}
175
+ far={2000}
176
+ up={[0, 0, -1]}
177
+ />
178
+ )}
179
+ {enabled && sized && (
180
+ <MapControls
181
+ enableRotate={false}
182
+ enableDamping
183
+ dampingFactor={0.12}
184
+ screenSpacePanning
185
+ zoomSpeed={1.15}
186
+ minZoom={Math.max(fitZoom * 0.45, 1)}
187
+ maxZoom={Math.max(fitZoom * 10, 20)}
188
+ target={[bounds.cx, 0, bounds.cz]}
189
+ />
190
+ )}
191
+ {enabled &&
192
+ Object.values(layout.folders).map((folder) => (
193
+ <Html
194
+ key={folder.path}
195
+ position={[folder.x, 14, folder.z + 1.35]}
196
+ center
197
+ style={{ pointerEvents: 'none' }}
198
+ >
199
+ <div
200
+ className={[
201
+ 'map-folder-label',
202
+ highlightedFolders?.[folder.path]
203
+ ? `map-folder-label-${highlightedFolders[folder.path]}`
204
+ : folder.added
205
+ ? 'map-folder-label-added'
206
+ : selectedFolder === folder.path
207
+ ? 'map-folder-label-selected'
208
+ : '',
209
+ ]
210
+ .filter(Boolean)
211
+ .join(' ')}
212
+ >
213
+ <span className="map-folder-name">
214
+ {folderKindLabel(
215
+ folder.name,
216
+ highlightedFolders?.[folder.path] ?? null,
217
+ folder.added ?? false,
218
+ )}
219
+ </span>
220
+ </div>
221
+ </Html>
222
+ ))}
223
+ {marker && <LandMarker marker={marker} />}
224
+ </>
225
+ )
226
+ }
227
+
228
+ function folderKindLabel(
229
+ name: string,
230
+ kind: ChangeKind | null | undefined,
231
+ added: boolean,
232
+ ) {
233
+ if (kind === 'remove') return `- ${name}`
234
+ if (kind === 'add' || added) return `+ ${name}`
235
+ return name
236
+ }
237
+
238
+ function LandMarker({ marker }: { marker: [number, number] }) {
239
+ return (
240
+ <group position={[marker[0], 0.2, marker[1]]}>
241
+ <mesh rotation={[-Math.PI / 2, 0, 0]}>
242
+ <ringGeometry args={[0.55, 0.85, 24]} />
243
+ <meshBasicMaterial color="#e8c36a" side={THREE.DoubleSide} />
244
+ </mesh>
245
+ </group>
246
+ )
247
+ }
@@ -0,0 +1,245 @@
1
+ import { useEffect, useRef, useState } from 'react'
2
+ import { useFrame, useThree } from '@react-three/fiber'
3
+ import { PerspectiveCamera, PointerLockControls } from '@react-three/drei'
4
+ import type { PointerLockControls as PointerLockControlsImpl } from 'three-stdlib'
5
+ import * as THREE from 'three'
6
+ import { CONFIG } from '../theme'
7
+ import { locationLabel } from '../layout'
8
+ import type { FlyTo, ViewMode, WorldLayout } from '../types'
9
+
10
+ type PlayerProps = {
11
+ layout: WorldLayout
12
+ mode: ViewMode
13
+ landAt: [number, number]
14
+ locked: boolean
15
+ lockEnabled?: boolean
16
+ onLockedChange: (locked: boolean) => void
17
+ onFolderChange: (label: string) => void
18
+ onWalkPosition: (x: number, z: number) => void
19
+ flyTo: FlyTo | null
20
+ }
21
+
22
+ export function Player({
23
+ layout,
24
+ mode,
25
+ landAt,
26
+ locked,
27
+ lockEnabled = true,
28
+ onLockedChange,
29
+ onFolderChange,
30
+ onWalkPosition,
31
+ flyTo,
32
+ }: PlayerProps) {
33
+ const { camera } = useThree()
34
+ const walking = mode === 'walk'
35
+ const keys = useRef({
36
+ forward: false,
37
+ back: false,
38
+ left: false,
39
+ right: false,
40
+ sprint: false,
41
+ })
42
+ const lastFolder = useRef<string | null>(null)
43
+ const front = useRef(new THREE.Vector3())
44
+ const right = useRef(new THREE.Vector3())
45
+ const up = useRef(new THREE.Vector3(0, 1, 0))
46
+ const move = useRef(new THREE.Vector3())
47
+ const lastFly = useRef(0)
48
+ const lookAt = useRef(new THREE.Vector3())
49
+ const [steering, setSteering] = useState(true)
50
+ const controlsRef = useRef<PointerLockControlsImpl>(null)
51
+ const capturedLook = useRef(false)
52
+ const flying = useRef<{
53
+ startPos: THREE.Vector3
54
+ endPos: THREE.Vector3
55
+ lookAt: THREE.Vector3
56
+ duration: number
57
+ t: number
58
+ } | null>(null)
59
+
60
+ useEffect(() => {
61
+ if (!walking) {
62
+ flying.current = null
63
+ capturedLook.current = false
64
+ setSteering(true)
65
+ return
66
+ }
67
+ if (flyTo && flyTo.nonce !== lastFly.current) {
68
+ lastFly.current = flyTo.nonce
69
+ const end = new THREE.Vector3(landAt[0], CONFIG.eyeHeight, landAt[1])
70
+ lookAt.current.set(flyTo.lookAt[0], flyTo.lookAt[1], flyTo.lookAt[2])
71
+ const dist = camera.position.distanceTo(end)
72
+ flying.current = {
73
+ startPos: camera.position.clone(),
74
+ endPos: end,
75
+ lookAt: lookAt.current.clone(),
76
+ duration: Math.min(1.35, Math.max(0.55, dist * 0.025)),
77
+ t: 0,
78
+ }
79
+ setSteering(false)
80
+ return
81
+ }
82
+ if (flying.current) return
83
+ camera.up.set(0, 1, 0)
84
+ camera.position.set(landAt[0], CONFIG.eyeHeight, landAt[1])
85
+ camera.lookAt(landAt[0], CONFIG.eyeHeight, landAt[1] + 10)
86
+ camera.updateProjectionMatrix()
87
+ }, [camera, flyTo, landAt, walking])
88
+
89
+ useEffect(() => {
90
+ const onKey = (event: KeyboardEvent, down: boolean) => {
91
+ if (
92
+ event.target instanceof HTMLElement &&
93
+ (event.target.tagName === 'TEXTAREA' ||
94
+ event.target.tagName === 'INPUT' ||
95
+ event.target.tagName === 'SELECT' ||
96
+ event.target.isContentEditable)
97
+ ) {
98
+ return
99
+ }
100
+ if (event.code === 'KeyW' || event.code === 'ArrowUp') keys.current.forward = down
101
+ if (event.code === 'KeyS' || event.code === 'ArrowDown') keys.current.back = down
102
+ if (event.code === 'KeyA' || event.code === 'ArrowLeft') keys.current.left = down
103
+ if (event.code === 'KeyD' || event.code === 'ArrowRight') keys.current.right = down
104
+ if (event.code === 'ShiftLeft' || event.code === 'ShiftRight') keys.current.sprint = down
105
+ }
106
+
107
+ const down = (event: KeyboardEvent) => onKey(event, true)
108
+ const up = (event: KeyboardEvent) => onKey(event, false)
109
+ window.addEventListener('keydown', down)
110
+ window.addEventListener('keyup', up)
111
+ return () => {
112
+ window.removeEventListener('keydown', down)
113
+ window.removeEventListener('keyup', up)
114
+ }
115
+ }, [])
116
+
117
+ useEffect(() => {
118
+ if (!walking || !lockEnabled) {
119
+ controlsRef.current?.unlock()
120
+ document.exitPointerLock()
121
+ return
122
+ }
123
+ if (!steering || capturedLook.current) return
124
+ const controls = controlsRef.current
125
+ const element = controls?.domElement
126
+ if (!controls || !element) return
127
+ capturedLook.current = true
128
+ if (element.ownerDocument.pointerLockElement === element) {
129
+ controls.isLocked = true
130
+ onLockedChange(true)
131
+ return
132
+ }
133
+ controls.lock()
134
+ }, [lockEnabled, onLockedChange, steering, walking])
135
+
136
+ useFrame((_, delta) => {
137
+ try {
138
+ if (walking && flying.current) {
139
+ const flight = flying.current
140
+ flight.t += delta / flight.duration
141
+ const t = Math.min(1, flight.t)
142
+ const ease = t * t * (3 - 2 * t)
143
+ camera.position.lerpVectors(flight.startPos, flight.endPos, ease)
144
+ const dist = flight.startPos.distanceTo(flight.endPos)
145
+ camera.position.y =
146
+ CONFIG.eyeHeight + Math.sin(t * Math.PI) * Math.min(14, 3.5 + dist * 0.1)
147
+ camera.up.set(0, 1, 0)
148
+ camera.lookAt(flight.lookAt)
149
+ if (t >= 1) {
150
+ camera.position.copy(flight.endPos)
151
+ camera.up.set(0, 1, 0)
152
+ camera.lookAt(flight.lookAt)
153
+ flying.current = null
154
+ setSteering(true)
155
+ }
156
+ } else if (walking && locked) {
157
+ camera.getWorldDirection(front.current)
158
+ front.current.y = 0
159
+ front.current.normalize()
160
+ right.current.crossVectors(front.current, up.current).normalize()
161
+ move.current.set(0, 0, 0)
162
+ if (keys.current.forward) move.current.add(front.current)
163
+ if (keys.current.back) move.current.sub(front.current)
164
+ if (keys.current.right) move.current.add(right.current)
165
+ if (keys.current.left) move.current.sub(right.current)
166
+ if (move.current.lengthSq() > 0) {
167
+ const speed = keys.current.sprint ? CONFIG.sprintSpeed : CONFIG.walkSpeed
168
+ move.current.normalize().multiplyScalar(speed * delta)
169
+ camera.position.add(move.current)
170
+ }
171
+ camera.position.y = CONFIG.eyeHeight
172
+ }
173
+
174
+ if (!walking) return
175
+ onWalkPosition(camera.position.x, camera.position.z)
176
+ const nextPath = locationLabel(camera.position.x, camera.position.z, layout)
177
+ if (nextPath !== lastFolder.current) {
178
+ lastFolder.current = nextPath
179
+ onFolderChange(nextPath)
180
+ }
181
+ } catch {
182
+ // Movement must never stop the render loop.
183
+ }
184
+ })
185
+
186
+ if (!walking) return null
187
+
188
+ return (
189
+ <>
190
+ <PerspectiveCamera
191
+ makeDefault
192
+ fov={70}
193
+ near={0.1}
194
+ far={400}
195
+ position={[landAt[0], CONFIG.eyeHeight, landAt[1]]}
196
+ />
197
+ <PlayerLight />
198
+ <PointerLockControls
199
+ ref={controlsRef}
200
+ makeDefault
201
+ enabled={steering && lockEnabled}
202
+ onLock={() => onLockedChange(true)}
203
+ onUnlock={() => onLockedChange(false)}
204
+ />
205
+ </>
206
+ )
207
+ }
208
+
209
+ function PlayerLight() {
210
+ const { camera } = useThree()
211
+ const overhead = useRef<THREE.PointLight>(null)
212
+ const look = useRef<THREE.PointLight>(null)
213
+ const direction = useRef(new THREE.Vector3())
214
+
215
+ useFrame(() => {
216
+ if (!overhead.current || !look.current) return
217
+
218
+ overhead.current.position.copy(camera.position)
219
+ overhead.current.position.y += 2.6
220
+
221
+ camera.getWorldDirection(direction.current)
222
+ look.current.position.copy(camera.position)
223
+ look.current.position.addScaledVector(direction.current, 5)
224
+ look.current.position.y = camera.position.y + 1.4
225
+ })
226
+
227
+ return (
228
+ <>
229
+ <pointLight
230
+ ref={overhead}
231
+ color="#f4f1e8"
232
+ intensity={7.5}
233
+ distance={26}
234
+ decay={1.4}
235
+ />
236
+ <pointLight
237
+ ref={look}
238
+ color="#e4eef8"
239
+ intensity={3.4}
240
+ distance={18}
241
+ decay={1.6}
242
+ />
243
+ </>
244
+ )
245
+ }