@sean.holung/minicode 0.3.7 → 0.3.9
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/agent/config.js +25 -0
- package/dist/src/model-utils.js +18 -1
- package/dist/src/serve/agent-bridge.js +89 -15
- package/dist/src/serve/server.js +151 -3
- package/dist/src/session/session-store.js +29 -1
- package/dist/src/web/app.js +691 -105
- package/dist/src/web/index.html +117 -9
- package/dist/src/web/style.css +198 -10
- package/dist/tests/agent.test.js +16 -0
- package/dist/tests/config-integration.test.js +91 -1
- package/dist/tests/context-indicator.test.js +9 -0
- package/dist/tests/file-tools.test.js +12 -0
- package/dist/tests/graph-onboarding.test.js +8 -0
- package/dist/tests/model-client-openai.test.js +41 -0
- package/dist/tests/model-dropdown-ui.test.js +23 -0
- package/dist/tests/model-utils.test.js +26 -1
- package/dist/tests/serve.integration.test.js +194 -0
- package/dist/tests/session-store.test.js +32 -1
- package/dist/tests/session-ui.test.js +6 -0
- package/dist/tests/settings-ui.test.js +11 -0
- package/dist/tests/setup-overlay-state.test.js +49 -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 +10 -1
- package/node_modules/@minicode/agent-sdk/dist/src/agent/agent.js.map +1 -1
- package/node_modules/@minicode/agent-sdk/dist/src/model/client.d.ts.map +1 -1
- package/node_modules/@minicode/agent-sdk/dist/src/model/client.js +21 -0
- package/node_modules/@minicode/agent-sdk/dist/src/model/client.js.map +1 -1
- package/node_modules/@minicode/agent-sdk/dist/src/tools/run-command.d.ts.map +1 -1
- package/node_modules/@minicode/agent-sdk/dist/src/tools/run-command.js +60 -6
- package/node_modules/@minicode/agent-sdk/dist/src/tools/run-command.js.map +1 -1
- package/node_modules/@minicode/agent-sdk/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
|
@@ -24,6 +24,7 @@ test('built HTML contains #cy graph container', () => {
|
|
|
24
24
|
assert.ok(html.includes('id="cy"'), 'HTML should contain the #cy graph container');
|
|
25
25
|
assert.ok(html.includes('id="graph-pane"'), 'HTML should contain the #graph-pane wrapper');
|
|
26
26
|
assert.ok(html.includes('Search symbols or files...'), 'HTML should expose mixed symbol/file search');
|
|
27
|
+
assert.ok(html.includes('id="graph-refresh"'), 'HTML should expose a graph refresh button');
|
|
27
28
|
assert.ok(html.includes('id="file-preview-modal"'), 'HTML should contain the file preview modal shell');
|
|
28
29
|
assert.ok(html.includes('id="file-preview-code"'), 'HTML should contain the file preview code surface');
|
|
29
30
|
});
|
|
@@ -67,6 +68,13 @@ test('built JS supports file search results and file-centered neighborhood rende
|
|
|
67
68
|
assert.ok(js.includes('/api/file-source?path='), 'JS should fetch full file contents for the preview modal');
|
|
68
69
|
assert.ok(js.includes('detail-file-link'), 'JS should wire the symbol detail filename into the preview modal');
|
|
69
70
|
});
|
|
71
|
+
test('built JS refreshes graph data manually and after mutating tool calls', () => {
|
|
72
|
+
const js = readFileSync(join(distWeb, 'app.js'), 'utf-8');
|
|
73
|
+
assert.ok(js.includes('/api/index/refresh'), 'manual graph refresh should call the backend index refresh endpoint');
|
|
74
|
+
assert.ok(js.includes('refreshGraphData'), 'JS should define a graph data refresh helper');
|
|
75
|
+
assert.ok(js.includes('scheduleGraphDataRefresh'), 'JS should debounce automatic graph refreshes after tool calls');
|
|
76
|
+
assert.ok(js.includes('"write_file"') && js.includes('"edit_file"') && js.includes('"run_command"'), 'mutating tools should trigger a graph data refresh so new symbols appear in search');
|
|
77
|
+
});
|
|
70
78
|
test('built CSS contains file preview modal sizing and link styling', () => {
|
|
71
79
|
const css = readFileSync(join(distWeb, 'style.css'), 'utf-8');
|
|
72
80
|
assert.ok(css.includes('.modal-panel-file-preview'), 'CSS should contain the file preview modal panel class');
|
|
@@ -102,6 +102,47 @@ test("openai-compatible client sends correct app URL in HTTP-Referer header", as
|
|
|
102
102
|
assert.equal(capturedHeaders["HTTP-Referer"], "https://minicode.seanholung.com", "HTTP-Referer should point to minicode.seanholung.com");
|
|
103
103
|
assert.equal(capturedHeaders["X-Title"], "minicode");
|
|
104
104
|
});
|
|
105
|
+
test("openai-compatible client repairs missing tool results before sending", async () => {
|
|
106
|
+
let capturedBody = "";
|
|
107
|
+
const fetchImpl = async (_input, init) => {
|
|
108
|
+
capturedBody = String(init?.body ?? "");
|
|
109
|
+
return new Response(JSON.stringify({
|
|
110
|
+
choices: [
|
|
111
|
+
{
|
|
112
|
+
finish_reason: "stop",
|
|
113
|
+
message: { content: "ok", tool_calls: [] },
|
|
114
|
+
},
|
|
115
|
+
],
|
|
116
|
+
usage: { prompt_tokens: 5, completion_tokens: 3 },
|
|
117
|
+
}), { status: 200, headers: { "content-type": "application/json" } });
|
|
118
|
+
};
|
|
119
|
+
const client = new OpenAICompatibleModelClient({
|
|
120
|
+
baseUrl: "http://localhost:1234/v1",
|
|
121
|
+
fetchImpl,
|
|
122
|
+
});
|
|
123
|
+
await client.chat({
|
|
124
|
+
model: "test-model",
|
|
125
|
+
system: "sys",
|
|
126
|
+
messages: [
|
|
127
|
+
{ role: "user", content: "start" },
|
|
128
|
+
{
|
|
129
|
+
role: "assistant",
|
|
130
|
+
content: "checking",
|
|
131
|
+
toolCalls: [{ id: "call-missing", name: "read_file", input: { path: "src/index.ts" } }],
|
|
132
|
+
},
|
|
133
|
+
{ role: "user", content: "continue" },
|
|
134
|
+
],
|
|
135
|
+
tools: [],
|
|
136
|
+
maxTokens: 64,
|
|
137
|
+
});
|
|
138
|
+
const parsedBody = JSON.parse(capturedBody);
|
|
139
|
+
const messages = parsedBody.messages;
|
|
140
|
+
assert.equal(messages[2]?.role, "assistant");
|
|
141
|
+
assert.equal(messages[3]?.role, "tool");
|
|
142
|
+
assert.equal(messages[3]?.tool_call_id, "call-missing");
|
|
143
|
+
assert.match(String(messages[3]?.content), /Tool result unavailable/);
|
|
144
|
+
assert.equal(messages[4]?.role, "user");
|
|
145
|
+
});
|
|
105
146
|
test("createModelClient returns openai-compatible client", () => {
|
|
106
147
|
const config = {
|
|
107
148
|
...createTestAgentConfig("/tmp"),
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { test } from "node:test";
|
|
5
|
+
const distWeb = join(import.meta.dirname, "..", "dist", "src", "web");
|
|
6
|
+
test("built HTML contains model search input", () => {
|
|
7
|
+
const html = readFileSync(join(distWeb, "index.html"), "utf8");
|
|
8
|
+
assert.ok(html.includes('id="model-search"'), "HTML should expose a dedicated model search input");
|
|
9
|
+
assert.ok(html.includes('placeholder="Search models..."'), "HTML should prompt users to search models");
|
|
10
|
+
});
|
|
11
|
+
test("built CSS contains searchable model dropdown styling", () => {
|
|
12
|
+
const css = readFileSync(join(distWeb, "style.css"), "utf8");
|
|
13
|
+
assert.ok(css.includes("#model-search"), "CSS should style the model search input");
|
|
14
|
+
assert.ok(css.includes(".model-item-body"), "CSS should support stacked model result content");
|
|
15
|
+
assert.ok(css.includes(".model-item-badge"), "CSS should style the active model badge");
|
|
16
|
+
});
|
|
17
|
+
test("built JS contains searchable model dropdown behavior", () => {
|
|
18
|
+
const js = readFileSync(join(distWeb, "app.js"), "utf8");
|
|
19
|
+
assert.ok(js.includes("filterModelsByQuery"), "JS should filter models by the dropdown query");
|
|
20
|
+
assert.ok(js.includes("focusModelSearchInput"), "JS should focus the search field when opening the dropdown");
|
|
21
|
+
assert.ok(js.includes("No matching models"), "JS should render an empty state for unmatched queries");
|
|
22
|
+
assert.ok(js.includes('modelSearchInput.addEventListener("input"'), "JS should update results as the user types");
|
|
23
|
+
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import { test } from "node:test";
|
|
3
|
-
import { sortModelsAlphabetically } from "../src/model-utils.js";
|
|
3
|
+
import { filterModelsByQuery, getModelDisplayName, sortModelsAlphabetically } from "../src/model-utils.js";
|
|
4
4
|
test("sortModelsAlphabetically sorts by display name without mutating input", () => {
|
|
5
5
|
const models = [
|
|
6
6
|
{ id: "zeta-2", name: "Zeta 2" },
|
|
@@ -20,3 +20,28 @@ test("sortModelsAlphabetically uses id as a stable tiebreaker", () => {
|
|
|
20
20
|
const sorted = sortModelsAlphabetically(models);
|
|
21
21
|
assert.deepEqual(sorted.map((model) => model.id), ["gpt-4.1-a", "gpt-4.1-b"]);
|
|
22
22
|
});
|
|
23
|
+
test("getModelDisplayName falls back to id and trims whitespace", () => {
|
|
24
|
+
assert.equal(getModelDisplayName({ id: "google/gemini-2.5-flash-preview", name: " Gemini 2.5 Flash " }), "Gemini 2.5 Flash");
|
|
25
|
+
assert.equal(getModelDisplayName({ id: "qwen/qwen3-coder" }), "qwen/qwen3-coder");
|
|
26
|
+
});
|
|
27
|
+
test("filterModelsByQuery matches on display name and id without mutating order", () => {
|
|
28
|
+
const models = [
|
|
29
|
+
{ id: "z-ai/glm-4.5-air", name: "GLM 4.5 Air" },
|
|
30
|
+
{ id: "google/gemini-2.5-flash-preview", name: "Gemini 2.5 Flash" },
|
|
31
|
+
{ id: "openai/gpt-4.1-mini", name: "GPT-4.1 Mini" },
|
|
32
|
+
];
|
|
33
|
+
const byName = filterModelsByQuery(models, "gemini flash");
|
|
34
|
+
const byId = filterModelsByQuery(models, "glm-4.5");
|
|
35
|
+
const blank = filterModelsByQuery(models, " ");
|
|
36
|
+
assert.deepEqual(byName.map((model) => model.id), ["google/gemini-2.5-flash-preview"]);
|
|
37
|
+
assert.deepEqual(byId.map((model) => model.id), ["z-ai/glm-4.5-air"]);
|
|
38
|
+
assert.deepEqual(blank.map((model) => model.id), models.map((model) => model.id));
|
|
39
|
+
});
|
|
40
|
+
test("filterModelsByQuery returns all tokens match across name and id", () => {
|
|
41
|
+
const models = [
|
|
42
|
+
{ id: "google/gemini-2.5-flash-preview", name: "Gemini Flash Preview" },
|
|
43
|
+
{ id: "google/gemini-2.5-pro-preview", name: "Gemini Pro Preview" },
|
|
44
|
+
];
|
|
45
|
+
const filtered = filterModelsByQuery(models, "google flash");
|
|
46
|
+
assert.deepEqual(filtered.map((model) => model.id), ["google/gemini-2.5-flash-preview"]);
|
|
47
|
+
});
|
|
@@ -7,6 +7,7 @@ import path from "node:path";
|
|
|
7
7
|
import { createRequestHandler, shutdownServe } from "../src/serve/server.js";
|
|
8
8
|
import { AgentBridge } from "../src/serve/agent-bridge.js";
|
|
9
9
|
import { Session } from "@minicode/agent-sdk";
|
|
10
|
+
import { DuplicateSessionLabelError } from "../src/session/session-store.js";
|
|
10
11
|
import { createTestAgentConfig } from "./test-utils.js";
|
|
11
12
|
/**
|
|
12
13
|
* Lightweight AgentBridge subclass for testing.
|
|
@@ -20,6 +21,10 @@ class MockBridge extends AgentBridge {
|
|
|
20
21
|
turnHistory = [];
|
|
21
22
|
openRouterKey;
|
|
22
23
|
openRouterSessionActive = false;
|
|
24
|
+
openAiCompatibleApiKey;
|
|
25
|
+
openAiCompatibleSessionActive = false;
|
|
26
|
+
refreshIndexCount = 0;
|
|
27
|
+
deletedSessionIds = [];
|
|
23
28
|
constructor() {
|
|
24
29
|
super(() => { }, false);
|
|
25
30
|
}
|
|
@@ -85,16 +90,39 @@ class MockBridge extends AgentBridge {
|
|
|
85
90
|
}
|
|
86
91
|
return { session, label };
|
|
87
92
|
}
|
|
93
|
+
async deleteSess(sessionId) {
|
|
94
|
+
if (sessionId !== "sess-1") {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
this.deletedSessionIds.push(sessionId);
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
88
100
|
getCurrentSessionId() {
|
|
89
101
|
return this._currentSessionId;
|
|
90
102
|
}
|
|
91
103
|
connectOpenRouter(apiKey) {
|
|
92
104
|
this.openRouterKey = apiKey;
|
|
93
105
|
this.openRouterSessionActive = true;
|
|
106
|
+
this.openAiCompatibleApiKey = undefined;
|
|
107
|
+
this.openAiCompatibleSessionActive = false;
|
|
94
108
|
this._config.modelProvider = "openai-compatible";
|
|
95
109
|
this._config.openAiBaseUrl = "https://openrouter.ai/api/v1";
|
|
96
110
|
this._config.openAiApiKey = apiKey;
|
|
97
111
|
}
|
|
112
|
+
connectOpenAiCompatible(baseUrl, apiKey) {
|
|
113
|
+
this.openAiCompatibleSessionActive = true;
|
|
114
|
+
this.openRouterKey = undefined;
|
|
115
|
+
this.openRouterSessionActive = false;
|
|
116
|
+
this.openAiCompatibleApiKey = apiKey?.trim() || undefined;
|
|
117
|
+
this._config.modelProvider = "openai-compatible";
|
|
118
|
+
this._config.openAiBaseUrl = baseUrl.trim().replace(/\/+$/, "");
|
|
119
|
+
if (this.openAiCompatibleApiKey) {
|
|
120
|
+
this._config.openAiApiKey = this.openAiCompatibleApiKey;
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
delete this._config.openAiApiKey;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
98
126
|
disconnectOpenRouter() {
|
|
99
127
|
if (!this.openRouterSessionActive) {
|
|
100
128
|
return false;
|
|
@@ -110,6 +138,21 @@ class MockBridge extends AgentBridge {
|
|
|
110
138
|
isOpenRouterSessionConnected() {
|
|
111
139
|
return this.openRouterSessionActive;
|
|
112
140
|
}
|
|
141
|
+
disconnectOpenAiCompatible() {
|
|
142
|
+
if (!this.openAiCompatibleSessionActive) {
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
this.openAiCompatibleSessionActive = false;
|
|
146
|
+
this.openAiCompatibleApiKey = undefined;
|
|
147
|
+
this._config.modelProvider = this._baseConfig.modelProvider;
|
|
148
|
+
this._config.model = this._baseConfig.model;
|
|
149
|
+
this._config.openAiBaseUrl = this._baseConfig.openAiBaseUrl;
|
|
150
|
+
delete this._config.openAiApiKey;
|
|
151
|
+
return true;
|
|
152
|
+
}
|
|
153
|
+
isOpenAiCompatibleSessionConnected() {
|
|
154
|
+
return this.openAiCompatibleSessionActive;
|
|
155
|
+
}
|
|
113
156
|
switchModel(modelId) {
|
|
114
157
|
this._config.model = modelId;
|
|
115
158
|
}
|
|
@@ -119,6 +162,10 @@ class MockBridge extends AgentBridge {
|
|
|
119
162
|
hasIndex() {
|
|
120
163
|
return true;
|
|
121
164
|
}
|
|
165
|
+
async refreshIndex() {
|
|
166
|
+
this.refreshIndexCount += 1;
|
|
167
|
+
return true;
|
|
168
|
+
}
|
|
122
169
|
getSymbols() {
|
|
123
170
|
return [
|
|
124
171
|
{ name: "foo", qualifiedName: "foo", kind: "function", filePath: "src/foo.ts", startLine: 1, endLine: 5, signature: "function foo(): void", exported: true },
|
|
@@ -351,6 +398,11 @@ class EmptySessionsBridge extends MockBridge {
|
|
|
351
398
|
};
|
|
352
399
|
}
|
|
353
400
|
}
|
|
401
|
+
class DuplicateSessionLabelBridge extends MockBridge {
|
|
402
|
+
async saveSess() {
|
|
403
|
+
throw new DuplicateSessionLabelError("test-session", "sess-1");
|
|
404
|
+
}
|
|
405
|
+
}
|
|
354
406
|
// ── Test harness ──
|
|
355
407
|
let activeServer;
|
|
356
408
|
function startTestServer(bridge, options = {}) {
|
|
@@ -436,6 +488,18 @@ test("POST /api/sessions/save saves a session", async () => {
|
|
|
436
488
|
const body = (await res.json());
|
|
437
489
|
assert.equal(body.label, "my-save");
|
|
438
490
|
});
|
|
491
|
+
test("POST /api/sessions/save rejects duplicate session labels", async () => {
|
|
492
|
+
const bridge = new DuplicateSessionLabelBridge();
|
|
493
|
+
const base = await startTestServer(bridge);
|
|
494
|
+
const res = await fetch(`${base}/api/sessions/save`, {
|
|
495
|
+
method: "POST",
|
|
496
|
+
headers: { "Content-Type": "application/json" },
|
|
497
|
+
body: JSON.stringify({ label: "test-session" }),
|
|
498
|
+
});
|
|
499
|
+
assert.equal(res.status, 409);
|
|
500
|
+
const body = (await res.json());
|
|
501
|
+
assert.match(body.error, /already exists/);
|
|
502
|
+
});
|
|
439
503
|
test("session APIs support the first save when no sessions exist yet", async () => {
|
|
440
504
|
const bridge = new EmptySessionsBridge();
|
|
441
505
|
const base = await startTestServer(bridge);
|
|
@@ -480,6 +544,29 @@ test("POST /api/sessions/load returns success for known session", async () => {
|
|
|
480
544
|
assert.equal(body.messages[9]?.content, "message-11");
|
|
481
545
|
assert.ok(body.messages.every((message) => !message.content.startsWith("[Conversation Summary")));
|
|
482
546
|
});
|
|
547
|
+
test("DELETE /api/sessions/:id deletes a saved session", async () => {
|
|
548
|
+
const bridge = new MockBridge();
|
|
549
|
+
const base = await startTestServer(bridge);
|
|
550
|
+
const res = await fetch(`${base}/api/sessions/sess-1`, {
|
|
551
|
+
method: "DELETE",
|
|
552
|
+
});
|
|
553
|
+
assert.equal(res.status, 200);
|
|
554
|
+
const body = (await res.json());
|
|
555
|
+
assert.equal(body.ok, true);
|
|
556
|
+
assert.equal(body.deleted, true);
|
|
557
|
+
assert.equal(body.id, "sess-1");
|
|
558
|
+
assert.deepEqual(bridge.deletedSessionIds, ["sess-1"]);
|
|
559
|
+
});
|
|
560
|
+
test("DELETE /api/sessions/:id returns 404 for unknown session", async () => {
|
|
561
|
+
const bridge = new MockBridge();
|
|
562
|
+
const base = await startTestServer(bridge);
|
|
563
|
+
const res = await fetch(`${base}/api/sessions/missing`, {
|
|
564
|
+
method: "DELETE",
|
|
565
|
+
});
|
|
566
|
+
assert.equal(res.status, 404);
|
|
567
|
+
const body = (await res.json());
|
|
568
|
+
assert.match(body.error, /Session not found/);
|
|
569
|
+
});
|
|
483
570
|
test("POST /api/chat returns agent response", async () => {
|
|
484
571
|
const bridge = new MockBridge();
|
|
485
572
|
const base = await startTestServer(bridge);
|
|
@@ -653,6 +740,90 @@ test("GET /api/status exposes OpenRouter session state and base URL", async () =
|
|
|
653
740
|
assert.equal(body.baseUrl, "https://openrouter.ai/api/v1");
|
|
654
741
|
assert.equal(body.sessionOpenRouterConnected, true);
|
|
655
742
|
});
|
|
743
|
+
test("POST /api/openai-compatible/connect stores a session-only endpoint", async () => {
|
|
744
|
+
const bridge = new MockBridge();
|
|
745
|
+
const base = await startTestServer(bridge);
|
|
746
|
+
const res = await fetch(`${base}/api/openai-compatible/connect`, {
|
|
747
|
+
method: "POST",
|
|
748
|
+
headers: { "Content-Type": "application/json" },
|
|
749
|
+
body: JSON.stringify({ baseUrl: "http://localhost:1234/v1/", apiKey: "local-key" }),
|
|
750
|
+
});
|
|
751
|
+
assert.equal(res.status, 200);
|
|
752
|
+
const body = await res.json();
|
|
753
|
+
assert.equal(body.ok, true);
|
|
754
|
+
assert.equal(body.sessionOnly, true);
|
|
755
|
+
assert.equal(body.persistedToEnv, false);
|
|
756
|
+
assert.equal(body.persistedEnvPath, null);
|
|
757
|
+
assert.equal(body.persistWarning, null);
|
|
758
|
+
assert.equal(body.baseUrl, "http://localhost:1234/v1");
|
|
759
|
+
assert.equal(body.needsSetup, false);
|
|
760
|
+
assert.deepEqual(body.missing, []);
|
|
761
|
+
assert.equal(body.message, "The OpenAI-compatible provider connected for this serve session.");
|
|
762
|
+
assert.equal(bridge.getConfig().modelProvider, "openai-compatible");
|
|
763
|
+
assert.equal(bridge.getConfig().openAiBaseUrl, "http://localhost:1234/v1");
|
|
764
|
+
assert.equal(bridge.getConfig().openAiApiKey, "local-key");
|
|
765
|
+
assert.equal(bridge.isOpenAiCompatibleSessionConnected(), true);
|
|
766
|
+
});
|
|
767
|
+
test("POST /api/openai-compatible/connect can persist the endpoint to ~/.minicode/.env", async () => {
|
|
768
|
+
const bridge = new MockBridge();
|
|
769
|
+
const minicodeHome = mkdtempSync(path.join(os.tmpdir(), "minicode-openai-compatible-home-"));
|
|
770
|
+
const base = await startTestServer(bridge, { minicodeHome });
|
|
771
|
+
try {
|
|
772
|
+
const res = await fetch(`${base}/api/openai-compatible/connect`, {
|
|
773
|
+
method: "POST",
|
|
774
|
+
headers: { "Content-Type": "application/json" },
|
|
775
|
+
body: JSON.stringify({
|
|
776
|
+
baseUrl: "http://127.0.0.1:1234/v1",
|
|
777
|
+
apiKey: "",
|
|
778
|
+
persistToEnv: true,
|
|
779
|
+
}),
|
|
780
|
+
});
|
|
781
|
+
assert.equal(res.status, 200);
|
|
782
|
+
const body = await res.json();
|
|
783
|
+
assert.equal(body.persistedToEnv, true);
|
|
784
|
+
assert.equal(body.persistWarning, null);
|
|
785
|
+
assert.equal(body.persistedEnvPath, path.join(minicodeHome, ".env"));
|
|
786
|
+
assert.match(body.message, /saved to ~\/\.minicode\/\.env/);
|
|
787
|
+
const envContents = readFileSync(path.join(minicodeHome, ".env"), "utf8");
|
|
788
|
+
assert.match(envContents, /^MODEL_PROVIDER=openai-compatible$/m);
|
|
789
|
+
assert.match(envContents, /^OPENAI_BASE_URL=http:\/\/127\.0\.0\.1:1234\/v1$/m);
|
|
790
|
+
assert.doesNotMatch(envContents, /^OPENAI_API_KEY=/m);
|
|
791
|
+
}
|
|
792
|
+
finally {
|
|
793
|
+
rmSync(minicodeHome, { recursive: true, force: true });
|
|
794
|
+
}
|
|
795
|
+
});
|
|
796
|
+
test("POST /api/openai-compatible/disconnect removes the session-only endpoint", async () => {
|
|
797
|
+
const bridge = new MockBridge();
|
|
798
|
+
bridge.connectOpenAiCompatible("http://localhost:1234/v1", "local-key");
|
|
799
|
+
const base = await startTestServer(bridge);
|
|
800
|
+
const res = await fetch(`${base}/api/openai-compatible/disconnect`, {
|
|
801
|
+
method: "POST",
|
|
802
|
+
headers: { "Content-Type": "application/json" },
|
|
803
|
+
});
|
|
804
|
+
assert.equal(res.status, 200);
|
|
805
|
+
const body = await res.json();
|
|
806
|
+
assert.equal(body.ok, true);
|
|
807
|
+
assert.equal(body.disconnected, true);
|
|
808
|
+
assert.equal(body.sessionOnly, true);
|
|
809
|
+
assert.equal(body.provider, "anthropic");
|
|
810
|
+
assert.equal(body.baseUrl, "http://localhost:1234/v1");
|
|
811
|
+
assert.equal(body.message, "Removed the session-only OpenAI-compatible connection and restored your original provider settings.");
|
|
812
|
+
assert.equal(bridge.isOpenAiCompatibleSessionConnected(), false);
|
|
813
|
+
assert.equal(bridge.getConfig().modelProvider, "anthropic");
|
|
814
|
+
assert.equal(bridge.getConfig().openAiApiKey, undefined);
|
|
815
|
+
});
|
|
816
|
+
test("GET /api/status exposes OpenAI-compatible session state and base URL", async () => {
|
|
817
|
+
const bridge = new MockBridge();
|
|
818
|
+
bridge.connectOpenAiCompatible("http://localhost:1234/v1");
|
|
819
|
+
const base = await startTestServer(bridge);
|
|
820
|
+
const res = await fetch(`${base}/api/status`);
|
|
821
|
+
assert.equal(res.status, 200);
|
|
822
|
+
const body = await res.json();
|
|
823
|
+
assert.equal(body.provider, "openai-compatible");
|
|
824
|
+
assert.equal(body.baseUrl, "http://localhost:1234/v1");
|
|
825
|
+
assert.equal(body.sessionOpenAiCompatibleConnected, true);
|
|
826
|
+
});
|
|
656
827
|
test("POST /api/openrouter/connect returns 400 when code is missing", async () => {
|
|
657
828
|
const bridge = new MockBridge();
|
|
658
829
|
const base = await startTestServer(bridge);
|
|
@@ -688,6 +859,18 @@ test("POST /api/openrouter/connect surfaces exchange failures", async () => {
|
|
|
688
859
|
globalThis.fetch = originalFetch;
|
|
689
860
|
}
|
|
690
861
|
});
|
|
862
|
+
test("POST /api/openai-compatible/connect rejects an invalid endpoint", async () => {
|
|
863
|
+
const bridge = new MockBridge();
|
|
864
|
+
const base = await startTestServer(bridge);
|
|
865
|
+
const res = await fetch(`${base}/api/openai-compatible/connect`, {
|
|
866
|
+
method: "POST",
|
|
867
|
+
headers: { "Content-Type": "application/json" },
|
|
868
|
+
body: JSON.stringify({ baseUrl: "localhost:1234/v1" }),
|
|
869
|
+
});
|
|
870
|
+
assert.equal(res.status, 400);
|
|
871
|
+
const body = await res.json();
|
|
872
|
+
assert.match(body.error, /valid absolute http\(s\) URL/);
|
|
873
|
+
});
|
|
691
874
|
// ── OpenAI-compatible API tests ──
|
|
692
875
|
test("GET /v1/models returns minicode-agent model", async () => {
|
|
693
876
|
const bridge = new MockBridge();
|
|
@@ -928,6 +1111,17 @@ test("GET /api/graph returns nodes and edges", async () => {
|
|
|
928
1111
|
assert.equal(body.edges[0].to, "Bar");
|
|
929
1112
|
assert.equal(body.edges[0].kind, "calls");
|
|
930
1113
|
});
|
|
1114
|
+
test("POST /api/index/refresh rebuilds the project index", async () => {
|
|
1115
|
+
const bridge = new MockBridge();
|
|
1116
|
+
const base = await startTestServer(bridge);
|
|
1117
|
+
const res = await fetch(`${base}/api/index/refresh`, { method: "POST" });
|
|
1118
|
+
assert.equal(res.status, 200);
|
|
1119
|
+
const body = (await res.json());
|
|
1120
|
+
assert.equal(body.ok, true);
|
|
1121
|
+
assert.equal(body.symbolCount, 2);
|
|
1122
|
+
assert.equal(body.edgeCount, 1);
|
|
1123
|
+
assert.equal(bridge.refreshIndexCount, 1);
|
|
1124
|
+
});
|
|
931
1125
|
test("GET /api/analysis returns deterministic structural findings", async () => {
|
|
932
1126
|
const bridge = new MockBridge();
|
|
933
1127
|
const base = await startTestServer(bridge);
|
|
@@ -4,7 +4,7 @@ import { mkdtemp, readdir, rm } from "node:fs/promises";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import os from "node:os";
|
|
6
6
|
import { Session } from "@minicode/agent-sdk";
|
|
7
|
-
import { listSessions, loadSession, loadSessionByLabel, saveSession, setSessionsDir, } from "../src/session/session-store.js";
|
|
7
|
+
import { deleteSession, DuplicateSessionLabelError, listSessions, loadSession, loadSessionByLabel, saveSession, setSessionsDir, } from "../src/session/session-store.js";
|
|
8
8
|
async function withTmpDir(fn) {
|
|
9
9
|
const dir = await mkdtemp(path.join(os.tmpdir(), "minicode-test-"));
|
|
10
10
|
setSessionsDir(dir);
|
|
@@ -98,6 +98,20 @@ test("loadSessionByLabel returns undefined for no match", async () => {
|
|
|
98
98
|
assert.equal(result, undefined);
|
|
99
99
|
});
|
|
100
100
|
});
|
|
101
|
+
test("saveSession rejects duplicate labels for different sessions", async () => {
|
|
102
|
+
await withTmpDir(async (dir) => {
|
|
103
|
+
const firstSession = new Session("first-id");
|
|
104
|
+
firstSession.addMessage({ role: "user", content: "first" });
|
|
105
|
+
await saveSession(firstSession, "My Label");
|
|
106
|
+
const secondSession = new Session("second-id");
|
|
107
|
+
secondSession.addMessage({ role: "user", content: "second" });
|
|
108
|
+
await assert.rejects(() => saveSession(secondSession, " my label "), (error) => error instanceof DuplicateSessionLabelError &&
|
|
109
|
+
error.label === "my label" &&
|
|
110
|
+
error.existingSessionId === "first-id");
|
|
111
|
+
const files = await readdir(dir);
|
|
112
|
+
assert.deepEqual(files, ["first-id.json"]);
|
|
113
|
+
});
|
|
114
|
+
});
|
|
101
115
|
test("saving same session twice overwrites the file", async () => {
|
|
102
116
|
await withTmpDir(async (dir) => {
|
|
103
117
|
const session = new Session("test-id");
|
|
@@ -113,3 +127,20 @@ test("saving same session twice overwrites the file", async () => {
|
|
|
113
127
|
assert.equal(result.session.getMessages().length, 2);
|
|
114
128
|
});
|
|
115
129
|
});
|
|
130
|
+
test("deleteSession removes the saved session file", async () => {
|
|
131
|
+
await withTmpDir(async (dir) => {
|
|
132
|
+
const session = new Session("test-id");
|
|
133
|
+
session.addMessage({ role: "user", content: "hello" });
|
|
134
|
+
await saveSession(session, "delete me");
|
|
135
|
+
const deleted = await deleteSession("test-id");
|
|
136
|
+
assert.equal(deleted, true);
|
|
137
|
+
const files = await readdir(dir);
|
|
138
|
+
assert.equal(files.length, 0);
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
test("deleteSession returns false for a missing session", async () => {
|
|
142
|
+
await withTmpDir(async () => {
|
|
143
|
+
const deleted = await deleteSession("missing-session");
|
|
144
|
+
assert.equal(deleted, false);
|
|
145
|
+
});
|
|
146
|
+
});
|
|
@@ -7,11 +7,14 @@ test("built HTML contains update action for the current saved session", () => {
|
|
|
7
7
|
const html = readFileSync(join(distWeb, "index.html"), "utf8");
|
|
8
8
|
assert.ok(html.includes('id="session-update-row"'), "HTML should contain the session update row");
|
|
9
9
|
assert.ok(html.includes('id="session-update-btn"'), "HTML should contain the session update button");
|
|
10
|
+
assert.ok(html.includes('id="session-autosave-toggle"'), "HTML should contain the auto-save sessions toggle");
|
|
10
11
|
});
|
|
11
12
|
test("built CSS contains active-session styling", () => {
|
|
12
13
|
const css = readFileSync(join(distWeb, "style.css"), "utf8");
|
|
13
14
|
assert.ok(css.includes(".session-item.active"), "CSS should style the active saved session row");
|
|
14
15
|
assert.ok(css.includes(".session-active-badge"), "CSS should style the active session badge");
|
|
16
|
+
assert.ok(css.includes(".session-delete-btn"), "CSS should style the session delete button");
|
|
17
|
+
assert.ok(css.includes(".session-autosave-row"), "CSS should style the auto-save toggle row");
|
|
15
18
|
});
|
|
16
19
|
test("built JS contains active saved session update logic", () => {
|
|
17
20
|
const js = readFileSync(join(distWeb, "app.js"), "utf8");
|
|
@@ -22,4 +25,7 @@ test("built JS contains active saved session update logic", () => {
|
|
|
22
25
|
assert.ok(js.includes('saveBtn.setAttribute("disabled", "true")'), "JS should disable saving while the first save is in flight");
|
|
23
26
|
assert.ok(js.includes("renderLoadedSessionMessages"), "JS should render session previews after load");
|
|
24
27
|
assert.ok(js.includes("body.messages"), "JS should read preview messages from the load session response");
|
|
28
|
+
assert.ok(js.includes("SESSION_AUTOSAVE_KEY"), "JS should persist the auto-save preference");
|
|
29
|
+
assert.ok(js.includes("window.confirm"), "JS should confirm before deleting a saved session");
|
|
30
|
+
assert.ok(js.includes('method: "DELETE"'), "JS should call the delete session API");
|
|
25
31
|
});
|
|
@@ -8,11 +8,15 @@ test("built HTML contains settings entry point and modal shell", () => {
|
|
|
8
8
|
assert.ok(html.includes('id="settings-btn"'), "HTML should contain the settings button");
|
|
9
9
|
assert.ok(html.includes('id="settings-modal"'), "HTML should contain the settings modal");
|
|
10
10
|
assert.ok(html.includes('id="connect-openrouter-btn"'), "HTML should contain the OpenRouter connect button");
|
|
11
|
+
assert.ok(html.includes('id="connect-openai-compatible-btn"'), "HTML should contain the OpenAI-compatible connect button");
|
|
11
12
|
assert.ok(html.includes('id="config-overlay-intro"'), "HTML should contain the setup overlay intro copy");
|
|
12
13
|
assert.ok(html.includes("Try minicode for free with OpenRouter"), "HTML should promote the free OpenRouter quick start");
|
|
13
14
|
assert.ok(html.includes('id="openrouter-connect-modal"'), "HTML should contain the OpenRouter consent modal");
|
|
15
|
+
assert.ok(html.includes('id="openai-compatible-connect-modal"'), "HTML should contain the OpenAI-compatible connect modal");
|
|
16
|
+
assert.ok(html.includes('id="openai-compatible-preset"'), "HTML should contain the OpenAI-compatible preset selector");
|
|
14
17
|
assert.ok(html.includes('id="openrouter-persist-checkbox"'), "HTML should contain the OpenRouter persistence checkbox");
|
|
15
18
|
assert.ok(html.includes('id="disconnect-openrouter-btn"'), "HTML should contain the OpenRouter disconnect button");
|
|
19
|
+
assert.ok(html.includes('id="disconnect-openai-compatible-btn"'), "HTML should contain the OpenAI-compatible disconnect button");
|
|
16
20
|
assert.ok(!html.includes('id="settings-scope"'), "HTML should no longer contain the settings scope selector");
|
|
17
21
|
assert.ok(html.includes('id="settings-save"'), "HTML should contain the settings save action");
|
|
18
22
|
});
|
|
@@ -24,9 +28,12 @@ test("built CSS contains modal and settings layout styles", () => {
|
|
|
24
28
|
assert.ok(css.includes(".settings-help-warning"), "CSS should contain warning styling for env overrides");
|
|
25
29
|
assert.ok(css.includes("align-items: flex-start;"), "CSS should top-align scrollable setup overlay content");
|
|
26
30
|
assert.ok(css.includes(".config-overlay-spotlight"), "CSS should style the OpenRouter quick-start spotlight");
|
|
31
|
+
assert.ok(css.includes(".config-overlay-shortcuts"), "CSS should lay out the setup quick-connect cards");
|
|
27
32
|
assert.ok(css.includes(".openrouter-connect-body"), "CSS should style the OpenRouter consent modal body");
|
|
33
|
+
assert.ok(css.includes(".provider-input"), "CSS should style provider connection form inputs");
|
|
28
34
|
assert.ok(css.includes(".config-connect-status.success"), "CSS should style OpenRouter connect success state");
|
|
29
35
|
assert.ok(css.includes(".settings-session-banner"), "CSS should style the OpenRouter session banner");
|
|
36
|
+
assert.ok(css.includes(".settings-provider-shortcuts"), "CSS should style provider shortcuts inside settings");
|
|
30
37
|
assert.ok(css.includes("body.modal-open"), "CSS should lock scroll while the settings modal is open");
|
|
31
38
|
});
|
|
32
39
|
test("built JS contains config loading and saving logic for settings", () => {
|
|
@@ -34,11 +41,15 @@ test("built JS contains config loading and saving logic for settings", () => {
|
|
|
34
41
|
assert.ok(js.includes("/api/config"), "JS should fetch the config API");
|
|
35
42
|
assert.ok(js.includes("/api/openrouter/connect"), "JS should call the OpenRouter connect API");
|
|
36
43
|
assert.ok(js.includes("/api/openrouter/disconnect"), "JS should call the OpenRouter disconnect API");
|
|
44
|
+
assert.ok(js.includes("/api/openai-compatible/connect"), "JS should call the OpenAI-compatible connect API");
|
|
45
|
+
assert.ok(js.includes("/api/openai-compatible/disconnect"), "JS should call the OpenAI-compatible disconnect API");
|
|
37
46
|
assert.ok(js.includes("persistToHomeEnv"), "JS should support persisting the selected model after OpenRouter setup");
|
|
38
47
|
assert.ok(js.includes("code_challenge_method"), "JS should generate an OpenRouter PKCE auth request");
|
|
39
48
|
assert.ok(js.includes("sessionStorage"), "JS should persist the PKCE verifier for the OAuth callback");
|
|
40
49
|
assert.ok(js.includes("minicode:openrouter:persist-to-env"), "JS should persist the optional OpenRouter env-write choice across OAuth");
|
|
41
50
|
assert.ok(js.includes("sessionOpenRouterConnected"), "JS should track session-only OpenRouter state");
|
|
51
|
+
assert.ok(js.includes("sessionOpenAiCompatibleConnected"), "JS should track session-only OpenAI-compatible state");
|
|
52
|
+
assert.ok(js.includes("OPENAI_COMPATIBLE_PRESETS"), "JS should include OpenAI-compatible endpoint presets");
|
|
42
53
|
assert.ok(js.includes("Save settings"), "JS should contain the settings save action text");
|
|
43
54
|
assert.ok(js.includes("settingsPayload"), "JS should track settings payload state");
|
|
44
55
|
assert.ok(js.includes("persistedValue"), "JS should wire persisted settings behavior");
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { test } from "node:test";
|
|
3
|
+
import { DEFAULT_SETUP_INTRO, deriveSetupOverlayState } from "../src/web/setup-overlay-state.js";
|
|
4
|
+
test("fresh install hides MODEL missing copy until a provider is configured", () => {
|
|
5
|
+
const state = deriveSetupOverlayState({
|
|
6
|
+
configuredProvider: null,
|
|
7
|
+
missing: ["MODEL is not set"],
|
|
8
|
+
});
|
|
9
|
+
assert.equal(state.introText, DEFAULT_SETUP_INTRO);
|
|
10
|
+
assert.equal(state.hideQuickConnects, false);
|
|
11
|
+
assert.equal(state.hideOpenRouterSpotlight, false);
|
|
12
|
+
assert.deepEqual(state.missingItems, []);
|
|
13
|
+
assert.equal(state.showModelSelectionHint, false);
|
|
14
|
+
assert.equal(state.modelSelectionNote, null);
|
|
15
|
+
});
|
|
16
|
+
test("configured OpenAI-compatible provider keeps model guidance visible", () => {
|
|
17
|
+
const state = deriveSetupOverlayState({
|
|
18
|
+
configuredProvider: "openai-compatible",
|
|
19
|
+
missing: ["MODEL is not set"],
|
|
20
|
+
});
|
|
21
|
+
assert.equal(state.introText, "An OpenAI-compatible provider is already configured. Select a model to continue:");
|
|
22
|
+
assert.equal(state.hideQuickConnects, true);
|
|
23
|
+
assert.equal(state.hideOpenRouterSpotlight, false);
|
|
24
|
+
assert.deepEqual(state.missingItems, ["MODEL is not set"]);
|
|
25
|
+
assert.equal(state.showModelSelectionHint, true);
|
|
26
|
+
assert.equal(state.modelSelectionNote, null);
|
|
27
|
+
});
|
|
28
|
+
test("configured OpenRouter provider keeps model guidance and hides spotlight", () => {
|
|
29
|
+
const state = deriveSetupOverlayState({
|
|
30
|
+
configuredProvider: "openrouter",
|
|
31
|
+
missing: ["MODEL is not set"],
|
|
32
|
+
});
|
|
33
|
+
assert.equal(state.introText, "OpenRouter is already configured. Select a model to continue:");
|
|
34
|
+
assert.equal(state.hideQuickConnects, true);
|
|
35
|
+
assert.equal(state.hideOpenRouterSpotlight, true);
|
|
36
|
+
assert.deepEqual(state.missingItems, ["MODEL is not set"]);
|
|
37
|
+
assert.equal(state.showModelSelectionHint, true);
|
|
38
|
+
assert.equal(state.modelSelectionNote, 'If you are on the OpenRouter free tier, search "free" in the model dropdown to find supported free models.');
|
|
39
|
+
});
|
|
40
|
+
test("non-model missing items still surface before provider selection", () => {
|
|
41
|
+
const state = deriveSetupOverlayState({
|
|
42
|
+
configuredProvider: null,
|
|
43
|
+
missing: ["SOME_OTHER_SETTING is not set"],
|
|
44
|
+
});
|
|
45
|
+
assert.equal(state.introText, DEFAULT_SETUP_INTRO);
|
|
46
|
+
assert.deepEqual(state.missingItems, ["SOME_OTHER_SETTING is not set"]);
|
|
47
|
+
assert.equal(state.showModelSelectionHint, false);
|
|
48
|
+
assert.equal(state.modelSelectionNote, null);
|
|
49
|
+
});
|
|
@@ -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":"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"}
|
|
@@ -343,7 +343,8 @@ export class CodingAgent {
|
|
|
343
343
|
content: thinkingContent,
|
|
344
344
|
toolCalls: response.toolCalls,
|
|
345
345
|
});
|
|
346
|
-
for (
|
|
346
|
+
for (let toolCallIndex = 0; toolCallIndex < response.toolCalls.length; toolCallIndex += 1) {
|
|
347
|
+
const toolCall = response.toolCalls[toolCallIndex];
|
|
347
348
|
const fingerprint = signatureForToolCall(toolCall);
|
|
348
349
|
recentToolCallFingerprints.push(fingerprint);
|
|
349
350
|
if (recentToolCallFingerprints.length >
|
|
@@ -353,6 +354,14 @@ export class CodingAgent {
|
|
|
353
354
|
const repeatedCalls = recentToolCallFingerprints.filter((value) => value === fingerprint).length;
|
|
354
355
|
if (repeatedCalls >= 3) {
|
|
355
356
|
const loopMessage = "Stopped due to repeated identical tool calls. Please refine the prompt or provide additional constraints.";
|
|
357
|
+
for (const skippedToolCall of response.toolCalls.slice(toolCallIndex)) {
|
|
358
|
+
this.session.addMessage({
|
|
359
|
+
role: "tool",
|
|
360
|
+
toolCallId: skippedToolCall.id,
|
|
361
|
+
toolName: skippedToolCall.name,
|
|
362
|
+
content: `Tool skipped: ${loopMessage}`,
|
|
363
|
+
});
|
|
364
|
+
}
|
|
356
365
|
this.session.addMessage({
|
|
357
366
|
role: "assistant",
|
|
358
367
|
content: loopMessage,
|