@jkwd/inbase 0.1.5 → 0.1.7
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 +4 -1
- package/apps/explorer/scripts/patch-lib.d.ts +13 -0
- package/apps/explorer/scripts/patch-lib.mjs +162 -25
- package/apps/explorer/scripts/session-store.d.ts +18 -1
- package/apps/explorer/scripts/session-store.mjs +102 -26
- package/apps/explorer/src/App.tsx +153 -3
- package/apps/explorer/src/agentIntent.ts +9 -0
- package/apps/explorer/src/index.css +103 -11
- package/apps/explorer/src/layout.ts +65 -1
- package/apps/explorer/src/scene/Bridge.tsx +23 -5
- package/apps/explorer/src/scene/FileBlock.tsx +13 -9
- package/apps/explorer/src/scene/FolderArea.tsx +18 -12
- package/apps/explorer/src/scene/MapView.tsx +69 -12
- package/apps/explorer/src/scene/Player.tsx +9 -1
- package/apps/explorer/src/scene/SelectionController.tsx +36 -13
- package/apps/explorer/src/scene/SelectionThumbnail.tsx +477 -33
- package/apps/explorer/src/scene/World.tsx +27 -10
- package/apps/explorer/src/theme.ts +10 -3
- package/apps/explorer/src/types.ts +5 -0
- package/apps/explorer/src/ui/HUD.tsx +294 -37
- package/apps/explorer/vite.config.ts +11 -1
- package/bin/session.mjs +18 -6
- package/package.json +1 -1
- package/skill/inbase/SKILL.md +13 -8
package/README.md
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
<p align="center">
|
|
2
|
-
<
|
|
2
|
+
<picture>
|
|
3
|
+
<source media="(prefers-color-scheme: dark)" srcset="docs/inbase-logo-white.png" />
|
|
4
|
+
<img src="docs/inbase-logo.png" alt="InBase — Dive into your codebase" width="520" />
|
|
5
|
+
</picture>
|
|
3
6
|
</p>
|
|
4
7
|
|
|
5
8
|
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.
|
|
@@ -46,14 +46,23 @@ export function extractPatchAdditions(
|
|
|
46
46
|
addedFunctions: PatchSymbolAddition[]
|
|
47
47
|
addedVariables: PatchSymbolAddition[]
|
|
48
48
|
addedImports: PatchImportAddition[]
|
|
49
|
+
changedFunctions: PatchSymbolAddition[]
|
|
50
|
+
changedVariables: PatchSymbolAddition[]
|
|
49
51
|
}
|
|
50
52
|
|
|
51
53
|
export function accumulatePatchAdditions(patches?: string[]): {
|
|
52
54
|
addedFunctions: PatchSymbolAddition[]
|
|
53
55
|
addedVariables: PatchSymbolAddition[]
|
|
54
56
|
addedImports: PatchImportAddition[]
|
|
57
|
+
changedFunctions: PatchSymbolAddition[]
|
|
58
|
+
changedVariables: PatchSymbolAddition[]
|
|
55
59
|
}
|
|
56
60
|
|
|
61
|
+
export function applyUnifiedPatchToContents(
|
|
62
|
+
files: Map<string, string>,
|
|
63
|
+
patch: string,
|
|
64
|
+
): ReturnType<typeof parseUnifiedPatch>
|
|
65
|
+
|
|
57
66
|
export function applyUnifiedPatch(
|
|
58
67
|
patch: string,
|
|
59
68
|
targetRoot: string,
|
|
@@ -74,6 +83,7 @@ export const emptyIntent: {
|
|
|
74
83
|
feature: null
|
|
75
84
|
steps: unknown[]
|
|
76
85
|
step: null
|
|
86
|
+
stepByStep: boolean
|
|
77
87
|
files: string[]
|
|
78
88
|
creates: string[]
|
|
79
89
|
deletes: string[]
|
|
@@ -83,6 +93,8 @@ export const emptyIntent: {
|
|
|
83
93
|
addedFunctions: PatchSymbolAddition[]
|
|
84
94
|
addedVariables: PatchSymbolAddition[]
|
|
85
95
|
addedImports: PatchImportAddition[]
|
|
96
|
+
changedFunctions: PatchSymbolAddition[]
|
|
97
|
+
changedVariables: PatchSymbolAddition[]
|
|
86
98
|
reason: null
|
|
87
99
|
sessionId: null
|
|
88
100
|
diffId: null
|
|
@@ -93,6 +105,7 @@ export const emptyIntent: {
|
|
|
93
105
|
preview: boolean
|
|
94
106
|
phase: null
|
|
95
107
|
working: boolean
|
|
108
|
+
stalledWait: boolean
|
|
96
109
|
creationMode: boolean
|
|
97
110
|
canEnterBlueprint: boolean
|
|
98
111
|
blueprintSessionId: string | null
|
|
@@ -39,7 +39,7 @@ function parseHunks(section) {
|
|
|
39
39
|
let current = null
|
|
40
40
|
|
|
41
41
|
for (const line of lines) {
|
|
42
|
-
const header = line.match(/^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))?
|
|
42
|
+
const header = line.match(/^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@(.*)$/)
|
|
43
43
|
if (header) {
|
|
44
44
|
if (current) hunks.push(current)
|
|
45
45
|
current = {
|
|
@@ -47,6 +47,7 @@ function parseHunks(section) {
|
|
|
47
47
|
oldCount: Number(header[2] ?? '1'),
|
|
48
48
|
newStart: Number(header[3]),
|
|
49
49
|
newCount: Number(header[4] ?? '1'),
|
|
50
|
+
headerContext: (header[5] ?? '').trim(),
|
|
50
51
|
lines: [],
|
|
51
52
|
}
|
|
52
53
|
continue
|
|
@@ -182,30 +183,107 @@ function emptyAdditions() {
|
|
|
182
183
|
addedFunctions: [],
|
|
183
184
|
addedVariables: [],
|
|
184
185
|
addedImports: [],
|
|
186
|
+
changedFunctions: [],
|
|
187
|
+
changedVariables: [],
|
|
185
188
|
}
|
|
186
189
|
}
|
|
187
190
|
|
|
191
|
+
function isImportLine(line) {
|
|
192
|
+
return /^\s*import\b/.test(line)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function isTopLevelSymbolLine(line) {
|
|
196
|
+
return extractSymbols(line).length > 0 && !/^\s/.test(line)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function enclosingChangedSymbols(entry) {
|
|
200
|
+
if (entry.kind === 'add') return []
|
|
201
|
+
const found = []
|
|
202
|
+
const seen = new Set()
|
|
203
|
+
|
|
204
|
+
for (const hunk of entry.hunks) {
|
|
205
|
+
const contextLines = []
|
|
206
|
+
let addedTopLevel = false
|
|
207
|
+
let hasBodyEdit = false
|
|
208
|
+
for (const line of hunk.lines) {
|
|
209
|
+
const tag = line[0]
|
|
210
|
+
const body = line.slice(1)
|
|
211
|
+
if (tag === ' ') contextLines.push(body)
|
|
212
|
+
if (tag !== '+' && tag !== '-') continue
|
|
213
|
+
if (tag === '+' && isTopLevelSymbolLine(body)) addedTopLevel = true
|
|
214
|
+
if (
|
|
215
|
+
!addedTopLevel &&
|
|
216
|
+
extractSymbols(body).length === 0 &&
|
|
217
|
+
!isImportLine(body) &&
|
|
218
|
+
body.trim()
|
|
219
|
+
) {
|
|
220
|
+
hasBodyEdit = true
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
if (!hasBodyEdit) continue
|
|
224
|
+
for (const symbol of [
|
|
225
|
+
...extractSymbols(hunk.headerContext ?? ''),
|
|
226
|
+
...extractSymbols(contextLines.join('\n')),
|
|
227
|
+
]) {
|
|
228
|
+
const key = `${symbol.kind}:${symbol.name}`
|
|
229
|
+
if (seen.has(key)) continue
|
|
230
|
+
seen.add(key)
|
|
231
|
+
found.push(symbol)
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return found
|
|
236
|
+
}
|
|
237
|
+
|
|
188
238
|
function applyEntriesToAdditions(current, entries) {
|
|
189
239
|
const functions = new Map(
|
|
190
|
-
current.addedFunctions.map((item) => [additionKey(item.file, item.name), item]),
|
|
240
|
+
(current.addedFunctions ?? []).map((item) => [additionKey(item.file, item.name), item]),
|
|
191
241
|
)
|
|
192
242
|
const variables = new Map(
|
|
193
|
-
current.addedVariables.map((item) => [additionKey(item.file, item.name), item]),
|
|
243
|
+
(current.addedVariables ?? []).map((item) => [additionKey(item.file, item.name), item]),
|
|
244
|
+
)
|
|
245
|
+
const changedFunctions = new Map(
|
|
246
|
+
(current.changedFunctions ?? []).map((item) => [
|
|
247
|
+
additionKey(item.file, item.name),
|
|
248
|
+
item,
|
|
249
|
+
]),
|
|
250
|
+
)
|
|
251
|
+
const changedVariables = new Map(
|
|
252
|
+
(current.changedVariables ?? []).map((item) => [
|
|
253
|
+
additionKey(item.file, item.name),
|
|
254
|
+
item,
|
|
255
|
+
]),
|
|
194
256
|
)
|
|
195
257
|
const imports = new Map(
|
|
196
|
-
current.addedImports.map((item) => [
|
|
258
|
+
(current.addedImports ?? []).map((item) => [
|
|
259
|
+
additionKey(item.file, item.name, item.from),
|
|
260
|
+
item,
|
|
261
|
+
]),
|
|
197
262
|
)
|
|
198
263
|
|
|
199
|
-
const
|
|
200
|
-
for (const key of [...
|
|
201
|
-
if (
|
|
264
|
+
const dropFrom = (bucket, fileId) => {
|
|
265
|
+
for (const key of [...bucket.keys()]) {
|
|
266
|
+
if (bucket.get(key).file === fileId) bucket.delete(key)
|
|
202
267
|
}
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const dropFile = (fileId) => {
|
|
271
|
+
dropFrom(functions, fileId)
|
|
272
|
+
dropFrom(variables, fileId)
|
|
273
|
+
dropFrom(changedFunctions, fileId)
|
|
274
|
+
dropFrom(changedVariables, fileId)
|
|
275
|
+
dropFrom(imports, fileId)
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const markChanged = (kind, file, name) => {
|
|
279
|
+
const key = additionKey(file, name)
|
|
280
|
+
if (kind === 'function') {
|
|
281
|
+
if (functions.has(key)) return
|
|
282
|
+
changedFunctions.set(key, { name, file })
|
|
283
|
+
return
|
|
208
284
|
}
|
|
285
|
+
if (variables.has(key)) return
|
|
286
|
+
changedVariables.set(key, { name, file })
|
|
209
287
|
}
|
|
210
288
|
|
|
211
289
|
for (const entry of entries) {
|
|
@@ -224,15 +302,36 @@ function applyEntriesToAdditions(current, entries) {
|
|
|
224
302
|
)
|
|
225
303
|
|
|
226
304
|
for (const symbol of removedSymbols) {
|
|
227
|
-
|
|
228
|
-
if (symbol.kind
|
|
229
|
-
|
|
305
|
+
const key = additionKey(entry.id, symbol.name)
|
|
306
|
+
if (keptSymbolKeys.has(`${symbol.kind}:${symbol.name}`)) {
|
|
307
|
+
markChanged(symbol.kind, entry.id, symbol.name)
|
|
308
|
+
continue
|
|
309
|
+
}
|
|
310
|
+
if (symbol.kind === 'function') {
|
|
311
|
+
functions.delete(key)
|
|
312
|
+
changedFunctions.delete(key)
|
|
313
|
+
} else {
|
|
314
|
+
variables.delete(key)
|
|
315
|
+
changedVariables.delete(key)
|
|
316
|
+
}
|
|
230
317
|
}
|
|
231
318
|
for (const symbol of addedSymbols) {
|
|
232
|
-
if (previousSymbolKeys.has(`${symbol.kind}:${symbol.name}`))
|
|
319
|
+
if (previousSymbolKeys.has(`${symbol.kind}:${symbol.name}`)) {
|
|
320
|
+
markChanged(symbol.kind, entry.id, symbol.name)
|
|
321
|
+
continue
|
|
322
|
+
}
|
|
233
323
|
const item = { name: symbol.name, file: entry.id }
|
|
234
|
-
|
|
235
|
-
|
|
324
|
+
const key = additionKey(entry.id, symbol.name)
|
|
325
|
+
if (symbol.kind === 'function') {
|
|
326
|
+
changedFunctions.delete(key)
|
|
327
|
+
functions.set(key, item)
|
|
328
|
+
} else {
|
|
329
|
+
changedVariables.delete(key)
|
|
330
|
+
variables.set(key, item)
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
for (const symbol of enclosingChangedSymbols(entry)) {
|
|
334
|
+
markChanged(symbol.kind, entry.id, symbol.name)
|
|
236
335
|
}
|
|
237
336
|
|
|
238
337
|
const removedBindings = extractImportBindings(removedSource(entry))
|
|
@@ -262,6 +361,8 @@ function applyEntriesToAdditions(current, entries) {
|
|
|
262
361
|
addedFunctions: [...functions.values()],
|
|
263
362
|
addedVariables: [...variables.values()],
|
|
264
363
|
addedImports: [...imports.values()],
|
|
364
|
+
changedFunctions: [...changedFunctions.values()],
|
|
365
|
+
changedVariables: [...changedVariables.values()],
|
|
265
366
|
}
|
|
266
367
|
}
|
|
267
368
|
|
|
@@ -345,30 +446,58 @@ function applyHunks(original, hunks) {
|
|
|
345
446
|
return `${lines.join('\n')}\n`
|
|
346
447
|
}
|
|
347
448
|
|
|
348
|
-
export function
|
|
449
|
+
export function applyUnifiedPatchToContents(files, patch) {
|
|
349
450
|
const parsed = parseUnifiedPatch(patch)
|
|
350
451
|
if (parsed.entries.length === 0) {
|
|
351
452
|
throw new Error('Patch did not contain any file changes')
|
|
352
453
|
}
|
|
353
454
|
|
|
354
455
|
for (const file of parsed.entries) {
|
|
355
|
-
const absolute = path.join(targetRoot, file.id)
|
|
356
456
|
if (file.kind === 'delete') {
|
|
357
|
-
|
|
457
|
+
files.delete(file.id)
|
|
358
458
|
continue
|
|
359
459
|
}
|
|
360
460
|
|
|
361
461
|
const original =
|
|
362
|
-
file.kind === 'add' || !
|
|
363
|
-
? ''
|
|
364
|
-
: fs.readFileSync(absolute, 'utf8')
|
|
462
|
+
file.kind === 'add' || !files.has(file.id) ? '' : files.get(file.id)
|
|
365
463
|
|
|
366
464
|
if (file.kind === 'modify' && original === '') {
|
|
367
465
|
throw new Error(`Cannot modify missing file ${file.id}`)
|
|
368
466
|
}
|
|
369
467
|
|
|
468
|
+
files.set(file.id, applyHunks(original, file.hunks))
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
return parsed
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
export function applyUnifiedPatch(patch, targetRoot) {
|
|
475
|
+
const parsed = parseUnifiedPatch(patch)
|
|
476
|
+
if (parsed.entries.length === 0) {
|
|
477
|
+
throw new Error('Patch did not contain any file changes')
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
const files = new Map()
|
|
481
|
+
for (const file of parsed.entries) {
|
|
482
|
+
const absolute = path.join(targetRoot, file.id)
|
|
483
|
+
if (
|
|
484
|
+
file.kind !== 'add' &&
|
|
485
|
+
fs.existsSync(absolute) &&
|
|
486
|
+
fs.statSync(absolute).isFile()
|
|
487
|
+
) {
|
|
488
|
+
files.set(file.id, fs.readFileSync(absolute, 'utf8'))
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
applyUnifiedPatchToContents(files, patch)
|
|
492
|
+
|
|
493
|
+
for (const file of parsed.entries) {
|
|
494
|
+
const absolute = path.join(targetRoot, file.id)
|
|
495
|
+
if (file.kind === 'delete') {
|
|
496
|
+
if (fs.existsSync(absolute)) fs.rmSync(absolute)
|
|
497
|
+
continue
|
|
498
|
+
}
|
|
370
499
|
fs.mkdirSync(path.dirname(absolute), { recursive: true })
|
|
371
|
-
fs.writeFileSync(absolute,
|
|
500
|
+
fs.writeFileSync(absolute, files.get(file.id))
|
|
372
501
|
}
|
|
373
502
|
|
|
374
503
|
return parsed
|
|
@@ -381,6 +510,7 @@ export const emptyIntent = {
|
|
|
381
510
|
feature: null,
|
|
382
511
|
steps: [],
|
|
383
512
|
step: null,
|
|
513
|
+
stepByStep: true,
|
|
384
514
|
files: [],
|
|
385
515
|
creates: [],
|
|
386
516
|
deletes: [],
|
|
@@ -390,6 +520,8 @@ export const emptyIntent = {
|
|
|
390
520
|
addedFunctions: [],
|
|
391
521
|
addedVariables: [],
|
|
392
522
|
addedImports: [],
|
|
523
|
+
changedFunctions: [],
|
|
524
|
+
changedVariables: [],
|
|
393
525
|
reason: null,
|
|
394
526
|
sessionId: null,
|
|
395
527
|
diffId: null,
|
|
@@ -400,6 +532,7 @@ export const emptyIntent = {
|
|
|
400
532
|
preview: false,
|
|
401
533
|
phase: null,
|
|
402
534
|
working: false,
|
|
535
|
+
stalledWait: false,
|
|
403
536
|
creationMode: false,
|
|
404
537
|
canEnterBlueprint: false,
|
|
405
538
|
blueprintSessionId: null,
|
|
@@ -435,6 +568,8 @@ export function overlayPatch(intent, patchText, knownFileIds = []) {
|
|
|
435
568
|
addedFunctions: [],
|
|
436
569
|
addedVariables: [],
|
|
437
570
|
addedImports: [],
|
|
571
|
+
changedFunctions: [],
|
|
572
|
+
changedVariables: [],
|
|
438
573
|
}
|
|
439
574
|
}
|
|
440
575
|
|
|
@@ -451,6 +586,8 @@ export function overlayPatch(intent, patchText, knownFileIds = []) {
|
|
|
451
586
|
addedFunctions: base.addedFunctions ?? [],
|
|
452
587
|
addedVariables: base.addedVariables ?? [],
|
|
453
588
|
addedImports: base.addedImports ?? [],
|
|
589
|
+
changedFunctions: base.changedFunctions ?? [],
|
|
590
|
+
changedVariables: base.changedVariables ?? [],
|
|
454
591
|
}
|
|
455
592
|
}
|
|
456
593
|
|
|
@@ -37,6 +37,7 @@ export type DiffManifest = {
|
|
|
37
37
|
activeDiffId: string | null
|
|
38
38
|
pendingInstruction: string | null
|
|
39
39
|
workStartedAt: string | null
|
|
40
|
+
stepByStep: boolean
|
|
40
41
|
createdAt: string
|
|
41
42
|
updatedAt: string
|
|
42
43
|
diffs: DiffEntry[]
|
|
@@ -52,7 +53,10 @@ export function isSessionConnected(
|
|
|
52
53
|
waiterIds?: Set<string>,
|
|
53
54
|
): boolean
|
|
54
55
|
export function listStoredSessionIds(dataDir: string): string[]
|
|
55
|
-
export function listOpenSessionIds(
|
|
56
|
+
export function listOpenSessionIds(
|
|
57
|
+
dataDir: string,
|
|
58
|
+
waiterIds?: Set<string>,
|
|
59
|
+
): string[]
|
|
56
60
|
export function discardInactiveDiffSessions(
|
|
57
61
|
dataDir: string,
|
|
58
62
|
targetRoot?: string | null,
|
|
@@ -127,6 +131,18 @@ export function reportPlan(
|
|
|
127
131
|
stepTitles: string[]
|
|
128
132
|
},
|
|
129
133
|
): DiffManifest
|
|
134
|
+
export function isStepByStep(manifest: DiffManifest | null | undefined): boolean
|
|
135
|
+
export function autoAdvance(
|
|
136
|
+
dataDir: string,
|
|
137
|
+
sessionId: string,
|
|
138
|
+
targetRoot?: string | null,
|
|
139
|
+
): DiffManifest | null
|
|
140
|
+
export function setStepByStep(
|
|
141
|
+
dataDir: string,
|
|
142
|
+
sessionId: string,
|
|
143
|
+
enabled: boolean,
|
|
144
|
+
targetRoot?: string | null,
|
|
145
|
+
): DiffManifest
|
|
130
146
|
export function invokeStep(
|
|
131
147
|
dataDir: string,
|
|
132
148
|
sessionId: string,
|
|
@@ -138,6 +154,7 @@ export function sessionIntent(
|
|
|
138
154
|
sessionId: string,
|
|
139
155
|
knownFileIds?: string[],
|
|
140
156
|
selectedDiffId?: string,
|
|
157
|
+
waiterIds?: Set<string>,
|
|
141
158
|
): Record<string, unknown> | null
|
|
142
159
|
export function resolveTargetFile(
|
|
143
160
|
targetRoot: string,
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import fs from 'node:fs'
|
|
2
|
-
import os from 'node:os'
|
|
3
2
|
import path from 'node:path'
|
|
4
3
|
import { spawnSync } from 'node:child_process'
|
|
5
4
|
import {
|
|
6
5
|
accumulatePatchAdditions,
|
|
7
6
|
applyUnifiedPatch,
|
|
7
|
+
applyUnifiedPatchToContents,
|
|
8
8
|
collectCreateFolders,
|
|
9
9
|
extractPatchImports,
|
|
10
10
|
foldersFromFileIds,
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
|
|
14
14
|
const SESSION_ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/
|
|
15
15
|
const CONNECTED_TTL_MS = 15_000
|
|
16
|
+
const STALLED_WAIT_MS = 2_000
|
|
16
17
|
|
|
17
18
|
export function assertSessionId(value) {
|
|
18
19
|
if (typeof value !== 'string' || !SESSION_ID.test(value) || value === '.' || value === '..') {
|
|
@@ -170,6 +171,13 @@ function isGeneratingPhase(phase) {
|
|
|
170
171
|
return phase === 'preparing' || phase === 'working' || phase === 'replanning'
|
|
171
172
|
}
|
|
172
173
|
|
|
174
|
+
function isStalledWorking(manifest, waiterIds, sessionId, now = Date.now()) {
|
|
175
|
+
if (manifest.phase !== 'working') return false
|
|
176
|
+
if (!waiterIds.has(sessionId)) return false
|
|
177
|
+
const started = Date.parse(manifest.workStartedAt)
|
|
178
|
+
return Number.isFinite(started) && now - started >= STALLED_WAIT_MS
|
|
179
|
+
}
|
|
180
|
+
|
|
173
181
|
export function isSessionConnected(
|
|
174
182
|
dataDir,
|
|
175
183
|
sessionId,
|
|
@@ -216,16 +224,15 @@ export function listStoredSessionIds(dataDir) {
|
|
|
216
224
|
return [...ids]
|
|
217
225
|
}
|
|
218
226
|
|
|
219
|
-
export function listOpenSessionIds(dataDir) {
|
|
227
|
+
export function listOpenSessionIds(dataDir, waiterIds = waiterSessionIds()) {
|
|
220
228
|
const root = diffSessionsRoot(dataDir)
|
|
221
229
|
if (!fs.existsSync(root)) return []
|
|
222
|
-
const waiters = waiterSessionIds()
|
|
223
230
|
const sessions = []
|
|
224
231
|
for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
|
|
225
232
|
if (!entry.isDirectory()) continue
|
|
226
233
|
try {
|
|
227
234
|
const sessionId = assertSessionId(entry.name)
|
|
228
|
-
if (!isSessionConnected(dataDir, sessionId,
|
|
235
|
+
if (!isSessionConnected(dataDir, sessionId, waiterIds)) continue
|
|
229
236
|
const manifest = readManifest(dataDir, sessionId)
|
|
230
237
|
if (!manifest) continue
|
|
231
238
|
sessions.push({
|
|
@@ -246,8 +253,11 @@ export function listOpenSessionIds(dataDir) {
|
|
|
246
253
|
}
|
|
247
254
|
|
|
248
255
|
export function listSessionIntents(dataDir, knownFileIds = []) {
|
|
249
|
-
|
|
250
|
-
|
|
256
|
+
const waiters = waiterSessionIds()
|
|
257
|
+
return listOpenSessionIds(dataDir, waiters)
|
|
258
|
+
.map((sessionId) =>
|
|
259
|
+
sessionIntent(dataDir, sessionId, knownFileIds, undefined, waiters),
|
|
260
|
+
)
|
|
251
261
|
.filter(Boolean)
|
|
252
262
|
}
|
|
253
263
|
|
|
@@ -313,6 +323,7 @@ export function readManifest(dataDir, sessionId) {
|
|
|
313
323
|
value.pendingInstruction ??= null
|
|
314
324
|
value.workStartedAt ??= null
|
|
315
325
|
}
|
|
326
|
+
if (typeof value.stepByStep !== 'boolean') value.stepByStep = true
|
|
316
327
|
return value
|
|
317
328
|
}
|
|
318
329
|
|
|
@@ -417,7 +428,13 @@ export function previewPatchChain(patches, knownFileIds = []) {
|
|
|
417
428
|
}
|
|
418
429
|
}
|
|
419
430
|
|
|
420
|
-
export function sessionIntent(
|
|
431
|
+
export function sessionIntent(
|
|
432
|
+
dataDir,
|
|
433
|
+
sessionId,
|
|
434
|
+
knownFileIds = [],
|
|
435
|
+
selectedDiffId,
|
|
436
|
+
waiterIds = waiterSessionIds(),
|
|
437
|
+
) {
|
|
421
438
|
const manifest = readManifest(dataDir, sessionId)
|
|
422
439
|
if (!manifest) return null
|
|
423
440
|
const selectedId = selectedDiffId || manifest.activeDiffId
|
|
@@ -467,6 +484,7 @@ export function sessionIntent(dataDir, sessionId, knownFileIds = [], selectedDif
|
|
|
467
484
|
feature: manifest.feature,
|
|
468
485
|
steps: manifest.steps,
|
|
469
486
|
step: activeView ? manifest.currentStep : selected?.step ?? manifest.currentStep,
|
|
487
|
+
stepByStep: isStepByStep(manifest),
|
|
470
488
|
reason: activeView ? currentPlanStep?.title ?? null : selected?.title ?? null,
|
|
471
489
|
sessionId,
|
|
472
490
|
diffId: selected?.id ?? null,
|
|
@@ -485,6 +503,7 @@ export function sessionIntent(dataDir, sessionId, knownFileIds = [], selectedDif
|
|
|
485
503
|
manifest.phase === 'preparing' ||
|
|
486
504
|
manifest.phase === 'working' ||
|
|
487
505
|
manifest.phase === 'replanning',
|
|
506
|
+
stalledWait: isStalledWorking(manifest, waiterIds, sessionId),
|
|
488
507
|
creationMode: manifest.phase === 'blueprint' && ownsBlueprintLock,
|
|
489
508
|
canEnterBlueprint,
|
|
490
509
|
blueprintSessionId,
|
|
@@ -633,25 +652,47 @@ export function materializeDiff(dataDir, targetRoot, sessionId, diffId) {
|
|
|
633
652
|
return manifest
|
|
634
653
|
}
|
|
635
654
|
|
|
636
|
-
function
|
|
637
|
-
const
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
for (const
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
fs.rmSync(temporary, { recursive: true, force: true })
|
|
655
|
+
function loadReplayContents(dataDir, sessionId, targetRoot, patches) {
|
|
656
|
+
const baseline = readBaseline(dataDir, sessionId)
|
|
657
|
+
const paths = sessionPaths(dataDir, sessionId)
|
|
658
|
+
const fileIds = new Set(Object.keys(baseline.files))
|
|
659
|
+
for (const patchText of patches) {
|
|
660
|
+
for (const entry of parseUnifiedPatch(patchText).entries) {
|
|
661
|
+
fileIds.add(entry.id)
|
|
662
|
+
}
|
|
645
663
|
}
|
|
664
|
+
|
|
665
|
+
const files = new Map()
|
|
666
|
+
for (const fileId of fileIds) {
|
|
667
|
+
const info = baseline.files[fileId]
|
|
668
|
+
if (info) {
|
|
669
|
+
if (!info.existed) continue
|
|
670
|
+
const stored = resolveTargetFile(paths.baselineFiles, fileId).absolute
|
|
671
|
+
if (fs.existsSync(stored) && fs.statSync(stored).isFile()) {
|
|
672
|
+
files.set(fileId, fs.readFileSync(stored, 'utf8'))
|
|
673
|
+
}
|
|
674
|
+
continue
|
|
675
|
+
}
|
|
676
|
+
const { absolute } = resolveTargetFile(targetRoot, fileId)
|
|
677
|
+
if (fs.existsSync(absolute) && fs.statSync(absolute).isFile()) {
|
|
678
|
+
files.set(fileId, fs.readFileSync(absolute, 'utf8'))
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
return files
|
|
646
682
|
}
|
|
647
683
|
|
|
648
684
|
export function validateContinuation(dataDir, manifest, targetRoot, patchText) {
|
|
649
685
|
const prior = manifest.diffs
|
|
650
686
|
.filter((entry) => entry.status !== 'rejected')
|
|
651
687
|
.map((entry) => readDiff(dataDir, manifest.sessionId, entry))
|
|
652
|
-
|
|
653
|
-
|
|
688
|
+
const patches = [...prior, patchText]
|
|
689
|
+
const files = loadReplayContents(
|
|
690
|
+
dataDir,
|
|
691
|
+
manifest.sessionId,
|
|
692
|
+
targetRoot,
|
|
693
|
+
patches,
|
|
654
694
|
)
|
|
695
|
+
for (const next of patches) applyUnifiedPatchToContents(files, next)
|
|
655
696
|
}
|
|
656
697
|
|
|
657
698
|
export function inspectTargetFile(
|
|
@@ -686,6 +727,33 @@ function featureName(value) {
|
|
|
686
727
|
return trimmed
|
|
687
728
|
}
|
|
688
729
|
|
|
730
|
+
export function isStepByStep(manifest) {
|
|
731
|
+
return manifest?.stepByStep !== false
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
export function autoAdvance(dataDir, sessionId, targetRoot = null) {
|
|
735
|
+
const manifest = readManifest(dataDir, sessionId)
|
|
736
|
+
if (!manifest || isStepByStep(manifest)) return manifest
|
|
737
|
+
if (manifest.phase === 'plan_ready') {
|
|
738
|
+
return invokeStep(dataDir, sessionId, manifest.currentStep, targetRoot)
|
|
739
|
+
}
|
|
740
|
+
if (manifest.phase === 'review') {
|
|
741
|
+
const active = manifest.diffs.at(-1)
|
|
742
|
+
if (!active || active.status !== 'pending') return manifest
|
|
743
|
+
if (active.step >= manifest.steps.length) return manifest
|
|
744
|
+
return invokeStep(dataDir, sessionId, active.step + 1, targetRoot)
|
|
745
|
+
}
|
|
746
|
+
return manifest
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
export function setStepByStep(dataDir, sessionId, enabled, targetRoot = null) {
|
|
750
|
+
const manifest = requireManifest(dataDir, sessionId)
|
|
751
|
+
manifest.stepByStep = Boolean(enabled)
|
|
752
|
+
writeManifest(dataDir, manifest)
|
|
753
|
+
if (isStepByStep(manifest)) return manifest
|
|
754
|
+
return autoAdvance(dataDir, sessionId, targetRoot)
|
|
755
|
+
}
|
|
756
|
+
|
|
689
757
|
export function startSession(dataDir, input) {
|
|
690
758
|
const sessionId = assertSessionId(input.sessionId)
|
|
691
759
|
clearStoppedMarker(dataDir, sessionId)
|
|
@@ -703,6 +771,7 @@ export function startSession(dataDir, input) {
|
|
|
703
771
|
steps: [],
|
|
704
772
|
status: 'active',
|
|
705
773
|
phase: 'blueprint_ask',
|
|
774
|
+
stepByStep: true,
|
|
706
775
|
currentStep: 1,
|
|
707
776
|
activeDiffId: null,
|
|
708
777
|
pendingInstruction: null,
|
|
@@ -802,6 +871,7 @@ export function reportPlan(dataDir, input) {
|
|
|
802
871
|
steps: [],
|
|
803
872
|
status: 'active',
|
|
804
873
|
phase: 'preparing',
|
|
874
|
+
stepByStep: true,
|
|
805
875
|
currentStep: 1,
|
|
806
876
|
activeDiffId: null,
|
|
807
877
|
pendingInstruction: null,
|
|
@@ -818,7 +888,7 @@ export function reportPlan(dataDir, input) {
|
|
|
818
888
|
manifest.workStartedAt = null
|
|
819
889
|
writeManifest(dataDir, manifest)
|
|
820
890
|
focusSession(dataDir, sessionId)
|
|
821
|
-
return
|
|
891
|
+
return autoAdvance(dataDir, sessionId)
|
|
822
892
|
}
|
|
823
893
|
|
|
824
894
|
if (existing.phase !== 'replanning') {
|
|
@@ -835,7 +905,7 @@ export function reportPlan(dataDir, input) {
|
|
|
835
905
|
existing.workStartedAt = null
|
|
836
906
|
writeManifest(dataDir, existing)
|
|
837
907
|
focusSession(dataDir, sessionId)
|
|
838
|
-
return
|
|
908
|
+
return autoAdvance(dataDir, sessionId)
|
|
839
909
|
}
|
|
840
910
|
|
|
841
911
|
export function invokeStep(dataDir, sessionId, step, targetRoot = null) {
|
|
@@ -939,7 +1009,14 @@ export function appendDiff(dataDir, targetRoot, input) {
|
|
|
939
1009
|
writeManifest(dataDir, manifest)
|
|
940
1010
|
materializeDiff(dataDir, targetRoot, sessionId, id)
|
|
941
1011
|
focusSession(dataDir, sessionId)
|
|
942
|
-
|
|
1012
|
+
const advanced = autoAdvance(dataDir, sessionId, targetRoot)
|
|
1013
|
+
if (!advanced) {
|
|
1014
|
+
throw new Error(`Session ${sessionId} disappeared after publishing a diff`)
|
|
1015
|
+
}
|
|
1016
|
+
return {
|
|
1017
|
+
manifest: advanced,
|
|
1018
|
+
entry: advanced.diffs.find((item) => item.id === id) ?? entry,
|
|
1019
|
+
}
|
|
943
1020
|
}
|
|
944
1021
|
|
|
945
1022
|
function pendingActive(manifest, diffId) {
|
|
@@ -976,13 +1053,12 @@ export function continueDiff(dataDir, targetRoot, sessionId, diffId) {
|
|
|
976
1053
|
writeManifest(dataDir, manifest)
|
|
977
1054
|
finalizeFinishedSession(dataDir, sessionId)
|
|
978
1055
|
return manifest
|
|
979
|
-
} else {
|
|
980
|
-
manifest.currentStep = active.step + 1
|
|
981
|
-
manifest.phase = 'plan_ready'
|
|
982
|
-
manifest.workStartedAt = null
|
|
983
1056
|
}
|
|
1057
|
+
manifest.currentStep = active.step + 1
|
|
1058
|
+
manifest.phase = 'plan_ready'
|
|
1059
|
+
manifest.workStartedAt = null
|
|
984
1060
|
writeManifest(dataDir, manifest)
|
|
985
|
-
return
|
|
1061
|
+
return autoAdvance(dataDir, sessionId, targetRoot)
|
|
986
1062
|
}
|
|
987
1063
|
|
|
988
1064
|
export function requestReplan(dataDir, sessionId, diffId, instruction) {
|