@jkwd/inbase 0.1.21 → 0.1.23
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 +13 -7
- package/apps/explorer/package.json +1 -0
- package/apps/explorer/scripts/explain-store.d.ts +135 -0
- package/apps/explorer/scripts/explain-store.mjs +666 -0
- package/apps/explorer/scripts/patch-lib.mjs +4 -0
- package/apps/explorer/scripts/scan-target.mjs +22 -5
- package/apps/explorer/scripts/session-store.d.ts +86 -11
- package/apps/explorer/scripts/session-store.mjs +371 -58
- package/apps/explorer/scripts/target-config.d.ts +38 -3
- package/apps/explorer/scripts/target-config.mjs +147 -3
- package/apps/explorer/src/App.tsx +1073 -158
- package/apps/explorer/src/agentIntent.ts +61 -7
- package/apps/explorer/src/codebase.ts +1 -1
- package/apps/explorer/src/devTargets.ts +66 -0
- package/apps/explorer/src/explain.ts +312 -0
- package/apps/explorer/src/index.css +874 -222
- package/apps/explorer/src/layout.ts +55 -0
- package/apps/explorer/src/scene/DistantFileBlocks.tsx +4 -2
- package/apps/explorer/src/scene/FileBlock.tsx +95 -72
- package/apps/explorer/src/scene/FolderArea.tsx +55 -32
- package/apps/explorer/src/scene/MapView.tsx +506 -33
- package/apps/explorer/src/scene/RelationLines.tsx +7 -0
- package/apps/explorer/src/scene/World.tsx +146 -32
- package/apps/explorer/src/speech.ts +228 -0
- package/apps/explorer/src/theme.ts +29 -0
- package/apps/explorer/src/types.ts +98 -8
- package/apps/explorer/src/ui/CanvasErrorBoundary.tsx +1 -1
- package/apps/explorer/src/ui/ExplainAskCard.tsx +142 -0
- package/apps/explorer/src/ui/ExplainHud.tsx +524 -0
- package/apps/explorer/src/ui/ExplainInfoPanel.tsx +135 -0
- package/apps/explorer/src/ui/ExplainPointer.tsx +73 -0
- package/apps/explorer/src/ui/EyeIcon.tsx +38 -1
- package/apps/explorer/src/ui/HUD.tsx +1067 -795
- package/apps/explorer/src/ui/NameInput.tsx +114 -5
- package/apps/explorer/src/userContext.ts +0 -11
- package/apps/explorer/src/userCreated.ts +54 -1
- package/apps/explorer/vite.config.ts +173 -26
- package/bin/inbase.mjs +11 -2
- package/bin/project.mjs +6 -4
- package/bin/session.mjs +287 -38
- package/package.json +4 -1
- package/skill/commands/amber.md +23 -0
- package/skill/commands/blue.md +13 -0
- package/skill/commands/coral.md +23 -0
- package/skill/commands/explain.md +77 -0
- package/skill/commands/green.md +23 -0
- package/skill/commands/inbase.md +7 -5
- package/skill/commands/lime.md +23 -0
- package/skill/commands/orange.md +23 -0
- package/skill/commands/purple.md +23 -0
- package/skill/commands/red.md +23 -0
- package/skill/commands/skipinbase.md +1 -1
- package/skill/commands/violet.md +23 -0
- package/skill/commands/yellow.md +23 -0
- package/skill/inbase/SKILL.md +124 -76
- package/apps/explorer/src/scene/BlockPlacer.tsx +0 -78
- package/apps/explorer/src/scene/IslandPlacer.tsx +0 -31
- 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: [],
|