@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
|
@@ -3,6 +3,7 @@ import type {
|
|
|
3
3
|
AgentIntentBundle,
|
|
4
4
|
BlueprintNote,
|
|
5
5
|
BlueprintPointer,
|
|
6
|
+
LocalBlueprint,
|
|
6
7
|
PatchImport,
|
|
7
8
|
PatchImportAddition,
|
|
8
9
|
PatchSymbolAddition,
|
|
@@ -10,6 +11,7 @@ import type {
|
|
|
10
11
|
UserCreatedIsland,
|
|
11
12
|
WorkflowAction,
|
|
12
13
|
} from './types'
|
|
14
|
+
import { GLOBAL_BLUEPRINT_COLOR } from './types'
|
|
13
15
|
import {
|
|
14
16
|
parseBlueprintNotes,
|
|
15
17
|
parseBlueprintPointers,
|
|
@@ -92,6 +94,9 @@ export const emptyIntent: AgentIntent = {
|
|
|
92
94
|
showMap: false,
|
|
93
95
|
status: 'idle',
|
|
94
96
|
name: null,
|
|
97
|
+
color: null,
|
|
98
|
+
colorName: null,
|
|
99
|
+
colorHex: null,
|
|
95
100
|
feature: null,
|
|
96
101
|
steps: [],
|
|
97
102
|
step: null,
|
|
@@ -121,7 +126,8 @@ export const emptyIntent: AgentIntent = {
|
|
|
121
126
|
llmIdle: false,
|
|
122
127
|
awaitingAttach: false,
|
|
123
128
|
listening: false,
|
|
124
|
-
|
|
129
|
+
lastAck: null,
|
|
130
|
+
pendingExplain: false,
|
|
125
131
|
initialInstruction: null,
|
|
126
132
|
contextFiles: [],
|
|
127
133
|
creationMode: false,
|
|
@@ -129,6 +135,7 @@ export const emptyIntent: AgentIntent = {
|
|
|
129
135
|
blueprintHidden: false,
|
|
130
136
|
blueprintRevision: 0,
|
|
131
137
|
blueprintSessionId: null,
|
|
138
|
+
localBlueprintEnabled: false,
|
|
132
139
|
userCreatedBlocks: [],
|
|
133
140
|
userCreatedIslands: [],
|
|
134
141
|
blueprintFunctions: [],
|
|
@@ -144,6 +151,9 @@ function normalize(data: Partial<AgentIntent> | null | undefined): AgentIntent {
|
|
|
144
151
|
showMap: Boolean(data?.showMap),
|
|
145
152
|
status: data?.status ?? 'idle',
|
|
146
153
|
name: data?.name ?? null,
|
|
154
|
+
color: typeof data?.color === 'string' ? data.color : null,
|
|
155
|
+
colorName: typeof data?.colorName === 'string' ? data.colorName : null,
|
|
156
|
+
colorHex: typeof data?.colorHex === 'string' ? data.colorHex : null,
|
|
147
157
|
feature: data?.feature ?? null,
|
|
148
158
|
steps: Array.isArray(data?.steps) ? data.steps : [],
|
|
149
159
|
step: typeof data?.step === 'number' ? data.step : null,
|
|
@@ -177,6 +187,7 @@ function normalize(data: Partial<AgentIntent> | null | undefined): AgentIntent {
|
|
|
177
187
|
awaitingAttach: Boolean(data?.awaitingAttach),
|
|
178
188
|
listening: Boolean(data?.listening),
|
|
179
189
|
lastAck: normalizeAck(data?.lastAck),
|
|
190
|
+
pendingExplain: Boolean(data?.pendingExplain),
|
|
180
191
|
initialInstruction:
|
|
181
192
|
typeof data?.initialInstruction === 'string' ? data.initialInstruction : null,
|
|
182
193
|
contextFiles: normalizeContextFiles(data?.contextFiles),
|
|
@@ -189,6 +200,7 @@ function normalize(data: Partial<AgentIntent> | null | undefined): AgentIntent {
|
|
|
189
200
|
typeof data?.blueprintSessionId === 'string'
|
|
190
201
|
? data.blueprintSessionId
|
|
191
202
|
: null,
|
|
203
|
+
localBlueprintEnabled: Boolean(data?.localBlueprintEnabled),
|
|
192
204
|
userCreatedBlocks: parseUserCreatedBlocks(data?.userCreatedBlocks),
|
|
193
205
|
userCreatedIslands: parseUserCreatedIslands(data?.userCreatedIslands),
|
|
194
206
|
blueprintFunctions: normalizeSymbolAdditions(data?.blueprintFunctions),
|
|
@@ -214,6 +226,28 @@ function normalizeBlueprint(data: Partial<AgentIntentBundle['blueprint']> | null
|
|
|
214
226
|
}
|
|
215
227
|
}
|
|
216
228
|
|
|
229
|
+
function normalizeLocalBlueprints(value: unknown): LocalBlueprint[] {
|
|
230
|
+
if (!Array.isArray(value)) return []
|
|
231
|
+
return value.flatMap((item) => {
|
|
232
|
+
if (!item || typeof item !== 'object') return []
|
|
233
|
+
const row = item as Partial<LocalBlueprint>
|
|
234
|
+
const color = typeof row.color === 'string' ? row.color : ''
|
|
235
|
+
const colorName = typeof row.colorName === 'string' ? row.colorName : ''
|
|
236
|
+
const colorHex = typeof row.colorHex === 'string' ? row.colorHex : ''
|
|
237
|
+
const sessionId = typeof row.sessionId === 'string' ? row.sessionId : ''
|
|
238
|
+
if (!color || !sessionId) return []
|
|
239
|
+
return [
|
|
240
|
+
{
|
|
241
|
+
...normalizeBlueprint(row),
|
|
242
|
+
color,
|
|
243
|
+
colorName,
|
|
244
|
+
colorHex,
|
|
245
|
+
sessionId,
|
|
246
|
+
},
|
|
247
|
+
]
|
|
248
|
+
})
|
|
249
|
+
}
|
|
250
|
+
|
|
217
251
|
export async function fetchAgentIntents(): Promise<AgentIntentBundle> {
|
|
218
252
|
const query = new URLSearchParams({ t: String(Date.now()) })
|
|
219
253
|
const response = await fetch(`/api/agent-intent?${query}`)
|
|
@@ -224,6 +258,7 @@ export async function fetchAgentIntents(): Promise<AgentIntentBundle> {
|
|
|
224
258
|
nextAttachSessionId: null,
|
|
225
259
|
intents: [],
|
|
226
260
|
blueprint: emptyBlueprint,
|
|
261
|
+
localBlueprints: [],
|
|
227
262
|
}
|
|
228
263
|
}
|
|
229
264
|
const data = (await response.json()) as {
|
|
@@ -232,6 +267,7 @@ export async function fetchAgentIntents(): Promise<AgentIntentBundle> {
|
|
|
232
267
|
intents?: unknown
|
|
233
268
|
sessionId?: string | null
|
|
234
269
|
blueprint?: Partial<AgentIntentBundle['blueprint']>
|
|
270
|
+
localBlueprints?: unknown
|
|
235
271
|
} & Partial<AgentIntent>
|
|
236
272
|
if (Array.isArray(data.intents)) {
|
|
237
273
|
return {
|
|
@@ -245,6 +281,7 @@ export async function fetchAgentIntents(): Promise<AgentIntentBundle> {
|
|
|
245
281
|
.map((intent) => normalize(intent as Partial<AgentIntent>))
|
|
246
282
|
.filter((intent) => Boolean(intent.sessionId)),
|
|
247
283
|
blueprint: normalizeBlueprint(data.blueprint),
|
|
284
|
+
localBlueprints: normalizeLocalBlueprints(data.localBlueprints),
|
|
248
285
|
}
|
|
249
286
|
}
|
|
250
287
|
const intent = normalize(data)
|
|
@@ -252,6 +289,7 @@ export async function fetchAgentIntents(): Promise<AgentIntentBundle> {
|
|
|
252
289
|
focusedSessionId: intent.sessionId,
|
|
253
290
|
nextAttachSessionId: intent.awaitingAttach ? intent.sessionId : null,
|
|
254
291
|
intents: intent.sessionId ? [intent] : [],
|
|
292
|
+
localBlueprints: [],
|
|
255
293
|
blueprint: normalizeBlueprint(data.blueprint ?? {
|
|
256
294
|
hidden: intent.blueprintHidden,
|
|
257
295
|
revision: intent.blueprintRevision,
|
|
@@ -320,6 +358,7 @@ export async function performAgentAction(
|
|
|
320
358
|
export function persistSessionBlueprint(
|
|
321
359
|
sessionId: string | null | undefined,
|
|
322
360
|
payload: {
|
|
361
|
+
color?: string | null
|
|
323
362
|
userCreatedBlocks: UserCreatedBlock[]
|
|
324
363
|
userCreatedIslands: UserCreatedIsland[]
|
|
325
364
|
addedFunctions?: PatchSymbolAddition[]
|
|
@@ -336,36 +375,51 @@ export function persistSessionBlueprint(
|
|
|
336
375
|
action: 'blueprint_update',
|
|
337
376
|
sessionId,
|
|
338
377
|
...payload,
|
|
378
|
+
color: payload.color ?? GLOBAL_BLUEPRINT_COLOR.id,
|
|
339
379
|
}),
|
|
340
380
|
}).catch(() => {
|
|
341
|
-
// Keep local drafts if the visualizer could not save the
|
|
381
|
+
// Keep local drafts if the visualizer could not save the blueprint.
|
|
342
382
|
})
|
|
343
383
|
}
|
|
344
384
|
|
|
345
|
-
export function persistBlueprintHidden(
|
|
385
|
+
export function persistBlueprintHidden(
|
|
386
|
+
hidden: boolean,
|
|
387
|
+
color: string | null | undefined = GLOBAL_BLUEPRINT_COLOR.id,
|
|
388
|
+
) {
|
|
346
389
|
return fetch('/api/agent-intent', {
|
|
347
390
|
method: 'POST',
|
|
348
391
|
headers: { 'Content-Type': 'application/json' },
|
|
349
392
|
body: JSON.stringify({
|
|
350
393
|
action: 'blueprint_set_hidden',
|
|
351
394
|
hidden,
|
|
395
|
+
color: color ?? GLOBAL_BLUEPRINT_COLOR.id,
|
|
352
396
|
}),
|
|
353
397
|
})
|
|
354
398
|
}
|
|
355
399
|
|
|
356
|
-
export function persistBlueprintClear(
|
|
400
|
+
export function persistBlueprintClear(
|
|
401
|
+
color: string | null | undefined = GLOBAL_BLUEPRINT_COLOR.id,
|
|
402
|
+
) {
|
|
357
403
|
return fetch('/api/agent-intent', {
|
|
358
404
|
method: 'POST',
|
|
359
405
|
headers: { 'Content-Type': 'application/json' },
|
|
360
|
-
body: JSON.stringify({
|
|
406
|
+
body: JSON.stringify({
|
|
407
|
+
action: 'blueprint_clear',
|
|
408
|
+
color: color ?? GLOBAL_BLUEPRINT_COLOR.id,
|
|
409
|
+
}),
|
|
361
410
|
})
|
|
362
411
|
}
|
|
363
412
|
|
|
364
|
-
export function persistBlueprintCleanup(
|
|
413
|
+
export function persistBlueprintCleanup(
|
|
414
|
+
color: string | null | undefined = GLOBAL_BLUEPRINT_COLOR.id,
|
|
415
|
+
) {
|
|
365
416
|
return fetch('/api/agent-intent', {
|
|
366
417
|
method: 'POST',
|
|
367
418
|
headers: { 'Content-Type': 'application/json' },
|
|
368
|
-
body: JSON.stringify({
|
|
419
|
+
body: JSON.stringify({
|
|
420
|
+
action: 'blueprint_cleanup',
|
|
421
|
+
color: color ?? GLOBAL_BLUEPRINT_COLOR.id,
|
|
422
|
+
}),
|
|
369
423
|
})
|
|
370
424
|
}
|
|
371
425
|
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { parseCodebase } from './codebase'
|
|
2
|
+
import type { CodebaseGraph } from './types'
|
|
3
|
+
|
|
4
|
+
export type DevTargetOption = {
|
|
5
|
+
id: string
|
|
6
|
+
label: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export type DevTargetsState = {
|
|
10
|
+
enabled: boolean
|
|
11
|
+
currentId: string | null
|
|
12
|
+
targets: DevTargetOption[]
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const emptyDevTargets: DevTargetsState = {
|
|
16
|
+
enabled: false,
|
|
17
|
+
currentId: null,
|
|
18
|
+
targets: [],
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function parseState(data: unknown): DevTargetsState {
|
|
22
|
+
if (!data || typeof data !== 'object') return emptyDevTargets
|
|
23
|
+
const record = data as DevTargetsState
|
|
24
|
+
if (!record.enabled || !Array.isArray(record.targets)) return emptyDevTargets
|
|
25
|
+
return {
|
|
26
|
+
enabled: true,
|
|
27
|
+
currentId: typeof record.currentId === 'string' ? record.currentId : null,
|
|
28
|
+
targets: record.targets.filter(
|
|
29
|
+
(item): item is DevTargetOption =>
|
|
30
|
+
Boolean(item) &&
|
|
31
|
+
typeof item.id === 'string' &&
|
|
32
|
+
typeof item.label === 'string',
|
|
33
|
+
),
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function fetchDevTargets(): Promise<DevTargetsState> {
|
|
38
|
+
try {
|
|
39
|
+
const response = await fetch('/api/dev-targets')
|
|
40
|
+
if (!response.ok) return emptyDevTargets
|
|
41
|
+
return parseState(await response.json())
|
|
42
|
+
} catch {
|
|
43
|
+
return emptyDevTargets
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function selectDevTarget(id: string): Promise<{
|
|
48
|
+
state: DevTargetsState
|
|
49
|
+
graph: CodebaseGraph | null
|
|
50
|
+
} | null> {
|
|
51
|
+
try {
|
|
52
|
+
const response = await fetch('/api/dev-targets', {
|
|
53
|
+
method: 'POST',
|
|
54
|
+
headers: { 'Content-Type': 'application/json' },
|
|
55
|
+
body: JSON.stringify({ id }),
|
|
56
|
+
})
|
|
57
|
+
if (!response.ok) return null
|
|
58
|
+
const payload = (await response.json()) as { codebase?: unknown }
|
|
59
|
+
return {
|
|
60
|
+
state: parseState(payload),
|
|
61
|
+
graph: parseCodebase(payload.codebase),
|
|
62
|
+
}
|
|
63
|
+
} catch {
|
|
64
|
+
return null
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
import { folderOfFile } from './layout'
|
|
2
|
+
import type {
|
|
3
|
+
CodebaseGraph,
|
|
4
|
+
ExplainSession,
|
|
5
|
+
ExplainStep,
|
|
6
|
+
ExplainSymbolRef,
|
|
7
|
+
ExplainTargetKind,
|
|
8
|
+
FileNode,
|
|
9
|
+
PatchImport,
|
|
10
|
+
} from './types'
|
|
11
|
+
|
|
12
|
+
export function emptyExplain(): ExplainSession {
|
|
13
|
+
return {
|
|
14
|
+
active: false,
|
|
15
|
+
question: '',
|
|
16
|
+
steps: [],
|
|
17
|
+
currentStep: '1',
|
|
18
|
+
pendingQuestion: null,
|
|
19
|
+
pendingStart: null,
|
|
20
|
+
answering: false,
|
|
21
|
+
presentation: 'walk',
|
|
22
|
+
updatedAt: null,
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function explainIsCard(explain: ExplainSession) {
|
|
27
|
+
return explain.active && explain.presentation === 'card'
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function fetchExplain(): Promise<ExplainSession> {
|
|
31
|
+
try {
|
|
32
|
+
const query = new URLSearchParams({ t: String(Date.now()) })
|
|
33
|
+
const response = await fetch(`/api/explain?${query}`)
|
|
34
|
+
if (!response.ok) return emptyExplain()
|
|
35
|
+
return (await response.json()) as ExplainSession
|
|
36
|
+
} catch {
|
|
37
|
+
return emptyExplain()
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function persistExplainStep(step: string) {
|
|
42
|
+
return fetch('/api/explain', {
|
|
43
|
+
method: 'POST',
|
|
44
|
+
headers: { 'Content-Type': 'application/json' },
|
|
45
|
+
body: JSON.stringify({ action: 'set_step', step }),
|
|
46
|
+
}).catch(() => {
|
|
47
|
+
// Keep the local step if the visualizer could not save it.
|
|
48
|
+
})
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function persistExplainAsk(step: string, question: string) {
|
|
52
|
+
return fetch('/api/explain', {
|
|
53
|
+
method: 'POST',
|
|
54
|
+
headers: { 'Content-Type': 'application/json' },
|
|
55
|
+
body: JSON.stringify({ action: 'ask', step, question }),
|
|
56
|
+
}).catch(() => {
|
|
57
|
+
// Keep the local question if the visualizer could not save it.
|
|
58
|
+
})
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function persistExplainStop() {
|
|
62
|
+
return fetch('/api/explain', {
|
|
63
|
+
method: 'POST',
|
|
64
|
+
headers: { 'Content-Type': 'application/json' },
|
|
65
|
+
body: JSON.stringify({ action: 'stop' }),
|
|
66
|
+
}).catch(() => {
|
|
67
|
+
// Keep the local overlay if the visualizer could not stop.
|
|
68
|
+
})
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function explainTargetLabel(input: {
|
|
72
|
+
kind: ExplainTargetKind
|
|
73
|
+
path: string
|
|
74
|
+
name?: string
|
|
75
|
+
}) {
|
|
76
|
+
const symbol = input.name?.trim() ?? ''
|
|
77
|
+
return symbol
|
|
78
|
+
? `${input.kind} ${symbol} in ${input.path}`
|
|
79
|
+
: `${input.kind} ${input.path}`
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function explainTargetQuestion(
|
|
83
|
+
kind: ExplainTargetKind,
|
|
84
|
+
path: string,
|
|
85
|
+
name?: string,
|
|
86
|
+
) {
|
|
87
|
+
return `Explain the function of the ${explainTargetLabel({ kind, path, name })} and where it fits in the codebase.`
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function persistExplainStart(input: {
|
|
91
|
+
kind: ExplainTargetKind
|
|
92
|
+
path: string
|
|
93
|
+
name?: string
|
|
94
|
+
sessionId?: string | null
|
|
95
|
+
}) {
|
|
96
|
+
return fetch('/api/explain', {
|
|
97
|
+
method: 'POST',
|
|
98
|
+
headers: { 'Content-Type': 'application/json' },
|
|
99
|
+
body: JSON.stringify({ action: 'start', ...input }),
|
|
100
|
+
}).catch(() => {
|
|
101
|
+
// Keep the local overlay if the visualizer could not start.
|
|
102
|
+
})
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function currentExplainStep(explain: ExplainSession): ExplainStep | null {
|
|
106
|
+
if (!explain.active || explain.steps.length === 0) return null
|
|
107
|
+
return (
|
|
108
|
+
explain.steps.find((step) => step.index === explain.currentStep) ??
|
|
109
|
+
explain.steps[0] ??
|
|
110
|
+
null
|
|
111
|
+
)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function explainStepPosition(explain: ExplainSession, id: string) {
|
|
115
|
+
return explain.steps.findIndex((step) => step.index === id)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function explainNeighbor(
|
|
119
|
+
explain: ExplainSession,
|
|
120
|
+
id: string,
|
|
121
|
+
delta: number,
|
|
122
|
+
) {
|
|
123
|
+
const position = explainStepPosition(explain, id)
|
|
124
|
+
if (position < 0) return null
|
|
125
|
+
return explain.steps[position + delta]?.index ?? null
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function explainStepDepth(id: string) {
|
|
129
|
+
return String(id).split('.').length - 1
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function topLevelExplainStepId(id: string) {
|
|
133
|
+
const next = String(id)
|
|
134
|
+
const dot = next.indexOf('.')
|
|
135
|
+
return dot === -1 ? next : next.slice(0, dot)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function stripExplainSubSteps(steps: ExplainStep[]): ExplainStep[] {
|
|
139
|
+
return steps
|
|
140
|
+
.filter((step) => !step.index.includes('.'))
|
|
141
|
+
.map((step) => ({ ...step, asked: '' }))
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function explainIsPreparing(explain: ExplainSession) {
|
|
145
|
+
if (!explain.active) return false
|
|
146
|
+
if (explain.steps.length === 0) return true
|
|
147
|
+
return Boolean(explain.pendingQuestion) || explain.answering
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function explainSpeechText(step: Pick<ExplainStep, 'title' | 'body'> | null) {
|
|
151
|
+
if (!step) return ''
|
|
152
|
+
return [step.title, step.body]
|
|
153
|
+
.map((part) => part.trim())
|
|
154
|
+
.filter(Boolean)
|
|
155
|
+
.join('. ')
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function explainCardCopy(explain: ExplainSession) {
|
|
159
|
+
const first =
|
|
160
|
+
explain.steps.find((step) => !step.index.includes('.') && step.body.trim()) ??
|
|
161
|
+
explain.steps.find((step) => !step.index.includes('.')) ??
|
|
162
|
+
null
|
|
163
|
+
const pending = explain.pendingStart
|
|
164
|
+
const title = pending
|
|
165
|
+
? pending.name?.trim() || pending.path.split('/').pop() || pending.path
|
|
166
|
+
: first?.title.trim() || 'Explanation'
|
|
167
|
+
return {
|
|
168
|
+
title,
|
|
169
|
+
body: first?.body.trim() ?? '',
|
|
170
|
+
ready: Boolean(first?.body.trim()),
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export function explainSymbolKey(ref: ExplainSymbolRef) {
|
|
175
|
+
return `${ref.kind}:${ref.name}`
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function explainMatchesSymbol(
|
|
179
|
+
ref: ExplainSymbolRef | null | undefined,
|
|
180
|
+
kind: FileNode['symbols'][number]['kind'] | 'file',
|
|
181
|
+
name: string,
|
|
182
|
+
) {
|
|
183
|
+
if (!ref) return false
|
|
184
|
+
if (ref.kind === 'file') return kind === 'file'
|
|
185
|
+
if (ref.kind === 'symbol') return kind !== 'file' && ref.name === name
|
|
186
|
+
return ref.kind === kind && ref.name === name
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export function explainHitsSymbol(
|
|
190
|
+
refs: ExplainSymbolRef[],
|
|
191
|
+
kind: FileNode['symbols'][number]['kind'] | 'file',
|
|
192
|
+
name: string,
|
|
193
|
+
) {
|
|
194
|
+
return refs.some((ref) => explainMatchesSymbol(ref, kind, name))
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export function explainInfoFile(
|
|
198
|
+
step: ExplainStep | null,
|
|
199
|
+
graph: CodebaseGraph,
|
|
200
|
+
): FileNode | null {
|
|
201
|
+
if (!step?.info || !step.select) return null
|
|
202
|
+
return (
|
|
203
|
+
graph.files.find((file) => file.id === step.select) ?? {
|
|
204
|
+
id: step.select,
|
|
205
|
+
name: step.select.split('/').pop() || step.select,
|
|
206
|
+
path: step.select,
|
|
207
|
+
folder: '',
|
|
208
|
+
lines: 0,
|
|
209
|
+
language: '',
|
|
210
|
+
symbols: [],
|
|
211
|
+
imports: [],
|
|
212
|
+
}
|
|
213
|
+
)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export type ExplainFocus = {
|
|
217
|
+
files: Set<string>
|
|
218
|
+
folders: Set<string>
|
|
219
|
+
select: string | null
|
|
220
|
+
importedBy: boolean
|
|
221
|
+
relations: PatchImport[]
|
|
222
|
+
zoomFiles: string[]
|
|
223
|
+
zoomFolders: string[]
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export function explainFocus(
|
|
227
|
+
step: ExplainStep | null,
|
|
228
|
+
graph: CodebaseGraph,
|
|
229
|
+
): ExplainFocus | null {
|
|
230
|
+
if (!step) return null
|
|
231
|
+
const files = new Set(step.files)
|
|
232
|
+
const folders = new Set(step.folders)
|
|
233
|
+
if (step.select) files.add(step.select)
|
|
234
|
+
const relations: PatchImport[] = step.relations.map((edge) => ({
|
|
235
|
+
from: edge.from,
|
|
236
|
+
to: edge.to,
|
|
237
|
+
}))
|
|
238
|
+
for (const edge of relations) {
|
|
239
|
+
files.add(edge.from)
|
|
240
|
+
files.add(edge.to)
|
|
241
|
+
}
|
|
242
|
+
if (step.select) {
|
|
243
|
+
if (step.importedBy) {
|
|
244
|
+
for (const file of graph.files) {
|
|
245
|
+
if (file.imports.includes(step.select)) files.add(file.id)
|
|
246
|
+
}
|
|
247
|
+
} else {
|
|
248
|
+
const selected = graph.files.find((file) => file.id === step.select)
|
|
249
|
+
for (const id of selected?.imports ?? []) files.add(id)
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
for (const file of graph.files) {
|
|
253
|
+
if (folders.has(file.folder)) files.add(file.id)
|
|
254
|
+
}
|
|
255
|
+
for (const id of files) {
|
|
256
|
+
const folder = graph.files.find((file) => file.id === id)?.folder ?? folderOfFile(id)
|
|
257
|
+
if (folder) folders.add(folder)
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const zoomFolders = step.zoom
|
|
261
|
+
? graph.folders.some((folder) => folder.path === step.zoom)
|
|
262
|
+
? [step.zoom]
|
|
263
|
+
: []
|
|
264
|
+
: [...step.folders]
|
|
265
|
+
const zoomFiles = step.zoom
|
|
266
|
+
? graph.files.some((file) => file.id === step.zoom)
|
|
267
|
+
? [step.zoom]
|
|
268
|
+
: []
|
|
269
|
+
: [...step.files]
|
|
270
|
+
if (step.zoom && zoomFolders.length === 0 && zoomFiles.length === 0) {
|
|
271
|
+
zoomFiles.push(step.zoom)
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return {
|
|
275
|
+
files,
|
|
276
|
+
folders,
|
|
277
|
+
select: step.select,
|
|
278
|
+
importedBy: step.importedBy,
|
|
279
|
+
relations,
|
|
280
|
+
zoomFiles: zoomFiles.length > 0 ? zoomFiles : [...files],
|
|
281
|
+
zoomFolders: zoomFolders.length > 0 ? zoomFolders : [...folders],
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
export function explainHasFocus(focus: ExplainFocus | null) {
|
|
286
|
+
return Boolean(focus && (focus.files.size > 0 || focus.folders.size > 0))
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export function explainFileFocused(
|
|
290
|
+
focus: ExplainFocus | null,
|
|
291
|
+
fileId: string,
|
|
292
|
+
_folder?: string,
|
|
293
|
+
) {
|
|
294
|
+
if (!explainHasFocus(focus) || !focus) return true
|
|
295
|
+
return focus.files.has(fileId)
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
export function explainFileHighlighted(
|
|
299
|
+
focus: ExplainFocus | null,
|
|
300
|
+
fileId: string,
|
|
301
|
+
folder?: string,
|
|
302
|
+
) {
|
|
303
|
+
return explainHasFocus(focus) && explainFileFocused(focus, fileId, folder)
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
export function explainFolderFocused(focus: ExplainFocus | null, folderPath: string) {
|
|
307
|
+
if (!explainHasFocus(focus) || !focus) return true
|
|
308
|
+
for (const id of focus.files) {
|
|
309
|
+
if (folderOfFile(id) === folderPath) return true
|
|
310
|
+
}
|
|
311
|
+
return false
|
|
312
|
+
}
|