@sean.holung/minicode 0.2.4 → 0.3.0
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/dist/src/serve/agent-bridge.js +127 -4
- package/dist/src/serve/server.js +101 -3
- package/dist/src/session/session-store.js +3 -1
- package/dist/src/ui/cli-ink.js +1 -0
- package/dist/src/web/app.js +2037 -117
- package/dist/src/web/index.html +28 -9
- package/dist/src/web/style.css +503 -3
- package/dist/tests/serve.integration.test.js +225 -0
- package/node_modules/@minicode/agent-sdk/dist/src/agent/agent.d.ts +5 -0
- package/node_modules/@minicode/agent-sdk/dist/src/agent/agent.d.ts.map +1 -1
- package/node_modules/@minicode/agent-sdk/dist/src/agent/agent.js +35 -20
- package/node_modules/@minicode/agent-sdk/dist/src/agent/agent.js.map +1 -1
- package/node_modules/@minicode/agent-sdk/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +7 -4
|
@@ -15,6 +15,7 @@ export class AgentBridge {
|
|
|
15
15
|
verbose;
|
|
16
16
|
listeners = new Set();
|
|
17
17
|
pinnedSymbols = new Set();
|
|
18
|
+
annotations = new Map();
|
|
18
19
|
constructor(broadcast, verbose) {
|
|
19
20
|
this.broadcast = broadcast;
|
|
20
21
|
this.verbose = verbose;
|
|
@@ -51,9 +52,15 @@ export class AgentBridge {
|
|
|
51
52
|
projectIndex = undefined;
|
|
52
53
|
}
|
|
53
54
|
const toolRegistry = createToolRegistry(config, projectIndex);
|
|
55
|
+
// Wrap tool registry execute to inject annotations into tool results
|
|
56
|
+
const originalExecute = toolRegistry.execute.bind(toolRegistry);
|
|
57
|
+
toolRegistry.execute = async (name, input) => {
|
|
58
|
+
const result = await originalExecute(name, input);
|
|
59
|
+
return this.appendAnnotationsToResult(name, input, result);
|
|
60
|
+
};
|
|
54
61
|
this.config = config;
|
|
55
62
|
this.projectIndex = projectIndex;
|
|
56
|
-
this.buildAgent = (session) => {
|
|
63
|
+
this.buildAgent = (session, onUiUpdate) => {
|
|
57
64
|
return new CodingAgent({
|
|
58
65
|
config,
|
|
59
66
|
modelClient,
|
|
@@ -63,9 +70,10 @@ export class AgentBridge {
|
|
|
63
70
|
...(projectIndex !== undefined
|
|
64
71
|
? { getCodeMap: (focusSymbols) => projectIndex.getCodeMap(undefined, focusSymbols) }
|
|
65
72
|
: {}),
|
|
66
|
-
onUiUpdate: (event) => {
|
|
73
|
+
onUiUpdate: onUiUpdate ?? ((event) => {
|
|
67
74
|
this.emit(event);
|
|
68
|
-
},
|
|
75
|
+
}),
|
|
76
|
+
getSystemPromptSuffix: () => this.buildAnnotationSuffix(),
|
|
69
77
|
});
|
|
70
78
|
};
|
|
71
79
|
this.agent = this.buildAgent();
|
|
@@ -118,13 +126,23 @@ export class AgentBridge {
|
|
|
118
126
|
}
|
|
119
127
|
// Session operations
|
|
120
128
|
async saveSess(label) {
|
|
121
|
-
|
|
129
|
+
const annotationsObj = this.annotations.size > 0
|
|
130
|
+
? Object.fromEntries(this.annotations)
|
|
131
|
+
: undefined;
|
|
132
|
+
return saveSession(this.agent.getSession(), label, annotationsObj);
|
|
122
133
|
}
|
|
123
134
|
async loadSess(label) {
|
|
124
135
|
const result = (await loadSessionByLabel(label)) ?? (await loadSession(label));
|
|
125
136
|
if (!result)
|
|
126
137
|
return null;
|
|
127
138
|
this.agent = this.buildAgent(result.session);
|
|
139
|
+
// Restore annotations from saved session
|
|
140
|
+
this.annotations.clear();
|
|
141
|
+
if (result.annotations) {
|
|
142
|
+
for (const [name, notes] of Object.entries(result.annotations)) {
|
|
143
|
+
this.annotations.set(name, notes);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
128
146
|
return result;
|
|
129
147
|
}
|
|
130
148
|
async listSess() {
|
|
@@ -230,4 +248,109 @@ export class AgentBridge {
|
|
|
230
248
|
this.pinnedSymbols.delete(sym.qualifiedName);
|
|
231
249
|
return true;
|
|
232
250
|
}
|
|
251
|
+
// ── Annotations ──
|
|
252
|
+
getAnnotations() {
|
|
253
|
+
this.evictStaleAnnotations();
|
|
254
|
+
return Object.fromEntries(this.annotations);
|
|
255
|
+
}
|
|
256
|
+
getAnnotationsForSymbol(name) {
|
|
257
|
+
return this.annotations.get(name) ?? [];
|
|
258
|
+
}
|
|
259
|
+
addAnnotation(name, text) {
|
|
260
|
+
if (!this.projectIndex)
|
|
261
|
+
return false;
|
|
262
|
+
const sym = this.projectIndex.getSymbol(name);
|
|
263
|
+
if (!sym)
|
|
264
|
+
return false;
|
|
265
|
+
const trimmed = text.slice(0, 500).trim();
|
|
266
|
+
if (trimmed.length === 0)
|
|
267
|
+
return false;
|
|
268
|
+
const key = sym.qualifiedName;
|
|
269
|
+
const existing = this.annotations.get(key) ?? [];
|
|
270
|
+
existing.push(trimmed);
|
|
271
|
+
this.annotations.set(key, existing);
|
|
272
|
+
return true;
|
|
273
|
+
}
|
|
274
|
+
removeAnnotation(name, index) {
|
|
275
|
+
const notes = this.annotations.get(name);
|
|
276
|
+
if (!notes || index < 0 || index >= notes.length)
|
|
277
|
+
return false;
|
|
278
|
+
notes.splice(index, 1);
|
|
279
|
+
if (notes.length === 0) {
|
|
280
|
+
this.annotations.delete(name);
|
|
281
|
+
}
|
|
282
|
+
return true;
|
|
283
|
+
}
|
|
284
|
+
clearAnnotations(name) {
|
|
285
|
+
this.annotations.delete(name);
|
|
286
|
+
}
|
|
287
|
+
evictStaleAnnotations() {
|
|
288
|
+
if (!this.projectIndex)
|
|
289
|
+
return;
|
|
290
|
+
for (const name of [...this.annotations.keys()]) {
|
|
291
|
+
if (!this.projectIndex.getSymbol(name)) {
|
|
292
|
+
this.annotations.delete(name);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
buildAnnotationSuffix() {
|
|
297
|
+
this.evictStaleAnnotations();
|
|
298
|
+
if (this.annotations.size === 0)
|
|
299
|
+
return undefined;
|
|
300
|
+
return `[Annotated symbols: ${[...this.annotations.keys()].join(", ")}]`;
|
|
301
|
+
}
|
|
302
|
+
appendAnnotationsToResult(toolName, input, result) {
|
|
303
|
+
if (this.annotations.size === 0)
|
|
304
|
+
return result;
|
|
305
|
+
const inp = input;
|
|
306
|
+
if (toolName === "read_symbol" || toolName === "find_references" || toolName === "get_dependencies") {
|
|
307
|
+
const symName = (inp.name ?? inp.symbol ?? inp.query);
|
|
308
|
+
if (!symName)
|
|
309
|
+
return result;
|
|
310
|
+
// Try direct match, then resolve via index
|
|
311
|
+
let notes = this.annotations.get(symName);
|
|
312
|
+
if (!notes && this.projectIndex) {
|
|
313
|
+
const sym = this.projectIndex.getSymbol(symName);
|
|
314
|
+
if (sym)
|
|
315
|
+
notes = this.annotations.get(sym.qualifiedName);
|
|
316
|
+
}
|
|
317
|
+
if (notes && notes.length > 0) {
|
|
318
|
+
return result + `\n[User annotation: ${notes.join("; ")}]`;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
if (toolName === "read_file") {
|
|
322
|
+
const filePath = inp.path;
|
|
323
|
+
if (!filePath)
|
|
324
|
+
return result;
|
|
325
|
+
const fileAnnotations = [];
|
|
326
|
+
for (const [name, notes] of this.annotations) {
|
|
327
|
+
if (!this.projectIndex)
|
|
328
|
+
continue;
|
|
329
|
+
const sym = this.projectIndex.getSymbol(name);
|
|
330
|
+
if (sym && (sym.filePath === filePath || filePath.endsWith(sym.filePath))) {
|
|
331
|
+
fileAnnotations.push(`- ${sym.name}: ${notes.join("; ")}`);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
if (fileAnnotations.length > 0) {
|
|
335
|
+
return result + `\n[User annotations for symbols in this file:]\n${fileAnnotations.join("\n")}`;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
return result;
|
|
339
|
+
}
|
|
340
|
+
// ── Explain ──
|
|
341
|
+
async explainSymbol(name, onEvent, signal) {
|
|
342
|
+
if (!this.projectIndex)
|
|
343
|
+
throw new Error("No project index");
|
|
344
|
+
const sym = this.projectIndex.getSymbol(name);
|
|
345
|
+
if (!sym)
|
|
346
|
+
throw new Error(`Symbol "${name}" not found`);
|
|
347
|
+
const explainAgent = this.buildAgent(undefined, onEvent);
|
|
348
|
+
const prompt = `Explain "${sym.name}" (${sym.kind} in ${sym.filePath}).
|
|
349
|
+
Use read_symbol, get_dependencies, find_references to gather context.
|
|
350
|
+
Explain what it does, how it works, what depends on it, and key design decisions.
|
|
351
|
+
Be concise but thorough.`;
|
|
352
|
+
const opts = signal ? { signal } : undefined;
|
|
353
|
+
const result = await explainAgent.runTurn(prompt, opts);
|
|
354
|
+
return result.text;
|
|
355
|
+
}
|
|
233
356
|
}
|
package/dist/src/serve/server.js
CHANGED
|
@@ -7,10 +7,12 @@ import { createWebSocketServer } from "./websocket.js";
|
|
|
7
7
|
import { handleChatCompletions, handleModels } from "./openai-compat.js";
|
|
8
8
|
import { formatConfigForDisplay } from "../agent/config.js";
|
|
9
9
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
10
|
-
// Resolve web dir:
|
|
10
|
+
// Resolve web dir: always serve from dist/src/web (built by scripts/build-web.mjs)
|
|
11
|
+
// In dev (tsx): __dirname = src/serve → go up to project root, then dist/src/web
|
|
12
|
+
// In prod (dist): __dirname = dist/src/serve → sibling dir dist/src/web
|
|
11
13
|
const webDir = __dirname.includes(`${path.sep}dist${path.sep}`)
|
|
12
|
-
? path.resolve(__dirname, "
|
|
13
|
-
: path.resolve(__dirname, "
|
|
14
|
+
? path.resolve(__dirname, "../web")
|
|
15
|
+
: path.resolve(__dirname, "../../dist/src/web");
|
|
14
16
|
const MIME_TYPES = {
|
|
15
17
|
".html": "text/html",
|
|
16
18
|
".css": "text/css",
|
|
@@ -133,6 +135,26 @@ export function createRequestHandler(bridge) {
|
|
|
133
135
|
sendJson(res, 200, { symbol: name, references: result });
|
|
134
136
|
return;
|
|
135
137
|
}
|
|
138
|
+
if (pathname.startsWith("/api/symbols/") && pathname.endsWith("/source") && method === "GET") {
|
|
139
|
+
const name = decodeURIComponent(pathname.slice("/api/symbols/".length, -"/source".length));
|
|
140
|
+
const sym = bridge.getSymbol(name);
|
|
141
|
+
if (!sym) {
|
|
142
|
+
sendJson(res, 404, { error: `Symbol "${name}" not found` });
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
try {
|
|
146
|
+
const fileContent = await readFile(path.resolve(config.workspaceRoot, sym.filePath), "utf8");
|
|
147
|
+
const lines = fileContent.split(/\r?\n/);
|
|
148
|
+
const start = Math.max(0, sym.startLine - 1);
|
|
149
|
+
const end = Math.min(lines.length, sym.endLine);
|
|
150
|
+
const source = lines.slice(start, end).join("\n");
|
|
151
|
+
sendJson(res, 200, { symbol: name, filePath: sym.filePath, startLine: sym.startLine, endLine: sym.endLine, source });
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
sendJson(res, 500, { error: `Could not read file: ${sym.filePath}` });
|
|
155
|
+
}
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
136
158
|
if (pathname === "/api/code-map" && method === "GET") {
|
|
137
159
|
const budgetParam = url.searchParams.get("budget");
|
|
138
160
|
const budget = budgetParam ? Number(budgetParam) : undefined;
|
|
@@ -180,6 +202,82 @@ export function createRequestHandler(bridge) {
|
|
|
180
202
|
sendJson(res, 400, { error: `Unknown action "${body.action}". Use "pin" or "unpin".` });
|
|
181
203
|
return;
|
|
182
204
|
}
|
|
205
|
+
// ── Annotations API ──
|
|
206
|
+
if (pathname === "/api/annotations" && method === "GET") {
|
|
207
|
+
sendJson(res, 200, { annotations: bridge.getAnnotations() });
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
if (pathname.startsWith("/api/symbols/") && pathname.endsWith("/annotations") && method === "GET") {
|
|
211
|
+
const name = decodeURIComponent(pathname.slice("/api/symbols/".length, -"/annotations".length));
|
|
212
|
+
const notes = bridge.getAnnotationsForSymbol(name);
|
|
213
|
+
sendJson(res, 200, { symbol: name, annotations: notes });
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
if (pathname.startsWith("/api/symbols/") && pathname.endsWith("/annotations") && method === "POST") {
|
|
217
|
+
const name = decodeURIComponent(pathname.slice("/api/symbols/".length, -"/annotations".length));
|
|
218
|
+
const body = JSON.parse(await readBody(req));
|
|
219
|
+
if (!body.text) {
|
|
220
|
+
sendJson(res, 400, { error: "text is required" });
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
const ok = bridge.addAnnotation(name, body.text);
|
|
224
|
+
if (!ok) {
|
|
225
|
+
sendJson(res, 404, { error: `Symbol "${name}" not found or text empty` });
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
sendJson(res, 200, { symbol: name, annotations: bridge.getAnnotationsForSymbol(name) });
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
// DELETE /api/symbols/:name/annotations/:index
|
|
232
|
+
{
|
|
233
|
+
const annoDeleteMatch = pathname.match(/^\/api\/symbols\/(.+)\/annotations\/(\d+)$/);
|
|
234
|
+
if (annoDeleteMatch && method === "DELETE") {
|
|
235
|
+
const name = decodeURIComponent(annoDeleteMatch[1]);
|
|
236
|
+
const index = Number(annoDeleteMatch[2]);
|
|
237
|
+
const ok = bridge.removeAnnotation(name, index);
|
|
238
|
+
if (!ok) {
|
|
239
|
+
sendJson(res, 404, { error: "Annotation not found" });
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
sendJson(res, 200, { symbol: name, annotations: bridge.getAnnotationsForSymbol(name) });
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
if (pathname.startsWith("/api/symbols/") && pathname.endsWith("/annotations") && method === "DELETE") {
|
|
247
|
+
const name = decodeURIComponent(pathname.slice("/api/symbols/".length, -"/annotations".length));
|
|
248
|
+
bridge.clearAnnotations(name);
|
|
249
|
+
sendJson(res, 200, { symbol: name, annotations: [] });
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
// ── Explain SSE ──
|
|
253
|
+
if (pathname.startsWith("/api/symbols/") && pathname.endsWith("/explain") && method === "GET") {
|
|
254
|
+
const name = decodeURIComponent(pathname.slice("/api/symbols/".length, -"/explain".length));
|
|
255
|
+
res.writeHead(200, {
|
|
256
|
+
"Content-Type": "text/event-stream",
|
|
257
|
+
"Cache-Control": "no-cache",
|
|
258
|
+
Connection: "keep-alive",
|
|
259
|
+
});
|
|
260
|
+
const abortController = new AbortController();
|
|
261
|
+
req.on("close", () => abortController.abort());
|
|
262
|
+
try {
|
|
263
|
+
await bridge.explainSymbol(name, (event) => {
|
|
264
|
+
if (!res.writableEnded) {
|
|
265
|
+
res.write(`data: ${JSON.stringify(event)}\n\n`);
|
|
266
|
+
}
|
|
267
|
+
}, abortController.signal);
|
|
268
|
+
}
|
|
269
|
+
catch (error) {
|
|
270
|
+
if (!res.writableEnded) {
|
|
271
|
+
const msg = error instanceof Error ? error.message : "Unknown error";
|
|
272
|
+
res.write(`data: ${JSON.stringify({ type: "error", message: msg })}\n\n`);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
if (!res.writableEnded) {
|
|
276
|
+
res.write("data: [DONE]\n\n");
|
|
277
|
+
res.end();
|
|
278
|
+
}
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
183
281
|
if (pathname === "/api/chat" && method === "POST") {
|
|
184
282
|
const body = JSON.parse(await readBody(req));
|
|
185
283
|
if (!body.message) {
|
|
@@ -7,7 +7,7 @@ let sessionsDir = path.join(os.homedir(), ".minicode", "sessions");
|
|
|
7
7
|
export function setSessionsDir(dir) {
|
|
8
8
|
sessionsDir = dir;
|
|
9
9
|
}
|
|
10
|
-
export async function saveSession(session, label) {
|
|
10
|
+
export async function saveSession(session, label, annotations) {
|
|
11
11
|
await mkdir(sessionsDir, { recursive: true });
|
|
12
12
|
const savedAt = new Date().toISOString();
|
|
13
13
|
const snapshot = session.toJSON();
|
|
@@ -18,6 +18,7 @@ export async function saveSession(session, label) {
|
|
|
18
18
|
label: resolvedLabel,
|
|
19
19
|
savedAt,
|
|
20
20
|
session: snapshot,
|
|
21
|
+
...(annotations && Object.keys(annotations).length > 0 ? { annotations } : {}),
|
|
21
22
|
};
|
|
22
23
|
const filePath = path.join(sessionsDir, `${snapshot.id}.json`);
|
|
23
24
|
await writeFile(filePath, JSON.stringify(data, null, 2), "utf8");
|
|
@@ -67,6 +68,7 @@ export async function loadSession(sessionId) {
|
|
|
67
68
|
return {
|
|
68
69
|
session: Session.fromJSON(data.session),
|
|
69
70
|
label: data.label,
|
|
71
|
+
...(data.annotations ? { annotations: data.annotations } : {}),
|
|
70
72
|
};
|
|
71
73
|
}
|
|
72
74
|
catch {
|
package/dist/src/ui/cli-ink.js
CHANGED
|
@@ -90,6 +90,7 @@ export async function runInkCli(verbose, initialTask) {
|
|
|
90
90
|
...(verbose
|
|
91
91
|
? {
|
|
92
92
|
onProgress: (msg) => store.addItem({ type: "system", content: msg }),
|
|
93
|
+
onVerbose: (msg) => store.addItem({ type: "system", content: msg }),
|
|
93
94
|
}
|
|
94
95
|
: {}),
|
|
95
96
|
onUiUpdate: createUiUpdateHandler(),
|