mini-coder 0.4.1 → 0.5.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/README.md +89 -48
- package/assets/icon-1-minimal.svg +31 -0
- package/assets/icon-2-dark-terminal.svg +48 -0
- package/assets/icon-3-gradient-modern.svg +45 -0
- package/assets/icon-4-filled-bold.svg +54 -0
- package/assets/icon-5-community-badge.svg +63 -0
- package/assets/preview-0-5-0.png +0 -0
- package/assets/preview.gif +0 -0
- package/bin/mc.ts +14 -0
- package/bun.lock +438 -0
- package/package.json +12 -29
- package/src/agent.ts +640 -0
- package/src/cli.ts +124 -0
- package/src/git.ts +171 -0
- package/src/headless.ts +140 -0
- package/src/index.ts +666 -0
- package/src/input.ts +155 -0
- package/src/paths.ts +37 -0
- package/src/plugins.ts +183 -0
- package/src/prompt.ts +301 -0
- package/src/session.ts +1043 -0
- package/src/settings.ts +191 -0
- package/src/skills.ts +262 -0
- package/src/submit.ts +323 -0
- package/src/theme.ts +147 -0
- package/src/tools.ts +636 -0
- package/src/ui/agent.test.ts +49 -0
- package/src/ui/agent.ts +210 -0
- package/src/ui/commands.test.ts +610 -0
- package/src/ui/commands.ts +638 -0
- package/src/ui/conversation.test.ts +892 -0
- package/src/ui/conversation.ts +926 -0
- package/src/ui/help.test.ts +44 -0
- package/src/ui/help.ts +125 -0
- package/src/ui/input.test.ts +74 -0
- package/src/ui/input.ts +138 -0
- package/src/ui/overlay.test.ts +42 -0
- package/src/ui/overlay.ts +59 -0
- package/src/ui/status.test.ts +451 -0
- package/src/ui/status.ts +357 -0
- package/src/ui.ts +694 -0
- package/.claude/settings.local.json +0 -54
- package/.prettierignore +0 -7
- package/dist/mc-edit.js +0 -275
- package/dist/mc.js +0 -7355
- package/docs/KNOWN_ISSUES.md +0 -13
- package/docs/design-decisions.md +0 -31
- package/docs/mini-coder.1.md +0 -227
- package/docs/superpowers/plans/2026-03-30-anthropic-oauth-removal.md +0 -61
- package/docs/superpowers/specs/2026-03-30-anthropic-oauth-removal-design.md +0 -47
- package/lefthook.yml +0 -4
|
@@ -0,0 +1,610 @@
|
|
|
1
|
+
import { afterEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import type { ContainerNode, Node } from "@cel-tui/types";
|
|
6
|
+
import { registerFauxProvider } from "@mariozechner/pi-ai";
|
|
7
|
+
import type { AppState } from "../index.ts";
|
|
8
|
+
import { COMMANDS } from "../input.ts";
|
|
9
|
+
import {
|
|
10
|
+
appendPromptHistory,
|
|
11
|
+
createSession,
|
|
12
|
+
openDatabase,
|
|
13
|
+
} from "../session.ts";
|
|
14
|
+
import { loadSettings } from "../settings.ts";
|
|
15
|
+
import { DEFAULT_THEME } from "../theme.ts";
|
|
16
|
+
import {
|
|
17
|
+
createCommandController,
|
|
18
|
+
formatPromptHistoryLabel,
|
|
19
|
+
formatPromptHistoryPreview,
|
|
20
|
+
formatRelativeDate,
|
|
21
|
+
formatSessionLabel,
|
|
22
|
+
} from "./commands.ts";
|
|
23
|
+
import type { ActiveOverlay } from "./overlay.ts";
|
|
24
|
+
|
|
25
|
+
function collectText(node: Node | null): string[] {
|
|
26
|
+
if (!node) {
|
|
27
|
+
return [];
|
|
28
|
+
}
|
|
29
|
+
if (node.type === "text") {
|
|
30
|
+
return [node.content];
|
|
31
|
+
}
|
|
32
|
+
if (node.type === "textinput") {
|
|
33
|
+
return [];
|
|
34
|
+
}
|
|
35
|
+
return node.children.flatMap((child) => collectText(child));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function expectOverlay(overlay: ActiveOverlay | null): ActiveOverlay {
|
|
39
|
+
if (!overlay) {
|
|
40
|
+
throw new Error("Expected an active overlay");
|
|
41
|
+
}
|
|
42
|
+
return overlay;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function renderSelect(overlay: ActiveOverlay): ContainerNode {
|
|
46
|
+
return overlay.select();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function createDeferred<T = void>() {
|
|
50
|
+
let resolve!: (value: T | PromiseLike<T>) => void;
|
|
51
|
+
let reject!: (reason?: unknown) => void;
|
|
52
|
+
const promise = new Promise<T>((resolvePromise, rejectPromise) => {
|
|
53
|
+
resolve = resolvePromise;
|
|
54
|
+
reject = rejectPromise;
|
|
55
|
+
});
|
|
56
|
+
return { promise, resolve, reject };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const tempDirs: string[] = [];
|
|
60
|
+
|
|
61
|
+
function createTempDir(): string {
|
|
62
|
+
const dir = mkdtempSync(join(tmpdir(), "mini-coder-ui-commands-test-"));
|
|
63
|
+
tempDirs.push(dir);
|
|
64
|
+
return dir;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function createTestState(): AppState {
|
|
68
|
+
const db = openDatabase(":memory:");
|
|
69
|
+
const cwd = "/tmp/mini-coder-ui-commands-test";
|
|
70
|
+
const settingsPath = join(createTempDir(), "settings.json");
|
|
71
|
+
return {
|
|
72
|
+
db,
|
|
73
|
+
session: null,
|
|
74
|
+
model: null,
|
|
75
|
+
effort: "medium",
|
|
76
|
+
messages: [],
|
|
77
|
+
stats: { totalInput: 0, totalOutput: 0, totalCost: 0 },
|
|
78
|
+
agentsMd: [],
|
|
79
|
+
skills: [],
|
|
80
|
+
plugins: [],
|
|
81
|
+
theme: DEFAULT_THEME,
|
|
82
|
+
git: null,
|
|
83
|
+
providers: new Map(),
|
|
84
|
+
oauthCredentials: {},
|
|
85
|
+
settings: {},
|
|
86
|
+
settingsPath,
|
|
87
|
+
cwd,
|
|
88
|
+
canonicalCwd: cwd,
|
|
89
|
+
running: false,
|
|
90
|
+
abortController: null,
|
|
91
|
+
activeTurnPromise: null,
|
|
92
|
+
showReasoning: true,
|
|
93
|
+
verbose: false,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
afterEach(() => {
|
|
98
|
+
for (const dir of tempDirs.splice(0)) {
|
|
99
|
+
rmSync(dir, { recursive: true, force: true });
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
describe("ui/commands", () => {
|
|
104
|
+
test("showCommandAutocomplete clears the current draft and opens the commands overlay", () => {
|
|
105
|
+
const state = createTestState();
|
|
106
|
+
const runtimeState = { overlay: null as ActiveOverlay | null };
|
|
107
|
+
let inputValue = "draft";
|
|
108
|
+
const controller = createCommandController({
|
|
109
|
+
openOverlay: (nextOverlay) => {
|
|
110
|
+
runtimeState.overlay = nextOverlay;
|
|
111
|
+
},
|
|
112
|
+
dismissOverlay: () => {
|
|
113
|
+
runtimeState.overlay = null;
|
|
114
|
+
},
|
|
115
|
+
setInputValue: (value) => {
|
|
116
|
+
inputValue = value;
|
|
117
|
+
},
|
|
118
|
+
appendInfoMessage: () => {},
|
|
119
|
+
scrollConversationToBottom: () => {},
|
|
120
|
+
render: () => {},
|
|
121
|
+
reloadPromptContext: async () => {},
|
|
122
|
+
openInBrowser: () => {},
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
try {
|
|
126
|
+
controller.showCommandAutocomplete(state);
|
|
127
|
+
|
|
128
|
+
const overlay = expectOverlay(runtimeState.overlay);
|
|
129
|
+
|
|
130
|
+
expect(inputValue).toBe("");
|
|
131
|
+
expect(overlay.title).toBe("Commands");
|
|
132
|
+
|
|
133
|
+
const text = collectText(overlay.select());
|
|
134
|
+
for (const command of COMMANDS) {
|
|
135
|
+
expect(text.some((line) => line.includes(`/${command}`))).toBe(true);
|
|
136
|
+
}
|
|
137
|
+
} finally {
|
|
138
|
+
state.db.close();
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test("showInputHistoryOverlay restores the selected raw prompt", () => {
|
|
143
|
+
const state = createTestState();
|
|
144
|
+
const runtimeState = { overlay: null as ActiveOverlay | null };
|
|
145
|
+
let inputValue = "draft";
|
|
146
|
+
const rawPrompt = "first line\nsecond line";
|
|
147
|
+
const controller = createCommandController({
|
|
148
|
+
openOverlay: (nextOverlay) => {
|
|
149
|
+
runtimeState.overlay = nextOverlay;
|
|
150
|
+
},
|
|
151
|
+
dismissOverlay: () => {
|
|
152
|
+
runtimeState.overlay = null;
|
|
153
|
+
},
|
|
154
|
+
setInputValue: (value) => {
|
|
155
|
+
inputValue = value;
|
|
156
|
+
},
|
|
157
|
+
appendInfoMessage: () => {},
|
|
158
|
+
scrollConversationToBottom: () => {},
|
|
159
|
+
render: () => {},
|
|
160
|
+
reloadPromptContext: async () => {},
|
|
161
|
+
openInBrowser: () => {},
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
try {
|
|
165
|
+
appendPromptHistory(state.db, {
|
|
166
|
+
text: "older prompt",
|
|
167
|
+
cwd: "/tmp/older",
|
|
168
|
+
});
|
|
169
|
+
appendPromptHistory(state.db, { text: rawPrompt, cwd: state.cwd });
|
|
170
|
+
|
|
171
|
+
controller.showInputHistoryOverlay(state);
|
|
172
|
+
|
|
173
|
+
const overlay = expectOverlay(runtimeState.overlay);
|
|
174
|
+
|
|
175
|
+
expect(overlay.title).toBe("Input history");
|
|
176
|
+
|
|
177
|
+
const selectNode = renderSelect(overlay);
|
|
178
|
+
|
|
179
|
+
selectNode.props.onKeyPress?.("enter");
|
|
180
|
+
|
|
181
|
+
expect(runtimeState.overlay).toBeNull();
|
|
182
|
+
expect(inputValue).toBe(rawPrompt);
|
|
183
|
+
} finally {
|
|
184
|
+
state.db.close();
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
test("showInputHistoryOverlay dismissal leaves the current draft unchanged", () => {
|
|
189
|
+
const state = createTestState();
|
|
190
|
+
const runtimeState = { overlay: null as ActiveOverlay | null };
|
|
191
|
+
let inputValue = "draft prompt";
|
|
192
|
+
const controller = createCommandController({
|
|
193
|
+
openOverlay: (nextOverlay) => {
|
|
194
|
+
runtimeState.overlay = nextOverlay;
|
|
195
|
+
},
|
|
196
|
+
dismissOverlay: () => {
|
|
197
|
+
runtimeState.overlay = null;
|
|
198
|
+
},
|
|
199
|
+
setInputValue: (value) => {
|
|
200
|
+
inputValue = value;
|
|
201
|
+
},
|
|
202
|
+
appendInfoMessage: () => {},
|
|
203
|
+
scrollConversationToBottom: () => {},
|
|
204
|
+
render: () => {},
|
|
205
|
+
reloadPromptContext: async () => {},
|
|
206
|
+
openInBrowser: () => {},
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
try {
|
|
210
|
+
appendPromptHistory(state.db, {
|
|
211
|
+
text: "saved prompt",
|
|
212
|
+
cwd: state.cwd,
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
controller.showInputHistoryOverlay(state);
|
|
216
|
+
|
|
217
|
+
const overlay = expectOverlay(runtimeState.overlay);
|
|
218
|
+
const selectNode = renderSelect(overlay);
|
|
219
|
+
|
|
220
|
+
selectNode.props.onBlur?.();
|
|
221
|
+
|
|
222
|
+
expect(runtimeState.overlay).toBeNull();
|
|
223
|
+
expect(inputValue).toBe("draft prompt");
|
|
224
|
+
} finally {
|
|
225
|
+
state.db.close();
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
test("handleCommand('session') overlay includes the first user preview for disambiguation", () => {
|
|
230
|
+
const state = createTestState();
|
|
231
|
+
const runtimeState = { overlay: null as ActiveOverlay | null };
|
|
232
|
+
const controller = createCommandController({
|
|
233
|
+
openOverlay: (nextOverlay) => {
|
|
234
|
+
runtimeState.overlay = nextOverlay;
|
|
235
|
+
},
|
|
236
|
+
dismissOverlay: () => {
|
|
237
|
+
runtimeState.overlay = null;
|
|
238
|
+
},
|
|
239
|
+
setInputValue: () => {},
|
|
240
|
+
appendInfoMessage: () => {},
|
|
241
|
+
scrollConversationToBottom: () => {},
|
|
242
|
+
render: () => {},
|
|
243
|
+
reloadPromptContext: async () => {},
|
|
244
|
+
openInBrowser: () => {},
|
|
245
|
+
});
|
|
246
|
+
const session = createSession(state.db, {
|
|
247
|
+
cwd: state.canonicalCwd,
|
|
248
|
+
model: "test/beta",
|
|
249
|
+
effort: "high",
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
state.db.run(
|
|
253
|
+
"INSERT INTO messages (session_id, turn, data, created_at) VALUES (?, ?, ?, ?)",
|
|
254
|
+
[
|
|
255
|
+
session.id,
|
|
256
|
+
null,
|
|
257
|
+
JSON.stringify({
|
|
258
|
+
role: "ui",
|
|
259
|
+
kind: "info",
|
|
260
|
+
content: "Help output",
|
|
261
|
+
timestamp: 1,
|
|
262
|
+
}),
|
|
263
|
+
1,
|
|
264
|
+
],
|
|
265
|
+
);
|
|
266
|
+
state.db.run(
|
|
267
|
+
"INSERT INTO messages (session_id, turn, data, created_at) VALUES (?, ?, ?, ?)",
|
|
268
|
+
[
|
|
269
|
+
session.id,
|
|
270
|
+
1,
|
|
271
|
+
JSON.stringify({
|
|
272
|
+
role: "user",
|
|
273
|
+
content: "Investigate\nthis session label",
|
|
274
|
+
timestamp: 2,
|
|
275
|
+
}),
|
|
276
|
+
2,
|
|
277
|
+
],
|
|
278
|
+
);
|
|
279
|
+
|
|
280
|
+
try {
|
|
281
|
+
expect(controller.handleCommand("session", state)).toBe(true);
|
|
282
|
+
|
|
283
|
+
const overlay = expectOverlay(runtimeState.overlay);
|
|
284
|
+
const text = collectText(renderSelect(overlay));
|
|
285
|
+
|
|
286
|
+
expect(
|
|
287
|
+
text.some((line) => line.includes("Investigate this session")),
|
|
288
|
+
).toBe(true);
|
|
289
|
+
} finally {
|
|
290
|
+
state.db.close();
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
test("handleCommand('session') restores the selected session and recomputes stats", () => {
|
|
295
|
+
const state = createTestState();
|
|
296
|
+
const runtimeState = { overlay: null as ActiveOverlay | null };
|
|
297
|
+
let scrollCalls = 0;
|
|
298
|
+
const controller = createCommandController({
|
|
299
|
+
openOverlay: (nextOverlay) => {
|
|
300
|
+
runtimeState.overlay = nextOverlay;
|
|
301
|
+
},
|
|
302
|
+
dismissOverlay: () => {
|
|
303
|
+
runtimeState.overlay = null;
|
|
304
|
+
},
|
|
305
|
+
setInputValue: () => {},
|
|
306
|
+
appendInfoMessage: () => {},
|
|
307
|
+
scrollConversationToBottom: () => {
|
|
308
|
+
scrollCalls += 1;
|
|
309
|
+
},
|
|
310
|
+
render: () => {},
|
|
311
|
+
reloadPromptContext: async () => {},
|
|
312
|
+
openInBrowser: () => {},
|
|
313
|
+
});
|
|
314
|
+
const now = new Date("2026-04-07T12:00:00Z").getTime();
|
|
315
|
+
const session = createSession(state.db, {
|
|
316
|
+
cwd: state.canonicalCwd,
|
|
317
|
+
model: "test/beta",
|
|
318
|
+
effort: "high",
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
state.db.run(
|
|
322
|
+
"INSERT INTO messages (session_id, turn, data, created_at) VALUES (?, ?, ?, ?)",
|
|
323
|
+
[
|
|
324
|
+
session.id,
|
|
325
|
+
1,
|
|
326
|
+
JSON.stringify({
|
|
327
|
+
role: "user",
|
|
328
|
+
content: "historical prompt",
|
|
329
|
+
timestamp: now,
|
|
330
|
+
}),
|
|
331
|
+
now,
|
|
332
|
+
],
|
|
333
|
+
);
|
|
334
|
+
state.db.run(
|
|
335
|
+
"INSERT INTO messages (session_id, turn, data, created_at) VALUES (?, ?, ?, ?)",
|
|
336
|
+
[
|
|
337
|
+
session.id,
|
|
338
|
+
1,
|
|
339
|
+
JSON.stringify({
|
|
340
|
+
role: "assistant",
|
|
341
|
+
content: [{ type: "text", text: "historical reply" }],
|
|
342
|
+
api: "anthropic-messages",
|
|
343
|
+
provider: "anthropic",
|
|
344
|
+
model: "test/beta",
|
|
345
|
+
stopReason: "stop",
|
|
346
|
+
timestamp: now + 1,
|
|
347
|
+
}),
|
|
348
|
+
now + 1,
|
|
349
|
+
],
|
|
350
|
+
);
|
|
351
|
+
state.db.run("UPDATE sessions SET updated_at = ? WHERE id = ?", [
|
|
352
|
+
now + 1,
|
|
353
|
+
session.id,
|
|
354
|
+
]);
|
|
355
|
+
|
|
356
|
+
try {
|
|
357
|
+
expect(controller.handleCommand("session", state)).toBe(true);
|
|
358
|
+
|
|
359
|
+
const overlay = expectOverlay(runtimeState.overlay);
|
|
360
|
+
expect(overlay.title).toBe("Resume a session");
|
|
361
|
+
|
|
362
|
+
const selectNode = renderSelect(overlay);
|
|
363
|
+
selectNode.props.onKeyPress?.("enter");
|
|
364
|
+
|
|
365
|
+
expect(runtimeState.overlay).toBeNull();
|
|
366
|
+
expect(scrollCalls).toBe(1);
|
|
367
|
+
expect(state.session?.id).toBe(session.id);
|
|
368
|
+
expect(state.messages.map((message) => message.role)).toEqual([
|
|
369
|
+
"user",
|
|
370
|
+
"assistant",
|
|
371
|
+
]);
|
|
372
|
+
expect(state.stats).toEqual({
|
|
373
|
+
totalInput: 0,
|
|
374
|
+
totalOutput: 0,
|
|
375
|
+
totalCost: 0,
|
|
376
|
+
});
|
|
377
|
+
} finally {
|
|
378
|
+
state.db.close();
|
|
379
|
+
}
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
test("handleCommand returns false for unknown commands", () => {
|
|
383
|
+
const state = createTestState();
|
|
384
|
+
const controller = createCommandController({
|
|
385
|
+
openOverlay: () => {},
|
|
386
|
+
dismissOverlay: () => {},
|
|
387
|
+
setInputValue: () => {},
|
|
388
|
+
appendInfoMessage: () => {},
|
|
389
|
+
scrollConversationToBottom: () => {},
|
|
390
|
+
render: () => {},
|
|
391
|
+
reloadPromptContext: async () => {},
|
|
392
|
+
openInBrowser: () => {},
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
try {
|
|
396
|
+
expect(controller.handleCommand("unknown", state)).toBe(false);
|
|
397
|
+
} finally {
|
|
398
|
+
state.db.close();
|
|
399
|
+
}
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
test("/new clears the active session state and reloads prompt context", async () => {
|
|
403
|
+
const state = createTestState();
|
|
404
|
+
let reloadCount = 0;
|
|
405
|
+
const reloadFinished = createDeferred<void>();
|
|
406
|
+
const controller = createCommandController({
|
|
407
|
+
openOverlay: () => {},
|
|
408
|
+
dismissOverlay: () => {},
|
|
409
|
+
setInputValue: () => {},
|
|
410
|
+
appendInfoMessage: () => {},
|
|
411
|
+
scrollConversationToBottom: () => {},
|
|
412
|
+
render: () => {},
|
|
413
|
+
reloadPromptContext: async (nextState) => {
|
|
414
|
+
reloadCount++;
|
|
415
|
+
nextState.agentsMd = [
|
|
416
|
+
{ path: "/tmp/reloaded/AGENTS.md", content: "Reloaded context" },
|
|
417
|
+
];
|
|
418
|
+
reloadFinished.resolve();
|
|
419
|
+
},
|
|
420
|
+
openInBrowser: () => {},
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
state.session = {
|
|
424
|
+
id: "session-1",
|
|
425
|
+
cwd: state.canonicalCwd,
|
|
426
|
+
model: null,
|
|
427
|
+
effort: state.effort,
|
|
428
|
+
forkedFrom: null,
|
|
429
|
+
createdAt: 1,
|
|
430
|
+
updatedAt: 1,
|
|
431
|
+
};
|
|
432
|
+
state.messages = [
|
|
433
|
+
{ role: "ui", kind: "info", content: "old", timestamp: 1 },
|
|
434
|
+
];
|
|
435
|
+
state.stats = { totalInput: 10, totalOutput: 20, totalCost: 0.5 };
|
|
436
|
+
|
|
437
|
+
try {
|
|
438
|
+
expect(controller.handleCommand("new", state)).toBe(true);
|
|
439
|
+
await reloadFinished.promise;
|
|
440
|
+
|
|
441
|
+
expect(reloadCount).toBe(1);
|
|
442
|
+
expect(state.session).toBeNull();
|
|
443
|
+
expect(state.messages).toEqual([]);
|
|
444
|
+
expect(state.stats).toEqual({
|
|
445
|
+
totalInput: 0,
|
|
446
|
+
totalOutput: 0,
|
|
447
|
+
totalCost: 0,
|
|
448
|
+
});
|
|
449
|
+
expect(state.agentsMd).toEqual([
|
|
450
|
+
{ path: "/tmp/reloaded/AGENTS.md", content: "Reloaded context" },
|
|
451
|
+
]);
|
|
452
|
+
} finally {
|
|
453
|
+
state.db.close();
|
|
454
|
+
}
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
test("/reasoning toggles the UI state and persists it", () => {
|
|
458
|
+
const state = createTestState();
|
|
459
|
+
const controller = createCommandController({
|
|
460
|
+
openOverlay: () => {},
|
|
461
|
+
dismissOverlay: () => {},
|
|
462
|
+
setInputValue: () => {},
|
|
463
|
+
appendInfoMessage: () => {},
|
|
464
|
+
scrollConversationToBottom: () => {},
|
|
465
|
+
render: () => {},
|
|
466
|
+
reloadPromptContext: async () => {},
|
|
467
|
+
openInBrowser: () => {},
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
try {
|
|
471
|
+
expect(controller.handleCommand("reasoning", state)).toBe(true);
|
|
472
|
+
expect(state.showReasoning).toBe(false);
|
|
473
|
+
expect(state.settings.showReasoning).toBe(false);
|
|
474
|
+
expect(loadSettings(state.settingsPath)).toEqual({
|
|
475
|
+
showReasoning: false,
|
|
476
|
+
});
|
|
477
|
+
} finally {
|
|
478
|
+
state.db.close();
|
|
479
|
+
}
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
test("/verbose toggles the UI state and persists it", () => {
|
|
483
|
+
const state = createTestState();
|
|
484
|
+
const controller = createCommandController({
|
|
485
|
+
openOverlay: () => {},
|
|
486
|
+
dismissOverlay: () => {},
|
|
487
|
+
setInputValue: () => {},
|
|
488
|
+
appendInfoMessage: () => {},
|
|
489
|
+
scrollConversationToBottom: () => {},
|
|
490
|
+
render: () => {},
|
|
491
|
+
reloadPromptContext: async () => {},
|
|
492
|
+
openInBrowser: () => {},
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
try {
|
|
496
|
+
expect(controller.handleCommand("verbose", state)).toBe(true);
|
|
497
|
+
expect(state.verbose).toBe(true);
|
|
498
|
+
expect(state.settings.verbose).toBe(true);
|
|
499
|
+
expect(loadSettings(state.settingsPath)).toEqual({ verbose: true });
|
|
500
|
+
} finally {
|
|
501
|
+
state.db.close();
|
|
502
|
+
}
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
test("applyModelSelection updates the state and persists the default model", () => {
|
|
506
|
+
const faux = registerFauxProvider();
|
|
507
|
+
const state = createTestState();
|
|
508
|
+
const model = faux.getModel();
|
|
509
|
+
const controller = createCommandController({
|
|
510
|
+
openOverlay: () => {},
|
|
511
|
+
dismissOverlay: () => {},
|
|
512
|
+
setInputValue: () => {},
|
|
513
|
+
appendInfoMessage: () => {},
|
|
514
|
+
scrollConversationToBottom: () => {},
|
|
515
|
+
render: () => {},
|
|
516
|
+
reloadPromptContext: async () => {},
|
|
517
|
+
openInBrowser: () => {},
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
try {
|
|
521
|
+
controller.applyModelSelection(state, model);
|
|
522
|
+
|
|
523
|
+
expect(state.model).toBe(model);
|
|
524
|
+
expect(state.settings.defaultModel).toBe(`${model.provider}/${model.id}`);
|
|
525
|
+
expect(loadSettings(state.settingsPath)).toEqual({
|
|
526
|
+
defaultModel: `${model.provider}/${model.id}`,
|
|
527
|
+
});
|
|
528
|
+
} finally {
|
|
529
|
+
faux.unregister();
|
|
530
|
+
state.db.close();
|
|
531
|
+
}
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
test("applyEffortSelection updates the active effort and persists the default effort", () => {
|
|
535
|
+
const state = createTestState();
|
|
536
|
+
const controller = createCommandController({
|
|
537
|
+
openOverlay: () => {},
|
|
538
|
+
dismissOverlay: () => {},
|
|
539
|
+
setInputValue: () => {},
|
|
540
|
+
appendInfoMessage: () => {},
|
|
541
|
+
scrollConversationToBottom: () => {},
|
|
542
|
+
render: () => {},
|
|
543
|
+
reloadPromptContext: async () => {},
|
|
544
|
+
openInBrowser: () => {},
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
try {
|
|
548
|
+
controller.applyEffortSelection(state, "xhigh");
|
|
549
|
+
|
|
550
|
+
expect(state.effort).toBe("xhigh");
|
|
551
|
+
expect(state.settings.defaultEffort).toBe("xhigh");
|
|
552
|
+
expect(loadSettings(state.settingsPath)).toEqual({
|
|
553
|
+
defaultEffort: "xhigh",
|
|
554
|
+
});
|
|
555
|
+
} finally {
|
|
556
|
+
state.db.close();
|
|
557
|
+
}
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
test("formatRelativeDate accepts an explicit clock for deterministic output", () => {
|
|
561
|
+
const now = new Date("2026-04-07T12:00:00Z");
|
|
562
|
+
|
|
563
|
+
expect(formatRelativeDate(new Date("2026-04-07T11:59:45Z"), now)).toBe(
|
|
564
|
+
"just now",
|
|
565
|
+
);
|
|
566
|
+
expect(formatRelativeDate(new Date("2026-04-07T11:50:00Z"), now)).toBe(
|
|
567
|
+
"10m ago",
|
|
568
|
+
);
|
|
569
|
+
expect(formatRelativeDate(new Date("2026-04-07T10:00:00Z"), now)).toBe(
|
|
570
|
+
"2h ago",
|
|
571
|
+
);
|
|
572
|
+
expect(formatRelativeDate(new Date("2026-04-04T12:00:00Z"), now)).toBe(
|
|
573
|
+
"3d ago",
|
|
574
|
+
);
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
test("formatPromptHistoryPreview collapses whitespace into one line", () => {
|
|
578
|
+
expect(formatPromptHistoryPreview(" first\n\n second\tthird ")).toBe(
|
|
579
|
+
"first second third",
|
|
580
|
+
);
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
test("formatPromptHistoryLabel_longPromptAndCwd_returnsATruncatedSingleLineLabel", () => {
|
|
584
|
+
expect(
|
|
585
|
+
formatPromptHistoryLabel(
|
|
586
|
+
" Investigate\n\n this prompt history row because it is much too wide for the overlay ",
|
|
587
|
+
"/tmp/projects/very/deeply/nested/mini-coder-audit",
|
|
588
|
+
"5m ago",
|
|
589
|
+
),
|
|
590
|
+
).toBe(
|
|
591
|
+
"Investigate this prompt history… · …/mini-coder-audit · 5m ago",
|
|
592
|
+
);
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
test("formatSessionLabel_longPreviewAndModel_returnsATruncatedReadableLabel", () => {
|
|
596
|
+
expect(
|
|
597
|
+
formatSessionLabel(
|
|
598
|
+
{
|
|
599
|
+
model: "openai-codex/gpt-5.4-super-long-variant",
|
|
600
|
+
firstUserPreview:
|
|
601
|
+
"Audit the session selector because every entry looks identical in real usage",
|
|
602
|
+
},
|
|
603
|
+
"just now",
|
|
604
|
+
true,
|
|
605
|
+
),
|
|
606
|
+
).toBe(
|
|
607
|
+
"Audit the session selector… · openai-codex/gpt… · just now · current",
|
|
608
|
+
);
|
|
609
|
+
});
|
|
610
|
+
});
|