@iinm/plain-agent 1.10.2 → 1.10.4
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/README.md +2 -2
- package/package.json +2 -1
- package/src/{cliBatch.mjs → cli/batch.mjs} +3 -3
- package/src/{cliCommands.mjs → cli/commands.mjs} +13 -11
- package/src/{cliCompleter.mjs → cli/completer.mjs} +4 -4
- package/src/{cliCost.mjs → cli/cost.mjs} +3 -3
- package/src/cli/eastAsianWideRanges.json +124 -0
- package/src/{cliFormatter.mjs → cli/formatter.mjs} +369 -23
- package/src/{cliInteractive.mjs → cli/interactive.mjs} +48 -14
- package/src/cli/tableDetector.mjs +228 -0
- package/src/config.d.ts +1 -1
- package/src/main.mjs +5 -5
- package/src/{mcpIntegration.mjs → mcp/integration.mjs} +7 -7
- package/src/tools/patchFile.mjs +18 -12
- package/src/{voiceInputGemini.mjs → voice/gemini.mjs} +2 -5
- package/src/voice/input.mjs +29 -0
- package/src/{voiceInputOpenAI.mjs → voice/openai.mjs} +15 -17
- package/src/voiceInput.mjs +0 -61
- /package/src/{cliArgs.mjs → cli/args.mjs} +0 -0
- /package/src/{cliInterruptTransform.mjs → cli/interruptTransform.mjs} +0 -0
- /package/src/{cliMuteTransform.mjs → cli/muteTransform.mjs} +0 -0
- /package/src/{cliPasteTransform.mjs → cli/pasteTransform.mjs} +0 -0
- /package/src/{mcpClient.mjs → mcp/client.mjs} +0 -0
- /package/src/{voiceInputSession.mjs → voice/session.mjs} +0 -0
- /package/src/{voiceToggleKey.mjs → voice/toggleKey.mjs} +0 -0
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @import { Message, MessageContentToolUse, MessageContentToolResult, ProviderTokenUsage } from "
|
|
3
|
-
* @import { CompactContextInput } from "
|
|
4
|
-
* @import { ExecCommandInput } from "
|
|
5
|
-
* @import { PatchBlock, PatchFileInput } from "
|
|
6
|
-
* @import { ReadFileInput } from "
|
|
7
|
-
* @import { WriteFileInput } from "
|
|
8
|
-
* @import { TmuxCommandInput } from "
|
|
9
|
-
* @import { SwitchToSubagentInput } from "
|
|
2
|
+
* @import { Message, MessageContentToolUse, MessageContentToolResult, ProviderTokenUsage } from "../model"
|
|
3
|
+
* @import { CompactContextInput } from "../tools/compactContext"
|
|
4
|
+
* @import { ExecCommandInput } from "../tools/execCommand"
|
|
5
|
+
* @import { PatchBlock, PatchFileInput } from "../tools/patchFile"
|
|
6
|
+
* @import { ReadFileInput } from "../tools/readFile"
|
|
7
|
+
* @import { WriteFileInput } from "../tools/writeFile"
|
|
8
|
+
* @import { TmuxCommandInput } from "../tools/tmuxCommand"
|
|
9
|
+
* @import { SwitchToSubagentInput } from "../tools/switchToSubagent"
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import fs from "node:fs/promises";
|
|
13
13
|
import { styleText } from "node:util";
|
|
14
|
-
import { parseBlocks } from "
|
|
15
|
-
import { diffLines } from "
|
|
16
|
-
import { noThrow } from "
|
|
14
|
+
import { parseBlocks } from "../tools/patchFile.mjs";
|
|
15
|
+
import { diffLines } from "../utils/diffLines.mjs";
|
|
16
|
+
import { noThrow } from "../utils/noThrow.mjs";
|
|
17
17
|
|
|
18
18
|
/** Length above which a single-line arg forces block-form rendering. */
|
|
19
19
|
const ARG_BLOCK_LENGTH_THRESHOLD = 60;
|
|
@@ -144,7 +144,7 @@ export async function formatToolUse(toolUse) {
|
|
|
144
144
|
}
|
|
145
145
|
|
|
146
146
|
if (toolName === "switch_to_main_agent") {
|
|
147
|
-
/** @type {Partial<import("
|
|
147
|
+
/** @type {Partial<import("../tools/switchToMainAgent").SwitchToMainAgentInput>} */
|
|
148
148
|
const switchToMainAgentInput = input;
|
|
149
149
|
return [
|
|
150
150
|
`tool: ${toolName}`,
|
|
@@ -153,7 +153,7 @@ export async function formatToolUse(toolUse) {
|
|
|
153
153
|
}
|
|
154
154
|
|
|
155
155
|
if (toolName === "web_search") {
|
|
156
|
-
/** @type {Partial<import("
|
|
156
|
+
/** @type {Partial<import("../tools/webSearch.mjs").WebSearchInput>} */
|
|
157
157
|
const webSearchInput = input;
|
|
158
158
|
const searchesLine = webSearchInput.searches
|
|
159
159
|
? webSearchInput.searches.map((s) => s.keywords.join(" ")).join(" | ")
|
|
@@ -166,7 +166,7 @@ export async function formatToolUse(toolUse) {
|
|
|
166
166
|
}
|
|
167
167
|
|
|
168
168
|
if (toolName === "web_fetch") {
|
|
169
|
-
/** @type {Partial<import("
|
|
169
|
+
/** @type {Partial<import("../tools/webFetch.mjs").WebFetchInput>} */
|
|
170
170
|
const webFetchInput = input;
|
|
171
171
|
return [
|
|
172
172
|
`tool: ${toolName}`,
|
|
@@ -296,7 +296,7 @@ export function formatProviderTokenUsage(usage) {
|
|
|
296
296
|
|
|
297
297
|
/**
|
|
298
298
|
* Format cost summary for interactive display
|
|
299
|
-
* @param {import("
|
|
299
|
+
* @param {import("../costTracker.mjs").CostSummary} summary
|
|
300
300
|
* @returns {string}
|
|
301
301
|
*/
|
|
302
302
|
export function formatCostSummary(summary) {
|
|
@@ -334,7 +334,7 @@ export function formatCostSummary(summary) {
|
|
|
334
334
|
|
|
335
335
|
/**
|
|
336
336
|
* Format cost for batch mode JSON output
|
|
337
|
-
* @param {import("
|
|
337
|
+
* @param {import("../costTracker.mjs").CostSummary} summary
|
|
338
338
|
*/
|
|
339
339
|
export function formatCostForBatch(summary) {
|
|
340
340
|
if (!summary || Object.keys(summary.breakdown).length === 0) {
|
|
@@ -428,6 +428,348 @@ export async function printMessage(message) {
|
|
|
428
428
|
}
|
|
429
429
|
}
|
|
430
430
|
}
|
|
431
|
+
/**
|
|
432
|
+
* Format markdown table lines with aligned columns.
|
|
433
|
+
* Input lines may have leading/trailing pipes.
|
|
434
|
+
* Output always has leading and trailing pipes with padded cells.
|
|
435
|
+
* When the table would exceed `maxWidth`, long cells are wrapped onto
|
|
436
|
+
* additional visual lines so the table stays within the terminal width.
|
|
437
|
+
* @param {string[]} lines - Raw table lines (including alignment row)
|
|
438
|
+
* @param {number} [maxWidth=Infinity] - Maximum terminal display width
|
|
439
|
+
* @returns {string} - Formatted table string with aligned columns
|
|
440
|
+
*/
|
|
441
|
+
export function formatMarkdownTable(
|
|
442
|
+
lines,
|
|
443
|
+
maxWidth = Number.POSITIVE_INFINITY,
|
|
444
|
+
) {
|
|
445
|
+
if (lines.length === 0) return "";
|
|
446
|
+
|
|
447
|
+
const rows = lines.map(splitTableRow);
|
|
448
|
+
|
|
449
|
+
// Calculate max display width for each column (natural width)
|
|
450
|
+
const colCount = Math.max(...rows.map((r) => r.length));
|
|
451
|
+
/** @type {number[]} */
|
|
452
|
+
const naturalWidths = new Array(colCount).fill(0);
|
|
453
|
+
for (const row of rows) {
|
|
454
|
+
for (let i = 0; i < row.length; i++) {
|
|
455
|
+
const width = charDisplayWidth(row[i]);
|
|
456
|
+
if (width > naturalWidths[i]) {
|
|
457
|
+
naturalWidths[i] = width;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// Determine column widths that fit within maxWidth
|
|
463
|
+
const colWidths = fitColumns(naturalWidths, colCount, maxWidth);
|
|
464
|
+
|
|
465
|
+
// Check if wrapping is needed (any column was shrunk)
|
|
466
|
+
const needsWrapping = colWidths.some((w, i) => w < naturalWidths[i]);
|
|
467
|
+
|
|
468
|
+
if (!needsWrapping) {
|
|
469
|
+
// Original path: no wrapping, just pad and join
|
|
470
|
+
return rows
|
|
471
|
+
.map((row) => {
|
|
472
|
+
const fullRow = row.concat(new Array(colCount - row.length).fill(""));
|
|
473
|
+
const padded = fullRow.map((cell, i) =>
|
|
474
|
+
padCell(cell, colWidths[i] ?? 0),
|
|
475
|
+
);
|
|
476
|
+
return `| ${padded.join(" | ")} |`;
|
|
477
|
+
})
|
|
478
|
+
.join("\n");
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// Wrapped path: wrap cells and render multi-line rows
|
|
482
|
+
const wrappedRows = rows.map((row) => {
|
|
483
|
+
const fullRow = row.concat(new Array(colCount - row.length).fill(""));
|
|
484
|
+
const isSeparator = isSeparatorRow(fullRow);
|
|
485
|
+
return fullRow.map((cell, i) => {
|
|
486
|
+
if (isSeparator) {
|
|
487
|
+
// Regenerate separator dashes to fit the column width (no wrapping)
|
|
488
|
+
return ["-".repeat(colWidths[i])];
|
|
489
|
+
}
|
|
490
|
+
return wrapCell(cell, colWidths[i]);
|
|
491
|
+
});
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
return wrappedRows
|
|
495
|
+
.map((wrappedCells) => renderWrappedRow(wrappedCells, colWidths))
|
|
496
|
+
.join("\n");
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* Check if a row is a markdown table separator row.
|
|
501
|
+
* A separator row contains only dashes, colons, and spaces
|
|
502
|
+
* (e.g., "------", ":----:", "-----:", ":-----").
|
|
503
|
+
* @param {string[]} cells
|
|
504
|
+
* @returns {boolean}
|
|
505
|
+
*/
|
|
506
|
+
function isSeparatorRow(cells) {
|
|
507
|
+
return cells.length > 0 && cells.every((cell) => /^[-: ]+$/.test(cell));
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* Determine column widths that fit within maxWidth.
|
|
512
|
+
* If the natural total width fits, returns natural widths unchanged.
|
|
513
|
+
* Otherwise, shrinks columns proportionally (minimum 3 chars each).
|
|
514
|
+
* Returns null in any entry if the table cannot fit at all (fallback signal).
|
|
515
|
+
* @param {number[]} naturalWidths - Natural (max content) width per column
|
|
516
|
+
* @param {number} colCount - Number of columns
|
|
517
|
+
* @param {number} maxWidth - Available terminal width
|
|
518
|
+
* @returns {number[]} - Target width per column
|
|
519
|
+
*/
|
|
520
|
+
function fitColumns(naturalWidths, colCount, maxWidth) {
|
|
521
|
+
const gutter = 4 + (colCount - 1) * 3; // "| " + " |" + inter-column " | "
|
|
522
|
+
const available = maxWidth - gutter;
|
|
523
|
+
|
|
524
|
+
// If natural widths fit, use them as-is
|
|
525
|
+
const totalNatural = naturalWidths.reduce((s, w) => s + w, 0);
|
|
526
|
+
if (totalNatural <= available || maxWidth === Number.POSITIVE_INFINITY) {
|
|
527
|
+
return naturalWidths;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// Shrink: allocate minimum width first, then distribute remainder proportionally
|
|
531
|
+
const minWidth = 3;
|
|
532
|
+
const minTotal = minWidth * colCount;
|
|
533
|
+
|
|
534
|
+
if (minTotal > available) {
|
|
535
|
+
// Cannot fit even at minimum — return natural widths (will overflow)
|
|
536
|
+
return naturalWidths;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
const result = naturalWidths.map(() => minWidth);
|
|
540
|
+
const remaining = available - minTotal;
|
|
541
|
+
|
|
542
|
+
// Distribute remaining space proportionally to natural widths
|
|
543
|
+
const naturalTotalAboveMin = naturalWidths.reduce(
|
|
544
|
+
(s, w) => s + Math.max(0, w - minWidth),
|
|
545
|
+
0,
|
|
546
|
+
);
|
|
547
|
+
|
|
548
|
+
if (naturalTotalAboveMin > 0) {
|
|
549
|
+
for (let i = 0; i < colCount; i++) {
|
|
550
|
+
const aboveMin = Math.max(0, naturalWidths[i] - minWidth);
|
|
551
|
+
const share = Math.round((aboveMin / naturalTotalAboveMin) * remaining);
|
|
552
|
+
result[i] = minWidth + share;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// Adjust for rounding: distribute leftover pixels to widest columns
|
|
556
|
+
const currentTotal = result.reduce((s, w) => s + w, 0);
|
|
557
|
+
let diff = available - currentTotal;
|
|
558
|
+
// Sort column indices by natural width descending for fair distribution
|
|
559
|
+
const sortedIndices = naturalWidths
|
|
560
|
+
.map((w, i) => /** @type {[number, number]} */ ([i, w]))
|
|
561
|
+
.sort((a, b) => b[1] - a[1])
|
|
562
|
+
.map(([i]) => i);
|
|
563
|
+
let idx = 0;
|
|
564
|
+
while (diff > 0) {
|
|
565
|
+
result[sortedIndices[idx % colCount]]++;
|
|
566
|
+
diff--;
|
|
567
|
+
idx++;
|
|
568
|
+
}
|
|
569
|
+
while (diff < 0) {
|
|
570
|
+
result[sortedIndices[idx % colCount]]--;
|
|
571
|
+
diff++;
|
|
572
|
+
idx++;
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
return result;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
/**
|
|
580
|
+
* Wrap a cell's content to fit within the given display width.
|
|
581
|
+
* Respects ANSI escape codes (does not break them) and CJK wide characters.
|
|
582
|
+
* @param {string} text - Cell content (may contain ANSI codes)
|
|
583
|
+
* @param {number} width - Maximum display width per line
|
|
584
|
+
* @returns {string[]} - Array of visual lines for this cell
|
|
585
|
+
*/
|
|
586
|
+
function wrapCell(text, width) {
|
|
587
|
+
if (width <= 0) return [text];
|
|
588
|
+
const textWidth = charDisplayWidth(text);
|
|
589
|
+
if (textWidth <= width) return [text];
|
|
590
|
+
|
|
591
|
+
// Build segments: each segment is either an ANSI escape code or a visible character
|
|
592
|
+
/** @type {{ text: string, displayWidth: number }[]} */
|
|
593
|
+
const segments = [];
|
|
594
|
+
let i = 0;
|
|
595
|
+
while (i < text.length) {
|
|
596
|
+
// Check for ANSI escape sequence
|
|
597
|
+
// biome-ignore lint/suspicious/noControlCharactersInRegex: ANSI escape code pattern
|
|
598
|
+
const ansiMatch = text.slice(i).match(/^\u001b\[[0-9;]*m/);
|
|
599
|
+
if (ansiMatch) {
|
|
600
|
+
segments.push({ text: ansiMatch[0], displayWidth: 0 });
|
|
601
|
+
i += ansiMatch[0].length;
|
|
602
|
+
} else {
|
|
603
|
+
const ch = text[i];
|
|
604
|
+
const code = /** @type {number} */ (ch.codePointAt(0));
|
|
605
|
+
const isWide = isWideChar(code);
|
|
606
|
+
segments.push({ text: ch, displayWidth: isWide ? 2 : 1 });
|
|
607
|
+
i++;
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
// Group segments into lines
|
|
612
|
+
/** @type {string[]} */
|
|
613
|
+
const lines = [];
|
|
614
|
+
/** @type {string} */
|
|
615
|
+
let currentLine = "";
|
|
616
|
+
let currentWidth = 0;
|
|
617
|
+
|
|
618
|
+
for (const seg of segments) {
|
|
619
|
+
if (seg.displayWidth === 0) {
|
|
620
|
+
// ANSI code: attach to current line without increasing width
|
|
621
|
+
currentLine += seg.text;
|
|
622
|
+
continue;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
if (currentWidth + seg.displayWidth > width) {
|
|
626
|
+
// This character would overflow — start a new line
|
|
627
|
+
lines.push(currentLine);
|
|
628
|
+
currentLine = seg.text;
|
|
629
|
+
currentWidth = seg.displayWidth;
|
|
630
|
+
} else {
|
|
631
|
+
currentLine += seg.text;
|
|
632
|
+
currentWidth += seg.displayWidth;
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
if (currentLine.length > 0 || lines.length === 0) {
|
|
637
|
+
lines.push(currentLine);
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
return lines;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
/**
|
|
644
|
+
* Sorted, merged [start, end] ranges of Unicode code points with
|
|
645
|
+
* East_Asian_Width property "W" (Wide) or "F" (Fullwidth).
|
|
646
|
+
*
|
|
647
|
+
* Generated by: node scripts/fetchEastAsianWideRanges.mjs
|
|
648
|
+
* Source: https://www.unicode.org/Public/16.0.0/ucd/EastAsianWidth.txt
|
|
649
|
+
*/
|
|
650
|
+
import WIDE_RANGES from "./eastAsianWideRanges.json" with { type: "json" };
|
|
651
|
+
|
|
652
|
+
/**
|
|
653
|
+
* Check if a Unicode code point is a wide (double-width) character.
|
|
654
|
+
* Uses binary search over the sorted WIDE_RANGES for efficiency.
|
|
655
|
+
* @param {number} code
|
|
656
|
+
* @returns {boolean}
|
|
657
|
+
*/
|
|
658
|
+
function isWideChar(code) {
|
|
659
|
+
let lo = 0;
|
|
660
|
+
let hi = WIDE_RANGES.length - 1;
|
|
661
|
+
while (lo <= hi) {
|
|
662
|
+
const mid = (lo + hi) >>> 1;
|
|
663
|
+
const [start, end] = WIDE_RANGES[mid];
|
|
664
|
+
if (code < start) hi = mid - 1;
|
|
665
|
+
else if (code > end) lo = mid + 1;
|
|
666
|
+
else return true;
|
|
667
|
+
}
|
|
668
|
+
return false;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
/**
|
|
672
|
+
* Render a wrapped row (where cells may span multiple visual lines).
|
|
673
|
+
* Each cell's visual lines are padded to the column width, and cells
|
|
674
|
+
* are aligned horizontally across visual lines.
|
|
675
|
+
* @param {string[][]} wrappedCells - Array of visual-line arrays per cell
|
|
676
|
+
* @param {number[]} colWidths - Target display width per column
|
|
677
|
+
* @returns {string} - Rendered row (may contain embedded newlines)
|
|
678
|
+
*/
|
|
679
|
+
function renderWrappedRow(wrappedCells, colWidths) {
|
|
680
|
+
const maxLines = Math.max(...wrappedCells.map((c) => c.length));
|
|
681
|
+
const visualLines = [];
|
|
682
|
+
for (let lineIdx = 0; lineIdx < maxLines; lineIdx++) {
|
|
683
|
+
const parts = wrappedCells.map((cell, colIdx) => {
|
|
684
|
+
const text = cell[lineIdx] ?? "";
|
|
685
|
+
return padCell(text, colWidths[colIdx]);
|
|
686
|
+
});
|
|
687
|
+
visualLines.push(`| ${parts.join(" | ")} |`);
|
|
688
|
+
}
|
|
689
|
+
return visualLines.join("\n");
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
/** @type {RegExp} - ANSI escape code pattern */
|
|
693
|
+
// biome-ignore lint/suspicious/noControlCharactersInRegex: ANSI escape code pattern
|
|
694
|
+
const ANSI_RE = /\u001b\[[0-9;]*m/g;
|
|
695
|
+
|
|
696
|
+
/**
|
|
697
|
+
* Strip ANSI escape codes for display width calculation.
|
|
698
|
+
* @param {string} str
|
|
699
|
+
* @returns {string}
|
|
700
|
+
*/
|
|
701
|
+
function stripAnsiCodes(str) {
|
|
702
|
+
return str.replace(ANSI_RE, "");
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
/**
|
|
706
|
+
* Calculate the terminal display width of a string.
|
|
707
|
+
* CJK full-width characters and emoji count as 2 columns; ASCII as 1.
|
|
708
|
+
* ANSI escape codes are stripped before measurement.
|
|
709
|
+
* @param {string} str
|
|
710
|
+
* @returns {number}
|
|
711
|
+
*/
|
|
712
|
+
function charDisplayWidth(str) {
|
|
713
|
+
const plain = stripAnsiCodes(str);
|
|
714
|
+
let width = 0;
|
|
715
|
+
for (const ch of plain) {
|
|
716
|
+
const code = /** @type {number} */ (ch.codePointAt(0));
|
|
717
|
+
width += isWideChar(code) ? 2 : 1;
|
|
718
|
+
}
|
|
719
|
+
return width;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
/**
|
|
723
|
+
* Split a markdown table row into cells.
|
|
724
|
+
* Removes leading/trailing pipes, splits by `|`.
|
|
725
|
+
* Respects escaped pipes (`\|`).
|
|
726
|
+
* @param {string} line
|
|
727
|
+
* @returns {string[]}
|
|
728
|
+
*/
|
|
729
|
+
function splitTableRow(line) {
|
|
730
|
+
const trimmed = line.trim();
|
|
731
|
+
// Remove leading and trailing pipes
|
|
732
|
+
let inner;
|
|
733
|
+
if (trimmed.startsWith("|") && trimmed.endsWith("|")) {
|
|
734
|
+
inner = trimmed.slice(1, -1);
|
|
735
|
+
} else if (trimmed.startsWith("|")) {
|
|
736
|
+
inner = trimmed.slice(1);
|
|
737
|
+
} else if (trimmed.endsWith("|")) {
|
|
738
|
+
inner = trimmed.slice(0, -1);
|
|
739
|
+
} else {
|
|
740
|
+
inner = trimmed;
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
// Split by pipe, respecting escaped pipes
|
|
744
|
+
/** @type {string[]} */
|
|
745
|
+
const cells = [];
|
|
746
|
+
let current = "";
|
|
747
|
+
for (let i = 0; i < inner.length; i++) {
|
|
748
|
+
if (inner[i] === "\\" && i + 1 < inner.length && inner[i + 1] === "|") {
|
|
749
|
+
current += "|";
|
|
750
|
+
i++;
|
|
751
|
+
} else if (inner[i] === "|") {
|
|
752
|
+
cells.push(current);
|
|
753
|
+
current = "";
|
|
754
|
+
} else {
|
|
755
|
+
current += inner[i];
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
cells.push(current);
|
|
759
|
+
return cells.map((c) => c.trim());
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
/**
|
|
763
|
+
* Pad a cell string with trailing spaces to the given display width.
|
|
764
|
+
* @param {string} cell - Original cell content (may contain ANSI codes)
|
|
765
|
+
* @param {number} targetWidth - Target display width
|
|
766
|
+
* @returns {string}
|
|
767
|
+
*/
|
|
768
|
+
function padCell(cell, targetWidth) {
|
|
769
|
+
const currentWidth = charDisplayWidth(cell);
|
|
770
|
+
if (currentWidth >= targetWidth) return cell;
|
|
771
|
+
return cell + " ".repeat(targetWidth - currentWidth);
|
|
772
|
+
}
|
|
431
773
|
|
|
432
774
|
/**
|
|
433
775
|
* Render a patch_file `patch` string for terminal display.
|
|
@@ -455,7 +797,11 @@ async function renderPatch(filePath, patch) {
|
|
|
455
797
|
let blocks;
|
|
456
798
|
try {
|
|
457
799
|
blocks = parseBlocks(patch, nonce);
|
|
458
|
-
} catch {
|
|
800
|
+
} catch (err) {
|
|
801
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
802
|
+
console.error(
|
|
803
|
+
styleText("yellow", `Warning: Patch parsing failed: ${message}`),
|
|
804
|
+
);
|
|
459
805
|
return fallback;
|
|
460
806
|
}
|
|
461
807
|
|
|
@@ -485,7 +831,7 @@ function renderPatchBlock(block, originalLines, nonce) {
|
|
|
485
831
|
out.push(
|
|
486
832
|
styleText(
|
|
487
833
|
"cyan",
|
|
488
|
-
|
|
834
|
+
`>>> ${nonce} ${block.start}:${block.startHash}-${block.end}:${block.endHash}`,
|
|
489
835
|
),
|
|
490
836
|
);
|
|
491
837
|
if (originalLines) {
|
|
@@ -513,12 +859,12 @@ function renderPatchBlock(block, originalLines, nonce) {
|
|
|
513
859
|
}
|
|
514
860
|
} else {
|
|
515
861
|
const afterSuffix = block.afterHash ? `:${block.afterHash}` : "";
|
|
516
|
-
out.push(styleText("cyan",
|
|
862
|
+
out.push(styleText("cyan", `>>> ${nonce} ${block.after}${afterSuffix}+`));
|
|
517
863
|
for (const line of block.body) {
|
|
518
864
|
out.push(styleText("green", `+ ${line}`));
|
|
519
865
|
}
|
|
520
866
|
}
|
|
521
|
-
out.push(styleText("cyan",
|
|
867
|
+
out.push(styleText("cyan", `<<< ${nonce}`));
|
|
522
868
|
return out.join("\n");
|
|
523
869
|
}
|
|
524
870
|
|
|
@@ -532,8 +878,8 @@ function highlightPatchPlain(patch) {
|
|
|
532
878
|
if (!patch) {
|
|
533
879
|
return "";
|
|
534
880
|
}
|
|
535
|
-
// Patch
|
|
536
|
-
const headerRegex =
|
|
881
|
+
// Patch open/close markers look like ">>> <nonce> ..." or "<<< <nonce>".
|
|
882
|
+
const headerRegex = /^(>>>|<<<)\s+\S+(\s.*)?$/;
|
|
537
883
|
return patch
|
|
538
884
|
.split("\n")
|
|
539
885
|
.map((line) => {
|
|
@@ -554,7 +900,7 @@ function highlightPatchPlain(patch) {
|
|
|
554
900
|
* @returns {string | null}
|
|
555
901
|
*/
|
|
556
902
|
function extractPatchNonce(patch) {
|
|
557
|
-
const match = patch.match(
|
|
903
|
+
const match = patch.match(/^>>>\s+(\S+)/m);
|
|
558
904
|
return match ? match[1] : null;
|
|
559
905
|
}
|
|
560
906
|
|
|
@@ -1,25 +1,28 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @import { UserEventEmitter, AgentEventEmitter, AgentCommands } from "
|
|
3
|
-
* @import { ClaudeCodePlugin } from "
|
|
4
|
-
* @import { VoiceInputConfig
|
|
2
|
+
* @import { UserEventEmitter, AgentEventEmitter, AgentCommands } from "../agent"
|
|
3
|
+
* @import { ClaudeCodePlugin } from "../claudeCodePlugin.mjs"
|
|
4
|
+
* @import { VoiceInputConfig } from "../voice/input.mjs"
|
|
5
|
+
* @import { VoiceSession } from "../voice/session.mjs"
|
|
5
6
|
*/
|
|
6
7
|
|
|
7
8
|
import readline from "node:readline";
|
|
8
9
|
import { styleText } from "node:util";
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
10
|
+
import { appendUsageRecord, buildUsageRecord } from "../usageStore.mjs";
|
|
11
|
+
import { createSequentialExecutor } from "../utils/createSequentialExecutor.mjs";
|
|
12
|
+
import { notify } from "../utils/notify.mjs";
|
|
13
|
+
import { startVoiceSession } from "../voice/input.mjs";
|
|
14
|
+
import { parseVoiceToggleKey } from "../voice/toggleKey.mjs";
|
|
15
|
+
import { createCommandHandler } from "./commands.mjs";
|
|
16
|
+
import { createCompleter, SLASH_COMMANDS } from "./completer.mjs";
|
|
11
17
|
import {
|
|
12
18
|
formatCostSummary,
|
|
13
19
|
formatProviderTokenUsage,
|
|
14
20
|
printMessage,
|
|
15
|
-
} from "./
|
|
16
|
-
import { createInterruptTransform } from "./
|
|
17
|
-
import { createMuteTransform } from "./
|
|
18
|
-
import { createPasteHandler } from "./
|
|
19
|
-
import {
|
|
20
|
-
import { createSequentialExecutor } from "./utils/createSequentialExecutor.mjs";
|
|
21
|
-
import { notify } from "./utils/notify.mjs";
|
|
22
|
-
import { parseVoiceToggleKey, startVoiceSession } from "./voiceInput.mjs";
|
|
21
|
+
} from "./formatter.mjs";
|
|
22
|
+
import { createInterruptTransform } from "./interruptTransform.mjs";
|
|
23
|
+
import { createMuteTransform } from "./muteTransform.mjs";
|
|
24
|
+
import { createPasteHandler } from "./pasteTransform.mjs";
|
|
25
|
+
import { createTableDetector } from "./tableDetector.mjs";
|
|
23
26
|
|
|
24
27
|
const HELP_MESSAGE = [
|
|
25
28
|
"Commands:",
|
|
@@ -70,7 +73,7 @@ const HELP_MESSAGE = [
|
|
|
70
73
|
* Persist the session's cost summary to the usage log.
|
|
71
74
|
* Failures are logged but never thrown so exit is not blocked.
|
|
72
75
|
*
|
|
73
|
-
* @param {import("
|
|
76
|
+
* @param {import("../costTracker.mjs").CostSummary} summary
|
|
74
77
|
* @param {{ sessionId: string, modelName: string, startTime: Date }} meta
|
|
75
78
|
*/
|
|
76
79
|
async function persistUsage(summary, { sessionId, modelName, startTime }) {
|
|
@@ -122,6 +125,9 @@ export function startInteractiveSession({
|
|
|
122
125
|
*/
|
|
123
126
|
let voice = null;
|
|
124
127
|
|
|
128
|
+
// Create the table buffer instance for this session
|
|
129
|
+
const tableBuffer = createTableBuffer();
|
|
130
|
+
|
|
125
131
|
// Parse the voice toggle key once at startup so misconfiguration fails
|
|
126
132
|
// loudly instead of silently falling back.
|
|
127
133
|
const voiceToggle = parseVoiceToggleKey(voiceInput?.toggleKey);
|
|
@@ -465,6 +471,8 @@ export function startInteractiveSession({
|
|
|
465
471
|
if (partialContent.content) {
|
|
466
472
|
if (partialContent.type === "tool_use") {
|
|
467
473
|
process.stdout.write(styleText("gray", partialContent.content));
|
|
474
|
+
} else if (partialContent.type === "text") {
|
|
475
|
+
tableBuffer.feed(partialContent.content);
|
|
468
476
|
} else {
|
|
469
477
|
process.stdout.write(partialContent.content);
|
|
470
478
|
}
|
|
@@ -520,6 +528,9 @@ export function startInteractiveSession({
|
|
|
520
528
|
});
|
|
521
529
|
|
|
522
530
|
agentEventEmitter.on("turnEnd", async () => {
|
|
531
|
+
// Flush any remaining table buffer content
|
|
532
|
+
tableBuffer.forceFlush();
|
|
533
|
+
|
|
523
534
|
const err = notify(notifyCmd);
|
|
524
535
|
if (err) {
|
|
525
536
|
console.error(
|
|
@@ -543,3 +554,26 @@ export function startInteractiveSession({
|
|
|
543
554
|
process.on("exit", cleanup);
|
|
544
555
|
process.on("SIGTERM", cleanup);
|
|
545
556
|
}
|
|
557
|
+
|
|
558
|
+
/**
|
|
559
|
+
* Creates a table buffer for detecting and formatting markdown tables
|
|
560
|
+
* in streaming text output.
|
|
561
|
+
* Thin shell: delegates pure logic to createTableDetector and handles I/O.
|
|
562
|
+
*/
|
|
563
|
+
function createTableBuffer() {
|
|
564
|
+
const detector = createTableDetector();
|
|
565
|
+
|
|
566
|
+
function feed(/** @type {string} */ chunk) {
|
|
567
|
+
const { output, warnings } = detector.feed(chunk);
|
|
568
|
+
for (const s of output) process.stdout.write(s);
|
|
569
|
+
for (const w of warnings) console.error(styleText("yellow", w));
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
function forceFlush() {
|
|
573
|
+
const { output, warnings } = detector.forceFlush();
|
|
574
|
+
for (const s of output) process.stdout.write(s);
|
|
575
|
+
for (const w of warnings) console.error(styleText("yellow", w));
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
return { feed, forceFlush };
|
|
579
|
+
}
|