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.
@@ -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, Node } from "@cel-tui/types";
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
- render: () => {},
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
- render: () => {},
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
- render: () => {},
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
- render: () => {},
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 help without creating a session", () => {
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
- render: () => {},
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).toContain("# Help");
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
- render: () => {},
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
- render: () => {},
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
- render: () => {},
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
  });
@@ -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
- computeContextTokens,
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 and trigger a re-render. */
50
+ /** Open an overlay. */
49
51
  openOverlay: (overlay: ActiveOverlay) => void;
50
- /** Dismiss the active overlay and trigger a re-render. */
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
- /** Trigger a UI re-render. */
68
- render: () => void;
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.replace(/\s+/g, " ").trim();
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 = truncateTrailingText(
168
+ const preview = truncateText(
187
169
  formatPromptHistoryPreview(text),
188
170
  HISTORY_PREVIEW_MAX_CHARS,
189
171
  );
190
- const displayCwd = truncateLeadingText(
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 = truncateTrailingText(
193
+ const preview = truncateText(
211
194
  session.firstUserPreview ?? "No messages yet",
212
195
  SESSION_PREVIEW_MAX_CHARS,
213
196
  );
214
- const model = truncateTrailingText(
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.messages = loadMessages(state.db, picked.id);
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.messages = [];
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.render();
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.messages = loadMessages(state.db, forked.id);
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.messages = loadMessages(state.db, state.session.id);
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.render();
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 (