mini-coder 0.5.9 → 0.5.11

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.
@@ -1,50 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
- import { DEFAULT_SHOW_REASONING } from "../settings.ts";
3
- import { buildHelpText, type HelpRenderState } from "./help.ts";
4
-
5
- describe("ui/help", () => {
6
- test("buildHelpText includes current reasoning and verbose state", () => {
7
- const helpState: HelpRenderState = {
8
- providers: new Map(),
9
- model: null,
10
- agentsMd: [],
11
- skills: [],
12
- plugins: [],
13
- showReasoning: DEFAULT_SHOW_REASONING,
14
- verbose: false,
15
- };
16
-
17
- const text = buildHelpText(helpState);
18
-
19
- expect(text).toContain(
20
- `/reasoning Toggle thinking display (currently ${DEFAULT_SHOW_REASONING ? "on" : "off"})`,
21
- );
22
- expect(text).toContain(
23
- "/verbose Toggle verbose tool rendering (currently off)",
24
- );
25
- expect(text).toContain("/todo Show the current todo list");
26
- });
27
-
28
- test("buildHelpText describes the current Escape behavior", () => {
29
- const helpState: HelpRenderState = {
30
- providers: new Map(),
31
- model: null,
32
- agentsMd: [],
33
- skills: [],
34
- plugins: [],
35
- showReasoning: DEFAULT_SHOW_REASONING,
36
- verbose: false,
37
- };
38
-
39
- const text = buildHelpText(helpState);
40
-
41
- expect(text).toContain(
42
- "Escape closes the current overlay and returns focus to the input",
43
- );
44
- expect(text).toContain(
45
- "With no overlay open, Escape interrupts the current turn",
46
- );
47
- expect(text).toContain("Otherwise Escape does nothing");
48
- expect(text).not.toContain("Escape blurs the input first");
49
- });
50
- });
@@ -1,42 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
- import { Select } from "@cel-tui/components";
3
- import type { Node } from "@cel-tui/types";
4
- import { DEFAULT_THEME } from "../theme.ts";
5
- import { type ActiveOverlay, renderOverlay } from "./overlay.ts";
6
-
7
- function collectText(node: Node | null): string[] {
8
- if (!node) {
9
- return [];
10
- }
11
- if (node.type === "text") {
12
- return [node.content];
13
- }
14
- if (node.type === "textinput") {
15
- return [];
16
- }
17
- return node.children.flatMap((child) => collectText(child));
18
- }
19
-
20
- describe("ui/overlay", () => {
21
- test("renderOverlay shows the title above the selectable body", () => {
22
- const overlay: ActiveOverlay = {
23
- title: "Commands",
24
- select: Select({
25
- items: [{ label: "overlay body", value: "body", filterText: "body" }],
26
- maxVisible: 1,
27
- placeholder: "type to filter...",
28
- focused: true,
29
- highlightColor: DEFAULT_THEME.accentText,
30
- onSelect: () => {},
31
- onBlur: () => {},
32
- }),
33
- };
34
-
35
- const text = collectText(renderOverlay(DEFAULT_THEME, overlay));
36
- const titleIndex = text.indexOf("Commands");
37
- const bodyIndex = text.indexOf("overlay body");
38
-
39
- expect(titleIndex).toBeGreaterThanOrEqual(0);
40
- expect(bodyIndex).toBeGreaterThan(titleIndex);
41
- });
42
- });
@@ -1,444 +0,0 @@
1
- import { afterEach, describe, test } from "bun:test";
2
- import { cel, MockTerminal } from "@cel-tui/core";
3
- import type { Node } from "@cel-tui/types";
4
- import {
5
- fauxAssistantMessage,
6
- fauxThinking,
7
- fauxToolCall,
8
- } from "@mariozechner/pi-ai";
9
- import type { AppState } from "../index.ts";
10
- import {
11
- computeContextTokens,
12
- computeStats,
13
- createUiMessage,
14
- openDatabase,
15
- } from "../session.ts";
16
- import { DEFAULT_SHOW_REASONING, DEFAULT_VERBOSE } from "../settings.ts";
17
- import { DEFAULT_THEME } from "../theme.ts";
18
- import {
19
- buildConversationLogNodes,
20
- resetConversationRenderCache,
21
- } from "../ui/conversation.ts";
22
- import {
23
- createInputController,
24
- renderActiveOverlay,
25
- renderBaseLayout,
26
- resetUiState,
27
- } from "../ui.ts";
28
-
29
- const VIEWPORT_COLS = 120;
30
- const VIEWPORT_ROWS = 40;
31
- const TYPING_SAMPLE_COUNT = 9;
32
- const LARGE_MARKDOWN_SECTION_COUNT = 8;
33
- const NODE_BUDGET = 6_000;
34
- const TYPING_MEDIAN_BUDGET_MS = 40;
35
-
36
- function flushCelRender(): Promise<void> {
37
- return new Promise((resolve) => {
38
- process.nextTick(resolve);
39
- });
40
- }
41
-
42
- function startUiViewport(
43
- state: AppState,
44
- terminal: MockTerminal,
45
- controller: ReturnType<typeof createInputController>,
46
- ): void {
47
- cel.init(terminal);
48
- cel.viewport(() => {
49
- const base = renderBaseLayout(state, terminal.columns, controller);
50
- const overlay = renderActiveOverlay(state);
51
- return overlay ? [base, overlay] : base;
52
- });
53
- }
54
-
55
- function createTestState(): AppState {
56
- const cwd = "/tmp/mini-coder-ui-render-perf-test";
57
- const model: NonNullable<AppState["model"]> = {
58
- id: "gpt-5.4",
59
- name: "gpt-5.4",
60
- provider: "openai-codex",
61
- api: "responses",
62
- baseUrl: "http://localhost:0",
63
- reasoning: true,
64
- input: ["text"],
65
- cost: {
66
- input: 0,
67
- output: 0,
68
- cacheRead: 0,
69
- cacheWrite: 0,
70
- },
71
- contextWindow: 272_000,
72
- maxTokens: 8_192,
73
- };
74
-
75
- return {
76
- db: openDatabase(":memory:"),
77
- session: null,
78
- model,
79
- effort: "medium",
80
- messages: [],
81
- stats: { totalInput: 0, totalOutput: 0, totalCost: 0 },
82
- contextTokens: 0,
83
- agentsMd: [],
84
- skills: [],
85
- plugins: [],
86
- theme: DEFAULT_THEME,
87
- git: null,
88
- providers: new Map(),
89
- oauthCredentials: {},
90
- settings: {},
91
- settingsPath: `${cwd}/settings.json`,
92
- cwd,
93
- canonicalCwd: cwd,
94
- running: false,
95
- abortController: null,
96
- activeTurnPromise: null,
97
- queuedUserMessages: [],
98
- showReasoning: DEFAULT_SHOW_REASONING,
99
- verbose: DEFAULT_VERBOSE,
100
- versionLabel: "dev",
101
- customModels: [],
102
- startupWarnings: [],
103
- };
104
- }
105
-
106
- function setMessages(state: AppState, messages: AppState["messages"]): void {
107
- state.messages = messages;
108
- state.stats = computeStats(messages);
109
- state.contextTokens = computeContextTokens(messages);
110
- }
111
-
112
- function buildLargeMarkdown(seed: number): string {
113
- return Array.from({ length: LARGE_MARKDOWN_SECTION_COUNT }, (_, index) => {
114
- const section = seed * 100 + index;
115
- return [
116
- `# Section ${section}`,
117
- "",
118
- `Paragraph ${section}: ${"lorem ipsum dolor sit amet ".repeat(10)}`,
119
- "",
120
- `- item ${section}a with [link](https://example.com/${section})`,
121
- `- item ${section}b with **bold** text and \`inline_code_${section}\``,
122
- "",
123
- `> Quote ${section}: ${"wrapped quoted text ".repeat(10)}`,
124
- "",
125
- "```ts",
126
- `const value${section} = ${section};`,
127
- `console.log(value${section});`,
128
- "```",
129
- ].join("\n");
130
- }).join("\n\n");
131
- }
132
-
133
- function createLargeMarkdownMessages(): AppState["messages"] {
134
- return [
135
- {
136
- role: "user",
137
- content: "Investigate the rendering lag in this session.",
138
- timestamp: 1,
139
- },
140
- fauxAssistantMessage(buildLargeMarkdown(1), { timestamp: 2 }),
141
- {
142
- role: "user",
143
- content: "Keep going with the detailed write-up.",
144
- timestamp: 3,
145
- },
146
- fauxAssistantMessage(buildLargeMarkdown(2), { timestamp: 4 }),
147
- {
148
- role: "user",
149
- content: "Add one more large markdown response for history.",
150
- timestamp: 5,
151
- },
152
- fauxAssistantMessage(buildLargeMarkdown(3), { timestamp: 6 }),
153
- ];
154
- }
155
-
156
- function countNodes(node: Node): number {
157
- if (node.type === "text" || node.type === "textinput") {
158
- return 1;
159
- }
160
-
161
- return (
162
- 1 + node.children.reduce((total, child) => total + countNodes(child), 0)
163
- );
164
- }
165
-
166
- function getConversationNode(
167
- messages: AppState["messages"],
168
- index: number,
169
- ): Node {
170
- resetConversationRenderCache();
171
-
172
- const nodes = buildConversationLogNodes(
173
- {
174
- messages,
175
- showReasoning: true,
176
- verbose: false,
177
- theme: DEFAULT_THEME,
178
- versionLabel: "dev",
179
- },
180
- {
181
- isStreaming: false,
182
- content: [],
183
- pendingToolResults: [],
184
- },
185
- 0,
186
- 80,
187
- );
188
- const node = nodes[index];
189
-
190
- if (!node) {
191
- throw new Error(`Expected a rendered node at index ${index}`);
192
- }
193
-
194
- return node;
195
- }
196
-
197
- function expectNodeCountAtMost(
198
- label: string,
199
- node: Node,
200
- maxNodes: number,
201
- ): void {
202
- const nodeCount = countNodes(node);
203
-
204
- if (nodeCount > maxNodes) {
205
- throw new Error(
206
- `Expected ${label} node count <= ${maxNodes}, got ${nodeCount}`,
207
- );
208
- }
209
- }
210
-
211
- function median(values: readonly number[]): number {
212
- const sorted = [...values].sort((a, b) => a - b);
213
- const middle = Math.floor(sorted.length / 2);
214
-
215
- if (sorted.length % 2 === 0) {
216
- return (sorted[middle - 1]! + sorted[middle]!) / 2;
217
- }
218
-
219
- return sorted[middle]!;
220
- }
221
-
222
- async function measureTypingMedianRerenderMs(state: AppState): Promise<number> {
223
- const terminal = new MockTerminal(VIEWPORT_COLS, VIEWPORT_ROWS);
224
- const controller = createInputController(state);
225
- const samples: number[] = [];
226
-
227
- startUiViewport(state, terminal, controller);
228
- await flushCelRender();
229
-
230
- try {
231
- for (let index = 0; index < TYPING_SAMPLE_COUNT; index++) {
232
- const nextValue = index % 2 === 0 ? "a" : "ab";
233
- const start = performance.now();
234
- controller.onChange(nextValue);
235
- await flushCelRender();
236
- samples.push(performance.now() - start);
237
- }
238
- } finally {
239
- cel.stop();
240
- }
241
-
242
- return median(samples);
243
- }
244
-
245
- afterEach(() => {
246
- resetUiState();
247
- cel.stop();
248
- });
249
-
250
- describe("ui render performance", () => {
251
- test("renderBaseLayout with large historical assistant markdown stays within the node budget", () => {
252
- // Arrange
253
- const state = createTestState();
254
- setMessages(state, createLargeMarkdownMessages());
255
- const controller = createInputController(state);
256
-
257
- try {
258
- // Act
259
- const base = renderBaseLayout(state, VIEWPORT_COLS, controller);
260
- const nodeCount = countNodes(base);
261
-
262
- // Assert
263
- if (nodeCount > NODE_BUDGET) {
264
- throw new Error(
265
- `Expected large-markdown layout node count <= ${NODE_BUDGET}, got ${nodeCount}`,
266
- );
267
- }
268
- } finally {
269
- state.db.close();
270
- }
271
- });
272
-
273
- test("non-markdown conversation log items stay within bounded node budgets", () => {
274
- // Arrange
275
- const userNode = getConversationNode(
276
- [
277
- {
278
- role: "user",
279
- content: "lorem ipsum dolor sit amet ".repeat(400),
280
- timestamp: 1,
281
- },
282
- ],
283
- 0,
284
- );
285
- const uiNode = getConversationNode(
286
- [createUiMessage("status update ".repeat(400))],
287
- 0,
288
- );
289
- const thinkingNode = getConversationNode(
290
- [fauxAssistantMessage([fauxThinking("plan\n".repeat(400))])],
291
- 0,
292
- );
293
- const assistantToolCallsNode = getConversationNode(
294
- [
295
- fauxAssistantMessage([
296
- fauxThinking("brief plan"),
297
- fauxToolCall(
298
- "shell",
299
- { command: "printf 'foo\\nbar'" },
300
- { id: "tool-1" },
301
- ),
302
- fauxToolCall(
303
- "edit",
304
- {
305
- path: "src/app.ts",
306
- oldText: "before",
307
- newText: "after",
308
- },
309
- { id: "tool-2" },
310
- ),
311
- fauxToolCall("readImage", { path: "diagram.png" }, { id: "tool-3" }),
312
- ]),
313
- ],
314
- 0,
315
- );
316
- const longShellToolCallNode = getConversationNode(
317
- [
318
- fauxAssistantMessage([
319
- fauxToolCall(
320
- "shell",
321
- { command: `printf ${"x".repeat(300)}TAIL` },
322
- { id: "tool-4" },
323
- ),
324
- ]),
325
- ],
326
- 0,
327
- );
328
- const shellResultNode = getConversationNode(
329
- [
330
- fauxAssistantMessage([
331
- fauxToolCall("shell", { command: "seq 1 200" }, { id: "tool-1" }),
332
- ]),
333
- {
334
- role: "toolResult",
335
- toolCallId: "tool-1",
336
- toolName: "shell",
337
- content: [
338
- {
339
- type: "text",
340
- text: Array.from(
341
- { length: 200 },
342
- (_, index) => `line ${index + 1}`,
343
- ).join("\n"),
344
- },
345
- ],
346
- isError: false,
347
- timestamp: 2,
348
- },
349
- ],
350
- 1,
351
- );
352
- const editErrorNode = getConversationNode(
353
- [
354
- fauxAssistantMessage([
355
- fauxToolCall(
356
- "edit",
357
- {
358
- path: "src/app.ts",
359
- oldText: "before",
360
- newText: "after",
361
- },
362
- { id: "tool-2" },
363
- ),
364
- ]),
365
- {
366
- role: "toolResult",
367
- toolCallId: "tool-2",
368
- toolName: "edit",
369
- content: [
370
- {
371
- type: "text",
372
- text: Array.from(
373
- { length: 120 },
374
- (_, index) => `line ${index + 1}`,
375
- ).join("\n"),
376
- },
377
- ],
378
- isError: true,
379
- timestamp: 3,
380
- },
381
- ],
382
- 1,
383
- );
384
- const genericResultNode = getConversationNode(
385
- [
386
- {
387
- role: "toolResult",
388
- toolCallId: "tool-3",
389
- toolName: "pluginSearch",
390
- content: [
391
- {
392
- type: "text",
393
- text: Array.from(
394
- { length: 200 },
395
- (_, index) => `row ${index + 1}`,
396
- ).join("\n"),
397
- },
398
- ],
399
- isError: false,
400
- timestamp: 4,
401
- },
402
- ],
403
- 0,
404
- );
405
-
406
- // Assert
407
- expectNodeCountAtMost("long user message", userNode, 4);
408
- expectNodeCountAtMost("long UI message", uiNode, 4);
409
- expectNodeCountAtMost("thinking-only assistant message", thinkingNode, 4);
410
- expectNodeCountAtMost(
411
- "assistant tool-call bundle",
412
- assistantToolCallsNode,
413
- 64,
414
- );
415
- expectNodeCountAtMost(
416
- "long single-token shell tool call",
417
- longShellToolCallNode,
418
- 64,
419
- );
420
- expectNodeCountAtMost("shell tool-result preview", shellResultNode, 24);
421
- expectNodeCountAtMost("edit error preview", editErrorNode, 24);
422
- expectNodeCountAtMost("generic tool result", genericResultNode, 240);
423
- });
424
-
425
- test("typing into the input with large historical assistant markdown stays within the rerender budget", async () => {
426
- // Arrange
427
- const state = createTestState();
428
- setMessages(state, createLargeMarkdownMessages());
429
-
430
- try {
431
- // Act
432
- const medianRerenderMs = await measureTypingMedianRerenderMs(state);
433
-
434
- // Assert
435
- if (medianRerenderMs > TYPING_MEDIAN_BUDGET_MS) {
436
- throw new Error(
437
- `Expected typing median rerender <= ${TYPING_MEDIAN_BUDGET_MS}ms, got ${medianRerenderMs.toFixed(1)}ms`,
438
- );
439
- }
440
- } finally {
441
- state.db.close();
442
- }
443
- });
444
- });