@jkwd/inbase 0.1.18 → 0.1.20

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.
@@ -10,15 +10,19 @@ export type MapContextMenuState = {
10
10
 
11
11
  type MapContextMenuProps = {
12
12
  menu: MapContextMenuState | null
13
+ pointed?: boolean
13
14
  onAddFile: (folder: string) => void
14
15
  onAddFolder: (folder: string) => void
16
+ onPointToFolder?: (folder: string) => void
15
17
  onClose: () => void
16
18
  }
17
19
 
18
20
  export function MapContextMenu({
19
21
  menu,
22
+ pointed = false,
20
23
  onAddFile,
21
24
  onAddFolder,
25
+ onPointToFolder,
22
26
  onClose,
23
27
  }: MapContextMenuProps) {
24
28
  useEffect(() => {
@@ -48,7 +52,7 @@ export function MapContextMenu({
48
52
 
49
53
  const pad = 8
50
54
  const width = 176
51
- const height = 84
55
+ const height = onPointToFolder ? 126 : 84
52
56
  const left = Math.min(
53
57
  Math.max(pad, menu.x),
54
58
  window.innerWidth - width - pad,
@@ -85,6 +89,19 @@ export function MapContextMenu({
85
89
  >
86
90
  Add folder
87
91
  </button>
92
+ {onPointToFolder && (
93
+ <button
94
+ type="button"
95
+ role="menuitem"
96
+ aria-pressed={pointed}
97
+ onClick={() => {
98
+ onPointToFolder(menu.folder)
99
+ onClose()
100
+ }}
101
+ >
102
+ {pointed ? 'Stop pointing' : 'Point to folder'}
103
+ </button>
104
+ )}
88
105
  </div>,
89
106
  document.body,
90
107
  )
@@ -3,6 +3,8 @@ import { folderOfFile, folderParent } from './layout'
3
3
  import type {
4
4
  BlueprintNote,
5
5
  BlueprintNoteKind,
6
+ BlueprintPointer,
7
+ BlueprintPointerKind,
6
8
  CodebaseGraph,
7
9
  FileNode,
8
10
  PatchImportAddition,
@@ -437,6 +439,101 @@ export function dropBlueprintSymbolNote(
437
439
  )
438
440
  }
439
441
 
442
+ export function blueprintPointerKey(
443
+ pointer: Pick<BlueprintPointer, 'kind' | 'path' | 'name'>,
444
+ ) {
445
+ return pointer.kind === 'file' || pointer.kind === 'folder'
446
+ ? `${pointer.kind}:${pointer.path}`
447
+ : `${pointer.kind}:${pointer.path}:${pointer.name ?? ''}`
448
+ }
449
+
450
+ function parseBlueprintPointer(value: unknown): BlueprintPointer | null {
451
+ if (!value || typeof value !== 'object') return null
452
+ const item = value as Partial<BlueprintPointer>
453
+ const path = typeof item.path === 'string' ? item.path.trim() : ''
454
+ if (!path) return null
455
+ if (item.kind === 'file' || item.kind === 'folder') {
456
+ return { kind: item.kind, path }
457
+ }
458
+ if (item.kind !== 'function' && item.kind !== 'variable') return null
459
+ const name = typeof item.name === 'string' ? item.name.trim() : ''
460
+ if (!name) return null
461
+ return { kind: item.kind, path, name }
462
+ }
463
+
464
+ export function parseBlueprintPointers(value: unknown): BlueprintPointer[] {
465
+ if (!Array.isArray(value)) return []
466
+ const seen = new Set<string>()
467
+ const pointers: BlueprintPointer[] = []
468
+ for (const item of value) {
469
+ const parsed = parseBlueprintPointer(item)
470
+ if (!parsed) continue
471
+ const key = blueprintPointerKey(parsed)
472
+ if (seen.has(key)) continue
473
+ seen.add(key)
474
+ pointers.push(parsed)
475
+ }
476
+ return pointers
477
+ }
478
+
479
+ export function findBlueprintPointer(
480
+ pointers: BlueprintPointer[],
481
+ kind: BlueprintPointerKind,
482
+ path: string,
483
+ name?: string,
484
+ ) {
485
+ return pointers.some((item) =>
486
+ kind === 'file' || kind === 'folder'
487
+ ? item.kind === kind && item.path === path
488
+ : item.kind === kind && item.path === path && item.name === name,
489
+ )
490
+ }
491
+
492
+ export function toggleBlueprintPointer(
493
+ pointers: BlueprintPointer[],
494
+ next: {
495
+ kind: BlueprintPointerKind
496
+ path: string
497
+ name?: string
498
+ },
499
+ ): BlueprintPointer[] {
500
+ const path = next.path.trim()
501
+ if (!path || path.startsWith('draft:')) return pointers
502
+ const key = blueprintPointerKey({ ...next, path })
503
+ const without = pointers.filter((item) => blueprintPointerKey(item) !== key)
504
+ if (without.length !== pointers.length) return without
505
+ if (next.kind === 'file' || next.kind === 'folder') {
506
+ return [...without, { kind: next.kind, path }]
507
+ }
508
+ const name = (next.name ?? '').trim()
509
+ if (!name) return without
510
+ return [...without, { kind: next.kind, path, name }]
511
+ }
512
+
513
+ export function dropBlueprintFilePointers(
514
+ pointers: BlueprintPointer[],
515
+ fileIds: Iterable<string>,
516
+ folderPaths: Iterable<string> = [],
517
+ ) {
518
+ const files = new Set(fileIds)
519
+ const folders = new Set(folderPaths)
520
+ return pointers.filter((item) =>
521
+ item.kind === 'folder' ? !folders.has(item.path) : !files.has(item.path),
522
+ )
523
+ }
524
+
525
+ export function dropBlueprintSymbolPointer(
526
+ pointers: BlueprintPointer[],
527
+ path: string,
528
+ kind: 'function' | 'variable',
529
+ name: string,
530
+ ) {
531
+ return pointers.filter(
532
+ (item) =>
533
+ !(item.kind === kind && item.path === path && item.name === name),
534
+ )
535
+ }
536
+
440
537
  export function parseBlueprintImport(
441
538
  raw: string,
442
539
  file: string,
@@ -23,6 +23,8 @@ import {
23
23
  sendBlueprint,
24
24
  sessionIntent,
25
25
  setInitialInstruction,
26
+ addContextFiles,
27
+ removeContextFile,
26
28
  setStepByStep,
27
29
  setupSession,
28
30
  focusSession,
@@ -112,6 +114,7 @@ function blueprintIntentFields() {
112
114
  blueprintVariables: blueprint.addedVariables,
113
115
  blueprintImports: blueprint.addedImports,
114
116
  blueprintNotes: blueprint.notes,
117
+ blueprintPointers: blueprint.pointers,
115
118
  }
116
119
  }
117
120
 
@@ -265,6 +268,9 @@ async function decideIntent(req: IncomingMessage, res: ServerResponse) {
265
268
  addedVariables?: unknown[]
266
269
  addedImports?: unknown[]
267
270
  notes?: unknown[]
271
+ pointers?: unknown[]
272
+ files?: unknown[]
273
+ fileId?: string
268
274
  }
269
275
  const action = body.action
270
276
  const blueprintActions = new Set([
@@ -288,6 +294,8 @@ async function decideIntent(req: IncomingMessage, res: ServerResponse) {
288
294
  action !== 'focus' &&
289
295
  action !== 'set_step_by_step' &&
290
296
  action !== 'set_initial_instruction' &&
297
+ action !== 'add_context_files' &&
298
+ action !== 'remove_context_file' &&
291
299
  action !== 'setup_session'
292
300
  ) {
293
301
  sendJson(res, 400, { error: 'invalid workflow action' })
@@ -342,7 +350,6 @@ async function decideIntent(req: IncomingMessage, res: ServerResponse) {
342
350
  body.instruction ?? '',
343
351
  targetRoot,
344
352
  )
345
- rescanTarget('after withdrawing a patch')
346
353
  } else if (action === 'blueprint_yes') {
347
354
  answerBlueprint(dataDir, body.sessionId, true)
348
355
  } else if (action === 'blueprint_no') {
@@ -355,6 +362,7 @@ async function decideIntent(req: IncomingMessage, res: ServerResponse) {
355
362
  addedVariables: body.addedVariables,
356
363
  addedImports: body.addedImports,
357
364
  notes: body.notes,
365
+ pointers: body.pointers,
358
366
  })
359
367
  } else if (action === 'blueprint_clear') {
360
368
  clearBlueprint(dataDir)
@@ -370,6 +378,7 @@ async function decideIntent(req: IncomingMessage, res: ServerResponse) {
370
378
  addedVariables: body.addedVariables,
371
379
  addedImports: body.addedImports,
372
380
  notes: body.notes,
381
+ pointers: body.pointers,
373
382
  })
374
383
  } else if (action === 'setup_session') {
375
384
  const manifest = setupSession(dataDir, {
@@ -381,6 +390,14 @@ async function decideIntent(req: IncomingMessage, res: ServerResponse) {
381
390
  return
382
391
  } else if (action === 'set_initial_instruction') {
383
392
  setInitialInstruction(dataDir, body.sessionId, body.instruction ?? '')
393
+ } else if (action === 'add_context_files') {
394
+ addContextFiles(dataDir, body.sessionId, body.files ?? [])
395
+ } else if (action === 'remove_context_file') {
396
+ if (!body.fileId) {
397
+ sendJson(res, 400, { error: 'fileId is required' })
398
+ return
399
+ }
400
+ removeContextFile(dataDir, body.sessionId, body.fileId)
384
401
  } else if (action === 'focus') {
385
402
  focusSession(dataDir, body.sessionId)
386
403
  } else if (action === 'set_step_by_step') {
package/bin/session.mjs CHANGED
@@ -122,21 +122,37 @@ function emitApprovalHandshake(store, dataDir, sessionId, manifest, initialDiff)
122
122
  if (manifest.phase === 'working') {
123
123
  const next = manifest.steps.find((step) => step.index === manifest.currentStep)
124
124
  const title = next?.title
125
+ const guidance =
126
+ typeof manifest.pendingInstruction === 'string'
127
+ ? manifest.pendingInstruction.trim()
128
+ : ''
129
+ const updating = Boolean(guidance)
125
130
  signalAck(
126
131
  store,
127
132
  dataDir,
128
133
  sessionId,
129
134
  'execute',
130
- title
131
- ? `step ${manifest.currentStep} — ${title}`
132
- : `step ${manifest.currentStep}`,
135
+ updating
136
+ ? 'a new instruction'
137
+ : title
138
+ ? `step ${manifest.currentStep} — ${title}`
139
+ : `step ${manifest.currentStep}`,
133
140
  )
141
+ const instruction = updating
142
+ ? `\nVISUAL_CODER_INSTRUCTION_START\n${guidance}\nVISUAL_CODER_INSTRUCTION_END`
143
+ : ''
134
144
  const continuing = (manifest.diffs?.length ?? 0) > 0
135
- console.log(
136
- continuing
137
- ? `VISUAL_CODER_EXECUTE Step ${manifest.currentStep} is invoked${title ? `: ${title}` : ''}. Continue immediately: edit live files for this step only, then inbase propose-patch --session ${sessionId} with no patch file. Do not explore, re-plan, or run wait-for-blueprint.`
138
- : `VISUAL_CODER_EXECUTE Step ${manifest.currentStep} is invoked${title ? `: ${title}` : ''}. Re-read the shared blueprint.json before implementing; the user can place files and islands at any time. Edit the live project files for this step only (Write, StrReplace, Delete). Then record the step with inbase propose-patch --session ${sessionId} — no patch file. Inbase diffs those edits against the invoke snapshot and stores the patch. Do not write a unified diff yourself.`,
139
- )
145
+ if (updating) {
146
+ console.log(
147
+ `VISUAL_CODER_EXECUTE Update the current proposal for step ${manifest.currentStep}${title ? `: ${title}` : ''}. Live files already contain that proposal do not reset them. Follow the instruction between VISUAL_CODER_INSTRUCTION_START and END, edit those live files, then inbase propose-patch --session ${sessionId} with no patch file. Do not report a new plan.${instruction}`,
148
+ )
149
+ } else {
150
+ console.log(
151
+ continuing
152
+ ? `VISUAL_CODER_EXECUTE Step ${manifest.currentStep} is invoked${title ? `: ${title}` : ''}. Continue immediately: edit live files for this step only, then inbase propose-patch --session ${sessionId} with no patch file. Do not explore, re-plan, or run wait-for-blueprint.`
153
+ : `VISUAL_CODER_EXECUTE Step ${manifest.currentStep} is invoked${title ? `: ${title}` : ''}. Re-read the shared blueprint.json before implementing; the user can place files and islands at any time. Edit the live project files for this step only (Write, StrReplace, Delete). Then record the step with inbase propose-patch --session ${sessionId} — no patch file. Inbase diffs those edits against the invoke snapshot and stores the patch. Do not write a unified diff yourself.`,
154
+ )
155
+ }
140
156
  process.exit(0)
141
157
  }
142
158
  if (manifest.phase === 'replanning') {
@@ -145,13 +161,13 @@ function emitApprovalHandshake(store, dataDir, sessionId, manifest, initialDiff)
145
161
  dataDir,
146
162
  sessionId,
147
163
  'replan',
148
- `revise the plan from step ${manifest.currentStep}`,
164
+ `revise the current proposal for step ${manifest.currentStep}`,
149
165
  )
150
166
  const instruction = manifest.pendingInstruction
151
167
  ? `\nVISUAL_CODER_INSTRUCTION_START\n${manifest.pendingInstruction}\nVISUAL_CODER_INSTRUCTION_END`
152
168
  : ''
153
169
  console.log(
154
- `VISUAL_CODER_REPLAN Keep accepted patch files before step ${manifest.currentStep}. Disk is baseline + accepted patches. Do not edit project files until the next EXECUTE. The shared blueprint remains leading; if this instruction would differ from it, ask the user before replacing the plan. Report the revised tail with inbase report-plan, then wait for invocation.${instruction}`,
170
+ `VISUAL_CODER_REPLAN Live files still contain the current proposal for step ${manifest.currentStep}. Do not reset them. Follow the instruction between VISUAL_CODER_INSTRUCTION_START and END, edit those live files, then inbase propose-patch. Do not report a new plan.${instruction}`,
155
171
  )
156
172
  process.exit(4)
157
173
  }
@@ -179,7 +195,7 @@ export async function attachSession(args) {
179
195
  console.log(`VISUAL_CODER_SESSION ${manifest.sessionId}`)
180
196
  printAck('attached', manifest.name || manifest.sessionId)
181
197
  console.log(
182
- `VISUAL_CODER_ATTACHED Attached to the next waiting visualizer session ${manifest.name || manifest.sessionId} (${manifest.phase}). Use --session ${manifest.sessionId} for every later command. Run inbase wait-for-blueprint --session ${manifest.sessionId} to read the optional blueprint and instruction; it does not wait.`,
198
+ `VISUAL_CODER_ATTACHED Attached to the next waiting visualizer session ${manifest.name || manifest.sessionId} (${manifest.phase}). Use --session ${manifest.sessionId} for every later command. Run inbase wait-for-blueprint --session ${manifest.sessionId} to read the optional blueprint, instruction, and attached files; it does not wait.`,
183
199
  )
184
200
  }
185
201
 
@@ -243,6 +259,20 @@ export async function waitForBlueprint(args) {
243
259
  console.log(instruction)
244
260
  console.log('VISUAL_CODER_INSTRUCTION_END')
245
261
  }
262
+ const attached = store.contextFileHandshake(config.dataDir, sessionId)
263
+ if (attached.files.length > 0) {
264
+ console.log(
265
+ 'Honor the user\'s attached context files. Read each path with your file tools before planning. They are session-only attachments, not project files to create. Use any printed VISUAL_CODER_CONTEXT_FILE contents directly.',
266
+ )
267
+ console.log('VISUAL_CODER_CONTEXT_FILES_START')
268
+ console.log(JSON.stringify(attached.files, null, 2))
269
+ console.log('VISUAL_CODER_CONTEXT_FILES_END')
270
+ for (const file of attached.texts) {
271
+ console.log(`VISUAL_CODER_CONTEXT_FILE_START ${file.name}`)
272
+ console.log(file.content)
273
+ console.log('VISUAL_CODER_CONTEXT_FILE_END')
274
+ }
275
+ }
246
276
  process.exit(0)
247
277
  }
248
278
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jkwd/inbase",
3
- "version": "0.1.18",
3
+ "version": "0.1.20",
4
4
  "description": "A first-person 3D map of a codebase, with a visual coding workflow for Cursor.",
5
5
  "license": "MIT",
6
6
  "author": "Joris Kuijper",
@@ -14,4 +14,4 @@ npx inbase attach
14
14
 
15
15
  2. Read `VISUAL_CODER_SESSION` from the output. That id is the session to use for every later `inbase` command.
16
16
 
17
- 3. Continue the Inbase visual edits skill from `wait-for-blueprint` onward with that `--session` id. `/inbase` already started the session. `wait-for-blueprint` only reads the optional blueprint and instruction; it does not wait.
17
+ 3. Continue the Inbase visual edits skill from `wait-for-blueprint` onward with that `--session` id. `/inbase` already started the session. `wait-for-blueprint` only reads the optional blueprint, instruction, and attached files; it does not wait.
@@ -43,8 +43,8 @@ over the recorded diffs.
43
43
  live project files with Write, StrReplace, and Delete for that step only. Then
44
44
  run `inbase propose-patch` with no patch file. Inbase diffs the working tree
45
45
  against the snapshot taken at invoke and stores that patch. A later instruction
46
- replaces the withdrawn step; edit from the accepted live files, not from the
47
- withdrawn proposal. Do not write a unified diff yourself.
46
+ updates that live proposal; edit those files, then propose-patch again. Wait
47
+ for **Accept proposal** before the next step. Do not write a unified diff yourself.
48
48
 
49
49
  The visualizer stores immutable diffs under
50
50
  `.inbase/diff-sessions/<session-id>/diffs/`. Inbase must already be running
@@ -85,7 +85,7 @@ new plan.
85
85
  ## Required sequence
86
86
 
87
87
  1. **Read the current layout**. `/inbase` already started the session. Run
88
- this once to load the optional blueprint and instruction — it returns
88
+ this once to load the optional blueprint, instruction, and attached files — it returns
89
89
  immediately. Do not wait for the user to send a blueprint. Then report a
90
90
  plan.
91
91
 
@@ -98,7 +98,11 @@ npx inbase wait-for-blueprint --session "<session-id>"
98
98
  keep placing at any time.
99
99
  If `wait-for-blueprint` prints `VISUAL_CODER_INSTRUCTION_START` /
100
100
  `VISUAL_CODER_INSTRUCTION_END`, that text is the user's request for this
101
- session. Plan from that instruction and the blueprint together. The
101
+ session. If it prints `VISUAL_CODER_CONTEXT_FILES_START` /
102
+ `VISUAL_CODER_CONTEXT_FILES_END`, those are session-only files the user
103
+ dropped as initial context. Read each `path` (and any printed
104
+ `VISUAL_CODER_CONTEXT_FILE` contents). They are not project files to create.
105
+ Plan from that instruction, attached files, and the blueprint together. The
102
106
  instruction does not override an enabled blueprint; if they conflict,
103
107
  ask the user.
104
108
  3. Read the handshake output between `VISUAL_CODER_BLUEPRINT_START` and
@@ -176,11 +180,15 @@ npx inbase wait-for-approval --session "<session-id>"
176
180
  12. Read the wait command output. Reply in chat with the `VISUAL_CODER_ACK`
177
181
  line first, then:
178
182
 
179
- - Exit `0` (`VISUAL_CODER_ACK execute` / `VISUAL_CODER_EXECUTE`): the
180
- highlighted step was invoked. If this is a later step, implement it now —
181
- do not explore, re-plan, or run `wait-for-blueprint`. Re-read the shared
182
- `blueprint.json` if you need placed files. Edit live files for that step
183
- only, record with `inbase propose-patch` (no patch file), then wait again.
183
+ - Exit `0` (`VISUAL_CODER_ACK execute` / `VISUAL_CODER_EXECUTE`): follow
184
+ the printed `VISUAL_CODER_EXECUTE` line. If it says **Update the current
185
+ proposal**, edit those live files, record with `inbase propose-patch` (no
186
+ patch file), then wait again for **Accept proposal**. Do not start the
187
+ next step and do not report a new plan. Otherwise the highlighted step
188
+ was invoked: implement that step now — do not explore, re-plan, or run
189
+ `wait-for-blueprint`. Re-read the shared `blueprint.json` if you need
190
+ placed files. Edit live files for that step only, record with
191
+ `inbase propose-patch` (no patch file), then wait again.
184
192
  - Exit `6` (`VISUAL_CODER_ACK blueprint` / `VISUAL_CODER_BLUEPRINT`): the
185
193
  shared blueprint changed. Follow the latest files, islands, functions,
186
194
  variables, and imports. Do not omit, rename, relocate, or replace them.
@@ -191,19 +199,12 @@ npx inbase wait-for-approval --session "<session-id>"
191
199
  stored session diffs. The shared blueprint remains. Optionally run
192
200
  `--clear` if anything remains, tell the user the feature is done, and
193
201
  **stop**. Do not propose another patch.
194
- - Exit `4` (`VISUAL_CODER_ACK replan` / `VISUAL_CODER_REPLAN`): do **not**
195
- edit project files and do not rewrite an earlier accepted patch. The
196
- withdrawn proposal is no longer live; disk is baseline + accepted patch
197
- files. Follow the text between `VISUAL_CODER_INSTRUCTION_START` and
198
- `VISUAL_CODER_INSTRUCTION_END`, read the shared `blueprint.json` when
199
- it is enabled (files, islands, `addedFunctions`, `addedVariables`,
200
- `addedImports`). The blueprint stays leading. If the new instruction would
201
- differ from it, ask the user before replacing the plan. Read
202
- `user-context.json` (follow the viewpoint only if `followLook` is true),
203
- replace the plan from the current step onward using `inbase report-plan`,
204
- then wait for the next `VISUAL_CODER_EXECUTE` before editing again. The
205
- replacement edits must sit on the accepted live files, not the withdrawn
206
- proposal.
202
+ - Exit `4` (`VISUAL_CODER_ACK replan` / `VISUAL_CODER_REPLAN`): live files
203
+ still contain the current proposal. Do not reset them and do not report a
204
+ new plan. Follow the text between `VISUAL_CODER_INSTRUCTION_START` and
205
+ `VISUAL_CODER_INSTRUCTION_END`, edit those live files, then
206
+ `inbase propose-patch`. Wait for **Accept proposal**. Do not start the
207
+ next step.
207
208
  - Exit `2` (`VISUAL_CODER_ACK stopped` / `VISUAL_CODER_STOPPED`) or `3`
208
209
  (`VISUAL_CODER_ACK timeout`): make no further project changes.
209
210
 
@@ -220,7 +221,7 @@ npx inbase propose-patch --session "<session-id>" --clear
220
221
  from **Setup LLM session** in the map
221
222
  - Edit files on a direct chat request; reply with the `/skipinbase` line and stop
222
223
  - Invent a session id for `/inbase`; run `npx inbase attach` with no `--session`
223
- - Skip `inbase wait-for-blueprint`; it returns immediately and provides the optional blueprint and instruction
224
+ - Skip `inbase wait-for-blueprint`; it returns immediately and provides the optional blueprint, instruction, and attached files
224
225
  - Treat the chat request, viewpoint, or your own plan as overriding an enabled blueprint
225
226
  - Skip, rename, relocate, or replace the shared `blueprint.json` files, islands, functions, variables, or imports when `enabled` is true
226
227
  - Silently differ from the blueprint; ask the user first