@jkwd/inbase 0.1.9 → 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 CHANGED
@@ -47,13 +47,16 @@ inbase run --target /path/to/your/project
47
47
 
48
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
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 you can walk before it is applied.
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
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 placement and lets the agent choose files from the request.
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
53
 
54
- 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** for each patch. With it off, the LLM implements the full plan; you can still walk Previous/Next over the diffs, then **Complete**. Send an alternative instruction from the HUD to revise the remaining plan, or **Stop** to end the session.
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
55
 
56
- The LLM never writes project files itself. Patches apply when you run or complete steps in the visualizer.
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.
57
60
 
58
61
  <br clear="all" />
59
62
 
@@ -106,6 +106,7 @@ export const emptyIntent: {
106
106
  phase: null
107
107
  working: boolean
108
108
  stalledWait: boolean
109
+ llmIdle: boolean
109
110
  creationMode: boolean
110
111
  canEnterBlueprint: boolean
111
112
  blueprintSessionId: string | null
@@ -533,6 +533,7 @@ export const emptyIntent = {
533
533
  phase: null,
534
534
  working: false,
535
535
  stalledWait: false,
536
+ llmIdle: false,
536
537
  creationMode: false,
537
538
  canEnterBlueprint: false,
538
539
  blueprintSessionId: null,
@@ -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 (!manifest) return false
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 (!manifest) continue
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 assertBlueprintSessionAvailable(dataDir, sessionId) {
277
+ function releaseBlueprintSession(dataDir, sessionId) {
277
278
  const safeId = assertSessionId(sessionId)
278
- const locked = readBlueprintSession(dataDir)
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 claimBlueprintSession(dataDir, sessionId) {
288
- const safeId = assertBlueprintSessionAvailable(dataDir, sessionId)
289
- writeBlueprintSession(dataDir, safeId)
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 releaseBlueprintSession(dataDir, sessionId) {
294
- const safeId = assertSessionId(sessionId)
295
- if (readBlueprintSession(dataDir) === safeId) writeBlueprintSession(dataDir, null)
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 focusSession(dataDir, sessionId) {
299
- const safeId = assertSessionId(sessionId)
300
- const locked = readBlueprintSession(dataDir)
301
- if (!locked || locked === safeId) writeActiveSession(dataDir, safeId)
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 unresolvedEntries(manifest, diffId = manifest.activeDiffId) {
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
- let lastApplied = -1
359
- chain.forEach((entry, index) => {
360
- if (entry.status === 'applied') lastApplied = index
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
- return chain.slice(lastApplied + 1).filter((entry) => entry.status !== 'rejected')
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.diffs
448
- .slice(0, selectedIndex + 1)
449
- .filter((entry) => entry.status !== 'rejected')
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 blueprintSessionId = readBlueprintSession(dataDir)
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
- creationMode: manifest.phase === 'blueprint' && ownsBlueprintLock,
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.diffs
686
- .filter((entry) => entry.status !== 'rejected')
687
- .map((entry) => readDiff(dataDir, manifest.sessionId, entry))
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.phase !== 'blueprint') {
811
- throw new Error(`Session ${safeId} is not in blueprint mode`)
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
- writeBlueprint(dataDir, safeId, {
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
- ? `Run step ${active.step} to finish`
923
- : `Run step ${expected} to continue`,
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(dataDir, sessionId, diffId, instruction) {
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 live = keep.has(sessionId) && Boolean(readManifest(dataDir, sessionId))
1165
+ const manifest = readManifest(dataDir, sessionId)
1166
+ if (!isTerminalSession(manifest)) continue
1143
1167
  const stopping = keep.has(sessionId) && isSessionStopped(dataDir, sessionId)
1144
- if (live || stopping) continue
1168
+ if (stopping) continue
1145
1169
  discardStoredSession(dataDir, sessionId, targetRoot)
1146
1170
  }
1147
1171
 
1148
- const liveIds = [...keep].filter((id) => readManifest(dataDir, id))
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
  }