@sean.holung/minicode 0.2.4 → 0.3.1

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.
Files changed (28) hide show
  1. package/dist/src/indexer/code-map.js +2 -1
  2. package/dist/src/indexer/plugins/typescript.js +63 -34
  3. package/dist/src/serve/agent-bridge.js +127 -4
  4. package/dist/src/serve/server.js +101 -3
  5. package/dist/src/session/session-store.js +3 -1
  6. package/dist/src/ui/cli-ink.js +1 -0
  7. package/dist/src/web/app.js +2037 -117
  8. package/dist/src/web/index.html +28 -9
  9. package/dist/src/web/style.css +503 -3
  10. package/dist/tests/benchmark-index.test.js +152 -0
  11. package/dist/tests/indexer.test.js +62 -0
  12. package/dist/tests/serve.integration.test.js +225 -0
  13. package/node_modules/@minicode/agent-sdk/dist/src/agent/agent.d.ts +6 -1
  14. package/node_modules/@minicode/agent-sdk/dist/src/agent/agent.d.ts.map +1 -1
  15. package/node_modules/@minicode/agent-sdk/dist/src/agent/agent.js +35 -20
  16. package/node_modules/@minicode/agent-sdk/dist/src/agent/agent.js.map +1 -1
  17. package/node_modules/@minicode/agent-sdk/dist/src/index.d.ts +2 -1
  18. package/node_modules/@minicode/agent-sdk/dist/src/index.d.ts.map +1 -1
  19. package/node_modules/@minicode/agent-sdk/dist/src/index.js.map +1 -1
  20. package/node_modules/@minicode/agent-sdk/dist/src/indexer/types.d.ts +68 -0
  21. package/node_modules/@minicode/agent-sdk/dist/src/indexer/types.d.ts.map +1 -0
  22. package/node_modules/@minicode/agent-sdk/dist/src/indexer/types.js +2 -0
  23. package/node_modules/@minicode/agent-sdk/dist/src/indexer/types.js.map +1 -0
  24. package/node_modules/@minicode/agent-sdk/dist/src/prompt/system-prompt.d.ts +1 -5
  25. package/node_modules/@minicode/agent-sdk/dist/src/prompt/system-prompt.d.ts.map +1 -1
  26. package/node_modules/@minicode/agent-sdk/dist/src/prompt/system-prompt.js.map +1 -1
  27. package/node_modules/@minicode/agent-sdk/dist/tsconfig.tsbuildinfo +1 -1
  28. package/package.json +7 -4
@@ -0,0 +1,152 @@
1
+ import assert from "node:assert/strict";
2
+ import path from "node:path";
3
+ import { test } from "node:test";
4
+ import { buildProjectIndex } from "../src/indexer/project-index.js";
5
+ const FIXTURE_ROOT = path.resolve(import.meta.dirname, "..", "test-programs", "benchmark-index");
6
+ let index;
7
+ // Build the index once, reuse across tests
8
+ test("benchmark fixture: build index", async () => {
9
+ index = await buildProjectIndex(FIXTURE_ROOT);
10
+ assert.ok(index.symbols.size > 0, "should index symbols");
11
+ assert.ok(index.files.size > 0, "should index files");
12
+ assert.ok(index.dependencyEdges.length > 0, "should produce dependency edges");
13
+ });
14
+ // ---------------------------------------------------------------------------
15
+ // Symbol extraction
16
+ // ---------------------------------------------------------------------------
17
+ test("benchmark: indexes TypeScript class declarations", () => {
18
+ const app = index.getSymbol("App");
19
+ assert.ok(app, "should index App class");
20
+ assert.equal(app.kind, "class");
21
+ assert.equal(app.exported, true);
22
+ assert.ok(index.getSymbol("App.start"), "should index App.start method");
23
+ assert.ok(index.getSymbol("App.constructor"), "should index App constructor");
24
+ });
25
+ test("benchmark: indexes JS named class expressions (const X = class X {})", () => {
26
+ const logger = index.getSymbol("ConsoleLogger");
27
+ assert.ok(logger, "should index ConsoleLogger class expression");
28
+ assert.equal(logger.kind, "class");
29
+ assert.ok(index.getSymbol("ConsoleLogger.info"), "should index ConsoleLogger.info");
30
+ assert.ok(index.getSymbol("ConsoleLogger.error"), "should index ConsoleLogger.error");
31
+ assert.ok(index.getSymbol("ConsoleLogger.constructor"), "should index constructor");
32
+ });
33
+ test("benchmark: indexes JS anonymous class expressions (const X = class {})", () => {
34
+ const bus = index.getSymbol("EventBus");
35
+ assert.ok(bus, "should index EventBus anonymous class expression");
36
+ assert.equal(bus.kind, "class");
37
+ assert.ok(index.getSymbol("EventBus.on"), "should index EventBus.on");
38
+ assert.ok(index.getSymbol("EventBus.emit"), "should index EventBus.emit");
39
+ assert.ok(index.getSymbol("EventBus.constructor"), "should index EventBus constructor");
40
+ });
41
+ test("benchmark: indexes JS class declarations with inheritance", () => {
42
+ const base = index.getSymbol("BasePlugin");
43
+ assert.ok(base, "should index BasePlugin");
44
+ assert.equal(base.kind, "class");
45
+ const auth = index.getSymbol("AuthPlugin");
46
+ assert.ok(auth, "should index AuthPlugin");
47
+ assert.equal(auth.kind, "class");
48
+ assert.ok(index.getSymbol("AuthPlugin.setupAuth"), "should index AuthPlugin.setupAuth");
49
+ });
50
+ test("benchmark: indexes interfaces and type aliases", () => {
51
+ const logger = index.getSymbol("Logger");
52
+ assert.ok(logger, "should index Logger interface");
53
+ assert.equal(logger.kind, "interface");
54
+ const plugin = index.getSymbol("Plugin");
55
+ assert.ok(plugin, "should index Plugin interface");
56
+ assert.equal(plugin.kind, "interface");
57
+ const startable = index.getSymbol("Startable");
58
+ assert.ok(startable, "should index Startable interface");
59
+ const logLevel = index.getSymbol("LogLevel");
60
+ assert.ok(logLevel, "should index LogLevel type alias");
61
+ assert.equal(logLevel.kind, "type");
62
+ const handler = index.getSymbol("EventHandler");
63
+ assert.ok(handler, "should index EventHandler type alias");
64
+ assert.equal(handler.kind, "type");
65
+ });
66
+ test("benchmark: indexes arrow functions and function expressions", () => {
67
+ const fmt = index.getSymbol("formatMessage");
68
+ assert.ok(fmt, "should index arrow function formatMessage");
69
+ assert.equal(fmt.kind, "function");
70
+ const parse = index.getSymbol("parseEvent");
71
+ assert.ok(parse, "should index function expression parseEvent");
72
+ assert.equal(parse.kind, "function");
73
+ const handler = index.getSymbol("createHandler");
74
+ assert.ok(handler, "should index function declaration createHandler");
75
+ assert.equal(handler.kind, "function");
76
+ });
77
+ test("benchmark: indexes function declarations in JS", () => {
78
+ const create = index.getSymbol("createLogger");
79
+ assert.ok(create, "should index createLogger");
80
+ assert.equal(create.kind, "function");
81
+ assert.equal(create.exported, true);
82
+ const factory = index.getSymbol("createPlugin");
83
+ assert.ok(factory, "should index createPlugin");
84
+ assert.equal(factory.kind, "function");
85
+ });
86
+ test("benchmark: indexes entry point function", () => {
87
+ const main = index.getSymbol("main");
88
+ assert.ok(main, "should index main");
89
+ assert.equal(main.kind, "function");
90
+ assert.equal(main.exported, true);
91
+ });
92
+ // ---------------------------------------------------------------------------
93
+ // Dependency edges
94
+ // ---------------------------------------------------------------------------
95
+ function hasEdge(from, to, kind) {
96
+ return index.dependencyEdges.some((e) => e.from === from && e.to === to && e.kind === kind);
97
+ }
98
+ test("benchmark: extends edges", () => {
99
+ assert.ok(hasEdge("AuthPlugin", "BasePlugin", "extends"), "AuthPlugin should extend BasePlugin");
100
+ });
101
+ test("benchmark: implements edges", () => {
102
+ assert.ok(hasEdge("App", "Startable", "implements"), "App should implement Startable");
103
+ });
104
+ test("benchmark: new expression call edges", () => {
105
+ assert.ok(hasEdge("main", "App", "calls"), "main should call new App()");
106
+ assert.ok(hasEdge("App.start", "AuthPlugin", "calls"), "App.start should call new AuthPlugin()");
107
+ assert.ok(hasEdge("createLogger", "ConsoleLogger", "calls"), "createLogger should call new ConsoleLogger()");
108
+ assert.ok(hasEdge("createPlugin", "AuthPlugin", "calls"), "createPlugin should call new AuthPlugin()");
109
+ assert.ok(hasEdge("createPlugin", "BasePlugin", "calls"), "createPlugin should call new BasePlugin()");
110
+ });
111
+ test("benchmark: function call edges", () => {
112
+ assert.ok(hasEdge("main", "createLogger", "calls"), "main should call createLogger()");
113
+ assert.ok(hasEdge("createHandler", "formatMessage", "calls"), "createHandler should call formatMessage()");
114
+ });
115
+ test("benchmark: type reference edges", () => {
116
+ assert.ok(hasEdge("App.constructor", "Logger", "references"), "App.constructor should reference Logger type");
117
+ assert.ok(hasEdge("App.start", "AuthPlugin", "calls"), "App.start should call new AuthPlugin()");
118
+ assert.ok(hasEdge("createHandler", "EventHandler", "references"), "createHandler should reference EventHandler type");
119
+ });
120
+ // ---------------------------------------------------------------------------
121
+ // Dependency cone traversal
122
+ // ---------------------------------------------------------------------------
123
+ test("benchmark: getDependencyCone from main", () => {
124
+ const cone = index.getDependencyCone("main", 2);
125
+ const names = cone.map((s) => s.qualifiedName);
126
+ assert.ok(names.includes("main"), "cone should include main itself");
127
+ assert.ok(names.includes("createLogger"), "depth 1: main calls createLogger");
128
+ assert.ok(names.includes("App"), "depth 1: main calls new App");
129
+ assert.ok(names.includes("ConsoleLogger"), "depth 2: createLogger calls new ConsoleLogger");
130
+ });
131
+ test("benchmark: getDependencyCone from App.start", () => {
132
+ const cone = index.getDependencyCone("App.start", 1);
133
+ const names = cone.map((s) => s.qualifiedName);
134
+ assert.ok(names.includes("App.start"), "cone should include App.start itself");
135
+ assert.ok(names.includes("AuthPlugin"), "App.start calls new AuthPlugin");
136
+ });
137
+ // ---------------------------------------------------------------------------
138
+ // Code map
139
+ // ---------------------------------------------------------------------------
140
+ test("benchmark: code map includes key symbols", () => {
141
+ const codeMap = index.getCodeMap();
142
+ assert.ok(codeMap.text.includes("App"), "code map should include App");
143
+ assert.ok(codeMap.text.includes("ConsoleLogger"), "code map should include ConsoleLogger");
144
+ assert.ok(codeMap.text.includes("EventBus"), "code map should include EventBus");
145
+ assert.ok(codeMap.text.includes("main"), "code map should include main");
146
+ assert.ok(codeMap.totalCount > 0, "should report total symbol count");
147
+ });
148
+ test("benchmark: code map includes JS and TS files", () => {
149
+ const codeMap = index.getCodeMap();
150
+ assert.ok(codeMap.text.includes(".ts"), "code map should include .ts files");
151
+ assert.ok(codeMap.text.includes(".js"), "code map should include .js files");
152
+ });
@@ -199,3 +199,65 @@ test("reindexFile updates symbols and code map after file change", async () => {
199
199
  const codeMap = index.getCodeMap();
200
200
  assert.ok(codeMap.text.includes("title?: string"), "code map should reflect new signature");
201
201
  });
202
+ test("TypeScript plugin indexes class expressions assigned to variables", () => {
203
+ const code = `const MyClass = class MyClass {
204
+ constructor(name) {
205
+ this.name = name;
206
+ }
207
+
208
+ greet() {
209
+ return "hello " + this.name;
210
+ }
211
+ };`;
212
+ const symbols = typescriptPlugin.indexFile("class-expr.js", code);
213
+ const names = symbols.map((s) => s.qualifiedName);
214
+ assert.ok(names.includes("MyClass"), "should extract class expression MyClass");
215
+ assert.ok(names.includes("MyClass.constructor"), "should extract constructor");
216
+ assert.ok(names.includes("MyClass.greet"), "should extract method greet");
217
+ const cls = symbols.find((s) => s.qualifiedName === "MyClass");
218
+ assert.equal(cls.kind, "class");
219
+ });
220
+ test("TypeScript plugin indexes anonymous class expressions using variable name", () => {
221
+ const code = `const Widget = class {
222
+ render() {
223
+ return "<div/>";
224
+ }
225
+ };`;
226
+ const symbols = typescriptPlugin.indexFile("anon-class.js", code);
227
+ const names = symbols.map((s) => s.qualifiedName);
228
+ assert.ok(names.includes("Widget"), "should extract anonymous class as Widget");
229
+ assert.ok(names.includes("Widget.render"), "should extract method with class prefix");
230
+ const cls = symbols.find((s) => s.qualifiedName === "Widget");
231
+ assert.equal(cls.kind, "class");
232
+ });
233
+ test("TypeScript plugin indexes exported class expressions", () => {
234
+ const code = `export const Service = class Service {
235
+ start() {}
236
+ stop() {}
237
+ };`;
238
+ const symbols = typescriptPlugin.indexFile("service.js", code);
239
+ const cls = symbols.find((s) => s.qualifiedName === "Service");
240
+ assert.ok(cls, "should extract exported class expression");
241
+ assert.equal(cls.exported, true, "should mark as exported");
242
+ assert.equal(cls.kind, "class");
243
+ const names = symbols.map((s) => s.qualifiedName);
244
+ assert.ok(names.includes("Service.start"));
245
+ assert.ok(names.includes("Service.stop"));
246
+ });
247
+ test("resolveDependencies tracks new expressions as calls edges", () => {
248
+ const code = `
249
+ class Logger {
250
+ log(msg) {}
251
+ }
252
+
253
+ function setup() {
254
+ const logger = new Logger();
255
+ logger.log("ready");
256
+ }
257
+ `;
258
+ const symbols = typescriptPlugin.indexFile("new-expr.js", code);
259
+ const projectFiles = new Map([["new-expr.js", code]]);
260
+ const edges = typescriptPlugin.resolveDependencies(symbols, projectFiles);
261
+ const newCallEdge = edges.find((e) => e.from === "setup" && e.to === "Logger" && e.kind === "calls");
262
+ assert.ok(newCallEdge, "new Logger() should produce a calls edge from setup to Logger");
263
+ });
@@ -1,6 +1,7 @@
1
1
  import assert from "node:assert/strict";
2
2
  import { test, afterEach } from "node:test";
3
3
  import { createServer } from "node:http";
4
+ import { mkdirSync, writeFileSync, rmSync } from "node:fs";
4
5
  import { createRequestHandler } from "../src/serve/server.js";
5
6
  import { AgentBridge } from "../src/serve/agent-bridge.js";
6
7
  import { createTestAgentConfig } from "./test-utils.js";
@@ -126,6 +127,49 @@ class MockBridge extends AgentBridge {
126
127
  this._pinned.delete(name);
127
128
  return true;
128
129
  }
130
+ // Annotation state for testing
131
+ _annotations = new Map();
132
+ getAnnotations() {
133
+ return Object.fromEntries(this._annotations);
134
+ }
135
+ getAnnotationsForSymbol(name) {
136
+ // Resolve symbol name to qualifiedName
137
+ const sym = this.getSymbol(name);
138
+ const key = sym ? sym.qualifiedName : name;
139
+ return this._annotations.get(key) ?? [];
140
+ }
141
+ addAnnotation(name, text) {
142
+ const sym = this.getSymbol(name);
143
+ if (!sym)
144
+ return false;
145
+ const trimmed = text.slice(0, 500).trim();
146
+ if (trimmed.length === 0)
147
+ return false;
148
+ const key = sym.qualifiedName;
149
+ const existing = this._annotations.get(key) ?? [];
150
+ existing.push(trimmed);
151
+ this._annotations.set(key, existing);
152
+ return true;
153
+ }
154
+ removeAnnotation(name, index) {
155
+ const notes = this._annotations.get(name);
156
+ if (!notes || index < 0 || index >= notes.length)
157
+ return false;
158
+ notes.splice(index, 1);
159
+ if (notes.length === 0)
160
+ this._annotations.delete(name);
161
+ return true;
162
+ }
163
+ clearAnnotations(name) {
164
+ this._annotations.delete(name);
165
+ }
166
+ async explainSymbol(name, onEvent) {
167
+ const sym = this.getSymbol(name);
168
+ if (!sym)
169
+ throw new Error(`Symbol "${name}" not found`);
170
+ onEvent({ type: "streaming_chunk", content: `Explaining ${name}...` });
171
+ return `Explaining ${name}...`;
172
+ }
129
173
  }
130
174
  // ── Test harness ──
131
175
  let activeServer;
@@ -532,3 +576,184 @@ test("POST /api/focus returns 400 for invalid action", async () => {
532
576
  });
533
577
  assert.equal(res.status, 400);
534
578
  });
579
+ // ── Symbol source endpoint tests ──
580
+ test("GET /api/symbols/:name/source returns source code for known symbol", async () => {
581
+ const bridge = new MockBridge();
582
+ const base = await startTestServer(bridge);
583
+ // Create a temp file matching the mock symbol's filePath
584
+ const wsRoot = "/tmp/test-workspace";
585
+ mkdirSync(`${wsRoot}/src`, { recursive: true });
586
+ writeFileSync(`${wsRoot}/src/foo.ts`, "line1\nfunction foo(): void {\n return;\n}\nline5\n");
587
+ try {
588
+ const res = await fetch(`${base}/api/symbols/foo/source`);
589
+ assert.equal(res.status, 200);
590
+ const body = (await res.json());
591
+ assert.equal(body.symbol, "foo");
592
+ assert.equal(body.filePath, "src/foo.ts");
593
+ assert.equal(body.startLine, 1);
594
+ assert.equal(body.endLine, 5);
595
+ assert.ok(body.source.includes("line1"));
596
+ }
597
+ finally {
598
+ rmSync(wsRoot, { recursive: true, force: true });
599
+ }
600
+ });
601
+ test("GET /api/symbols/:name/source returns 404 for unknown symbol", async () => {
602
+ const bridge = new MockBridge();
603
+ const base = await startTestServer(bridge);
604
+ const res = await fetch(`${base}/api/symbols/nonexistent/source`);
605
+ assert.equal(res.status, 404);
606
+ const body = (await res.json());
607
+ assert.ok(body.error.includes("nonexistent"));
608
+ });
609
+ test("GET /api/symbols/:name/source returns 500 when file is missing", async () => {
610
+ const bridge = new MockBridge();
611
+ const base = await startTestServer(bridge);
612
+ // Don't create the file — let it fail with a read error
613
+ const res = await fetch(`${base}/api/symbols/foo/source`);
614
+ assert.equal(res.status, 500);
615
+ const body = (await res.json());
616
+ assert.ok(body.error.includes("src/foo.ts"));
617
+ });
618
+ // ── Annotations API tests ──
619
+ test("GET /api/annotations returns empty annotations initially", async () => {
620
+ const bridge = new MockBridge();
621
+ const base = await startTestServer(bridge);
622
+ const res = await fetch(`${base}/api/annotations`);
623
+ assert.equal(res.status, 200);
624
+ const body = (await res.json());
625
+ assert.deepEqual(body.annotations, {});
626
+ });
627
+ test("POST /api/symbols/:name/annotations adds annotation", async () => {
628
+ const bridge = new MockBridge();
629
+ const base = await startTestServer(bridge);
630
+ const res = await fetch(`${base}/api/symbols/foo/annotations`, {
631
+ method: "POST",
632
+ headers: { "Content-Type": "application/json" },
633
+ body: JSON.stringify({ text: "don't modify, stable API" }),
634
+ });
635
+ assert.equal(res.status, 200);
636
+ const body = (await res.json());
637
+ assert.equal(body.symbol, "foo");
638
+ assert.equal(body.annotations.length, 1);
639
+ assert.equal(body.annotations[0], "don't modify, stable API");
640
+ });
641
+ test("GET /api/symbols/:name/annotations returns annotations for symbol", async () => {
642
+ const bridge = new MockBridge();
643
+ const base = await startTestServer(bridge);
644
+ // Add one first
645
+ await fetch(`${base}/api/symbols/foo/annotations`, {
646
+ method: "POST",
647
+ headers: { "Content-Type": "application/json" },
648
+ body: JSON.stringify({ text: "note 1" }),
649
+ });
650
+ const res = await fetch(`${base}/api/symbols/foo/annotations`);
651
+ assert.equal(res.status, 200);
652
+ const body = (await res.json());
653
+ assert.equal(body.annotations.length, 1);
654
+ assert.equal(body.annotations[0], "note 1");
655
+ });
656
+ test("POST /api/symbols/:name/annotations returns 404 for unknown symbol", async () => {
657
+ const bridge = new MockBridge();
658
+ const base = await startTestServer(bridge);
659
+ const res = await fetch(`${base}/api/symbols/nonexistent/annotations`, {
660
+ method: "POST",
661
+ headers: { "Content-Type": "application/json" },
662
+ body: JSON.stringify({ text: "hello" }),
663
+ });
664
+ assert.equal(res.status, 404);
665
+ });
666
+ test("POST /api/symbols/:name/annotations returns 400 for missing text", async () => {
667
+ const bridge = new MockBridge();
668
+ const base = await startTestServer(bridge);
669
+ const res = await fetch(`${base}/api/symbols/foo/annotations`, {
670
+ method: "POST",
671
+ headers: { "Content-Type": "application/json" },
672
+ body: JSON.stringify({}),
673
+ });
674
+ assert.equal(res.status, 400);
675
+ });
676
+ test("DELETE /api/symbols/:name/annotations/:index removes annotation", async () => {
677
+ const bridge = new MockBridge();
678
+ const base = await startTestServer(bridge);
679
+ // Add two annotations
680
+ await fetch(`${base}/api/symbols/foo/annotations`, {
681
+ method: "POST",
682
+ headers: { "Content-Type": "application/json" },
683
+ body: JSON.stringify({ text: "first" }),
684
+ });
685
+ await fetch(`${base}/api/symbols/foo/annotations`, {
686
+ method: "POST",
687
+ headers: { "Content-Type": "application/json" },
688
+ body: JSON.stringify({ text: "second" }),
689
+ });
690
+ // Remove first
691
+ const res = await fetch(`${base}/api/symbols/foo/annotations/0`, {
692
+ method: "DELETE",
693
+ });
694
+ assert.equal(res.status, 200);
695
+ const body = (await res.json());
696
+ assert.equal(body.annotations.length, 1);
697
+ assert.equal(body.annotations[0], "second");
698
+ });
699
+ test("DELETE /api/symbols/:name/annotations/:index returns 404 for invalid index", async () => {
700
+ const bridge = new MockBridge();
701
+ const base = await startTestServer(bridge);
702
+ const res = await fetch(`${base}/api/symbols/foo/annotations/99`, {
703
+ method: "DELETE",
704
+ });
705
+ assert.equal(res.status, 404);
706
+ });
707
+ test("DELETE /api/symbols/:name/annotations clears all annotations", async () => {
708
+ const bridge = new MockBridge();
709
+ const base = await startTestServer(bridge);
710
+ // Add annotations
711
+ await fetch(`${base}/api/symbols/foo/annotations`, {
712
+ method: "POST",
713
+ headers: { "Content-Type": "application/json" },
714
+ body: JSON.stringify({ text: "note" }),
715
+ });
716
+ // Clear all
717
+ const res = await fetch(`${base}/api/symbols/foo/annotations`, {
718
+ method: "DELETE",
719
+ });
720
+ assert.equal(res.status, 200);
721
+ const body = (await res.json());
722
+ assert.deepEqual(body.annotations, []);
723
+ });
724
+ test("GET /api/annotations returns all annotations after adding", async () => {
725
+ const bridge = new MockBridge();
726
+ const base = await startTestServer(bridge);
727
+ await fetch(`${base}/api/symbols/foo/annotations`, {
728
+ method: "POST",
729
+ headers: { "Content-Type": "application/json" },
730
+ body: JSON.stringify({ text: "foo note" }),
731
+ });
732
+ await fetch(`${base}/api/symbols/Bar/annotations`, {
733
+ method: "POST",
734
+ headers: { "Content-Type": "application/json" },
735
+ body: JSON.stringify({ text: "bar note" }),
736
+ });
737
+ const res = await fetch(`${base}/api/annotations`);
738
+ assert.equal(res.status, 200);
739
+ const body = (await res.json());
740
+ assert.ok(Object.keys(body.annotations).length >= 2);
741
+ });
742
+ test("GET /api/symbols/:name/explain returns SSE stream", async () => {
743
+ const bridge = new MockBridge();
744
+ const base = await startTestServer(bridge);
745
+ const res = await fetch(`${base}/api/symbols/foo/explain`);
746
+ assert.equal(res.status, 200);
747
+ assert.equal(res.headers.get("content-type"), "text/event-stream");
748
+ const text = await res.text();
749
+ assert.ok(text.includes("data: "));
750
+ assert.ok(text.includes("[DONE]"));
751
+ });
752
+ test("GET /api/symbols/:name/explain returns error for unknown symbol", async () => {
753
+ const bridge = new MockBridge();
754
+ const base = await startTestServer(bridge);
755
+ const res = await fetch(`${base}/api/symbols/nonexistent/explain`);
756
+ assert.equal(res.status, 200); // SSE always starts 200
757
+ const text = await res.text();
758
+ assert.ok(text.includes("error"));
759
+ });
@@ -1,4 +1,4 @@
1
- import type { CodeMapResult } from "../prompt/system-prompt.js";
1
+ import type { CodeMapResult } from "../indexer/types.js";
2
2
  import { Session } from "../session/session.js";
3
3
  import type { CompactionResult } from "../session/session.js";
4
4
  import { ToolRegistry } from "../tools/registry.js";
@@ -37,6 +37,8 @@ export declare class CodingAgent {
37
37
  private readonly verbose;
38
38
  private readonly onProgress;
39
39
  private readonly onUiUpdate;
40
+ private readonly onVerbose;
41
+ private readonly getSystemPromptSuffix;
40
42
  /**
41
43
  * Tracks symbol names the user/agent has been working with.
42
44
  * Persists across turns so the code map stays focused on the
@@ -59,7 +61,10 @@ export declare class CodingAgent {
59
61
  verbose?: boolean;
60
62
  onProgress?: (message: string) => void;
61
63
  onUiUpdate?: (event: UiUpdate) => void;
64
+ onVerbose?: (message: string) => void;
65
+ getSystemPromptSuffix?: () => string | undefined;
62
66
  });
67
+ private verboseLog;
63
68
  getSession(): Session;
64
69
  /**
65
70
  * Manually compact the conversation context.
@@ -1 +1 @@
1
- {"version":3,"file":"agent.d.ts","sourceRoot":"","sources":["../../../src/agent/agent.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"agent.d.ts","sourceRoot":"","sources":["../../../src/agent/agent.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEzD,OAAO,EAAE,OAAO,EAAE,MAAM,uBAAuB,CAAC;AAChD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAC9D,OAAO,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AACpD,OAAO,KAAK,EAAE,WAAW,EAAE,WAAW,EAAY,MAAM,YAAY,CAAC;AAiGrE,MAAM,MAAM,gBAAgB,GAAG;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AACrE,MAAM,MAAM,sBAAsB,GAAG;IAAE,IAAI,EAAE,iBAAiB,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AAClF,MAAM,MAAM,YAAY,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC;AAC1D,MAAM,MAAM,qBAAqB,GAAG;IAClC,IAAI,EAAE,iBAAiB,CAAC;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAChC,CAAC;AACF,MAAM,MAAM,mBAAmB,GAAG;IAChC,IAAI,EAAE,eAAe,CAAC;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC/B,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;CACnB,CAAC;AACF,MAAM,MAAM,QAAQ,GAChB,gBAAgB,GAChB,sBAAsB,GACtB,YAAY,GACZ,qBAAqB,GACrB,mBAAmB,CAAC;AA+BxB,qBAAa,WAAW;IACtB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAU;IAClC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAc;IACrC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAc;IAC1C,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAe;IAC5C,OAAO,CAAC,QAAQ,CAAC,UAAU,CAA0E;IACrG,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAU;IAClC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAA0C;IACrE,OAAO,CAAC,QAAQ,CAAC,UAAU,CAA0C;IACrE,OAAO,CAAC,QAAQ,CAAC,SAAS,CAA0C;IACpE,OAAO,CAAC,QAAQ,CAAC,qBAAqB,CAAyC;IAE/E;;;;OAIG;IACH,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAkC;IACjE,OAAO,CAAC,eAAe,CAAK;IAE5B;;;;OAIG;IACH,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAkC;gBAEpD,MAAM,EAAE;QAClB,MAAM,EAAE,WAAW,CAAC;QACpB,WAAW,EAAE,WAAW,CAAC;QACzB,YAAY,EAAE,YAAY,CAAC;QAC3B,OAAO,CAAC,EAAE,OAAO,CAAC;QAClB,UAAU,CAAC,EAAE,CAAC,YAAY,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,KAAK,aAAa,GAAG,SAAS,CAAC;QACvE,OAAO,CAAC,EAAE,OAAO,CAAC;QAClB,UAAU,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;QACvC,UAAU,CAAC,EAAE,CAAC,KAAK,EAAE,QAAQ,KAAK,IAAI,CAAC;QACvC,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;QACtC,qBAAqB,CAAC,EAAE,MAAM,MAAM,GAAG,SAAS,CAAC;KAClD;IAaD,OAAO,CAAC,UAAU;IASlB,UAAU,IAAI,OAAO;IAIrB;;;;OAIG;IACG,cAAc,IAAI,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC;IAQxD,OAAO,CAAC,cAAc;IAoBtB,OAAO,CAAC,WAAW;IAMnB;;;;;OAKG;IACH,OAAO,CAAC,wBAAwB;IAe1B,OAAO,CACX,WAAW,EAAE,MAAM,EACnB,OAAO,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,WAAW,CAAA;KAAE,GACjC,OAAO,CAAC;QACT,IAAI,EAAE,MAAM,CAAC;QACb,KAAK,CAAC,EAAE;YAAE,WAAW,EAAE,MAAM,CAAC;YAAC,YAAY,EAAE,MAAM,CAAA;SAAE,CAAC;QACtD,QAAQ,CAAC,EAAE,OAAO,CAAC;KACpB,CAAC;CAwRH"}
@@ -107,6 +107,8 @@ export class CodingAgent {
107
107
  verbose;
108
108
  onProgress;
109
109
  onUiUpdate;
110
+ onVerbose;
111
+ getSystemPromptSuffix;
110
112
  /**
111
113
  * Tracks symbol names the user/agent has been working with.
112
114
  * Persists across turns so the code map stays focused on the
@@ -129,6 +131,17 @@ export class CodingAgent {
129
131
  this.verbose = params.verbose ?? false;
130
132
  this.onProgress = params.onProgress;
131
133
  this.onUiUpdate = params.onUiUpdate;
134
+ this.onVerbose = params.onVerbose;
135
+ this.getSystemPromptSuffix = params.getSystemPromptSuffix;
136
+ }
137
+ verboseLog(...args) {
138
+ const msg = args.map((a) => typeof a === "string" ? a : JSON.stringify(a, null, 2)).join(" ");
139
+ if (this.onVerbose) {
140
+ this.onVerbose(msg);
141
+ }
142
+ else {
143
+ console.error(msg);
144
+ }
132
145
  }
133
146
  getSession() {
134
147
  return this.session;
@@ -232,15 +245,17 @@ export class CodingAgent {
232
245
  // Rebuild system prompt each step with the latest focus set,
233
246
  // so the code map dynamically adapts as the agent explores symbols.
234
247
  const codeMapResult = this.getCodeMap?.(this.getFocusSet());
235
- const systemPrompt = buildSystemPrompt(this.config, toolSchemas, codeMapResult);
248
+ const basePrompt = buildSystemPrompt(this.config, toolSchemas, codeMapResult);
249
+ const suffix = this.getSystemPromptSuffix?.();
250
+ const systemPrompt = suffix ? basePrompt + "\n\n" + suffix : basePrompt;
236
251
  const messages = this.session.getMessages();
237
252
  if (this.verbose) {
238
- console.error(`\n${VERBOSE_SEP}`);
239
- console.error(`[verbose] Request (step ${step})`);
240
- console.error(`${VERBOSE_SEP}`);
241
- console.error("\n[System Prompt]\n", systemPrompt);
242
- console.error("\n[Messages]\n", JSON.stringify(messages, null, 2));
243
- console.error(VERBOSE_SEP);
253
+ this.verboseLog(`\n${VERBOSE_SEP}`);
254
+ this.verboseLog(`[verbose] Request (step ${step})`);
255
+ this.verboseLog(VERBOSE_SEP);
256
+ this.verboseLog("\n[System Prompt]\n", systemPrompt);
257
+ this.verboseLog("\n[Messages]\n", JSON.stringify(messages, null, 2));
258
+ this.verboseLog(VERBOSE_SEP);
244
259
  }
245
260
  const response = await this.modelClient.chat({
246
261
  model: this.config.model,
@@ -260,16 +275,16 @@ export class CodingAgent {
260
275
  totalInputTokens += response.usage.inputTokens;
261
276
  totalOutputTokens += response.usage.outputTokens;
262
277
  if (this.verbose) {
263
- console.error(`\n${VERBOSE_SEP}`);
264
- console.error("[verbose] Response");
265
- console.error(`${VERBOSE_SEP}`);
266
- console.error("Text:", response.text);
267
- console.error("Tool calls:", response.toolCalls.length);
278
+ this.verboseLog(`\n${VERBOSE_SEP}`);
279
+ this.verboseLog("[verbose] Response");
280
+ this.verboseLog(VERBOSE_SEP);
281
+ this.verboseLog("Text:", response.text);
282
+ this.verboseLog("Tool calls:", response.toolCalls.length);
268
283
  if (response.toolCalls.length > 0) {
269
- console.error("Tools:", response.toolCalls.map((t) => `${t.name}(${JSON.stringify(t.input)})`).join(", "));
284
+ this.verboseLog("Tools:", response.toolCalls.map((t) => `${t.name}(${JSON.stringify(t.input)})`).join(", "));
270
285
  }
271
- console.error("Usage:", response.usage);
272
- console.error(VERBOSE_SEP);
286
+ this.verboseLog("Usage:", response.usage);
287
+ this.verboseLog(VERBOSE_SEP);
273
288
  }
274
289
  if (response.toolCalls.length === 0) {
275
290
  const finalText = response.text.length > 0
@@ -345,9 +360,9 @@ export class CodingAgent {
345
360
  });
346
361
  }
347
362
  if (this.verbose) {
348
- console.error(`\n${VERBOSE_SEP}`);
349
- console.error(`[verbose] Tool: ${toolCall.name}`);
350
- console.error("Arguments:", JSON.stringify(toolCall.input, null, 2));
363
+ this.verboseLog(`\n${VERBOSE_SEP}`);
364
+ this.verboseLog(`[verbose] Tool: ${toolCall.name}`);
365
+ this.verboseLog("Arguments:", JSON.stringify(toolCall.input, null, 2));
351
366
  }
352
367
  let toolResult;
353
368
  const toolStartMs = Date.now();
@@ -388,8 +403,8 @@ export class CodingAgent {
388
403
  });
389
404
  }
390
405
  if (this.verbose) {
391
- console.error("Output:", toolResult);
392
- console.error(VERBOSE_SEP);
406
+ this.verboseLog("Output:", toolResult);
407
+ this.verboseLog(VERBOSE_SEP);
393
408
  }
394
409
  this.session.addMessage({
395
410
  role: "tool",