@jkwd/inbase 0.1.3 → 0.1.4
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/apps/explorer/scripts/open-editor.d.ts +6 -0
- package/apps/explorer/scripts/open-editor.mjs +219 -0
- package/apps/explorer/scripts/session-store.d.ts +31 -1
- package/apps/explorer/scripts/session-store.mjs +153 -26
- package/apps/explorer/src/App.tsx +35 -4
- package/apps/explorer/src/agentIntent.ts +21 -0
- package/apps/explorer/src/index.css +21 -0
- package/apps/explorer/src/scene/MapView.tsx +34 -9
- package/apps/explorer/src/scene/World.tsx +4 -16
- package/apps/explorer/src/theme.ts +3 -0
- package/apps/explorer/src/ui/HUD.tsx +38 -33
- package/apps/explorer/vite.config.ts +39 -1
- package/bin/session.mjs +1 -1
- package/package.json +3 -1
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export function editorFileUri(filePath: string): string
|
|
2
|
+
export function defaultCursorUserDataDir(): string
|
|
3
|
+
export function discoverCursorUserDataDirs(): string[]
|
|
4
|
+
export function openFoldersFromStorage(storage: unknown): string[]
|
|
5
|
+
export function cursorUserDataDirForFile(filePath: string): string | null
|
|
6
|
+
export function openInEditor(filePath: string): boolean
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process'
|
|
2
|
+
import fs from 'node:fs'
|
|
3
|
+
import os from 'node:os'
|
|
4
|
+
import path from 'node:path'
|
|
5
|
+
import { fileURLToPath } from 'node:url'
|
|
6
|
+
|
|
7
|
+
export function editorFileUri(filePath) {
|
|
8
|
+
return `vscode://file${encodeURI(filePath)}`
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function which(command) {
|
|
12
|
+
const result = spawnSync('which', [command], { encoding: 'utf8' })
|
|
13
|
+
if (result.error || result.status !== 0) return null
|
|
14
|
+
return result.stdout.trim().split('\n')[0] || null
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function defaultCursorUserDataDir() {
|
|
18
|
+
if (process.platform === 'darwin') {
|
|
19
|
+
return path.join(os.homedir(), 'Library/Application Support/Cursor')
|
|
20
|
+
}
|
|
21
|
+
if (process.platform === 'win32') {
|
|
22
|
+
return path.join(os.homedir(), 'AppData/Roaming/Cursor')
|
|
23
|
+
}
|
|
24
|
+
return path.join(os.homedir(), '.config/Cursor')
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function expandUser(value) {
|
|
28
|
+
if (!value) return null
|
|
29
|
+
const trimmed = String(value).trim().replace(/^['"]|['"]$/g, '')
|
|
30
|
+
if (!trimmed) return null
|
|
31
|
+
if (trimmed.startsWith('~')) {
|
|
32
|
+
return path.resolve(os.homedir() + trimmed.slice(1))
|
|
33
|
+
}
|
|
34
|
+
return path.resolve(trimmed)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function isCursorUserDataDir(dir) {
|
|
38
|
+
try {
|
|
39
|
+
if (fs.existsSync(path.join(dir, 'User/globalStorage/storage.json'))) return true
|
|
40
|
+
return fs.readdirSync(dir).some((name) => name.endsWith('-main.sock'))
|
|
41
|
+
} catch {
|
|
42
|
+
return false
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function userDataDirFromHook(hook = process.env.VSCODE_IPC_HOOK) {
|
|
47
|
+
if (!hook || !hook.endsWith('.sock')) return null
|
|
48
|
+
const dir = path.dirname(hook)
|
|
49
|
+
return isCursorUserDataDir(dir) ? dir : null
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function userDataDirsFromProcessList() {
|
|
53
|
+
const result = spawnSync('ps', ['-ax', '-o', 'command='], { encoding: 'utf8' })
|
|
54
|
+
if (result.error || result.status !== 0) return []
|
|
55
|
+
const dirs = []
|
|
56
|
+
for (const line of result.stdout.split('\n')) {
|
|
57
|
+
const match = line.match(/--user-data-dir(?:=|\s+)(\S+)/)
|
|
58
|
+
if (!match) continue
|
|
59
|
+
const dir = expandUser(match[1])
|
|
60
|
+
if (dir && isCursorUserDataDir(dir)) dirs.push(dir)
|
|
61
|
+
}
|
|
62
|
+
return dirs
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function profileDirsInHome() {
|
|
66
|
+
let names = []
|
|
67
|
+
try {
|
|
68
|
+
names = fs.readdirSync(os.homedir())
|
|
69
|
+
} catch {
|
|
70
|
+
return []
|
|
71
|
+
}
|
|
72
|
+
return names
|
|
73
|
+
.filter((name) => name.startsWith('cursor-profile'))
|
|
74
|
+
.map((name) => path.join(os.homedir(), name))
|
|
75
|
+
.filter(isCursorUserDataDir)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function discoverCursorUserDataDirs() {
|
|
79
|
+
const dirs = new Set()
|
|
80
|
+
const explicit = expandUser(
|
|
81
|
+
process.env.INBASE_CURSOR_USER_DATA_DIR || process.env.CURSOR_USER_DATA_DIR,
|
|
82
|
+
)
|
|
83
|
+
if (explicit && isCursorUserDataDir(explicit)) dirs.add(explicit)
|
|
84
|
+
const hookDir = userDataDirFromHook()
|
|
85
|
+
if (hookDir) dirs.add(hookDir)
|
|
86
|
+
const def = defaultCursorUserDataDir()
|
|
87
|
+
if (isCursorUserDataDir(def)) dirs.add(def)
|
|
88
|
+
for (const dir of profileDirsInHome()) dirs.add(dir)
|
|
89
|
+
for (const dir of userDataDirsFromProcessList()) dirs.add(dir)
|
|
90
|
+
return [...dirs]
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function folderPathFromUri(value) {
|
|
94
|
+
if (typeof value !== 'string' || !value) return null
|
|
95
|
+
try {
|
|
96
|
+
return value.startsWith('file:') ? fileURLToPath(value) : path.resolve(value)
|
|
97
|
+
} catch {
|
|
98
|
+
return null
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function fileIsInside(folder, filePath) {
|
|
103
|
+
const root = path.resolve(folder)
|
|
104
|
+
const file = path.resolve(filePath)
|
|
105
|
+
return file === root || file.startsWith(`${root}${path.sep}`)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function openFoldersFromStorage(storage) {
|
|
109
|
+
const state = storage?.windowsState
|
|
110
|
+
if (!state || typeof state !== 'object') return []
|
|
111
|
+
const windows = [
|
|
112
|
+
state.lastActiveWindow,
|
|
113
|
+
...(Array.isArray(state.openedWindows) ? state.openedWindows : []),
|
|
114
|
+
]
|
|
115
|
+
const folders = []
|
|
116
|
+
for (const window of windows) {
|
|
117
|
+
const folder = folderPathFromUri(window?.folder)
|
|
118
|
+
if (folder) folders.push(folder)
|
|
119
|
+
}
|
|
120
|
+
return folders
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function readOpenFolders(userDataDir) {
|
|
124
|
+
const file = path.join(userDataDir, 'User/globalStorage/storage.json')
|
|
125
|
+
try {
|
|
126
|
+
return openFoldersFromStorage(JSON.parse(fs.readFileSync(file, 'utf8')))
|
|
127
|
+
} catch {
|
|
128
|
+
return []
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function lastActiveFolder(userDataDir) {
|
|
133
|
+
const file = path.join(userDataDir, 'User/globalStorage/storage.json')
|
|
134
|
+
try {
|
|
135
|
+
const parsed = JSON.parse(fs.readFileSync(file, 'utf8'))
|
|
136
|
+
return folderPathFromUri(parsed?.windowsState?.lastActiveWindow?.folder)
|
|
137
|
+
} catch {
|
|
138
|
+
return null
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function cursorUserDataDirForFile(filePath) {
|
|
143
|
+
const explicit = expandUser(
|
|
144
|
+
process.env.INBASE_CURSOR_USER_DATA_DIR || process.env.CURSOR_USER_DATA_DIR,
|
|
145
|
+
)
|
|
146
|
+
if (explicit && isCursorUserDataDir(explicit)) return explicit
|
|
147
|
+
|
|
148
|
+
const hookDir = userDataDirFromHook()
|
|
149
|
+
let best = null
|
|
150
|
+
let bestScore = 0
|
|
151
|
+
for (const dir of discoverCursorUserDataDirs()) {
|
|
152
|
+
const folders = readOpenFolders(dir)
|
|
153
|
+
if (!folders.some((folder) => fileIsInside(folder, filePath))) continue
|
|
154
|
+
let score = 1
|
|
155
|
+
if (hookDir && path.resolve(hookDir) === path.resolve(dir)) score += 2
|
|
156
|
+
const last = lastActiveFolder(dir)
|
|
157
|
+
if (last && fileIsInside(last, filePath)) score += 2
|
|
158
|
+
if (score > bestScore) {
|
|
159
|
+
best = dir
|
|
160
|
+
bestScore = score
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return best ?? hookDir
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function cursorCliPaths() {
|
|
167
|
+
const home = os.homedir()
|
|
168
|
+
const fromEnv =
|
|
169
|
+
process.env.CURSOR_CLI && fs.existsSync(process.env.CURSOR_CLI)
|
|
170
|
+
? process.env.CURSOR_CLI
|
|
171
|
+
: null
|
|
172
|
+
return [
|
|
173
|
+
fromEnv,
|
|
174
|
+
which('cursor'),
|
|
175
|
+
'/Applications/Cursor.app/Contents/Resources/app/bin/cursor',
|
|
176
|
+
path.join(home, '.local/bin/cursor'),
|
|
177
|
+
'/usr/local/bin/cursor',
|
|
178
|
+
'/opt/homebrew/bin/cursor',
|
|
179
|
+
].filter((candidate, index, list) => {
|
|
180
|
+
if (!candidate || typeof candidate !== 'string') return false
|
|
181
|
+
return list.indexOf(candidate) === index
|
|
182
|
+
})
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function editorEnv() {
|
|
186
|
+
const env = { ...process.env }
|
|
187
|
+
delete env.ELECTRON_RUN_AS_NODE
|
|
188
|
+
delete env.ELECTRON_NO_ASAR
|
|
189
|
+
delete env.CURSOR_AGENT
|
|
190
|
+
// A second Cursor instance uses its own sockets. Inherited hooks from
|
|
191
|
+
// another profile would send --goto to the wrong window.
|
|
192
|
+
delete env.VSCODE_IPC_HOOK
|
|
193
|
+
delete env.VSCODE_IPC_HOOK_CLI
|
|
194
|
+
return env
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function runEditor(command, args) {
|
|
198
|
+
if (command.includes(path.sep) && !fs.existsSync(command)) return false
|
|
199
|
+
const result = spawnSync(command, args, {
|
|
200
|
+
stdio: 'ignore',
|
|
201
|
+
env: editorEnv(),
|
|
202
|
+
})
|
|
203
|
+
return !result.error && result.status === 0
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export function openInEditor(filePath) {
|
|
207
|
+
const userDataDir = cursorUserDataDirForFile(filePath)
|
|
208
|
+
const goto = ['--goto', `${filePath}:1`]
|
|
209
|
+
if (userDataDir) {
|
|
210
|
+
goto.unshift('--user-data-dir', userDataDir)
|
|
211
|
+
}
|
|
212
|
+
for (const command of cursorCliPaths()) {
|
|
213
|
+
if (runEditor(command, goto)) return true
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const code = which('code')
|
|
217
|
+
if (code && runEditor(code, goto)) return true
|
|
218
|
+
return false
|
|
219
|
+
}
|
|
@@ -119,6 +119,36 @@ export function sessionIntent(
|
|
|
119
119
|
knownFileIds?: string[],
|
|
120
120
|
selectedDiffId?: string,
|
|
121
121
|
): Record<string, unknown> | null
|
|
122
|
+
export function resolveTargetFile(
|
|
123
|
+
targetRoot: string,
|
|
124
|
+
fileId: string,
|
|
125
|
+
): { id: string; absolute: string }
|
|
126
|
+
export function captureBaseline(
|
|
127
|
+
dataDir: string,
|
|
128
|
+
sessionId: string,
|
|
129
|
+
targetRoot: string,
|
|
130
|
+
fileIds?: string[],
|
|
131
|
+
): { files: Record<string, { existed: boolean }> }
|
|
132
|
+
export function restoreBaseline(
|
|
133
|
+
dataDir: string,
|
|
134
|
+
sessionId: string,
|
|
135
|
+
targetRoot: string,
|
|
136
|
+
): void
|
|
137
|
+
export function materializeDiff(
|
|
138
|
+
dataDir: string,
|
|
139
|
+
targetRoot: string,
|
|
140
|
+
sessionId: string,
|
|
141
|
+
diffId?: string | null,
|
|
142
|
+
): DiffManifest
|
|
143
|
+
export function inspectTargetFile(
|
|
144
|
+
dataDir: string,
|
|
145
|
+
targetRoot: string,
|
|
146
|
+
input?: {
|
|
147
|
+
sessionId?: string | null
|
|
148
|
+
diffId?: string | null
|
|
149
|
+
fileId?: string | null
|
|
150
|
+
},
|
|
151
|
+
): string | null
|
|
122
152
|
export function appendDiff(
|
|
123
153
|
dataDir: string,
|
|
124
154
|
targetRoot: string,
|
|
@@ -142,7 +172,7 @@ export function requestReplan(
|
|
|
142
172
|
export function stopSession(
|
|
143
173
|
dataDir: string,
|
|
144
174
|
sessionId: string,
|
|
145
|
-
|
|
175
|
+
targetRoot?: string | null,
|
|
146
176
|
): null
|
|
147
177
|
export function decideDiff(
|
|
148
178
|
dataDir: string,
|
|
@@ -44,9 +44,28 @@ export function sessionPaths(dataDir, sessionId) {
|
|
|
44
44
|
diffs: path.join(root, 'diffs'),
|
|
45
45
|
manifest: path.join(root, 'manifest.json'),
|
|
46
46
|
blueprint: path.join(root, 'blueprint.json'),
|
|
47
|
+
baseline: path.join(root, 'baseline.json'),
|
|
48
|
+
baselineFiles: path.join(root, 'baseline'),
|
|
47
49
|
}
|
|
48
50
|
}
|
|
49
51
|
|
|
52
|
+
export function resolveTargetFile(targetRoot, fileId) {
|
|
53
|
+
if (typeof fileId !== 'string' || fileId.trim() === '') {
|
|
54
|
+
throw new Error('fileId is required')
|
|
55
|
+
}
|
|
56
|
+
const normalized = fileId.trim().replaceAll('\\', '/').replace(/^\/+/, '')
|
|
57
|
+
if (!normalized || normalized === '.' || normalized.includes('..')) {
|
|
58
|
+
throw new Error(`Invalid file id ${fileId}`)
|
|
59
|
+
}
|
|
60
|
+
const root = path.resolve(targetRoot)
|
|
61
|
+
const absolute = path.resolve(root, normalized)
|
|
62
|
+
const prefix = root.endsWith(path.sep) ? root : `${root}${path.sep}`
|
|
63
|
+
if (absolute !== root && !absolute.startsWith(prefix)) {
|
|
64
|
+
throw new Error(`Invalid file id ${fileId}`)
|
|
65
|
+
}
|
|
66
|
+
return { id: normalized, absolute }
|
|
67
|
+
}
|
|
68
|
+
|
|
50
69
|
export function readActiveSession(dataDir) {
|
|
51
70
|
const value = readJson(path.join(dataDir, 'active-session.json'), null)
|
|
52
71
|
return value?.sessionId ? assertSessionId(value.sessionId) : null
|
|
@@ -305,10 +324,105 @@ export function sessionIntent(dataDir, sessionId, knownFileIds = [], selectedDif
|
|
|
305
324
|
}
|
|
306
325
|
}
|
|
307
326
|
|
|
308
|
-
function
|
|
327
|
+
function emptyBaseline() {
|
|
328
|
+
return { files: {} }
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function readBaseline(dataDir, sessionId) {
|
|
332
|
+
const { baseline } = sessionPaths(dataDir, sessionId)
|
|
333
|
+
const value = readJson(baseline, emptyBaseline())
|
|
334
|
+
return {
|
|
335
|
+
files:
|
|
336
|
+
value?.files && typeof value.files === 'object' && !Array.isArray(value.files)
|
|
337
|
+
? value.files
|
|
338
|
+
: {},
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function writeBaseline(dataDir, sessionId, baseline) {
|
|
343
|
+
const { baseline: file } = sessionPaths(dataDir, sessionId)
|
|
344
|
+
atomicWrite(file, `${JSON.stringify({ files: baseline.files ?? {} }, null, 2)}\n`)
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function pruneEmptyDirs(targetRoot, filePath) {
|
|
348
|
+
const root = path.resolve(targetRoot)
|
|
349
|
+
let current = path.dirname(filePath)
|
|
350
|
+
while (current.startsWith(`${root}${path.sep}`)) {
|
|
351
|
+
if (!fs.existsSync(current)) {
|
|
352
|
+
current = path.dirname(current)
|
|
353
|
+
continue
|
|
354
|
+
}
|
|
355
|
+
if (fs.readdirSync(current).length > 0) break
|
|
356
|
+
fs.rmdirSync(current)
|
|
357
|
+
current = path.dirname(current)
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
export function captureBaseline(dataDir, sessionId, targetRoot, fileIds = []) {
|
|
362
|
+
const paths = sessionPaths(dataDir, sessionId)
|
|
363
|
+
const baseline = readBaseline(dataDir, sessionId)
|
|
364
|
+
let changed = false
|
|
365
|
+
for (const fileId of fileIds) {
|
|
366
|
+
const { id, absolute } = resolveTargetFile(targetRoot, fileId)
|
|
367
|
+
if (baseline.files[id]) continue
|
|
368
|
+
const existed = fs.existsSync(absolute) && fs.statSync(absolute).isFile()
|
|
369
|
+
baseline.files[id] = { existed }
|
|
370
|
+
if (existed) {
|
|
371
|
+
const stored = resolveTargetFile(paths.baselineFiles, id).absolute
|
|
372
|
+
fs.mkdirSync(path.dirname(stored), { recursive: true })
|
|
373
|
+
fs.copyFileSync(absolute, stored)
|
|
374
|
+
}
|
|
375
|
+
changed = true
|
|
376
|
+
}
|
|
377
|
+
if (changed) writeBaseline(dataDir, sessionId, baseline)
|
|
378
|
+
return baseline
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
export function restoreBaseline(dataDir, sessionId, targetRoot) {
|
|
382
|
+
const paths = sessionPaths(dataDir, sessionId)
|
|
383
|
+
const baseline = readBaseline(dataDir, sessionId)
|
|
384
|
+
for (const [fileId, info] of Object.entries(baseline.files)) {
|
|
385
|
+
const { absolute } = resolveTargetFile(targetRoot, fileId)
|
|
386
|
+
if (!info?.existed) {
|
|
387
|
+
fs.rmSync(absolute, { force: true })
|
|
388
|
+
pruneEmptyDirs(targetRoot, absolute)
|
|
389
|
+
continue
|
|
390
|
+
}
|
|
391
|
+
const stored = resolveTargetFile(paths.baselineFiles, fileId).absolute
|
|
392
|
+
fs.mkdirSync(path.dirname(absolute), { recursive: true })
|
|
393
|
+
fs.copyFileSync(stored, absolute)
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function replayPatches(dataDir, sessionId, targetRoot, entries) {
|
|
398
|
+
for (const entry of entries) {
|
|
399
|
+
applyUnifiedPatch(readDiff(dataDir, sessionId, entry), targetRoot)
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function acceptedEntries(manifest) {
|
|
404
|
+
return manifest.diffs.filter((entry) => entry.status === 'applied')
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function liveEntries(manifest, diffId) {
|
|
408
|
+
return chainThrough(manifest, diffId).filter((entry) => entry.status !== 'rejected')
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
export function materializeDiff(dataDir, targetRoot, sessionId, diffId) {
|
|
412
|
+
const manifest = readManifest(dataDir, assertSessionId(sessionId))
|
|
413
|
+
if (!manifest) throw new Error(`Unknown session ${sessionId}`)
|
|
414
|
+
const through = diffId || manifest.activeDiffId
|
|
415
|
+
if (!through) return manifest
|
|
416
|
+
restoreBaseline(dataDir, sessionId, targetRoot)
|
|
417
|
+
replayPatches(dataDir, sessionId, targetRoot, liveEntries(manifest, through))
|
|
418
|
+
return manifest
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function withSessionReplay(dataDir, sessionId, targetRoot, patches, action) {
|
|
309
422
|
const temporary = fs.mkdtempSync(path.join(os.tmpdir(), 'visual-coder-chain-'))
|
|
310
423
|
try {
|
|
311
424
|
fs.cpSync(targetRoot, temporary, { recursive: true })
|
|
425
|
+
restoreBaseline(dataDir, sessionId, temporary)
|
|
312
426
|
for (const patchText of patches) applyUnifiedPatch(patchText, temporary)
|
|
313
427
|
return action(temporary)
|
|
314
428
|
} finally {
|
|
@@ -317,10 +431,28 @@ function withVirtualTarget(targetRoot, patches, action) {
|
|
|
317
431
|
}
|
|
318
432
|
|
|
319
433
|
export function validateContinuation(dataDir, manifest, targetRoot, patchText) {
|
|
320
|
-
const prior =
|
|
321
|
-
|
|
434
|
+
const prior = manifest.diffs
|
|
435
|
+
.filter((entry) => entry.status !== 'rejected')
|
|
436
|
+
.map((entry) => readDiff(dataDir, manifest.sessionId, entry))
|
|
437
|
+
withSessionReplay(dataDir, manifest.sessionId, targetRoot, [...prior, patchText], () =>
|
|
438
|
+
undefined,
|
|
322
439
|
)
|
|
323
|
-
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
export function inspectTargetFile(
|
|
443
|
+
dataDir,
|
|
444
|
+
targetRoot,
|
|
445
|
+
{ sessionId, diffId, fileId } = {},
|
|
446
|
+
) {
|
|
447
|
+
if (sessionId && readManifest(dataDir, sessionId)) {
|
|
448
|
+
materializeDiff(dataDir, targetRoot, sessionId, diffId)
|
|
449
|
+
}
|
|
450
|
+
if (!fileId) return null
|
|
451
|
+
const { absolute } = resolveTargetFile(targetRoot, fileId)
|
|
452
|
+
if (!fs.existsSync(absolute) || !fs.statSync(absolute).isFile()) {
|
|
453
|
+
throw new Error(`File ${fileId} is not on disk`)
|
|
454
|
+
}
|
|
455
|
+
return absolute
|
|
324
456
|
}
|
|
325
457
|
|
|
326
458
|
function planSteps(titles, startAt = 1) {
|
|
@@ -555,6 +687,12 @@ export function appendDiff(dataDir, targetRoot, input) {
|
|
|
555
687
|
}
|
|
556
688
|
|
|
557
689
|
validateContinuation(dataDir, manifest, targetRoot, input.patchText)
|
|
690
|
+
captureBaseline(
|
|
691
|
+
dataDir,
|
|
692
|
+
sessionId,
|
|
693
|
+
targetRoot,
|
|
694
|
+
parseUnifiedPatch(input.patchText).entries.map((entry) => entry.id),
|
|
695
|
+
)
|
|
558
696
|
if (parent?.status === 'extend') parent.status = 'extended'
|
|
559
697
|
|
|
560
698
|
const id = String(manifest.diffs.length + 1).padStart(4, '0')
|
|
@@ -581,6 +719,7 @@ export function appendDiff(dataDir, targetRoot, input) {
|
|
|
581
719
|
manifest.workStartedAt = null
|
|
582
720
|
manifest.diffs.push(entry)
|
|
583
721
|
writeManifest(dataDir, manifest)
|
|
722
|
+
materializeDiff(dataDir, targetRoot, sessionId, id)
|
|
584
723
|
focusSession(dataDir, sessionId)
|
|
585
724
|
return { manifest, entry }
|
|
586
725
|
}
|
|
@@ -601,25 +740,7 @@ function pendingActive(manifest, diffId) {
|
|
|
601
740
|
|
|
602
741
|
function applyUnresolved(dataDir, targetRoot, manifest, diffId) {
|
|
603
742
|
const unresolved = unresolvedEntries(manifest, diffId)
|
|
604
|
-
|
|
605
|
-
readDiff(dataDir, manifest.sessionId, entry),
|
|
606
|
-
)
|
|
607
|
-
const touched = new Set()
|
|
608
|
-
for (const patch of patches) {
|
|
609
|
-
for (const entry of parseUnifiedPatch(patch).entries) touched.add(entry.id)
|
|
610
|
-
}
|
|
611
|
-
withVirtualTarget(targetRoot, patches, (virtualRoot) => {
|
|
612
|
-
for (const id of touched) {
|
|
613
|
-
const source = path.join(virtualRoot, id)
|
|
614
|
-
const destination = path.join(targetRoot, id)
|
|
615
|
-
if (!fs.existsSync(source)) {
|
|
616
|
-
fs.rmSync(destination, { recursive: true, force: true })
|
|
617
|
-
continue
|
|
618
|
-
}
|
|
619
|
-
fs.mkdirSync(path.dirname(destination), { recursive: true })
|
|
620
|
-
fs.copyFileSync(source, destination)
|
|
621
|
-
}
|
|
622
|
-
})
|
|
743
|
+
materializeDiff(dataDir, targetRoot, manifest.sessionId, diffId)
|
|
623
744
|
for (const entry of unresolved) {
|
|
624
745
|
entry.status = 'applied'
|
|
625
746
|
entry.decidedAt = new Date().toISOString()
|
|
@@ -663,8 +784,14 @@ export function requestReplan(dataDir, sessionId, diffId, instruction) {
|
|
|
663
784
|
return manifest
|
|
664
785
|
}
|
|
665
786
|
|
|
666
|
-
export function stopSession(dataDir, sessionId,
|
|
667
|
-
|
|
787
|
+
export function stopSession(dataDir, sessionId, targetRoot = null) {
|
|
788
|
+
const safeId = assertSessionId(sessionId)
|
|
789
|
+
const manifest = readManifest(dataDir, safeId)
|
|
790
|
+
if (manifest && targetRoot) {
|
|
791
|
+
restoreBaseline(dataDir, safeId, targetRoot)
|
|
792
|
+
replayPatches(dataDir, safeId, targetRoot, acceptedEntries(manifest))
|
|
793
|
+
}
|
|
794
|
+
finalizeFinishedSession(dataDir, safeId)
|
|
668
795
|
return null
|
|
669
796
|
}
|
|
670
797
|
|
|
@@ -682,7 +809,7 @@ export function decideDiff(
|
|
|
682
809
|
if (decision === 'extend') {
|
|
683
810
|
return requestReplan(dataDir, sessionId, diffId, instruction)
|
|
684
811
|
}
|
|
685
|
-
return stopSession(dataDir, sessionId,
|
|
812
|
+
return stopSession(dataDir, sessionId, targetRoot)
|
|
686
813
|
}
|
|
687
814
|
|
|
688
815
|
export function closeSession(dataDir, sessionId) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
|
2
2
|
import { Canvas } from '@react-three/fiber'
|
|
3
|
-
import { emptyIntent, fetchAgentIntent, performAgentAction, persistSessionBlueprint } from './agentIntent'
|
|
3
|
+
import { emptyIntent, fetchAgentIntent, inspectTargetFile, performAgentAction, persistSessionBlueprint } from './agentIntent'
|
|
4
4
|
import { fetchCodebase } from './codebase'
|
|
5
5
|
import {
|
|
6
6
|
layoutWorld,
|
|
@@ -163,6 +163,7 @@ function Explorer({
|
|
|
163
163
|
])
|
|
164
164
|
const walkPos = useRef<[number, number]>([layout.spawn[0], layout.spawn[2]])
|
|
165
165
|
const [selectedId, setSelectedId] = useState<string | null>(null)
|
|
166
|
+
const [selectedTick, setSelectedTick] = useState(0)
|
|
166
167
|
const [selectedFolder, setSelectedFolder] = useState<string | null>(null)
|
|
167
168
|
const [aimedRelation, setAimedRelation] = useState<AimedRelation | null>(null)
|
|
168
169
|
const [flyTo, setFlyTo] = useState<FlyTo | null>(null)
|
|
@@ -274,7 +275,7 @@ function Explorer({
|
|
|
274
275
|
browsingHistory.current = false
|
|
275
276
|
lastIntentSig.current = intentSignature(next)
|
|
276
277
|
applyIntent(next)
|
|
277
|
-
if (action === 'invoke' || action === 'continue') {
|
|
278
|
+
if (action === 'invoke' || action === 'continue' || action === 'stop') {
|
|
278
279
|
await onRefreshGraph()
|
|
279
280
|
}
|
|
280
281
|
} catch {
|
|
@@ -300,6 +301,16 @@ function Explorer({
|
|
|
300
301
|
try {
|
|
301
302
|
const latest = intent.chain.at(-1)?.id
|
|
302
303
|
browsingHistory.current = diffId !== latest
|
|
304
|
+
if (intent.sessionId) {
|
|
305
|
+
try {
|
|
306
|
+
await inspectTargetFile({
|
|
307
|
+
sessionId: intent.sessionId,
|
|
308
|
+
diffId,
|
|
309
|
+
})
|
|
310
|
+
} catch {
|
|
311
|
+
// Still show the historical preview if disk replay failed.
|
|
312
|
+
}
|
|
313
|
+
}
|
|
303
314
|
const next = await fetchAgentIntent(diffId)
|
|
304
315
|
lastIntentSig.current = intentSignature(next)
|
|
305
316
|
applyIntent(next)
|
|
@@ -307,7 +318,22 @@ function Explorer({
|
|
|
307
318
|
// Keep the current chain position if navigation failed.
|
|
308
319
|
}
|
|
309
320
|
},
|
|
310
|
-
[applyIntent, intent.chain],
|
|
321
|
+
[applyIntent, intent.chain, intent.sessionId],
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
const inspectFile = useCallback(
|
|
325
|
+
async (fileId: string) => {
|
|
326
|
+
try {
|
|
327
|
+
await inspectTargetFile({
|
|
328
|
+
sessionId: intent.sessionId,
|
|
329
|
+
diffId: intent.diffId,
|
|
330
|
+
fileId,
|
|
331
|
+
})
|
|
332
|
+
} catch {
|
|
333
|
+
// Keep the current view if the editor could not open the file.
|
|
334
|
+
}
|
|
335
|
+
},
|
|
336
|
+
[intent.diffId, intent.sessionId],
|
|
311
337
|
)
|
|
312
338
|
|
|
313
339
|
useEffect(() => {
|
|
@@ -566,7 +592,10 @@ function Explorer({
|
|
|
566
592
|
const selectFile = useCallback(
|
|
567
593
|
(fileId: string | null) => {
|
|
568
594
|
setSelectedId(fileId)
|
|
569
|
-
if (fileId)
|
|
595
|
+
if (fileId) {
|
|
596
|
+
setSelectedFolder(null)
|
|
597
|
+
setSelectedTick((tick) => tick + 1)
|
|
598
|
+
}
|
|
570
599
|
if (fileId && intent.creationMode) document.exitPointerLock()
|
|
571
600
|
},
|
|
572
601
|
[intent.creationMode],
|
|
@@ -904,6 +933,7 @@ function Explorer({
|
|
|
904
933
|
mode={mode}
|
|
905
934
|
locked={locked}
|
|
906
935
|
selectedId={selectedId}
|
|
936
|
+
selectedTick={selectedTick}
|
|
907
937
|
selectedFolder={selectedFolder}
|
|
908
938
|
aimedRelation={aimedRelation}
|
|
909
939
|
currentFolder={currentFolder}
|
|
@@ -935,6 +965,7 @@ function Explorer({
|
|
|
935
965
|
onRemoveBlueprintImport={removeBlueprintImport}
|
|
936
966
|
onMapAddFile={placeBlockOnFolder}
|
|
937
967
|
onMapAddFolder={placeIslandOnFolder}
|
|
968
|
+
onInspectFile={inspectFile}
|
|
938
969
|
/>
|
|
939
970
|
</>
|
|
940
971
|
)
|
|
@@ -180,3 +180,24 @@ export function persistSessionBlueprint(
|
|
|
180
180
|
// Keep local drafts if the session handshake is no longer open.
|
|
181
181
|
})
|
|
182
182
|
}
|
|
183
|
+
|
|
184
|
+
export async function inspectTargetFile(payload: {
|
|
185
|
+
sessionId?: string | null
|
|
186
|
+
diffId?: string | null
|
|
187
|
+
fileId?: string
|
|
188
|
+
}) {
|
|
189
|
+
const response = await fetch('/api/inspect-file', {
|
|
190
|
+
method: 'POST',
|
|
191
|
+
headers: { 'Content-Type': 'application/json' },
|
|
192
|
+
body: JSON.stringify(payload),
|
|
193
|
+
})
|
|
194
|
+
if (!response.ok) {
|
|
195
|
+
const detail = await response.text()
|
|
196
|
+
throw new Error(detail || 'Could not inspect file')
|
|
197
|
+
}
|
|
198
|
+
return (await response.json()) as {
|
|
199
|
+
path: string | null
|
|
200
|
+
uri: string | null
|
|
201
|
+
opened: boolean
|
|
202
|
+
}
|
|
203
|
+
}
|
|
@@ -22,6 +22,7 @@ body,
|
|
|
22
22
|
position: absolute;
|
|
23
23
|
inset: 0;
|
|
24
24
|
z-index: 0;
|
|
25
|
+
background: #000;
|
|
25
26
|
}
|
|
26
27
|
|
|
27
28
|
* {
|
|
@@ -566,6 +567,11 @@ button {
|
|
|
566
567
|
word-break: break-all;
|
|
567
568
|
}
|
|
568
569
|
|
|
570
|
+
.hud-inspect {
|
|
571
|
+
margin: 10px 0 0;
|
|
572
|
+
width: 100%;
|
|
573
|
+
}
|
|
574
|
+
|
|
569
575
|
.hud-panel ul {
|
|
570
576
|
margin: 8px 0 0;
|
|
571
577
|
padding-left: 18px;
|
|
@@ -599,6 +605,21 @@ button {
|
|
|
599
605
|
border-color: #8a4a4a;
|
|
600
606
|
}
|
|
601
607
|
|
|
608
|
+
.hud-item-inspect {
|
|
609
|
+
flex: none;
|
|
610
|
+
padding: 2px 6px;
|
|
611
|
+
color: #d7eef8;
|
|
612
|
+
background: transparent;
|
|
613
|
+
border: 1px solid #3a4250;
|
|
614
|
+
cursor: pointer;
|
|
615
|
+
font-size: 11px;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
.hud-item-inspect:hover,
|
|
619
|
+
.hud-item-inspect:focus-visible {
|
|
620
|
+
border-color: #9ad8ff;
|
|
621
|
+
}
|
|
622
|
+
|
|
602
623
|
.hud-add-row {
|
|
603
624
|
display: flex;
|
|
604
625
|
gap: 6px;
|
|
@@ -66,8 +66,14 @@ export function MapView({
|
|
|
66
66
|
const element = gl.domElement
|
|
67
67
|
element.style.cursor = 'grab'
|
|
68
68
|
|
|
69
|
+
const isWalkClick = (event: PointerEvent | MouseEvent) =>
|
|
70
|
+
event.shiftKey || event.ctrlKey || event.metaKey
|
|
71
|
+
|
|
72
|
+
const isWalkButton = (event: PointerEvent) =>
|
|
73
|
+
event.button === 0 || (event.ctrlKey && event.button === 2)
|
|
74
|
+
|
|
69
75
|
const onDown = (event: PointerEvent) => {
|
|
70
|
-
if (event
|
|
76
|
+
if (!isWalkButton(event)) return
|
|
71
77
|
drag.current = { x: event.clientX, y: event.clientY, moved: false, active: true }
|
|
72
78
|
element.style.cursor = 'grabbing'
|
|
73
79
|
}
|
|
@@ -93,20 +99,31 @@ export function MapView({
|
|
|
93
99
|
return { raycaster, relationHit, fileHit }
|
|
94
100
|
}
|
|
95
101
|
|
|
96
|
-
const
|
|
102
|
+
const landAt = (x: number, z: number) => {
|
|
103
|
+
const lock = element.requestPointerLock()
|
|
104
|
+
if (lock && typeof lock.catch === 'function') {
|
|
105
|
+
void lock.catch(() => {})
|
|
106
|
+
}
|
|
107
|
+
onLand(x, z)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const landAtPointer = (clientX: number, clientY: number, allowIsland: boolean) => {
|
|
97
111
|
const pick = pickAt(clientX, clientY)
|
|
98
112
|
if (!pick) return false
|
|
99
113
|
const { raycaster, relationHit, fileHit } = pick
|
|
100
|
-
if (relationHit || fileHit) return false
|
|
101
114
|
|
|
102
115
|
const hit = new THREE.Vector3()
|
|
103
116
|
const plane = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0)
|
|
104
117
|
if (!raycaster.ray.intersectPlane(plane, hit)) return false
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
118
|
+
|
|
119
|
+
const folder = folderAt(hit.x, hit.z, layout)
|
|
120
|
+
if (allowIsland && folder) {
|
|
121
|
+
landAt(hit.x, hit.z)
|
|
122
|
+
return true
|
|
108
123
|
}
|
|
109
|
-
|
|
124
|
+
if (relationHit || fileHit) return false
|
|
125
|
+
|
|
126
|
+
landAt(hit.x, hit.z)
|
|
110
127
|
return true
|
|
111
128
|
}
|
|
112
129
|
|
|
@@ -114,9 +131,11 @@ export function MapView({
|
|
|
114
131
|
element.style.cursor = 'grab'
|
|
115
132
|
const startedOnCanvas = drag.current.active
|
|
116
133
|
drag.current.active = false
|
|
117
|
-
if (!startedOnCanvas || event
|
|
134
|
+
if (!startedOnCanvas || !isWalkButton(event) || drag.current.moved) return
|
|
118
135
|
|
|
119
|
-
if (event
|
|
136
|
+
if (isWalkClick(event) && landAtPointer(event.clientX, event.clientY, true)) {
|
|
137
|
+
return
|
|
138
|
+
}
|
|
120
139
|
|
|
121
140
|
const pick = pickAt(event.clientX, event.clientY)
|
|
122
141
|
if (!pick) return
|
|
@@ -149,14 +168,20 @@ export function MapView({
|
|
|
149
168
|
onSelectFolder(null)
|
|
150
169
|
}
|
|
151
170
|
|
|
171
|
+
const onContextMenu = (event: MouseEvent) => {
|
|
172
|
+
if (event.ctrlKey) event.preventDefault()
|
|
173
|
+
}
|
|
174
|
+
|
|
152
175
|
element.addEventListener('pointerdown', onDown)
|
|
153
176
|
window.addEventListener('pointermove', onMove)
|
|
154
177
|
window.addEventListener('pointerup', onUp)
|
|
178
|
+
element.addEventListener('contextmenu', onContextMenu)
|
|
155
179
|
return () => {
|
|
156
180
|
element.style.cursor = ''
|
|
157
181
|
element.removeEventListener('pointerdown', onDown)
|
|
158
182
|
window.removeEventListener('pointermove', onMove)
|
|
159
183
|
window.removeEventListener('pointerup', onUp)
|
|
184
|
+
element.removeEventListener('contextmenu', onContextMenu)
|
|
160
185
|
}
|
|
161
186
|
}, [
|
|
162
187
|
camera,
|
|
@@ -8,8 +8,8 @@ import { SelectionController } from './SelectionController'
|
|
|
8
8
|
import { UserContextTracker } from './UserContextTracker'
|
|
9
9
|
import { BlockPlacer } from './BlockPlacer'
|
|
10
10
|
import { IslandPlacer } from './IslandPlacer'
|
|
11
|
-
import { folderOfFile, filesImporting
|
|
12
|
-
import {
|
|
11
|
+
import { folderOfFile, filesImporting } from '../layout'
|
|
12
|
+
import { WORLD_VOID } from '../theme'
|
|
13
13
|
import type { ChangeKind } from '../theme'
|
|
14
14
|
import type {
|
|
15
15
|
CodebaseGraph,
|
|
@@ -159,28 +159,16 @@ export function World({
|
|
|
159
159
|
return null
|
|
160
160
|
}
|
|
161
161
|
|
|
162
|
-
const bounds = worldBounds(layout)
|
|
163
|
-
const groundPad = 120
|
|
164
|
-
const groundWidth = Math.max(bounds.width + groundPad * 2, 400)
|
|
165
|
-
const groundDepth = Math.max(bounds.depth + groundPad * 2, 400)
|
|
166
|
-
|
|
167
162
|
return (
|
|
168
163
|
<>
|
|
169
|
-
<color attach="background" args={[
|
|
170
|
-
{!mapping && <fog attach="fog" args={[
|
|
164
|
+
<color attach="background" args={[WORLD_VOID]} />
|
|
165
|
+
{!mapping && <fog attach="fog" args={[WORLD_VOID, 38, 160]} />}
|
|
171
166
|
<hemisphereLight args={['#d7e2ee', '#2a3038', mapping ? 1.1 : 0.85]} />
|
|
172
167
|
<directionalLight
|
|
173
168
|
position={mapping ? [8, 60, 8] : [12, 22, 8]}
|
|
174
169
|
intensity={mapping ? 1.35 : 0.55}
|
|
175
170
|
/>
|
|
176
171
|
<ambientLight intensity={mapping ? 0.7 : 0.42} />
|
|
177
|
-
<mesh
|
|
178
|
-
rotation={[-Math.PI / 2, 0, 0]}
|
|
179
|
-
position={[bounds.cx, -0.06, bounds.cz]}
|
|
180
|
-
>
|
|
181
|
-
<planeGeometry args={[groundWidth, groundDepth]} />
|
|
182
|
-
<meshBasicMaterial color={EDITOR_GREY.chrome} />
|
|
183
|
-
</mesh>
|
|
184
172
|
<MapView
|
|
185
173
|
layout={layout}
|
|
186
174
|
enabled={mapping}
|
|
@@ -25,6 +25,9 @@ export const EDITOR_GREY = {
|
|
|
25
25
|
surface: '#272c36',
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
/** Infinite map/walk backdrop. Keep distinct from chrome HUD greys. */
|
|
29
|
+
export const WORLD_VOID = '#000000'
|
|
30
|
+
|
|
28
31
|
export type ChangeKind = 'add' | 'edit' | 'remove'
|
|
29
32
|
|
|
30
33
|
export const CHANGE_HIGHLIGHT: Record<
|
|
@@ -128,6 +128,7 @@ type HUDProps = {
|
|
|
128
128
|
mode: ViewMode
|
|
129
129
|
locked: boolean
|
|
130
130
|
selectedId: string | null
|
|
131
|
+
selectedTick?: number
|
|
131
132
|
selectedFolder?: string | null
|
|
132
133
|
aimedRelation: AimedRelation | null
|
|
133
134
|
currentFolder: string
|
|
@@ -162,6 +163,7 @@ type HUDProps = {
|
|
|
162
163
|
) => void
|
|
163
164
|
onMapAddFile?: (folderPath: string) => void
|
|
164
165
|
onMapAddFolder?: (folderPath: string) => void
|
|
166
|
+
onInspectFile?: (fileId: string) => void
|
|
165
167
|
}
|
|
166
168
|
|
|
167
169
|
export function HUD({
|
|
@@ -169,6 +171,7 @@ export function HUD({
|
|
|
169
171
|
mode,
|
|
170
172
|
locked,
|
|
171
173
|
selectedId,
|
|
174
|
+
selectedTick = 0,
|
|
172
175
|
selectedFolder = null,
|
|
173
176
|
aimedRelation,
|
|
174
177
|
currentFolder,
|
|
@@ -196,6 +199,7 @@ export function HUD({
|
|
|
196
199
|
onRemoveBlueprintImport,
|
|
197
200
|
onMapAddFile,
|
|
198
201
|
onMapAddFolder,
|
|
202
|
+
onInspectFile,
|
|
199
203
|
}: HUDProps) {
|
|
200
204
|
const selected = graph.files.find((file) => file.id === selectedId)
|
|
201
205
|
const selectedFolderNode = graph.folders.find(
|
|
@@ -299,6 +303,11 @@ export function HUD({
|
|
|
299
303
|
const canRunNext =
|
|
300
304
|
Boolean(nextStep) && (planReady || pending) && !working
|
|
301
305
|
const canComplete = pending && lastStep
|
|
306
|
+
const canInspectFile = (fileId: string, userCreated = false) =>
|
|
307
|
+
Boolean(onInspectFile) &&
|
|
308
|
+
!fileId.startsWith('draft:') &&
|
|
309
|
+
!(previewing && (intent.deletes ?? []).includes(fileId)) &&
|
|
310
|
+
(!userCreated || (intent.creates ?? []).includes(fileId))
|
|
302
311
|
const panelDone =
|
|
303
312
|
intent.status === 'finished' ||
|
|
304
313
|
intent.status === 'approved' ||
|
|
@@ -332,8 +341,8 @@ export function HUD({
|
|
|
332
341
|
}, [selectedId, selectedFolder])
|
|
333
342
|
|
|
334
343
|
useEffect(() => {
|
|
335
|
-
if (
|
|
336
|
-
}, [selectedId,
|
|
344
|
+
if (selectedId) setInfoVisible(true)
|
|
345
|
+
}, [selectedId, selectedTick])
|
|
337
346
|
|
|
338
347
|
useEffect(() => {
|
|
339
348
|
if (selectedFolder) setInfoVisible(true)
|
|
@@ -402,7 +411,7 @@ export function HUD({
|
|
|
402
411
|
, <kbd>Space</kbd> place a file, <kbd>B</kbd> place an island
|
|
403
412
|
</>
|
|
404
413
|
) : null}
|
|
405
|
-
, click a block for
|
|
414
|
+
, click a block for info.
|
|
406
415
|
</p>
|
|
407
416
|
</div>
|
|
408
417
|
</div>
|
|
@@ -757,6 +766,15 @@ export function HUD({
|
|
|
757
766
|
<p>
|
|
758
767
|
{selected.lines} lines · {selected.language}
|
|
759
768
|
</p>
|
|
769
|
+
{canInspectFile(selected.id, selected.userCreated) && (
|
|
770
|
+
<button
|
|
771
|
+
className="hud-button hud-inspect"
|
|
772
|
+
type="button"
|
|
773
|
+
onClick={() => onInspectFile?.(selected.id)}
|
|
774
|
+
>
|
|
775
|
+
Inspect file
|
|
776
|
+
</button>
|
|
777
|
+
)}
|
|
760
778
|
{selectedClasses.length > 0 && (
|
|
761
779
|
<>
|
|
762
780
|
<div className="hud-section-title">Classes</div>
|
|
@@ -955,7 +973,18 @@ export function HUD({
|
|
|
955
973
|
) : (
|
|
956
974
|
<ul>
|
|
957
975
|
{folderFiles.map((file) => (
|
|
958
|
-
<li key={file.id}>
|
|
976
|
+
<li key={file.id}>
|
|
977
|
+
<span>{file.path || file.id}</span>
|
|
978
|
+
{canInspectFile(file.id, file.userCreated) && (
|
|
979
|
+
<button
|
|
980
|
+
className="hud-item-inspect"
|
|
981
|
+
type="button"
|
|
982
|
+
onClick={() => onInspectFile?.(file.id)}
|
|
983
|
+
>
|
|
984
|
+
Inspect
|
|
985
|
+
</button>
|
|
986
|
+
)}
|
|
987
|
+
</li>
|
|
959
988
|
))}
|
|
960
989
|
</ul>
|
|
961
990
|
)}
|
|
@@ -988,11 +1017,7 @@ export function HUD({
|
|
|
988
1017
|
<>
|
|
989
1018
|
<span>Scroll zoom</span>
|
|
990
1019
|
<span>Drag pan</span>
|
|
991
|
-
<span>
|
|
992
|
-
{sessionMode
|
|
993
|
-
? 'Click a block for info'
|
|
994
|
-
: 'Click a block for relations'}
|
|
995
|
-
</span>
|
|
1020
|
+
<span>Click a block for info</span>
|
|
996
1021
|
<span>Click an island for its files</span>
|
|
997
1022
|
{creatingBlueprint && (
|
|
998
1023
|
<>
|
|
@@ -1001,19 +1026,11 @@ export function HUD({
|
|
|
1001
1026
|
</>
|
|
1002
1027
|
)}
|
|
1003
1028
|
<span>Click a line to fly there</span>
|
|
1004
|
-
<span>
|
|
1029
|
+
<span>Ctrl-click an island to walk</span>
|
|
1005
1030
|
{selected?.userCreated && creatingBlueprint && (
|
|
1006
1031
|
<span>Backspace delete</span>
|
|
1007
1032
|
)}
|
|
1008
|
-
<span>
|
|
1009
|
-
{sessionMode
|
|
1010
|
-
? infoVisible
|
|
1011
|
-
? 'I hide info'
|
|
1012
|
-
: 'Click a block for info'
|
|
1013
|
-
: infoVisible
|
|
1014
|
-
? 'I hide info'
|
|
1015
|
-
: 'I show info'}
|
|
1016
|
-
</span>
|
|
1033
|
+
<span>{infoVisible ? 'I hide info' : 'I show info'}</span>
|
|
1017
1034
|
{infoVisible && <span>↑↓ scroll info</span>}
|
|
1018
1035
|
<span>
|
|
1019
1036
|
{importedBy ? 'K show imports' : 'K show imported by'}
|
|
@@ -1034,21 +1051,9 @@ export function HUD({
|
|
|
1034
1051
|
{selected?.userCreated && creatingBlueprint && (
|
|
1035
1052
|
<span>Backspace delete</span>
|
|
1036
1053
|
)}
|
|
1037
|
-
<span>
|
|
1038
|
-
{sessionMode
|
|
1039
|
-
? 'Click a block for info'
|
|
1040
|
-
: 'Click block for relations'}
|
|
1041
|
-
</span>
|
|
1054
|
+
<span>Click a block for info</span>
|
|
1042
1055
|
<span>Aim a line to fly</span>
|
|
1043
|
-
<span>
|
|
1044
|
-
{sessionMode
|
|
1045
|
-
? infoVisible
|
|
1046
|
-
? 'I hide info'
|
|
1047
|
-
: 'Click a block for info'
|
|
1048
|
-
: infoVisible
|
|
1049
|
-
? 'I hide info'
|
|
1050
|
-
: 'I show info'}
|
|
1051
|
-
</span>
|
|
1056
|
+
<span>{infoVisible ? 'I hide info' : 'I show info'}</span>
|
|
1052
1057
|
{infoVisible && <span>↑↓ scroll info</span>}
|
|
1053
1058
|
<span>
|
|
1054
1059
|
{importedBy ? 'K show imports' : 'K show imported by'}
|
|
@@ -7,9 +7,11 @@ import { fileURLToPath } from 'node:url'
|
|
|
7
7
|
import type { IncomingMessage, ServerResponse } from 'node:http'
|
|
8
8
|
import { emptyIntent } from './scripts/patch-lib.mjs'
|
|
9
9
|
import { dataDir, targetRoot } from './scripts/target-config.mjs'
|
|
10
|
+
import { editorFileUri, openInEditor } from './scripts/open-editor.mjs'
|
|
10
11
|
import {
|
|
11
12
|
answerBlueprint,
|
|
12
13
|
continueDiff,
|
|
14
|
+
inspectTargetFile,
|
|
13
15
|
invokeStep,
|
|
14
16
|
readActiveSession,
|
|
15
17
|
requestReplan,
|
|
@@ -105,10 +107,46 @@ function jsonFilePlugin(): Plugin {
|
|
|
105
107
|
|
|
106
108
|
next()
|
|
107
109
|
})
|
|
110
|
+
|
|
111
|
+
server.middlewares.use('/api/inspect-file', (req, res, next) => {
|
|
112
|
+
if (req.method === 'POST') {
|
|
113
|
+
void inspectFile(req, res)
|
|
114
|
+
return
|
|
115
|
+
}
|
|
116
|
+
next()
|
|
117
|
+
})
|
|
108
118
|
},
|
|
109
119
|
}
|
|
110
120
|
}
|
|
111
121
|
|
|
122
|
+
async function inspectFile(req: IncomingMessage, res: ServerResponse) {
|
|
123
|
+
try {
|
|
124
|
+
const body = JSON.parse(await readBody(req)) as {
|
|
125
|
+
sessionId?: string
|
|
126
|
+
diffId?: string
|
|
127
|
+
fileId?: string
|
|
128
|
+
}
|
|
129
|
+
const filePath = inspectTargetFile(dataDir, targetRoot, {
|
|
130
|
+
sessionId: body.sessionId,
|
|
131
|
+
diffId: body.diffId,
|
|
132
|
+
fileId: body.fileId,
|
|
133
|
+
})
|
|
134
|
+
if (!filePath) {
|
|
135
|
+
sendJson(res, 200, { path: null, uri: null, opened: false })
|
|
136
|
+
return
|
|
137
|
+
}
|
|
138
|
+
const opened = openInEditor(filePath)
|
|
139
|
+
sendJson(res, 200, {
|
|
140
|
+
path: filePath,
|
|
141
|
+
uri: editorFileUri(filePath),
|
|
142
|
+
opened,
|
|
143
|
+
})
|
|
144
|
+
} catch (error) {
|
|
145
|
+
const message = error instanceof Error ? error.message : 'invalid request'
|
|
146
|
+
sendJson(res, 400, { error: message })
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
112
150
|
async function decideIntent(req: IncomingMessage, res: ServerResponse) {
|
|
113
151
|
try {
|
|
114
152
|
const body = JSON.parse(await readBody(req)) as {
|
|
@@ -209,7 +247,7 @@ async function decideIntent(req: IncomingMessage, res: ServerResponse) {
|
|
|
209
247
|
addedImports: body.addedImports,
|
|
210
248
|
})
|
|
211
249
|
} else {
|
|
212
|
-
stopSession(dataDir, body.sessionId,
|
|
250
|
+
stopSession(dataDir, body.sessionId, targetRoot)
|
|
213
251
|
}
|
|
214
252
|
const next = sessionIntent(dataDir, body.sessionId, knownFileIds())
|
|
215
253
|
sendJson(res, 200, next ?? { ...emptyIntent })
|
package/bin/session.mjs
CHANGED
|
@@ -201,7 +201,7 @@ export async function proposePatch(args) {
|
|
|
201
201
|
|
|
202
202
|
if (clear) {
|
|
203
203
|
if (!sessionId) usage('propose-patch', '--session <cursor-chat-id> --clear')
|
|
204
|
-
store.stopSession(config.dataDir, sessionId)
|
|
204
|
+
store.stopSession(config.dataDir, sessionId, config.targetRoot)
|
|
205
205
|
console.log(
|
|
206
206
|
`Cleared session ${sessionId}; stored diffs and blueprint drafts were removed.`,
|
|
207
207
|
)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jkwd/inbase",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "A first-person 3D map of a codebase, with a visual coding workflow for Cursor.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -29,6 +29,8 @@
|
|
|
29
29
|
"apps/explorer/src/scene",
|
|
30
30
|
"apps/explorer/src/ui",
|
|
31
31
|
"apps/explorer/scripts/js-source.mjs",
|
|
32
|
+
"apps/explorer/scripts/open-editor.d.ts",
|
|
33
|
+
"apps/explorer/scripts/open-editor.mjs",
|
|
32
34
|
"apps/explorer/scripts/patch-lib.d.ts",
|
|
33
35
|
"apps/explorer/scripts/patch-lib.mjs",
|
|
34
36
|
"apps/explorer/scripts/scan-target.mjs",
|