mini-coder 0.5.11 → 0.5.13
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 +107 -0
- package/PROGRESS.md +4 -0
- package/README.md +66 -21
- package/assets/mc-claude-smart.png +0 -0
- package/assets/mc-gpt-smart.png +0 -0
- package/benchmark-baseline.sh +15 -0
- package/benchmark-loop.sh +19 -0
- package/bun.lock +265 -90
- package/package.json +8 -7
- package/skills-lock.json +15 -0
- package/src/agent.ts +20 -0
- package/src/cli.ts +2 -1
- package/src/headless.ts +152 -38
- package/src/index.ts +107 -143
- package/src/input.ts +13 -1
- package/src/mcp.ts +609 -0
- package/src/prompt.ts +10 -12
- package/src/session-message.ts +393 -0
- package/src/session.ts +102 -396
- package/src/settings.ts +218 -22
- package/src/shared.ts +39 -0
- package/src/skills.ts +12 -3
- package/src/submit.ts +20 -25
- package/src/text.ts +71 -0
- package/src/theme.ts +186 -3
- package/src/tool-common.ts +93 -0
- package/src/tool-grep.ts +606 -0
- package/src/tool-read.ts +313 -0
- package/src/tool-shell.ts +1001 -0
- package/src/tools.ts +190 -995
- package/src/ui/agent.ts +206 -110
- package/src/ui/commands.test.ts +489 -17
- package/src/ui/commands.ts +231 -55
- package/src/ui/conversation.test.ts +585 -0
- package/src/ui/conversation.ts +878 -455
- package/src/ui/help.ts +27 -11
- package/src/ui/input.test.ts +1 -43
- package/src/ui/runtime.ts +69 -0
- package/src/ui.ts +422 -185
- 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
|
computeContextTokens,
|
|
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,12 @@ 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
64
|
providers: new Map(),
|
|
65
65
|
oauthCredentials: {},
|
|
66
66
|
settings: {},
|
|
67
|
+
repoSettings: {},
|
|
67
68
|
settingsPath,
|
|
68
69
|
cwd,
|
|
69
70
|
canonicalCwd: cwd,
|
|
@@ -74,6 +75,7 @@ function createTestState(): AppState {
|
|
|
74
75
|
showReasoning: true,
|
|
75
76
|
verbose: false,
|
|
76
77
|
versionLabel: "dev",
|
|
78
|
+
mcpServers: [],
|
|
77
79
|
customModels: [],
|
|
78
80
|
startupWarnings: [],
|
|
79
81
|
};
|
|
@@ -90,6 +92,7 @@ describe("ui/commands", () => {
|
|
|
90
92
|
const state = createTestState();
|
|
91
93
|
const runtimeState = { overlay: null as ActiveOverlay | null };
|
|
92
94
|
let scrollCalls = 0;
|
|
95
|
+
let renderCalls = 0;
|
|
93
96
|
const controller = createCommandController({
|
|
94
97
|
openOverlay: (nextOverlay) => {
|
|
95
98
|
runtimeState.overlay = nextOverlay;
|
|
@@ -103,7 +106,9 @@ describe("ui/commands", () => {
|
|
|
103
106
|
scrollConversationToBottom: () => {
|
|
104
107
|
scrollCalls += 1;
|
|
105
108
|
},
|
|
106
|
-
|
|
109
|
+
requestRender: () => {
|
|
110
|
+
renderCalls += 1;
|
|
111
|
+
},
|
|
107
112
|
reloadPromptContext: async () => {},
|
|
108
113
|
openInBrowser: () => {},
|
|
109
114
|
});
|
|
@@ -113,6 +118,9 @@ describe("ui/commands", () => {
|
|
|
113
118
|
model: "test/beta",
|
|
114
119
|
effort: "high",
|
|
115
120
|
});
|
|
121
|
+
state.queuedUserMessages = [
|
|
122
|
+
{ role: "user", content: "stale steering", timestamp: now - 1 },
|
|
123
|
+
];
|
|
116
124
|
|
|
117
125
|
state.db.run(
|
|
118
126
|
"INSERT INTO messages (session_id, turn, data, created_at) VALUES (?, ?, ?, ?)",
|
|
@@ -153,13 +161,12 @@ describe("ui/commands", () => {
|
|
|
153
161
|
expect(controller.handleCommand("session", state)).toBe(true);
|
|
154
162
|
|
|
155
163
|
const overlay = expectOverlay(runtimeState.overlay);
|
|
156
|
-
expect(overlay.title).toBe("Resume a session");
|
|
157
|
-
|
|
158
164
|
const selectNode = renderSelect(overlay);
|
|
159
165
|
selectNode.props.onKeyPress?.("enter");
|
|
160
166
|
|
|
161
167
|
expect(runtimeState.overlay).toBeNull();
|
|
162
168
|
expect(scrollCalls).toBe(1);
|
|
169
|
+
expect(renderCalls).toBe(1);
|
|
163
170
|
expect(state.session?.id).toBe(session.id);
|
|
164
171
|
expect(state.messages.map((message) => message.role)).toEqual([
|
|
165
172
|
"user",
|
|
@@ -171,6 +178,7 @@ describe("ui/commands", () => {
|
|
|
171
178
|
totalCost: 0,
|
|
172
179
|
});
|
|
173
180
|
expect(state.contextTokens).toBe(computeContextTokens(state.messages));
|
|
181
|
+
expect(state.queuedUserMessages).toEqual([]);
|
|
174
182
|
} finally {
|
|
175
183
|
state.db.close();
|
|
176
184
|
}
|
|
@@ -187,7 +195,7 @@ describe("ui/commands", () => {
|
|
|
187
195
|
appendInfoMessage: () => {},
|
|
188
196
|
appendTodoMessage: () => {},
|
|
189
197
|
scrollConversationToBottom: () => {},
|
|
190
|
-
|
|
198
|
+
requestRender: () => {},
|
|
191
199
|
reloadPromptContext: async (nextState) => {
|
|
192
200
|
reloadCount++;
|
|
193
201
|
nextState.agentsMd = [
|
|
@@ -212,6 +220,9 @@ describe("ui/commands", () => {
|
|
|
212
220
|
];
|
|
213
221
|
state.stats = { totalInput: 10, totalOutput: 20, totalCost: 0.5 };
|
|
214
222
|
state.contextTokens = 123;
|
|
223
|
+
state.queuedUserMessages = [
|
|
224
|
+
{ role: "user", content: "stale steering", timestamp: 2 },
|
|
225
|
+
];
|
|
215
226
|
|
|
216
227
|
try {
|
|
217
228
|
expect(controller.handleCommand("new", state)).toBe(true);
|
|
@@ -229,6 +240,74 @@ describe("ui/commands", () => {
|
|
|
229
240
|
expect(state.agentsMd).toEqual([
|
|
230
241
|
{ path: "/tmp/reloaded/AGENTS.md", content: "Reloaded context" },
|
|
231
242
|
]);
|
|
243
|
+
expect(state.queuedUserMessages).toEqual([]);
|
|
244
|
+
} finally {
|
|
245
|
+
state.db.close();
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
test("/undo clears queued steering before removing the latest turn", async () => {
|
|
250
|
+
const state = createTestState();
|
|
251
|
+
const controller = createCommandController({
|
|
252
|
+
openOverlay: () => {},
|
|
253
|
+
dismissOverlay: () => {},
|
|
254
|
+
setInputValue: () => {},
|
|
255
|
+
appendInfoMessage: () => {},
|
|
256
|
+
appendTodoMessage: () => {},
|
|
257
|
+
scrollConversationToBottom: () => {},
|
|
258
|
+
requestRender: () => {},
|
|
259
|
+
reloadPromptContext: async () => {},
|
|
260
|
+
openInBrowser: () => {},
|
|
261
|
+
});
|
|
262
|
+
const now = new Date("2026-04-07T12:00:00Z").getTime();
|
|
263
|
+
const session = createSession(state.db, {
|
|
264
|
+
cwd: state.canonicalCwd,
|
|
265
|
+
model: "test/beta",
|
|
266
|
+
effort: "high",
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
state.session = session;
|
|
270
|
+
state.queuedUserMessages = [
|
|
271
|
+
{ role: "user", content: "stale steering", timestamp: now + 2 },
|
|
272
|
+
];
|
|
273
|
+
state.db.run(
|
|
274
|
+
"INSERT INTO messages (session_id, turn, data, created_at) VALUES (?, ?, ?, ?)",
|
|
275
|
+
[
|
|
276
|
+
session.id,
|
|
277
|
+
1,
|
|
278
|
+
JSON.stringify({
|
|
279
|
+
role: "user",
|
|
280
|
+
content: "historical prompt",
|
|
281
|
+
timestamp: now,
|
|
282
|
+
}),
|
|
283
|
+
now,
|
|
284
|
+
],
|
|
285
|
+
);
|
|
286
|
+
state.db.run(
|
|
287
|
+
"INSERT INTO messages (session_id, turn, data, created_at) VALUES (?, ?, ?, ?)",
|
|
288
|
+
[
|
|
289
|
+
session.id,
|
|
290
|
+
1,
|
|
291
|
+
JSON.stringify({
|
|
292
|
+
role: "assistant",
|
|
293
|
+
content: [{ type: "text", text: "historical reply" }],
|
|
294
|
+
api: "anthropic-messages",
|
|
295
|
+
provider: "anthropic",
|
|
296
|
+
model: "test/beta",
|
|
297
|
+
stopReason: "stop",
|
|
298
|
+
timestamp: now + 1,
|
|
299
|
+
}),
|
|
300
|
+
now + 1,
|
|
301
|
+
],
|
|
302
|
+
);
|
|
303
|
+
state.messages = loadMessages(state.db, session.id);
|
|
304
|
+
|
|
305
|
+
try {
|
|
306
|
+
expect(controller.handleCommand("undo", state)).toBe(true);
|
|
307
|
+
await Promise.resolve();
|
|
308
|
+
|
|
309
|
+
expect(state.queuedUserMessages).toEqual([]);
|
|
310
|
+
expect(state.messages).toEqual([]);
|
|
232
311
|
} finally {
|
|
233
312
|
state.db.close();
|
|
234
313
|
}
|
|
@@ -243,7 +322,7 @@ describe("ui/commands", () => {
|
|
|
243
322
|
appendInfoMessage: () => {},
|
|
244
323
|
appendTodoMessage: () => {},
|
|
245
324
|
scrollConversationToBottom: () => {},
|
|
246
|
-
|
|
325
|
+
requestRender: () => {},
|
|
247
326
|
reloadPromptContext: async () => {},
|
|
248
327
|
openInBrowser: () => {},
|
|
249
328
|
});
|
|
@@ -269,7 +348,7 @@ describe("ui/commands", () => {
|
|
|
269
348
|
appendInfoMessage: () => {},
|
|
270
349
|
appendTodoMessage: () => {},
|
|
271
350
|
scrollConversationToBottom: () => {},
|
|
272
|
-
|
|
351
|
+
requestRender: () => {},
|
|
273
352
|
reloadPromptContext: async () => {},
|
|
274
353
|
openInBrowser: () => {},
|
|
275
354
|
});
|
|
@@ -284,8 +363,396 @@ describe("ui/commands", () => {
|
|
|
284
363
|
}
|
|
285
364
|
});
|
|
286
365
|
|
|
287
|
-
test("/
|
|
366
|
+
test("/mcp toggles the selected server, persists it, and reconnects when re-enabled", async () => {
|
|
288
367
|
const state = createTestState();
|
|
368
|
+
state.model = {
|
|
369
|
+
id: "test-model",
|
|
370
|
+
name: "test-model",
|
|
371
|
+
api: "openai-completions",
|
|
372
|
+
provider: "test-provider",
|
|
373
|
+
baseUrl: "http://localhost:1234/v1",
|
|
374
|
+
reasoning: false,
|
|
375
|
+
input: ["text"],
|
|
376
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
377
|
+
contextWindow: 8192,
|
|
378
|
+
maxTokens: 4096,
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
const docsTools = [
|
|
382
|
+
{
|
|
383
|
+
name: "docs__search",
|
|
384
|
+
description: "[MCP docs] Search the docs",
|
|
385
|
+
parameters: Type.Object({}),
|
|
386
|
+
},
|
|
387
|
+
{
|
|
388
|
+
name: "docs__fetch",
|
|
389
|
+
description: "[MCP docs] Fetch a page",
|
|
390
|
+
parameters: Type.Object({}),
|
|
391
|
+
},
|
|
392
|
+
];
|
|
393
|
+
const docsHandlers = new Map();
|
|
394
|
+
let disconnectCalls = 0;
|
|
395
|
+
let connectCalls = 0;
|
|
396
|
+
|
|
397
|
+
state.mcpServers = [
|
|
398
|
+
{
|
|
399
|
+
name: "docs",
|
|
400
|
+
url: "http://docs.test/mcp",
|
|
401
|
+
enabled: true,
|
|
402
|
+
connected: true,
|
|
403
|
+
tools: docsTools,
|
|
404
|
+
toolHandlers: docsHandlers,
|
|
405
|
+
close: async () => {
|
|
406
|
+
disconnectCalls += 1;
|
|
407
|
+
},
|
|
408
|
+
},
|
|
409
|
+
];
|
|
410
|
+
state.settings = saveSettings(state.settingsPath, {
|
|
411
|
+
mcp: {
|
|
412
|
+
servers: [
|
|
413
|
+
{
|
|
414
|
+
name: "docs",
|
|
415
|
+
url: "http://docs.test/mcp",
|
|
416
|
+
enabled: true,
|
|
417
|
+
},
|
|
418
|
+
{
|
|
419
|
+
name: "down",
|
|
420
|
+
url: "http://down.test/mcp",
|
|
421
|
+
enabled: false,
|
|
422
|
+
},
|
|
423
|
+
],
|
|
424
|
+
},
|
|
425
|
+
});
|
|
426
|
+
const runtimeState = { overlay: null as ActiveOverlay | null };
|
|
427
|
+
const appended: Array<{
|
|
428
|
+
text: string;
|
|
429
|
+
sessionId: string | null;
|
|
430
|
+
}> = [];
|
|
431
|
+
const controller = createCommandController(
|
|
432
|
+
{
|
|
433
|
+
openOverlay: (nextOverlay) => {
|
|
434
|
+
runtimeState.overlay = nextOverlay;
|
|
435
|
+
},
|
|
436
|
+
dismissOverlay: () => {
|
|
437
|
+
runtimeState.overlay = null;
|
|
438
|
+
},
|
|
439
|
+
setInputValue: () => {},
|
|
440
|
+
appendInfoMessage: (text, nextState) => {
|
|
441
|
+
appended.push({ text, sessionId: nextState.session?.id ?? null });
|
|
442
|
+
},
|
|
443
|
+
appendTodoMessage: () => {},
|
|
444
|
+
scrollConversationToBottom: () => {},
|
|
445
|
+
requestRender: () => {},
|
|
446
|
+
reloadPromptContext: async () => {},
|
|
447
|
+
openInBrowser: () => {},
|
|
448
|
+
},
|
|
449
|
+
{
|
|
450
|
+
connectMcpServer: async (server) => {
|
|
451
|
+
connectCalls += 1;
|
|
452
|
+
server.connected = true;
|
|
453
|
+
server.tools = docsTools;
|
|
454
|
+
server.toolHandlers = docsHandlers;
|
|
455
|
+
server.close = async () => {
|
|
456
|
+
disconnectCalls += 1;
|
|
457
|
+
};
|
|
458
|
+
return [];
|
|
459
|
+
},
|
|
460
|
+
disconnectMcpServer: async (server) => {
|
|
461
|
+
await server.close();
|
|
462
|
+
server.connected = false;
|
|
463
|
+
server.tools = [];
|
|
464
|
+
server.toolHandlers = new Map();
|
|
465
|
+
server.close = async () => {};
|
|
466
|
+
},
|
|
467
|
+
},
|
|
468
|
+
);
|
|
469
|
+
|
|
470
|
+
try {
|
|
471
|
+
expect(controller.handleCommand("mcp", state)).toBe(true);
|
|
472
|
+
const firstOverlay = expectOverlay(runtimeState.overlay);
|
|
473
|
+
expect(firstOverlay.title).toBe("Toggle MCP servers");
|
|
474
|
+
expect(buildToolList(state).tools.map((tool) => tool.name)).toContain(
|
|
475
|
+
"docs__search",
|
|
476
|
+
);
|
|
477
|
+
renderSelect(firstOverlay).props.onKeyPress?.("enter");
|
|
478
|
+
await Promise.resolve();
|
|
479
|
+
await Promise.resolve();
|
|
480
|
+
|
|
481
|
+
expect(runtimeState.overlay).toBeNull();
|
|
482
|
+
expect(state.mcpServers[0]?.enabled).toBe(false);
|
|
483
|
+
expect(state.mcpServers[0]?.connected).toBe(false);
|
|
484
|
+
expect(state.settings.mcp).toEqual({
|
|
485
|
+
servers: [
|
|
486
|
+
{
|
|
487
|
+
name: "docs",
|
|
488
|
+
url: "http://docs.test/mcp",
|
|
489
|
+
enabled: false,
|
|
490
|
+
},
|
|
491
|
+
{
|
|
492
|
+
name: "down",
|
|
493
|
+
url: "http://down.test/mcp",
|
|
494
|
+
enabled: false,
|
|
495
|
+
},
|
|
496
|
+
],
|
|
497
|
+
});
|
|
498
|
+
expect(loadSettings(state.settingsPath)).toEqual({
|
|
499
|
+
mcp: {
|
|
500
|
+
servers: [
|
|
501
|
+
{
|
|
502
|
+
name: "docs",
|
|
503
|
+
url: "http://docs.test/mcp",
|
|
504
|
+
enabled: false,
|
|
505
|
+
},
|
|
506
|
+
{
|
|
507
|
+
name: "down",
|
|
508
|
+
url: "http://down.test/mcp",
|
|
509
|
+
enabled: false,
|
|
510
|
+
},
|
|
511
|
+
],
|
|
512
|
+
},
|
|
513
|
+
});
|
|
514
|
+
expect(disconnectCalls).toBe(1);
|
|
515
|
+
expect(appended).toEqual([
|
|
516
|
+
{
|
|
517
|
+
text: 'Disabled MCP server "docs" (-2 tools).',
|
|
518
|
+
sessionId: null,
|
|
519
|
+
},
|
|
520
|
+
]);
|
|
521
|
+
expect(buildToolList(state).tools.map((tool) => tool.name)).not.toContain(
|
|
522
|
+
"docs__search",
|
|
523
|
+
);
|
|
524
|
+
|
|
525
|
+
expect(controller.handleCommand("mcp", state)).toBe(true);
|
|
526
|
+
const secondOverlay = expectOverlay(runtimeState.overlay);
|
|
527
|
+
renderSelect(secondOverlay).props.onKeyPress?.("enter");
|
|
528
|
+
await Promise.resolve();
|
|
529
|
+
await Promise.resolve();
|
|
530
|
+
|
|
531
|
+
expect(state.mcpServers[0]?.enabled).toBe(true);
|
|
532
|
+
expect(state.mcpServers[0]?.connected).toBe(true);
|
|
533
|
+
expect(connectCalls).toBe(1);
|
|
534
|
+
expect(loadSettings(state.settingsPath)).toEqual({
|
|
535
|
+
mcp: {
|
|
536
|
+
servers: [
|
|
537
|
+
{
|
|
538
|
+
name: "docs",
|
|
539
|
+
url: "http://docs.test/mcp",
|
|
540
|
+
enabled: true,
|
|
541
|
+
},
|
|
542
|
+
{
|
|
543
|
+
name: "down",
|
|
544
|
+
url: "http://down.test/mcp",
|
|
545
|
+
enabled: false,
|
|
546
|
+
},
|
|
547
|
+
],
|
|
548
|
+
},
|
|
549
|
+
});
|
|
550
|
+
expect(appended[1]).toEqual({
|
|
551
|
+
text: 'Enabled MCP server "docs" (+2 tools).',
|
|
552
|
+
sessionId: null,
|
|
553
|
+
});
|
|
554
|
+
expect(buildToolList(state).tools.map((tool) => tool.name)).toContain(
|
|
555
|
+
"docs__search",
|
|
556
|
+
);
|
|
557
|
+
} finally {
|
|
558
|
+
state.db.close();
|
|
559
|
+
}
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
test("/mcp toggles repo-local servers for the current run without persisting a global shadow copy", async () => {
|
|
563
|
+
const state = createTestState();
|
|
564
|
+
state.model = {
|
|
565
|
+
id: "test-model",
|
|
566
|
+
name: "test-model",
|
|
567
|
+
api: "openai-completions",
|
|
568
|
+
provider: "test-provider",
|
|
569
|
+
baseUrl: "http://localhost:1234/v1",
|
|
570
|
+
reasoning: false,
|
|
571
|
+
input: ["text"],
|
|
572
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
573
|
+
contextWindow: 8192,
|
|
574
|
+
maxTokens: 4096,
|
|
575
|
+
};
|
|
576
|
+
|
|
577
|
+
const docsTools = [
|
|
578
|
+
{
|
|
579
|
+
name: "docs__search",
|
|
580
|
+
description: "[MCP docs] Search the docs",
|
|
581
|
+
parameters: Type.Object({}),
|
|
582
|
+
},
|
|
583
|
+
];
|
|
584
|
+
const docsHandlers = new Map();
|
|
585
|
+
let disconnectCalls = 0;
|
|
586
|
+
let connectCalls = 0;
|
|
587
|
+
|
|
588
|
+
state.mcpServers = [
|
|
589
|
+
{
|
|
590
|
+
name: "docs",
|
|
591
|
+
url: "http://repo-docs.test/mcp",
|
|
592
|
+
enabled: true,
|
|
593
|
+
connected: true,
|
|
594
|
+
tools: docsTools,
|
|
595
|
+
toolHandlers: docsHandlers,
|
|
596
|
+
close: async () => {
|
|
597
|
+
disconnectCalls += 1;
|
|
598
|
+
},
|
|
599
|
+
},
|
|
600
|
+
];
|
|
601
|
+
state.settings = saveSettings(state.settingsPath, {
|
|
602
|
+
mcp: {
|
|
603
|
+
servers: [
|
|
604
|
+
{
|
|
605
|
+
name: "docs",
|
|
606
|
+
url: "http://global-docs.test/mcp",
|
|
607
|
+
enabled: false,
|
|
608
|
+
},
|
|
609
|
+
{
|
|
610
|
+
name: "down",
|
|
611
|
+
url: "http://down.test/mcp",
|
|
612
|
+
enabled: false,
|
|
613
|
+
},
|
|
614
|
+
],
|
|
615
|
+
},
|
|
616
|
+
});
|
|
617
|
+
state.repoSettings = {
|
|
618
|
+
mcp: {
|
|
619
|
+
servers: [
|
|
620
|
+
{
|
|
621
|
+
name: "docs",
|
|
622
|
+
url: "http://repo-docs.test/mcp",
|
|
623
|
+
enabled: true,
|
|
624
|
+
},
|
|
625
|
+
],
|
|
626
|
+
},
|
|
627
|
+
};
|
|
628
|
+
const runtimeState = { overlay: null as ActiveOverlay | null };
|
|
629
|
+
const appended: Array<{
|
|
630
|
+
text: string;
|
|
631
|
+
sessionId: string | null;
|
|
632
|
+
}> = [];
|
|
633
|
+
const controller = createCommandController(
|
|
634
|
+
{
|
|
635
|
+
openOverlay: (nextOverlay) => {
|
|
636
|
+
runtimeState.overlay = nextOverlay;
|
|
637
|
+
},
|
|
638
|
+
dismissOverlay: () => {
|
|
639
|
+
runtimeState.overlay = null;
|
|
640
|
+
},
|
|
641
|
+
setInputValue: () => {},
|
|
642
|
+
appendInfoMessage: (text, nextState) => {
|
|
643
|
+
appended.push({ text, sessionId: nextState.session?.id ?? null });
|
|
644
|
+
},
|
|
645
|
+
appendTodoMessage: () => {},
|
|
646
|
+
scrollConversationToBottom: () => {},
|
|
647
|
+
requestRender: () => {},
|
|
648
|
+
reloadPromptContext: async () => {},
|
|
649
|
+
openInBrowser: () => {},
|
|
650
|
+
},
|
|
651
|
+
{
|
|
652
|
+
connectMcpServer: async (server) => {
|
|
653
|
+
connectCalls += 1;
|
|
654
|
+
server.connected = true;
|
|
655
|
+
server.tools = docsTools;
|
|
656
|
+
server.toolHandlers = docsHandlers;
|
|
657
|
+
server.close = async () => {
|
|
658
|
+
disconnectCalls += 1;
|
|
659
|
+
};
|
|
660
|
+
return [];
|
|
661
|
+
},
|
|
662
|
+
disconnectMcpServer: async (server) => {
|
|
663
|
+
await server.close();
|
|
664
|
+
server.connected = false;
|
|
665
|
+
server.tools = [];
|
|
666
|
+
server.toolHandlers = new Map();
|
|
667
|
+
server.close = async () => {};
|
|
668
|
+
},
|
|
669
|
+
},
|
|
670
|
+
);
|
|
671
|
+
|
|
672
|
+
try {
|
|
673
|
+
expect(controller.handleCommand("mcp", state)).toBe(true);
|
|
674
|
+
renderSelect(expectOverlay(runtimeState.overlay)).props.onKeyPress?.(
|
|
675
|
+
"enter",
|
|
676
|
+
);
|
|
677
|
+
await Promise.resolve();
|
|
678
|
+
await Promise.resolve();
|
|
679
|
+
|
|
680
|
+
expect(state.mcpServers[0]?.enabled).toBe(false);
|
|
681
|
+
expect(state.mcpServers[0]?.connected).toBe(false);
|
|
682
|
+
expect(state.settings).toEqual({
|
|
683
|
+
mcp: {
|
|
684
|
+
servers: [
|
|
685
|
+
{
|
|
686
|
+
name: "docs",
|
|
687
|
+
url: "http://global-docs.test/mcp",
|
|
688
|
+
enabled: false,
|
|
689
|
+
},
|
|
690
|
+
{
|
|
691
|
+
name: "down",
|
|
692
|
+
url: "http://down.test/mcp",
|
|
693
|
+
enabled: false,
|
|
694
|
+
},
|
|
695
|
+
],
|
|
696
|
+
},
|
|
697
|
+
});
|
|
698
|
+
expect(loadSettings(state.settingsPath)).toEqual(state.settings);
|
|
699
|
+
expect(disconnectCalls).toBe(1);
|
|
700
|
+
|
|
701
|
+
expect(controller.handleCommand("mcp", state)).toBe(true);
|
|
702
|
+
renderSelect(expectOverlay(runtimeState.overlay)).props.onKeyPress?.(
|
|
703
|
+
"enter",
|
|
704
|
+
);
|
|
705
|
+
await Promise.resolve();
|
|
706
|
+
await Promise.resolve();
|
|
707
|
+
|
|
708
|
+
expect(state.mcpServers[0]?.enabled).toBe(true);
|
|
709
|
+
expect(state.mcpServers[0]?.connected).toBe(true);
|
|
710
|
+
expect(connectCalls).toBe(1);
|
|
711
|
+
expect(state.settings).toEqual({
|
|
712
|
+
mcp: {
|
|
713
|
+
servers: [
|
|
714
|
+
{
|
|
715
|
+
name: "docs",
|
|
716
|
+
url: "http://global-docs.test/mcp",
|
|
717
|
+
enabled: false,
|
|
718
|
+
},
|
|
719
|
+
{
|
|
720
|
+
name: "down",
|
|
721
|
+
url: "http://down.test/mcp",
|
|
722
|
+
enabled: false,
|
|
723
|
+
},
|
|
724
|
+
],
|
|
725
|
+
},
|
|
726
|
+
});
|
|
727
|
+
expect(loadSettings(state.settingsPath)).toEqual(state.settings);
|
|
728
|
+
expect(appended).toEqual([
|
|
729
|
+
{
|
|
730
|
+
text: 'Disabled MCP server "docs" (-1 tool).',
|
|
731
|
+
sessionId: null,
|
|
732
|
+
},
|
|
733
|
+
{
|
|
734
|
+
text: 'Enabled MCP server "docs" (+1 tool).',
|
|
735
|
+
sessionId: null,
|
|
736
|
+
},
|
|
737
|
+
]);
|
|
738
|
+
} finally {
|
|
739
|
+
state.db.close();
|
|
740
|
+
}
|
|
741
|
+
});
|
|
742
|
+
|
|
743
|
+
test("/help appends a markdown info message without creating a session", () => {
|
|
744
|
+
const state = createTestState();
|
|
745
|
+
state.mcpServers = [
|
|
746
|
+
{
|
|
747
|
+
name: "docs",
|
|
748
|
+
url: "http://docs.test/mcp",
|
|
749
|
+
enabled: false,
|
|
750
|
+
connected: false,
|
|
751
|
+
tools: [],
|
|
752
|
+
toolHandlers: new Map(),
|
|
753
|
+
close: async () => {},
|
|
754
|
+
},
|
|
755
|
+
];
|
|
289
756
|
const appended: Array<{
|
|
290
757
|
text: string;
|
|
291
758
|
format: string | undefined;
|
|
@@ -304,7 +771,7 @@ describe("ui/commands", () => {
|
|
|
304
771
|
},
|
|
305
772
|
appendTodoMessage: () => {},
|
|
306
773
|
scrollConversationToBottom: () => {},
|
|
307
|
-
|
|
774
|
+
requestRender: () => {},
|
|
308
775
|
reloadPromptContext: async () => {},
|
|
309
776
|
openInBrowser: () => {},
|
|
310
777
|
});
|
|
@@ -312,8 +779,13 @@ describe("ui/commands", () => {
|
|
|
312
779
|
try {
|
|
313
780
|
expect(controller.handleCommand("help", state)).toBe(true);
|
|
314
781
|
expect(appended).toHaveLength(1);
|
|
315
|
-
expect(appended[0]?.text).
|
|
316
|
-
expect(appended[0]?.text).toContain("
|
|
782
|
+
expect(appended[0]?.text.length).toBeGreaterThan(0);
|
|
783
|
+
expect(appended[0]?.text).toContain("`/mcp`");
|
|
784
|
+
expect(appended[0]?.text).toContain("`/skill:name`");
|
|
785
|
+
expect(appended[0]?.text).toContain(
|
|
786
|
+
"submit `/skill` to open the skill picker",
|
|
787
|
+
);
|
|
788
|
+
expect(appended[0]?.text).toContain("`docs` (off)");
|
|
317
789
|
expect(appended[0]?.format).toBe("markdown");
|
|
318
790
|
expect(appended[0]?.sessionId).toBeNull();
|
|
319
791
|
expect(state.session).toBeNull();
|
|
@@ -360,7 +832,7 @@ describe("ui/commands", () => {
|
|
|
360
832
|
});
|
|
361
833
|
},
|
|
362
834
|
scrollConversationToBottom: () => {},
|
|
363
|
-
|
|
835
|
+
requestRender: () => {},
|
|
364
836
|
reloadPromptContext: async () => {},
|
|
365
837
|
openInBrowser: () => {},
|
|
366
838
|
});
|
|
@@ -393,7 +865,7 @@ describe("ui/commands", () => {
|
|
|
393
865
|
appendInfoMessage: () => {},
|
|
394
866
|
appendTodoMessage: () => {},
|
|
395
867
|
scrollConversationToBottom: () => {},
|
|
396
|
-
|
|
868
|
+
requestRender: () => {},
|
|
397
869
|
reloadPromptContext: async () => {},
|
|
398
870
|
openInBrowser: () => {},
|
|
399
871
|
});
|
|
@@ -421,7 +893,7 @@ describe("ui/commands", () => {
|
|
|
421
893
|
appendInfoMessage: () => {},
|
|
422
894
|
appendTodoMessage: () => {},
|
|
423
895
|
scrollConversationToBottom: () => {},
|
|
424
|
-
|
|
896
|
+
requestRender: () => {},
|
|
425
897
|
reloadPromptContext: async () => {},
|
|
426
898
|
openInBrowser: () => {},
|
|
427
899
|
});
|