@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.
@@ -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
- diffId?: string,
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 withVirtualTarget(targetRoot, patches, action) {
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 = unresolvedEntries(manifest).map((entry) =>
321
- readDiff(dataDir, manifest.sessionId, entry),
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
- withVirtualTarget(targetRoot, [...prior, patchText], () => undefined)
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
- const patches = unresolved.map((entry) =>
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, _diffId) {
667
- finalizeFinishedSession(dataDir, sessionId)
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, diffId)
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) setSelectedFolder(null)
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
+ }