mini-coder 0.5.10 → 0.5.12
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 +408 -0
- package/PROGRESS.md +5 -0
- package/README.md +14 -1
- package/assets/mc-claude-smart.png +0 -0
- package/assets/mc-gpt-smart.png +0 -0
- package/benchmark-loop.sh +19 -0
- package/package.json +1 -1
- package/src/agent.ts +5 -1
- package/src/headless.ts +62 -21
- package/src/index.ts +35 -56
- package/src/prompt.ts +8 -0
- package/src/session-message.ts +393 -0
- package/src/session.ts +102 -396
- package/src/settings.ts +19 -15
- package/src/shared.ts +39 -0
- package/src/submit.ts +3 -25
- package/src/text.ts +71 -0
- package/src/tool-common.ts +91 -0
- package/src/tool-grep.ts +606 -0
- package/src/tool-read.ts +313 -0
- package/src/tool-shell.ts +869 -0
- package/src/tools.ts +186 -995
- package/src/ui/agent.ts +199 -110
- package/src/ui/commands.test.ts +16 -302
- package/src/ui/commands.ts +21 -47
- package/src/ui/conversation.test.ts +263 -1389
- package/src/ui/conversation.ts +496 -151
- package/src/ui/input.test.ts +1 -43
- package/src/ui/runtime.ts +69 -0
- package/src/ui.ts +196 -114
- package/src/ui/agent.test.ts +0 -49
- package/src/ui/help.test.ts +0 -65
- package/src/ui/overlay.test.ts +0 -42
- package/src/ui/render-performance.test.ts +0 -444
- package/src/ui/status.test.ts +0 -489
package/src/ui/commands.test.ts
CHANGED
|
@@ -2,40 +2,19 @@ import { afterEach, describe, expect, test } from "bun:test";
|
|
|
2
2
|
import { mkdtempSync, rmSync } from "node:fs";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
|
-
import type { ContainerNode
|
|
5
|
+
import type { ContainerNode } from "@cel-tui/types";
|
|
6
6
|
import { registerFauxProvider } from "@mariozechner/pi-ai";
|
|
7
7
|
import type { AppState } from "../index.ts";
|
|
8
|
-
import { COMMANDS } from "../input.ts";
|
|
9
8
|
import {
|
|
10
|
-
appendPromptHistory,
|
|
11
9
|
computeContextTokens,
|
|
12
10
|
createSession,
|
|
13
11
|
openDatabase,
|
|
14
12
|
} from "../session.ts";
|
|
15
13
|
import { loadSettings } from "../settings.ts";
|
|
16
14
|
import { DEFAULT_THEME } from "../theme.ts";
|
|
17
|
-
import {
|
|
18
|
-
createCommandController,
|
|
19
|
-
formatPromptHistoryLabel,
|
|
20
|
-
formatPromptHistoryPreview,
|
|
21
|
-
formatRelativeDate,
|
|
22
|
-
formatSessionLabel,
|
|
23
|
-
} from "./commands.ts";
|
|
15
|
+
import { createCommandController } from "./commands.ts";
|
|
24
16
|
import type { ActiveOverlay } from "./overlay.ts";
|
|
25
17
|
|
|
26
|
-
function collectText(node: Node | null): string[] {
|
|
27
|
-
if (!node) {
|
|
28
|
-
return [];
|
|
29
|
-
}
|
|
30
|
-
if (node.type === "text") {
|
|
31
|
-
return [node.content];
|
|
32
|
-
}
|
|
33
|
-
if (node.type === "textinput") {
|
|
34
|
-
return [];
|
|
35
|
-
}
|
|
36
|
-
return node.children.flatMap((child) => collectText(child));
|
|
37
|
-
}
|
|
38
|
-
|
|
39
18
|
function expectOverlay(overlay: ActiveOverlay | null): ActiveOverlay {
|
|
40
19
|
if (!overlay) {
|
|
41
20
|
throw new Error("Expected an active overlay");
|
|
@@ -107,204 +86,11 @@ afterEach(() => {
|
|
|
107
86
|
});
|
|
108
87
|
|
|
109
88
|
describe("ui/commands", () => {
|
|
110
|
-
test("showCommandAutocomplete clears the current draft and opens the commands overlay", () => {
|
|
111
|
-
const state = createTestState();
|
|
112
|
-
const runtimeState = { overlay: null as ActiveOverlay | null };
|
|
113
|
-
let inputValue = "draft";
|
|
114
|
-
const controller = createCommandController({
|
|
115
|
-
openOverlay: (nextOverlay) => {
|
|
116
|
-
runtimeState.overlay = nextOverlay;
|
|
117
|
-
},
|
|
118
|
-
dismissOverlay: () => {
|
|
119
|
-
runtimeState.overlay = null;
|
|
120
|
-
},
|
|
121
|
-
setInputValue: (value) => {
|
|
122
|
-
inputValue = value;
|
|
123
|
-
},
|
|
124
|
-
appendInfoMessage: () => {},
|
|
125
|
-
appendTodoMessage: () => {},
|
|
126
|
-
scrollConversationToBottom: () => {},
|
|
127
|
-
render: () => {},
|
|
128
|
-
reloadPromptContext: async () => {},
|
|
129
|
-
openInBrowser: () => {},
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
try {
|
|
133
|
-
controller.showCommandAutocomplete(state);
|
|
134
|
-
|
|
135
|
-
const overlay = expectOverlay(runtimeState.overlay);
|
|
136
|
-
|
|
137
|
-
expect(inputValue).toBe("");
|
|
138
|
-
expect(overlay.title).toBe("Commands");
|
|
139
|
-
|
|
140
|
-
const text = collectText(overlay.select());
|
|
141
|
-
for (const command of COMMANDS) {
|
|
142
|
-
expect(text.some((line) => line.includes(`/${command}`))).toBe(true);
|
|
143
|
-
}
|
|
144
|
-
} finally {
|
|
145
|
-
state.db.close();
|
|
146
|
-
}
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
test("showInputHistoryOverlay restores the selected raw prompt", () => {
|
|
150
|
-
const state = createTestState();
|
|
151
|
-
const runtimeState = { overlay: null as ActiveOverlay | null };
|
|
152
|
-
let inputValue = "draft";
|
|
153
|
-
const rawPrompt = "first line\nsecond line";
|
|
154
|
-
const controller = createCommandController({
|
|
155
|
-
openOverlay: (nextOverlay) => {
|
|
156
|
-
runtimeState.overlay = nextOverlay;
|
|
157
|
-
},
|
|
158
|
-
dismissOverlay: () => {
|
|
159
|
-
runtimeState.overlay = null;
|
|
160
|
-
},
|
|
161
|
-
setInputValue: (value) => {
|
|
162
|
-
inputValue = value;
|
|
163
|
-
},
|
|
164
|
-
appendInfoMessage: () => {},
|
|
165
|
-
appendTodoMessage: () => {},
|
|
166
|
-
scrollConversationToBottom: () => {},
|
|
167
|
-
render: () => {},
|
|
168
|
-
reloadPromptContext: async () => {},
|
|
169
|
-
openInBrowser: () => {},
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
try {
|
|
173
|
-
appendPromptHistory(state.db, {
|
|
174
|
-
text: "older prompt",
|
|
175
|
-
cwd: "/tmp/older",
|
|
176
|
-
});
|
|
177
|
-
appendPromptHistory(state.db, { text: rawPrompt, cwd: state.cwd });
|
|
178
|
-
|
|
179
|
-
controller.showInputHistoryOverlay(state);
|
|
180
|
-
|
|
181
|
-
const overlay = expectOverlay(runtimeState.overlay);
|
|
182
|
-
|
|
183
|
-
expect(overlay.title).toBe("Input history");
|
|
184
|
-
|
|
185
|
-
const selectNode = renderSelect(overlay);
|
|
186
|
-
|
|
187
|
-
selectNode.props.onKeyPress?.("enter");
|
|
188
|
-
|
|
189
|
-
expect(runtimeState.overlay).toBeNull();
|
|
190
|
-
expect(inputValue).toBe(rawPrompt);
|
|
191
|
-
} finally {
|
|
192
|
-
state.db.close();
|
|
193
|
-
}
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
test("showInputHistoryOverlay dismissal leaves the current draft unchanged", () => {
|
|
197
|
-
const state = createTestState();
|
|
198
|
-
const runtimeState = { overlay: null as ActiveOverlay | null };
|
|
199
|
-
let inputValue = "draft prompt";
|
|
200
|
-
const controller = createCommandController({
|
|
201
|
-
openOverlay: (nextOverlay) => {
|
|
202
|
-
runtimeState.overlay = nextOverlay;
|
|
203
|
-
},
|
|
204
|
-
dismissOverlay: () => {
|
|
205
|
-
runtimeState.overlay = null;
|
|
206
|
-
},
|
|
207
|
-
setInputValue: (value) => {
|
|
208
|
-
inputValue = value;
|
|
209
|
-
},
|
|
210
|
-
appendInfoMessage: () => {},
|
|
211
|
-
appendTodoMessage: () => {},
|
|
212
|
-
scrollConversationToBottom: () => {},
|
|
213
|
-
render: () => {},
|
|
214
|
-
reloadPromptContext: async () => {},
|
|
215
|
-
openInBrowser: () => {},
|
|
216
|
-
});
|
|
217
|
-
|
|
218
|
-
try {
|
|
219
|
-
appendPromptHistory(state.db, {
|
|
220
|
-
text: "saved prompt",
|
|
221
|
-
cwd: state.cwd,
|
|
222
|
-
});
|
|
223
|
-
|
|
224
|
-
controller.showInputHistoryOverlay(state);
|
|
225
|
-
|
|
226
|
-
const overlay = expectOverlay(runtimeState.overlay);
|
|
227
|
-
const selectNode = renderSelect(overlay);
|
|
228
|
-
|
|
229
|
-
selectNode.props.onBlur?.();
|
|
230
|
-
|
|
231
|
-
expect(runtimeState.overlay).toBeNull();
|
|
232
|
-
expect(inputValue).toBe("draft prompt");
|
|
233
|
-
} finally {
|
|
234
|
-
state.db.close();
|
|
235
|
-
}
|
|
236
|
-
});
|
|
237
|
-
|
|
238
|
-
test("handleCommand('session') overlay includes the first user preview for disambiguation", () => {
|
|
239
|
-
const state = createTestState();
|
|
240
|
-
const runtimeState = { overlay: null as ActiveOverlay | null };
|
|
241
|
-
const controller = createCommandController({
|
|
242
|
-
openOverlay: (nextOverlay) => {
|
|
243
|
-
runtimeState.overlay = nextOverlay;
|
|
244
|
-
},
|
|
245
|
-
dismissOverlay: () => {
|
|
246
|
-
runtimeState.overlay = null;
|
|
247
|
-
},
|
|
248
|
-
setInputValue: () => {},
|
|
249
|
-
appendInfoMessage: () => {},
|
|
250
|
-
appendTodoMessage: () => {},
|
|
251
|
-
scrollConversationToBottom: () => {},
|
|
252
|
-
render: () => {},
|
|
253
|
-
reloadPromptContext: async () => {},
|
|
254
|
-
openInBrowser: () => {},
|
|
255
|
-
});
|
|
256
|
-
const session = createSession(state.db, {
|
|
257
|
-
cwd: state.canonicalCwd,
|
|
258
|
-
model: "test/beta",
|
|
259
|
-
effort: "high",
|
|
260
|
-
});
|
|
261
|
-
|
|
262
|
-
state.db.run(
|
|
263
|
-
"INSERT INTO messages (session_id, turn, data, created_at) VALUES (?, ?, ?, ?)",
|
|
264
|
-
[
|
|
265
|
-
session.id,
|
|
266
|
-
null,
|
|
267
|
-
JSON.stringify({
|
|
268
|
-
role: "ui",
|
|
269
|
-
kind: "info",
|
|
270
|
-
content: "Help output",
|
|
271
|
-
timestamp: 1,
|
|
272
|
-
}),
|
|
273
|
-
1,
|
|
274
|
-
],
|
|
275
|
-
);
|
|
276
|
-
state.db.run(
|
|
277
|
-
"INSERT INTO messages (session_id, turn, data, created_at) VALUES (?, ?, ?, ?)",
|
|
278
|
-
[
|
|
279
|
-
session.id,
|
|
280
|
-
1,
|
|
281
|
-
JSON.stringify({
|
|
282
|
-
role: "user",
|
|
283
|
-
content: "Investigate\nthis session label",
|
|
284
|
-
timestamp: 2,
|
|
285
|
-
}),
|
|
286
|
-
2,
|
|
287
|
-
],
|
|
288
|
-
);
|
|
289
|
-
|
|
290
|
-
try {
|
|
291
|
-
expect(controller.handleCommand("session", state)).toBe(true);
|
|
292
|
-
|
|
293
|
-
const overlay = expectOverlay(runtimeState.overlay);
|
|
294
|
-
const text = collectText(renderSelect(overlay));
|
|
295
|
-
|
|
296
|
-
expect(
|
|
297
|
-
text.some((line) => line.includes("Investigate this session")),
|
|
298
|
-
).toBe(true);
|
|
299
|
-
} finally {
|
|
300
|
-
state.db.close();
|
|
301
|
-
}
|
|
302
|
-
});
|
|
303
|
-
|
|
304
89
|
test("handleCommand('session') restores the selected session and recomputes stats", () => {
|
|
305
90
|
const state = createTestState();
|
|
306
91
|
const runtimeState = { overlay: null as ActiveOverlay | null };
|
|
307
92
|
let scrollCalls = 0;
|
|
93
|
+
let renderCalls = 0;
|
|
308
94
|
const controller = createCommandController({
|
|
309
95
|
openOverlay: (nextOverlay) => {
|
|
310
96
|
runtimeState.overlay = nextOverlay;
|
|
@@ -318,7 +104,9 @@ describe("ui/commands", () => {
|
|
|
318
104
|
scrollConversationToBottom: () => {
|
|
319
105
|
scrollCalls += 1;
|
|
320
106
|
},
|
|
321
|
-
|
|
107
|
+
requestRender: () => {
|
|
108
|
+
renderCalls += 1;
|
|
109
|
+
},
|
|
322
110
|
reloadPromptContext: async () => {},
|
|
323
111
|
openInBrowser: () => {},
|
|
324
112
|
});
|
|
@@ -368,13 +156,12 @@ describe("ui/commands", () => {
|
|
|
368
156
|
expect(controller.handleCommand("session", state)).toBe(true);
|
|
369
157
|
|
|
370
158
|
const overlay = expectOverlay(runtimeState.overlay);
|
|
371
|
-
expect(overlay.title).toBe("Resume a session");
|
|
372
|
-
|
|
373
159
|
const selectNode = renderSelect(overlay);
|
|
374
160
|
selectNode.props.onKeyPress?.("enter");
|
|
375
161
|
|
|
376
162
|
expect(runtimeState.overlay).toBeNull();
|
|
377
163
|
expect(scrollCalls).toBe(1);
|
|
164
|
+
expect(renderCalls).toBe(1);
|
|
378
165
|
expect(state.session?.id).toBe(session.id);
|
|
379
166
|
expect(state.messages.map((message) => message.role)).toEqual([
|
|
380
167
|
"user",
|
|
@@ -391,27 +178,6 @@ describe("ui/commands", () => {
|
|
|
391
178
|
}
|
|
392
179
|
});
|
|
393
180
|
|
|
394
|
-
test("handleCommand returns false for unknown commands", () => {
|
|
395
|
-
const state = createTestState();
|
|
396
|
-
const controller = createCommandController({
|
|
397
|
-
openOverlay: () => {},
|
|
398
|
-
dismissOverlay: () => {},
|
|
399
|
-
setInputValue: () => {},
|
|
400
|
-
appendInfoMessage: () => {},
|
|
401
|
-
appendTodoMessage: () => {},
|
|
402
|
-
scrollConversationToBottom: () => {},
|
|
403
|
-
render: () => {},
|
|
404
|
-
reloadPromptContext: async () => {},
|
|
405
|
-
openInBrowser: () => {},
|
|
406
|
-
});
|
|
407
|
-
|
|
408
|
-
try {
|
|
409
|
-
expect(controller.handleCommand("unknown", state)).toBe(false);
|
|
410
|
-
} finally {
|
|
411
|
-
state.db.close();
|
|
412
|
-
}
|
|
413
|
-
});
|
|
414
|
-
|
|
415
181
|
test("/new clears the active session state and reloads prompt context", async () => {
|
|
416
182
|
const state = createTestState();
|
|
417
183
|
let reloadCount = 0;
|
|
@@ -423,7 +189,7 @@ describe("ui/commands", () => {
|
|
|
423
189
|
appendInfoMessage: () => {},
|
|
424
190
|
appendTodoMessage: () => {},
|
|
425
191
|
scrollConversationToBottom: () => {},
|
|
426
|
-
|
|
192
|
+
requestRender: () => {},
|
|
427
193
|
reloadPromptContext: async (nextState) => {
|
|
428
194
|
reloadCount++;
|
|
429
195
|
nextState.agentsMd = [
|
|
@@ -479,7 +245,7 @@ describe("ui/commands", () => {
|
|
|
479
245
|
appendInfoMessage: () => {},
|
|
480
246
|
appendTodoMessage: () => {},
|
|
481
247
|
scrollConversationToBottom: () => {},
|
|
482
|
-
|
|
248
|
+
requestRender: () => {},
|
|
483
249
|
reloadPromptContext: async () => {},
|
|
484
250
|
openInBrowser: () => {},
|
|
485
251
|
});
|
|
@@ -505,7 +271,7 @@ describe("ui/commands", () => {
|
|
|
505
271
|
appendInfoMessage: () => {},
|
|
506
272
|
appendTodoMessage: () => {},
|
|
507
273
|
scrollConversationToBottom: () => {},
|
|
508
|
-
|
|
274
|
+
requestRender: () => {},
|
|
509
275
|
reloadPromptContext: async () => {},
|
|
510
276
|
openInBrowser: () => {},
|
|
511
277
|
});
|
|
@@ -520,7 +286,7 @@ describe("ui/commands", () => {
|
|
|
520
286
|
}
|
|
521
287
|
});
|
|
522
288
|
|
|
523
|
-
test("/help appends markdown
|
|
289
|
+
test("/help appends a markdown info message without creating a session", () => {
|
|
524
290
|
const state = createTestState();
|
|
525
291
|
const appended: Array<{
|
|
526
292
|
text: string;
|
|
@@ -540,7 +306,7 @@ describe("ui/commands", () => {
|
|
|
540
306
|
},
|
|
541
307
|
appendTodoMessage: () => {},
|
|
542
308
|
scrollConversationToBottom: () => {},
|
|
543
|
-
|
|
309
|
+
requestRender: () => {},
|
|
544
310
|
reloadPromptContext: async () => {},
|
|
545
311
|
openInBrowser: () => {},
|
|
546
312
|
});
|
|
@@ -548,8 +314,7 @@ describe("ui/commands", () => {
|
|
|
548
314
|
try {
|
|
549
315
|
expect(controller.handleCommand("help", state)).toBe(true);
|
|
550
316
|
expect(appended).toHaveLength(1);
|
|
551
|
-
expect(appended[0]?.text).
|
|
552
|
-
expect(appended[0]?.text).toContain("## Commands");
|
|
317
|
+
expect(appended[0]?.text.length).toBeGreaterThan(0);
|
|
553
318
|
expect(appended[0]?.format).toBe("markdown");
|
|
554
319
|
expect(appended[0]?.sessionId).toBeNull();
|
|
555
320
|
expect(state.session).toBeNull();
|
|
@@ -596,7 +361,7 @@ describe("ui/commands", () => {
|
|
|
596
361
|
});
|
|
597
362
|
},
|
|
598
363
|
scrollConversationToBottom: () => {},
|
|
599
|
-
|
|
364
|
+
requestRender: () => {},
|
|
600
365
|
reloadPromptContext: async () => {},
|
|
601
366
|
openInBrowser: () => {},
|
|
602
367
|
});
|
|
@@ -629,7 +394,7 @@ describe("ui/commands", () => {
|
|
|
629
394
|
appendInfoMessage: () => {},
|
|
630
395
|
appendTodoMessage: () => {},
|
|
631
396
|
scrollConversationToBottom: () => {},
|
|
632
|
-
|
|
397
|
+
requestRender: () => {},
|
|
633
398
|
reloadPromptContext: async () => {},
|
|
634
399
|
openInBrowser: () => {},
|
|
635
400
|
});
|
|
@@ -657,7 +422,7 @@ describe("ui/commands", () => {
|
|
|
657
422
|
appendInfoMessage: () => {},
|
|
658
423
|
appendTodoMessage: () => {},
|
|
659
424
|
scrollConversationToBottom: () => {},
|
|
660
|
-
|
|
425
|
+
requestRender: () => {},
|
|
661
426
|
reloadPromptContext: async () => {},
|
|
662
427
|
openInBrowser: () => {},
|
|
663
428
|
});
|
|
@@ -674,55 +439,4 @@ describe("ui/commands", () => {
|
|
|
674
439
|
state.db.close();
|
|
675
440
|
}
|
|
676
441
|
});
|
|
677
|
-
|
|
678
|
-
test("formatRelativeDate accepts an explicit clock for deterministic output", () => {
|
|
679
|
-
const now = new Date("2026-04-07T12:00:00Z");
|
|
680
|
-
|
|
681
|
-
expect(formatRelativeDate(new Date("2026-04-07T11:59:45Z"), now)).toBe(
|
|
682
|
-
"just now",
|
|
683
|
-
);
|
|
684
|
-
expect(formatRelativeDate(new Date("2026-04-07T11:50:00Z"), now)).toBe(
|
|
685
|
-
"10m ago",
|
|
686
|
-
);
|
|
687
|
-
expect(formatRelativeDate(new Date("2026-04-07T10:00:00Z"), now)).toBe(
|
|
688
|
-
"2h ago",
|
|
689
|
-
);
|
|
690
|
-
expect(formatRelativeDate(new Date("2026-04-04T12:00:00Z"), now)).toBe(
|
|
691
|
-
"3d ago",
|
|
692
|
-
);
|
|
693
|
-
});
|
|
694
|
-
|
|
695
|
-
test("formatPromptHistoryPreview collapses whitespace into one line", () => {
|
|
696
|
-
expect(formatPromptHistoryPreview(" first\n\n second\tthird ")).toBe(
|
|
697
|
-
"first second third",
|
|
698
|
-
);
|
|
699
|
-
});
|
|
700
|
-
|
|
701
|
-
test("formatPromptHistoryLabel_longPromptAndCwd_returnsATruncatedSingleLineLabel", () => {
|
|
702
|
-
expect(
|
|
703
|
-
formatPromptHistoryLabel(
|
|
704
|
-
" Investigate\n\n this prompt history row because it is much too wide for the overlay ",
|
|
705
|
-
"/tmp/projects/very/deeply/nested/mini-coder-audit",
|
|
706
|
-
"5m ago",
|
|
707
|
-
),
|
|
708
|
-
).toBe(
|
|
709
|
-
"Investigate this prompt history… · …/mini-coder-audit · 5m ago",
|
|
710
|
-
);
|
|
711
|
-
});
|
|
712
|
-
|
|
713
|
-
test("formatSessionLabel_longPreviewAndModel_returnsATruncatedReadableLabel", () => {
|
|
714
|
-
expect(
|
|
715
|
-
formatSessionLabel(
|
|
716
|
-
{
|
|
717
|
-
model: "openai-codex/gpt-5.4-super-long-variant",
|
|
718
|
-
firstUserPreview:
|
|
719
|
-
"Audit the session selector because every entry looks identical in real usage",
|
|
720
|
-
},
|
|
721
|
-
"just now",
|
|
722
|
-
true,
|
|
723
|
-
),
|
|
724
|
-
).toBe(
|
|
725
|
-
"Audit the session selector… · openai-codex/gpt… · just now · current",
|
|
726
|
-
);
|
|
727
|
-
});
|
|
728
442
|
});
|
package/src/ui/commands.ts
CHANGED
|
@@ -19,20 +19,22 @@ import type { AppState } from "../index.ts";
|
|
|
19
19
|
import { getAvailableModels, saveOAuthCredentials } from "../index.ts";
|
|
20
20
|
import { COMMANDS } from "../input.ts";
|
|
21
21
|
import {
|
|
22
|
-
|
|
23
|
-
computeStats,
|
|
22
|
+
clearConversationState,
|
|
24
23
|
forkSession,
|
|
25
24
|
listPromptHistory,
|
|
26
25
|
listSessions,
|
|
27
26
|
loadMessages,
|
|
27
|
+
replaceConversationState,
|
|
28
28
|
type SessionListEntry,
|
|
29
29
|
type UiInfoFormat,
|
|
30
30
|
undoLastTurn,
|
|
31
31
|
} from "../session.ts";
|
|
32
32
|
import { updateSettings } from "../settings.ts";
|
|
33
|
+
import { collapseWhitespace, truncateText } from "../text.ts";
|
|
33
34
|
import { getTodoItems } from "../tools.ts";
|
|
34
35
|
import { buildHelpText, COMMAND_DESCRIPTIONS } from "./help.ts";
|
|
35
36
|
import { type ActiveOverlay, OVERLAY_MAX_VISIBLE } from "./overlay.ts";
|
|
37
|
+
import type { UiRenderPriority } from "./runtime.ts";
|
|
36
38
|
import { abbreviatePath } from "./status.ts";
|
|
37
39
|
|
|
38
40
|
/** Effort levels available for selection. */
|
|
@@ -45,9 +47,9 @@ const EFFORT_LEVELS: { label: string; value: ThinkingLevel }[] = [
|
|
|
45
47
|
|
|
46
48
|
/** Runtime hooks injected from the stateful UI module. */
|
|
47
49
|
interface UiCommandRuntime {
|
|
48
|
-
/** Open an overlay
|
|
50
|
+
/** Open an overlay. */
|
|
49
51
|
openOverlay: (overlay: ActiveOverlay) => void;
|
|
50
|
-
/** Dismiss the active overlay
|
|
52
|
+
/** Dismiss the active overlay. */
|
|
51
53
|
dismissOverlay: () => void;
|
|
52
54
|
/** Update the current input draft. */
|
|
53
55
|
setInputValue: (value: string) => void;
|
|
@@ -64,8 +66,8 @@ interface UiCommandRuntime {
|
|
|
64
66
|
) => void;
|
|
65
67
|
/** Re-enable stick-to-bottom behavior for the conversation log. */
|
|
66
68
|
scrollConversationToBottom: () => void;
|
|
67
|
-
/**
|
|
68
|
-
|
|
69
|
+
/** Schedule a UI re-render. */
|
|
70
|
+
requestRender: (priority?: UiRenderPriority) => void;
|
|
69
71
|
/** Reload prompt/session context at a boundary like `/new`. */
|
|
70
72
|
reloadPromptContext: (state: AppState) => Promise<void>;
|
|
71
73
|
/** Open a URL in the user's default browser. */
|
|
@@ -142,7 +144,7 @@ export function formatRelativeDate(date: Date, now = new Date()): string {
|
|
|
142
144
|
* @returns A single-line preview string.
|
|
143
145
|
*/
|
|
144
146
|
export function formatPromptHistoryPreview(text: string): string {
|
|
145
|
-
return text
|
|
147
|
+
return collapseWhitespace(text);
|
|
146
148
|
}
|
|
147
149
|
|
|
148
150
|
const HISTORY_PREVIEW_MAX_CHARS = 32;
|
|
@@ -150,26 +152,6 @@ const HISTORY_CWD_MAX_CHARS = 18;
|
|
|
150
152
|
const SESSION_PREVIEW_MAX_CHARS = 27;
|
|
151
153
|
const SESSION_MODEL_MAX_CHARS = 17;
|
|
152
154
|
|
|
153
|
-
function truncateTrailingText(text: string, maxChars: number): string {
|
|
154
|
-
if (text.length <= maxChars) {
|
|
155
|
-
return text;
|
|
156
|
-
}
|
|
157
|
-
if (maxChars <= 1) {
|
|
158
|
-
return "…";
|
|
159
|
-
}
|
|
160
|
-
return `${text.slice(0, maxChars - 1)}…`;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
function truncateLeadingText(text: string, maxChars: number): string {
|
|
164
|
-
if (text.length <= maxChars) {
|
|
165
|
-
return text;
|
|
166
|
-
}
|
|
167
|
-
if (maxChars <= 1) {
|
|
168
|
-
return "…";
|
|
169
|
-
}
|
|
170
|
-
return `…${text.slice(text.length - (maxChars - 1))}`;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
155
|
/**
|
|
174
156
|
* Format a prompt-history row for the Select overlay.
|
|
175
157
|
*
|
|
@@ -183,13 +165,14 @@ export function formatPromptHistoryLabel(
|
|
|
183
165
|
cwd: string,
|
|
184
166
|
date: string,
|
|
185
167
|
): string {
|
|
186
|
-
const preview =
|
|
168
|
+
const preview = truncateText(
|
|
187
169
|
formatPromptHistoryPreview(text),
|
|
188
170
|
HISTORY_PREVIEW_MAX_CHARS,
|
|
189
171
|
);
|
|
190
|
-
const displayCwd =
|
|
172
|
+
const displayCwd = truncateText(
|
|
191
173
|
abbreviatePath(cwd),
|
|
192
174
|
HISTORY_CWD_MAX_CHARS,
|
|
175
|
+
"start",
|
|
193
176
|
);
|
|
194
177
|
return `${preview} · ${displayCwd} · ${date}`;
|
|
195
178
|
}
|
|
@@ -207,11 +190,11 @@ export function formatSessionLabel(
|
|
|
207
190
|
date: string,
|
|
208
191
|
isCurrent: boolean,
|
|
209
192
|
): string {
|
|
210
|
-
const preview =
|
|
193
|
+
const preview = truncateText(
|
|
211
194
|
session.firstUserPreview ?? "No messages yet",
|
|
212
195
|
SESSION_PREVIEW_MAX_CHARS,
|
|
213
196
|
);
|
|
214
|
-
const model =
|
|
197
|
+
const model = truncateText(
|
|
215
198
|
session.model ?? "no model",
|
|
216
199
|
SESSION_MODEL_MAX_CHARS,
|
|
217
200
|
);
|
|
@@ -397,13 +380,12 @@ export function createCommandController(
|
|
|
397
380
|
const picked = sessions.find((session) => session.id === sessionId);
|
|
398
381
|
if (picked) {
|
|
399
382
|
state.session = picked;
|
|
400
|
-
state
|
|
401
|
-
state.stats = computeStats(state.messages);
|
|
402
|
-
state.contextTokens = computeContextTokens(state.messages);
|
|
383
|
+
replaceConversationState(state, loadMessages(state.db, picked.id));
|
|
403
384
|
runtime.scrollConversationToBottom();
|
|
404
385
|
}
|
|
405
386
|
}
|
|
406
387
|
runtime.dismissOverlay();
|
|
388
|
+
runtime.requestRender("normal");
|
|
407
389
|
},
|
|
408
390
|
);
|
|
409
391
|
};
|
|
@@ -413,12 +395,10 @@ export function createCommandController(
|
|
|
413
395
|
return;
|
|
414
396
|
}
|
|
415
397
|
state.session = null;
|
|
416
|
-
state
|
|
417
|
-
state.stats = { totalInput: 0, totalOutput: 0, totalCost: 0 };
|
|
418
|
-
state.contextTokens = 0;
|
|
398
|
+
clearConversationState(state);
|
|
419
399
|
await runtime.reloadPromptContext(state);
|
|
420
400
|
runtime.scrollConversationToBottom();
|
|
421
|
-
runtime.
|
|
401
|
+
runtime.requestRender("normal");
|
|
422
402
|
};
|
|
423
403
|
|
|
424
404
|
const handleForkCommand = (state: AppState): void => {
|
|
@@ -427,9 +407,7 @@ export function createCommandController(
|
|
|
427
407
|
}
|
|
428
408
|
const forked = forkSession(state.db, state.session.id);
|
|
429
409
|
state.session = forked;
|
|
430
|
-
state
|
|
431
|
-
state.stats = computeStats(state.messages);
|
|
432
|
-
state.contextTokens = computeContextTokens(state.messages);
|
|
410
|
+
replaceConversationState(state, loadMessages(state.db, forked.id));
|
|
433
411
|
runtime.appendInfoMessage("Forked session.", state);
|
|
434
412
|
};
|
|
435
413
|
|
|
@@ -447,11 +425,9 @@ export function createCommandController(
|
|
|
447
425
|
}
|
|
448
426
|
const removed = undoLastTurn(state.db, state.session.id);
|
|
449
427
|
if (removed) {
|
|
450
|
-
state
|
|
451
|
-
state.stats = computeStats(state.messages);
|
|
452
|
-
state.contextTokens = computeContextTokens(state.messages);
|
|
428
|
+
replaceConversationState(state, loadMessages(state.db, state.session.id));
|
|
453
429
|
runtime.scrollConversationToBottom();
|
|
454
|
-
runtime.
|
|
430
|
+
runtime.requestRender("normal");
|
|
455
431
|
}
|
|
456
432
|
};
|
|
457
433
|
|
|
@@ -460,7 +436,6 @@ export function createCommandController(
|
|
|
460
436
|
state.settings = updateSettings(state.settingsPath, {
|
|
461
437
|
showReasoning: state.showReasoning,
|
|
462
438
|
});
|
|
463
|
-
runtime.render();
|
|
464
439
|
};
|
|
465
440
|
|
|
466
441
|
const handleVerboseCommand = (state: AppState): void => {
|
|
@@ -468,7 +443,6 @@ export function createCommandController(
|
|
|
468
443
|
state.settings = updateSettings(state.settingsPath, {
|
|
469
444
|
verbose: state.verbose,
|
|
470
445
|
});
|
|
471
|
-
runtime.render();
|
|
472
446
|
};
|
|
473
447
|
|
|
474
448
|
const performLogin = async (
|