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.
@@ -1,65 +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 formats commands as markdown list items with current 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("# Help");
20
- expect(text).toContain("## Commands");
21
- expect(text).toContain(
22
- `- \`/reasoning\` — Toggle thinking display _(currently ${DEFAULT_SHOW_REASONING ? "on" : "off"})_`,
23
- );
24
- expect(text).toContain(
25
- "- `/verbose` — Toggle verbose tool rendering _(currently off)_",
26
- );
27
- expect(text).toContain("- `/todo` — Show the current todo list");
28
- });
29
-
30
- test("buildHelpText lists the supported keyboard shortcuts in markdown", () => {
31
- const helpState: HelpRenderState = {
32
- providers: new Map(),
33
- model: null,
34
- agentsMd: [],
35
- skills: [],
36
- plugins: [],
37
- showReasoning: DEFAULT_SHOW_REASONING,
38
- verbose: false,
39
- };
40
-
41
- const text = buildHelpText(helpState);
42
-
43
- expect(text).toContain("## Keyboard");
44
- expect(text).toContain("- `Enter` submits the current draft.");
45
- expect(text).toContain("- `Shift+Enter` inserts a newline.");
46
- expect(text).toContain(
47
- "- `Tab` opens command autocomplete when the draft starts with `/`.",
48
- );
49
- expect(text).toContain(
50
- "- Otherwise, `Tab` autocompletes file paths and can open a path picker when there are multiple matches.",
51
- );
52
- expect(text).toContain("- `Ctrl+R` opens global input history search.");
53
- expect(text).toContain(
54
- "- `Escape` closes the current overlay and returns focus to the input.",
55
- );
56
- expect(text).toContain(
57
- "- With no overlay open, `Escape` interrupts the current turn.",
58
- );
59
- expect(text).toContain("- Otherwise, `Escape` does nothing.");
60
- expect(text).toContain("- `Ctrl+C` exits gracefully.");
61
- expect(text).toContain("- `Ctrl+D` exits when the input is empty.");
62
- expect(text).toContain("- `Ctrl+Z` suspends the app to the background.");
63
- expect(text).not.toContain("Escape blurs the input first");
64
- });
65
- });
@@ -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
- });