s-app-scoder-agent 0.9.2 → 0.9.6

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.
Files changed (112) hide show
  1. package/README.md +615 -638
  2. package/dist/{guardrails.js → agent/guardrails.js} +1 -1
  3. package/dist/agent/interaction.js +70 -0
  4. package/dist/agent/mcp-bridge.js +136 -0
  5. package/dist/agent/prompt-context.js +83 -0
  6. package/dist/agent/run-loop.js +318 -0
  7. package/dist/agent/session-persistence.js +61 -0
  8. package/dist/agent/tool-executor.js +158 -0
  9. package/dist/agent/types.js +88 -0
  10. package/dist/agent/verification.js +99 -0
  11. package/dist/agent.js +239 -1002
  12. package/dist/cli/args.js +230 -0
  13. package/dist/cli/init.js +79 -0
  14. package/dist/cli/main.js +211 -0
  15. package/dist/cli/mcp-command.js +185 -0
  16. package/dist/cli/repl.js +77 -0
  17. package/dist/cli/run-once.js +26 -0
  18. package/dist/{tui/commands.js → commands/definitions.js} +6 -16
  19. package/dist/commands/registry.js +260 -0
  20. package/dist/{config.js → config/config.js} +1 -1
  21. package/dist/{prompt-builder.js → context/prompt-builder.js} +12 -1
  22. package/dist/{prompt.js → context/prompt.js} +111 -9
  23. package/dist/{skills.js → context/skills.js} +41 -2
  24. package/dist/index.js +5 -798
  25. package/dist/io/console-reporter.js +24 -0
  26. package/dist/io/reporter.js +1 -0
  27. package/dist/mcp/connection.js +227 -0
  28. package/dist/mcp/env.js +8 -0
  29. package/dist/{mcp.js → mcp/manager.js} +3 -515
  30. package/dist/mcp/sse.js +28 -0
  31. package/dist/mcp/transport-http.js +140 -0
  32. package/dist/mcp/transport-stdio.js +98 -0
  33. package/dist/mcp/types.js +11 -0
  34. package/dist/media/image.js +149 -0
  35. package/dist/{server.js → server/server.js} +1 -1
  36. package/dist/{cost.js → session/cost.js} +1 -1
  37. package/dist/{session.js → session/session.js} +1 -1
  38. package/dist/{version.js → shared/version.js} +1 -1
  39. package/dist/tools/apply-patch.js +62 -0
  40. package/dist/tools/edit-file.js +29 -0
  41. package/dist/tools/file-edit-common.js +124 -0
  42. package/dist/tools/list-files.js +64 -0
  43. package/dist/tools/path-safety.js +75 -0
  44. package/dist/tools/read-file.js +64 -0
  45. package/dist/tools/registry.js +297 -0
  46. package/dist/tools/run-command.js +192 -0
  47. package/dist/tools/schemas.js +8 -0
  48. package/dist/tools/search.js +237 -0
  49. package/dist/tools/task-tool.js +48 -0
  50. package/dist/tools/types.js +1 -0
  51. package/dist/tools/write-file.js +34 -0
  52. package/dist/tui/app.js +59 -532
  53. package/dist/tui/ask-user-modal.js +2 -1
  54. package/dist/tui/at-complete.js +2 -93
  55. package/dist/tui/charset.js +41 -0
  56. package/dist/tui/chat-views.js +72 -16
  57. package/dist/tui/chrome.js +11 -4
  58. package/dist/tui/hooks/use-command-host.js +26 -0
  59. package/dist/tui/hooks/use-event-queue.js +69 -0
  60. package/dist/tui/hooks/use-keybinds.js +87 -0
  61. package/dist/tui/hooks/use-scroll.js +38 -0
  62. package/dist/tui/hooks/use-session-resume.js +47 -0
  63. package/dist/tui/hooks/use-submit.js +106 -0
  64. package/dist/tui/image.js +2 -149
  65. package/dist/tui/input.js +1 -1
  66. package/dist/tui/markdown/ast.js +5 -0
  67. package/dist/tui/markdown/diagram.js +239 -0
  68. package/dist/tui/markdown/inline-render.js +24 -0
  69. package/dist/tui/markdown/inline.js +93 -0
  70. package/dist/tui/markdown/parse.js +139 -0
  71. package/dist/tui/markdown/render.js +146 -0
  72. package/dist/tui/markdown/table.js +106 -0
  73. package/dist/tui/markdown.js +32 -379
  74. package/dist/tui/permission-modal.js +2 -1
  75. package/dist/tui/selectors.js +157 -0
  76. package/dist/tui/session-picker.js +4 -3
  77. package/dist/tui/sidebar.js +130 -15
  78. package/dist/tui/state.js +47 -118
  79. package/dist/tui/text-width.js +139 -0
  80. package/dist/tui/theme.js +7 -2
  81. package/dist/{build.js → workspace/build.js} +1 -1
  82. package/dist/workspace/file-index.js +93 -0
  83. package/package.json +5 -1
  84. package/dist/tools.js +0 -1223
  85. package/dist/tui/command-registry.js +0 -477
  86. package/dist/tui/review.js +0 -64
  87. package/dist/tui/transcript.js +0 -145
  88. /package/dist/{agents-guide.js → context/agents-guide.js} +0 -0
  89. /package/dist/{agents.js → context/agents.js} +0 -0
  90. /package/dist/{compaction.js → context/compaction.js} +0 -0
  91. /package/dist/{context.js → context/context.js} +0 -0
  92. /package/dist/{memory.js → context/memory.js} +0 -0
  93. /package/dist/{repo-map.js → context/repo-map.js} +0 -0
  94. /package/dist/{ui.js → io/terminal.js} +0 -0
  95. /package/dist/{llm.js → llm/llm.js} +0 -0
  96. /package/dist/{model-catalog.js → llm/model-catalog.js} +0 -0
  97. /package/dist/{provider-anthropic.js → llm/provider-anthropic.js} +0 -0
  98. /package/dist/{provider-registry.js → llm/provider-registry.js} +0 -0
  99. /package/dist/{retry.js → llm/retry.js} +0 -0
  100. /package/dist/{mcp-import.js → mcp/mcp-import.js} +0 -0
  101. /package/dist/{permissions.js → security/permissions.js} +0 -0
  102. /package/dist/{sandbox.js → security/sandbox.js} +0 -0
  103. /package/dist/{shell-scanner.js → security/shell-scanner.js} +0 -0
  104. /package/dist/{logger.js → session/logger.js} +0 -0
  105. /package/dist/{steering.js → session/steering.js} +0 -0
  106. /package/dist/{constants.js → shared/constants.js} +0 -0
  107. /package/dist/{json-schema.js → shared/json-schema.js} +0 -0
  108. /package/dist/{webfetch.js → tools/webfetch.js} +0 -0
  109. /package/dist/{diagnostics.js → workspace/diagnostics.js} +0 -0
  110. /package/dist/{diff.js → workspace/diff.js} +0 -0
  111. /package/dist/{snapshot.js → workspace/snapshot.js} +0 -0
  112. /package/dist/{watch.js → workspace/watch.js} +0 -0
@@ -0,0 +1,139 @@
1
+ import { splitOutsideCodeSpans } from "./inline.js";
2
+ const TABLE_CELL_RX = /^\s*\|.*\|\s*$/;
3
+ const TABLE_SEP_RX = /^\s*\|[\s:|-]+\|\s*$/;
4
+ const HR_RX = /^\s*(-{3,}|\*{3,}|_{3,})\s*$/;
5
+ /** Split one table row into cells: \| escapes and `code|spans` are safe. */
6
+ export function parseTableRow(line) {
7
+ const trimmed = line.trim().replace(/^\|/, "").replace(/\|$/, "");
8
+ return splitOutsideCodeSpans(trimmed, "|").map((c) => c.trim());
9
+ }
10
+ /** Parse the GFM separator row (`:---`, `:--:`, `--:`) into alignments. */
11
+ export function parseTableAlign(line) {
12
+ return parseTableRow(line).map((cell) => {
13
+ const c = cell.replace(/\s/g, "");
14
+ const left = c.startsWith(":");
15
+ const right = c.endsWith(":");
16
+ if (left && right)
17
+ return "center";
18
+ if (right)
19
+ return "right";
20
+ return "left";
21
+ });
22
+ }
23
+ /** Fenced code block (```lang … ```) — EOF closes an unclosed fence. */
24
+ const matchFence = (lines, i) => {
25
+ if (!lines[i].startsWith("```"))
26
+ return null;
27
+ const lang = lines[i].slice(3).trim().toLowerCase();
28
+ const code = [];
29
+ let j = i + 1;
30
+ while (j < lines.length && !lines[j].startsWith("```"))
31
+ code.push(lines[j++]);
32
+ j++; // consume closing fence (or EOF)
33
+ let block;
34
+ if (lang === "mermaid" && code.length > 0)
35
+ block = { kind: "mermaid", lines: code };
36
+ else if ((lang === "chart" || lang.startsWith("chart:")) && code.length > 0)
37
+ block = { kind: "chart", lang, source: code.join("\n") };
38
+ else if ((lang === "timeline" || lang === "events") && code.length > 0)
39
+ block = { kind: "timeline", source: code.join("\n") };
40
+ else
41
+ block = { kind: "code", lang, lines: code };
42
+ return { blocks: [block], next: j };
43
+ };
44
+ const matchHeading = (lines, i) => {
45
+ const h = /^(#{1,6})\s+(.*)$/.exec(lines[i]);
46
+ if (!h)
47
+ return null;
48
+ return { blocks: [{ kind: "heading", level: h[1].length, text: h[2].trim() }], next: i + 1 };
49
+ };
50
+ const matchHr = (lines, i) => HR_RX.test(lines[i]) ? { blocks: [{ kind: "hr" }], next: i + 1 } : null;
51
+ /** GFM table: header row | separator row (with alignment) | body rows. */
52
+ const matchTable = (lines, i) => {
53
+ if (!TABLE_CELL_RX.test(lines[i]))
54
+ return null;
55
+ if (i + 1 >= lines.length || !TABLE_SEP_RX.test(lines[i + 1]))
56
+ return null;
57
+ const header = parseTableRow(lines[i]);
58
+ const align = parseTableAlign(lines[i + 1]);
59
+ let j = i + 2;
60
+ const rows = [];
61
+ while (j < lines.length && TABLE_CELL_RX.test(lines[j]))
62
+ rows.push(parseTableRow(lines[j++]));
63
+ return { blocks: [{ kind: "table", header, align, rows }], next: j };
64
+ };
65
+ function parseListItem(line) {
66
+ const bullet = /^(\s*)[-*]\s+(.*)$/.exec(line);
67
+ if (bullet) {
68
+ const depth = Math.floor(bullet[1].replace(/\t/g, " ").length / 2);
69
+ const body = bullet[2];
70
+ const task = /^\[([ xX])\]\s+(.*)$/.exec(body);
71
+ if (task) {
72
+ return { depth, marker: null, checked: task[1].toLowerCase() === "x", text: task[2] };
73
+ }
74
+ return { depth, marker: null, text: body };
75
+ }
76
+ const numbered = /^(\s*)(\d+\.)\s+(.*)$/.exec(line);
77
+ if (numbered) {
78
+ const depth = Math.floor(numbered[1].replace(/\t/g, " ").length / 2);
79
+ return { depth, marker: numbered[2], text: numbered[3] };
80
+ }
81
+ return null;
82
+ }
83
+ /** Consecutive list items (bullets, task lists, numbered) → one list block. */
84
+ const matchList = (lines, i) => {
85
+ if (!parseListItem(lines[i]))
86
+ return null;
87
+ const items = [];
88
+ let j = i;
89
+ while (j < lines.length) {
90
+ const item = parseListItem(lines[j]);
91
+ if (!item)
92
+ break;
93
+ items.push(item);
94
+ j++;
95
+ }
96
+ return { blocks: [{ kind: "list", items }], next: j };
97
+ };
98
+ /** Consecutive `>` lines → one quote block. */
99
+ const matchQuote = (lines, i) => {
100
+ if (!/^>\s?/.test(lines[i]))
101
+ return null;
102
+ const quoted = [];
103
+ let j = i;
104
+ while (j < lines.length && /^>\s?/.test(lines[j]))
105
+ quoted.push(lines[j++].replace(/^>\s?/, ""));
106
+ return { blocks: [{ kind: "quote", text: quoted.join("\n") }], next: j };
107
+ };
108
+ const MATCHERS = [matchFence, matchHeading, matchHr, matchTable, matchList, matchQuote];
109
+ /** Parse markdown text into blocks. Never throws — unknown input → paragraph. */
110
+ export function parseMarkdown(text) {
111
+ const lines = text.split("\n");
112
+ const blocks = [];
113
+ let i = 0;
114
+ while (i < lines.length) {
115
+ const line = lines[i];
116
+ // Blank line → single spacer (collapsed).
117
+ if (line.trim() === "") {
118
+ if (blocks.length > 0 && blocks[blocks.length - 1].kind !== "spacer") {
119
+ blocks.push({ kind: "spacer" });
120
+ }
121
+ i++;
122
+ continue;
123
+ }
124
+ let matched = null;
125
+ for (const matcher of MATCHERS) {
126
+ matched = matcher(lines, i);
127
+ if (matched)
128
+ break;
129
+ }
130
+ if (matched) {
131
+ blocks.push(...matched.blocks);
132
+ i = matched.next;
133
+ continue;
134
+ }
135
+ blocks.push({ kind: "paragraph", text: line });
136
+ i++;
137
+ }
138
+ return blocks;
139
+ }
@@ -0,0 +1,146 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * Block AST → Ink elements. All layout decisions (clip/pad/wrap) go through
4
+ * text-width.ts so borders and columns stay aligned with Unicode content.
5
+ */
6
+ import React from "react";
7
+ import { Box, Text } from "ink";
8
+ import { theme } from "../theme.js";
9
+ import { boxBorderStyle, charset } from "../charset.js";
10
+ import { measure, wrapCells } from "../text-width.js";
11
+ import { renderInline } from "./inline-render.js";
12
+ import { TableView } from "./table.js";
13
+ import { ChartPreview, MermaidPreview, TimelinePreview, parseChartSpec, parseTimelineSpec, } from "./diagram.js";
14
+ const MAX_CODE_LINES = 60;
15
+ // ── Syntax highlighting (JS/TS/JSON/shell keyword sets) ─────────────────────
16
+ const KEYWORDS = new Set(["const", "let", "var", "function", "async", "await", "return",
17
+ "if", "else", "for", "while", "do", "switch", "case", "break", "continue",
18
+ "try", "catch", "finally", "throw", "class", "extends", "import", "export",
19
+ "from", "default", "new", "typeof", "instanceof", "void", "in", "of",
20
+ "interface", "type", "enum", "implements", "private", "public", "protected",
21
+ "readonly", "abstract", "override", "declare", "namespace", "module"]);
22
+ const TYPE_KEYWORDS = new Set(["string", "number", "boolean", "any", "unknown", "never",
23
+ "null", "undefined", "void", "object", "true", "false", "Promise", "Array",
24
+ "Record", "Partial", "Required", "Omit", "Pick", "keyof", "infer"]);
25
+ /**
26
+ * Highlight one logical line. `inBlockComment` carries multi-line comment
27
+ * state across lines (returned so the caller can thread it through the block).
28
+ * Node keys derive from nodes.length — no separate counter.
29
+ */
30
+ function highlightLine(line, inBlockComment) {
31
+ const nodes = [];
32
+ const keyed = (color, text, bold = false) => {
33
+ nodes.push(_jsx(Text, { color: color, bold: bold, children: text }, `n${nodes.length}`));
34
+ };
35
+ if (inBlockComment) {
36
+ const end = line.indexOf("*/");
37
+ if (end === -1) {
38
+ return { nodes: [_jsx(Text, { color: theme.thinking, children: line }, "bc")], inBlockComment: true };
39
+ }
40
+ keyed(theme.thinking, line.slice(0, end + 2));
41
+ const rest = highlightLine(line.slice(end + 2), false);
42
+ nodes.push(...rest.nodes.map((n, i) => (typeof n === "object" && n !== null ? React.cloneElement(n, { key: `r${i}` }) : n)));
43
+ return { nodes, inBlockComment: false };
44
+ }
45
+ const re = /("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|`(?:[^`\\]|\\.)*`)|(\/\/[^\n]*|\/\*[\s\S]*?(?:\*\/|$))|(\b0x[\da-fA-F]+|\d+\.?\d*(?:[eE][+-]?\d+)?\b)|(\w+)/g;
46
+ let last = 0;
47
+ let openComment = false;
48
+ for (const m of line.matchAll(re)) {
49
+ if (m.index > last)
50
+ nodes.push(line.slice(last, m.index));
51
+ const tok = m[0];
52
+ if (m[1])
53
+ keyed(theme.code, tok);
54
+ else if (m[2]) {
55
+ keyed(theme.thinking, tok);
56
+ if (tok.startsWith("/*") && !tok.endsWith("*/"))
57
+ openComment = true;
58
+ }
59
+ else if (m[3])
60
+ keyed("magenta", tok);
61
+ else if (KEYWORDS.has(tok))
62
+ keyed("cyan", tok, true);
63
+ else if (TYPE_KEYWORDS.has(tok))
64
+ keyed("yellow", tok);
65
+ else
66
+ nodes.push(tok);
67
+ last = m.index + tok.length;
68
+ }
69
+ if (last < line.length)
70
+ nodes.push(line.slice(last));
71
+ return { nodes, inBlockComment: openComment };
72
+ }
73
+ // ── Block renderers ──────────────────────────────────────────────────────────
74
+ function CodeBlock({ lang, lines, width }) {
75
+ const gutter = String(Math.min(lines.length, MAX_CODE_LINES)).length;
76
+ const codeWidth = Math.max(8, width - gutter - 6); // gutter + "│ " + border + padding
77
+ const shown = lines.slice(0, MAX_CODE_LINES);
78
+ const elements = [];
79
+ let inBlockComment = false;
80
+ shown.forEach((sourceLine, i) => {
81
+ const wrapped = wrapCells(sourceLine === "" ? " " : sourceLine, codeWidth);
82
+ wrapped.forEach((segment, wi) => {
83
+ const hl = highlightLine(segment, inBlockComment);
84
+ inBlockComment = hl.inBlockComment;
85
+ elements.push(_jsxs(Text, { children: [wi === 0
86
+ ? _jsxs(Text, { dimColor: true, children: [String(i + 1).padStart(gutter, " "), " ", charset.v, " "] })
87
+ : _jsxs(Text, { dimColor: true, children: [" ".repeat(gutter), " ", charset.cont, " "] }), hl.nodes] }, `${i}-${wi}`));
88
+ });
89
+ });
90
+ return (_jsxs(Box, { flexDirection: "column", borderStyle: boxBorderStyle(), borderColor: theme.border, paddingX: 1, marginTop: 1, children: [lang && _jsx(Text, { dimColor: true, bold: true, children: lang }), lines.length === 0 ? _jsx(Text, { dimColor: true, children: "(empty)" }) : elements, lines.length > MAX_CODE_LINES && (_jsxs(Text, { dimColor: true, children: [" \u2026 +", lines.length - MAX_CODE_LINES, " more lines (omitted)"] }))] }));
91
+ }
92
+ function ListBlock({ items, width }) {
93
+ return (_jsx(Box, { flexDirection: "column", children: items.map((item, i) => {
94
+ const indent = " ".repeat(item.depth);
95
+ const marker = item.checked !== undefined
96
+ ? (item.checked ? `${charset.taskDone} ` : `${charset.taskTodo} `)
97
+ : item.marker
98
+ ? `${item.marker} `
99
+ : `${charset.bullet} `;
100
+ const prefixWidth = measure(indent + marker);
101
+ const bodyWidth = Math.max(8, width - prefixWidth);
102
+ const wrappedLines = wrapCells(item.text, bodyWidth);
103
+ return (_jsx(Box, { flexDirection: "column", children: wrappedLines.map((l, wi) => (_jsxs(Text, { children: [wi === 0 ? (_jsxs(Text, { color: item.marker ? "cyan" : theme.accent, children: [indent, marker] })) : (_jsx(Text, { children: " ".repeat(prefixWidth) })), item.checked
104
+ ? _jsx(Text, { dimColor: true, strikethrough: true, children: renderInline(l, `l${i}-${wi}`) })
105
+ : renderInline(l, `l${i}-${wi}`)] }, wi))) }, i));
106
+ }) }));
107
+ }
108
+ function QuoteBlock({ text, width }) {
109
+ const bodyWidth = Math.max(8, width - 2);
110
+ return (_jsx(Box, { flexDirection: "column", children: text.split("\n").flatMap((line, li) => wrapCells(line, bodyWidth).map((segment, wi) => (_jsxs(Text, { dimColor: true, children: [_jsxs(Text, { color: theme.dim, children: [charset.v, " "] }), _jsx(Text, { italic: true, children: renderInline(segment, `q${li}-${wi}`) })] }, `${li}-${wi}`)))) }));
111
+ }
112
+ /** Render one block; returns null for spacers handled by margin. */
113
+ export function renderBlock(block, width, key) {
114
+ switch (block.kind) {
115
+ case "spacer":
116
+ return _jsx(Text, { children: " " }, key);
117
+ case "hr":
118
+ return _jsx(Text, { dimColor: true, children: charset.h.repeat(Math.min(width, 60)) }, key);
119
+ case "heading":
120
+ return (_jsx(Text, { bold: true, color: block.level <= 2 ? "magenta" : "cyan", children: renderInline(block.text, `h${key}`) }, key));
121
+ case "paragraph":
122
+ return (_jsx(Box, { flexDirection: "column", children: wrapCells(block.text, width).map((l, wi) => (_jsx(Text, { children: renderInline(l, `p${key}-${wi}`) }, wi))) }, key));
123
+ case "code":
124
+ return _jsx(CodeBlock, { lang: block.lang, lines: block.lines, width: width }, key);
125
+ case "table":
126
+ return _jsx(TableView, { header: block.header, align: block.align, rows: block.rows, width: width }, key);
127
+ case "list":
128
+ return _jsx(ListBlock, { items: block.items, width: width }, key);
129
+ case "quote":
130
+ return _jsx(QuoteBlock, { text: block.text, width: width }, key);
131
+ case "mermaid":
132
+ return (_jsx(Box, { marginTop: 1, flexDirection: "column", children: _jsx(MermaidPreview, { source: block.lines, width: width }) }, key));
133
+ case "chart": {
134
+ const chart = parseChartSpec(block.source, block.lang);
135
+ return chart
136
+ ? _jsx(Box, { flexDirection: "column", children: _jsx(ChartPreview, { spec: chart, width: width }) }, key)
137
+ : _jsx(Text, { dimColor: true, children: " (invalid chart data)" }, key);
138
+ }
139
+ case "timeline": {
140
+ const events = parseTimelineSpec(block.source);
141
+ return events.length > 0
142
+ ? _jsx(Box, { flexDirection: "column", children: _jsx(TimelinePreview, { events: events, width: width }) }, key)
143
+ : _jsx(Text, { dimColor: true, children: " (empty timeline)" }, key);
144
+ }
145
+ }
146
+ }
@@ -0,0 +1,106 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text } from "ink";
3
+ import { theme } from "../theme.js";
4
+ import { charset } from "../charset.js";
5
+ import { clipCells, measure, padEndCells, padStartCells, wrapCells } from "../text-width.js";
6
+ import { inlineToPlain } from "./inline.js";
7
+ import { renderInline } from "./inline-render.js";
8
+ const MAX_CELL = 40;
9
+ const MIN_CELL = 4;
10
+ const MAX_ROWS = 50;
11
+ /** Below this pane width (or when columns can't fit even at MIN_CELL), degrade. */
12
+ const MIN_TABLE_WIDTH = 28;
13
+ /** Pad a plain-text cell per alignment; returns text of exactly `width` cells. */
14
+ function alignCell(plain, width, align) {
15
+ const clipped = clipCells(plain, width);
16
+ const missing = width - measure(clipped);
17
+ if (missing <= 0)
18
+ return clipped;
19
+ if (align === "right")
20
+ return padStartCells(clipped, width);
21
+ if (align === "center") {
22
+ const left = Math.floor(missing / 2);
23
+ return " ".repeat(left) + clipped + " ".repeat(missing - left);
24
+ }
25
+ return padEndCells(clipped, width);
26
+ }
27
+ /** Column widths: natural content widths, proportionally shrunk to fit. */
28
+ function columnWidths(spec, cols, available) {
29
+ const natural = [];
30
+ for (let c = 0; c < cols; c++) {
31
+ const cells = [spec.header[c] ?? "", ...spec.rows.map((r) => r[c] ?? "")];
32
+ natural.push(Math.min(MAX_CELL, Math.max(...cells.map((t) => measure(inlineToPlain(t))), 1)));
33
+ }
34
+ const overhead = cols * 2 + (cols + 1); // 2 padding cells + 1 border per column
35
+ const totalNatural = natural.reduce((a, b) => a + b, 0);
36
+ if (totalNatural + overhead <= available)
37
+ return natural;
38
+ const budget = available - overhead;
39
+ if (cols * MIN_CELL > budget)
40
+ return null; // cannot fit even minimal columns
41
+ const shrunk = natural.map((n) => Math.max(MIN_CELL, Math.floor((n / totalNatural) * budget)));
42
+ return shrunk;
43
+ }
44
+ /** One physical border line: ┌───┬───┐ / ├───┼───┤ / └───┴───┘ */
45
+ function borderLine(widths, chars) {
46
+ return chars.l + widths.map((w) => chars.h.repeat(w + 2)).join(chars.m) + chars.r;
47
+ }
48
+ export function TableView({ header, align, rows, width }) {
49
+ const cols = header.length;
50
+ const available = Math.max(12, width - 2);
51
+ const widths = columnWidths({ header, align, rows }, cols, available);
52
+ // Degrade: pane too narrow / too many columns → key-value per row.
53
+ if (widths === null || available < MIN_TABLE_WIDTH) {
54
+ return _jsx(KeyValueTable, { header: header, rows: rows, width: width });
55
+ }
56
+ const alignOf = (c) => align[c] ?? "left";
57
+ const lines = [];
58
+ let k = 0;
59
+ const vSep = `${charset.v} `;
60
+ lines.push(_jsx(Text, { color: theme.table, children: borderLine(widths, { l: charset.tl, m: charset.teeDown, r: charset.tr, h: charset.h }) }, k++));
61
+ // Header row — cells may wrap; bold.
62
+ const headerWrapped = header.map((h, c) => wrapCells(inlineToPlain(h), widths[c]));
63
+ const headerHeight = Math.max(...headerWrapped.map((w) => w.length));
64
+ for (let r = 0; r < headerHeight; r++) {
65
+ lines.push(_jsxs(Text, { bold: true, color: theme.table, children: [`${charset.v} `, header.map((_, c) => alignCell(headerWrapped[c][r] ?? "", widths[c], alignOf(c))).join(` ${charset.v} `), ` ${charset.v}`] }, k++));
66
+ }
67
+ lines.push(_jsx(Text, { color: theme.table, children: borderLine(widths, { l: charset.teeRight, m: charset.cross, r: charset.teeLeft, h: charset.h }) }, k++));
68
+ const shown = rows.slice(0, MAX_ROWS);
69
+ for (const row of shown) {
70
+ // Wrap each cell; row height = tallest cell.
71
+ const wrapped = Array.from({ length: cols }, (_, c) => wrapCells(inlineToPlain(row[c] ?? ""), widths[c]));
72
+ const height = Math.max(...wrapped.map((w) => w.length), 1);
73
+ for (let r = 0; r < height; r++) {
74
+ lines.push(_jsxs(Text, { children: [_jsx(Text, { color: theme.table, children: vSep }), Array.from({ length: cols }, (_, c) => {
75
+ const plain = wrapped[c][r] ?? "";
76
+ const missing = Math.max(0, widths[c] - measure(plain));
77
+ const a = alignOf(c);
78
+ const lead = a === "right" ? missing : a === "center" ? Math.floor(missing / 2) : 0;
79
+ const trail = missing - lead;
80
+ // First physical line of a single-line cell renders inline markdown;
81
+ // wrapped/extra lines render plain (markers already stripped).
82
+ const content = r === 0 && plain === inlineToPlain(row[c] ?? "")
83
+ ? renderInline(row[c] ?? "", `t${k}-${c}`)
84
+ : plain;
85
+ return (_jsxs(Text, { children: [" ".repeat(lead), content, " ".repeat(trail)] }, c));
86
+ }).flatMap((node, c) => (c === 0 ? [node] : [_jsx(Text, { color: theme.table, children: ` ${charset.v} ` }, `s${c}`), node])), _jsx(Text, { color: theme.table, children: ` ${charset.v}` })] }, k++));
87
+ }
88
+ }
89
+ if (rows.length > MAX_ROWS) {
90
+ lines.push(_jsxs(Text, { dimColor: true, children: [charset.v, " ", "… " + (rows.length - MAX_ROWS) + " more rows"] }, k++));
91
+ }
92
+ lines.push(_jsx(Text, { color: theme.table, children: borderLine(widths, { l: charset.bl, m: charset.teeUp, r: charset.br, h: charset.h }) }, k++));
93
+ return _jsx(Box, { flexDirection: "column", marginTop: 1, children: lines });
94
+ }
95
+ /** Narrow-pane fallback: one labelled mini-record per row. */
96
+ function KeyValueTable({ header, rows, width }) {
97
+ const labelWidth = Math.min(20, Math.max(...header.map((h) => measure(inlineToPlain(h))), 1));
98
+ const valueWidth = Math.max(8, width - labelWidth - 5);
99
+ const shown = rows.slice(0, MAX_ROWS);
100
+ return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Text, { color: theme.table, bold: true, children: [" table \u00B7 ", rows.length, " row", rows.length === 1 ? "" : "s", " \u00D7 ", header.length, " cols"] }), shown.map((row, ri) => (_jsxs(Box, { flexDirection: "column", children: [ri > 0 && _jsxs(Text, { dimColor: true, children: [" ", charset.h.repeat(Math.min(20, width - 2))] }), header.map((h, c) => {
101
+ const value = row[c] ?? "";
102
+ if (value.trim() === "")
103
+ return null;
104
+ return (_jsxs(Text, { children: [" ", _jsx(Text, { color: theme.table, children: padEndCells(clipCells(inlineToPlain(h), labelWidth), labelWidth) }), ` ${charset.v} `, renderInline(clipCells(inlineToPlain(value), valueWidth), `kv${ri}-${c}`)] }, c));
105
+ })] }, ri))), rows.length > MAX_ROWS && _jsxs(Text, { dimColor: true, children: [" \u2026 ", rows.length - MAX_ROWS, " more rows"] })] }));
106
+ }