@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.
@@ -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
- return saveSession(this.agent.getSession(), label);
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
  }
@@ -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: works in both dev (src/serve/) and dist (dist/src/serve/)
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, "../../src/web")
13
- : path.resolve(__dirname, "../web");
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 {
@@ -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(),