@jkwd/inbase 0.1.4 → 0.1.5
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.
- package/README.md +3 -1
- package/apps/explorer/scripts/session-store.d.ts +20 -0
- package/apps/explorer/scripts/session-store.mjs +318 -32
- package/apps/explorer/src/App.tsx +154 -85
- package/apps/explorer/src/agentIntent.ts +33 -1
- package/apps/explorer/src/index.css +217 -2
- package/apps/explorer/src/layout.ts +46 -0
- package/apps/explorer/src/scene/FileBlock.tsx +110 -145
- package/apps/explorer/src/scene/FolderArea.tsx +17 -4
- package/apps/explorer/src/scene/MapSelectBorder.tsx +3 -1
- package/apps/explorer/src/scene/MapView.tsx +10 -12
- package/apps/explorer/src/scene/Player.tsx +27 -0
- package/apps/explorer/src/scene/RelationLines.tsx +36 -32
- package/apps/explorer/src/scene/SelectionController.tsx +21 -3
- package/apps/explorer/src/scene/SelectionThumbnail.tsx +451 -0
- package/apps/explorer/src/scene/World.tsx +16 -35
- package/apps/explorer/src/theme.ts +7 -1
- package/apps/explorer/src/types.ts +12 -0
- package/apps/explorer/src/ui/HUD.tsx +662 -358
- package/apps/explorer/vite.config.ts +33 -21
- package/bin/session.mjs +16 -2
- package/package.json +1 -1
- package/skill/inbase/SKILL.md +22 -14
|
@@ -0,0 +1,451 @@
|
|
|
1
|
+
import { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'
|
|
2
|
+
import { Canvas, useFrame, useThree } from '@react-three/fiber'
|
|
3
|
+
import {
|
|
4
|
+
fileChangeKind,
|
|
5
|
+
filesImporting,
|
|
6
|
+
folderAt,
|
|
7
|
+
folderChangeHighlights,
|
|
8
|
+
folderOfFile,
|
|
9
|
+
} from '../layout'
|
|
10
|
+
import { CONFIG, WORLD_VOID, type ChangeKind } from '../theme'
|
|
11
|
+
import type { CodebaseGraph, PlacedFile, PlacedFolder, WorldLayout } from '../types'
|
|
12
|
+
import { FileBlock } from './FileBlock'
|
|
13
|
+
import { FolderArea } from './FolderArea'
|
|
14
|
+
import { RelationLines } from './RelationLines'
|
|
15
|
+
|
|
16
|
+
type SelectionThumbnailProps = {
|
|
17
|
+
graph: CodebaseGraph
|
|
18
|
+
layout: WorldLayout
|
|
19
|
+
selectedId: string | null
|
|
20
|
+
selectedFolder: string | null
|
|
21
|
+
landAt: [number, number]
|
|
22
|
+
importedBy?: boolean
|
|
23
|
+
minimized?: boolean
|
|
24
|
+
plannedIds?: string[]
|
|
25
|
+
createdIds?: string[]
|
|
26
|
+
deletedIds?: string[]
|
|
27
|
+
onMinimize?: () => void
|
|
28
|
+
onHide: () => void
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
type ThumbnailTarget = {
|
|
32
|
+
folder: PlacedFolder
|
|
33
|
+
files: Array<{ file: CodebaseGraph['files'][number]; placed: PlacedFile }>
|
|
34
|
+
relatedIds: Set<string>
|
|
35
|
+
selectedFileId: string | null
|
|
36
|
+
camera: {
|
|
37
|
+
position: [number, number, number]
|
|
38
|
+
lookAt: [number, number, number]
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function elevatedCamera(
|
|
43
|
+
lookAt: [number, number, number],
|
|
44
|
+
spanX: number,
|
|
45
|
+
spanY: number,
|
|
46
|
+
spanZ: number,
|
|
47
|
+
towardX: 1 | -1,
|
|
48
|
+
): ThumbnailTarget['camera'] {
|
|
49
|
+
const span = Math.max(spanX, spanZ, 4)
|
|
50
|
+
const distance = Math.max(12, span * 0.9, spanY * 1.8)
|
|
51
|
+
const height = Math.max(CONFIG.eyeHeight * 6, spanY + 8, distance * 0.7)
|
|
52
|
+
return {
|
|
53
|
+
position: [
|
|
54
|
+
lookAt[0] + towardX * distance * 0.55,
|
|
55
|
+
lookAt[1] + height,
|
|
56
|
+
lookAt[2] + distance * 0.72,
|
|
57
|
+
],
|
|
58
|
+
lookAt,
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function filesOnFolder(
|
|
63
|
+
graph: CodebaseGraph,
|
|
64
|
+
layout: WorldLayout,
|
|
65
|
+
folderPath: string,
|
|
66
|
+
) {
|
|
67
|
+
return graph.files.flatMap((file) => {
|
|
68
|
+
if (file.folder !== folderPath && folderOfFile(file.id) !== folderPath) {
|
|
69
|
+
return []
|
|
70
|
+
}
|
|
71
|
+
const placed = layout.files[file.id]
|
|
72
|
+
return placed ? [{ file, placed }] : []
|
|
73
|
+
})
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function resolveTarget(
|
|
77
|
+
graph: CodebaseGraph,
|
|
78
|
+
layout: WorldLayout,
|
|
79
|
+
selectedId: string | null,
|
|
80
|
+
selectedFolder: string | null,
|
|
81
|
+
landAt: [number, number],
|
|
82
|
+
importedBy: boolean,
|
|
83
|
+
): ThumbnailTarget | null {
|
|
84
|
+
const selectedFile = selectedId
|
|
85
|
+
? graph.files.find((file) => file.id === selectedId)
|
|
86
|
+
: undefined
|
|
87
|
+
const selectedPlaced = selectedFile
|
|
88
|
+
? layout.files[selectedFile.id]
|
|
89
|
+
: undefined
|
|
90
|
+
|
|
91
|
+
const folder =
|
|
92
|
+
(selectedFile
|
|
93
|
+
? layout.folders[selectedFile.folder] ??
|
|
94
|
+
layout.folders[folderOfFile(selectedFile.id)]
|
|
95
|
+
: undefined) ??
|
|
96
|
+
(selectedFolder ? layout.folders[selectedFolder] : undefined) ??
|
|
97
|
+
folderAt(landAt[0], landAt[1], layout) ??
|
|
98
|
+
Object.values(layout.folders)[0]
|
|
99
|
+
if (!folder) return null
|
|
100
|
+
|
|
101
|
+
const islandFiles = filesOnFolder(graph, layout, folder.path)
|
|
102
|
+
const relatedIds = new Set(
|
|
103
|
+
selectedFile
|
|
104
|
+
? importedBy
|
|
105
|
+
? filesImporting(graph.files, selectedFile.id).map((file) => file.id)
|
|
106
|
+
: selectedFile.imports
|
|
107
|
+
: [],
|
|
108
|
+
)
|
|
109
|
+
const extraFiles = graph.files.flatMap((file) => {
|
|
110
|
+
if (!relatedIds.has(file.id)) return []
|
|
111
|
+
if (islandFiles.some((entry) => entry.file.id === file.id)) return []
|
|
112
|
+
const placed = layout.files[file.id]
|
|
113
|
+
return placed ? [{ file, placed }] : []
|
|
114
|
+
})
|
|
115
|
+
const files = [...islandFiles, ...extraFiles]
|
|
116
|
+
|
|
117
|
+
if (selectedPlaced) {
|
|
118
|
+
const [width, height, depth] = selectedPlaced.size
|
|
119
|
+
return {
|
|
120
|
+
folder,
|
|
121
|
+
files,
|
|
122
|
+
relatedIds,
|
|
123
|
+
selectedFileId: selectedFile?.id ?? null,
|
|
124
|
+
camera: elevatedCamera(
|
|
125
|
+
[
|
|
126
|
+
selectedPlaced.position[0],
|
|
127
|
+
selectedPlaced.position[1],
|
|
128
|
+
selectedPlaced.position[2],
|
|
129
|
+
],
|
|
130
|
+
width,
|
|
131
|
+
height,
|
|
132
|
+
depth,
|
|
133
|
+
selectedPlaced.aisleFace,
|
|
134
|
+
),
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const tallest = files.reduce(
|
|
139
|
+
(height, entry) => Math.max(height, entry.placed.size[1]),
|
|
140
|
+
CONFIG.minHeight,
|
|
141
|
+
)
|
|
142
|
+
return {
|
|
143
|
+
folder,
|
|
144
|
+
files,
|
|
145
|
+
relatedIds,
|
|
146
|
+
selectedFileId: null,
|
|
147
|
+
camera: elevatedCamera(
|
|
148
|
+
[folder.x, 1.2, folder.z + folder.depth / 2],
|
|
149
|
+
folder.width,
|
|
150
|
+
tallest,
|
|
151
|
+
folder.depth,
|
|
152
|
+
1,
|
|
153
|
+
),
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const MIN_ZOOM = 0.55
|
|
158
|
+
const MAX_ZOOM = 3.4
|
|
159
|
+
|
|
160
|
+
function clampZoom(value: number) {
|
|
161
|
+
return Math.min(MAX_ZOOM, Math.max(MIN_ZOOM, value))
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function touchDistance(touches: TouchList) {
|
|
165
|
+
if (touches.length < 2) return 0
|
|
166
|
+
return Math.hypot(
|
|
167
|
+
touches[0].clientX - touches[1].clientX,
|
|
168
|
+
touches[0].clientY - touches[1].clientY,
|
|
169
|
+
)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function CameraRig({
|
|
173
|
+
position,
|
|
174
|
+
lookAt,
|
|
175
|
+
zoom,
|
|
176
|
+
}: {
|
|
177
|
+
position: [number, number, number]
|
|
178
|
+
lookAt: [number, number, number]
|
|
179
|
+
zoom: number
|
|
180
|
+
}) {
|
|
181
|
+
const { camera } = useThree()
|
|
182
|
+
const aim = () => {
|
|
183
|
+
const scale = 1 / zoom
|
|
184
|
+
camera.up.set(0, 1, 0)
|
|
185
|
+
camera.position.set(
|
|
186
|
+
lookAt[0] + (position[0] - lookAt[0]) * scale,
|
|
187
|
+
lookAt[1] + (position[1] - lookAt[1]) * scale,
|
|
188
|
+
lookAt[2] + (position[2] - lookAt[2]) * scale,
|
|
189
|
+
)
|
|
190
|
+
camera.lookAt(lookAt[0], lookAt[1], lookAt[2])
|
|
191
|
+
camera.updateProjectionMatrix()
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
useLayoutEffect(aim, [camera, lookAt, position, zoom])
|
|
195
|
+
useFrame(aim)
|
|
196
|
+
return null
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function ThumbnailScene({
|
|
200
|
+
graph,
|
|
201
|
+
layout,
|
|
202
|
+
importedBy,
|
|
203
|
+
target,
|
|
204
|
+
highlightedFolders,
|
|
205
|
+
planned,
|
|
206
|
+
created,
|
|
207
|
+
deleted,
|
|
208
|
+
zoom,
|
|
209
|
+
}: {
|
|
210
|
+
graph: CodebaseGraph
|
|
211
|
+
layout: WorldLayout
|
|
212
|
+
importedBy: boolean
|
|
213
|
+
target: ThumbnailTarget
|
|
214
|
+
highlightedFolders: Partial<Record<string, ChangeKind>>
|
|
215
|
+
planned: Set<string>
|
|
216
|
+
created: Set<string>
|
|
217
|
+
deleted: Set<string>
|
|
218
|
+
zoom: number
|
|
219
|
+
}) {
|
|
220
|
+
return (
|
|
221
|
+
<>
|
|
222
|
+
<color attach="background" args={[WORLD_VOID]} />
|
|
223
|
+
<hemisphereLight args={['#d7e2ee', '#2a3038', 1.1]} />
|
|
224
|
+
<directionalLight position={[8, 60, 8]} intensity={1.35} />
|
|
225
|
+
<ambientLight intensity={0.7} />
|
|
226
|
+
<pointLight
|
|
227
|
+
position={[
|
|
228
|
+
target.camera.position[0],
|
|
229
|
+
target.camera.position[1] - 1.4,
|
|
230
|
+
target.camera.position[2],
|
|
231
|
+
]}
|
|
232
|
+
color="#f4f1e8"
|
|
233
|
+
intensity={7}
|
|
234
|
+
distance={40}
|
|
235
|
+
decay={1.2}
|
|
236
|
+
/>
|
|
237
|
+
<CameraRig
|
|
238
|
+
position={target.camera.position}
|
|
239
|
+
lookAt={target.camera.lookAt}
|
|
240
|
+
zoom={zoom}
|
|
241
|
+
/>
|
|
242
|
+
<FolderArea
|
|
243
|
+
folder={target.folder}
|
|
244
|
+
highlightKind={highlightedFolders[target.folder.path] ?? null}
|
|
245
|
+
previewLabels
|
|
246
|
+
/>
|
|
247
|
+
{target.files.map(({ file, placed }) => {
|
|
248
|
+
const selected = file.id === target.selectedFileId
|
|
249
|
+
const related = target.relatedIds.has(file.id)
|
|
250
|
+
const changeKind = fileChangeKind(file.id, planned, created, deleted)
|
|
251
|
+
return (
|
|
252
|
+
<FileBlock
|
|
253
|
+
key={file.id}
|
|
254
|
+
file={file}
|
|
255
|
+
placed={placed}
|
|
256
|
+
selected={selected}
|
|
257
|
+
related={related}
|
|
258
|
+
planned={Boolean(changeKind)}
|
|
259
|
+
changeKind={changeKind}
|
|
260
|
+
added={created.has(file.id) || file.userCreated}
|
|
261
|
+
highlightMapChange
|
|
262
|
+
previewLabels
|
|
263
|
+
dimmed={
|
|
264
|
+
Boolean(target.selectedFileId) &&
|
|
265
|
+
!selected &&
|
|
266
|
+
!related &&
|
|
267
|
+
!changeKind
|
|
268
|
+
}
|
|
269
|
+
/>
|
|
270
|
+
)
|
|
271
|
+
})}
|
|
272
|
+
{target.selectedFileId && (
|
|
273
|
+
<RelationLines
|
|
274
|
+
selectedId={target.selectedFileId}
|
|
275
|
+
aimedRelation={null}
|
|
276
|
+
files={graph.files}
|
|
277
|
+
layout={layout}
|
|
278
|
+
fromAbove
|
|
279
|
+
importedBy={importedBy}
|
|
280
|
+
/>
|
|
281
|
+
)}
|
|
282
|
+
</>
|
|
283
|
+
)
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
export function SelectionThumbnail({
|
|
287
|
+
graph,
|
|
288
|
+
layout,
|
|
289
|
+
selectedId,
|
|
290
|
+
selectedFolder,
|
|
291
|
+
landAt,
|
|
292
|
+
importedBy = false,
|
|
293
|
+
minimized = false,
|
|
294
|
+
plannedIds = [],
|
|
295
|
+
createdIds = [],
|
|
296
|
+
deletedIds = [],
|
|
297
|
+
onMinimize,
|
|
298
|
+
onHide,
|
|
299
|
+
}: SelectionThumbnailProps) {
|
|
300
|
+
const stageRef = useRef<HTMLDivElement>(null)
|
|
301
|
+
const zoomRef = useRef(1)
|
|
302
|
+
const pinchRef = useRef({ start: 0, zoom: 1 })
|
|
303
|
+
const [zoom, setZoom] = useState(1)
|
|
304
|
+
const planned = useMemo(() => new Set(plannedIds), [plannedIds])
|
|
305
|
+
const created = useMemo(() => new Set(createdIds), [createdIds])
|
|
306
|
+
const deleted = useMemo(() => new Set(deletedIds), [deletedIds])
|
|
307
|
+
const highlightedFolders = useMemo(
|
|
308
|
+
() => folderChangeHighlights(planned, created, deleted, layout.folders),
|
|
309
|
+
[created, deleted, layout.folders, planned],
|
|
310
|
+
)
|
|
311
|
+
const target = useMemo(
|
|
312
|
+
() =>
|
|
313
|
+
resolveTarget(
|
|
314
|
+
graph,
|
|
315
|
+
layout,
|
|
316
|
+
selectedId,
|
|
317
|
+
selectedFolder,
|
|
318
|
+
landAt,
|
|
319
|
+
importedBy,
|
|
320
|
+
),
|
|
321
|
+
[graph, importedBy, landAt, layout, selectedFolder, selectedId],
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
useEffect(() => {
|
|
325
|
+
setZoom(1)
|
|
326
|
+
zoomRef.current = 1
|
|
327
|
+
}, [selectedId, selectedFolder, landAt])
|
|
328
|
+
|
|
329
|
+
useEffect(() => {
|
|
330
|
+
zoomRef.current = zoom
|
|
331
|
+
}, [zoom])
|
|
332
|
+
|
|
333
|
+
useEffect(() => {
|
|
334
|
+
const stage = stageRef.current
|
|
335
|
+
if (!stage || minimized) return
|
|
336
|
+
|
|
337
|
+
const applyZoom = (next: number) => {
|
|
338
|
+
const clamped = clampZoom(next)
|
|
339
|
+
zoomRef.current = clamped
|
|
340
|
+
setZoom(clamped)
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const onWheel = (event: WheelEvent) => {
|
|
344
|
+
if (!event.ctrlKey && !event.metaKey) return
|
|
345
|
+
event.preventDefault()
|
|
346
|
+
event.stopPropagation()
|
|
347
|
+
applyZoom(zoomRef.current * Math.exp(-event.deltaY * 0.01))
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const onGestureStart = (event: Event) => {
|
|
351
|
+
event.preventDefault()
|
|
352
|
+
pinchRef.current.zoom = zoomRef.current
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const onGestureChange = (event: Event) => {
|
|
356
|
+
event.preventDefault()
|
|
357
|
+
const scale = Number((event as Event & { scale?: number }).scale)
|
|
358
|
+
if (!Number.isFinite(scale) || scale <= 0) return
|
|
359
|
+
applyZoom(pinchRef.current.zoom * scale)
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const onTouchStart = (event: TouchEvent) => {
|
|
363
|
+
if (event.touches.length !== 2) return
|
|
364
|
+
pinchRef.current = {
|
|
365
|
+
start: touchDistance(event.touches),
|
|
366
|
+
zoom: zoomRef.current,
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const onTouchMove = (event: TouchEvent) => {
|
|
371
|
+
if (event.touches.length !== 2 || pinchRef.current.start <= 0) return
|
|
372
|
+
event.preventDefault()
|
|
373
|
+
event.stopPropagation()
|
|
374
|
+
applyZoom(
|
|
375
|
+
pinchRef.current.zoom *
|
|
376
|
+
(touchDistance(event.touches) / pinchRef.current.start),
|
|
377
|
+
)
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
stage.addEventListener('wheel', onWheel, { passive: false, capture: true })
|
|
381
|
+
stage.addEventListener('gesturestart', onGestureStart, { capture: true })
|
|
382
|
+
stage.addEventListener('gesturechange', onGestureChange, { capture: true })
|
|
383
|
+
stage.addEventListener('touchstart', onTouchStart, { passive: true })
|
|
384
|
+
stage.addEventListener('touchmove', onTouchMove, { passive: false, capture: true })
|
|
385
|
+
return () => {
|
|
386
|
+
stage.removeEventListener('wheel', onWheel, { capture: true })
|
|
387
|
+
stage.removeEventListener('gesturestart', onGestureStart, { capture: true })
|
|
388
|
+
stage.removeEventListener('gesturechange', onGestureChange, { capture: true })
|
|
389
|
+
stage.removeEventListener('touchstart', onTouchStart)
|
|
390
|
+
stage.removeEventListener('touchmove', onTouchMove, { capture: true })
|
|
391
|
+
}
|
|
392
|
+
}, [minimized])
|
|
393
|
+
|
|
394
|
+
if (!target) return null
|
|
395
|
+
|
|
396
|
+
return (
|
|
397
|
+
<div className="hud-thumbnail" data-minimized={minimized}>
|
|
398
|
+
<div className="hud-thumbnail-bar">
|
|
399
|
+
<span>3D view</span>
|
|
400
|
+
<div className="hud-panel-controls">
|
|
401
|
+
{onMinimize && (
|
|
402
|
+
<button
|
|
403
|
+
className="hud-button hud-icon-button hud-panel-control"
|
|
404
|
+
type="button"
|
|
405
|
+
aria-label={minimized ? 'Restore 3D view' : 'Minimize 3D view'}
|
|
406
|
+
onClick={onMinimize}
|
|
407
|
+
>
|
|
408
|
+
{minimized ? '+' : '−'}
|
|
409
|
+
</button>
|
|
410
|
+
)}
|
|
411
|
+
<button
|
|
412
|
+
className="hud-button hud-icon-button hud-panel-control"
|
|
413
|
+
type="button"
|
|
414
|
+
aria-label="Close 3D view"
|
|
415
|
+
onClick={onHide}
|
|
416
|
+
>
|
|
417
|
+
×
|
|
418
|
+
</button>
|
|
419
|
+
</div>
|
|
420
|
+
</div>
|
|
421
|
+
{!minimized && (
|
|
422
|
+
<div className="hud-thumbnail-stage" ref={stageRef}>
|
|
423
|
+
<Canvas
|
|
424
|
+
shadows={false}
|
|
425
|
+
dpr={[1, 1.5]}
|
|
426
|
+
resize={{ offsetSize: true }}
|
|
427
|
+
gl={{ antialias: true, toneMappingExposure: 1.25 }}
|
|
428
|
+
camera={{
|
|
429
|
+
fov: 50,
|
|
430
|
+
near: 0.1,
|
|
431
|
+
far: 400,
|
|
432
|
+
position: target.camera.position,
|
|
433
|
+
}}
|
|
434
|
+
>
|
|
435
|
+
<ThumbnailScene
|
|
436
|
+
graph={graph}
|
|
437
|
+
layout={layout}
|
|
438
|
+
importedBy={importedBy}
|
|
439
|
+
target={target}
|
|
440
|
+
highlightedFolders={highlightedFolders}
|
|
441
|
+
planned={planned}
|
|
442
|
+
created={created}
|
|
443
|
+
deleted={deleted}
|
|
444
|
+
zoom={zoom}
|
|
445
|
+
/>
|
|
446
|
+
</Canvas>
|
|
447
|
+
</div>
|
|
448
|
+
)}
|
|
449
|
+
</div>
|
|
450
|
+
)
|
|
451
|
+
}
|
|
@@ -8,9 +8,13 @@ import { SelectionController } from './SelectionController'
|
|
|
8
8
|
import { UserContextTracker } from './UserContextTracker'
|
|
9
9
|
import { BlockPlacer } from './BlockPlacer'
|
|
10
10
|
import { IslandPlacer } from './IslandPlacer'
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
fileChangeKind,
|
|
13
|
+
filesImporting,
|
|
14
|
+
folderChangeHighlights,
|
|
15
|
+
folderOfFile,
|
|
16
|
+
} from '../layout'
|
|
12
17
|
import { WORLD_VOID } from '../theme'
|
|
13
|
-
import type { ChangeKind } from '../theme'
|
|
14
18
|
import type {
|
|
15
19
|
CodebaseGraph,
|
|
16
20
|
FileNode,
|
|
@@ -127,37 +131,12 @@ export function World({
|
|
|
127
131
|
Boolean(selectedFolder) ||
|
|
128
132
|
planned.size > 0 ||
|
|
129
133
|
deleted.size > 0
|
|
130
|
-
const highlightedFolders
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
folderKinds.set(folder, kinds)
|
|
137
|
-
}
|
|
138
|
-
for (const id of planned) {
|
|
139
|
-
addFolderKind(id, created.has(id) ? 'add' : 'edit')
|
|
140
|
-
}
|
|
141
|
-
for (const id of deleted) addFolderKind(id, 'remove')
|
|
142
|
-
for (const [folder, kinds] of folderKinds) {
|
|
143
|
-
if (kinds.size === 1) highlightedFolders[folder] = [...kinds][0]
|
|
144
|
-
}
|
|
145
|
-
if (planned.size > 0 || deleted.size > 0) {
|
|
146
|
-
for (const folder of Object.values(layout.folders)) {
|
|
147
|
-
if (!folder.added) continue
|
|
148
|
-
const kinds = folderKinds.get(folder.path)
|
|
149
|
-
if (!kinds || kinds.size === 1) {
|
|
150
|
-
highlightedFolders[folder.path] ??= 'add'
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
const changeKindOf = (id: string): ChangeKind | null => {
|
|
156
|
-
if (deleted.has(id)) return 'remove'
|
|
157
|
-
if (created.has(id)) return 'add'
|
|
158
|
-
if (planned.has(id)) return 'edit'
|
|
159
|
-
return null
|
|
160
|
-
}
|
|
134
|
+
const highlightedFolders = folderChangeHighlights(
|
|
135
|
+
planned,
|
|
136
|
+
created,
|
|
137
|
+
deleted,
|
|
138
|
+
layout.folders,
|
|
139
|
+
)
|
|
161
140
|
|
|
162
141
|
return (
|
|
163
142
|
<>
|
|
@@ -188,7 +167,9 @@ export function World({
|
|
|
188
167
|
naming={folder.path === namingIslandId}
|
|
189
168
|
selected={folder.path === selectedFolder}
|
|
190
169
|
mapMode={mapping}
|
|
191
|
-
highlightKind={
|
|
170
|
+
highlightKind={
|
|
171
|
+
mapping ? highlightedFolders[folder.path] ?? null : null
|
|
172
|
+
}
|
|
192
173
|
/>
|
|
193
174
|
))}
|
|
194
175
|
{layout.bridges.map((bridge) => (
|
|
@@ -200,7 +181,7 @@ export function World({
|
|
|
200
181
|
const selected = file.id === selectedId
|
|
201
182
|
const isRelated = related.has(file.id)
|
|
202
183
|
const isPlanned = planned.has(file.id) || deleted.has(file.id)
|
|
203
|
-
const changeKind =
|
|
184
|
+
const changeKind = fileChangeKind(file.id, planned, created, deleted)
|
|
204
185
|
const naming = file.id === namingId
|
|
205
186
|
return (
|
|
206
187
|
<FileBlock
|
|
@@ -54,10 +54,16 @@ export const CHANGE_HIGHLIGHT: Record<
|
|
|
54
54
|
},
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
export const FILE_SELECTION = {
|
|
58
|
+
color: '#c026ff',
|
|
59
|
+
emissive: '#6d00b8',
|
|
60
|
+
}
|
|
61
|
+
|
|
57
62
|
export const MAP_SELECTION = {
|
|
58
63
|
color: '#000000',
|
|
64
|
+
island: FILE_SELECTION.color,
|
|
59
65
|
islandPad: 0.38,
|
|
60
|
-
blockPad: 0.
|
|
66
|
+
blockPad: 0.1,
|
|
61
67
|
}
|
|
62
68
|
|
|
63
69
|
export function fileHeight(lines: number) {
|
|
@@ -152,6 +152,13 @@ export function isReviewingIntent(status: AgentIntentStatus) {
|
|
|
152
152
|
)
|
|
153
153
|
}
|
|
154
154
|
|
|
155
|
+
export function canStopSession(intent: {
|
|
156
|
+
sessionId: string | null
|
|
157
|
+
status: AgentIntentStatus
|
|
158
|
+
}) {
|
|
159
|
+
return Boolean(intent.sessionId) && intent.status !== 'idle'
|
|
160
|
+
}
|
|
161
|
+
|
|
155
162
|
export type PlanStep = {
|
|
156
163
|
index: number
|
|
157
164
|
title: string
|
|
@@ -208,6 +215,11 @@ export type WorkflowAction =
|
|
|
208
215
|
| 'blueprint_send'
|
|
209
216
|
| 'blueprint_update'
|
|
210
217
|
|
|
218
|
+
export type AgentIntentBundle = {
|
|
219
|
+
focusedSessionId: string | null
|
|
220
|
+
intents: AgentIntent[]
|
|
221
|
+
}
|
|
222
|
+
|
|
211
223
|
export type AgentIntent = {
|
|
212
224
|
updatedAt: string | null
|
|
213
225
|
showMap: boolean
|