@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.
- package/dist/src/indexer/code-map.js +2 -1
- package/dist/src/indexer/plugins/typescript.js +63 -34
- 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/benchmark-index.test.js +152 -0
- package/dist/tests/indexer.test.js +62 -0
- package/dist/tests/serve.integration.test.js +225 -0
- package/node_modules/@minicode/agent-sdk/dist/src/agent/agent.d.ts +6 -1
- 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/src/index.d.ts +2 -1
- package/node_modules/@minicode/agent-sdk/dist/src/index.d.ts.map +1 -1
- package/node_modules/@minicode/agent-sdk/dist/src/index.js.map +1 -1
- package/node_modules/@minicode/agent-sdk/dist/src/indexer/types.d.ts +68 -0
- package/node_modules/@minicode/agent-sdk/dist/src/indexer/types.d.ts.map +1 -0
- package/node_modules/@minicode/agent-sdk/dist/src/indexer/types.js +2 -0
- package/node_modules/@minicode/agent-sdk/dist/src/indexer/types.js.map +1 -0
- package/node_modules/@minicode/agent-sdk/dist/src/prompt/system-prompt.d.ts +1 -5
- package/node_modules/@minicode/agent-sdk/dist/src/prompt/system-prompt.d.ts.map +1 -1
- package/node_modules/@minicode/agent-sdk/dist/src/prompt/system-prompt.js.map +1 -1
- package/node_modules/@minicode/agent-sdk/dist/tsconfig.tsbuildinfo +1 -1
- 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 "../
|
|
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":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"agent.d.ts","sourceRoot":"","sources":["../../../src/agent/agent.ts"],"names":[],"mappings":"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"}
|
|
@@ -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
|
|
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
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
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
|
-
|
|
284
|
+
this.verboseLog("Tools:", response.toolCalls.map((t) => `${t.name}(${JSON.stringify(t.input)})`).join(", "));
|
|
270
285
|
}
|
|
271
|
-
|
|
272
|
-
|
|
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
|
-
|
|
349
|
-
|
|
350
|
-
|
|
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
|
-
|
|
392
|
-
|
|
406
|
+
this.verboseLog("Output:", toolResult);
|
|
407
|
+
this.verboseLog(VERBOSE_SEP);
|
|
393
408
|
}
|
|
394
409
|
this.session.addMessage({
|
|
395
410
|
role: "tool",
|