@jkwd/inbase 0.1.21 → 0.1.22

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.
Files changed (58) hide show
  1. package/README.md +13 -7
  2. package/apps/explorer/package.json +1 -0
  3. package/apps/explorer/scripts/explain-store.d.ts +135 -0
  4. package/apps/explorer/scripts/explain-store.mjs +666 -0
  5. package/apps/explorer/scripts/patch-lib.mjs +4 -0
  6. package/apps/explorer/scripts/scan-target.mjs +22 -5
  7. package/apps/explorer/scripts/session-store.d.ts +86 -11
  8. package/apps/explorer/scripts/session-store.mjs +371 -58
  9. package/apps/explorer/scripts/target-config.d.ts +38 -3
  10. package/apps/explorer/scripts/target-config.mjs +147 -3
  11. package/apps/explorer/src/App.tsx +1073 -158
  12. package/apps/explorer/src/agentIntent.ts +61 -7
  13. package/apps/explorer/src/codebase.ts +1 -1
  14. package/apps/explorer/src/devTargets.ts +66 -0
  15. package/apps/explorer/src/explain.ts +312 -0
  16. package/apps/explorer/src/index.css +874 -222
  17. package/apps/explorer/src/layout.ts +55 -0
  18. package/apps/explorer/src/scene/DistantFileBlocks.tsx +4 -2
  19. package/apps/explorer/src/scene/FileBlock.tsx +95 -72
  20. package/apps/explorer/src/scene/FolderArea.tsx +55 -32
  21. package/apps/explorer/src/scene/MapView.tsx +506 -33
  22. package/apps/explorer/src/scene/RelationLines.tsx +7 -0
  23. package/apps/explorer/src/scene/World.tsx +146 -32
  24. package/apps/explorer/src/speech.ts +228 -0
  25. package/apps/explorer/src/theme.ts +29 -0
  26. package/apps/explorer/src/types.ts +98 -8
  27. package/apps/explorer/src/ui/CanvasErrorBoundary.tsx +1 -1
  28. package/apps/explorer/src/ui/ExplainAskCard.tsx +142 -0
  29. package/apps/explorer/src/ui/ExplainHud.tsx +524 -0
  30. package/apps/explorer/src/ui/ExplainInfoPanel.tsx +135 -0
  31. package/apps/explorer/src/ui/ExplainPointer.tsx +73 -0
  32. package/apps/explorer/src/ui/EyeIcon.tsx +38 -1
  33. package/apps/explorer/src/ui/HUD.tsx +1067 -795
  34. package/apps/explorer/src/ui/NameInput.tsx +114 -5
  35. package/apps/explorer/src/userContext.ts +0 -11
  36. package/apps/explorer/src/userCreated.ts +54 -1
  37. package/apps/explorer/vite.config.ts +173 -26
  38. package/bin/inbase.mjs +11 -2
  39. package/bin/project.mjs +1 -1
  40. package/bin/session.mjs +287 -38
  41. package/package.json +4 -1
  42. package/skill/commands/amber.md +23 -0
  43. package/skill/commands/blue.md +13 -0
  44. package/skill/commands/coral.md +23 -0
  45. package/skill/commands/explain.md +77 -0
  46. package/skill/commands/green.md +23 -0
  47. package/skill/commands/inbase.md +7 -5
  48. package/skill/commands/lime.md +23 -0
  49. package/skill/commands/orange.md +23 -0
  50. package/skill/commands/purple.md +23 -0
  51. package/skill/commands/red.md +23 -0
  52. package/skill/commands/skipinbase.md +1 -1
  53. package/skill/commands/violet.md +23 -0
  54. package/skill/commands/yellow.md +23 -0
  55. package/skill/inbase/SKILL.md +124 -76
  56. package/apps/explorer/src/scene/BlockPlacer.tsx +0 -78
  57. package/apps/explorer/src/scene/IslandPlacer.tsx +0 -31
  58. package/apps/explorer/src/scene/SelectionThumbnail.tsx +0 -1069
@@ -1,28 +1,63 @@
1
- import { useEffect, useRef } from 'react'
1
+ import { useEffect, useRef, useState } from 'react'
2
+ import { beginKeyboardIsolation } from '../keyboard'
2
3
 
3
4
  type NameInputProps = {
4
5
  placeholder: string
6
+ fallbackName?: string
5
7
  onCommit: (name: string) => void
6
8
  onCancel: () => void
7
9
  }
8
10
 
9
- export function NameInput({ placeholder, onCommit, onCancel }: NameInputProps) {
11
+ export function NameInput({
12
+ placeholder,
13
+ fallbackName,
14
+ onCommit,
15
+ onCancel,
16
+ }: NameInputProps) {
10
17
  const input = useRef<HTMLInputElement>(null)
18
+ const form = useRef<HTMLFormElement>(null)
19
+ const onCommitRef = useRef(onCommit)
20
+ const fallbackRef = useRef(fallbackName)
21
+ onCommitRef.current = onCommit
22
+ fallbackRef.current = fallbackName
11
23
 
12
24
  useEffect(() => {
13
25
  const timer = window.setTimeout(() => input.current?.focus(), 40)
14
26
  return () => window.clearTimeout(timer)
15
27
  }, [])
16
28
 
29
+ const commit = (allowFallback: boolean) => {
30
+ const typed = input.current?.value ?? ''
31
+ const value = typed.trim()
32
+ ? typed
33
+ : allowFallback
34
+ ? (fallbackRef.current ?? '')
35
+ : ''
36
+ if (!value.trim()) return
37
+ onCommitRef.current(value)
38
+ }
39
+
40
+ useEffect(() => {
41
+ const onPointerDown = (event: PointerEvent) => {
42
+ if (event.button !== 0) return
43
+ const target = event.target
44
+ if (!(target instanceof Element)) return
45
+ if (form.current?.contains(target)) return
46
+ if (!target.closest('.stage')) return
47
+ commit(true)
48
+ }
49
+ window.addEventListener('pointerdown', onPointerDown, true)
50
+ return () => window.removeEventListener('pointerdown', onPointerDown, true)
51
+ }, [])
52
+
17
53
  return (
18
54
  <form
55
+ ref={form}
19
56
  className="block-name-form"
20
57
  onPointerDown={(event) => event.stopPropagation()}
21
58
  onSubmit={(event) => {
22
59
  event.preventDefault()
23
- const value = input.current?.value ?? ''
24
- if (!value.trim()) return
25
- onCommit(value)
60
+ commit(false)
26
61
  }}
27
62
  >
28
63
  <input
@@ -43,3 +78,77 @@ export function NameInput({ placeholder, onCommit, onCancel }: NameInputProps) {
43
78
  </form>
44
79
  )
45
80
  }
81
+
82
+ export function InfoNameField({
83
+ name,
84
+ onRename,
85
+ }: {
86
+ name: string
87
+ onRename: (name: string) => boolean
88
+ }) {
89
+ const [value, setValue] = useState(name)
90
+ const [focused, setFocused] = useState(false)
91
+ const nameRef = useRef(name)
92
+ const skipCommit = useRef(false)
93
+ nameRef.current = name
94
+
95
+ useEffect(() => {
96
+ setValue(name)
97
+ }, [name])
98
+
99
+ useEffect(() => {
100
+ if (!focused) return
101
+ return beginKeyboardIsolation()
102
+ }, [focused])
103
+
104
+ const commit = () => {
105
+ const trimmed = value.trim()
106
+ if (!trimmed) {
107
+ setValue(nameRef.current)
108
+ return
109
+ }
110
+ if (trimmed === nameRef.current) return
111
+ if (!onRename(trimmed)) setValue(nameRef.current)
112
+ }
113
+
114
+ return (
115
+ <input
116
+ className="hud-info-name"
117
+ value={value}
118
+ aria-label="File name"
119
+ title="Rename file"
120
+ autoComplete="off"
121
+ spellCheck={false}
122
+ onChange={(event) => setValue(event.target.value)}
123
+ onFocus={(event) => {
124
+ setFocused(true)
125
+ const field = event.currentTarget
126
+ window.requestAnimationFrame(() => field.select())
127
+ }}
128
+ onBlur={() => {
129
+ setFocused(false)
130
+ if (skipCommit.current) {
131
+ skipCommit.current = false
132
+ return
133
+ }
134
+ commit()
135
+ }}
136
+ onPointerDown={(event) => event.stopPropagation()}
137
+ onKeyDown={(event) => {
138
+ event.stopPropagation()
139
+ if (event.code === 'Enter') {
140
+ event.preventDefault()
141
+ commit()
142
+ skipCommit.current = true
143
+ event.currentTarget.blur()
144
+ }
145
+ if (event.code === 'Escape') {
146
+ event.preventDefault()
147
+ skipCommit.current = true
148
+ setValue(nameRef.current)
149
+ event.currentTarget.blur()
150
+ }
151
+ }}
152
+ />
153
+ )
154
+ }
@@ -28,16 +28,6 @@ export async function fetchUserContext(): Promise<UserContext | null> {
28
28
  }
29
29
  }
30
30
 
31
- export function persistFollowLook(followLook: boolean) {
32
- fetch('/api/user-context', {
33
- method: 'POST',
34
- headers: { 'Content-Type': 'application/json' },
35
- body: `${JSON.stringify({ followLook })}\n`,
36
- }).catch(() => {
37
- lastWritten = ''
38
- })
39
- }
40
-
41
31
  export function persistShowBranchChanges(showBranchChanges: boolean) {
42
32
  fetch('/api/user-context', {
43
33
  method: 'POST',
@@ -60,7 +50,6 @@ function flushUserContext() {
60
50
  pending = null
61
51
  if (!context) return
62
52
  const {
63
- followLook: _followLook,
64
53
  showBranchChanges: _showBranchChanges,
65
54
  userCreatedBlocks: _userCreatedBlocks,
66
55
  userCreatedIslands: _userCreatedIslands,
@@ -32,6 +32,7 @@ export function toCreatedFile(block: UserCreatedBlock): FileNode {
32
32
  symbols: [],
33
33
  imports: [],
34
34
  userCreated: true,
35
+ colorHex: block.colorHex,
35
36
  }
36
37
  }
37
38
 
@@ -174,6 +175,7 @@ export function withUserCreatedGraph(
174
175
  files: [],
175
176
  children: [],
176
177
  userCreated: true,
178
+ colorHex: island.colorHex,
177
179
  })
178
180
  if (parent) {
179
181
  const parentFolder = folders.get(parent)
@@ -259,7 +261,12 @@ function overlayIslands(
259
261
  for (const island of islands) {
260
262
  const id = islandKey(island)
261
263
  if (layout.folders[id]) {
262
- folders[id] = { ...folders[id], added: true, name: island.name || folders[id].name }
264
+ folders[id] = {
265
+ ...folders[id],
266
+ added: true,
267
+ name: island.name || folders[id].name,
268
+ colorHex: island.colorHex ?? folders[id].colorHex,
269
+ }
263
270
  continue
264
271
  }
265
272
  const parentPath = island.parent
@@ -286,6 +293,7 @@ function overlayIslands(
286
293
  width,
287
294
  depth,
288
295
  added: true,
296
+ colorHex: island.colorHex,
289
297
  }
290
298
  bridges.push({
291
299
  id: `${parentPath}→${id}`,
@@ -534,6 +542,51 @@ export function dropBlueprintSymbolPointer(
534
542
  )
535
543
  }
536
544
 
545
+ export function remapBlueprintFileId(
546
+ fromId: string,
547
+ toId: string,
548
+ data: {
549
+ functions: PatchSymbolAddition[]
550
+ variables: PatchSymbolAddition[]
551
+ imports: PatchImportAddition[]
552
+ notes: BlueprintNote[]
553
+ pointers: BlueprintPointer[]
554
+ },
555
+ ) {
556
+ if (!fromId || fromId === toId) return data
557
+ return {
558
+ functions: data.functions.map((item) =>
559
+ item.file === fromId ? { ...item, file: toId } : item,
560
+ ),
561
+ variables: data.variables.map((item) =>
562
+ item.file === fromId ? { ...item, file: toId } : item,
563
+ ),
564
+ imports: data.imports.map((item) => ({
565
+ ...item,
566
+ file: item.file === fromId ? toId : item.file,
567
+ from: item.from === fromId ? toId : item.from,
568
+ })),
569
+ notes: data.notes.map((item) =>
570
+ item.file === fromId ? { ...item, file: toId } : item,
571
+ ),
572
+ pointers: data.pointers.map((item) =>
573
+ item.kind !== 'folder' && item.path === fromId
574
+ ? { ...item, path: toId }
575
+ : item,
576
+ ),
577
+ }
578
+ }
579
+
580
+ export function blueprintImportRawFromFile(file: {
581
+ id: string
582
+ name: string
583
+ }): string {
584
+ const trimmed = file.name.trim()
585
+ const dot = trimmed.lastIndexOf('.')
586
+ const base = dot > 0 ? trimmed.slice(0, dot) : trimmed
587
+ return `${base || trimmed} from ${file.id}`
588
+ }
589
+
537
590
  export function parseBlueprintImport(
538
591
  raw: string,
539
592
  file: string,
@@ -8,18 +8,29 @@ import type { IncomingMessage, ServerResponse } from 'node:http'
8
8
  import { emptyIntent } from './scripts/patch-lib.mjs'
9
9
  import { readBranchChanges } from './scripts/branch-changes.mjs'
10
10
  import { writeRunningInstance, isolatedViteConfig, packageDirFromPackage } from '../../bin/project.mjs'
11
- import { dataDir, targetRoot } from './scripts/target-config.mjs'
11
+ import {
12
+ dataDir,
13
+ isWorkspaceDevSwitcherEnabled,
14
+ setWorkspaceTarget,
15
+ targetRoot,
16
+ workspaceDevTargetsState,
17
+ } from './scripts/target-config.mjs'
12
18
  import { editorFileUri, openInEditor } from './scripts/open-editor.mjs'
13
19
  import {
14
20
  answerBlueprint,
15
21
  clearDiffSessions,
22
+ ensureSessionPool,
16
23
  continueDiff,
17
24
  inspectTargetFile,
18
25
  invokeStep,
19
26
  listSessionIntents,
20
27
  nextAttachSessionId,
28
+ listOpenSessionIds,
21
29
  readActiveSession,
30
+ recycleDisconnectedSessions,
22
31
  requestReplan,
32
+ notifySessionExplain,
33
+ requestExplainProposal,
23
34
  sendBlueprint,
24
35
  sessionIntent,
25
36
  setInitialInstruction,
@@ -31,10 +42,20 @@ import {
31
42
  stopSession,
32
43
  updateBlueprint,
33
44
  readBlueprint,
45
+ listLocalBlueprints,
34
46
  setBlueprintHidden,
35
47
  clearBlueprint,
36
48
  cleanupBlueprint,
37
49
  } from './scripts/session-store.mjs'
50
+ import {
51
+ askExplainQuestion,
52
+ explainTargetLabel,
53
+ parseExplainTargetKind,
54
+ readExplain,
55
+ requestExplainTarget,
56
+ setExplainStep,
57
+ stopExplain,
58
+ } from './scripts/explain-store.mjs'
38
59
 
39
60
  const here = path.dirname(fileURLToPath(import.meta.url))
40
61
  const isolation = isolatedViteConfig(dataDir)
@@ -44,10 +65,12 @@ const scanScript = path.resolve(here, 'scripts/scan-target.mjs')
44
65
 
45
66
  // The rescan runs with cwd set to the explorer, so hand it the already-resolved
46
67
  // root and data dir instead of letting relative env values resolve differently.
47
- const scanEnv = {
48
- ...process.env,
49
- VISUAL_CODER_TARGET: targetRoot,
50
- INBASE_DATA_DIR: dataDir,
68
+ function scanEnv() {
69
+ return {
70
+ ...process.env,
71
+ VISUAL_CODER_TARGET: targetRoot,
72
+ INBASE_DATA_DIR: dataDir,
73
+ }
51
74
  }
52
75
 
53
76
  function readBody(req: IncomingMessage) {
@@ -65,7 +88,7 @@ function rescanTarget(when: string) {
65
88
  const scan = spawnSync(process.execPath, [scanScript], {
66
89
  cwd: here,
67
90
  encoding: 'utf8',
68
- env: scanEnv,
91
+ env: scanEnv(),
69
92
  })
70
93
  if (scan.status !== 0) {
71
94
  console.error(scan.stderr || scan.stdout || `scan failed ${when}`)
@@ -136,14 +159,24 @@ function jsonFilePlugin(): Plugin {
136
159
  return {
137
160
  name: 'visual-coder-json-files',
138
161
  configureServer(server) {
162
+ const serverPort = server.config.server.port ?? 5173
139
163
  writeRunningInstance({
140
164
  dataDir,
141
165
  targetRoot,
142
- port: server.config.server.port ?? 5173,
166
+ port: serverPort,
143
167
  })
144
- // Always boot with no LLM session. Leftover diffs and pointers are not restored.
168
+ // Discard leftover LLM sessions, then open 5 empty chat slots.
145
169
  clearDiffSessions(dataDir, targetRoot)
170
+ ensureSessionPool(dataDir)
146
171
  rescanTarget('after discarding leftover LLM sessions')
172
+ let lastLiveSessionKey = listOpenSessionIds(dataDir).join('\0')
173
+ function syncDisconnectedSessions() {
174
+ const recycled = recycleDisconnectedSessions(dataDir, targetRoot)
175
+ const liveKey = listOpenSessionIds(dataDir).join('\0')
176
+ const changed = recycled.length > 0 || liveKey !== lastLiveSessionKey
177
+ lastLiveSessionKey = liveKey
178
+ if (changed) rescanTarget('after LLM session reset')
179
+ }
147
180
  server.middlewares.use('/api/user-context', (req, res, next) => {
148
181
  if (req.method === 'GET') {
149
182
  sendJson(res, 200, readUserContext())
@@ -174,6 +207,7 @@ function jsonFilePlugin(): Plugin {
174
207
 
175
208
  server.middlewares.use('/api/agent-intent', (req, res, next) => {
176
209
  if (req.method === 'GET') {
210
+ syncDisconnectedSessions()
177
211
  const url = new URL(req.url ?? '/', 'http://visual-coder.local')
178
212
  const sessionId = url.searchParams.get('sessionId')
179
213
  const diffId = url.searchParams.get('diffId') ?? undefined
@@ -192,6 +226,7 @@ function jsonFilePlugin(): Plugin {
192
226
  nextAttachSessionId: nextAttachSessionId(dataDir),
193
227
  intents: listSessionIntents(dataDir, knownFileIds()),
194
228
  blueprint: readBlueprint(dataDir),
229
+ localBlueprints: listLocalBlueprints(dataDir),
195
230
  })
196
231
  return
197
232
  }
@@ -212,6 +247,18 @@ function jsonFilePlugin(): Plugin {
212
247
  next()
213
248
  })
214
249
 
250
+ server.middlewares.use('/api/explain', (req, res, next) => {
251
+ if (req.method === 'GET') {
252
+ sendJson(res, 200, readExplain(dataDir))
253
+ return
254
+ }
255
+ if (req.method === 'POST') {
256
+ void decideExplain(req, res)
257
+ return
258
+ }
259
+ next()
260
+ })
261
+
215
262
  server.middlewares.use('/api/inspect-file', (req, res, next) => {
216
263
  if (req.method === 'POST') {
217
264
  void inspectFile(req, res)
@@ -219,10 +266,113 @@ function jsonFilePlugin(): Plugin {
219
266
  }
220
267
  next()
221
268
  })
269
+
270
+ server.middlewares.use('/api/dev-targets', (req, res, next) => {
271
+ if (req.method === 'GET') {
272
+ sendJson(res, 200, workspaceDevTargetsState())
273
+ return
274
+ }
275
+ if (req.method === 'POST') {
276
+ void switchDevTarget(req, res, serverPort)
277
+ return
278
+ }
279
+ next()
280
+ })
222
281
  },
223
282
  }
224
283
  }
225
284
 
285
+ async function switchDevTarget(
286
+ req: IncomingMessage,
287
+ res: ServerResponse,
288
+ port: number,
289
+ ) {
290
+ if (!isWorkspaceDevSwitcherEnabled()) {
291
+ sendJson(res, 404, { error: 'dev target switcher is not available' })
292
+ return
293
+ }
294
+ try {
295
+ const body = JSON.parse(await readBody(req)) as { id?: string }
296
+ const id = body.id?.trim()
297
+ if (!id || id === 'custom') {
298
+ sendJson(res, 400, { error: 'id is required' })
299
+ return
300
+ }
301
+ setWorkspaceTarget(id)
302
+ writeRunningInstance({ dataDir, targetRoot, port })
303
+ stopExplain(dataDir)
304
+ clearDiffSessions(dataDir, targetRoot)
305
+ ensureSessionPool(dataDir)
306
+ if (!rescanTarget('after switching target')) {
307
+ sendJson(res, 500, { error: 'scan failed' })
308
+ return
309
+ }
310
+ sendJson(res, 200, {
311
+ ...workspaceDevTargetsState(),
312
+ codebase: readCodebase(),
313
+ })
314
+ } catch (error) {
315
+ const message = error instanceof Error ? error.message : 'invalid request'
316
+ sendJson(res, 400, { error: message })
317
+ }
318
+ }
319
+
320
+ async function decideExplain(req: IncomingMessage, res: ServerResponse) {
321
+ try {
322
+ const body = JSON.parse(await readBody(req)) as {
323
+ action?: string
324
+ step?: string | number
325
+ question?: string
326
+ kind?: string
327
+ path?: string
328
+ name?: string
329
+ sessionId?: string
330
+ }
331
+ if (body.action === 'stop') {
332
+ sendJson(res, 200, stopExplain(dataDir))
333
+ return
334
+ }
335
+ if (body.action === 'start') {
336
+ const next = requestExplainTarget(dataDir, {
337
+ kind: parseExplainTargetKind(body.kind) ?? 'file',
338
+ path: body.path ?? '',
339
+ name: body.name,
340
+ question: body.question,
341
+ })
342
+ const sessionId = body.sessionId?.trim()
343
+ if (sessionId && next.pendingStart) {
344
+ notifySessionExplain(
345
+ dataDir,
346
+ sessionId,
347
+ explainTargetLabel(next.pendingStart),
348
+ )
349
+ }
350
+ sendJson(res, 200, next)
351
+ return
352
+ }
353
+ if (body.action === 'set_step') {
354
+ if (body.step == null || body.step === '') {
355
+ sendJson(res, 400, { error: 'step is required' })
356
+ return
357
+ }
358
+ sendJson(res, 200, setExplainStep(dataDir, body.step))
359
+ return
360
+ }
361
+ if (body.action === 'ask') {
362
+ sendJson(
363
+ res,
364
+ 200,
365
+ askExplainQuestion(dataDir, body.step ?? '', body.question ?? ''),
366
+ )
367
+ return
368
+ }
369
+ sendJson(res, 400, { error: 'invalid explain action' })
370
+ } catch (error) {
371
+ const message = error instanceof Error ? error.message : 'invalid request'
372
+ sendJson(res, 400, { error: message })
373
+ }
374
+ }
375
+
226
376
  async function inspectFile(req: IncomingMessage, res: ServerResponse) {
227
377
  try {
228
378
  const body = JSON.parse(await readBody(req)) as {
@@ -262,6 +412,7 @@ async function decideIntent(req: IncomingMessage, res: ServerResponse) {
262
412
  step?: number
263
413
  stepByStep?: boolean
264
414
  hidden?: boolean
415
+ color?: string
265
416
  userCreatedBlocks?: unknown[]
266
417
  userCreatedIslands?: unknown[]
267
418
  addedFunctions?: unknown[]
@@ -283,6 +434,7 @@ async function decideIntent(req: IncomingMessage, res: ServerResponse) {
283
434
  action !== 'invoke' &&
284
435
  action !== 'continue' &&
285
436
  action !== 'instruct' &&
437
+ action !== 'explain_proposal' &&
286
438
  action !== 'stop' &&
287
439
  action !== 'blueprint_yes' &&
288
440
  action !== 'blueprint_no' &&
@@ -350,12 +502,15 @@ async function decideIntent(req: IncomingMessage, res: ServerResponse) {
350
502
  body.instruction ?? '',
351
503
  targetRoot,
352
504
  )
505
+ } else if (action === 'explain_proposal') {
506
+ requestExplainProposal(dataDir, body.sessionId, body.diffId)
353
507
  } else if (action === 'blueprint_yes') {
354
508
  answerBlueprint(dataDir, body.sessionId, true)
355
509
  } else if (action === 'blueprint_no') {
356
510
  answerBlueprint(dataDir, body.sessionId, false)
357
511
  } else if (action === 'blueprint_update') {
358
512
  updateBlueprint(dataDir, body.sessionId, {
513
+ color: body.color,
359
514
  userCreatedBlocks: body.userCreatedBlocks,
360
515
  userCreatedIslands: body.userCreatedIslands,
361
516
  addedFunctions: body.addedFunctions,
@@ -365,21 +520,13 @@ async function decideIntent(req: IncomingMessage, res: ServerResponse) {
365
520
  pointers: body.pointers,
366
521
  })
367
522
  } else if (action === 'blueprint_clear') {
368
- clearBlueprint(dataDir)
523
+ clearBlueprint(dataDir, body.color)
369
524
  } else if (action === 'blueprint_cleanup') {
370
- cleanupBlueprint(dataDir, knownFileIds(), knownFolderPaths())
525
+ cleanupBlueprint(dataDir, knownFileIds(), knownFolderPaths(), body.color)
371
526
  } else if (action === 'blueprint_set_hidden') {
372
- setBlueprintHidden(dataDir, Boolean(body.hidden))
527
+ setBlueprintHidden(dataDir, Boolean(body.hidden), body.color)
373
528
  } else if (action === 'blueprint_send') {
374
- sendBlueprint(dataDir, body.sessionId, {
375
- userCreatedBlocks: body.userCreatedBlocks,
376
- userCreatedIslands: body.userCreatedIslands,
377
- addedFunctions: body.addedFunctions,
378
- addedVariables: body.addedVariables,
379
- addedImports: body.addedImports,
380
- notes: body.notes,
381
- pointers: body.pointers,
382
- })
529
+ sendBlueprint(dataDir, body.sessionId)
383
530
  } else if (action === 'setup_session') {
384
531
  const manifest = setupSession(dataDir, {
385
532
  sessionId: body.sessionId,
@@ -453,11 +600,10 @@ function readUserContext() {
453
600
  >
454
601
  return {
455
602
  ...parsed,
456
- followLook: Boolean(parsed.followLook),
457
603
  showBranchChanges: Boolean(parsed.showBranchChanges),
458
604
  }
459
605
  } catch {
460
- return { followLook: false, showBranchChanges: false }
606
+ return { showBranchChanges: false }
461
607
  }
462
608
  }
463
609
 
@@ -468,15 +614,12 @@ async function writeUserContext(req: IncomingMessage, res: ServerResponse) {
468
614
  const next = {
469
615
  ...existing,
470
616
  ...incoming,
471
- followLook:
472
- typeof incoming.followLook === 'boolean'
473
- ? incoming.followLook
474
- : Boolean(existing.followLook),
475
617
  showBranchChanges:
476
618
  typeof incoming.showBranchChanges === 'boolean'
477
619
  ? incoming.showBranchChanges
478
620
  : Boolean(existing.showBranchChanges),
479
621
  }
622
+ delete next.followLook
480
623
  delete next.userCreatedBlocks
481
624
  delete next.userCreatedIslands
482
625
  fs.mkdirSync(path.dirname(userContextFile), { recursive: true })
@@ -517,6 +660,10 @@ export default defineConfig({
517
660
  '@react-three/fiber',
518
661
  '@react-three/drei',
519
662
  ],
663
+ exclude: ['kokoro-js', '@huggingface/transformers'],
664
+ },
665
+ worker: {
666
+ format: 'es',
520
667
  },
521
668
  server: {
522
669
  ...isolation.server,
package/bin/inbase.mjs CHANGED
@@ -18,6 +18,7 @@ import {
18
18
  import {
19
19
  proposePatch,
20
20
  reportPlan,
21
+ runExplain,
21
22
  startSession,
22
23
  attachSession,
23
24
  waitForApproval,
@@ -33,12 +34,16 @@ Usage:
33
34
 
34
35
  Agent commands (used by the Cursor skill):
35
36
  inbase start-session --session <id> --name "short name"
36
- inbase attach [--session <id>]
37
+ inbase attach [--session <id>] [--color <name>]
37
38
  inbase wait-for-blueprint --session <id>
38
39
  inbase report-plan --session <id> --feature "name" --steps "one" [--steps "two"]
39
40
  inbase wait-for-approval --session <id>
40
41
  inbase propose-patch --session <id> [file.patch|-]
41
42
  inbase propose-patch --session <id> --clear
43
+ inbase explain start --question "How does this work?"
44
+ inbase explain report --question "..." --step "Title" --body "..." --files path
45
+ inbase explain wait
46
+ inbase explain stop
42
47
 
43
48
  Options for run:
44
49
  --target <dir> Project to map (default: current directory)
@@ -112,7 +117,7 @@ async function runServer(args) {
112
117
  const local = server.resolvedUrls?.local?.[0] ?? `http://localhost:${port}/`
113
118
  console.log(`Inbase is mapping ${targetRoot}`)
114
119
  console.log(`Open ${local}`)
115
- console.log('Leave this running. In Cursor, the inbase skill talks to this server.')
120
+ console.log('Leave this running. Open a Cursor chat to connect 5 chats can be connected at once.')
116
121
  }
117
122
 
118
123
  export async function main(argv = process.argv.slice(2)) {
@@ -175,6 +180,10 @@ export async function main(argv = process.argv.slice(2)) {
175
180
  await proposePatch(args)
176
181
  return
177
182
  }
183
+ if (command === 'explain') {
184
+ await runExplain(args)
185
+ return
186
+ }
178
187
 
179
188
  console.error(`Unknown command: ${command}\n`)
180
189
  printHelp()
package/bin/project.mjs CHANGED
@@ -131,7 +131,7 @@ export function ensureDataDir(dataDir) {
131
131
  if (!fs.existsSync(userContextFile)) {
132
132
  fs.writeFileSync(
133
133
  userContextFile,
134
- `${JSON.stringify({ followLook: false, showBranchChanges: false }, null, 2)}\n`,
134
+ `${JSON.stringify({ showBranchChanges: false }, null, 2)}\n`,
135
135
  )
136
136
  }
137
137
  return dataDir