@jkwd/inbase 0.1.9 → 0.1.11
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/LICENSE +21 -0
- package/README.md +23 -7
- package/apps/explorer/package.json +1 -0
- package/apps/explorer/scripts/branch-changes.d.ts +24 -0
- package/apps/explorer/scripts/branch-changes.mjs +237 -0
- package/apps/explorer/scripts/js-source.mjs +12 -15
- package/apps/explorer/scripts/patch-lib.d.ts +6 -0
- package/apps/explorer/scripts/patch-lib.mjs +6 -0
- package/apps/explorer/scripts/scan-target.mjs +46 -13
- package/apps/explorer/scripts/session-store.d.ts +64 -1
- package/apps/explorer/scripts/session-store.mjs +369 -88
- package/apps/explorer/scripts/tree-diff.mjs +121 -0
- package/apps/explorer/src/App.tsx +215 -86
- package/apps/explorer/src/agentIntent.ts +78 -0
- package/apps/explorer/src/branchChanges.ts +54 -0
- package/apps/explorer/src/index.css +152 -10
- package/apps/explorer/src/scene/FileBlock.tsx +55 -5
- package/apps/explorer/src/types.ts +46 -0
- package/apps/explorer/src/ui/HUD.tsx +670 -158
- package/apps/explorer/src/userContext.ts +11 -0
- package/apps/explorer/vite.config.ts +65 -5
- package/bin/inbase.mjs +23 -4
- package/bin/project.mjs +62 -4
- package/bin/session.mjs +215 -125
- package/package.json +20 -2
- package/skill/commands/inbase.md +17 -0
- package/skill/commands/skipinbase.md +9 -0
- package/skill/inbase/SKILL.md +142 -89
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from 'node:fs'
|
|
2
2
|
import path from 'node:path'
|
|
3
|
+
import crypto from 'node:crypto'
|
|
3
4
|
import { spawnSync } from 'node:child_process'
|
|
4
5
|
import {
|
|
5
6
|
accumulatePatchAdditions,
|
|
@@ -10,6 +11,7 @@ import {
|
|
|
10
11
|
foldersFromFileIds,
|
|
11
12
|
parseUnifiedPatch,
|
|
12
13
|
} from './patch-lib.mjs'
|
|
14
|
+
import { diffSourceTrees, snapshotSourceTree } from './tree-diff.mjs'
|
|
13
15
|
|
|
14
16
|
const SESSION_ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/
|
|
15
17
|
const CONNECTED_TTL_MS = 15_000
|
|
@@ -39,6 +41,19 @@ function atomicWrite(file, contents) {
|
|
|
39
41
|
fs.renameSync(temporary, file)
|
|
40
42
|
}
|
|
41
43
|
|
|
44
|
+
function featureName(value) {
|
|
45
|
+
const trimmed = typeof value === 'string' ? value.trim() : ''
|
|
46
|
+
return trimmed
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function sessionName(value) {
|
|
50
|
+
return featureName(value)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function resolvedSessionName(manifest) {
|
|
54
|
+
return sessionName(manifest?.name) || sessionName(manifest?.feature)
|
|
55
|
+
}
|
|
56
|
+
|
|
42
57
|
export function sessionPaths(dataDir, sessionId) {
|
|
43
58
|
const safeId = assertSessionId(sessionId)
|
|
44
59
|
const root = path.join(dataDir, 'diff-sessions', safeId)
|
|
@@ -49,6 +64,7 @@ export function sessionPaths(dataDir, sessionId) {
|
|
|
49
64
|
blueprint: path.join(root, 'blueprint.json'),
|
|
50
65
|
baseline: path.join(root, 'baseline.json'),
|
|
51
66
|
baselineFiles: path.join(root, 'baseline'),
|
|
67
|
+
preStep: path.join(root, 'pre-step'),
|
|
52
68
|
stopped: path.join(dataDir, 'diff-sessions', `${safeId}.stopped`),
|
|
53
69
|
}
|
|
54
70
|
}
|
|
@@ -124,6 +140,38 @@ function connectionFile(dataDir, sessionId) {
|
|
|
124
140
|
return path.join(sessionPaths(dataDir, sessionId).root, 'connected.json')
|
|
125
141
|
}
|
|
126
142
|
|
|
143
|
+
function ackFile(dataDir, sessionId) {
|
|
144
|
+
return path.join(sessionPaths(dataDir, sessionId).root, 'ack.json')
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function recordSessionAck(dataDir, sessionId, kind, detail = '') {
|
|
148
|
+
const safeId = assertSessionId(sessionId)
|
|
149
|
+
if (isSessionStopped(dataDir, safeId) && kind !== 'stopped' && kind !== 'finished') {
|
|
150
|
+
return null
|
|
151
|
+
}
|
|
152
|
+
const payload = {
|
|
153
|
+
kind: String(kind),
|
|
154
|
+
detail: String(detail ?? ''),
|
|
155
|
+
at: new Date().toISOString(),
|
|
156
|
+
}
|
|
157
|
+
try {
|
|
158
|
+
atomicWrite(ackFile(dataDir, safeId), `${JSON.stringify(payload, null, 2)}\n`)
|
|
159
|
+
} catch {
|
|
160
|
+
return null
|
|
161
|
+
}
|
|
162
|
+
return payload
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function readSessionAck(dataDir, sessionId) {
|
|
166
|
+
const value = readJson(ackFile(dataDir, sessionId), null)
|
|
167
|
+
if (!value || typeof value.kind !== 'string' || value.kind.trim() === '') return null
|
|
168
|
+
return {
|
|
169
|
+
kind: value.kind,
|
|
170
|
+
detail: typeof value.detail === 'string' ? value.detail : '',
|
|
171
|
+
at: typeof value.at === 'string' ? value.at : null,
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
127
175
|
function isFreshTimestamp(value, now = Date.now()) {
|
|
128
176
|
if (typeof value !== 'string') return false
|
|
129
177
|
const at = Date.parse(value)
|
|
@@ -137,6 +185,11 @@ export function touchSessionConnection(dataDir, sessionId) {
|
|
|
137
185
|
connectionFile(dataDir, safeId),
|
|
138
186
|
`${JSON.stringify({ sessionId: safeId, connectedAt: new Date().toISOString() }, null, 2)}\n`,
|
|
139
187
|
)
|
|
188
|
+
const manifest = readManifest(dataDir, safeId)
|
|
189
|
+
if (manifest?.awaitingAttach) {
|
|
190
|
+
manifest.awaitingAttach = false
|
|
191
|
+
writeManifest(dataDir, manifest)
|
|
192
|
+
}
|
|
140
193
|
}
|
|
141
194
|
|
|
142
195
|
function waiterSessionIds() {
|
|
@@ -171,6 +224,16 @@ function isGeneratingPhase(phase) {
|
|
|
171
224
|
return phase === 'preparing' || phase === 'working' || phase === 'replanning'
|
|
172
225
|
}
|
|
173
226
|
|
|
227
|
+
function isTerminalSession(manifest) {
|
|
228
|
+
return (
|
|
229
|
+
!manifest ||
|
|
230
|
+
manifest.phase === 'finished' ||
|
|
231
|
+
manifest.phase === 'stopped' ||
|
|
232
|
+
manifest.status === 'finished' ||
|
|
233
|
+
manifest.status === 'rejected'
|
|
234
|
+
)
|
|
235
|
+
}
|
|
236
|
+
|
|
174
237
|
function isStalledWorking(manifest, waiterIds, sessionId, now = Date.now()) {
|
|
175
238
|
if (manifest.phase !== 'working') return false
|
|
176
239
|
if (!waiterIds.has(sessionId)) return false
|
|
@@ -185,19 +248,13 @@ export function isSessionConnected(
|
|
|
185
248
|
) {
|
|
186
249
|
const safeId = assertSessionId(sessionId)
|
|
187
250
|
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
|
-
}
|
|
197
|
-
if (isGeneratingPhase(manifest.phase)) return true
|
|
198
|
-
if (waiterIds.has(safeId)) return true
|
|
251
|
+
if (isTerminalSession(manifest)) return false
|
|
199
252
|
const connected = readJson(connectionFile(dataDir, safeId), null)
|
|
200
|
-
|
|
253
|
+
const heartbeat =
|
|
254
|
+
waiterIds.has(safeId) || isFreshTimestamp(connected?.connectedAt)
|
|
255
|
+
if (manifest.awaitingAttach) return heartbeat
|
|
256
|
+
if (isGeneratingPhase(manifest.phase)) return true
|
|
257
|
+
if (heartbeat) return true
|
|
201
258
|
return isFreshTimestamp(manifest.updatedAt) || isFreshTimestamp(manifest.createdAt)
|
|
202
259
|
}
|
|
203
260
|
|
|
@@ -232,9 +289,8 @@ export function listOpenSessionIds(dataDir, waiterIds = waiterSessionIds()) {
|
|
|
232
289
|
if (!entry.isDirectory()) continue
|
|
233
290
|
try {
|
|
234
291
|
const sessionId = assertSessionId(entry.name)
|
|
235
|
-
if (!isSessionConnected(dataDir, sessionId, waiterIds)) continue
|
|
236
292
|
const manifest = readManifest(dataDir, sessionId)
|
|
237
|
-
if (
|
|
293
|
+
if (isTerminalSession(manifest)) continue
|
|
238
294
|
sessions.push({
|
|
239
295
|
sessionId,
|
|
240
296
|
createdAt: typeof manifest.createdAt === 'string' ? manifest.createdAt : '',
|
|
@@ -273,32 +329,34 @@ export function writeBlueprintSession(dataDir, sessionId) {
|
|
|
273
329
|
)
|
|
274
330
|
}
|
|
275
331
|
|
|
276
|
-
function
|
|
332
|
+
function releaseBlueprintSession(dataDir, sessionId) {
|
|
277
333
|
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
|
|
334
|
+
if (readBlueprintSession(dataDir) === safeId) writeBlueprintSession(dataDir, null)
|
|
285
335
|
}
|
|
286
336
|
|
|
287
|
-
function
|
|
288
|
-
const safeId =
|
|
289
|
-
|
|
337
|
+
export function focusSession(dataDir, sessionId) {
|
|
338
|
+
const safeId = assertSessionId(sessionId)
|
|
339
|
+
requireManifest(dataDir, safeId)
|
|
340
|
+
writeActiveSession(dataDir, safeId)
|
|
290
341
|
return safeId
|
|
291
342
|
}
|
|
292
343
|
|
|
293
|
-
function
|
|
294
|
-
|
|
295
|
-
|
|
344
|
+
function sessionAllowsPlacement(manifest) {
|
|
345
|
+
return (
|
|
346
|
+
manifest.phase !== 'blueprint_ask' &&
|
|
347
|
+
manifest.phase !== 'finished' &&
|
|
348
|
+
manifest.phase !== 'stopped'
|
|
349
|
+
)
|
|
296
350
|
}
|
|
297
351
|
|
|
298
|
-
function
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
352
|
+
function blueprintHasContent(blueprint) {
|
|
353
|
+
return (
|
|
354
|
+
(blueprint.userCreatedBlocks?.length ?? 0) > 0 ||
|
|
355
|
+
(blueprint.userCreatedIslands?.length ?? 0) > 0 ||
|
|
356
|
+
(blueprint.addedFunctions?.length ?? 0) > 0 ||
|
|
357
|
+
(blueprint.addedVariables?.length ?? 0) > 0 ||
|
|
358
|
+
(blueprint.addedImports?.length ?? 0) > 0
|
|
359
|
+
)
|
|
302
360
|
}
|
|
303
361
|
|
|
304
362
|
export function readManifest(dataDir, sessionId) {
|
|
@@ -324,6 +382,8 @@ export function readManifest(dataDir, sessionId) {
|
|
|
324
382
|
value.workStartedAt ??= null
|
|
325
383
|
}
|
|
326
384
|
if (typeof value.stepByStep !== 'boolean') value.stepByStep = true
|
|
385
|
+
value.initialInstruction =
|
|
386
|
+
typeof value.initialInstruction === 'string' ? value.initialInstruction : null
|
|
327
387
|
return value
|
|
328
388
|
}
|
|
329
389
|
|
|
@@ -353,13 +413,34 @@ export function chainThrough(manifest, diffId = manifest.activeDiffId) {
|
|
|
353
413
|
return manifest.diffs.slice(0, entryIndex(manifest, diffId) + 1)
|
|
354
414
|
}
|
|
355
415
|
|
|
356
|
-
function
|
|
416
|
+
function isSupersededPatch(entry) {
|
|
417
|
+
return (
|
|
418
|
+
entry.status === 'rejected' ||
|
|
419
|
+
entry.status === 'extend' ||
|
|
420
|
+
entry.status === 'extended'
|
|
421
|
+
)
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function liveEntries(manifest, diffId) {
|
|
357
425
|
const chain = chainThrough(manifest, diffId)
|
|
358
|
-
|
|
359
|
-
chain.
|
|
360
|
-
|
|
426
|
+
if (chain.length === 0) return []
|
|
427
|
+
const selected = chain.at(-1)
|
|
428
|
+
const browsingHistory = selected.id !== manifest.activeDiffId
|
|
429
|
+
return chain.filter((entry) => {
|
|
430
|
+
if (entry.status === 'rejected') return false
|
|
431
|
+
if (entry.status === 'extended' || entry.status === 'extend') {
|
|
432
|
+
return browsingHistory && entry.id === selected.id
|
|
433
|
+
}
|
|
434
|
+
return true
|
|
361
435
|
})
|
|
362
|
-
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function workingPatchEntries(manifest) {
|
|
439
|
+
return manifest.diffs.filter((entry) => !isSupersededPatch(entry))
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function unresolvedEntries(manifest, diffId = manifest.activeDiffId) {
|
|
443
|
+
return liveEntries(manifest, diffId).filter((entry) => entry.status !== 'applied')
|
|
363
444
|
}
|
|
364
445
|
|
|
365
446
|
export function previewPatchChain(patches, knownFileIds = []) {
|
|
@@ -444,10 +525,9 @@ export function sessionIntent(
|
|
|
444
525
|
const patches =
|
|
445
526
|
selectedIndex === null
|
|
446
527
|
? []
|
|
447
|
-
: manifest.
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
.map((entry) => readDiff(dataDir, sessionId, entry))
|
|
528
|
+
: liveEntries(manifest, selectedId).map((entry) =>
|
|
529
|
+
readDiff(dataDir, sessionId, entry),
|
|
530
|
+
)
|
|
451
531
|
const preview = previewPatchChain(patches, knownFileIds)
|
|
452
532
|
const activeView = !selectedDiffId || selectedId === manifest.activeDiffId
|
|
453
533
|
const phaseStatus = {
|
|
@@ -470,18 +550,19 @@ export function sessionIntent(
|
|
|
470
550
|
)
|
|
471
551
|
const previewVisible = patches.length > 0
|
|
472
552
|
const blueprint = readBlueprint(dataDir, sessionId)
|
|
473
|
-
const
|
|
474
|
-
const ownsBlueprintLock = blueprintSessionId === sessionId
|
|
475
|
-
const canEnterBlueprint =
|
|
476
|
-
manifest.phase === 'blueprint_ask' &&
|
|
477
|
-
(!blueprintSessionId || ownsBlueprintLock)
|
|
553
|
+
const canEnterBlueprint = manifest.phase === 'blueprint_ask'
|
|
478
554
|
|
|
479
555
|
return {
|
|
480
556
|
updatedAt: manifest.updatedAt,
|
|
481
557
|
showMap: previewVisible,
|
|
482
558
|
status: activeView ? phaseStatus ?? historicalStatus : historicalStatus,
|
|
483
559
|
phase: manifest.phase,
|
|
560
|
+
name: resolvedSessionName(manifest) || null,
|
|
484
561
|
feature: manifest.feature,
|
|
562
|
+
initialInstruction:
|
|
563
|
+
typeof manifest.initialInstruction === 'string'
|
|
564
|
+
? manifest.initialInstruction
|
|
565
|
+
: null,
|
|
485
566
|
steps: manifest.steps,
|
|
486
567
|
step: activeView ? manifest.currentStep : selected?.step ?? manifest.currentStep,
|
|
487
568
|
stepByStep: isStepByStep(manifest),
|
|
@@ -500,13 +581,20 @@ export function sessionIntent(
|
|
|
500
581
|
isActiveDiff: Boolean(selected && selected.id === manifest.activeDiffId),
|
|
501
582
|
preview: previewVisible,
|
|
502
583
|
working:
|
|
503
|
-
manifest.
|
|
504
|
-
manifest.phase === '
|
|
505
|
-
|
|
584
|
+
!manifest.awaitingAttach &&
|
|
585
|
+
(manifest.phase === 'preparing' ||
|
|
586
|
+
manifest.phase === 'working' ||
|
|
587
|
+
manifest.phase === 'replanning'),
|
|
506
588
|
stalledWait: isStalledWorking(manifest, waiterIds, sessionId),
|
|
507
|
-
|
|
589
|
+
llmIdle: !isSessionConnected(dataDir, sessionId, waiterIds),
|
|
590
|
+
awaitingAttach:
|
|
591
|
+
Boolean(manifest.awaitingAttach) &&
|
|
592
|
+
!isSessionConnected(dataDir, sessionId, waiterIds),
|
|
593
|
+
listening: waiterIds.has(sessionId),
|
|
594
|
+
lastAck: readSessionAck(dataDir, sessionId),
|
|
595
|
+
creationMode: sessionAllowsPlacement(manifest),
|
|
508
596
|
canEnterBlueprint,
|
|
509
|
-
blueprintSessionId,
|
|
597
|
+
blueprintSessionId: null,
|
|
510
598
|
userCreatedBlocks: blueprint.userCreatedBlocks,
|
|
511
599
|
userCreatedIslands: blueprint.userCreatedIslands,
|
|
512
600
|
...preview,
|
|
@@ -639,10 +727,6 @@ function unstagePaths(fromDir, absolutePaths) {
|
|
|
639
727
|
}
|
|
640
728
|
}
|
|
641
729
|
|
|
642
|
-
function liveEntries(manifest, diffId) {
|
|
643
|
-
return chainThrough(manifest, diffId).filter((entry) => entry.status !== 'rejected')
|
|
644
|
-
}
|
|
645
|
-
|
|
646
730
|
export function materializeDiff(dataDir, targetRoot, sessionId, diffId) {
|
|
647
731
|
const manifest = requireManifest(dataDir, sessionId)
|
|
648
732
|
const through = diffId || manifest.activeDiffId
|
|
@@ -682,9 +766,9 @@ function loadReplayContents(dataDir, sessionId, targetRoot, patches) {
|
|
|
682
766
|
}
|
|
683
767
|
|
|
684
768
|
export function validateContinuation(dataDir, manifest, targetRoot, patchText) {
|
|
685
|
-
const prior = manifest.
|
|
686
|
-
|
|
687
|
-
|
|
769
|
+
const prior = workingPatchEntries(manifest).map((entry) =>
|
|
770
|
+
readDiff(dataDir, manifest.sessionId, entry),
|
|
771
|
+
)
|
|
688
772
|
const patches = [...prior, patchText]
|
|
689
773
|
const files = loadReplayContents(
|
|
690
774
|
dataDir,
|
|
@@ -722,11 +806,6 @@ function planSteps(titles, startAt = 1) {
|
|
|
722
806
|
})
|
|
723
807
|
}
|
|
724
808
|
|
|
725
|
-
function featureName(value) {
|
|
726
|
-
const trimmed = typeof value === 'string' ? value.trim() : ''
|
|
727
|
-
return trimmed
|
|
728
|
-
}
|
|
729
|
-
|
|
730
809
|
export function isStepByStep(manifest) {
|
|
731
810
|
return manifest?.stepByStep !== false
|
|
732
811
|
}
|
|
@@ -758,7 +837,12 @@ export function startSession(dataDir, input) {
|
|
|
758
837
|
const sessionId = assertSessionId(input.sessionId)
|
|
759
838
|
clearStoppedMarker(dataDir, sessionId)
|
|
760
839
|
const existing = readManifest(dataDir, sessionId)
|
|
840
|
+
const name = sessionName(input.name) || sessionName(input.feature)
|
|
761
841
|
if (existing) {
|
|
842
|
+
if (name && existing.name !== name) {
|
|
843
|
+
existing.name = name
|
|
844
|
+
writeManifest(dataDir, existing)
|
|
845
|
+
}
|
|
762
846
|
focusSession(dataDir, sessionId)
|
|
763
847
|
return existing
|
|
764
848
|
}
|
|
@@ -767,7 +851,8 @@ export function startSession(dataDir, input) {
|
|
|
767
851
|
const manifest = {
|
|
768
852
|
version: 2,
|
|
769
853
|
sessionId,
|
|
770
|
-
|
|
854
|
+
name,
|
|
855
|
+
feature: featureName(input.feature) || name,
|
|
771
856
|
steps: [],
|
|
772
857
|
status: 'active',
|
|
773
858
|
phase: 'blueprint_ask',
|
|
@@ -775,6 +860,7 @@ export function startSession(dataDir, input) {
|
|
|
775
860
|
currentStep: 1,
|
|
776
861
|
activeDiffId: null,
|
|
777
862
|
pendingInstruction: null,
|
|
863
|
+
initialInstruction: null,
|
|
778
864
|
workStartedAt: null,
|
|
779
865
|
createdAt: now,
|
|
780
866
|
updatedAt: now,
|
|
@@ -786,12 +872,120 @@ export function startSession(dataDir, input) {
|
|
|
786
872
|
return manifest
|
|
787
873
|
}
|
|
788
874
|
|
|
875
|
+
export function setupSession(dataDir, input = {}) {
|
|
876
|
+
const sessionId = input.sessionId
|
|
877
|
+
? assertSessionId(input.sessionId)
|
|
878
|
+
: generateVisualizerSessionId(dataDir)
|
|
879
|
+
const existing = readManifest(dataDir, sessionId)
|
|
880
|
+
if (existing && !isTerminalSession(existing)) {
|
|
881
|
+
throw new Error(`Session ${sessionId} already exists`)
|
|
882
|
+
}
|
|
883
|
+
if (existing) {
|
|
884
|
+
discardStoredSession(dataDir, sessionId, null, { restore: false })
|
|
885
|
+
}
|
|
886
|
+
clearStoppedMarker(dataDir, sessionId)
|
|
887
|
+
const now = new Date().toISOString()
|
|
888
|
+
const name = sessionName(input.name)
|
|
889
|
+
const manifest = {
|
|
890
|
+
version: 2,
|
|
891
|
+
sessionId,
|
|
892
|
+
name,
|
|
893
|
+
feature: featureName(input.feature) || name,
|
|
894
|
+
steps: [],
|
|
895
|
+
status: 'active',
|
|
896
|
+
phase: 'blueprint',
|
|
897
|
+
awaitingAttach: true,
|
|
898
|
+
stepByStep: true,
|
|
899
|
+
currentStep: 1,
|
|
900
|
+
activeDiffId: null,
|
|
901
|
+
pendingInstruction: null,
|
|
902
|
+
initialInstruction: null,
|
|
903
|
+
workStartedAt: null,
|
|
904
|
+
createdAt: now,
|
|
905
|
+
updatedAt: now,
|
|
906
|
+
diffs: [],
|
|
907
|
+
}
|
|
908
|
+
writeManifest(dataDir, manifest)
|
|
909
|
+
writeBlueprint(dataDir, sessionId, {
|
|
910
|
+
...emptyBlueprint(),
|
|
911
|
+
enabled: true,
|
|
912
|
+
sent: false,
|
|
913
|
+
})
|
|
914
|
+
focusSession(dataDir, sessionId)
|
|
915
|
+
return manifest
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
export function setInitialInstruction(dataDir, sessionId, instruction) {
|
|
919
|
+
const manifest = requireManifest(dataDir, sessionId)
|
|
920
|
+
if (isTerminalSession(manifest)) {
|
|
921
|
+
throw sessionStoppedError(sessionId)
|
|
922
|
+
}
|
|
923
|
+
const text = typeof instruction === 'string' ? instruction : ''
|
|
924
|
+
if (text.length > 4000) {
|
|
925
|
+
throw new Error('instruction must be a string up to 4000 characters')
|
|
926
|
+
}
|
|
927
|
+
const next = text.trim() === '' ? null : text
|
|
928
|
+
if ((manifest.initialInstruction ?? null) === next) return manifest
|
|
929
|
+
manifest.initialInstruction = next
|
|
930
|
+
writeManifest(dataDir, manifest)
|
|
931
|
+
return manifest
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
function generateVisualizerSessionId(dataDir) {
|
|
935
|
+
for (let attempt = 0; attempt < 8; attempt += 1) {
|
|
936
|
+
const sessionId = `viz-${crypto.randomBytes(6).toString('hex')}`
|
|
937
|
+
if (!readManifest(dataDir, sessionId) && !isSessionStopped(dataDir, sessionId)) {
|
|
938
|
+
return sessionId
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
throw new Error('Could not allocate a visualizer session id')
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
export function readAttachedSession(dataDir) {
|
|
945
|
+
for (const sessionId of listOpenSessionIds(dataDir)) {
|
|
946
|
+
const manifest = readManifest(dataDir, sessionId)
|
|
947
|
+
if (manifest?.awaitingAttach === false) return sessionId
|
|
948
|
+
}
|
|
949
|
+
return null
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
export function attachSession(dataDir, sessionId) {
|
|
953
|
+
const safeId = sessionId
|
|
954
|
+
? assertSessionId(sessionId)
|
|
955
|
+
: readActiveSession(dataDir)
|
|
956
|
+
if (!safeId) {
|
|
957
|
+
throw new Error(
|
|
958
|
+
'No visualizer session is focused. Click Setup LLM session in the map, then /inbase.',
|
|
959
|
+
)
|
|
960
|
+
}
|
|
961
|
+
const manifest = requireManifest(
|
|
962
|
+
dataDir,
|
|
963
|
+
safeId,
|
|
964
|
+
`No visualizer session ${safeId}. Click Setup LLM session in the map, then /inbase.`,
|
|
965
|
+
)
|
|
966
|
+
if (isTerminalSession(manifest)) {
|
|
967
|
+
throw sessionStoppedError(safeId)
|
|
968
|
+
}
|
|
969
|
+
const attached = readAttachedSession(dataDir)
|
|
970
|
+
if (attached && attached !== safeId) {
|
|
971
|
+
const other = readManifest(dataDir, attached)
|
|
972
|
+
const label = resolvedSessionName(other) || attached
|
|
973
|
+
throw new Error(
|
|
974
|
+
`An LLM is already attached to ${label}. Stop that session before attaching another.`,
|
|
975
|
+
)
|
|
976
|
+
}
|
|
977
|
+
focusSession(dataDir, safeId)
|
|
978
|
+
touchSessionConnection(dataDir, safeId)
|
|
979
|
+
recordSessionAck(dataDir, safeId, 'attached', resolvedSessionName(manifest) || safeId)
|
|
980
|
+
maybeStartVisualizerHandshake(dataDir, safeId)
|
|
981
|
+
return readManifest(dataDir, safeId) ?? manifest
|
|
982
|
+
}
|
|
983
|
+
|
|
789
984
|
export function answerBlueprint(dataDir, sessionId, enabled) {
|
|
790
985
|
const manifest = requireManifest(dataDir, sessionId)
|
|
791
986
|
if (manifest.phase !== 'blueprint_ask') {
|
|
792
987
|
throw new Error(`Session ${sessionId} is not asking for a blueprint`)
|
|
793
988
|
}
|
|
794
|
-
if (enabled) claimBlueprintSession(dataDir, sessionId)
|
|
795
989
|
const blueprint = {
|
|
796
990
|
...emptyBlueprint(),
|
|
797
991
|
enabled: Boolean(enabled),
|
|
@@ -807,20 +1001,22 @@ export function answerBlueprint(dataDir, sessionId, enabled) {
|
|
|
807
1001
|
export function updateBlueprint(dataDir, sessionId, input = {}) {
|
|
808
1002
|
const safeId = assertSessionId(sessionId)
|
|
809
1003
|
const manifest = requireManifest(dataDir, safeId)
|
|
810
|
-
if (manifest
|
|
811
|
-
throw new Error(`Session ${safeId} is not
|
|
1004
|
+
if (!sessionAllowsPlacement(manifest)) {
|
|
1005
|
+
throw new Error(`Session ${safeId} is not accepting blueprint edits`)
|
|
812
1006
|
}
|
|
813
|
-
assertBlueprintSessionAvailable(dataDir, safeId)
|
|
814
1007
|
const current = readBlueprint(dataDir, safeId)
|
|
815
|
-
|
|
1008
|
+
const next = {
|
|
816
1009
|
...current,
|
|
817
|
-
enabled: true,
|
|
818
|
-
sent: false,
|
|
819
1010
|
userCreatedBlocks: input.userCreatedBlocks ?? current.userCreatedBlocks,
|
|
820
1011
|
userCreatedIslands: input.userCreatedIslands ?? current.userCreatedIslands,
|
|
821
1012
|
addedFunctions: input.addedFunctions ?? current.addedFunctions,
|
|
822
1013
|
addedVariables: input.addedVariables ?? current.addedVariables,
|
|
823
1014
|
addedImports: input.addedImports ?? current.addedImports,
|
|
1015
|
+
}
|
|
1016
|
+
writeBlueprint(dataDir, safeId, {
|
|
1017
|
+
...next,
|
|
1018
|
+
enabled: current.enabled || blueprintHasContent(next),
|
|
1019
|
+
sent: manifest.phase === 'blueprint' ? false : current.sent,
|
|
824
1020
|
})
|
|
825
1021
|
return readBlueprint(dataDir, safeId)
|
|
826
1022
|
}
|
|
@@ -831,16 +1027,37 @@ export function sendBlueprint(dataDir, sessionId, input = {}) {
|
|
|
831
1027
|
if (manifest.phase !== 'blueprint') {
|
|
832
1028
|
throw new Error(`Session ${safeId} is not in blueprint mode`)
|
|
833
1029
|
}
|
|
834
|
-
assertBlueprintSessionAvailable(dataDir, safeId)
|
|
835
1030
|
const current = readBlueprint(dataDir, safeId)
|
|
836
|
-
|
|
837
|
-
enabled: true,
|
|
838
|
-
sent: true,
|
|
1031
|
+
const next = {
|
|
839
1032
|
userCreatedBlocks: input.userCreatedBlocks ?? current.userCreatedBlocks,
|
|
840
1033
|
userCreatedIslands: input.userCreatedIslands ?? current.userCreatedIslands,
|
|
841
1034
|
addedFunctions: input.addedFunctions ?? current.addedFunctions,
|
|
842
1035
|
addedVariables: input.addedVariables ?? current.addedVariables,
|
|
843
1036
|
addedImports: input.addedImports ?? current.addedImports,
|
|
1037
|
+
}
|
|
1038
|
+
writeBlueprint(dataDir, safeId, {
|
|
1039
|
+
...next,
|
|
1040
|
+
enabled: blueprintHasContent(next),
|
|
1041
|
+
sent: true,
|
|
1042
|
+
})
|
|
1043
|
+
manifest.phase = 'preparing'
|
|
1044
|
+
manifest.workStartedAt = new Date().toISOString()
|
|
1045
|
+
writeManifest(dataDir, manifest)
|
|
1046
|
+
releaseBlueprintSession(dataDir, safeId)
|
|
1047
|
+
return manifest
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
export function maybeStartVisualizerHandshake(dataDir, sessionId) {
|
|
1051
|
+
const safeId = assertSessionId(sessionId)
|
|
1052
|
+
const manifest = requireManifest(dataDir, safeId)
|
|
1053
|
+
if (manifest.phase !== 'blueprint_ask' && manifest.phase !== 'blueprint') {
|
|
1054
|
+
return manifest
|
|
1055
|
+
}
|
|
1056
|
+
const current = readBlueprint(dataDir, safeId)
|
|
1057
|
+
writeBlueprint(dataDir, safeId, {
|
|
1058
|
+
...current,
|
|
1059
|
+
enabled: blueprintHasContent(current),
|
|
1060
|
+
sent: true,
|
|
844
1061
|
})
|
|
845
1062
|
manifest.phase = 'preparing'
|
|
846
1063
|
manifest.workStartedAt = new Date().toISOString()
|
|
@@ -867,6 +1084,7 @@ export function reportPlan(dataDir, input) {
|
|
|
867
1084
|
const manifest = existing ?? {
|
|
868
1085
|
version: 2,
|
|
869
1086
|
sessionId,
|
|
1087
|
+
name: sessionName(input.name) || sessionName(input.feature),
|
|
870
1088
|
feature: input.feature,
|
|
871
1089
|
steps: [],
|
|
872
1090
|
status: 'active',
|
|
@@ -875,11 +1093,15 @@ export function reportPlan(dataDir, input) {
|
|
|
875
1093
|
currentStep: 1,
|
|
876
1094
|
activeDiffId: null,
|
|
877
1095
|
pendingInstruction: null,
|
|
1096
|
+
initialInstruction: null,
|
|
878
1097
|
workStartedAt: null,
|
|
879
1098
|
createdAt: now,
|
|
880
1099
|
updatedAt: now,
|
|
881
1100
|
diffs: [],
|
|
882
1101
|
}
|
|
1102
|
+
if (!sessionName(manifest.name)) {
|
|
1103
|
+
manifest.name = sessionName(input.feature)
|
|
1104
|
+
}
|
|
883
1105
|
manifest.feature = input.feature
|
|
884
1106
|
manifest.steps = planSteps(input.stepTitles)
|
|
885
1107
|
manifest.status = 'active'
|
|
@@ -888,7 +1110,13 @@ export function reportPlan(dataDir, input) {
|
|
|
888
1110
|
manifest.workStartedAt = null
|
|
889
1111
|
writeManifest(dataDir, manifest)
|
|
890
1112
|
focusSession(dataDir, sessionId)
|
|
891
|
-
|
|
1113
|
+
recordSessionAck(
|
|
1114
|
+
dataDir,
|
|
1115
|
+
sessionId,
|
|
1116
|
+
'plan',
|
|
1117
|
+
`${manifest.steps.length} step(s)`,
|
|
1118
|
+
)
|
|
1119
|
+
return autoAdvance(dataDir, sessionId, input.targetRoot)
|
|
892
1120
|
}
|
|
893
1121
|
|
|
894
1122
|
if (existing.phase !== 'replanning') {
|
|
@@ -905,7 +1133,13 @@ export function reportPlan(dataDir, input) {
|
|
|
905
1133
|
existing.workStartedAt = null
|
|
906
1134
|
writeManifest(dataDir, existing)
|
|
907
1135
|
focusSession(dataDir, sessionId)
|
|
908
|
-
|
|
1136
|
+
recordSessionAck(
|
|
1137
|
+
dataDir,
|
|
1138
|
+
sessionId,
|
|
1139
|
+
'plan',
|
|
1140
|
+
`${existing.steps.filter((step) => step.index >= startAt).length} step(s)`,
|
|
1141
|
+
)
|
|
1142
|
+
return autoAdvance(dataDir, sessionId, input.targetRoot)
|
|
909
1143
|
}
|
|
910
1144
|
|
|
911
1145
|
export function invokeStep(dataDir, sessionId, step, targetRoot = null) {
|
|
@@ -919,8 +1153,8 @@ export function invokeStep(dataDir, sessionId, step, targetRoot = null) {
|
|
|
919
1153
|
if (step !== expected) {
|
|
920
1154
|
throw new Error(
|
|
921
1155
|
last
|
|
922
|
-
? `
|
|
923
|
-
: `
|
|
1156
|
+
? `Accept proposal on step ${active.step} to finish`
|
|
1157
|
+
: `Accept proposal on step ${active.step} to continue`,
|
|
924
1158
|
)
|
|
925
1159
|
}
|
|
926
1160
|
applyUnresolved(dataDir, targetRoot, manifest, active.id)
|
|
@@ -945,9 +1179,33 @@ export function invokeStep(dataDir, sessionId, step, targetRoot = null) {
|
|
|
945
1179
|
manifest.phase = 'working'
|
|
946
1180
|
manifest.workStartedAt = new Date().toISOString()
|
|
947
1181
|
writeManifest(dataDir, manifest)
|
|
1182
|
+
const title = manifest.steps.find((item) => item.index === step)?.title
|
|
1183
|
+
recordSessionAck(
|
|
1184
|
+
dataDir,
|
|
1185
|
+
sessionId,
|
|
1186
|
+
'invoke',
|
|
1187
|
+
title ? `step ${step} — ${title}` : `step ${step}`,
|
|
1188
|
+
)
|
|
1189
|
+
if (targetRoot) snapshotPreStep(dataDir, sessionId, targetRoot)
|
|
948
1190
|
return manifest
|
|
949
1191
|
}
|
|
950
1192
|
|
|
1193
|
+
export function snapshotPreStep(dataDir, sessionId, targetRoot) {
|
|
1194
|
+
const { preStep } = sessionPaths(dataDir, sessionId)
|
|
1195
|
+
snapshotSourceTree(targetRoot, preStep)
|
|
1196
|
+
return preStep
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
export function readLiveDiff(dataDir, sessionId, targetRoot) {
|
|
1200
|
+
const { preStep } = sessionPaths(dataDir, sessionId)
|
|
1201
|
+
if (!fs.existsSync(preStep)) {
|
|
1202
|
+
throw new Error(
|
|
1203
|
+
`Step ${sessionId} has no invoke snapshot. Wait for VISUAL_CODER_EXECUTE before recording file changes.`,
|
|
1204
|
+
)
|
|
1205
|
+
}
|
|
1206
|
+
return diffSourceTrees(preStep, targetRoot)
|
|
1207
|
+
}
|
|
1208
|
+
|
|
951
1209
|
export function appendDiff(dataDir, targetRoot, input) {
|
|
952
1210
|
const sessionId = assertSessionId(input.sessionId)
|
|
953
1211
|
const manifest = requireManifest(
|
|
@@ -974,12 +1232,19 @@ export function appendDiff(dataDir, targetRoot, input) {
|
|
|
974
1232
|
throw new Error(`The next diff must implement step ${parent.step + 1}`)
|
|
975
1233
|
}
|
|
976
1234
|
|
|
977
|
-
|
|
1235
|
+
const patchText = input.patchText ?? readLiveDiff(dataDir, sessionId, targetRoot)
|
|
1236
|
+
if (!patchText.trim()) {
|
|
1237
|
+
throw new Error('No file changes to record for this step')
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
const snapshotRoot = sessionPaths(dataDir, sessionId).preStep
|
|
1241
|
+
const originRoot = fs.existsSync(snapshotRoot) ? snapshotRoot : targetRoot
|
|
1242
|
+
validateContinuation(dataDir, manifest, originRoot, patchText)
|
|
978
1243
|
captureBaseline(
|
|
979
1244
|
dataDir,
|
|
980
1245
|
sessionId,
|
|
981
|
-
|
|
982
|
-
parseUnifiedPatch(
|
|
1246
|
+
originRoot,
|
|
1247
|
+
parseUnifiedPatch(patchText).entries.map((entry) => entry.id),
|
|
983
1248
|
)
|
|
984
1249
|
if (parent?.status === 'extend') parent.status = 'extended'
|
|
985
1250
|
|
|
@@ -1000,7 +1265,7 @@ export function appendDiff(dataDir, targetRoot, input) {
|
|
|
1000
1265
|
fs.mkdirSync(paths.diffs, { recursive: true })
|
|
1001
1266
|
atomicWrite(
|
|
1002
1267
|
path.join(paths.root, file),
|
|
1003
|
-
|
|
1268
|
+
patchText.endsWith('\n') ? patchText : `${patchText}\n`,
|
|
1004
1269
|
)
|
|
1005
1270
|
manifest.activeDiffId = id
|
|
1006
1271
|
manifest.phase = 'review'
|
|
@@ -1061,7 +1326,13 @@ export function continueDiff(dataDir, targetRoot, sessionId, diffId) {
|
|
|
1061
1326
|
return autoAdvance(dataDir, sessionId, targetRoot)
|
|
1062
1327
|
}
|
|
1063
1328
|
|
|
1064
|
-
export function requestReplan(
|
|
1329
|
+
export function requestReplan(
|
|
1330
|
+
dataDir,
|
|
1331
|
+
sessionId,
|
|
1332
|
+
diffId,
|
|
1333
|
+
instruction,
|
|
1334
|
+
targetRoot = null,
|
|
1335
|
+
) {
|
|
1065
1336
|
const manifest = requireManifest(dataDir, sessionId)
|
|
1066
1337
|
const active = pendingActive(manifest, diffId)
|
|
1067
1338
|
const guidance = typeof instruction === 'string' ? instruction.trim() : ''
|
|
@@ -1073,6 +1344,7 @@ export function requestReplan(dataDir, sessionId, diffId, instruction) {
|
|
|
1073
1344
|
manifest.pendingInstruction = guidance
|
|
1074
1345
|
manifest.workStartedAt = new Date().toISOString()
|
|
1075
1346
|
writeManifest(dataDir, manifest)
|
|
1347
|
+
if (targetRoot) materializeDiff(dataDir, targetRoot, sessionId, active.id)
|
|
1076
1348
|
return manifest
|
|
1077
1349
|
}
|
|
1078
1350
|
|
|
@@ -1139,13 +1411,16 @@ export function discardInactiveDiffSessions(
|
|
|
1139
1411
|
}
|
|
1140
1412
|
|
|
1141
1413
|
for (const sessionId of listStoredSessionIds(dataDir)) {
|
|
1142
|
-
const
|
|
1414
|
+
const manifest = readManifest(dataDir, sessionId)
|
|
1415
|
+
if (!isTerminalSession(manifest)) continue
|
|
1143
1416
|
const stopping = keep.has(sessionId) && isSessionStopped(dataDir, sessionId)
|
|
1144
|
-
if (
|
|
1417
|
+
if (stopping) continue
|
|
1145
1418
|
discardStoredSession(dataDir, sessionId, targetRoot)
|
|
1146
1419
|
}
|
|
1147
1420
|
|
|
1148
|
-
const liveIds =
|
|
1421
|
+
const liveIds = listStoredSessionIds(dataDir).filter(
|
|
1422
|
+
(id) => !isTerminalSession(readManifest(dataDir, id)),
|
|
1423
|
+
)
|
|
1149
1424
|
const active = readActiveSession(dataDir)
|
|
1150
1425
|
if (active && !liveIds.includes(active)) writeActiveSession(dataDir, null)
|
|
1151
1426
|
const locked = readBlueprintSession(dataDir)
|
|
@@ -1170,6 +1445,12 @@ export function clearDiffSessions(dataDir, targetRoot = null) {
|
|
|
1170
1445
|
unstageDiffSessionArtifacts(dataDir, targetRoot)
|
|
1171
1446
|
}
|
|
1172
1447
|
|
|
1448
|
+
export function recoverOpenDiffSessions(dataDir, targetRoot = null) {
|
|
1449
|
+
// Visualizer startup never reopens an LLM session.
|
|
1450
|
+
clearDiffSessions(dataDir, targetRoot)
|
|
1451
|
+
return []
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1173
1454
|
export function stopSession(dataDir, sessionId, targetRoot = null) {
|
|
1174
1455
|
const safeId = assertSessionId(sessionId)
|
|
1175
1456
|
writeStoppedMarker(dataDir, safeId)
|
|
@@ -1192,7 +1473,7 @@ export function decideDiff(
|
|
|
1192
1473
|
return continueDiff(dataDir, targetRoot, sessionId, diffId)
|
|
1193
1474
|
}
|
|
1194
1475
|
if (decision === 'extend') {
|
|
1195
|
-
return requestReplan(dataDir, sessionId, diffId, instruction)
|
|
1476
|
+
return requestReplan(dataDir, sessionId, diffId, instruction, targetRoot)
|
|
1196
1477
|
}
|
|
1197
1478
|
return stopSession(dataDir, sessionId, targetRoot)
|
|
1198
1479
|
}
|