@jkwd/inbase 0.1.1 → 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 +81 -8
- package/apps/explorer/src/scene/FileBlock.tsx +1 -1
- package/apps/explorer/src/scene/FolderArea.tsx +5 -2
- package/apps/explorer/src/scene/MapView.tsx +61 -29
- package/apps/explorer/src/scene/World.tsx +4 -7
- package/apps/explorer/src/theme.ts +3 -0
- package/apps/explorer/src/ui/HUD.tsx +144 -109
- 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
|
+
}
|