@jkwd/inbase 0.1.8 → 0.1.10
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 +26 -4
- package/apps/explorer/scripts/patch-lib.d.ts +1 -0
- package/apps/explorer/scripts/patch-lib.mjs +1 -0
- package/apps/explorer/scripts/session-store.d.ts +6 -0
- package/apps/explorer/scripts/session-store.mjs +124 -67
- package/apps/explorer/src/App.tsx +84 -60
- package/apps/explorer/src/agentIntent.ts +15 -0
- package/apps/explorer/src/index.css +28 -10
- package/apps/explorer/src/types.ts +2 -0
- package/apps/explorer/src/ui/HUD.tsx +124 -96
- package/apps/explorer/vite.config.ts +9 -3
- package/bin/session.mjs +9 -9
- package/package.json +1 -1
- package/skill/inbase/SKILL.md +46 -22
package/README.md
CHANGED
|
@@ -8,11 +8,12 @@
|
|
|
8
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.
|
|
9
9
|
|
|
10
10
|
<p align="center">
|
|
11
|
-
<img src="docs/inbase-1.png" alt="First-person walk view of the codebase map" width="
|
|
12
|
-
|
|
11
|
+
<img src="docs/inbase-1.png" alt="First-person walk view of the codebase map" width="47%" />
|
|
12
|
+
|
|
13
|
+
<img src="docs/inbase-2.png" alt="Map view with 3D overlay" width="47%" />
|
|
13
14
|
</p>
|
|
14
15
|
|
|
15
|
-
Install the package in a project, run `inbase init`, then `inbase run`. Cursor
|
|
16
|
+
Install the package in a project, run `inbase init`, then `inbase run`. Cursor’s LLM then plans and patches through the visual map instead of editing files directly.
|
|
16
17
|
|
|
17
18
|
The npm package is `@jkwd/inbase` (npm blocks the unscoped name `inbase`). The command is still `inbase`.
|
|
18
19
|
|
|
@@ -40,7 +41,24 @@ Open the printed URL (http://localhost:5173 by default), click the scene, then w
|
|
|
40
41
|
inbase run --target /path/to/your/project
|
|
41
42
|
```
|
|
42
43
|
|
|
43
|
-
`inbase init` copies a Cursor skill into `.cursor/skills/inbase/` and gitignores `.inbase/`.
|
|
44
|
+
`inbase init` copies a Cursor skill into `.cursor/skills/inbase/` and gitignores `.inbase/`. Keep `inbase run` open, then ask Cursor to change source files — the LLM follows the visual plan and patch loop described below.
|
|
45
|
+
|
|
46
|
+
## LLM integration
|
|
47
|
+
|
|
48
|
+
<img src="docs/inbase-llm.png" alt="LLM working through a plan in the heads-up display overlay" align="left" width="280" hspace="16" />
|
|
49
|
+
|
|
50
|
+
Inbase does not call a model itself. The visual coding loop currently supports **Cursor**. The installed skill makes the agent work through the map: it reports a plan, waits on the **HUD** (heads-up display — the overlay panel on the 3D map), and publishes each step as a patch file. Those patch files are the source of truth and are applied on every step update.
|
|
51
|
+
|
|
52
|
+
When a chat starts, that overlay asks whether to set up a **blueprint**. **Create blueprint** lets you place files (`Space`) and folders (`B`), then **Send blueprint** — that layout is the source of truth for the chat. **Let LLM continue** skips the initial placement. You can still place files and islands on later steps; they are stored on that chat’s blueprint. When the session finishes, placement stops.
|
|
53
|
+
|
|
54
|
+
If several chats are open, only one session window is shown at a time. Switch with the tabs so the focused chat is the one whose blueprint you see and edit.
|
|
55
|
+
|
|
56
|
+
Turn on **Make LLM look where I look** if the agent should prefer the island you are standing on and the blocks you are facing. With **Step by step** on, click **Run step** to start a step, then **Accept proposal** when the patch is ready. With it off, the LLM implements the full plan; you can still walk Previous/Next over the diffs, then **Accept proposal**. Send an alternative instruction from the HUD to revise the remaining plan, or **Stop** to end the session.
|
|
57
|
+
|
|
58
|
+
The LLM never writes project files itself. It only publishes patch files. Those
|
|
59
|
+
stored patches are the source of truth and are applied on every step update.
|
|
60
|
+
|
|
61
|
+
<br clear="all" />
|
|
44
62
|
|
|
45
63
|
## Commands
|
|
46
64
|
|
|
@@ -52,6 +70,10 @@ inbase run --target /path/to/your/project
|
|
|
52
70
|
|
|
53
71
|
Session commands (`start-session`, `wait-for-blueprint`, `report-plan`, `wait-for-approval`, `propose-patch`) are used by the Cursor skill. You do not need to run them yourself.
|
|
54
72
|
|
|
73
|
+
## Editor support
|
|
74
|
+
|
|
75
|
+
The map runs in the browser. The LLM plan and patch loop currently works in **Cursor** only (`inbase init` installs a skill into `.cursor/skills/inbase/`).
|
|
76
|
+
|
|
55
77
|
## Language support
|
|
56
78
|
|
|
57
79
|
Source files (`.ts`, `.tsx`, `.js`, `.jsx`, `.mjs`, `.cjs`) become blocks with **functions**, **classes**, and **variables**, plus import edges from `import` and `require()`. Styles and docs (`.css`, `.scss`, `.html`, `.json`, `.md`) show up as blocks without symbols. Only relative specifiers (`./`, `../`) become edges — package names like `react` or `@angular/core` do not.
|
|
@@ -46,6 +46,7 @@ export type DiffManifest = {
|
|
|
46
46
|
export function assertSessionId(value: unknown): string
|
|
47
47
|
export function readActiveSession(dataDir: string): string | null
|
|
48
48
|
export function writeActiveSession(dataDir: string, sessionId: string | null): void
|
|
49
|
+
export function focusSession(dataDir: string, sessionId: string): string
|
|
49
50
|
export function touchSessionConnection(dataDir: string, sessionId: string): void
|
|
50
51
|
export function isSessionConnected(
|
|
51
52
|
dataDir: string,
|
|
@@ -62,6 +63,10 @@ export function discardInactiveDiffSessions(
|
|
|
62
63
|
targetRoot?: string | null,
|
|
63
64
|
waiterIds?: Iterable<string>,
|
|
64
65
|
): string[]
|
|
66
|
+
export function recoverOpenDiffSessions(
|
|
67
|
+
dataDir: string,
|
|
68
|
+
targetRoot?: string | null,
|
|
69
|
+
): string[]
|
|
65
70
|
export function clearDiffSessions(
|
|
66
71
|
dataDir: string,
|
|
67
72
|
targetRoot?: string | null,
|
|
@@ -209,6 +214,7 @@ export function requestReplan(
|
|
|
209
214
|
sessionId: string,
|
|
210
215
|
diffId: string,
|
|
211
216
|
instruction: string,
|
|
217
|
+
targetRoot?: string | null,
|
|
212
218
|
): DiffManifest
|
|
213
219
|
export function stopSession(
|
|
214
220
|
dataDir: string,
|
|
@@ -171,6 +171,16 @@ function isGeneratingPhase(phase) {
|
|
|
171
171
|
return phase === 'preparing' || phase === 'working' || phase === 'replanning'
|
|
172
172
|
}
|
|
173
173
|
|
|
174
|
+
function isTerminalSession(manifest) {
|
|
175
|
+
return (
|
|
176
|
+
!manifest ||
|
|
177
|
+
manifest.phase === 'finished' ||
|
|
178
|
+
manifest.phase === 'stopped' ||
|
|
179
|
+
manifest.status === 'finished' ||
|
|
180
|
+
manifest.status === 'rejected'
|
|
181
|
+
)
|
|
182
|
+
}
|
|
183
|
+
|
|
174
184
|
function isStalledWorking(manifest, waiterIds, sessionId, now = Date.now()) {
|
|
175
185
|
if (manifest.phase !== 'working') return false
|
|
176
186
|
if (!waiterIds.has(sessionId)) return false
|
|
@@ -185,15 +195,7 @@ export function isSessionConnected(
|
|
|
185
195
|
) {
|
|
186
196
|
const safeId = assertSessionId(sessionId)
|
|
187
197
|
const manifest = readManifest(dataDir, safeId)
|
|
188
|
-
if (
|
|
189
|
-
if (
|
|
190
|
-
manifest.phase === 'finished' ||
|
|
191
|
-
manifest.phase === 'stopped' ||
|
|
192
|
-
manifest.status === 'finished' ||
|
|
193
|
-
manifest.status === 'rejected'
|
|
194
|
-
) {
|
|
195
|
-
return false
|
|
196
|
-
}
|
|
198
|
+
if (isTerminalSession(manifest)) return false
|
|
197
199
|
if (isGeneratingPhase(manifest.phase)) return true
|
|
198
200
|
if (waiterIds.has(safeId)) return true
|
|
199
201
|
const connected = readJson(connectionFile(dataDir, safeId), null)
|
|
@@ -232,9 +234,8 @@ export function listOpenSessionIds(dataDir, waiterIds = waiterSessionIds()) {
|
|
|
232
234
|
if (!entry.isDirectory()) continue
|
|
233
235
|
try {
|
|
234
236
|
const sessionId = assertSessionId(entry.name)
|
|
235
|
-
if (!isSessionConnected(dataDir, sessionId, waiterIds)) continue
|
|
236
237
|
const manifest = readManifest(dataDir, sessionId)
|
|
237
|
-
if (
|
|
238
|
+
if (isTerminalSession(manifest)) continue
|
|
238
239
|
sessions.push({
|
|
239
240
|
sessionId,
|
|
240
241
|
createdAt: typeof manifest.createdAt === 'string' ? manifest.createdAt : '',
|
|
@@ -273,32 +274,34 @@ export function writeBlueprintSession(dataDir, sessionId) {
|
|
|
273
274
|
)
|
|
274
275
|
}
|
|
275
276
|
|
|
276
|
-
function
|
|
277
|
+
function releaseBlueprintSession(dataDir, sessionId) {
|
|
277
278
|
const safeId = assertSessionId(sessionId)
|
|
278
|
-
|
|
279
|
-
if (locked && locked !== safeId) {
|
|
280
|
-
throw new Error(
|
|
281
|
-
`Blueprint edit mode is active in session ${locked}. Finish or stop it before starting another.`,
|
|
282
|
-
)
|
|
283
|
-
}
|
|
284
|
-
return safeId
|
|
279
|
+
if (readBlueprintSession(dataDir) === safeId) writeBlueprintSession(dataDir, null)
|
|
285
280
|
}
|
|
286
281
|
|
|
287
|
-
function
|
|
288
|
-
const safeId =
|
|
289
|
-
|
|
282
|
+
export function focusSession(dataDir, sessionId) {
|
|
283
|
+
const safeId = assertSessionId(sessionId)
|
|
284
|
+
requireManifest(dataDir, safeId)
|
|
285
|
+
writeActiveSession(dataDir, safeId)
|
|
290
286
|
return safeId
|
|
291
287
|
}
|
|
292
288
|
|
|
293
|
-
function
|
|
294
|
-
|
|
295
|
-
|
|
289
|
+
function sessionAllowsPlacement(manifest) {
|
|
290
|
+
return (
|
|
291
|
+
manifest.phase !== 'blueprint_ask' &&
|
|
292
|
+
manifest.phase !== 'finished' &&
|
|
293
|
+
manifest.phase !== 'stopped'
|
|
294
|
+
)
|
|
296
295
|
}
|
|
297
296
|
|
|
298
|
-
function
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
297
|
+
function blueprintHasContent(blueprint) {
|
|
298
|
+
return (
|
|
299
|
+
(blueprint.userCreatedBlocks?.length ?? 0) > 0 ||
|
|
300
|
+
(blueprint.userCreatedIslands?.length ?? 0) > 0 ||
|
|
301
|
+
(blueprint.addedFunctions?.length ?? 0) > 0 ||
|
|
302
|
+
(blueprint.addedVariables?.length ?? 0) > 0 ||
|
|
303
|
+
(blueprint.addedImports?.length ?? 0) > 0
|
|
304
|
+
)
|
|
302
305
|
}
|
|
303
306
|
|
|
304
307
|
export function readManifest(dataDir, sessionId) {
|
|
@@ -353,13 +356,34 @@ export function chainThrough(manifest, diffId = manifest.activeDiffId) {
|
|
|
353
356
|
return manifest.diffs.slice(0, entryIndex(manifest, diffId) + 1)
|
|
354
357
|
}
|
|
355
358
|
|
|
356
|
-
function
|
|
359
|
+
function isSupersededPatch(entry) {
|
|
360
|
+
return (
|
|
361
|
+
entry.status === 'rejected' ||
|
|
362
|
+
entry.status === 'extend' ||
|
|
363
|
+
entry.status === 'extended'
|
|
364
|
+
)
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function liveEntries(manifest, diffId) {
|
|
357
368
|
const chain = chainThrough(manifest, diffId)
|
|
358
|
-
|
|
359
|
-
chain.
|
|
360
|
-
|
|
369
|
+
if (chain.length === 0) return []
|
|
370
|
+
const selected = chain.at(-1)
|
|
371
|
+
const browsingHistory = selected.id !== manifest.activeDiffId
|
|
372
|
+
return chain.filter((entry) => {
|
|
373
|
+
if (entry.status === 'rejected') return false
|
|
374
|
+
if (entry.status === 'extended' || entry.status === 'extend') {
|
|
375
|
+
return browsingHistory && entry.id === selected.id
|
|
376
|
+
}
|
|
377
|
+
return true
|
|
361
378
|
})
|
|
362
|
-
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function workingPatchEntries(manifest) {
|
|
382
|
+
return manifest.diffs.filter((entry) => !isSupersededPatch(entry))
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function unresolvedEntries(manifest, diffId = manifest.activeDiffId) {
|
|
386
|
+
return liveEntries(manifest, diffId).filter((entry) => entry.status !== 'applied')
|
|
363
387
|
}
|
|
364
388
|
|
|
365
389
|
export function previewPatchChain(patches, knownFileIds = []) {
|
|
@@ -444,10 +468,9 @@ export function sessionIntent(
|
|
|
444
468
|
const patches =
|
|
445
469
|
selectedIndex === null
|
|
446
470
|
? []
|
|
447
|
-
: manifest.
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
.map((entry) => readDiff(dataDir, sessionId, entry))
|
|
471
|
+
: liveEntries(manifest, selectedId).map((entry) =>
|
|
472
|
+
readDiff(dataDir, sessionId, entry),
|
|
473
|
+
)
|
|
451
474
|
const preview = previewPatchChain(patches, knownFileIds)
|
|
452
475
|
const activeView = !selectedDiffId || selectedId === manifest.activeDiffId
|
|
453
476
|
const phaseStatus = {
|
|
@@ -470,11 +493,7 @@ export function sessionIntent(
|
|
|
470
493
|
)
|
|
471
494
|
const previewVisible = patches.length > 0
|
|
472
495
|
const blueprint = readBlueprint(dataDir, sessionId)
|
|
473
|
-
const
|
|
474
|
-
const ownsBlueprintLock = blueprintSessionId === sessionId
|
|
475
|
-
const canEnterBlueprint =
|
|
476
|
-
manifest.phase === 'blueprint_ask' &&
|
|
477
|
-
(!blueprintSessionId || ownsBlueprintLock)
|
|
496
|
+
const canEnterBlueprint = manifest.phase === 'blueprint_ask'
|
|
478
497
|
|
|
479
498
|
return {
|
|
480
499
|
updatedAt: manifest.updatedAt,
|
|
@@ -504,9 +523,10 @@ export function sessionIntent(
|
|
|
504
523
|
manifest.phase === 'working' ||
|
|
505
524
|
manifest.phase === 'replanning',
|
|
506
525
|
stalledWait: isStalledWorking(manifest, waiterIds, sessionId),
|
|
507
|
-
|
|
526
|
+
llmIdle: !isSessionConnected(dataDir, sessionId, waiterIds),
|
|
527
|
+
creationMode: sessionAllowsPlacement(manifest),
|
|
508
528
|
canEnterBlueprint,
|
|
509
|
-
blueprintSessionId,
|
|
529
|
+
blueprintSessionId: null,
|
|
510
530
|
userCreatedBlocks: blueprint.userCreatedBlocks,
|
|
511
531
|
userCreatedIslands: blueprint.userCreatedIslands,
|
|
512
532
|
...preview,
|
|
@@ -639,10 +659,6 @@ function unstagePaths(fromDir, absolutePaths) {
|
|
|
639
659
|
}
|
|
640
660
|
}
|
|
641
661
|
|
|
642
|
-
function liveEntries(manifest, diffId) {
|
|
643
|
-
return chainThrough(manifest, diffId).filter((entry) => entry.status !== 'rejected')
|
|
644
|
-
}
|
|
645
|
-
|
|
646
662
|
export function materializeDiff(dataDir, targetRoot, sessionId, diffId) {
|
|
647
663
|
const manifest = requireManifest(dataDir, sessionId)
|
|
648
664
|
const through = diffId || manifest.activeDiffId
|
|
@@ -682,9 +698,9 @@ function loadReplayContents(dataDir, sessionId, targetRoot, patches) {
|
|
|
682
698
|
}
|
|
683
699
|
|
|
684
700
|
export function validateContinuation(dataDir, manifest, targetRoot, patchText) {
|
|
685
|
-
const prior = manifest.
|
|
686
|
-
|
|
687
|
-
|
|
701
|
+
const prior = workingPatchEntries(manifest).map((entry) =>
|
|
702
|
+
readDiff(dataDir, manifest.sessionId, entry),
|
|
703
|
+
)
|
|
688
704
|
const patches = [...prior, patchText]
|
|
689
705
|
const files = loadReplayContents(
|
|
690
706
|
dataDir,
|
|
@@ -791,7 +807,6 @@ export function answerBlueprint(dataDir, sessionId, enabled) {
|
|
|
791
807
|
if (manifest.phase !== 'blueprint_ask') {
|
|
792
808
|
throw new Error(`Session ${sessionId} is not asking for a blueprint`)
|
|
793
809
|
}
|
|
794
|
-
if (enabled) claimBlueprintSession(dataDir, sessionId)
|
|
795
810
|
const blueprint = {
|
|
796
811
|
...emptyBlueprint(),
|
|
797
812
|
enabled: Boolean(enabled),
|
|
@@ -807,20 +822,22 @@ export function answerBlueprint(dataDir, sessionId, enabled) {
|
|
|
807
822
|
export function updateBlueprint(dataDir, sessionId, input = {}) {
|
|
808
823
|
const safeId = assertSessionId(sessionId)
|
|
809
824
|
const manifest = requireManifest(dataDir, safeId)
|
|
810
|
-
if (manifest
|
|
811
|
-
throw new Error(`Session ${safeId} is not
|
|
825
|
+
if (!sessionAllowsPlacement(manifest)) {
|
|
826
|
+
throw new Error(`Session ${safeId} is not accepting blueprint edits`)
|
|
812
827
|
}
|
|
813
|
-
assertBlueprintSessionAvailable(dataDir, safeId)
|
|
814
828
|
const current = readBlueprint(dataDir, safeId)
|
|
815
|
-
|
|
829
|
+
const next = {
|
|
816
830
|
...current,
|
|
817
|
-
enabled: true,
|
|
818
|
-
sent: false,
|
|
819
831
|
userCreatedBlocks: input.userCreatedBlocks ?? current.userCreatedBlocks,
|
|
820
832
|
userCreatedIslands: input.userCreatedIslands ?? current.userCreatedIslands,
|
|
821
833
|
addedFunctions: input.addedFunctions ?? current.addedFunctions,
|
|
822
834
|
addedVariables: input.addedVariables ?? current.addedVariables,
|
|
823
835
|
addedImports: input.addedImports ?? current.addedImports,
|
|
836
|
+
}
|
|
837
|
+
writeBlueprint(dataDir, safeId, {
|
|
838
|
+
...next,
|
|
839
|
+
enabled: current.enabled || blueprintHasContent(next),
|
|
840
|
+
sent: manifest.phase === 'blueprint' ? false : current.sent,
|
|
824
841
|
})
|
|
825
842
|
return readBlueprint(dataDir, safeId)
|
|
826
843
|
}
|
|
@@ -831,7 +848,6 @@ export function sendBlueprint(dataDir, sessionId, input = {}) {
|
|
|
831
848
|
if (manifest.phase !== 'blueprint') {
|
|
832
849
|
throw new Error(`Session ${safeId} is not in blueprint mode`)
|
|
833
850
|
}
|
|
834
|
-
assertBlueprintSessionAvailable(dataDir, safeId)
|
|
835
851
|
const current = readBlueprint(dataDir, safeId)
|
|
836
852
|
writeBlueprint(dataDir, safeId, {
|
|
837
853
|
enabled: true,
|
|
@@ -919,8 +935,8 @@ export function invokeStep(dataDir, sessionId, step, targetRoot = null) {
|
|
|
919
935
|
if (step !== expected) {
|
|
920
936
|
throw new Error(
|
|
921
937
|
last
|
|
922
|
-
? `
|
|
923
|
-
: `
|
|
938
|
+
? `Accept proposal on step ${active.step} to finish`
|
|
939
|
+
: `Accept proposal on step ${active.step} to continue`,
|
|
924
940
|
)
|
|
925
941
|
}
|
|
926
942
|
applyUnresolved(dataDir, targetRoot, manifest, active.id)
|
|
@@ -1061,7 +1077,13 @@ export function continueDiff(dataDir, targetRoot, sessionId, diffId) {
|
|
|
1061
1077
|
return autoAdvance(dataDir, sessionId, targetRoot)
|
|
1062
1078
|
}
|
|
1063
1079
|
|
|
1064
|
-
export function requestReplan(
|
|
1080
|
+
export function requestReplan(
|
|
1081
|
+
dataDir,
|
|
1082
|
+
sessionId,
|
|
1083
|
+
diffId,
|
|
1084
|
+
instruction,
|
|
1085
|
+
targetRoot = null,
|
|
1086
|
+
) {
|
|
1065
1087
|
const manifest = requireManifest(dataDir, sessionId)
|
|
1066
1088
|
const active = pendingActive(manifest, diffId)
|
|
1067
1089
|
const guidance = typeof instruction === 'string' ? instruction.trim() : ''
|
|
@@ -1073,6 +1095,7 @@ export function requestReplan(dataDir, sessionId, diffId, instruction) {
|
|
|
1073
1095
|
manifest.pendingInstruction = guidance
|
|
1074
1096
|
manifest.workStartedAt = new Date().toISOString()
|
|
1075
1097
|
writeManifest(dataDir, manifest)
|
|
1098
|
+
if (targetRoot) materializeDiff(dataDir, targetRoot, sessionId, active.id)
|
|
1076
1099
|
return manifest
|
|
1077
1100
|
}
|
|
1078
1101
|
|
|
@@ -1139,13 +1162,16 @@ export function discardInactiveDiffSessions(
|
|
|
1139
1162
|
}
|
|
1140
1163
|
|
|
1141
1164
|
for (const sessionId of listStoredSessionIds(dataDir)) {
|
|
1142
|
-
const
|
|
1165
|
+
const manifest = readManifest(dataDir, sessionId)
|
|
1166
|
+
if (!isTerminalSession(manifest)) continue
|
|
1143
1167
|
const stopping = keep.has(sessionId) && isSessionStopped(dataDir, sessionId)
|
|
1144
|
-
if (
|
|
1168
|
+
if (stopping) continue
|
|
1145
1169
|
discardStoredSession(dataDir, sessionId, targetRoot)
|
|
1146
1170
|
}
|
|
1147
1171
|
|
|
1148
|
-
const liveIds =
|
|
1172
|
+
const liveIds = listStoredSessionIds(dataDir).filter(
|
|
1173
|
+
(id) => !isTerminalSession(readManifest(dataDir, id)),
|
|
1174
|
+
)
|
|
1149
1175
|
const active = readActiveSession(dataDir)
|
|
1150
1176
|
if (active && !liveIds.includes(active)) writeActiveSession(dataDir, null)
|
|
1151
1177
|
const locked = readBlueprintSession(dataDir)
|
|
@@ -1154,6 +1180,37 @@ export function discardInactiveDiffSessions(
|
|
|
1154
1180
|
return liveIds
|
|
1155
1181
|
}
|
|
1156
1182
|
|
|
1183
|
+
export function recoverOpenDiffSessions(dataDir, targetRoot = null) {
|
|
1184
|
+
const kept = []
|
|
1185
|
+
for (const sessionId of listStoredSessionIds(dataDir)) {
|
|
1186
|
+
const manifest = readManifest(dataDir, sessionId)
|
|
1187
|
+
if (!isTerminalSession(manifest)) {
|
|
1188
|
+
if (targetRoot && manifest.activeDiffId) {
|
|
1189
|
+
try {
|
|
1190
|
+
materializeDiff(dataDir, targetRoot, sessionId, manifest.activeDiffId)
|
|
1191
|
+
} catch {
|
|
1192
|
+
// Keep the stored diffs visible even if disk replay fails.
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
kept.push(sessionId)
|
|
1196
|
+
continue
|
|
1197
|
+
}
|
|
1198
|
+
discardStoredSession(dataDir, sessionId, targetRoot, {
|
|
1199
|
+
restore:
|
|
1200
|
+
manifest?.phase === 'stopped' ||
|
|
1201
|
+
manifest?.status === 'rejected' ||
|
|
1202
|
+
!manifest,
|
|
1203
|
+
})
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
const active = readActiveSession(dataDir)
|
|
1207
|
+
if (active && !kept.includes(active)) writeActiveSession(dataDir, null)
|
|
1208
|
+
const locked = readBlueprintSession(dataDir)
|
|
1209
|
+
if (locked && !kept.includes(locked)) writeBlueprintSession(dataDir, null)
|
|
1210
|
+
unstageDiffSessionArtifacts(dataDir, targetRoot)
|
|
1211
|
+
return kept
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1157
1214
|
export function clearDiffSessions(dataDir, targetRoot = null) {
|
|
1158
1215
|
for (const sessionId of listStoredSessionIds(dataDir)) {
|
|
1159
1216
|
discardStoredSession(dataDir, sessionId, targetRoot)
|
|
@@ -1192,7 +1249,7 @@ export function decideDiff(
|
|
|
1192
1249
|
return continueDiff(dataDir, targetRoot, sessionId, diffId)
|
|
1193
1250
|
}
|
|
1194
1251
|
if (decision === 'extend') {
|
|
1195
|
-
return requestReplan(dataDir, sessionId, diffId, instruction)
|
|
1252
|
+
return requestReplan(dataDir, sessionId, diffId, instruction, targetRoot)
|
|
1196
1253
|
}
|
|
1197
1254
|
return stopSession(dataDir, sessionId, targetRoot)
|
|
1198
1255
|
}
|