@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
@@ -0,0 +1,666 @@
1
+ import fs from 'node:fs'
2
+ import path from 'node:path'
3
+
4
+ export const EXPLAIN_FILE = 'explain.json'
5
+
6
+ function atomicWrite(file, contents) {
7
+ fs.mkdirSync(path.dirname(file), { recursive: true })
8
+ const temporary = `${file}.${process.pid}.tmp`
9
+ fs.writeFileSync(temporary, contents)
10
+ fs.renameSync(temporary, file)
11
+ }
12
+
13
+ function explainPath(dataDir) {
14
+ return path.join(dataDir, EXPLAIN_FILE)
15
+ }
16
+
17
+ function splitList(value) {
18
+ if (typeof value !== 'string') return []
19
+ return value
20
+ .split(',')
21
+ .map((item) => item.trim())
22
+ .filter(Boolean)
23
+ }
24
+
25
+ function parseRelation(value) {
26
+ if (typeof value !== 'string') return null
27
+ const index = value.lastIndexOf(':')
28
+ if (index <= 0 || index === value.length - 1) return null
29
+ const from = value.slice(0, index).trim()
30
+ const to = value.slice(index + 1).trim()
31
+ if (!from || !to) return null
32
+ return { from, to }
33
+ }
34
+
35
+ function uniqueStrings(values) {
36
+ const seen = new Set()
37
+ const items = []
38
+ for (const value of values) {
39
+ if (typeof value !== 'string') continue
40
+ const next = value.trim()
41
+ if (!next || seen.has(next)) continue
42
+ seen.add(next)
43
+ items.push(next)
44
+ }
45
+ return items
46
+ }
47
+
48
+ function uniqueRelations(values) {
49
+ const seen = new Set()
50
+ const items = []
51
+ for (const value of values) {
52
+ const edge =
53
+ value && typeof value === 'object'
54
+ ? {
55
+ from: typeof value.from === 'string' ? value.from.trim() : '',
56
+ to: typeof value.to === 'string' ? value.to.trim() : '',
57
+ }
58
+ : parseRelation(value)
59
+ if (!edge?.from || !edge?.to) continue
60
+ const key = `${edge.from}->${edge.to}`
61
+ if (seen.has(key)) continue
62
+ seen.add(key)
63
+ items.push(edge)
64
+ }
65
+ return items
66
+ }
67
+
68
+ export function isExplainStepId(value) {
69
+ return typeof value === 'string' && /^\d+(?:\.\d+)*$/.test(value)
70
+ }
71
+
72
+ export function explainStepId(value, fallback = '') {
73
+ if (typeof value === 'number' && Number.isInteger(value) && value > 0) {
74
+ return String(value)
75
+ }
76
+ if (typeof value === 'string') {
77
+ const next = value.trim()
78
+ if (isExplainStepId(next)) return next
79
+ }
80
+ return fallback
81
+ }
82
+
83
+ export function isExplainDescendant(id, parent) {
84
+ const child = explainStepId(id, '')
85
+ const root = explainStepId(parent, '')
86
+ return Boolean(child && root && child.startsWith(`${root}.`))
87
+ }
88
+
89
+ export function topLevelExplainStepId(id) {
90
+ const next = explainStepId(id, '')
91
+ const dot = next.indexOf('.')
92
+ return dot === -1 ? next : next.slice(0, dot)
93
+ }
94
+
95
+ export function isExplainSubStep(id) {
96
+ return explainStepId(id, '').includes('.')
97
+ }
98
+
99
+ export function stripExplainSubSteps(steps) {
100
+ return (Array.isArray(steps) ? steps : [])
101
+ .filter((step) => !isExplainSubStep(step?.index))
102
+ .map((step) => ({ ...step, asked: '' }))
103
+ }
104
+
105
+ function parseSymbolKind(value) {
106
+ const key = typeof value === 'string' ? value.trim().toLowerCase() : ''
107
+ if (key === 'function' || key === 'fn') return 'function'
108
+ if (key === 'variable' || key === 'var') return 'variable'
109
+ if (key === 'class') return 'class'
110
+ if (key === 'file') return 'file'
111
+ if (key === 'symbol') return 'symbol'
112
+ return null
113
+ }
114
+
115
+ export function parseExplainSymbolRef(value) {
116
+ if (typeof value !== 'string') return null
117
+ const trimmed = value.trim()
118
+ if (!trimmed) return null
119
+ if (trimmed.toLowerCase() === 'file') return { kind: 'file', name: '' }
120
+ const index = trimmed.indexOf(':')
121
+ if (index > 0) {
122
+ const kind = parseSymbolKind(trimmed.slice(0, index))
123
+ const name = trimmed.slice(index + 1).trim()
124
+ if (kind === 'file') return { kind: 'file', name }
125
+ if (kind && name) return { kind, name }
126
+ }
127
+ return { kind: 'symbol', name: trimmed }
128
+ }
129
+
130
+ function uniqueSymbolRefs(values) {
131
+ const seen = new Set()
132
+ const items = []
133
+ for (const value of values) {
134
+ const ref =
135
+ value && typeof value === 'object'
136
+ ? parseExplainSymbolRef(
137
+ `${typeof value.kind === 'string' ? value.kind : 'symbol'}:${typeof value.name === 'string' ? value.name : ''}`,
138
+ ) ??
139
+ (typeof value.name === 'string' ? parseExplainSymbolRef(value.name) : null)
140
+ : parseExplainSymbolRef(value)
141
+ if (!ref) continue
142
+ const key = `${ref.kind}:${ref.name}`
143
+ if (seen.has(key)) continue
144
+ seen.add(key)
145
+ items.push(ref)
146
+ }
147
+ return items
148
+ }
149
+
150
+ function emptyStep(title, index) {
151
+ return {
152
+ index: explainStepId(index, '1'),
153
+ title: typeof title === 'string' ? title.trim() : '',
154
+ body: '',
155
+ asked: '',
156
+ files: [],
157
+ folders: [],
158
+ select: null,
159
+ zoom: null,
160
+ relations: [],
161
+ importedBy: false,
162
+ info: false,
163
+ highlights: [],
164
+ point: null,
165
+ }
166
+ }
167
+
168
+ export function emptyExplain() {
169
+ return {
170
+ active: false,
171
+ question: '',
172
+ steps: [],
173
+ currentStep: '1',
174
+ pendingQuestion: null,
175
+ pendingStart: null,
176
+ answering: false,
177
+ presentation: 'walk',
178
+ updatedAt: null,
179
+ }
180
+ }
181
+
182
+ export function normalizeExplainPresentation(value) {
183
+ return value === 'card' ? 'card' : 'walk'
184
+ }
185
+
186
+ const EXPLAIN_TARGET_KINDS = new Set([
187
+ 'file',
188
+ 'folder',
189
+ 'function',
190
+ 'variable',
191
+ 'class',
192
+ ])
193
+
194
+ export function parseExplainTargetKind(value) {
195
+ return typeof value === 'string' && EXPLAIN_TARGET_KINDS.has(value)
196
+ ? value
197
+ : null
198
+ }
199
+
200
+ export function explainTargetLabel({ kind, path, name }) {
201
+ const noun = parseExplainTargetKind(kind) ?? 'file'
202
+ const target = typeof path === 'string' ? path.trim() : ''
203
+ const symbol = typeof name === 'string' ? name.trim() : ''
204
+ return symbol ? `${noun} ${symbol} in ${target}` : `${noun} ${target}`
205
+ }
206
+
207
+ export function explainTargetQuestion(kind, path, name) {
208
+ return `Explain the function of the ${explainTargetLabel({ kind, path, name })} and where it fits in the codebase.`
209
+ }
210
+
211
+ function normalizePendingStart(value) {
212
+ if (!value || typeof value !== 'object') return null
213
+ const kind = parseExplainTargetKind(value.kind)
214
+ const path = typeof value.path === 'string' ? value.path.trim() : ''
215
+ const name = typeof value.name === 'string' ? value.name.trim() : ''
216
+ const question = typeof value.question === 'string' ? value.question.trim() : ''
217
+ if (!kind || !path || !question) return null
218
+ if ((kind === 'function' || kind === 'variable' || kind === 'class') && !name) {
219
+ return null
220
+ }
221
+ return name ? { kind, path, name, question } : { kind, path, question }
222
+ }
223
+
224
+ function normalizePendingQuestion(value) {
225
+ if (!value || typeof value !== 'object') return null
226
+ const parent = explainStepId(value.parent ?? value.step, '')
227
+ const question =
228
+ typeof value.question === 'string' ? value.question.trim() : ''
229
+ if (!parent || !question) return null
230
+ const from = explainStepId(value.from, parent)
231
+ const fromTitle =
232
+ typeof value.fromTitle === 'string' ? value.fromTitle.trim() : ''
233
+ return {
234
+ parent,
235
+ question,
236
+ from: from || parent,
237
+ fromTitle,
238
+ }
239
+ }
240
+
241
+ function normalizeStep(value, index) {
242
+ const assigned = explainStepId(
243
+ value && typeof value === 'object' ? (value.index ?? value.id) : index,
244
+ explainStepId(index, '1'),
245
+ )
246
+ const fallback = emptyStep('', assigned)
247
+ if (!value || typeof value !== 'object') return fallback
248
+ const title =
249
+ typeof value.title === 'string' && value.title.trim()
250
+ ? value.title.trim()
251
+ : `Step ${assigned}`
252
+ const select =
253
+ typeof value.select === 'string' && value.select.trim()
254
+ ? value.select.trim()
255
+ : null
256
+ const zoom =
257
+ typeof value.zoom === 'string' && value.zoom.trim()
258
+ ? value.zoom.trim()
259
+ : null
260
+ const asked =
261
+ typeof value.asked === 'string'
262
+ ? value.asked.trim()
263
+ : typeof value.question === 'string' && value.question.trim()
264
+ ? value.question.trim()
265
+ : ''
266
+ const files = uniqueStrings(Array.isArray(value.files) ? value.files : [])
267
+ const highlights = uniqueSymbolRefs(
268
+ Array.isArray(value.highlights) ? value.highlights : [],
269
+ )
270
+ const point = value.point
271
+ ? parseExplainSymbolRef(
272
+ typeof value.point === 'string'
273
+ ? value.point
274
+ : `${typeof value.point.kind === 'string' ? value.point.kind : 'symbol'}:${typeof value.point.name === 'string' ? value.point.name : ''}`,
275
+ )
276
+ : null
277
+ const info = Boolean(value.info) || highlights.length > 0 || Boolean(point)
278
+ let nextSelect = select
279
+ if (info && !nextSelect && files[0]) nextSelect = files[0]
280
+ if (info && nextSelect && !files.includes(nextSelect)) files.unshift(nextSelect)
281
+ return {
282
+ index: assigned,
283
+ title,
284
+ body: typeof value.body === 'string' ? value.body.trim() : '',
285
+ asked,
286
+ files,
287
+ folders: uniqueStrings(Array.isArray(value.folders) ? value.folders : []),
288
+ select: nextSelect,
289
+ zoom,
290
+ relations: uniqueRelations(Array.isArray(value.relations) ? value.relations : []),
291
+ importedBy: Boolean(value.importedBy),
292
+ info,
293
+ highlights,
294
+ point,
295
+ }
296
+ }
297
+
298
+ function assignStepIndexes(steps, parent = '') {
299
+ const prefix = parent ? `${parent}.` : ''
300
+ return steps.map((step, index) =>
301
+ normalizeStep({ ...step, index: `${prefix}${index + 1}`, asked: parent ? '' : step.asked }, index + 1),
302
+ )
303
+ }
304
+
305
+ export function normalizeExplain(value) {
306
+ const empty = emptyExplain()
307
+ if (!value || typeof value !== 'object') return empty
308
+ const steps = Array.isArray(value.steps)
309
+ ? value.steps.map((step, index) => normalizeStep(step, index + 1))
310
+ : []
311
+ const ids = new Set(steps.map((step) => step.index))
312
+ const current = explainStepId(
313
+ value.currentStep,
314
+ steps[0]?.index ?? empty.currentStep,
315
+ )
316
+ const pendingQuestion = normalizePendingQuestion(value.pendingQuestion)
317
+ return {
318
+ active: Boolean(value.active),
319
+ question: typeof value.question === 'string' ? value.question.trim() : '',
320
+ steps,
321
+ currentStep: ids.has(current) ? current : (steps[0]?.index ?? empty.currentStep),
322
+ pendingQuestion:
323
+ pendingQuestion && ids.has(pendingQuestion.parent) ? pendingQuestion : null,
324
+ pendingStart: normalizePendingStart(value.pendingStart),
325
+ answering: Boolean(value.answering),
326
+ presentation: normalizeExplainPresentation(value.presentation),
327
+ updatedAt: typeof value.updatedAt === 'string' ? value.updatedAt : null,
328
+ }
329
+ }
330
+
331
+ export function readExplain(dataDir) {
332
+ try {
333
+ return normalizeExplain(
334
+ JSON.parse(fs.readFileSync(explainPath(dataDir), 'utf8')),
335
+ )
336
+ } catch {
337
+ return emptyExplain()
338
+ }
339
+ }
340
+
341
+ function writeExplain(dataDir, value) {
342
+ const next = {
343
+ ...normalizeExplain(value),
344
+ updatedAt: new Date().toISOString(),
345
+ }
346
+ atomicWrite(explainPath(dataDir), `${JSON.stringify(next, null, 2)}\n`)
347
+ return next
348
+ }
349
+
350
+ export function startExplain(dataDir, question) {
351
+ const title = typeof question === 'string' ? question.trim() : ''
352
+ if (!title) {
353
+ throw new Error('question is required')
354
+ }
355
+ const previous = readExplain(dataDir)
356
+ const continuingCard =
357
+ previous.presentation === 'card' &&
358
+ previous.active &&
359
+ (Boolean(previous.pendingStart) || previous.question === title)
360
+ return writeExplain(dataDir, {
361
+ active: true,
362
+ question: title,
363
+ steps: [],
364
+ currentStep: '1',
365
+ pendingQuestion: null,
366
+ pendingStart: null,
367
+ answering: false,
368
+ presentation: continuingCard ? 'card' : 'walk',
369
+ })
370
+ }
371
+
372
+ export function requestExplainTarget(dataDir, input) {
373
+ const kind = parseExplainTargetKind(input?.kind)
374
+ const target = typeof input?.path === 'string' ? input.path.trim() : ''
375
+ const name = typeof input?.name === 'string' ? input.name.trim() : ''
376
+ if (!kind) {
377
+ throw new Error('kind must be file, folder, function, variable, or class')
378
+ }
379
+ if (!target) {
380
+ throw new Error('path is required')
381
+ }
382
+ if ((kind === 'function' || kind === 'variable' || kind === 'class') && !name) {
383
+ throw new Error('name is required')
384
+ }
385
+ const question =
386
+ typeof input?.question === 'string' && input.question.trim()
387
+ ? input.question.trim()
388
+ : explainTargetQuestion(kind, target, name)
389
+ return writeExplain(dataDir, {
390
+ active: true,
391
+ question,
392
+ steps: [],
393
+ currentStep: '1',
394
+ pendingQuestion: null,
395
+ pendingStart: name
396
+ ? { kind, path: target, name, question }
397
+ : { kind, path: target, question },
398
+ answering: false,
399
+ presentation: 'card',
400
+ })
401
+ }
402
+
403
+ export function consumeExplainStart(dataDir) {
404
+ const current = readExplain(dataDir)
405
+ const pending = current.pendingStart
406
+ if (!pending) return null
407
+ writeExplain(dataDir, {
408
+ ...current,
409
+ pendingStart: null,
410
+ })
411
+ return pending
412
+ }
413
+
414
+ function reportExplainChildren(dataDir, parent, input) {
415
+ const current = readExplain(dataDir)
416
+ const parentId = topLevelExplainStepId(parent)
417
+ const roots = stripExplainSubSteps(current.steps)
418
+ const parentIndex = roots.findIndex((step) => step.index === parentId)
419
+ if (!current.active || parentIndex < 0) {
420
+ throw new Error(`unknown parent step ${parentId || parent}`)
421
+ }
422
+ const raw = Array.isArray(input?.steps) ? input.steps : []
423
+ if (raw.length === 0) {
424
+ throw new Error('at least one --step is required')
425
+ }
426
+ const asked =
427
+ typeof input?.question === 'string' && input.question.trim()
428
+ ? input.question.trim()
429
+ : current.pendingQuestion?.parent === parentId
430
+ ? current.pendingQuestion.question
431
+ : ''
432
+ const children = assignStepIndexes(raw, parentId)
433
+ const parentStep = {
434
+ ...roots[parentIndex],
435
+ asked,
436
+ }
437
+ const steps = [
438
+ ...roots.slice(0, parentIndex),
439
+ parentStep,
440
+ ...children,
441
+ ...roots.slice(parentIndex + 1),
442
+ ]
443
+ return writeExplain(dataDir, {
444
+ ...current,
445
+ steps,
446
+ currentStep: children[0].index,
447
+ pendingQuestion: null,
448
+ answering: false,
449
+ })
450
+ }
451
+
452
+ export function reportExplain(dataDir, input) {
453
+ const question =
454
+ typeof input?.question === 'string' ? input.question.trim() : ''
455
+ const steps = Array.isArray(input?.steps) ? input.steps : []
456
+ if (steps.length === 0) {
457
+ throw new Error('at least one --step is required')
458
+ }
459
+ const parent = explainStepId(input?.parent, '')
460
+ if (parent) {
461
+ return reportExplainChildren(dataDir, parent, input)
462
+ }
463
+ const previous = readExplain(dataDir)
464
+ return writeExplain(dataDir, {
465
+ active: true,
466
+ question: question || previous.question,
467
+ steps: assignStepIndexes(steps),
468
+ currentStep: '1',
469
+ pendingQuestion: null,
470
+ answering: false,
471
+ presentation: previous.presentation === 'card' ? 'card' : 'walk',
472
+ })
473
+ }
474
+
475
+ export function setExplainStep(dataDir, step) {
476
+ const current = readExplain(dataDir)
477
+ if (!current.active) return current
478
+ const next = explainStepId(step, '')
479
+ if (!current.steps.some((item) => item.index === next)) return current
480
+ return writeExplain(dataDir, {
481
+ ...current,
482
+ currentStep: next,
483
+ })
484
+ }
485
+
486
+ export function askExplainQuestion(dataDir, step, question) {
487
+ const current = readExplain(dataDir)
488
+ if (!current.active) {
489
+ throw new Error('explain mode is not active')
490
+ }
491
+ const clicked = explainStepId(step, '')
492
+ const clickedStep = current.steps.find((item) => item.index === clicked)
493
+ const text = typeof question === 'string' ? question.trim() : ''
494
+ if (!text) {
495
+ throw new Error('question is required')
496
+ }
497
+ if (!clickedStep) {
498
+ throw new Error(`unknown step ${clicked || step}`)
499
+ }
500
+ const parent = topLevelExplainStepId(clicked)
501
+ if (!current.steps.some((item) => item.index === parent)) {
502
+ throw new Error(`unknown step ${parent || step}`)
503
+ }
504
+ return writeExplain(dataDir, {
505
+ ...current,
506
+ steps: stripExplainSubSteps(current.steps),
507
+ currentStep: parent,
508
+ pendingQuestion: {
509
+ parent,
510
+ question: text,
511
+ from: clicked,
512
+ fromTitle: clickedStep.title,
513
+ },
514
+ answering: false,
515
+ })
516
+ }
517
+
518
+ export function consumeExplainQuestion(dataDir) {
519
+ const current = readExplain(dataDir)
520
+ const pending = current.pendingQuestion
521
+ if (!pending || current.answering) return null
522
+ writeExplain(dataDir, {
523
+ ...current,
524
+ pendingQuestion: pending,
525
+ answering: true,
526
+ })
527
+ return pending
528
+ }
529
+
530
+ export function stopExplain(dataDir) {
531
+ return writeExplain(dataDir, emptyExplain())
532
+ }
533
+
534
+ export function parseExplainArgs(args) {
535
+ const steps = []
536
+ let current = null
537
+ let question = ''
538
+ let parent = ''
539
+
540
+ const requireCurrent = () => {
541
+ if (current) return current
542
+ current = emptyStep('Explanation', steps.length + 1)
543
+ steps.push(current)
544
+ return current
545
+ }
546
+
547
+ for (let index = 0; index < args.length; index += 1) {
548
+ const flag = args[index]
549
+ const value = args[index + 1]
550
+ if (flag === '--imported-by') {
551
+ requireCurrent().importedBy = true
552
+ continue
553
+ }
554
+ if (flag === '--info') {
555
+ const step = requireCurrent()
556
+ step.info = true
557
+ if (typeof value === 'string' && value && !value.startsWith('--')) {
558
+ const file = value.trim()
559
+ if (file) {
560
+ step.files = uniqueStrings([...step.files, file])
561
+ if (!step.select) step.select = file
562
+ }
563
+ index += 1
564
+ }
565
+ continue
566
+ }
567
+ if (typeof value !== 'string') continue
568
+ if (flag === '--question') {
569
+ question = value.trim()
570
+ index += 1
571
+ continue
572
+ }
573
+ if (flag === '--parent') {
574
+ parent = value.trim()
575
+ index += 1
576
+ continue
577
+ }
578
+ if (flag === '--step') {
579
+ current = emptyStep(value, steps.length + 1)
580
+ steps.push(current)
581
+ index += 1
582
+ continue
583
+ }
584
+ if (flag === '--body') {
585
+ requireCurrent().body = value.trim()
586
+ index += 1
587
+ continue
588
+ }
589
+ if (flag === '--files') {
590
+ const step = requireCurrent()
591
+ step.files = uniqueStrings([...step.files, ...splitList(value)])
592
+ index += 1
593
+ continue
594
+ }
595
+ if (flag === '--folders') {
596
+ const step = requireCurrent()
597
+ step.folders = uniqueStrings([...step.folders, ...splitList(value)])
598
+ index += 1
599
+ continue
600
+ }
601
+ if (flag === '--select') {
602
+ requireCurrent().select = value.trim() || null
603
+ index += 1
604
+ continue
605
+ }
606
+ if (flag === '--zoom') {
607
+ requireCurrent().zoom = value.trim() || null
608
+ index += 1
609
+ continue
610
+ }
611
+ if (flag === '--relations') {
612
+ const step = requireCurrent()
613
+ const extra = splitList(value)
614
+ .map(parseRelation)
615
+ .filter(Boolean)
616
+ step.relations = uniqueRelations([...step.relations, ...extra])
617
+ index += 1
618
+ continue
619
+ }
620
+ if (flag === '--highlight') {
621
+ const step = requireCurrent()
622
+ const extra = splitList(value)
623
+ .map(parseExplainSymbolRef)
624
+ .filter(Boolean)
625
+ step.highlights = uniqueSymbolRefs([...step.highlights, ...extra])
626
+ index += 1
627
+ continue
628
+ }
629
+ if (flag === '--point') {
630
+ requireCurrent().point = parseExplainSymbolRef(value)
631
+ index += 1
632
+ }
633
+ }
634
+
635
+ return {
636
+ question,
637
+ parent: explainStepId(parent, ''),
638
+ steps: steps
639
+ .filter((step) => step.title)
640
+ .map((step, index) => normalizeStep(step, index + 1)),
641
+ }
642
+ }
643
+
644
+ export function parseExplainCli(args) {
645
+ const raw = Array.isArray(args) ? args : []
646
+ const sub =
647
+ raw[0] === 'start' ||
648
+ raw[0] === 'report' ||
649
+ raw[0] === 'stop' ||
650
+ raw[0] === 'wait'
651
+ ? raw[0]
652
+ : null
653
+ const rest = sub ? raw.slice(1) : raw
654
+ if (sub === 'stop') return { action: 'stop' }
655
+ if (sub === 'wait') return { action: 'wait' }
656
+ const parsed = parseExplainArgs(rest)
657
+ if (sub === 'start' || (parsed.question && parsed.steps.length === 0 && !sub)) {
658
+ return { action: 'start', question: parsed.question }
659
+ }
660
+ return {
661
+ action: 'report',
662
+ question: parsed.question,
663
+ parent: parsed.parent,
664
+ steps: parsed.steps,
665
+ }
666
+ }
@@ -508,6 +508,9 @@ export const emptyIntent = {
508
508
  showMap: false,
509
509
  status: 'idle',
510
510
  name: null,
511
+ color: null,
512
+ colorName: null,
513
+ colorHex: null,
511
514
  feature: null,
512
515
  steps: [],
513
516
  step: null,
@@ -545,6 +548,7 @@ export const emptyIntent = {
545
548
  blueprintHidden: false,
546
549
  blueprintRevision: 0,
547
550
  blueprintSessionId: null,
551
+ localBlueprintEnabled: false,
548
552
  userCreatedBlocks: [],
549
553
  userCreatedIslands: [],
550
554
  blueprintFunctions: [],