@jkwd/inbase 0.1.3 → 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/open-editor.d.ts +6 -0
- package/apps/explorer/scripts/open-editor.mjs +219 -0
- package/apps/explorer/scripts/session-store.d.ts +51 -1
- package/apps/explorer/scripts/session-store.mjs +461 -48
- package/apps/explorer/src/App.tsx +178 -78
- package/apps/explorer/src/agentIntent.ts +54 -1
- package/apps/explorer/src/index.css +238 -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 +41 -18
- 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 +19 -50
- package/apps/explorer/src/theme.ts +10 -1
- package/apps/explorer/src/types.ts +12 -0
- package/apps/explorer/src/ui/HUD.tsx +699 -390
- package/apps/explorer/vite.config.ts +72 -22
- package/bin/session.mjs +17 -3
- package/package.json +3 -1
- package/skill/inbase/SKILL.md +22 -14
|
@@ -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 === '..') {
|
|
@@ -44,9 +46,67 @@ export function sessionPaths(dataDir, sessionId) {
|
|
|
44
46
|
diffs: path.join(root, 'diffs'),
|
|
45
47
|
manifest: path.join(root, 'manifest.json'),
|
|
46
48
|
blueprint: path.join(root, 'blueprint.json'),
|
|
49
|
+
baseline: path.join(root, 'baseline.json'),
|
|
50
|
+
baselineFiles: path.join(root, 'baseline'),
|
|
51
|
+
stopped: path.join(dataDir, 'diff-sessions', `${safeId}.stopped`),
|
|
47
52
|
}
|
|
48
53
|
}
|
|
49
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
|
+
|
|
93
|
+
export function resolveTargetFile(targetRoot, fileId) {
|
|
94
|
+
if (typeof fileId !== 'string' || fileId.trim() === '') {
|
|
95
|
+
throw new Error('fileId is required')
|
|
96
|
+
}
|
|
97
|
+
const normalized = fileId.trim().replaceAll('\\', '/').replace(/^\/+/, '')
|
|
98
|
+
if (!normalized || normalized === '.' || normalized.includes('..')) {
|
|
99
|
+
throw new Error(`Invalid file id ${fileId}`)
|
|
100
|
+
}
|
|
101
|
+
const root = path.resolve(targetRoot)
|
|
102
|
+
const absolute = path.resolve(root, normalized)
|
|
103
|
+
const prefix = root.endsWith(path.sep) ? root : `${root}${path.sep}`
|
|
104
|
+
if (absolute !== root && !absolute.startsWith(prefix)) {
|
|
105
|
+
throw new Error(`Invalid file id ${fileId}`)
|
|
106
|
+
}
|
|
107
|
+
return { id: normalized, absolute }
|
|
108
|
+
}
|
|
109
|
+
|
|
50
110
|
export function readActiveSession(dataDir) {
|
|
51
111
|
const value = readJson(path.join(dataDir, 'active-session.json'), null)
|
|
52
112
|
return value?.sessionId ? assertSessionId(value.sessionId) : null
|
|
@@ -59,6 +119,138 @@ export function writeActiveSession(dataDir, sessionId) {
|
|
|
59
119
|
)
|
|
60
120
|
}
|
|
61
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
|
+
|
|
62
254
|
export function readBlueprintSession(dataDir) {
|
|
63
255
|
const value = readJson(path.join(dataDir, 'blueprint-session.json'), null)
|
|
64
256
|
return value?.sessionId ? assertSessionId(value.sessionId) : null
|
|
@@ -305,10 +497,147 @@ export function sessionIntent(dataDir, sessionId, knownFileIds = [], selectedDif
|
|
|
305
497
|
}
|
|
306
498
|
}
|
|
307
499
|
|
|
308
|
-
function
|
|
500
|
+
function emptyBaseline() {
|
|
501
|
+
return { files: {} }
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
function readBaseline(dataDir, sessionId) {
|
|
505
|
+
const { baseline } = sessionPaths(dataDir, sessionId)
|
|
506
|
+
const value = readJson(baseline, emptyBaseline())
|
|
507
|
+
return {
|
|
508
|
+
files:
|
|
509
|
+
value?.files && typeof value.files === 'object' && !Array.isArray(value.files)
|
|
510
|
+
? value.files
|
|
511
|
+
: {},
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
function writeBaseline(dataDir, sessionId, baseline) {
|
|
516
|
+
const { baseline: file } = sessionPaths(dataDir, sessionId)
|
|
517
|
+
atomicWrite(file, `${JSON.stringify({ files: baseline.files ?? {} }, null, 2)}\n`)
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
function pruneEmptyDirs(targetRoot, filePath) {
|
|
521
|
+
const root = path.resolve(targetRoot)
|
|
522
|
+
let current = path.dirname(filePath)
|
|
523
|
+
while (current.startsWith(`${root}${path.sep}`)) {
|
|
524
|
+
if (!fs.existsSync(current)) {
|
|
525
|
+
current = path.dirname(current)
|
|
526
|
+
continue
|
|
527
|
+
}
|
|
528
|
+
if (fs.readdirSync(current).length > 0) break
|
|
529
|
+
fs.rmdirSync(current)
|
|
530
|
+
current = path.dirname(current)
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
export function captureBaseline(dataDir, sessionId, targetRoot, fileIds = []) {
|
|
535
|
+
const paths = sessionPaths(dataDir, sessionId)
|
|
536
|
+
const baseline = readBaseline(dataDir, sessionId)
|
|
537
|
+
let changed = false
|
|
538
|
+
for (const fileId of fileIds) {
|
|
539
|
+
const { id, absolute } = resolveTargetFile(targetRoot, fileId)
|
|
540
|
+
if (baseline.files[id]) continue
|
|
541
|
+
const existed = fs.existsSync(absolute) && fs.statSync(absolute).isFile()
|
|
542
|
+
baseline.files[id] = { existed }
|
|
543
|
+
if (existed) {
|
|
544
|
+
const stored = resolveTargetFile(paths.baselineFiles, id).absolute
|
|
545
|
+
fs.mkdirSync(path.dirname(stored), { recursive: true })
|
|
546
|
+
fs.copyFileSync(absolute, stored)
|
|
547
|
+
}
|
|
548
|
+
changed = true
|
|
549
|
+
}
|
|
550
|
+
if (changed) writeBaseline(dataDir, sessionId, baseline)
|
|
551
|
+
return baseline
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
export function restoreBaseline(dataDir, sessionId, targetRoot) {
|
|
555
|
+
const paths = sessionPaths(dataDir, sessionId)
|
|
556
|
+
const baseline = readBaseline(dataDir, sessionId)
|
|
557
|
+
for (const [fileId, info] of Object.entries(baseline.files)) {
|
|
558
|
+
const { absolute } = resolveTargetFile(targetRoot, fileId)
|
|
559
|
+
if (!info?.existed) {
|
|
560
|
+
fs.rmSync(absolute, { force: true })
|
|
561
|
+
pruneEmptyDirs(targetRoot, absolute)
|
|
562
|
+
continue
|
|
563
|
+
}
|
|
564
|
+
const stored = resolveTargetFile(paths.baselineFiles, fileId).absolute
|
|
565
|
+
fs.mkdirSync(path.dirname(absolute), { recursive: true })
|
|
566
|
+
fs.copyFileSync(stored, absolute)
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
function replayPatches(dataDir, sessionId, targetRoot, entries) {
|
|
571
|
+
for (const entry of entries) {
|
|
572
|
+
applyUnifiedPatch(readDiff(dataDir, sessionId, entry), targetRoot)
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
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
|
+
}
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
function liveEntries(manifest, diffId) {
|
|
624
|
+
return chainThrough(manifest, diffId).filter((entry) => entry.status !== 'rejected')
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
export function materializeDiff(dataDir, targetRoot, sessionId, diffId) {
|
|
628
|
+
const manifest = requireManifest(dataDir, sessionId)
|
|
629
|
+
const through = diffId || manifest.activeDiffId
|
|
630
|
+
if (!through) return manifest
|
|
631
|
+
restoreBaseline(dataDir, sessionId, targetRoot)
|
|
632
|
+
replayPatches(dataDir, sessionId, targetRoot, liveEntries(manifest, through))
|
|
633
|
+
return manifest
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
function withSessionReplay(dataDir, sessionId, targetRoot, patches, action) {
|
|
309
637
|
const temporary = fs.mkdtempSync(path.join(os.tmpdir(), 'visual-coder-chain-'))
|
|
310
638
|
try {
|
|
311
639
|
fs.cpSync(targetRoot, temporary, { recursive: true })
|
|
640
|
+
restoreBaseline(dataDir, sessionId, temporary)
|
|
312
641
|
for (const patchText of patches) applyUnifiedPatch(patchText, temporary)
|
|
313
642
|
return action(temporary)
|
|
314
643
|
} finally {
|
|
@@ -317,10 +646,28 @@ function withVirtualTarget(targetRoot, patches, action) {
|
|
|
317
646
|
}
|
|
318
647
|
|
|
319
648
|
export function validateContinuation(dataDir, manifest, targetRoot, patchText) {
|
|
320
|
-
const prior =
|
|
321
|
-
|
|
649
|
+
const prior = manifest.diffs
|
|
650
|
+
.filter((entry) => entry.status !== 'rejected')
|
|
651
|
+
.map((entry) => readDiff(dataDir, manifest.sessionId, entry))
|
|
652
|
+
withSessionReplay(dataDir, manifest.sessionId, targetRoot, [...prior, patchText], () =>
|
|
653
|
+
undefined,
|
|
322
654
|
)
|
|
323
|
-
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
export function inspectTargetFile(
|
|
658
|
+
dataDir,
|
|
659
|
+
targetRoot,
|
|
660
|
+
{ sessionId, diffId, fileId } = {},
|
|
661
|
+
) {
|
|
662
|
+
if (sessionId && readManifest(dataDir, sessionId)) {
|
|
663
|
+
materializeDiff(dataDir, targetRoot, sessionId, diffId)
|
|
664
|
+
}
|
|
665
|
+
if (!fileId) return null
|
|
666
|
+
const { absolute } = resolveTargetFile(targetRoot, fileId)
|
|
667
|
+
if (!fs.existsSync(absolute) || !fs.statSync(absolute).isFile()) {
|
|
668
|
+
throw new Error(`File ${fileId} is not on disk`)
|
|
669
|
+
}
|
|
670
|
+
return absolute
|
|
324
671
|
}
|
|
325
672
|
|
|
326
673
|
function planSteps(titles, startAt = 1) {
|
|
@@ -341,6 +688,7 @@ function featureName(value) {
|
|
|
341
688
|
|
|
342
689
|
export function startSession(dataDir, input) {
|
|
343
690
|
const sessionId = assertSessionId(input.sessionId)
|
|
691
|
+
clearStoppedMarker(dataDir, sessionId)
|
|
344
692
|
const existing = readManifest(dataDir, sessionId)
|
|
345
693
|
if (existing) {
|
|
346
694
|
focusSession(dataDir, sessionId)
|
|
@@ -370,8 +718,7 @@ export function startSession(dataDir, input) {
|
|
|
370
718
|
}
|
|
371
719
|
|
|
372
720
|
export function answerBlueprint(dataDir, sessionId, enabled) {
|
|
373
|
-
const manifest =
|
|
374
|
-
if (!manifest) throw new Error(`Unknown session ${sessionId}`)
|
|
721
|
+
const manifest = requireManifest(dataDir, sessionId)
|
|
375
722
|
if (manifest.phase !== 'blueprint_ask') {
|
|
376
723
|
throw new Error(`Session ${sessionId} is not asking for a blueprint`)
|
|
377
724
|
}
|
|
@@ -390,8 +737,7 @@ export function answerBlueprint(dataDir, sessionId, enabled) {
|
|
|
390
737
|
|
|
391
738
|
export function updateBlueprint(dataDir, sessionId, input = {}) {
|
|
392
739
|
const safeId = assertSessionId(sessionId)
|
|
393
|
-
const manifest =
|
|
394
|
-
if (!manifest) throw new Error(`Unknown session ${safeId}`)
|
|
740
|
+
const manifest = requireManifest(dataDir, safeId)
|
|
395
741
|
if (manifest.phase !== 'blueprint') {
|
|
396
742
|
throw new Error(`Session ${safeId} is not in blueprint mode`)
|
|
397
743
|
}
|
|
@@ -412,8 +758,7 @@ export function updateBlueprint(dataDir, sessionId, input = {}) {
|
|
|
412
758
|
|
|
413
759
|
export function sendBlueprint(dataDir, sessionId, input = {}) {
|
|
414
760
|
const safeId = assertSessionId(sessionId)
|
|
415
|
-
const manifest =
|
|
416
|
-
if (!manifest) throw new Error(`Unknown session ${safeId}`)
|
|
761
|
+
const manifest = requireManifest(dataDir, safeId)
|
|
417
762
|
if (manifest.phase !== 'blueprint') {
|
|
418
763
|
throw new Error(`Session ${safeId} is not in blueprint mode`)
|
|
419
764
|
}
|
|
@@ -438,6 +783,9 @@ export function sendBlueprint(dataDir, sessionId, input = {}) {
|
|
|
438
783
|
export function reportPlan(dataDir, input) {
|
|
439
784
|
const sessionId = assertSessionId(input.sessionId)
|
|
440
785
|
const existing = readManifest(dataDir, sessionId)
|
|
786
|
+
if (!existing && isSessionStopped(dataDir, sessionId)) {
|
|
787
|
+
throw sessionStoppedError(sessionId)
|
|
788
|
+
}
|
|
441
789
|
const now = new Date().toISOString()
|
|
442
790
|
|
|
443
791
|
if (existing?.phase === 'blueprint_ask' || existing?.phase === 'blueprint') {
|
|
@@ -491,8 +839,7 @@ export function reportPlan(dataDir, input) {
|
|
|
491
839
|
}
|
|
492
840
|
|
|
493
841
|
export function invokeStep(dataDir, sessionId, step, targetRoot = null) {
|
|
494
|
-
const manifest =
|
|
495
|
-
if (!manifest) throw new Error(`Unknown session ${sessionId}`)
|
|
842
|
+
const manifest = requireManifest(dataDir, sessionId)
|
|
496
843
|
|
|
497
844
|
if (manifest.phase === 'review') {
|
|
498
845
|
if (!targetRoot) throw new Error('A target root is required to apply the current step')
|
|
@@ -533,8 +880,11 @@ export function invokeStep(dataDir, sessionId, step, targetRoot = null) {
|
|
|
533
880
|
|
|
534
881
|
export function appendDiff(dataDir, targetRoot, input) {
|
|
535
882
|
const sessionId = assertSessionId(input.sessionId)
|
|
536
|
-
const manifest =
|
|
537
|
-
|
|
883
|
+
const manifest = requireManifest(
|
|
884
|
+
dataDir,
|
|
885
|
+
sessionId,
|
|
886
|
+
`Report a plan for session ${sessionId} first`,
|
|
887
|
+
)
|
|
538
888
|
if (manifest.phase !== 'working') {
|
|
539
889
|
throw new Error(`Step ${manifest.currentStep} has not been invoked`)
|
|
540
890
|
}
|
|
@@ -555,6 +905,12 @@ export function appendDiff(dataDir, targetRoot, input) {
|
|
|
555
905
|
}
|
|
556
906
|
|
|
557
907
|
validateContinuation(dataDir, manifest, targetRoot, input.patchText)
|
|
908
|
+
captureBaseline(
|
|
909
|
+
dataDir,
|
|
910
|
+
sessionId,
|
|
911
|
+
targetRoot,
|
|
912
|
+
parseUnifiedPatch(input.patchText).entries.map((entry) => entry.id),
|
|
913
|
+
)
|
|
558
914
|
if (parent?.status === 'extend') parent.status = 'extended'
|
|
559
915
|
|
|
560
916
|
const id = String(manifest.diffs.length + 1).padStart(4, '0')
|
|
@@ -581,6 +937,7 @@ export function appendDiff(dataDir, targetRoot, input) {
|
|
|
581
937
|
manifest.workStartedAt = null
|
|
582
938
|
manifest.diffs.push(entry)
|
|
583
939
|
writeManifest(dataDir, manifest)
|
|
940
|
+
materializeDiff(dataDir, targetRoot, sessionId, id)
|
|
584
941
|
focusSession(dataDir, sessionId)
|
|
585
942
|
return { manifest, entry }
|
|
586
943
|
}
|
|
@@ -601,25 +958,7 @@ function pendingActive(manifest, diffId) {
|
|
|
601
958
|
|
|
602
959
|
function applyUnresolved(dataDir, targetRoot, manifest, diffId) {
|
|
603
960
|
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
|
-
})
|
|
961
|
+
materializeDiff(dataDir, targetRoot, manifest.sessionId, diffId)
|
|
623
962
|
for (const entry of unresolved) {
|
|
624
963
|
entry.status = 'applied'
|
|
625
964
|
entry.decidedAt = new Date().toISOString()
|
|
@@ -627,8 +966,7 @@ function applyUnresolved(dataDir, targetRoot, manifest, diffId) {
|
|
|
627
966
|
}
|
|
628
967
|
|
|
629
968
|
export function continueDiff(dataDir, targetRoot, sessionId, diffId) {
|
|
630
|
-
const manifest =
|
|
631
|
-
if (!manifest) throw new Error(`Unknown session ${sessionId}`)
|
|
969
|
+
const manifest = requireManifest(dataDir, sessionId)
|
|
632
970
|
const active = pendingActive(manifest, diffId)
|
|
633
971
|
applyUnresolved(dataDir, targetRoot, manifest, diffId)
|
|
634
972
|
|
|
@@ -648,8 +986,7 @@ export function continueDiff(dataDir, targetRoot, sessionId, diffId) {
|
|
|
648
986
|
}
|
|
649
987
|
|
|
650
988
|
export function requestReplan(dataDir, sessionId, diffId, instruction) {
|
|
651
|
-
const manifest =
|
|
652
|
-
if (!manifest) throw new Error(`Unknown session ${sessionId}`)
|
|
989
|
+
const manifest = requireManifest(dataDir, sessionId)
|
|
653
990
|
const active = pendingActive(manifest, diffId)
|
|
654
991
|
const guidance = typeof instruction === 'string' ? instruction.trim() : ''
|
|
655
992
|
if (!guidance) throw new Error('An alternative instruction is required')
|
|
@@ -663,8 +1000,91 @@ export function requestReplan(dataDir, sessionId, diffId, instruction) {
|
|
|
663
1000
|
return manifest
|
|
664
1001
|
}
|
|
665
1002
|
|
|
666
|
-
|
|
667
|
-
|
|
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
|
+
) {
|
|
1019
|
+
const safeId = assertSessionId(sessionId)
|
|
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)
|
|
1070
|
+
}
|
|
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)
|
|
668
1088
|
return null
|
|
669
1089
|
}
|
|
670
1090
|
|
|
@@ -682,7 +1102,7 @@ export function decideDiff(
|
|
|
682
1102
|
if (decision === 'extend') {
|
|
683
1103
|
return requestReplan(dataDir, sessionId, diffId, instruction)
|
|
684
1104
|
}
|
|
685
|
-
return stopSession(dataDir, sessionId,
|
|
1105
|
+
return stopSession(dataDir, sessionId, targetRoot)
|
|
686
1106
|
}
|
|
687
1107
|
|
|
688
1108
|
export function closeSession(dataDir, sessionId) {
|
|
@@ -692,14 +1112,7 @@ export function closeSession(dataDir, sessionId) {
|
|
|
692
1112
|
}
|
|
693
1113
|
|
|
694
1114
|
export function finalizeFinishedSession(dataDir, sessionId) {
|
|
695
|
-
|
|
696
|
-
const paths = sessionPaths(dataDir, safeId)
|
|
697
|
-
releaseBlueprintSession(dataDir, safeId)
|
|
698
|
-
if (fs.existsSync(paths.root)) {
|
|
699
|
-
fs.rmSync(paths.root, { recursive: true, force: true })
|
|
700
|
-
}
|
|
701
|
-
const active = readActiveSession(dataDir)
|
|
702
|
-
if (active === safeId) writeActiveSession(dataDir, null)
|
|
1115
|
+
discardStoredSession(dataDir, sessionId, null, { restore: false })
|
|
703
1116
|
}
|
|
704
1117
|
|
|
705
1118
|
export function emptyBlueprint() {
|