@jkwd/inbase 0.1.4 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -1
- package/apps/explorer/scripts/session-store.d.ts +20 -0
- package/apps/explorer/scripts/session-store.mjs +318 -32
- package/apps/explorer/src/App.tsx +154 -85
- package/apps/explorer/src/agentIntent.ts +33 -1
- package/apps/explorer/src/index.css +217 -2
- package/apps/explorer/src/layout.ts +46 -0
- package/apps/explorer/src/scene/FileBlock.tsx +110 -145
- package/apps/explorer/src/scene/FolderArea.tsx +17 -4
- package/apps/explorer/src/scene/MapSelectBorder.tsx +3 -1
- package/apps/explorer/src/scene/MapView.tsx +10 -12
- package/apps/explorer/src/scene/Player.tsx +27 -0
- package/apps/explorer/src/scene/RelationLines.tsx +36 -32
- package/apps/explorer/src/scene/SelectionController.tsx +21 -3
- package/apps/explorer/src/scene/SelectionThumbnail.tsx +451 -0
- package/apps/explorer/src/scene/World.tsx +16 -35
- package/apps/explorer/src/theme.ts +7 -1
- package/apps/explorer/src/types.ts +12 -0
- package/apps/explorer/src/ui/HUD.tsx +662 -358
- package/apps/explorer/vite.config.ts +33 -21
- package/bin/session.mjs +16 -2
- package/package.json +1 -1
- package/skill/inbase/SKILL.md +22 -14
package/README.md
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="docs/inbase-logo.png" alt="InBase — Dive into your codebase" width="520" />
|
|
3
|
+
</p>
|
|
2
4
|
|
|
3
5
|
A first-person 3D map of a JavaScript or TypeScript codebase. Files become blocks, folders become walkable areas, and imports become lines in the air.
|
|
4
6
|
|
|
@@ -45,10 +45,30 @@ export type DiffManifest = {
|
|
|
45
45
|
export function assertSessionId(value: unknown): string
|
|
46
46
|
export function readActiveSession(dataDir: string): string | null
|
|
47
47
|
export function writeActiveSession(dataDir: string, sessionId: string | null): void
|
|
48
|
+
export function touchSessionConnection(dataDir: string, sessionId: string): void
|
|
49
|
+
export function isSessionConnected(
|
|
50
|
+
dataDir: string,
|
|
51
|
+
sessionId: string,
|
|
52
|
+
waiterIds?: Set<string>,
|
|
53
|
+
): boolean
|
|
54
|
+
export function listStoredSessionIds(dataDir: string): string[]
|
|
55
|
+
export function listOpenSessionIds(dataDir: string): string[]
|
|
56
|
+
export function discardInactiveDiffSessions(
|
|
57
|
+
dataDir: string,
|
|
58
|
+
targetRoot?: string | null,
|
|
59
|
+
waiterIds?: Iterable<string>,
|
|
60
|
+
): string[]
|
|
61
|
+
export function listSessionIntents(
|
|
62
|
+
dataDir: string,
|
|
63
|
+
knownFileIds?: string[],
|
|
64
|
+
): Array<Record<string, unknown>>
|
|
48
65
|
export function readBlueprintSession(dataDir: string): string | null
|
|
49
66
|
export function writeBlueprintSession(dataDir: string, sessionId: string | null): void
|
|
50
67
|
export function readManifest(dataDir: string, sessionId: string): DiffManifest | null
|
|
51
68
|
export function writeManifest(dataDir: string, manifest: DiffManifest): void
|
|
69
|
+
export function isSessionStopped(dataDir: string, sessionId: string): boolean
|
|
70
|
+
export function isWorkflowStopped(dataDir: string, sessionId: string): boolean
|
|
71
|
+
export function sessionStoppedError(sessionId: string): Error
|
|
52
72
|
export function startSession(
|
|
53
73
|
dataDir: string,
|
|
54
74
|
input: {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs'
|
|
2
2
|
import os from 'node:os'
|
|
3
3
|
import path from 'node:path'
|
|
4
|
+
import { spawnSync } from 'node:child_process'
|
|
4
5
|
import {
|
|
5
6
|
accumulatePatchAdditions,
|
|
6
7
|
applyUnifiedPatch,
|
|
@@ -11,6 +12,7 @@ import {
|
|
|
11
12
|
} from './patch-lib.mjs'
|
|
12
13
|
|
|
13
14
|
const SESSION_ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/
|
|
15
|
+
const CONNECTED_TTL_MS = 15_000
|
|
14
16
|
|
|
15
17
|
export function assertSessionId(value) {
|
|
16
18
|
if (typeof value !== 'string' || !SESSION_ID.test(value) || value === '.' || value === '..') {
|
|
@@ -46,9 +48,48 @@ export function sessionPaths(dataDir, sessionId) {
|
|
|
46
48
|
blueprint: path.join(root, 'blueprint.json'),
|
|
47
49
|
baseline: path.join(root, 'baseline.json'),
|
|
48
50
|
baselineFiles: path.join(root, 'baseline'),
|
|
51
|
+
stopped: path.join(dataDir, 'diff-sessions', `${safeId}.stopped`),
|
|
49
52
|
}
|
|
50
53
|
}
|
|
51
54
|
|
|
55
|
+
export function sessionStoppedError(sessionId) {
|
|
56
|
+
return new Error(
|
|
57
|
+
`VISUAL_CODER_STOPPED Session ${assertSessionId(sessionId)} was stopped. Do not modify project files.`,
|
|
58
|
+
)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function isSessionStopped(dataDir, sessionId) {
|
|
62
|
+
return fs.existsSync(sessionPaths(dataDir, sessionId).stopped)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function isWorkflowStopped(dataDir, sessionId) {
|
|
66
|
+
const safeId = assertSessionId(sessionId)
|
|
67
|
+
const manifest = readManifest(dataDir, safeId)
|
|
68
|
+
if (manifest?.phase === 'stopped') return true
|
|
69
|
+
return !manifest && isSessionStopped(dataDir, safeId)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function writeStoppedMarker(dataDir, sessionId) {
|
|
73
|
+
const { stopped } = sessionPaths(dataDir, sessionId)
|
|
74
|
+
atomicWrite(
|
|
75
|
+
stopped,
|
|
76
|
+
`${JSON.stringify({ sessionId: assertSessionId(sessionId), stoppedAt: new Date().toISOString() }, null, 2)}\n`,
|
|
77
|
+
)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function clearStoppedMarker(dataDir, sessionId) {
|
|
81
|
+
const { stopped } = sessionPaths(dataDir, sessionId)
|
|
82
|
+
if (fs.existsSync(stopped)) fs.unlinkSync(stopped)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function requireManifest(dataDir, sessionId, missingMessage) {
|
|
86
|
+
const safeId = assertSessionId(sessionId)
|
|
87
|
+
const manifest = readManifest(dataDir, safeId)
|
|
88
|
+
if (manifest) return manifest
|
|
89
|
+
if (isSessionStopped(dataDir, safeId)) throw sessionStoppedError(safeId)
|
|
90
|
+
throw new Error(missingMessage ?? `Unknown session ${safeId}`)
|
|
91
|
+
}
|
|
92
|
+
|
|
52
93
|
export function resolveTargetFile(targetRoot, fileId) {
|
|
53
94
|
if (typeof fileId !== 'string' || fileId.trim() === '') {
|
|
54
95
|
throw new Error('fileId is required')
|
|
@@ -78,6 +119,138 @@ export function writeActiveSession(dataDir, sessionId) {
|
|
|
78
119
|
)
|
|
79
120
|
}
|
|
80
121
|
|
|
122
|
+
function connectionFile(dataDir, sessionId) {
|
|
123
|
+
return path.join(sessionPaths(dataDir, sessionId).root, 'connected.json')
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function isFreshTimestamp(value, now = Date.now()) {
|
|
127
|
+
if (typeof value !== 'string') return false
|
|
128
|
+
const at = Date.parse(value)
|
|
129
|
+
return Number.isFinite(at) && now - at >= 0 && now - at < CONNECTED_TTL_MS
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function touchSessionConnection(dataDir, sessionId) {
|
|
133
|
+
const safeId = assertSessionId(sessionId)
|
|
134
|
+
if (isSessionStopped(dataDir, safeId)) return
|
|
135
|
+
atomicWrite(
|
|
136
|
+
connectionFile(dataDir, safeId),
|
|
137
|
+
`${JSON.stringify({ sessionId: safeId, connectedAt: new Date().toISOString() }, null, 2)}\n`,
|
|
138
|
+
)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function waiterSessionIds() {
|
|
142
|
+
try {
|
|
143
|
+
const result = spawnSync('ps', ['-ax', '-o', 'command='], {
|
|
144
|
+
encoding: 'utf8',
|
|
145
|
+
})
|
|
146
|
+
if (result.status !== 0 || !result.stdout) return new Set()
|
|
147
|
+
const ids = new Set()
|
|
148
|
+
for (const line of result.stdout.split('\n')) {
|
|
149
|
+
if (
|
|
150
|
+
!line.includes('wait-for-blueprint') &&
|
|
151
|
+
!line.includes('wait-for-approval')
|
|
152
|
+
) {
|
|
153
|
+
continue
|
|
154
|
+
}
|
|
155
|
+
const match = line.match(/--session\s+(\S+)/)
|
|
156
|
+
if (!match) continue
|
|
157
|
+
try {
|
|
158
|
+
ids.add(assertSessionId(match[1]))
|
|
159
|
+
} catch {
|
|
160
|
+
// Ignore process command lines with invalid session ids.
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return ids
|
|
164
|
+
} catch {
|
|
165
|
+
return new Set()
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function isGeneratingPhase(phase) {
|
|
170
|
+
return phase === 'preparing' || phase === 'working' || phase === 'replanning'
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function isSessionConnected(
|
|
174
|
+
dataDir,
|
|
175
|
+
sessionId,
|
|
176
|
+
waiterIds = waiterSessionIds(),
|
|
177
|
+
) {
|
|
178
|
+
const safeId = assertSessionId(sessionId)
|
|
179
|
+
const manifest = readManifest(dataDir, safeId)
|
|
180
|
+
if (!manifest) return false
|
|
181
|
+
if (
|
|
182
|
+
manifest.phase === 'finished' ||
|
|
183
|
+
manifest.phase === 'stopped' ||
|
|
184
|
+
manifest.status === 'finished' ||
|
|
185
|
+
manifest.status === 'rejected'
|
|
186
|
+
) {
|
|
187
|
+
return false
|
|
188
|
+
}
|
|
189
|
+
if (isGeneratingPhase(manifest.phase)) return true
|
|
190
|
+
if (waiterIds.has(safeId)) return true
|
|
191
|
+
const connected = readJson(connectionFile(dataDir, safeId), null)
|
|
192
|
+
if (isFreshTimestamp(connected?.connectedAt)) return true
|
|
193
|
+
return isFreshTimestamp(manifest.updatedAt) || isFreshTimestamp(manifest.createdAt)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function diffSessionsRoot(dataDir) {
|
|
197
|
+
return path.join(dataDir, 'diff-sessions')
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export function listStoredSessionIds(dataDir) {
|
|
201
|
+
const root = diffSessionsRoot(dataDir)
|
|
202
|
+
if (!fs.existsSync(root)) return []
|
|
203
|
+
const ids = new Set()
|
|
204
|
+
for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
|
|
205
|
+
if (entry.name === '.gitkeep') continue
|
|
206
|
+
const name =
|
|
207
|
+
entry.isFile() && entry.name.endsWith('.stopped')
|
|
208
|
+
? entry.name.slice(0, -'.stopped'.length)
|
|
209
|
+
: entry.name
|
|
210
|
+
try {
|
|
211
|
+
ids.add(assertSessionId(name))
|
|
212
|
+
} catch {
|
|
213
|
+
// Skip files that are not valid session ids.
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return [...ids]
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export function listOpenSessionIds(dataDir) {
|
|
220
|
+
const root = diffSessionsRoot(dataDir)
|
|
221
|
+
if (!fs.existsSync(root)) return []
|
|
222
|
+
const waiters = waiterSessionIds()
|
|
223
|
+
const sessions = []
|
|
224
|
+
for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
|
|
225
|
+
if (!entry.isDirectory()) continue
|
|
226
|
+
try {
|
|
227
|
+
const sessionId = assertSessionId(entry.name)
|
|
228
|
+
if (!isSessionConnected(dataDir, sessionId, waiters)) continue
|
|
229
|
+
const manifest = readManifest(dataDir, sessionId)
|
|
230
|
+
if (!manifest) continue
|
|
231
|
+
sessions.push({
|
|
232
|
+
sessionId,
|
|
233
|
+
createdAt: typeof manifest.createdAt === 'string' ? manifest.createdAt : '',
|
|
234
|
+
})
|
|
235
|
+
} catch {
|
|
236
|
+
// Skip folders that are not valid session ids.
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
sessions.sort((left, right) => {
|
|
240
|
+
if (left.createdAt !== right.createdAt) {
|
|
241
|
+
return left.createdAt.localeCompare(right.createdAt)
|
|
242
|
+
}
|
|
243
|
+
return left.sessionId.localeCompare(right.sessionId)
|
|
244
|
+
})
|
|
245
|
+
return sessions.map((item) => item.sessionId)
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export function listSessionIntents(dataDir, knownFileIds = []) {
|
|
249
|
+
return listOpenSessionIds(dataDir)
|
|
250
|
+
.map((sessionId) => sessionIntent(dataDir, sessionId, knownFileIds))
|
|
251
|
+
.filter(Boolean)
|
|
252
|
+
}
|
|
253
|
+
|
|
81
254
|
export function readBlueprintSession(dataDir) {
|
|
82
255
|
const value = readJson(path.join(dataDir, 'blueprint-session.json'), null)
|
|
83
256
|
return value?.sessionId ? assertSessionId(value.sessionId) : null
|
|
@@ -400,8 +573,51 @@ function replayPatches(dataDir, sessionId, targetRoot, entries) {
|
|
|
400
573
|
}
|
|
401
574
|
}
|
|
402
575
|
|
|
403
|
-
function
|
|
404
|
-
|
|
576
|
+
function gitTopLevel(fromDir) {
|
|
577
|
+
try {
|
|
578
|
+
const result = spawnSync('git', ['rev-parse', '--show-toplevel'], {
|
|
579
|
+
cwd: fromDir,
|
|
580
|
+
encoding: 'utf8',
|
|
581
|
+
})
|
|
582
|
+
if (result.status !== 0) return null
|
|
583
|
+
const root = result.stdout.trim()
|
|
584
|
+
return root ? fs.realpathSync(root) : null
|
|
585
|
+
} catch {
|
|
586
|
+
return null
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
function repoRelativePath(root, absolutePath) {
|
|
591
|
+
const resolved = path.resolve(absolutePath)
|
|
592
|
+
let candidate = resolved
|
|
593
|
+
try {
|
|
594
|
+
if (fs.existsSync(resolved)) candidate = fs.realpathSync(resolved)
|
|
595
|
+
else if (fs.existsSync(path.dirname(resolved))) {
|
|
596
|
+
candidate = path.join(fs.realpathSync(path.dirname(resolved)), path.basename(resolved))
|
|
597
|
+
}
|
|
598
|
+
} catch {
|
|
599
|
+
candidate = resolved
|
|
600
|
+
}
|
|
601
|
+
const relative = path.relative(root, candidate)
|
|
602
|
+
if (!relative || relative.startsWith('..') || path.isAbsolute(relative)) return null
|
|
603
|
+
return relative
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
function unstagePaths(fromDir, absolutePaths) {
|
|
607
|
+
if (!absolutePaths.length) return
|
|
608
|
+
const root = gitTopLevel(fromDir)
|
|
609
|
+
if (!root) return
|
|
610
|
+
const relative = [...new Set(absolutePaths)]
|
|
611
|
+
.map((item) => repoRelativePath(root, item))
|
|
612
|
+
.filter((item) => Boolean(item))
|
|
613
|
+
if (!relative.length) return
|
|
614
|
+
for (const item of relative) {
|
|
615
|
+
spawnSync('git', ['restore', '--staged', '--', item], {
|
|
616
|
+
cwd: root,
|
|
617
|
+
encoding: 'utf8',
|
|
618
|
+
stdio: 'ignore',
|
|
619
|
+
})
|
|
620
|
+
}
|
|
405
621
|
}
|
|
406
622
|
|
|
407
623
|
function liveEntries(manifest, diffId) {
|
|
@@ -409,8 +625,7 @@ function liveEntries(manifest, diffId) {
|
|
|
409
625
|
}
|
|
410
626
|
|
|
411
627
|
export function materializeDiff(dataDir, targetRoot, sessionId, diffId) {
|
|
412
|
-
const manifest =
|
|
413
|
-
if (!manifest) throw new Error(`Unknown session ${sessionId}`)
|
|
628
|
+
const manifest = requireManifest(dataDir, sessionId)
|
|
414
629
|
const through = diffId || manifest.activeDiffId
|
|
415
630
|
if (!through) return manifest
|
|
416
631
|
restoreBaseline(dataDir, sessionId, targetRoot)
|
|
@@ -473,6 +688,7 @@ function featureName(value) {
|
|
|
473
688
|
|
|
474
689
|
export function startSession(dataDir, input) {
|
|
475
690
|
const sessionId = assertSessionId(input.sessionId)
|
|
691
|
+
clearStoppedMarker(dataDir, sessionId)
|
|
476
692
|
const existing = readManifest(dataDir, sessionId)
|
|
477
693
|
if (existing) {
|
|
478
694
|
focusSession(dataDir, sessionId)
|
|
@@ -502,8 +718,7 @@ export function startSession(dataDir, input) {
|
|
|
502
718
|
}
|
|
503
719
|
|
|
504
720
|
export function answerBlueprint(dataDir, sessionId, enabled) {
|
|
505
|
-
const manifest =
|
|
506
|
-
if (!manifest) throw new Error(`Unknown session ${sessionId}`)
|
|
721
|
+
const manifest = requireManifest(dataDir, sessionId)
|
|
507
722
|
if (manifest.phase !== 'blueprint_ask') {
|
|
508
723
|
throw new Error(`Session ${sessionId} is not asking for a blueprint`)
|
|
509
724
|
}
|
|
@@ -522,8 +737,7 @@ export function answerBlueprint(dataDir, sessionId, enabled) {
|
|
|
522
737
|
|
|
523
738
|
export function updateBlueprint(dataDir, sessionId, input = {}) {
|
|
524
739
|
const safeId = assertSessionId(sessionId)
|
|
525
|
-
const manifest =
|
|
526
|
-
if (!manifest) throw new Error(`Unknown session ${safeId}`)
|
|
740
|
+
const manifest = requireManifest(dataDir, safeId)
|
|
527
741
|
if (manifest.phase !== 'blueprint') {
|
|
528
742
|
throw new Error(`Session ${safeId} is not in blueprint mode`)
|
|
529
743
|
}
|
|
@@ -544,8 +758,7 @@ export function updateBlueprint(dataDir, sessionId, input = {}) {
|
|
|
544
758
|
|
|
545
759
|
export function sendBlueprint(dataDir, sessionId, input = {}) {
|
|
546
760
|
const safeId = assertSessionId(sessionId)
|
|
547
|
-
const manifest =
|
|
548
|
-
if (!manifest) throw new Error(`Unknown session ${safeId}`)
|
|
761
|
+
const manifest = requireManifest(dataDir, safeId)
|
|
549
762
|
if (manifest.phase !== 'blueprint') {
|
|
550
763
|
throw new Error(`Session ${safeId} is not in blueprint mode`)
|
|
551
764
|
}
|
|
@@ -570,6 +783,9 @@ export function sendBlueprint(dataDir, sessionId, input = {}) {
|
|
|
570
783
|
export function reportPlan(dataDir, input) {
|
|
571
784
|
const sessionId = assertSessionId(input.sessionId)
|
|
572
785
|
const existing = readManifest(dataDir, sessionId)
|
|
786
|
+
if (!existing && isSessionStopped(dataDir, sessionId)) {
|
|
787
|
+
throw sessionStoppedError(sessionId)
|
|
788
|
+
}
|
|
573
789
|
const now = new Date().toISOString()
|
|
574
790
|
|
|
575
791
|
if (existing?.phase === 'blueprint_ask' || existing?.phase === 'blueprint') {
|
|
@@ -623,8 +839,7 @@ export function reportPlan(dataDir, input) {
|
|
|
623
839
|
}
|
|
624
840
|
|
|
625
841
|
export function invokeStep(dataDir, sessionId, step, targetRoot = null) {
|
|
626
|
-
const manifest =
|
|
627
|
-
if (!manifest) throw new Error(`Unknown session ${sessionId}`)
|
|
842
|
+
const manifest = requireManifest(dataDir, sessionId)
|
|
628
843
|
|
|
629
844
|
if (manifest.phase === 'review') {
|
|
630
845
|
if (!targetRoot) throw new Error('A target root is required to apply the current step')
|
|
@@ -665,8 +880,11 @@ export function invokeStep(dataDir, sessionId, step, targetRoot = null) {
|
|
|
665
880
|
|
|
666
881
|
export function appendDiff(dataDir, targetRoot, input) {
|
|
667
882
|
const sessionId = assertSessionId(input.sessionId)
|
|
668
|
-
const manifest =
|
|
669
|
-
|
|
883
|
+
const manifest = requireManifest(
|
|
884
|
+
dataDir,
|
|
885
|
+
sessionId,
|
|
886
|
+
`Report a plan for session ${sessionId} first`,
|
|
887
|
+
)
|
|
670
888
|
if (manifest.phase !== 'working') {
|
|
671
889
|
throw new Error(`Step ${manifest.currentStep} has not been invoked`)
|
|
672
890
|
}
|
|
@@ -748,8 +966,7 @@ function applyUnresolved(dataDir, targetRoot, manifest, diffId) {
|
|
|
748
966
|
}
|
|
749
967
|
|
|
750
968
|
export function continueDiff(dataDir, targetRoot, sessionId, diffId) {
|
|
751
|
-
const manifest =
|
|
752
|
-
if (!manifest) throw new Error(`Unknown session ${sessionId}`)
|
|
969
|
+
const manifest = requireManifest(dataDir, sessionId)
|
|
753
970
|
const active = pendingActive(manifest, diffId)
|
|
754
971
|
applyUnresolved(dataDir, targetRoot, manifest, diffId)
|
|
755
972
|
|
|
@@ -769,8 +986,7 @@ export function continueDiff(dataDir, targetRoot, sessionId, diffId) {
|
|
|
769
986
|
}
|
|
770
987
|
|
|
771
988
|
export function requestReplan(dataDir, sessionId, diffId, instruction) {
|
|
772
|
-
const manifest =
|
|
773
|
-
if (!manifest) throw new Error(`Unknown session ${sessionId}`)
|
|
989
|
+
const manifest = requireManifest(dataDir, sessionId)
|
|
774
990
|
const active = pendingActive(manifest, diffId)
|
|
775
991
|
const guidance = typeof instruction === 'string' ? instruction.trim() : ''
|
|
776
992
|
if (!guidance) throw new Error('An alternative instruction is required')
|
|
@@ -784,14 +1000,91 @@ export function requestReplan(dataDir, sessionId, diffId, instruction) {
|
|
|
784
1000
|
return manifest
|
|
785
1001
|
}
|
|
786
1002
|
|
|
787
|
-
|
|
1003
|
+
function unstageDiffSessionArtifacts(dataDir, targetRoot, extraPaths = []) {
|
|
1004
|
+
if (!targetRoot) return
|
|
1005
|
+
unstagePaths(targetRoot, [
|
|
1006
|
+
...extraPaths,
|
|
1007
|
+
diffSessionsRoot(dataDir),
|
|
1008
|
+
path.join(dataDir, 'active-session.json'),
|
|
1009
|
+
path.join(dataDir, 'blueprint-session.json'),
|
|
1010
|
+
])
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
function discardStoredSession(
|
|
1014
|
+
dataDir,
|
|
1015
|
+
sessionId,
|
|
1016
|
+
targetRoot = null,
|
|
1017
|
+
{ restore = true, keepStoppedMarker = false } = {},
|
|
1018
|
+
) {
|
|
788
1019
|
const safeId = assertSessionId(sessionId)
|
|
789
|
-
const
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
1020
|
+
const paths = sessionPaths(dataDir, safeId)
|
|
1021
|
+
const fileIds = Object.keys(readBaseline(dataDir, safeId).files)
|
|
1022
|
+
if (targetRoot && restore) {
|
|
1023
|
+
try {
|
|
1024
|
+
restoreBaseline(dataDir, safeId, targetRoot)
|
|
1025
|
+
} catch {
|
|
1026
|
+
// Incomplete session artifacts should still be deleted.
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
if (targetRoot) {
|
|
1030
|
+
unstageDiffSessionArtifacts(
|
|
1031
|
+
dataDir,
|
|
1032
|
+
targetRoot,
|
|
1033
|
+
fileIds.flatMap((id) => {
|
|
1034
|
+
try {
|
|
1035
|
+
return [resolveTargetFile(targetRoot, id).absolute]
|
|
1036
|
+
} catch {
|
|
1037
|
+
return []
|
|
1038
|
+
}
|
|
1039
|
+
}),
|
|
1040
|
+
)
|
|
1041
|
+
}
|
|
1042
|
+
releaseBlueprintSession(dataDir, safeId)
|
|
1043
|
+
if (fs.existsSync(paths.root)) {
|
|
1044
|
+
fs.rmSync(paths.root, { recursive: true, force: true })
|
|
1045
|
+
}
|
|
1046
|
+
if (!keepStoppedMarker) clearStoppedMarker(dataDir, safeId)
|
|
1047
|
+
const active = readActiveSession(dataDir)
|
|
1048
|
+
if (active === safeId) writeActiveSession(dataDir, null)
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
export function discardInactiveDiffSessions(
|
|
1052
|
+
dataDir,
|
|
1053
|
+
targetRoot = null,
|
|
1054
|
+
waiterIds = waiterSessionIds(),
|
|
1055
|
+
) {
|
|
1056
|
+
const keep = new Set()
|
|
1057
|
+
for (const value of waiterIds) {
|
|
1058
|
+
try {
|
|
1059
|
+
keep.add(assertSessionId(value))
|
|
1060
|
+
} catch {
|
|
1061
|
+
// Ignore process command lines with invalid session ids.
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
for (const sessionId of listStoredSessionIds(dataDir)) {
|
|
1066
|
+
const live = keep.has(sessionId) && Boolean(readManifest(dataDir, sessionId))
|
|
1067
|
+
const stopping = keep.has(sessionId) && isSessionStopped(dataDir, sessionId)
|
|
1068
|
+
if (live || stopping) continue
|
|
1069
|
+
discardStoredSession(dataDir, sessionId, targetRoot)
|
|
793
1070
|
}
|
|
794
|
-
|
|
1071
|
+
|
|
1072
|
+
const liveIds = [...keep].filter((id) => readManifest(dataDir, id))
|
|
1073
|
+
const active = readActiveSession(dataDir)
|
|
1074
|
+
if (active && !liveIds.includes(active)) writeActiveSession(dataDir, null)
|
|
1075
|
+
const locked = readBlueprintSession(dataDir)
|
|
1076
|
+
if (locked && !liveIds.includes(locked)) writeBlueprintSession(dataDir, null)
|
|
1077
|
+
unstageDiffSessionArtifacts(dataDir, targetRoot)
|
|
1078
|
+
return liveIds
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
export function stopSession(dataDir, sessionId, targetRoot = null) {
|
|
1082
|
+
const safeId = assertSessionId(sessionId)
|
|
1083
|
+
writeStoppedMarker(dataDir, safeId)
|
|
1084
|
+
discardStoredSession(dataDir, safeId, targetRoot, { keepStoppedMarker: true })
|
|
1085
|
+
const waiters = waiterSessionIds()
|
|
1086
|
+
waiters.add(safeId)
|
|
1087
|
+
discardInactiveDiffSessions(dataDir, targetRoot, waiters)
|
|
795
1088
|
return null
|
|
796
1089
|
}
|
|
797
1090
|
|
|
@@ -819,14 +1112,7 @@ export function closeSession(dataDir, sessionId) {
|
|
|
819
1112
|
}
|
|
820
1113
|
|
|
821
1114
|
export function finalizeFinishedSession(dataDir, sessionId) {
|
|
822
|
-
|
|
823
|
-
const paths = sessionPaths(dataDir, safeId)
|
|
824
|
-
releaseBlueprintSession(dataDir, safeId)
|
|
825
|
-
if (fs.existsSync(paths.root)) {
|
|
826
|
-
fs.rmSync(paths.root, { recursive: true, force: true })
|
|
827
|
-
}
|
|
828
|
-
const active = readActiveSession(dataDir)
|
|
829
|
-
if (active === safeId) writeActiveSession(dataDir, null)
|
|
1115
|
+
discardStoredSession(dataDir, sessionId, null, { restore: false })
|
|
830
1116
|
}
|
|
831
1117
|
|
|
832
1118
|
export function emptyBlueprint() {
|