@jkwd/inbase 0.1.18 → 0.1.19

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
@@ -49,9 +49,9 @@ inbase run --target /path/to/your/project
49
49
 
50
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), edits live files for each invoked step, and records that step as a patch. Those stored patches are the session record and are applied on every step update.
51
51
 
52
- Sessions start in the map: click **Setup LLM session**, type an **initial instruction**, optionally place a **blueprint** (`Space` for files, `B` for folders), then open a Cursor chat and run **`/inbase`**. That command connects the chat and starts the work. The layout is the source of truth for the chat. The blueprint is shared across sessions and stays on the map even after those files and folders exist; use **Hide/Show blueprint**, **Clear blueprint**, or **Cleanup blueprint** (drops planned items that already exist). When a session finishes it is discarded; the shared blueprint remains. Restarting the visualizer also starts with no LLM session; leftover session files are not restored.
52
+ Sessions start in the map: click **Setup LLM session**, type an **initial instruction**, optionally drop **context files** for the chat to read, optionally place a **blueprint** (`Space` for files, `B` for folders), then open a Cursor chat and run **`/inbase`**. That command connects the chat and starts the work. The layout is the source of truth for the chat. Dropped context files stay with that session until it ends. The blueprint is shared across sessions and stays on the map even after those files and folders exist; use **Hide/Show blueprint**, **Clear blueprint**, or **Cleanup blueprint** (drops planned items that already exist). When a session finishes it is discarded; the shared blueprint remains. Restarting the visualizer also starts with no LLM session; leftover session files are not restored.
53
53
 
54
- A normal chat request does not open a session. Use `/inbase` after Setup LLM session, or `/skipinbase [request]` to work outside the map. `/inbase` starts the session immediately; `wait-for-blueprint` only reads the optional blueprint.
54
+ A normal chat request does not open a session. Use `/inbase` after Setup LLM session, or `/skipinbase [request]` to work outside the map. `/inbase` starts the session immediately; `wait-for-blueprint` only reads the optional blueprint, instruction, and attached files.
55
55
 
56
56
  If several sessions are open, `/inbase` attaches the oldest session that is still waiting. Sessions that already have an LLM are skipped, and the map window does not need to be focused.
57
57
 
@@ -112,6 +112,7 @@ export const emptyIntent: {
112
112
  listening: boolean
113
113
  lastAck: { kind: string; detail: string; at: string | null } | null
114
114
  initialInstruction: string | null
115
+ contextFiles: unknown[]
115
116
  creationMode: boolean
116
117
  canEnterBlueprint: boolean
117
118
  blueprintHidden?: boolean
@@ -539,6 +539,7 @@ export const emptyIntent = {
539
539
  listening: false,
540
540
  lastAck: null,
541
541
  initialInstruction: null,
542
+ contextFiles: [],
542
543
  creationMode: false,
543
544
  canEnterBlueprint: false,
544
545
  blueprintHidden: false,
@@ -17,6 +17,22 @@ export type DiffEntry = {
17
17
  decidedAt: string | null
18
18
  }
19
19
 
20
+ export type SessionContextFile = {
21
+ id: string
22
+ name: string
23
+ storedName: string
24
+ mimeType: string
25
+ size: number
26
+ }
27
+
28
+ export type SessionContextFileInfo = {
29
+ id: string
30
+ name: string
31
+ mimeType: string
32
+ size: number
33
+ path?: string
34
+ }
35
+
20
36
  export type DiffManifest = {
21
37
  version: number
22
38
  sessionId: string
@@ -39,6 +55,7 @@ export type DiffManifest = {
39
55
  activeDiffId: string | null
40
56
  pendingInstruction: string | null
41
57
  initialInstruction?: string | null
58
+ contextFiles?: SessionContextFile[]
42
59
  workStartedAt: string | null
43
60
  stepByStep: boolean
44
61
  createdAt: string
@@ -61,6 +78,7 @@ export function sessionPaths(
61
78
  baseline: string
62
79
  baselineFiles: string
63
80
  preStep: string
81
+ context: string
64
82
  stopped: string
65
83
  }
66
84
  export function touchSessionConnection(dataDir: string, sessionId: string): void
@@ -129,6 +147,30 @@ export function setInitialInstruction(
129
147
  sessionId: string,
130
148
  instruction: string | null | undefined,
131
149
  ): DiffManifest
150
+ export const MAX_CONTEXT_FILES: number
151
+ export const MAX_CONTEXT_FILE_BYTES: number
152
+ export const MAX_CONTEXT_TOTAL_BYTES: number
153
+ export function listContextFiles(
154
+ dataDir: string,
155
+ sessionId: string,
156
+ ): Array<SessionContextFile & { path: string }>
157
+ export function contextFileHandshake(
158
+ dataDir: string,
159
+ sessionId: string,
160
+ ): {
161
+ files: Array<{ name: string; path: string; mimeType: string; size: number }>
162
+ texts: Array<{ name: string; content: string }>
163
+ }
164
+ export function addContextFiles(
165
+ dataDir: string,
166
+ sessionId: string,
167
+ files: unknown,
168
+ ): DiffManifest
169
+ export function removeContextFile(
170
+ dataDir: string,
171
+ sessionId: string,
172
+ fileId: string,
173
+ ): DiffManifest
132
174
  export function readAttachedSession(dataDir: string): string | null
133
175
  export function listAttachQueue(dataDir: string): string[]
134
176
  export function nextAttachSessionId(dataDir: string): string | null
@@ -16,6 +16,10 @@ import { diffSourceTrees, snapshotSourceTree } from './tree-diff.mjs'
16
16
  const SESSION_ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/
17
17
  const CONNECTED_TTL_MS = 15_000
18
18
  const STALLED_WAIT_MS = 2_000
19
+ export const MAX_CONTEXT_FILES = 16
20
+ export const MAX_CONTEXT_FILE_BYTES = 8 * 1024 * 1024
21
+ export const MAX_CONTEXT_TOTAL_BYTES = 24 * 1024 * 1024
22
+ const CONTEXT_TEXT_INLINE_BYTES = 100_000
19
23
 
20
24
  export function assertSessionId(value) {
21
25
  if (typeof value !== 'string' || !SESSION_ID.test(value) || value === '.' || value === '..') {
@@ -65,6 +69,7 @@ export function sessionPaths(dataDir, sessionId) {
65
69
  baseline: path.join(root, 'baseline.json'),
66
70
  baselineFiles: path.join(root, 'baseline'),
67
71
  preStep: path.join(root, 'pre-step'),
72
+ context: path.join(root, 'context'),
68
73
  stopped: path.join(dataDir, 'diff-sessions', `${safeId}.stopped`),
69
74
  }
70
75
  }
@@ -423,6 +428,129 @@ function blueprintHasContent(blueprint) {
423
428
  )
424
429
  }
425
430
 
431
+ function normalizeContextFiles(value) {
432
+ if (!Array.isArray(value)) return []
433
+ return value.flatMap((item) => {
434
+ if (!item || typeof item !== 'object') return []
435
+ const id = typeof item.id === 'string' ? item.id.trim() : ''
436
+ const name = typeof item.name === 'string' ? item.name.trim() : ''
437
+ const storedName =
438
+ typeof item.storedName === 'string' ? item.storedName.trim() : ''
439
+ const size = item.size
440
+ if (!id || !name || !storedName || !Number.isFinite(size) || size < 0) {
441
+ return []
442
+ }
443
+ if (storedName.includes('..') || path.basename(storedName) !== storedName) {
444
+ return []
445
+ }
446
+ return [
447
+ {
448
+ id,
449
+ name,
450
+ storedName,
451
+ mimeType:
452
+ typeof item.mimeType === 'string' && item.mimeType.trim()
453
+ ? item.mimeType.trim()
454
+ : 'application/octet-stream',
455
+ size,
456
+ },
457
+ ]
458
+ })
459
+ }
460
+
461
+ function safeContextFileName(name) {
462
+ const base = path.basename(String(name || 'file')).replace(/[\u0000-\u001f]/g, '')
463
+ const cleaned = base
464
+ .replace(/[^\w.\- ()[\]]+/g, '_')
465
+ .replace(/^\.+/, '')
466
+ .trim()
467
+ return (cleaned || 'file').slice(0, 120)
468
+ }
469
+
470
+ function decodeContextFileBytes(file) {
471
+ if (Buffer.isBuffer(file?.bytes)) return file.bytes
472
+ if (typeof file?.contentBase64 === 'string' && file.contentBase64.trim()) {
473
+ if (!/^[A-Za-z0-9+/=\s]+$/.test(file.contentBase64)) {
474
+ throw new Error('context file content must be base64')
475
+ }
476
+ return Buffer.from(file.contentBase64, 'base64')
477
+ }
478
+ throw new Error('context file bytes are required')
479
+ }
480
+
481
+ function contextFileAbsolute(dir, storedName) {
482
+ const absolute = path.resolve(dir, storedName)
483
+ const prefix = dir.endsWith(path.sep) ? dir : `${dir}${path.sep}`
484
+ if (absolute !== dir && !absolute.startsWith(prefix)) {
485
+ throw new Error(`Invalid context file path ${storedName}`)
486
+ }
487
+ return absolute
488
+ }
489
+
490
+ function publicContextFile(item) {
491
+ return {
492
+ id: item.id,
493
+ name: item.name,
494
+ mimeType: item.mimeType,
495
+ size: item.size,
496
+ }
497
+ }
498
+
499
+ function isInlineContextText(mimeType, bytes) {
500
+ if (bytes.length === 0 || bytes.length > CONTEXT_TEXT_INLINE_BYTES) return false
501
+ if (bytes.includes(0)) return false
502
+ const mime = typeof mimeType === 'string' ? mimeType.toLowerCase() : ''
503
+ if (mime.startsWith('audio/') || mime.startsWith('video/')) return false
504
+ if (mime === 'application/pdf' || mime === 'application/zip') return false
505
+ if (mime.startsWith('image/') && mime !== 'image/svg+xml') return false
506
+ if (
507
+ mime.startsWith('text/') ||
508
+ mime === 'application/json' ||
509
+ mime === 'application/javascript' ||
510
+ mime === 'application/xml' ||
511
+ mime === 'image/svg+xml' ||
512
+ mime === 'application/octet-stream' ||
513
+ mime === ''
514
+ ) {
515
+ return true
516
+ }
517
+ return !mime.startsWith('image/')
518
+ }
519
+
520
+ export function listContextFiles(dataDir, sessionId) {
521
+ const manifest = requireManifest(dataDir, sessionId)
522
+ const dir = sessionPaths(dataDir, sessionId).context
523
+ return normalizeContextFiles(manifest.contextFiles).flatMap((item) => {
524
+ let absolute
525
+ try {
526
+ absolute = contextFileAbsolute(dir, item.storedName)
527
+ } catch {
528
+ return []
529
+ }
530
+ if (!fs.existsSync(absolute)) return []
531
+ return [{ ...item, path: absolute }]
532
+ })
533
+ }
534
+
535
+ export function contextFileHandshake(dataDir, sessionId) {
536
+ const files = listContextFiles(dataDir, sessionId)
537
+ const listed = []
538
+ const texts = []
539
+ for (const file of files) {
540
+ listed.push({
541
+ name: file.name,
542
+ path: file.path,
543
+ mimeType: file.mimeType,
544
+ size: file.size,
545
+ })
546
+ const bytes = fs.readFileSync(file.path)
547
+ if (isInlineContextText(file.mimeType, bytes)) {
548
+ texts.push({ name: file.name, content: bytes.toString('utf8') })
549
+ }
550
+ }
551
+ return { files: listed, texts }
552
+ }
553
+
426
554
  export function readManifest(dataDir, sessionId) {
427
555
  const { manifest } = sessionPaths(dataDir, sessionId)
428
556
  const value = readJson(manifest, null)
@@ -448,6 +576,7 @@ export function readManifest(dataDir, sessionId) {
448
576
  if (typeof value.stepByStep !== 'boolean') value.stepByStep = true
449
577
  value.initialInstruction =
450
578
  typeof value.initialInstruction === 'string' ? value.initialInstruction : null
579
+ value.contextFiles = normalizeContextFiles(value.contextFiles)
451
580
  return value
452
581
  }
453
582
 
@@ -492,9 +621,12 @@ function liveEntries(manifest, diffId) {
492
621
  const browsingHistory = selected.id !== manifest.activeDiffId
493
622
  return chain.filter((entry) => {
494
623
  if (entry.status === 'rejected') return false
495
- if (entry.status === 'extended' || entry.status === 'extend') {
624
+ if (entry.status === 'extended') {
496
625
  return browsingHistory && entry.id === selected.id
497
626
  }
627
+ if (entry.status === 'extend') {
628
+ return entry.id === selected.id
629
+ }
498
630
  return true
499
631
  })
500
632
  }
@@ -627,6 +759,7 @@ export function sessionIntent(
627
759
  typeof manifest.initialInstruction === 'string'
628
760
  ? manifest.initialInstruction
629
761
  : null,
762
+ contextFiles: listContextFiles(dataDir, sessionId).map(publicContextFile),
630
763
  steps: manifest.steps,
631
764
  step: activeView ? manifest.currentStep : selected?.step ?? manifest.currentStep,
632
765
  stepByStep: isStepByStep(manifest),
@@ -928,6 +1061,7 @@ export function startSession(dataDir, input) {
928
1061
  activeDiffId: null,
929
1062
  pendingInstruction: null,
930
1063
  initialInstruction: null,
1064
+ contextFiles: [],
931
1065
  workStartedAt: null,
932
1066
  createdAt: now,
933
1067
  updatedAt: now,
@@ -966,6 +1100,7 @@ export function setupSession(dataDir, input = {}) {
966
1100
  activeDiffId: null,
967
1101
  pendingInstruction: null,
968
1102
  initialInstruction: null,
1103
+ contextFiles: [],
969
1104
  workStartedAt: null,
970
1105
  createdAt: now,
971
1106
  updatedAt: now,
@@ -993,6 +1128,92 @@ export function setInitialInstruction(dataDir, sessionId, instruction) {
993
1128
  return manifest
994
1129
  }
995
1130
 
1131
+ export function addContextFiles(dataDir, sessionId, files) {
1132
+ const manifest = requireManifest(dataDir, sessionId)
1133
+ if (isTerminalSession(manifest)) {
1134
+ throw sessionStoppedError(sessionId)
1135
+ }
1136
+ const incoming = Array.isArray(files) ? files : files ? [files] : []
1137
+ if (incoming.length === 0) {
1138
+ throw new Error('at least one context file is required')
1139
+ }
1140
+
1141
+ const existing = listContextFiles(dataDir, sessionId)
1142
+ if (existing.length + incoming.length > MAX_CONTEXT_FILES) {
1143
+ throw new Error(`at most ${MAX_CONTEXT_FILES} context files can be attached`)
1144
+ }
1145
+
1146
+ const dir = sessionPaths(dataDir, sessionId).context
1147
+ fs.mkdirSync(dir, { recursive: true })
1148
+ const existingBytes = existing.reduce((sum, item) => sum + item.size, 0)
1149
+ const next = [...existing.map((item) => ({
1150
+ id: item.id,
1151
+ name: item.name,
1152
+ storedName: item.storedName,
1153
+ mimeType: item.mimeType,
1154
+ size: item.size,
1155
+ }))]
1156
+ let addedBytes = 0
1157
+
1158
+ for (const file of incoming) {
1159
+ const bytes = decodeContextFileBytes(file)
1160
+ if (bytes.length === 0) {
1161
+ throw new Error('context file is empty')
1162
+ }
1163
+ if (bytes.length > MAX_CONTEXT_FILE_BYTES) {
1164
+ throw new Error(
1165
+ `context file must be ${MAX_CONTEXT_FILE_BYTES} bytes or smaller`,
1166
+ )
1167
+ }
1168
+ addedBytes += bytes.length
1169
+ if (existingBytes + addedBytes > MAX_CONTEXT_TOTAL_BYTES) {
1170
+ throw new Error(
1171
+ `attached files must total ${MAX_CONTEXT_TOTAL_BYTES} bytes or less`,
1172
+ )
1173
+ }
1174
+ const id = crypto.randomBytes(4).toString('hex')
1175
+ const originalName =
1176
+ typeof file?.name === 'string' ? path.basename(file.name.trim()) : ''
1177
+ const storedName = `${id}-${safeContextFileName(originalName || 'file')}`
1178
+ fs.writeFileSync(contextFileAbsolute(dir, storedName), bytes)
1179
+ next.push({
1180
+ id,
1181
+ name: originalName || storedName,
1182
+ storedName,
1183
+ mimeType:
1184
+ typeof file?.mimeType === 'string' && file.mimeType.trim()
1185
+ ? file.mimeType.trim()
1186
+ : 'application/octet-stream',
1187
+ size: bytes.length,
1188
+ })
1189
+ }
1190
+
1191
+ manifest.contextFiles = next
1192
+ writeManifest(dataDir, manifest)
1193
+ return manifest
1194
+ }
1195
+
1196
+ export function removeContextFile(dataDir, sessionId, fileId) {
1197
+ const manifest = requireManifest(dataDir, sessionId)
1198
+ if (isTerminalSession(manifest)) {
1199
+ throw sessionStoppedError(sessionId)
1200
+ }
1201
+ const id = typeof fileId === 'string' ? fileId.trim() : ''
1202
+ if (!id) throw new Error('fileId is required')
1203
+ const existing = normalizeContextFiles(manifest.contextFiles)
1204
+ const item = existing.find((file) => file.id === id)
1205
+ if (!item) return manifest
1206
+ const dir = sessionPaths(dataDir, sessionId).context
1207
+ try {
1208
+ fs.unlinkSync(contextFileAbsolute(dir, item.storedName))
1209
+ } catch {
1210
+ // Drop the manifest entry even if the file is already gone.
1211
+ }
1212
+ manifest.contextFiles = existing.filter((file) => file.id !== id)
1213
+ writeManifest(dataDir, manifest)
1214
+ return manifest
1215
+ }
1216
+
996
1217
  function generateVisualizerSessionId(dataDir) {
997
1218
  for (let attempt = 0; attempt < 8; attempt += 1) {
998
1219
  const sessionId = `viz-${crypto.randomBytes(6).toString('hex')}`
@@ -1124,6 +1345,7 @@ export function reportPlan(dataDir, input) {
1124
1345
  activeDiffId: null,
1125
1346
  pendingInstruction: null,
1126
1347
  initialInstruction: null,
1348
+ contextFiles: [],
1127
1349
  workStartedAt: null,
1128
1350
  createdAt: now,
1129
1351
  updatedAt: now,
@@ -1299,6 +1521,7 @@ export function appendDiff(dataDir, targetRoot, input) {
1299
1521
  )
1300
1522
  manifest.activeDiffId = id
1301
1523
  manifest.phase = 'review'
1524
+ manifest.pendingInstruction = null
1302
1525
  manifest.workStartedAt = null
1303
1526
  manifest.diffs.push(entry)
1304
1527
  writeManifest(dataDir, manifest)
@@ -1370,11 +1593,12 @@ export function requestReplan(
1370
1593
  active.status = 'extend'
1371
1594
  active.instruction = guidance
1372
1595
  active.decidedAt = new Date().toISOString()
1373
- manifest.phase = 'replanning'
1596
+ manifest.phase = 'working'
1374
1597
  manifest.pendingInstruction = guidance
1375
1598
  manifest.workStartedAt = new Date().toISOString()
1376
1599
  writeManifest(dataDir, manifest)
1377
- if (targetRoot) materializeDiff(dataDir, targetRoot, sessionId, active.id)
1600
+ // Keep the current proposal on disk. The instruction updates that latest state.
1601
+ void targetRoot
1378
1602
  return manifest
1379
1603
  }
1380
1604
 
@@ -475,10 +475,11 @@ function Explorer({
475
475
 
476
476
  const flyAlongRelation = useCallback(
477
477
  (fromId: string, toId: string) => {
478
+ if (mode !== 'walk') return
478
479
  const [x, z] = walkPos.current
479
480
  travelToFile(relationTravelTarget(fromId, toId, x, z, layout.files), true)
480
481
  },
481
- [layout.files, travelToFile],
482
+ [layout.files, mode, travelToFile],
482
483
  )
483
484
 
484
485
  const runWorkflowAction = useCallback(
@@ -65,6 +65,26 @@ function normalizeAck(
65
65
  }
66
66
  }
67
67
 
68
+ function normalizeContextFiles(value: unknown): NonNullable<AgentIntent['contextFiles']> {
69
+ if (!Array.isArray(value)) return []
70
+ return value.flatMap((item) => {
71
+ if (!item || typeof item !== 'object') return []
72
+ const id = (item as { id?: unknown }).id
73
+ const name = (item as { name?: unknown }).name
74
+ const mimeType = (item as { mimeType?: unknown }).mimeType
75
+ const size = (item as { size?: unknown }).size
76
+ if (
77
+ typeof id !== 'string' ||
78
+ typeof name !== 'string' ||
79
+ typeof mimeType !== 'string' ||
80
+ typeof size !== 'number'
81
+ ) {
82
+ return []
83
+ }
84
+ return [{ id, name, mimeType, size }]
85
+ })
86
+ }
87
+
68
88
  export const emptyIntent: AgentIntent = {
69
89
  updatedAt: null,
70
90
  showMap: false,
@@ -101,6 +121,7 @@ export const emptyIntent: AgentIntent = {
101
121
  listening: false,
102
122
  lastAck: null,
103
123
  initialInstruction: null,
124
+ contextFiles: [],
104
125
  creationMode: false,
105
126
  canEnterBlueprint: false,
106
127
  blueprintHidden: false,
@@ -155,6 +176,7 @@ function normalize(data: Partial<AgentIntent> | null | undefined): AgentIntent {
155
176
  lastAck: normalizeAck(data?.lastAck),
156
177
  initialInstruction:
157
178
  typeof data?.initialInstruction === 'string' ? data.initialInstruction : null,
179
+ contextFiles: normalizeContextFiles(data?.contextFiles),
158
180
  creationMode: Boolean(data?.creationMode),
159
181
  canEnterBlueprint: Boolean(data?.canEnterBlueprint),
160
182
  blueprintHidden: Boolean(data?.blueprintHidden),
@@ -352,6 +374,50 @@ export function persistInitialInstruction(sessionId: string, instruction: string
352
374
  })
353
375
  }
354
376
 
377
+ export async function persistAddContextFiles(
378
+ sessionId: string,
379
+ files: Array<{ name: string; mimeType: string; contentBase64: string }>,
380
+ ) {
381
+ const response = await fetch('/api/agent-intent', {
382
+ method: 'POST',
383
+ headers: { 'Content-Type': 'application/json' },
384
+ body: JSON.stringify({
385
+ action: 'add_context_files',
386
+ sessionId,
387
+ files,
388
+ }),
389
+ })
390
+ if (!response.ok) {
391
+ const detail = await response.text()
392
+ let message = detail || 'Could not attach files'
393
+ try {
394
+ const parsed = JSON.parse(detail) as { error?: string }
395
+ if (parsed?.error) message = parsed.error
396
+ } catch {
397
+ // Use the raw body when it is not JSON.
398
+ }
399
+ throw new Error(message)
400
+ }
401
+ return normalize((await response.json()) as AgentIntent)
402
+ }
403
+
404
+ export async function persistRemoveContextFile(sessionId: string, fileId: string) {
405
+ const response = await fetch('/api/agent-intent', {
406
+ method: 'POST',
407
+ headers: { 'Content-Type': 'application/json' },
408
+ body: JSON.stringify({
409
+ action: 'remove_context_file',
410
+ sessionId,
411
+ fileId,
412
+ }),
413
+ })
414
+ if (!response.ok) {
415
+ const detail = await response.text()
416
+ throw new Error(detail || 'Could not remove file')
417
+ }
418
+ return normalize((await response.json()) as AgentIntent)
419
+ }
420
+
355
421
  export function persistSessionFocus(sessionId: string) {
356
422
  fetch('/api/agent-intent', {
357
423
  method: 'POST',
@@ -1133,6 +1133,71 @@ button {
1133
1133
  margin-top: 0;
1134
1134
  }
1135
1135
 
1136
+ .hud-context {
1137
+ display: grid;
1138
+ gap: 8px;
1139
+ margin-top: 8px;
1140
+ }
1141
+
1142
+ .hud-context-drop {
1143
+ width: 100%;
1144
+ min-height: 52px;
1145
+ padding: 10px 8px;
1146
+ color: #8b95a5;
1147
+ background: var(--vc-surface);
1148
+ border: 1px dashed #3a4250;
1149
+ cursor: pointer;
1150
+ font: 12px/1.4 Inter, ui-sans-serif, system-ui, sans-serif;
1151
+ text-align: center;
1152
+ }
1153
+
1154
+ .hud-context-drop:hover,
1155
+ .hud-context-drop:focus-visible,
1156
+ .hud-context-drop[data-over='true'] {
1157
+ color: #e7ebf2;
1158
+ border-color: #d6b56a;
1159
+ outline: none;
1160
+ }
1161
+
1162
+ .hud-context-drop[data-busy='true'],
1163
+ .hud-context-drop:disabled {
1164
+ cursor: wait;
1165
+ opacity: 0.7;
1166
+ }
1167
+
1168
+ .hud-context-list {
1169
+ display: grid;
1170
+ gap: 4px;
1171
+ margin: 0;
1172
+ padding: 0;
1173
+ list-style: none;
1174
+ }
1175
+
1176
+ .hud-context-list li {
1177
+ display: grid;
1178
+ grid-template-columns: minmax(0, 1fr) auto auto;
1179
+ align-items: center;
1180
+ gap: 6px;
1181
+ color: #e7ebf2;
1182
+ font-size: 11px;
1183
+ }
1184
+
1185
+ .hud-context-name {
1186
+ overflow: hidden;
1187
+ text-overflow: ellipsis;
1188
+ white-space: nowrap;
1189
+ }
1190
+
1191
+ .hud-context-size {
1192
+ color: #8b95a5;
1193
+ }
1194
+
1195
+ .hud-context-error {
1196
+ margin: 0;
1197
+ color: #ff8a9b;
1198
+ font-size: 11px;
1199
+ }
1200
+
1136
1201
  .hud-instruction textarea {
1137
1202
  width: 100%;
1138
1203
  resize: vertical;
@@ -22,7 +22,6 @@ type MapViewProps = {
22
22
  onLand: (x: number, z: number) => void
23
23
  onSelect: (fileId: string | null) => void
24
24
  onSelectFolder: (folderPath: string | null) => void
25
- onTravelTo: (fromId: string, toId: string) => void
26
25
  onBlueprintMenu?: (menu: MapBlueprintMenu) => void
27
26
  }
28
27
 
@@ -36,7 +35,6 @@ export function MapView({
36
35
  onLand,
37
36
  onSelect,
38
37
  onSelectFolder,
39
- onTravelTo,
40
38
  onBlueprintMenu,
41
39
  }: MapViewProps) {
42
40
  const size = useThree((state) => state.size)
@@ -157,18 +155,11 @@ export function MapView({
157
155
 
158
156
  const pick = pickAt(event.clientX, event.clientY)
159
157
  if (!pick) return
160
- const { relationHit, fileHit } = pick
158
+ const { fileHit } = pick
161
159
  if (fileHit) {
162
160
  onSelect(fileHit.object.userData.fileId as string)
163
161
  return
164
162
  }
165
- if (relationHit) {
166
- onTravelTo(
167
- relationHit.object.userData.relationFrom as string,
168
- relationHit.object.userData.relationTo as string,
169
- )
170
- return
171
- }
172
163
 
173
164
  const hit = new THREE.Vector3()
174
165
  const plane = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0)
@@ -286,7 +277,6 @@ export function MapView({
286
277
  onBlueprintMenu,
287
278
  onSelect,
288
279
  onSelectFolder,
289
- onTravelTo,
290
280
  scene,
291
281
  selectedFolder,
292
282
  ])
@@ -237,7 +237,6 @@ export function World({
237
237
  onLand={onLand}
238
238
  onSelect={onSelect}
239
239
  onSelectFolder={onSelectFolder}
240
- onTravelTo={onTravelTo}
241
240
  onBlueprintMenu={
242
241
  mapping && !placing && onBlueprintMenu ? onBlueprintMenu : undefined
243
242
  }
@@ -324,6 +324,12 @@ export type AgentIntent = {
324
324
  at: string | null
325
325
  } | null
326
326
  initialInstruction?: string | null
327
+ contextFiles?: Array<{
328
+ id: string
329
+ name: string
330
+ mimeType: string
331
+ size: number
332
+ }>
327
333
  creationMode: boolean
328
334
  canEnterBlueprint: boolean
329
335
  blueprintHidden?: boolean
@@ -1,5 +1,5 @@
1
1
  import { useEffect, useRef, useState, type ReactNode } from 'react'
2
- import { persistInitialInstruction } from '../agentIntent'
2
+ import { persistAddContextFiles, persistInitialInstruction, persistRemoveContextFile } from '../agentIntent'
3
3
  import { NameInput } from './NameInput'
4
4
  import { SelectionThumbnail } from '../scene/SelectionThumbnail'
5
5
  import {
@@ -450,6 +450,119 @@ function InitialInstructionField({
450
450
  )
451
451
  }
452
452
 
453
+ function formatFileSize(bytes: number) {
454
+ if (bytes < 1024) return `${bytes} B`
455
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`
456
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`
457
+ }
458
+
459
+ async function fileToBase64(file: File) {
460
+ const bytes = new Uint8Array(await file.arrayBuffer())
461
+ const chunk = 0x8000
462
+ let binary = ''
463
+ for (let offset = 0; offset < bytes.length; offset += chunk) {
464
+ binary += String.fromCharCode(...bytes.subarray(offset, offset + chunk))
465
+ }
466
+ return btoa(binary)
467
+ }
468
+
469
+ type ContextFileInfo = {
470
+ id: string
471
+ name: string
472
+ mimeType: string
473
+ size: number
474
+ }
475
+
476
+ function ContextFileDrop({
477
+ files,
478
+ busy,
479
+ error,
480
+ onAdd,
481
+ onRemove,
482
+ }: {
483
+ files: ContextFileInfo[]
484
+ busy: boolean
485
+ error: string | null
486
+ onAdd: (files: File[]) => void
487
+ onRemove: (fileId: string) => void
488
+ }) {
489
+ const inputRef = useRef<HTMLInputElement>(null)
490
+ const [over, setOver] = useState(false)
491
+
492
+ const takeFiles = (list: FileList | File[] | null) => {
493
+ if (!list || busy) return
494
+ const next = [...list].filter((file) => file.size > 0)
495
+ if (next.length > 0) onAdd(next)
496
+ }
497
+
498
+ return (
499
+ <div className="hud-context">
500
+ <button
501
+ className="hud-context-drop"
502
+ type="button"
503
+ data-over={over}
504
+ data-busy={busy}
505
+ disabled={busy}
506
+ aria-label="Attach files for the LLM"
507
+ onDragEnter={(event) => {
508
+ event.preventDefault()
509
+ if (event.dataTransfer.types.includes('Files')) setOver(true)
510
+ }}
511
+ onDragOver={(event) => {
512
+ event.preventDefault()
513
+ event.dataTransfer.dropEffect = 'copy'
514
+ }}
515
+ onDragLeave={(event) => {
516
+ if (event.currentTarget.contains(event.relatedTarget as Node)) return
517
+ setOver(false)
518
+ }}
519
+ onDrop={(event) => {
520
+ event.preventDefault()
521
+ event.stopPropagation()
522
+ setOver(false)
523
+ takeFiles(event.dataTransfer.files)
524
+ }}
525
+ onKeyDown={(event) => event.stopPropagation()}
526
+ onClick={() => inputRef.current?.click()}
527
+ >
528
+ <input
529
+ ref={inputRef}
530
+ type="file"
531
+ multiple
532
+ hidden
533
+ onChange={(event) => {
534
+ takeFiles(event.target.files)
535
+ event.target.value = ''
536
+ }}
537
+ />
538
+ {busy ? 'Attaching…' : 'Drop files or click to attach'}
539
+ </button>
540
+ {files.length > 0 && (
541
+ <ul className="hud-context-list">
542
+ {files.map((file) => (
543
+ <li key={file.id}>
544
+ <span className="hud-context-name" title={file.name}>
545
+ {file.name}
546
+ </span>
547
+ <span className="hud-context-size">{formatFileSize(file.size)}</span>
548
+ <button
549
+ className="hud-item-remove"
550
+ type="button"
551
+ aria-label={`Remove ${file.name}`}
552
+ disabled={busy}
553
+ onClick={() => onRemove(file.id)}
554
+ >
555
+ ×
556
+ </button>
557
+ </li>
558
+ ))}
559
+ </ul>
560
+ )}
561
+ {error ? <p className="hud-context-error">{error}</p> : null}
562
+ </div>
563
+ )
564
+ }
565
+
453
566
  function blueprintIsDefined(intent: AgentIntent) {
454
567
  return (
455
568
  (intent.userCreatedBlocks?.length ?? 0) > 0 ||
@@ -464,24 +577,53 @@ function blueprintIsDefined(intent: AgentIntent) {
464
577
  function HandshakeSetup({
465
578
  instruction,
466
579
  onInstructionChange,
580
+ contextFiles,
581
+ contextBusy,
582
+ contextError,
583
+ onAddContextFiles,
584
+ onRemoveContextFile,
467
585
  blueprintDefined,
468
586
  awaitingAttach,
469
587
  nextAttachLabel,
470
588
  }: {
471
589
  instruction: string
472
590
  onInstructionChange: (value: string) => void
591
+ contextFiles: ContextFileInfo[]
592
+ contextBusy: boolean
593
+ contextError: string | null
594
+ onAddContextFiles: (files: File[]) => void
595
+ onRemoveContextFile: (fileId: string) => void
473
596
  blueprintDefined: boolean
474
597
  awaitingAttach: boolean
475
598
  nextAttachLabel: string | null
476
599
  }) {
477
600
  return (
478
601
  <div className="hud-setup">
479
- <section className="hud-setup-section">
602
+ <section
603
+ className="hud-setup-section"
604
+ onDragOver={(event) => {
605
+ if (event.dataTransfer.types.includes('Files')) event.preventDefault()
606
+ }}
607
+ onDrop={(event) => {
608
+ event.preventDefault()
609
+ const dropped = [...(event.dataTransfer.files ?? [])].filter(
610
+ (file) => file.size > 0,
611
+ )
612
+ if (dropped.length > 0) onAddContextFiles(dropped)
613
+ }}
614
+ >
480
615
  <h2 className="hud-setup-heading">Instructions</h2>
481
616
  <InitialInstructionField
482
617
  value={instruction}
483
618
  onChange={onInstructionChange}
484
619
  />
620
+ <ContextFileDrop
621
+ files={contextFiles}
622
+ busy={contextBusy}
623
+ error={contextError}
624
+ onAdd={onAddContextFiles}
625
+ onRemove={onRemoveContextFile}
626
+ />
485
627
  </section>
486
628
  <section className="hud-setup-section">
487
629
  <h2 className="hud-setup-heading">
@@ -649,6 +791,11 @@ function SessionPanel({
649
791
  const [initialInstruction, setInitialInstruction] = useState(
650
792
  () => intent.initialInstruction ?? '',
651
793
  )
794
+ const [contextFiles, setContextFiles] = useState<ContextFileInfo[]>(
795
+ () => intent.contextFiles ?? [],
796
+ )
797
+ const [contextBusy, setContextBusy] = useState(false)
798
+ const [contextError, setContextError] = useState<string | null>(null)
652
799
  const sessionId = intent.sessionId
653
800
  const pending = intent.status === 'pending' && intent.isActiveDiff
654
801
  const askingBlueprint = intent.status === 'blueprint_ask'
@@ -710,8 +857,15 @@ function SessionPanel({
710
857
 
711
858
  useEffect(() => {
712
859
  setInitialInstruction(intent.initialInstruction ?? '')
860
+ setContextFiles(intent.contextFiles ?? [])
861
+ setContextError(null)
713
862
  }, [sessionId])
714
863
 
864
+ useEffect(() => {
865
+ if (contextBusy) return
866
+ setContextFiles(intent.contextFiles ?? [])
867
+ }, [contextBusy, intent.contextFiles])
868
+
715
869
  if (!sessionId || !isReviewingIntent(intent.status)) return null
716
870
 
717
871
  const updateInitialInstruction = (value: string) => {
@@ -719,6 +873,43 @@ function SessionPanel({
719
873
  persistInitialInstruction(sessionId, value)
720
874
  }
721
875
 
876
+ const addContextFiles = (files: File[]) => {
877
+ setContextBusy(true)
878
+ setContextError(null)
879
+ void Promise.all(
880
+ files.map(async (file) => ({
881
+ name: file.name,
882
+ mimeType: file.type || 'application/octet-stream',
883
+ contentBase64: await fileToBase64(file),
884
+ })),
885
+ )
886
+ .then((payload) => persistAddContextFiles(sessionId, payload))
887
+ .then((next) => {
888
+ setContextFiles(next.contextFiles ?? [])
889
+ })
890
+ .catch((caught) => {
891
+ setContextError(
892
+ caught instanceof Error ? caught.message : 'Could not attach files',
893
+ )
894
+ })
895
+ .finally(() => setContextBusy(false))
896
+ }
897
+
898
+ const removeContextFile = (fileId: string) => {
899
+ setContextBusy(true)
900
+ setContextError(null)
901
+ void persistRemoveContextFile(sessionId, fileId)
902
+ .then((next) => {
903
+ setContextFiles(next.contextFiles ?? [])
904
+ })
905
+ .catch((caught) => {
906
+ setContextError(
907
+ caught instanceof Error ? caught.message : 'Could not remove file',
908
+ )
909
+ })
910
+ .finally(() => setContextBusy(false))
911
+ }
912
+
722
913
  const showInitialInstruction =
723
914
  Boolean(intent.awaitingAttach) &&
724
915
  (askingBlueprint || sendingBlueprint || preparing)
@@ -800,6 +991,11 @@ function SessionPanel({
800
991
  <HandshakeSetup
801
992
  instruction={initialInstruction}
802
993
  onInstructionChange={updateInitialInstruction}
994
+ contextFiles={contextFiles}
995
+ contextBusy={contextBusy}
996
+ contextError={contextError}
997
+ onAddContextFiles={addContextFiles}
998
+ onRemoveContextFile={removeContextFile}
803
999
  blueprintDefined={blueprintIsDefined(intent)}
804
1000
  awaitingAttach={Boolean(intent.awaitingAttach)}
805
1001
  nextAttachLabel={queuedBehind}
@@ -1437,7 +1633,6 @@ function explorerInstructions({
1437
1633
  },
1438
1634
  ]
1439
1635
  : []),
1440
- { id: 'click-line', keys: ['Click'], label: 'A line to fly there' },
1441
1636
  {
1442
1637
  id: 'option-click-walk',
1443
1638
  keys: ['Option', 'Click'],
@@ -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,
@@ -265,6 +267,8 @@ async function decideIntent(req: IncomingMessage, res: ServerResponse) {
265
267
  addedVariables?: unknown[]
266
268
  addedImports?: unknown[]
267
269
  notes?: unknown[]
270
+ files?: unknown[]
271
+ fileId?: string
268
272
  }
269
273
  const action = body.action
270
274
  const blueprintActions = new Set([
@@ -288,6 +292,8 @@ async function decideIntent(req: IncomingMessage, res: ServerResponse) {
288
292
  action !== 'focus' &&
289
293
  action !== 'set_step_by_step' &&
290
294
  action !== 'set_initial_instruction' &&
295
+ action !== 'add_context_files' &&
296
+ action !== 'remove_context_file' &&
291
297
  action !== 'setup_session'
292
298
  ) {
293
299
  sendJson(res, 400, { error: 'invalid workflow action' })
@@ -342,7 +348,6 @@ async function decideIntent(req: IncomingMessage, res: ServerResponse) {
342
348
  body.instruction ?? '',
343
349
  targetRoot,
344
350
  )
345
- rescanTarget('after withdrawing a patch')
346
351
  } else if (action === 'blueprint_yes') {
347
352
  answerBlueprint(dataDir, body.sessionId, true)
348
353
  } else if (action === 'blueprint_no') {
@@ -381,6 +386,14 @@ async function decideIntent(req: IncomingMessage, res: ServerResponse) {
381
386
  return
382
387
  } else if (action === 'set_initial_instruction') {
383
388
  setInitialInstruction(dataDir, body.sessionId, body.instruction ?? '')
389
+ } else if (action === 'add_context_files') {
390
+ addContextFiles(dataDir, body.sessionId, body.files ?? [])
391
+ } else if (action === 'remove_context_file') {
392
+ if (!body.fileId) {
393
+ sendJson(res, 400, { error: 'fileId is required' })
394
+ return
395
+ }
396
+ removeContextFile(dataDir, body.sessionId, body.fileId)
384
397
  } else if (action === 'focus') {
385
398
  focusSession(dataDir, body.sessionId)
386
399
  } 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.19",
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.
@@ -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
@@ -220,7 +224,7 @@ npx inbase propose-patch --session "<session-id>" --clear
220
224
  from **Setup LLM session** in the map
221
225
  - Edit files on a direct chat request; reply with the `/skipinbase` line and stop
222
226
  - 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
227
+ - Skip `inbase wait-for-blueprint`; it returns immediately and provides the optional blueprint, instruction, and attached files
224
228
  - Treat the chat request, viewpoint, or your own plan as overriding an enabled blueprint
225
229
  - Skip, rename, relocate, or replace the shared `blueprint.json` files, islands, functions, variables, or imports when `enabled` is true
226
230
  - Silently differ from the blueprint; ask the user first