@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
|
@@ -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({
|
|
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
|
-
|
|
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] = {
|
|
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 {
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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:
|
|
166
|
+
port: serverPort,
|
|
143
167
|
})
|
|
144
|
-
//
|
|
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 {
|
|
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.
|
|
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
|
@@ -55,13 +55,15 @@ export function resolveOptionalPath(value, fallback) {
|
|
|
55
55
|
|
|
56
56
|
export const INSTANCE_FILE = 'instance.json'
|
|
57
57
|
|
|
58
|
-
function isPidAlive(pid) {
|
|
58
|
+
export function isPidAlive(pid) {
|
|
59
59
|
if (!Number.isInteger(pid)) return true
|
|
60
60
|
try {
|
|
61
61
|
process.kill(pid, 0)
|
|
62
62
|
return true
|
|
63
|
-
} catch {
|
|
64
|
-
|
|
63
|
+
} catch (error) {
|
|
64
|
+
// Cursor's agent sandbox cannot signal other processes (EPERM) even when
|
|
65
|
+
// they are alive. POSIX EPERM means the pid exists; only ESRCH is gone.
|
|
66
|
+
return error?.code === 'EPERM'
|
|
65
67
|
}
|
|
66
68
|
}
|
|
67
69
|
|
|
@@ -131,7 +133,7 @@ export function ensureDataDir(dataDir) {
|
|
|
131
133
|
if (!fs.existsSync(userContextFile)) {
|
|
132
134
|
fs.writeFileSync(
|
|
133
135
|
userContextFile,
|
|
134
|
-
`${JSON.stringify({
|
|
136
|
+
`${JSON.stringify({ showBranchChanges: false }, null, 2)}\n`,
|
|
135
137
|
)
|
|
136
138
|
}
|
|
137
139
|
return dataDir
|