mini-coder 0.5.12 → 0.5.14

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.
@@ -17,9 +17,12 @@ import { getOAuthProviders } from "@mariozechner/pi-ai/oauth";
17
17
  import { getErrorMessage } from "../errors.ts";
18
18
  import type { AppState } from "../index.ts";
19
19
  import { getAvailableModels, saveOAuthCredentials } from "../index.ts";
20
- import { COMMANDS } from "../input.ts";
20
+ import { COMMANDS, SKILL_COMMAND } from "../input.ts";
21
+ import { connectMcpServer, disconnectMcpServer } from "../mcp.ts";
21
22
  import {
22
23
  clearConversationState,
24
+ computeSessionContextTokens,
25
+ computeSessionStats,
23
26
  forkSession,
24
27
  listPromptHistory,
25
28
  listSessions,
@@ -30,9 +33,15 @@ import {
30
33
  undoLastTurn,
31
34
  } from "../session.ts";
32
35
  import { updateSettings } from "../settings.ts";
36
+ import { clearQueuedUserMessages } from "../submit.ts";
33
37
  import { collapseWhitespace, truncateText } from "../text.ts";
34
38
  import { getTodoItems } from "../tools.ts";
35
- import { buildHelpText, COMMAND_DESCRIPTIONS } from "./help.ts";
39
+ import {
40
+ buildHelpText,
41
+ COMMAND_DESCRIPTIONS,
42
+ SKILL_REFERENCE_DESCRIPTION,
43
+ SKILL_REFERENCE_LABEL,
44
+ } from "./help.ts";
36
45
  import { type ActiveOverlay, OVERLAY_MAX_VISIBLE } from "./overlay.ts";
37
46
  import type { UiRenderPriority } from "./runtime.ts";
38
47
  import { abbreviatePath } from "./status.ts";
@@ -74,6 +83,22 @@ interface UiCommandRuntime {
74
83
  openInBrowser: (url: string) => void;
75
84
  }
76
85
 
86
+ interface UiCommandDeps {
87
+ /** Connect a configured MCP server on demand. */
88
+ connectMcpServer: (
89
+ server: AppState["mcpServers"][number],
90
+ ) => Promise<string[]>;
91
+ /** Disconnect an MCP server and clear its imported tools. */
92
+ disconnectMcpServer: (
93
+ server: AppState["mcpServers"][number],
94
+ ) => Promise<void>;
95
+ }
96
+
97
+ const defaultUiCommandDeps: UiCommandDeps = {
98
+ connectMcpServer,
99
+ disconnectMcpServer,
100
+ };
101
+
77
102
  /** Public command actions consumed by `ui.ts` and unit tests. */
78
103
  interface UiCommandController {
79
104
  /** Apply a model selection and persist it to settings. */
@@ -212,14 +237,117 @@ interface OverlayItem {
212
237
  filterText: string;
213
238
  }
214
239
 
240
+ function formatSkillLabel(skill: AppState["skills"][number]): string {
241
+ return skill.description
242
+ ? `${skill.name} · ${skill.description}`
243
+ : skill.name;
244
+ }
245
+
246
+ function formatToolCount(count: number): string {
247
+ return `${count} tool${count === 1 ? "" : "s"}`;
248
+ }
249
+
250
+ function formatMcpServerLabel(server: AppState["mcpServers"][number]): string {
251
+ return `${server.name} · ${server.enabled ? "on" : "off"} · ${formatToolCount(server.tools.length)}`;
252
+ }
253
+
254
+ function formatMcpToggleMessage(
255
+ server: AppState["mcpServers"][number],
256
+ toolCount: number,
257
+ ): string {
258
+ const action = server.enabled ? "Enabled" : "Disabled";
259
+ const delta = `${server.enabled ? "+" : "-"}${toolCount}`;
260
+ const toolSuffix = toolCount === 1 ? "tool" : "tools";
261
+ return `${action} MCP server "${server.name}" (${delta} ${toolSuffix}).`;
262
+ }
263
+
264
+ function isRepoLocalMcpServer(state: AppState, serverName: string): boolean {
265
+ return (
266
+ state.repoSettings.mcp?.servers?.some(
267
+ (entry) => entry.name === serverName,
268
+ ) ?? false
269
+ );
270
+ }
271
+
272
+ function persistMcpServerSettings(
273
+ state: AppState,
274
+ server: AppState["mcpServers"][number],
275
+ ): void {
276
+ if (isRepoLocalMcpServer(state, server.name)) {
277
+ return;
278
+ }
279
+
280
+ state.settings = updateSettings(state.settingsPath, {
281
+ mcp: {
282
+ servers: [
283
+ {
284
+ name: server.name,
285
+ url: server.url,
286
+ enabled: server.enabled,
287
+ },
288
+ ],
289
+ },
290
+ });
291
+ }
292
+
293
+ async function toggleMcpServer(
294
+ server: AppState["mcpServers"][number],
295
+ state: AppState,
296
+ runtime: UiCommandRuntime,
297
+ deps: UiCommandDeps,
298
+ ): Promise<void> {
299
+ try {
300
+ if (server.enabled) {
301
+ const toolCount = server.tools.length;
302
+ await deps.disconnectMcpServer(server);
303
+ server.enabled = false;
304
+ persistMcpServerSettings(state, server);
305
+ runtime.appendInfoMessage(
306
+ formatMcpToggleMessage(server, toolCount),
307
+ state,
308
+ );
309
+ runtime.requestRender("normal");
310
+ return;
311
+ }
312
+
313
+ const warnings = await deps.connectMcpServer(server);
314
+ if (!server.connected) {
315
+ for (const warning of warnings) {
316
+ runtime.appendInfoMessage(warning, state);
317
+ }
318
+ runtime.requestRender("normal");
319
+ return;
320
+ }
321
+
322
+ server.enabled = true;
323
+ persistMcpServerSettings(state, server);
324
+ runtime.appendInfoMessage(
325
+ formatMcpToggleMessage(server, server.tools.length),
326
+ state,
327
+ );
328
+ for (const warning of warnings) {
329
+ runtime.appendInfoMessage(warning, state);
330
+ }
331
+ runtime.requestRender("normal");
332
+ } catch (error) {
333
+ runtime.appendInfoMessage(
334
+ `MCP server "${server.name}": ${getErrorMessage(error)}`,
335
+ state,
336
+ );
337
+ runtime.requestRender("normal");
338
+ }
339
+ }
340
+
215
341
  /**
216
342
  * Create the UI command controller bound to the current UI runtime hooks.
217
343
  *
218
344
  * @param runtime - State mutation and rendering hooks owned by `ui.ts`.
345
+ * @param deps - Internal MCP command dependencies, mainly for tests.
219
346
  * @returns Command actions for slash commands and overlays.
220
347
  */
221
348
  export function createCommandController(
222
349
  runtime: UiCommandRuntime,
350
+ deps: UiCommandDeps = defaultUiCommandDeps,
223
351
  ): UiCommandController {
224
352
  const openSelectOverlay = (
225
353
  state: AppState,
@@ -235,20 +363,52 @@ export function createCommandController(
235
363
  focused: true,
236
364
  highlightColor: state.theme.accentText,
237
365
  onSelect,
366
+ onKeyPress: (key) => {
367
+ if (key === "escape") {
368
+ runtime.dismissOverlay();
369
+ return;
370
+ }
371
+ return false;
372
+ },
238
373
  onBlur: runtime.dismissOverlay,
239
374
  });
240
375
 
241
376
  runtime.openOverlay({ select, title });
242
377
  };
243
378
 
244
- const showCommandAutocomplete = (state: AppState): void => {
245
- const items = COMMANDS.map((command) => ({
246
- label: `/${command} ${COMMAND_DESCRIPTIONS[command] ?? ""}`,
247
- value: command,
248
- filterText: command,
379
+ const handleSkillCommand = (state: AppState): void => {
380
+ const items = state.skills.map((skill) => ({
381
+ label: formatSkillLabel(skill),
382
+ value: skill.name,
383
+ filterText: `${skill.name} ${skill.description ?? ""}`,
249
384
  }));
250
385
 
251
- runtime.setInputValue("");
386
+ openSelectOverlay(
387
+ state,
388
+ "Select a skill",
389
+ items,
390
+ "type to filter skills...",
391
+ (skillName) => {
392
+ runtime.setInputValue(`/${SKILL_COMMAND}:${skillName}`);
393
+ runtime.dismissOverlay();
394
+ },
395
+ );
396
+ };
397
+
398
+ const showCommandAutocomplete = (state: AppState): void => {
399
+ const items: OverlayItem[] = [
400
+ ...COMMANDS.map((command) => ({
401
+ label: `/${command} ${COMMAND_DESCRIPTIONS[command] ?? ""}`,
402
+ value: command,
403
+ filterText: command,
404
+ })),
405
+ {
406
+ label: `${SKILL_REFERENCE_LABEL} ${SKILL_REFERENCE_DESCRIPTION}`,
407
+ value: SKILL_COMMAND,
408
+ filterText: `${SKILL_COMMAND} ${SKILL_REFERENCE_LABEL}`,
409
+ },
410
+ ];
411
+
252
412
  openSelectOverlay(
253
413
  state,
254
414
  "Commands",
@@ -256,6 +416,12 @@ export function createCommandController(
256
416
  "type to filter commands...",
257
417
  (value) => {
258
418
  runtime.dismissOverlay();
419
+ if (value === SKILL_COMMAND) {
420
+ handleSkillCommand(state);
421
+ return;
422
+ }
423
+
424
+ runtime.setInputValue("");
259
425
  handleCommand(value, state);
260
426
  },
261
427
  );
@@ -376,11 +542,17 @@ export function createCommandController(
376
542
  items,
377
543
  "type to filter sessions...",
378
544
  (sessionId) => {
545
+ clearQueuedUserMessages(state);
379
546
  if (sessionId !== currentSessionId) {
380
547
  const picked = sessions.find((session) => session.id === sessionId);
381
548
  if (picked) {
382
549
  state.session = picked;
383
550
  replaceConversationState(state, loadMessages(state.db, picked.id));
551
+ state.stats = computeSessionStats(state.db, picked.id);
552
+ state.contextTokens = computeSessionContextTokens(
553
+ state.db,
554
+ picked.id,
555
+ );
384
556
  runtime.scrollConversationToBottom();
385
557
  }
386
558
  }
@@ -394,6 +566,7 @@ export function createCommandController(
394
566
  if (state.running) {
395
567
  return;
396
568
  }
569
+ clearQueuedUserMessages(state);
397
570
  state.session = null;
398
571
  clearConversationState(state);
399
572
  await runtime.reloadPromptContext(state);
@@ -408,10 +581,14 @@ export function createCommandController(
408
581
  const forked = forkSession(state.db, state.session.id);
409
582
  state.session = forked;
410
583
  replaceConversationState(state, loadMessages(state.db, forked.id));
584
+ state.stats = computeSessionStats(state.db, forked.id);
585
+ state.contextTokens = computeSessionContextTokens(state.db, forked.id);
411
586
  runtime.appendInfoMessage("Forked session.", state);
412
587
  };
413
588
 
414
589
  const handleUndoCommand = async (state: AppState): Promise<void> => {
590
+ clearQueuedUserMessages(state);
591
+
415
592
  if (state.running && state.abortController) {
416
593
  state.abortController.abort();
417
594
  }
@@ -426,6 +603,11 @@ export function createCommandController(
426
603
  const removed = undoLastTurn(state.db, state.session.id);
427
604
  if (removed) {
428
605
  replaceConversationState(state, loadMessages(state.db, state.session.id));
606
+ state.stats = computeSessionStats(state.db, state.session.id);
607
+ state.contextTokens = computeSessionContextTokens(
608
+ state.db,
609
+ state.session.id,
610
+ );
429
611
  runtime.scrollConversationToBottom();
430
612
  runtime.requestRender("normal");
431
613
  }
@@ -445,6 +627,34 @@ export function createCommandController(
445
627
  });
446
628
  };
447
629
 
630
+ const handleMcpCommand = (state: AppState): void => {
631
+ if (state.mcpServers.length === 0) {
632
+ return;
633
+ }
634
+
635
+ const items = state.mcpServers.map((server) => ({
636
+ label: formatMcpServerLabel(server),
637
+ value: server.name,
638
+ filterText: `${server.name} ${server.url} ${server.enabled ? "on" : "off"}`,
639
+ }));
640
+
641
+ openSelectOverlay(
642
+ state,
643
+ "Toggle MCP servers",
644
+ items,
645
+ "type to filter MCP servers...",
646
+ (serverName) => {
647
+ const server = state.mcpServers.find(
648
+ (entry) => entry.name === serverName,
649
+ );
650
+ runtime.dismissOverlay();
651
+ if (server) {
652
+ void toggleMcpServer(server, state, runtime, deps);
653
+ }
654
+ },
655
+ );
656
+ };
657
+
448
658
  const performLogin = async (
449
659
  provider: OAuthProviderInterface,
450
660
  state: AppState,
@@ -611,12 +821,18 @@ export function createCommandController(
611
821
  case "verbose":
612
822
  handleVerboseCommand(state);
613
823
  return true;
824
+ case "mcp":
825
+ handleMcpCommand(state);
826
+ return true;
614
827
  case "todo":
615
828
  handleTodoCommand(state);
616
829
  return true;
617
830
  case "help":
618
831
  handleHelpCommand(state);
619
832
  return true;
833
+ case SKILL_COMMAND:
834
+ handleSkillCommand(state);
835
+ return true;
620
836
  default:
621
837
  return false;
622
838
  }
@@ -55,17 +55,6 @@ function collectTextNodes(node: Node | null): TextNode[] {
55
55
  return node.children.flatMap((child) => collectTextNodes(child));
56
56
  }
57
57
 
58
- function findTextNode(node: Node | null, content: string): TextNode {
59
- const textNode = collectTextNodes(node).find(
60
- (text) => text.content === content,
61
- );
62
- expect(textNode).toBeDefined();
63
- if (!textNode) {
64
- throw new Error(`Missing text node: ${content}`);
65
- }
66
- return textNode;
67
- }
68
-
69
58
  function makeAssistantToolCallMessage(): AssistantMessage {
70
59
  return {
71
60
  role: "assistant",
@@ -146,7 +135,7 @@ describe("ui/conversation", () => {
146
135
  expect(lines.join("\n")).not.toContain('"path"');
147
136
  });
148
137
 
149
- test("shell tool-call previews use cel-tui syntax scopes for custom colors", () => {
138
+ test("shell tool-call previews render the command instead of raw JSON", () => {
150
139
  const node = renderAssistantMessage(
151
140
  {
152
141
  content: [
@@ -169,9 +158,220 @@ describe("ui/conversation", () => {
169
158
  },
170
159
  );
171
160
 
172
- expect(findTextNode(node, "true").props.fgColor).toBe(
173
- DEFAULT_THEME.secondaryAccentText,
161
+ const lines = collectRenderedLines(node);
162
+ expect(lines).toContain("│ shell ->");
163
+ expect(lines.join("\n")).toContain('if true; then echo "$HOME"; fi');
164
+ expect(lines.join("\n")).not.toContain('"command"');
165
+ });
166
+
167
+ test("compact shell tool-call previews only render the visible tail slice", () => {
168
+ const command = Array.from(
169
+ { length: 14 },
170
+ (_, index) => `echo line ${index + 1}`,
171
+ ).join("\n");
172
+
173
+ const compactLines = collectRenderedLines(
174
+ renderAssistantMessage(
175
+ {
176
+ content: [
177
+ {
178
+ type: "toolCall",
179
+ id: "call-shell",
180
+ name: "shell",
181
+ arguments: { command },
182
+ },
183
+ ],
184
+ },
185
+ {
186
+ showReasoning: true,
187
+ verbose: false,
188
+ theme: DEFAULT_THEME,
189
+ cwd: "/tmp/project",
190
+ previewWidth: 80,
191
+ },
192
+ ),
193
+ );
194
+ const verboseLines = collectRenderedLines(
195
+ renderAssistantMessage(
196
+ {
197
+ content: [
198
+ {
199
+ type: "toolCall",
200
+ id: "call-shell",
201
+ name: "shell",
202
+ arguments: { command },
203
+ },
204
+ ],
205
+ },
206
+ {
207
+ showReasoning: true,
208
+ verbose: true,
209
+ theme: DEFAULT_THEME,
210
+ cwd: "/tmp/project",
211
+ previewWidth: 80,
212
+ },
213
+ ),
174
214
  );
215
+
216
+ expect(compactLines).toContain("│ shell ->");
217
+ expect(compactLines).toContain("│ echo line 14");
218
+ expect(compactLines).toContain("│ And 6 lines more");
219
+ expect(compactLines).not.toContain("│ echo line 1");
220
+ expect(verboseLines).toContain("│ echo line 1");
221
+ expect(compactLines.length).toBeLessThan(verboseLines.length);
222
+ });
223
+
224
+ test("MCP tool-call previews honor verbose mode", () => {
225
+ const args = {
226
+ query: "routing",
227
+ section: "guides",
228
+ limit: 10,
229
+ offset: 0,
230
+ filter1: "alpha",
231
+ filter2: "beta",
232
+ filter3: "gamma",
233
+ filter4: "delta",
234
+ filter5: "epsilon",
235
+ filter6: "zeta",
236
+ filter7: "eta",
237
+ filter8: "theta",
238
+ };
239
+
240
+ const compactLines = collectRenderedLines(
241
+ renderAssistantMessage(
242
+ {
243
+ content: [
244
+ {
245
+ type: "toolCall",
246
+ id: "call-mcp",
247
+ name: "docs__search",
248
+ arguments: args,
249
+ },
250
+ ],
251
+ },
252
+ {
253
+ showReasoning: true,
254
+ verbose: false,
255
+ theme: DEFAULT_THEME,
256
+ cwd: "/tmp/project",
257
+ previewWidth: 80,
258
+ },
259
+ ),
260
+ );
261
+ const verboseLines = collectRenderedLines(
262
+ renderAssistantMessage(
263
+ {
264
+ content: [
265
+ {
266
+ type: "toolCall",
267
+ id: "call-mcp",
268
+ name: "docs__search",
269
+ arguments: args,
270
+ },
271
+ ],
272
+ },
273
+ {
274
+ showReasoning: true,
275
+ verbose: true,
276
+ theme: DEFAULT_THEME,
277
+ cwd: "/tmp/project",
278
+ previewWidth: 80,
279
+ },
280
+ ),
281
+ );
282
+
283
+ expect(compactLines).toContain("│ docs__search ->");
284
+ expect(compactLines.some((line) => line.includes("And "))).toBe(true);
285
+ expect(compactLines.join("\n")).not.toContain('"query": "routing"');
286
+ expect(verboseLines.join("\n")).toContain('"query": "routing"');
287
+ expect(compactLines.length).toBeLessThan(verboseLines.length);
288
+ });
289
+
290
+ test("shell tool results render structured stdout/stderr details without stderr labels or error styling", () => {
291
+ const stdout = Array.from(
292
+ { length: 12 },
293
+ (_, index) => `line ${index + 1}`,
294
+ ).join("\n");
295
+ const node = renderToolResult(
296
+ "shell",
297
+ { command: "run-tests" },
298
+ "Exit code: 1\nlegacy text should be ignored when details exist",
299
+ true,
300
+ {
301
+ showReasoning: true,
302
+ verbose: false,
303
+ theme: DEFAULT_THEME,
304
+ cwd: "/tmp/project",
305
+ previewWidth: 48,
306
+ },
307
+ {
308
+ stdout,
309
+ stderr: "boom",
310
+ exitCode: 1,
311
+ },
312
+ );
313
+ const lines = collectRenderedLines(node);
314
+ const textNodes = collectTextNodes(node);
315
+
316
+ expect(lines).toContain("│ shell <-");
317
+ expect(lines).toContain("│ boom");
318
+ expect(lines).toContain("│ exit 1");
319
+ expect(lines).toContain("│ And 6 lines more");
320
+ expect(lines.join("\n")).not.toContain("stderr:");
321
+ expect(lines.join("\n")).not.toContain("legacy text should be ignored");
322
+ expect(
323
+ textNodes.some(
324
+ (textNode) =>
325
+ textNode.content === "boom" &&
326
+ textNode.props.fgColor === DEFAULT_THEME.toolText,
327
+ ),
328
+ ).toBe(true);
329
+ expect(
330
+ textNodes.some(
331
+ (textNode) =>
332
+ textNode.content === "exit 1" &&
333
+ textNode.props.fgColor === DEFAULT_THEME.toolText,
334
+ ),
335
+ ).toBe(true);
336
+ });
337
+
338
+ test("shell tool results still render legacy flattened results from persisted history without stderr labels", () => {
339
+ const node = renderToolResult(
340
+ "shell",
341
+ { command: "run-tests" },
342
+ "Exit code: 1\nout\n\n[stderr]\nerr",
343
+ true,
344
+ {
345
+ showReasoning: true,
346
+ verbose: true,
347
+ theme: DEFAULT_THEME,
348
+ cwd: "/tmp/project",
349
+ previewWidth: 80,
350
+ },
351
+ );
352
+ const lines = collectRenderedLines(node);
353
+ const textNodes = collectTextNodes(node);
354
+
355
+ expect(lines).toContain("│ out");
356
+ expect(lines).toContain("│ err");
357
+ expect(lines).toContain("│ exit 1");
358
+ expect(lines.join("\n")).not.toContain("stderr:");
359
+ expect(lines.join("\n")).not.toContain("[stderr]");
360
+ expect(lines.join("\n")).not.toContain("Exit code:");
361
+ expect(
362
+ textNodes.some(
363
+ (textNode) =>
364
+ textNode.content === "err" &&
365
+ textNode.props.fgColor === DEFAULT_THEME.toolText,
366
+ ),
367
+ ).toBe(true);
368
+ expect(
369
+ textNodes.some(
370
+ (textNode) =>
371
+ textNode.content === "exit 1" &&
372
+ textNode.props.fgColor === DEFAULT_THEME.toolText,
373
+ ),
374
+ ).toBe(true);
175
375
  });
176
376
 
177
377
  test("read tool results include the resolved path, hide model paging hints, and render fewer body lines when verbose is off", () => {
@@ -292,17 +492,12 @@ describe("ui/conversation", () => {
292
492
  },
293
493
  );
294
494
 
295
- expect(findTextNode(node, "42").props.fgColor).toBe(
296
- DEFAULT_THEME.secondaryAccentText,
297
- );
298
- expect(
299
- findTextNode(node, "supercalifragilisticexpialidociousIdentifier")
300
- .content,
301
- ).toBe("supercalifragilisticexpialidociousIdentifier");
302
495
  expect(
303
- findTextNode(node, "supercalifragilisticexpialidociousIdentifier").props
304
- .fgColor,
305
- ).toBeUndefined();
496
+ collectTextNodes(node).some(
497
+ (textNode) =>
498
+ textNode.content === "supercalifragilisticexpialidociousIdentifier",
499
+ ),
500
+ ).toBe(true);
306
501
  });
307
502
 
308
503
  test("grep tool results render grouped files and lines instead of raw JSON", () => {
@@ -353,8 +548,38 @@ describe("ui/conversation", () => {
353
548
  expect(lines).toContain("│ 858: spec: ToolBlockSpec,");
354
549
  expect(lines.join("\n")).not.toContain('"files"');
355
550
  expect(lines.join("\n")).not.toContain('"kind"');
356
- expect(
357
- findTextNode(node, " 857: function renderToolBlock(").props.fgColor,
358
- ).toBe(DEFAULT_THEME.toolText);
551
+ });
552
+
553
+ test("MCP tool results honor verbose mode", () => {
554
+ const output = Array.from(
555
+ { length: 14 },
556
+ (_, index) => `result ${index + 1}`,
557
+ ).join("\n");
558
+
559
+ const compactLines = collectRenderedLines(
560
+ renderToolResult("docs__search", { query: "routing" }, output, false, {
561
+ showReasoning: true,
562
+ verbose: false,
563
+ theme: DEFAULT_THEME,
564
+ cwd: "/tmp/project",
565
+ previewWidth: 80,
566
+ }),
567
+ );
568
+ const verboseLines = collectRenderedLines(
569
+ renderToolResult("docs__search", { query: "routing" }, output, false, {
570
+ showReasoning: true,
571
+ verbose: true,
572
+ theme: DEFAULT_THEME,
573
+ cwd: "/tmp/project",
574
+ previewWidth: 80,
575
+ }),
576
+ );
577
+
578
+ expect(compactLines).toContain("│ docs__search <-");
579
+ expect(compactLines).toContain("│ result 14");
580
+ expect(compactLines).toContain("│ And 6 lines more");
581
+ expect(compactLines).not.toContain("│ result 1");
582
+ expect(verboseLines).toContain("│ result 1");
583
+ expect(compactLines.length).toBeLessThan(verboseLines.length);
359
584
  });
360
585
  });