mini-coder 0.5.12 → 0.5.14
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/BENCHMARK.md +15 -316
- package/PROGRESS.md +3 -3
- package/README.md +54 -21
- package/benchmark-baseline.sh +15 -0
- package/bun.lock +265 -90
- package/package.json +8 -7
- package/skills-lock.json +15 -0
- package/src/agent.ts +526 -13
- package/src/assistant-output.ts +73 -0
- package/src/cli.ts +2 -1
- package/src/delegation.ts +238 -0
- package/src/headless.ts +90 -44
- package/src/index.ts +267 -102
- package/src/input.ts +13 -1
- package/src/mcp.ts +609 -0
- package/src/prompt.ts +11 -13
- package/src/session-message.ts +57 -65
- package/src/session.ts +389 -42
- package/src/settings.ts +199 -7
- package/src/skills.ts +12 -3
- package/src/submit.ts +24 -2
- package/src/theme.ts +186 -3
- package/src/tool-common.ts +2 -0
- package/src/tool-delegate.ts +125 -0
- package/src/tool-shell.ts +190 -8
- package/src/tools.ts +335 -6
- package/src/ui/agent.ts +10 -0
- package/src/ui/commands.test.ts +525 -10
- package/src/ui/commands.ts +224 -8
- package/src/ui/conversation.test.ts +252 -27
- package/src/ui/conversation.ts +468 -390
- package/src/ui/help.ts +27 -11
- package/src/ui.ts +230 -75
- package/src/plugins.ts +0 -183
package/src/ui/commands.test.ts
CHANGED
|
@@ -3,14 +3,15 @@ import { mkdtempSync, rmSync } from "node:fs";
|
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import type { ContainerNode } from "@cel-tui/types";
|
|
6
|
-
import { registerFauxProvider } from "@mariozechner/pi-ai";
|
|
7
|
-
import type
|
|
6
|
+
import { registerFauxProvider, Type } from "@mariozechner/pi-ai";
|
|
7
|
+
import { type AppState, buildToolList } from "../index.ts";
|
|
8
8
|
import {
|
|
9
|
-
|
|
9
|
+
computeSessionContextTokens,
|
|
10
10
|
createSession,
|
|
11
|
+
loadMessages,
|
|
11
12
|
openDatabase,
|
|
12
13
|
} from "../session.ts";
|
|
13
|
-
import { loadSettings } from "../settings.ts";
|
|
14
|
+
import { loadSettings, saveSettings } from "../settings.ts";
|
|
14
15
|
import { DEFAULT_THEME } from "../theme.ts";
|
|
15
16
|
import { createCommandController } from "./commands.ts";
|
|
16
17
|
import type { ActiveOverlay } from "./overlay.ts";
|
|
@@ -58,12 +59,15 @@ function createTestState(): AppState {
|
|
|
58
59
|
contextTokens: 0,
|
|
59
60
|
agentsMd: [],
|
|
60
61
|
skills: [],
|
|
61
|
-
plugins: [],
|
|
62
62
|
theme: DEFAULT_THEME,
|
|
63
63
|
git: null,
|
|
64
|
+
delegationDepth: 0,
|
|
65
|
+
delegationBudgetLimit: 4,
|
|
66
|
+
delegationBudgetRemaining: 4,
|
|
64
67
|
providers: new Map(),
|
|
65
68
|
oauthCredentials: {},
|
|
66
69
|
settings: {},
|
|
70
|
+
repoSettings: {},
|
|
67
71
|
settingsPath,
|
|
68
72
|
cwd,
|
|
69
73
|
canonicalCwd: cwd,
|
|
@@ -74,6 +78,7 @@ function createTestState(): AppState {
|
|
|
74
78
|
showReasoning: true,
|
|
75
79
|
verbose: false,
|
|
76
80
|
versionLabel: "dev",
|
|
81
|
+
mcpServers: [],
|
|
77
82
|
customModels: [],
|
|
78
83
|
startupWarnings: [],
|
|
79
84
|
};
|
|
@@ -116,6 +121,9 @@ describe("ui/commands", () => {
|
|
|
116
121
|
model: "test/beta",
|
|
117
122
|
effort: "high",
|
|
118
123
|
});
|
|
124
|
+
state.queuedUserMessages = [
|
|
125
|
+
{ role: "user", content: "stale steering", timestamp: now - 1 },
|
|
126
|
+
];
|
|
119
127
|
|
|
120
128
|
state.db.run(
|
|
121
129
|
"INSERT INTO messages (session_id, turn, data, created_at) VALUES (?, ?, ?, ?)",
|
|
@@ -147,8 +155,47 @@ describe("ui/commands", () => {
|
|
|
147
155
|
now + 1,
|
|
148
156
|
],
|
|
149
157
|
);
|
|
158
|
+
const messageRows = state.db
|
|
159
|
+
.query<
|
|
160
|
+
{ id: number },
|
|
161
|
+
[string]
|
|
162
|
+
>("SELECT id FROM messages WHERE session_id = ? ORDER BY id")
|
|
163
|
+
.all(session.id);
|
|
164
|
+
state.db.run(
|
|
165
|
+
"INSERT INTO session_compactions (session_id, message_end_id, summary_data, usage_data, created_at) VALUES (?, ?, ?, ?, ?)",
|
|
166
|
+
[
|
|
167
|
+
session.id,
|
|
168
|
+
messageRows[1]!.id,
|
|
169
|
+
JSON.stringify({
|
|
170
|
+
role: "user",
|
|
171
|
+
content: [
|
|
172
|
+
"summary of the earlier turn",
|
|
173
|
+
"",
|
|
174
|
+
"<system-message>",
|
|
175
|
+
"Read the originals from the SQLite DB if needed.",
|
|
176
|
+
"</system-message>",
|
|
177
|
+
].join("\n"),
|
|
178
|
+
timestamp: now + 2,
|
|
179
|
+
}),
|
|
180
|
+
JSON.stringify({
|
|
181
|
+
input: 12,
|
|
182
|
+
output: 3,
|
|
183
|
+
cacheRead: 0,
|
|
184
|
+
cacheWrite: 0,
|
|
185
|
+
totalTokens: 15,
|
|
186
|
+
cost: {
|
|
187
|
+
input: 0.01,
|
|
188
|
+
output: 0.02,
|
|
189
|
+
cacheRead: 0,
|
|
190
|
+
cacheWrite: 0,
|
|
191
|
+
total: 0.03,
|
|
192
|
+
},
|
|
193
|
+
}),
|
|
194
|
+
now + 2,
|
|
195
|
+
],
|
|
196
|
+
);
|
|
150
197
|
state.db.run("UPDATE sessions SET updated_at = ? WHERE id = ?", [
|
|
151
|
-
now +
|
|
198
|
+
now + 2,
|
|
152
199
|
session.id,
|
|
153
200
|
]);
|
|
154
201
|
|
|
@@ -168,11 +215,14 @@ describe("ui/commands", () => {
|
|
|
168
215
|
"assistant",
|
|
169
216
|
]);
|
|
170
217
|
expect(state.stats).toEqual({
|
|
171
|
-
totalInput:
|
|
172
|
-
totalOutput:
|
|
173
|
-
totalCost: 0,
|
|
218
|
+
totalInput: 12,
|
|
219
|
+
totalOutput: 3,
|
|
220
|
+
totalCost: 0.03,
|
|
174
221
|
});
|
|
175
|
-
expect(state.contextTokens).toBe(
|
|
222
|
+
expect(state.contextTokens).toBe(
|
|
223
|
+
computeSessionContextTokens(state.db, session.id),
|
|
224
|
+
);
|
|
225
|
+
expect(state.queuedUserMessages).toEqual([]);
|
|
176
226
|
} finally {
|
|
177
227
|
state.db.close();
|
|
178
228
|
}
|
|
@@ -214,6 +264,9 @@ describe("ui/commands", () => {
|
|
|
214
264
|
];
|
|
215
265
|
state.stats = { totalInput: 10, totalOutput: 20, totalCost: 0.5 };
|
|
216
266
|
state.contextTokens = 123;
|
|
267
|
+
state.queuedUserMessages = [
|
|
268
|
+
{ role: "user", content: "stale steering", timestamp: 2 },
|
|
269
|
+
];
|
|
217
270
|
|
|
218
271
|
try {
|
|
219
272
|
expect(controller.handleCommand("new", state)).toBe(true);
|
|
@@ -231,6 +284,74 @@ describe("ui/commands", () => {
|
|
|
231
284
|
expect(state.agentsMd).toEqual([
|
|
232
285
|
{ path: "/tmp/reloaded/AGENTS.md", content: "Reloaded context" },
|
|
233
286
|
]);
|
|
287
|
+
expect(state.queuedUserMessages).toEqual([]);
|
|
288
|
+
} finally {
|
|
289
|
+
state.db.close();
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
test("/undo clears queued steering before removing the latest turn", async () => {
|
|
294
|
+
const state = createTestState();
|
|
295
|
+
const controller = createCommandController({
|
|
296
|
+
openOverlay: () => {},
|
|
297
|
+
dismissOverlay: () => {},
|
|
298
|
+
setInputValue: () => {},
|
|
299
|
+
appendInfoMessage: () => {},
|
|
300
|
+
appendTodoMessage: () => {},
|
|
301
|
+
scrollConversationToBottom: () => {},
|
|
302
|
+
requestRender: () => {},
|
|
303
|
+
reloadPromptContext: async () => {},
|
|
304
|
+
openInBrowser: () => {},
|
|
305
|
+
});
|
|
306
|
+
const now = new Date("2026-04-07T12:00:00Z").getTime();
|
|
307
|
+
const session = createSession(state.db, {
|
|
308
|
+
cwd: state.canonicalCwd,
|
|
309
|
+
model: "test/beta",
|
|
310
|
+
effort: "high",
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
state.session = session;
|
|
314
|
+
state.queuedUserMessages = [
|
|
315
|
+
{ role: "user", content: "stale steering", timestamp: now + 2 },
|
|
316
|
+
];
|
|
317
|
+
state.db.run(
|
|
318
|
+
"INSERT INTO messages (session_id, turn, data, created_at) VALUES (?, ?, ?, ?)",
|
|
319
|
+
[
|
|
320
|
+
session.id,
|
|
321
|
+
1,
|
|
322
|
+
JSON.stringify({
|
|
323
|
+
role: "user",
|
|
324
|
+
content: "historical prompt",
|
|
325
|
+
timestamp: now,
|
|
326
|
+
}),
|
|
327
|
+
now,
|
|
328
|
+
],
|
|
329
|
+
);
|
|
330
|
+
state.db.run(
|
|
331
|
+
"INSERT INTO messages (session_id, turn, data, created_at) VALUES (?, ?, ?, ?)",
|
|
332
|
+
[
|
|
333
|
+
session.id,
|
|
334
|
+
1,
|
|
335
|
+
JSON.stringify({
|
|
336
|
+
role: "assistant",
|
|
337
|
+
content: [{ type: "text", text: "historical reply" }],
|
|
338
|
+
api: "anthropic-messages",
|
|
339
|
+
provider: "anthropic",
|
|
340
|
+
model: "test/beta",
|
|
341
|
+
stopReason: "stop",
|
|
342
|
+
timestamp: now + 1,
|
|
343
|
+
}),
|
|
344
|
+
now + 1,
|
|
345
|
+
],
|
|
346
|
+
);
|
|
347
|
+
state.messages = loadMessages(state.db, session.id);
|
|
348
|
+
|
|
349
|
+
try {
|
|
350
|
+
expect(controller.handleCommand("undo", state)).toBe(true);
|
|
351
|
+
await Promise.resolve();
|
|
352
|
+
|
|
353
|
+
expect(state.queuedUserMessages).toEqual([]);
|
|
354
|
+
expect(state.messages).toEqual([]);
|
|
234
355
|
} finally {
|
|
235
356
|
state.db.close();
|
|
236
357
|
}
|
|
@@ -286,8 +407,396 @@ describe("ui/commands", () => {
|
|
|
286
407
|
}
|
|
287
408
|
});
|
|
288
409
|
|
|
410
|
+
test("/mcp toggles the selected server, persists it, and reconnects when re-enabled", async () => {
|
|
411
|
+
const state = createTestState();
|
|
412
|
+
state.model = {
|
|
413
|
+
id: "test-model",
|
|
414
|
+
name: "test-model",
|
|
415
|
+
api: "openai-completions",
|
|
416
|
+
provider: "test-provider",
|
|
417
|
+
baseUrl: "http://localhost:1234/v1",
|
|
418
|
+
reasoning: false,
|
|
419
|
+
input: ["text"],
|
|
420
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
421
|
+
contextWindow: 8192,
|
|
422
|
+
maxTokens: 4096,
|
|
423
|
+
};
|
|
424
|
+
|
|
425
|
+
const docsTools = [
|
|
426
|
+
{
|
|
427
|
+
name: "docs__search",
|
|
428
|
+
description: "[MCP docs] Search the docs",
|
|
429
|
+
parameters: Type.Object({}),
|
|
430
|
+
},
|
|
431
|
+
{
|
|
432
|
+
name: "docs__fetch",
|
|
433
|
+
description: "[MCP docs] Fetch a page",
|
|
434
|
+
parameters: Type.Object({}),
|
|
435
|
+
},
|
|
436
|
+
];
|
|
437
|
+
const docsHandlers = new Map();
|
|
438
|
+
let disconnectCalls = 0;
|
|
439
|
+
let connectCalls = 0;
|
|
440
|
+
|
|
441
|
+
state.mcpServers = [
|
|
442
|
+
{
|
|
443
|
+
name: "docs",
|
|
444
|
+
url: "http://docs.test/mcp",
|
|
445
|
+
enabled: true,
|
|
446
|
+
connected: true,
|
|
447
|
+
tools: docsTools,
|
|
448
|
+
toolHandlers: docsHandlers,
|
|
449
|
+
close: async () => {
|
|
450
|
+
disconnectCalls += 1;
|
|
451
|
+
},
|
|
452
|
+
},
|
|
453
|
+
];
|
|
454
|
+
state.settings = saveSettings(state.settingsPath, {
|
|
455
|
+
mcp: {
|
|
456
|
+
servers: [
|
|
457
|
+
{
|
|
458
|
+
name: "docs",
|
|
459
|
+
url: "http://docs.test/mcp",
|
|
460
|
+
enabled: true,
|
|
461
|
+
},
|
|
462
|
+
{
|
|
463
|
+
name: "down",
|
|
464
|
+
url: "http://down.test/mcp",
|
|
465
|
+
enabled: false,
|
|
466
|
+
},
|
|
467
|
+
],
|
|
468
|
+
},
|
|
469
|
+
});
|
|
470
|
+
const runtimeState = { overlay: null as ActiveOverlay | null };
|
|
471
|
+
const appended: Array<{
|
|
472
|
+
text: string;
|
|
473
|
+
sessionId: string | null;
|
|
474
|
+
}> = [];
|
|
475
|
+
const controller = createCommandController(
|
|
476
|
+
{
|
|
477
|
+
openOverlay: (nextOverlay) => {
|
|
478
|
+
runtimeState.overlay = nextOverlay;
|
|
479
|
+
},
|
|
480
|
+
dismissOverlay: () => {
|
|
481
|
+
runtimeState.overlay = null;
|
|
482
|
+
},
|
|
483
|
+
setInputValue: () => {},
|
|
484
|
+
appendInfoMessage: (text, nextState) => {
|
|
485
|
+
appended.push({ text, sessionId: nextState.session?.id ?? null });
|
|
486
|
+
},
|
|
487
|
+
appendTodoMessage: () => {},
|
|
488
|
+
scrollConversationToBottom: () => {},
|
|
489
|
+
requestRender: () => {},
|
|
490
|
+
reloadPromptContext: async () => {},
|
|
491
|
+
openInBrowser: () => {},
|
|
492
|
+
},
|
|
493
|
+
{
|
|
494
|
+
connectMcpServer: async (server) => {
|
|
495
|
+
connectCalls += 1;
|
|
496
|
+
server.connected = true;
|
|
497
|
+
server.tools = docsTools;
|
|
498
|
+
server.toolHandlers = docsHandlers;
|
|
499
|
+
server.close = async () => {
|
|
500
|
+
disconnectCalls += 1;
|
|
501
|
+
};
|
|
502
|
+
return [];
|
|
503
|
+
},
|
|
504
|
+
disconnectMcpServer: async (server) => {
|
|
505
|
+
await server.close();
|
|
506
|
+
server.connected = false;
|
|
507
|
+
server.tools = [];
|
|
508
|
+
server.toolHandlers = new Map();
|
|
509
|
+
server.close = async () => {};
|
|
510
|
+
},
|
|
511
|
+
},
|
|
512
|
+
);
|
|
513
|
+
|
|
514
|
+
try {
|
|
515
|
+
expect(controller.handleCommand("mcp", state)).toBe(true);
|
|
516
|
+
const firstOverlay = expectOverlay(runtimeState.overlay);
|
|
517
|
+
expect(firstOverlay.title).toBe("Toggle MCP servers");
|
|
518
|
+
expect(buildToolList(state).tools.map((tool) => tool.name)).toContain(
|
|
519
|
+
"docs__search",
|
|
520
|
+
);
|
|
521
|
+
renderSelect(firstOverlay).props.onKeyPress?.("enter");
|
|
522
|
+
await Promise.resolve();
|
|
523
|
+
await Promise.resolve();
|
|
524
|
+
|
|
525
|
+
expect(runtimeState.overlay).toBeNull();
|
|
526
|
+
expect(state.mcpServers[0]?.enabled).toBe(false);
|
|
527
|
+
expect(state.mcpServers[0]?.connected).toBe(false);
|
|
528
|
+
expect(state.settings.mcp).toEqual({
|
|
529
|
+
servers: [
|
|
530
|
+
{
|
|
531
|
+
name: "docs",
|
|
532
|
+
url: "http://docs.test/mcp",
|
|
533
|
+
enabled: false,
|
|
534
|
+
},
|
|
535
|
+
{
|
|
536
|
+
name: "down",
|
|
537
|
+
url: "http://down.test/mcp",
|
|
538
|
+
enabled: false,
|
|
539
|
+
},
|
|
540
|
+
],
|
|
541
|
+
});
|
|
542
|
+
expect(loadSettings(state.settingsPath)).toEqual({
|
|
543
|
+
mcp: {
|
|
544
|
+
servers: [
|
|
545
|
+
{
|
|
546
|
+
name: "docs",
|
|
547
|
+
url: "http://docs.test/mcp",
|
|
548
|
+
enabled: false,
|
|
549
|
+
},
|
|
550
|
+
{
|
|
551
|
+
name: "down",
|
|
552
|
+
url: "http://down.test/mcp",
|
|
553
|
+
enabled: false,
|
|
554
|
+
},
|
|
555
|
+
],
|
|
556
|
+
},
|
|
557
|
+
});
|
|
558
|
+
expect(disconnectCalls).toBe(1);
|
|
559
|
+
expect(appended).toEqual([
|
|
560
|
+
{
|
|
561
|
+
text: 'Disabled MCP server "docs" (-2 tools).',
|
|
562
|
+
sessionId: null,
|
|
563
|
+
},
|
|
564
|
+
]);
|
|
565
|
+
expect(buildToolList(state).tools.map((tool) => tool.name)).not.toContain(
|
|
566
|
+
"docs__search",
|
|
567
|
+
);
|
|
568
|
+
|
|
569
|
+
expect(controller.handleCommand("mcp", state)).toBe(true);
|
|
570
|
+
const secondOverlay = expectOverlay(runtimeState.overlay);
|
|
571
|
+
renderSelect(secondOverlay).props.onKeyPress?.("enter");
|
|
572
|
+
await Promise.resolve();
|
|
573
|
+
await Promise.resolve();
|
|
574
|
+
|
|
575
|
+
expect(state.mcpServers[0]?.enabled).toBe(true);
|
|
576
|
+
expect(state.mcpServers[0]?.connected).toBe(true);
|
|
577
|
+
expect(connectCalls).toBe(1);
|
|
578
|
+
expect(loadSettings(state.settingsPath)).toEqual({
|
|
579
|
+
mcp: {
|
|
580
|
+
servers: [
|
|
581
|
+
{
|
|
582
|
+
name: "docs",
|
|
583
|
+
url: "http://docs.test/mcp",
|
|
584
|
+
enabled: true,
|
|
585
|
+
},
|
|
586
|
+
{
|
|
587
|
+
name: "down",
|
|
588
|
+
url: "http://down.test/mcp",
|
|
589
|
+
enabled: false,
|
|
590
|
+
},
|
|
591
|
+
],
|
|
592
|
+
},
|
|
593
|
+
});
|
|
594
|
+
expect(appended[1]).toEqual({
|
|
595
|
+
text: 'Enabled MCP server "docs" (+2 tools).',
|
|
596
|
+
sessionId: null,
|
|
597
|
+
});
|
|
598
|
+
expect(buildToolList(state).tools.map((tool) => tool.name)).toContain(
|
|
599
|
+
"docs__search",
|
|
600
|
+
);
|
|
601
|
+
} finally {
|
|
602
|
+
state.db.close();
|
|
603
|
+
}
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
test("/mcp toggles repo-local servers for the current run without persisting a global shadow copy", async () => {
|
|
607
|
+
const state = createTestState();
|
|
608
|
+
state.model = {
|
|
609
|
+
id: "test-model",
|
|
610
|
+
name: "test-model",
|
|
611
|
+
api: "openai-completions",
|
|
612
|
+
provider: "test-provider",
|
|
613
|
+
baseUrl: "http://localhost:1234/v1",
|
|
614
|
+
reasoning: false,
|
|
615
|
+
input: ["text"],
|
|
616
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
617
|
+
contextWindow: 8192,
|
|
618
|
+
maxTokens: 4096,
|
|
619
|
+
};
|
|
620
|
+
|
|
621
|
+
const docsTools = [
|
|
622
|
+
{
|
|
623
|
+
name: "docs__search",
|
|
624
|
+
description: "[MCP docs] Search the docs",
|
|
625
|
+
parameters: Type.Object({}),
|
|
626
|
+
},
|
|
627
|
+
];
|
|
628
|
+
const docsHandlers = new Map();
|
|
629
|
+
let disconnectCalls = 0;
|
|
630
|
+
let connectCalls = 0;
|
|
631
|
+
|
|
632
|
+
state.mcpServers = [
|
|
633
|
+
{
|
|
634
|
+
name: "docs",
|
|
635
|
+
url: "http://repo-docs.test/mcp",
|
|
636
|
+
enabled: true,
|
|
637
|
+
connected: true,
|
|
638
|
+
tools: docsTools,
|
|
639
|
+
toolHandlers: docsHandlers,
|
|
640
|
+
close: async () => {
|
|
641
|
+
disconnectCalls += 1;
|
|
642
|
+
},
|
|
643
|
+
},
|
|
644
|
+
];
|
|
645
|
+
state.settings = saveSettings(state.settingsPath, {
|
|
646
|
+
mcp: {
|
|
647
|
+
servers: [
|
|
648
|
+
{
|
|
649
|
+
name: "docs",
|
|
650
|
+
url: "http://global-docs.test/mcp",
|
|
651
|
+
enabled: false,
|
|
652
|
+
},
|
|
653
|
+
{
|
|
654
|
+
name: "down",
|
|
655
|
+
url: "http://down.test/mcp",
|
|
656
|
+
enabled: false,
|
|
657
|
+
},
|
|
658
|
+
],
|
|
659
|
+
},
|
|
660
|
+
});
|
|
661
|
+
state.repoSettings = {
|
|
662
|
+
mcp: {
|
|
663
|
+
servers: [
|
|
664
|
+
{
|
|
665
|
+
name: "docs",
|
|
666
|
+
url: "http://repo-docs.test/mcp",
|
|
667
|
+
enabled: true,
|
|
668
|
+
},
|
|
669
|
+
],
|
|
670
|
+
},
|
|
671
|
+
};
|
|
672
|
+
const runtimeState = { overlay: null as ActiveOverlay | null };
|
|
673
|
+
const appended: Array<{
|
|
674
|
+
text: string;
|
|
675
|
+
sessionId: string | null;
|
|
676
|
+
}> = [];
|
|
677
|
+
const controller = createCommandController(
|
|
678
|
+
{
|
|
679
|
+
openOverlay: (nextOverlay) => {
|
|
680
|
+
runtimeState.overlay = nextOverlay;
|
|
681
|
+
},
|
|
682
|
+
dismissOverlay: () => {
|
|
683
|
+
runtimeState.overlay = null;
|
|
684
|
+
},
|
|
685
|
+
setInputValue: () => {},
|
|
686
|
+
appendInfoMessage: (text, nextState) => {
|
|
687
|
+
appended.push({ text, sessionId: nextState.session?.id ?? null });
|
|
688
|
+
},
|
|
689
|
+
appendTodoMessage: () => {},
|
|
690
|
+
scrollConversationToBottom: () => {},
|
|
691
|
+
requestRender: () => {},
|
|
692
|
+
reloadPromptContext: async () => {},
|
|
693
|
+
openInBrowser: () => {},
|
|
694
|
+
},
|
|
695
|
+
{
|
|
696
|
+
connectMcpServer: async (server) => {
|
|
697
|
+
connectCalls += 1;
|
|
698
|
+
server.connected = true;
|
|
699
|
+
server.tools = docsTools;
|
|
700
|
+
server.toolHandlers = docsHandlers;
|
|
701
|
+
server.close = async () => {
|
|
702
|
+
disconnectCalls += 1;
|
|
703
|
+
};
|
|
704
|
+
return [];
|
|
705
|
+
},
|
|
706
|
+
disconnectMcpServer: async (server) => {
|
|
707
|
+
await server.close();
|
|
708
|
+
server.connected = false;
|
|
709
|
+
server.tools = [];
|
|
710
|
+
server.toolHandlers = new Map();
|
|
711
|
+
server.close = async () => {};
|
|
712
|
+
},
|
|
713
|
+
},
|
|
714
|
+
);
|
|
715
|
+
|
|
716
|
+
try {
|
|
717
|
+
expect(controller.handleCommand("mcp", state)).toBe(true);
|
|
718
|
+
renderSelect(expectOverlay(runtimeState.overlay)).props.onKeyPress?.(
|
|
719
|
+
"enter",
|
|
720
|
+
);
|
|
721
|
+
await Promise.resolve();
|
|
722
|
+
await Promise.resolve();
|
|
723
|
+
|
|
724
|
+
expect(state.mcpServers[0]?.enabled).toBe(false);
|
|
725
|
+
expect(state.mcpServers[0]?.connected).toBe(false);
|
|
726
|
+
expect(state.settings).toEqual({
|
|
727
|
+
mcp: {
|
|
728
|
+
servers: [
|
|
729
|
+
{
|
|
730
|
+
name: "docs",
|
|
731
|
+
url: "http://global-docs.test/mcp",
|
|
732
|
+
enabled: false,
|
|
733
|
+
},
|
|
734
|
+
{
|
|
735
|
+
name: "down",
|
|
736
|
+
url: "http://down.test/mcp",
|
|
737
|
+
enabled: false,
|
|
738
|
+
},
|
|
739
|
+
],
|
|
740
|
+
},
|
|
741
|
+
});
|
|
742
|
+
expect(loadSettings(state.settingsPath)).toEqual(state.settings);
|
|
743
|
+
expect(disconnectCalls).toBe(1);
|
|
744
|
+
|
|
745
|
+
expect(controller.handleCommand("mcp", state)).toBe(true);
|
|
746
|
+
renderSelect(expectOverlay(runtimeState.overlay)).props.onKeyPress?.(
|
|
747
|
+
"enter",
|
|
748
|
+
);
|
|
749
|
+
await Promise.resolve();
|
|
750
|
+
await Promise.resolve();
|
|
751
|
+
|
|
752
|
+
expect(state.mcpServers[0]?.enabled).toBe(true);
|
|
753
|
+
expect(state.mcpServers[0]?.connected).toBe(true);
|
|
754
|
+
expect(connectCalls).toBe(1);
|
|
755
|
+
expect(state.settings).toEqual({
|
|
756
|
+
mcp: {
|
|
757
|
+
servers: [
|
|
758
|
+
{
|
|
759
|
+
name: "docs",
|
|
760
|
+
url: "http://global-docs.test/mcp",
|
|
761
|
+
enabled: false,
|
|
762
|
+
},
|
|
763
|
+
{
|
|
764
|
+
name: "down",
|
|
765
|
+
url: "http://down.test/mcp",
|
|
766
|
+
enabled: false,
|
|
767
|
+
},
|
|
768
|
+
],
|
|
769
|
+
},
|
|
770
|
+
});
|
|
771
|
+
expect(loadSettings(state.settingsPath)).toEqual(state.settings);
|
|
772
|
+
expect(appended).toEqual([
|
|
773
|
+
{
|
|
774
|
+
text: 'Disabled MCP server "docs" (-1 tool).',
|
|
775
|
+
sessionId: null,
|
|
776
|
+
},
|
|
777
|
+
{
|
|
778
|
+
text: 'Enabled MCP server "docs" (+1 tool).',
|
|
779
|
+
sessionId: null,
|
|
780
|
+
},
|
|
781
|
+
]);
|
|
782
|
+
} finally {
|
|
783
|
+
state.db.close();
|
|
784
|
+
}
|
|
785
|
+
});
|
|
786
|
+
|
|
289
787
|
test("/help appends a markdown info message without creating a session", () => {
|
|
290
788
|
const state = createTestState();
|
|
789
|
+
state.mcpServers = [
|
|
790
|
+
{
|
|
791
|
+
name: "docs",
|
|
792
|
+
url: "http://docs.test/mcp",
|
|
793
|
+
enabled: false,
|
|
794
|
+
connected: false,
|
|
795
|
+
tools: [],
|
|
796
|
+
toolHandlers: new Map(),
|
|
797
|
+
close: async () => {},
|
|
798
|
+
},
|
|
799
|
+
];
|
|
291
800
|
const appended: Array<{
|
|
292
801
|
text: string;
|
|
293
802
|
format: string | undefined;
|
|
@@ -315,6 +824,12 @@ describe("ui/commands", () => {
|
|
|
315
824
|
expect(controller.handleCommand("help", state)).toBe(true);
|
|
316
825
|
expect(appended).toHaveLength(1);
|
|
317
826
|
expect(appended[0]?.text.length).toBeGreaterThan(0);
|
|
827
|
+
expect(appended[0]?.text).toContain("`/mcp`");
|
|
828
|
+
expect(appended[0]?.text).toContain("`/skill:name`");
|
|
829
|
+
expect(appended[0]?.text).toContain(
|
|
830
|
+
"submit `/skill` to open the skill picker",
|
|
831
|
+
);
|
|
832
|
+
expect(appended[0]?.text).toContain("`docs` (off)");
|
|
318
833
|
expect(appended[0]?.format).toBe("markdown");
|
|
319
834
|
expect(appended[0]?.sessionId).toBeNull();
|
|
320
835
|
expect(state.session).toBeNull();
|