s-app-scoder-agent 0.9.3 → 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.
- package/README.md +615 -638
- package/dist/{guardrails.js → agent/guardrails.js} +1 -1
- package/dist/agent/interaction.js +70 -0
- package/dist/agent/mcp-bridge.js +136 -0
- package/dist/agent/prompt-context.js +83 -0
- package/dist/agent/run-loop.js +318 -0
- package/dist/agent/session-persistence.js +61 -0
- package/dist/agent/tool-executor.js +158 -0
- package/dist/agent/types.js +88 -0
- package/dist/agent/verification.js +99 -0
- package/dist/agent.js +239 -1002
- package/dist/cli/args.js +230 -0
- package/dist/cli/init.js +79 -0
- package/dist/cli/main.js +211 -0
- package/dist/cli/mcp-command.js +185 -0
- package/dist/cli/repl.js +77 -0
- package/dist/cli/run-once.js +26 -0
- package/dist/{tui/commands.js → commands/definitions.js} +6 -16
- package/dist/commands/registry.js +260 -0
- package/dist/{config.js → config/config.js} +1 -1
- package/dist/{prompt-builder.js → context/prompt-builder.js} +12 -1
- package/dist/{prompt.js → context/prompt.js} +111 -9
- package/dist/{skills.js → context/skills.js} +41 -2
- package/dist/index.js +5 -798
- package/dist/io/console-reporter.js +24 -0
- package/dist/io/reporter.js +1 -0
- package/dist/mcp/connection.js +227 -0
- package/dist/mcp/env.js +8 -0
- package/dist/{mcp.js → mcp/manager.js} +3 -515
- package/dist/mcp/sse.js +28 -0
- package/dist/mcp/transport-http.js +140 -0
- package/dist/mcp/transport-stdio.js +98 -0
- package/dist/mcp/types.js +11 -0
- package/dist/media/image.js +149 -0
- package/dist/{server.js → server/server.js} +1 -1
- package/dist/{cost.js → session/cost.js} +1 -1
- package/dist/{session.js → session/session.js} +1 -1
- package/dist/{version.js → shared/version.js} +1 -1
- package/dist/tools/apply-patch.js +62 -0
- package/dist/tools/edit-file.js +29 -0
- package/dist/tools/file-edit-common.js +124 -0
- package/dist/tools/list-files.js +64 -0
- package/dist/tools/path-safety.js +75 -0
- package/dist/tools/read-file.js +64 -0
- package/dist/tools/registry.js +297 -0
- package/dist/tools/run-command.js +192 -0
- package/dist/tools/schemas.js +8 -0
- package/dist/tools/search.js +237 -0
- package/dist/tools/task-tool.js +48 -0
- package/dist/tools/types.js +1 -0
- package/dist/tools/write-file.js +34 -0
- package/dist/tui/app.js +59 -532
- package/dist/tui/ask-user-modal.js +2 -1
- package/dist/tui/at-complete.js +2 -93
- package/dist/tui/charset.js +41 -0
- package/dist/tui/chat-views.js +72 -16
- package/dist/tui/chrome.js +11 -5
- package/dist/tui/hooks/use-command-host.js +26 -0
- package/dist/tui/hooks/use-event-queue.js +69 -0
- package/dist/tui/hooks/use-keybinds.js +87 -0
- package/dist/tui/hooks/use-scroll.js +38 -0
- package/dist/tui/hooks/use-session-resume.js +47 -0
- package/dist/tui/hooks/use-submit.js +106 -0
- package/dist/tui/image.js +2 -149
- package/dist/tui/input.js +1 -1
- package/dist/tui/markdown/ast.js +5 -0
- package/dist/tui/markdown/diagram.js +239 -0
- package/dist/tui/markdown/inline-render.js +24 -0
- package/dist/tui/markdown/inline.js +93 -0
- package/dist/tui/markdown/parse.js +139 -0
- package/dist/tui/markdown/render.js +146 -0
- package/dist/tui/markdown/table.js +106 -0
- package/dist/tui/markdown.js +32 -379
- package/dist/tui/permission-modal.js +2 -1
- package/dist/tui/selectors.js +157 -0
- package/dist/tui/session-picker.js +4 -3
- package/dist/tui/sidebar.js +130 -15
- package/dist/tui/state.js +47 -118
- package/dist/tui/text-width.js +139 -0
- package/dist/tui/theme.js +7 -2
- package/dist/{build.js → workspace/build.js} +1 -1
- package/dist/workspace/file-index.js +93 -0
- package/package.json +5 -1
- package/dist/tools.js +0 -1223
- package/dist/tui/command-registry.js +0 -477
- package/dist/tui/review.js +0 -64
- package/dist/tui/transcript.js +0 -145
- /package/dist/{agents-guide.js → context/agents-guide.js} +0 -0
- /package/dist/{agents.js → context/agents.js} +0 -0
- /package/dist/{compaction.js → context/compaction.js} +0 -0
- /package/dist/{context.js → context/context.js} +0 -0
- /package/dist/{memory.js → context/memory.js} +0 -0
- /package/dist/{repo-map.js → context/repo-map.js} +0 -0
- /package/dist/{ui.js → io/terminal.js} +0 -0
- /package/dist/{llm.js → llm/llm.js} +0 -0
- /package/dist/{model-catalog.js → llm/model-catalog.js} +0 -0
- /package/dist/{provider-anthropic.js → llm/provider-anthropic.js} +0 -0
- /package/dist/{provider-registry.js → llm/provider-registry.js} +0 -0
- /package/dist/{retry.js → llm/retry.js} +0 -0
- /package/dist/{mcp-import.js → mcp/mcp-import.js} +0 -0
- /package/dist/{permissions.js → security/permissions.js} +0 -0
- /package/dist/{sandbox.js → security/sandbox.js} +0 -0
- /package/dist/{shell-scanner.js → security/shell-scanner.js} +0 -0
- /package/dist/{logger.js → session/logger.js} +0 -0
- /package/dist/{steering.js → session/steering.js} +0 -0
- /package/dist/{constants.js → shared/constants.js} +0 -0
- /package/dist/{json-schema.js → shared/json-schema.js} +0 -0
- /package/dist/{webfetch.js → tools/webfetch.js} +0 -0
- /package/dist/{diagnostics.js → workspace/diagnostics.js} +0 -0
- /package/dist/{diff.js → workspace/diff.js} +0 -0
- /package/dist/{snapshot.js → workspace/snapshot.js} +0 -0
- /package/dist/{watch.js → workspace/watch.js} +0 -0
package/dist/tui/markdown.js
CHANGED
|
@@ -1,387 +1,40 @@
|
|
|
1
|
-
import { jsx as _jsx
|
|
2
|
-
import { Box
|
|
3
|
-
import {
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Box } from "ink";
|
|
3
|
+
import { parseMarkdown } from "./markdown/parse.js";
|
|
4
|
+
import { renderBlock } from "./markdown/render.js";
|
|
5
|
+
export { parseChartSpec, parseTimelineSpec, } from "./markdown/diagram.js";
|
|
6
|
+
export { parseMarkdown } from "./markdown/parse.js";
|
|
7
|
+
export { tokenizeInline, inlineToPlain } from "./markdown/inline.js";
|
|
4
8
|
const DEFAULT_WIDTH = 100;
|
|
5
|
-
const MAX_CODE_LINES = 60;
|
|
6
|
-
function clip(line, width = DEFAULT_WIDTH) {
|
|
7
|
-
const safeWidth = Math.max(8, width);
|
|
8
|
-
return line.length > safeWidth ? line.slice(0, safeWidth - 1) + "…" : line;
|
|
9
|
-
}
|
|
10
9
|
function contentWidth(width) {
|
|
11
10
|
return Math.max(32, Math.floor(width ?? DEFAULT_WIDTH));
|
|
12
11
|
}
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
? parsed
|
|
37
|
-
: Array.isArray(object.data) ? object.data : [];
|
|
38
|
-
const data = rows.flatMap((row, index) => {
|
|
39
|
-
if (Array.isArray(row)) {
|
|
40
|
-
const value = Number(row[1]);
|
|
41
|
-
return Number.isFinite(value) ? [{ label: scalar(row[0]) || String(index + 1), value }] : [];
|
|
42
|
-
}
|
|
43
|
-
if (row && typeof row === "object") {
|
|
44
|
-
const r = row;
|
|
45
|
-
const value = Number(r.value ?? r.y ?? r.amount);
|
|
46
|
-
return Number.isFinite(value) ? [{ label: scalar(r.label ?? r.name ?? r.x) || String(index + 1), value }] : [];
|
|
47
|
-
}
|
|
48
|
-
return [];
|
|
49
|
-
});
|
|
50
|
-
// Friendly fallback: `label | value` or `label, value` per line.
|
|
51
|
-
if (data.length === 0) {
|
|
52
|
-
for (const [index, line] of raw.split("\n").entries()) {
|
|
53
|
-
if (!line.trim() || /^(type|title|data)\s*:/i.test(line))
|
|
54
|
-
continue;
|
|
55
|
-
const parts = line.split(/\s*[|,]\s*/);
|
|
56
|
-
const value = Number(parts.at(-1));
|
|
57
|
-
if (parts.length >= 2 && Number.isFinite(value))
|
|
58
|
-
data.push({ label: parts.slice(0, -1).join(" ").trim() || String(index + 1), value });
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
if (data.length === 0 || !["bar", "line", "pie"].includes(type))
|
|
62
|
-
return null;
|
|
63
|
-
const title = scalar(object.title);
|
|
64
|
-
return {
|
|
65
|
-
type: type,
|
|
66
|
-
...(title ? { title } : {}),
|
|
67
|
-
data: data.slice(0, 40),
|
|
68
|
-
};
|
|
69
|
-
}
|
|
70
|
-
/** Parse JSON or `time | title | detail | status` timeline rows. */
|
|
71
|
-
export function parseTimelineSpec(source) {
|
|
72
|
-
const raw = source.trim();
|
|
73
|
-
if (!raw)
|
|
74
|
-
return [];
|
|
75
|
-
let parsed;
|
|
76
|
-
try {
|
|
77
|
-
parsed = JSON.parse(raw);
|
|
78
|
-
}
|
|
79
|
-
catch {
|
|
80
|
-
parsed = null;
|
|
81
|
-
}
|
|
82
|
-
const rows = Array.isArray(parsed)
|
|
83
|
-
? parsed
|
|
84
|
-
: parsed && typeof parsed === "object" && Array.isArray(parsed.events)
|
|
85
|
-
? parsed.events
|
|
86
|
-
: [];
|
|
87
|
-
const events = rows.flatMap((row) => {
|
|
88
|
-
if (!row || typeof row !== "object")
|
|
89
|
-
return [];
|
|
90
|
-
const r = row;
|
|
91
|
-
const title = scalar(r.title ?? r.name ?? r.event);
|
|
92
|
-
if (!title)
|
|
93
|
-
return [];
|
|
94
|
-
const status = scalar(r.status).toLowerCase();
|
|
95
|
-
return [{
|
|
96
|
-
time: scalar(r.time ?? r.date ?? r.timestamp),
|
|
97
|
-
title,
|
|
98
|
-
detail: scalar(r.detail ?? r.description ?? r.body) || undefined,
|
|
99
|
-
status: ["done", "active", "pending", "error"].includes(status) ? status : undefined,
|
|
100
|
-
}];
|
|
101
|
-
});
|
|
102
|
-
if (events.length > 0)
|
|
103
|
-
return events.slice(0, 40);
|
|
104
|
-
return raw.split("\n").flatMap((line) => {
|
|
105
|
-
const trimmed = line.replace(/^[-*]\s*/, "").trim();
|
|
106
|
-
if (!trimmed)
|
|
107
|
-
return [];
|
|
108
|
-
const parts = trimmed.split(/\s*\|\s*/);
|
|
109
|
-
if (parts.length < 2)
|
|
110
|
-
return [];
|
|
111
|
-
const status = parts[3]?.toLowerCase();
|
|
112
|
-
return [{ time: parts[0], title: parts[1], detail: parts[2] || undefined,
|
|
113
|
-
status: ["done", "active", "pending", "error"].includes(status) ? status : undefined }];
|
|
114
|
-
}).slice(0, 40);
|
|
115
|
-
}
|
|
116
|
-
// ── Syntax highlighting ───────────────────────────────────────────────────────
|
|
117
|
-
/** T-15: token-based syntax highlighter for TypeScript/JavaScript/JSON/shell. */
|
|
118
|
-
function highlightCode(code, width = DEFAULT_WIDTH) {
|
|
119
|
-
const kw = new Set(["const", "let", "var", "function", "async", "await", "return",
|
|
120
|
-
"if", "else", "for", "while", "do", "switch", "case", "break", "continue",
|
|
121
|
-
"try", "catch", "finally", "throw", "class", "extends", "import", "export",
|
|
122
|
-
"from", "default", "new", "typeof", "instanceof", "void", "in", "of",
|
|
123
|
-
"interface", "type", "enum", "implements", "private", "public", "protected",
|
|
124
|
-
"readonly", "abstract", "override", "declare", "namespace", "module"]);
|
|
125
|
-
const typeKw = new Set(["string", "number", "boolean", "any", "unknown", "never",
|
|
126
|
-
"null", "undefined", "void", "object", "true", "false", "Promise", "Array",
|
|
127
|
-
"Record", "Partial", "Required", "Omit", "Pick", "keyof", "infer"]);
|
|
128
|
-
const codeWidth = Math.max(8, width - 6);
|
|
129
|
-
return code.slice(0, MAX_CODE_LINES).map((sourceLine, i) => {
|
|
130
|
-
const line = clip(sourceLine, codeWidth);
|
|
131
|
-
const parts = [];
|
|
132
|
-
const re = /("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|`(?:[^`\\]|\\.)*`)|(\/\/[^\n]*|\/\*[\s\S]*?\*\/)|(\b0x[\da-fA-F]+|\d+\.?\d*(?:[eE][+-]?\d+)?\b)|(\w+)/g;
|
|
133
|
-
let last = 0;
|
|
134
|
-
let k = 0;
|
|
135
|
-
let m;
|
|
136
|
-
while ((m = re.exec(line)) !== null) {
|
|
137
|
-
if (m.index > last)
|
|
138
|
-
parts.push(line.slice(last, m.index));
|
|
139
|
-
const tok = m[0];
|
|
140
|
-
if (m[1])
|
|
141
|
-
parts.push(_jsx(Text, { color: theme.code, children: tok }, `c${i}-${k++}`));
|
|
142
|
-
else if (m[2])
|
|
143
|
-
parts.push(_jsx(Text, { color: theme.thinking, children: tok }, `c${i}-${k++}`));
|
|
144
|
-
else if (m[3])
|
|
145
|
-
parts.push(_jsx(Text, { color: "magenta", children: tok }, `c${i}-${k++}`));
|
|
146
|
-
else if (kw.has(tok))
|
|
147
|
-
parts.push(_jsx(Text, { bold: true, color: "cyan", children: tok }, `c${i}-${k++}`));
|
|
148
|
-
else if (typeKw.has(tok))
|
|
149
|
-
parts.push(_jsx(Text, { color: "yellow", children: tok }, `c${i}-${k++}`));
|
|
150
|
-
else
|
|
151
|
-
parts.push(line.slice(m.index, m.index + tok.length));
|
|
152
|
-
last = m.index + tok.length;
|
|
153
|
-
}
|
|
154
|
-
if (last < line.length)
|
|
155
|
-
parts.push(line.slice(last));
|
|
156
|
-
return (_jsxs(Text, { children: [_jsxs(Text, { dimColor: true, children: [String(i + 1).padStart(3, " "), " \u2502 "] }), parts.length > 0 ? parts : line] }, i));
|
|
157
|
-
});
|
|
158
|
-
}
|
|
159
|
-
// ── Mermaid ASCII preview ──────────────────────────────────────────────────────
|
|
160
|
-
/** T-15: parse mermaid source → compact ASCII art preview. */
|
|
161
|
-
function mermaidPreview(code) {
|
|
162
|
-
const text = code.join("\n").trim();
|
|
163
|
-
const isFlowchart = /^(graph|flowchart|stateDiagram|erDiagram|gitGraph)/m.test(text);
|
|
164
|
-
const isSequence = /^sequenceDiagram/m.test(text);
|
|
165
|
-
const result = [];
|
|
166
|
-
result.push("┌─ ◉ diagram preview ──────────────────────────────────┐");
|
|
167
|
-
if (isFlowchart) {
|
|
168
|
-
const nodeMap = new Map();
|
|
169
|
-
const edges = [];
|
|
170
|
-
const nodeRe = /([A-Z][A-Za-z0-9_]*)(\[[^\]]*\]|\([^)]*\)|\{[^}]*\}|\(\([^)]*\)\))/g;
|
|
171
|
-
let m;
|
|
172
|
-
while ((m = nodeRe.exec(text)) !== null) {
|
|
173
|
-
nodeMap.set(m[1], m[2] ?? m[3] ?? m[4] ?? m[5] ?? m[1]);
|
|
174
|
-
}
|
|
175
|
-
const edgeRe = /([A-Z][A-Za-z0-9_]*)\s*(-->?|-.->|==>|<-->|===>|---)\|?([^|]+)\|?\s*([A-Z][A-Za-z0-9_]*)/g;
|
|
176
|
-
while ((m = edgeRe.exec(text)) !== null) {
|
|
177
|
-
edges.push([m[1], "", m[4]]);
|
|
178
|
-
}
|
|
179
|
-
const nodes = [...nodeMap.entries()].slice(0, 6);
|
|
180
|
-
for (const [id, label] of nodes) {
|
|
181
|
-
result.push(`│ [${id}] ${label.slice(0, 24).padEnd(24)} │`);
|
|
182
|
-
}
|
|
183
|
-
if (nodes.length > 0)
|
|
184
|
-
result.push("│ ───────────────────────────────── │");
|
|
185
|
-
for (const [from, , to] of edges.slice(0, 4)) {
|
|
186
|
-
const fl = nodeMap.get(from) ?? from;
|
|
187
|
-
const tl = nodeMap.get(to) ?? to;
|
|
188
|
-
result.push(`│ ${fl.slice(0, 8)} → ${tl.slice(0, 8).padEnd(8)} │`);
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
else if (isSequence) {
|
|
192
|
-
const parts = [];
|
|
193
|
-
const partRe = /(?:participant|actor)\s+(\w+)/gi;
|
|
194
|
-
let m;
|
|
195
|
-
while ((m = partRe.exec(text)) !== null)
|
|
196
|
-
parts.push(m[1]);
|
|
197
|
-
if (parts.length > 0)
|
|
198
|
-
result.push(`│ participants: ${parts.slice(0, 5).join(", ")} │`);
|
|
199
|
-
result.push("│ ───────────────────────────────── │");
|
|
200
|
-
const msgRe = /([\w]+)\s+(?:->|-->|-.>|=>)\s+([\w]+)(?:\s*:\s*(.*))?/gi;
|
|
201
|
-
while ((m = msgRe.exec(text)) !== null) {
|
|
202
|
-
if (result.length >= 10) {
|
|
203
|
-
result.push("│ … (more messages) │");
|
|
204
|
-
break;
|
|
205
|
-
}
|
|
206
|
-
const from = (m[1] ?? "").slice(0, 8), to = (m[2] ?? "").slice(0, 8), msg = (m[3] ?? "").slice(0, 20);
|
|
207
|
-
result.push(`│ ${from} → ${to.padEnd(8)} : ${msg} │`);
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
else {
|
|
211
|
-
result.push("│ ◈ mermaid diagram │");
|
|
212
|
-
result.push("│ (run: npx @mermaid-js/mermaid-cli -i <file>) │");
|
|
213
|
-
}
|
|
214
|
-
result.push("└─────────────────────────────────────────────────────┘");
|
|
215
|
-
return (_jsx(Box, { flexDirection: "column", marginTop: 1, children: result.map((l, i) => (_jsx(Text, { color: theme.mermaid, children: i === 0 || i === result.length - 1 ? _jsx(Text, { bold: true, children: l }) : l }, i))) }));
|
|
216
|
-
}
|
|
217
|
-
function chartPreview(spec, width) {
|
|
218
|
-
const chartWidth = Math.max(24, Math.min(72, width - 8));
|
|
219
|
-
const max = Math.max(...spec.data.map((p) => Math.abs(p.value)), 1);
|
|
220
|
-
const min = Math.min(...spec.data.map((p) => p.value), 0);
|
|
221
|
-
const range = Math.max(1, max - min);
|
|
222
|
-
const labelWidth = Math.min(14, Math.max(4, ...spec.data.map((p) => p.label.length)));
|
|
223
|
-
const plotWidth = Math.max(12, chartWidth - labelWidth - 10);
|
|
224
|
-
const title = spec.title ? ` ${spec.title} ` : ` ${spec.type} chart `;
|
|
225
|
-
const rows = spec.data.map((point, index) => {
|
|
226
|
-
const ratio = (point.value - min) / range;
|
|
227
|
-
const filled = Math.max(0, Math.min(plotWidth, Math.round(ratio * plotWidth)));
|
|
228
|
-
const bar = "█".repeat(filled) + "░".repeat(plotWidth - filled);
|
|
229
|
-
const marker = spec.type === "line" ? (index === 0 ? "╭" : index === spec.data.length - 1 ? "╰" : "│") : "▌";
|
|
230
|
-
return ` ${point.label.slice(0, labelWidth).padEnd(labelWidth)} ${marker}${bar} ${String(point.value)}`;
|
|
231
|
-
});
|
|
232
|
-
if (spec.type === "pie") {
|
|
233
|
-
const total = spec.data.reduce((sum, p) => sum + Math.max(0, p.value), 0) || 1;
|
|
234
|
-
return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Text, { color: theme.table, bold: true, children: ["\u250C\u2500", title, "─".repeat(Math.max(0, chartWidth - title.length - 2)), "\u2510"] }), spec.data.map((p, i) => {
|
|
235
|
-
const pct = Math.round((Math.max(0, p.value) / total) * 100);
|
|
236
|
-
const pieBarWidth = Math.max(1, chartWidth - labelWidth - 9);
|
|
237
|
-
const pieBar = "█".repeat(Math.min(pieBarWidth, Math.max(1, Math.round((pct / 100) * pieBarWidth))));
|
|
238
|
-
return _jsxs(Text, { color: theme.code, children: ["\u2502 ", p.label.slice(0, labelWidth).padEnd(labelWidth), " ", String(pct).padStart(3), "% ", pieBar.padEnd(pieBarWidth, " ")] }, i);
|
|
239
|
-
}), _jsxs(Text, { color: theme.table, bold: true, children: ["\u2514", "─".repeat(chartWidth), "\u2518"] })] }));
|
|
240
|
-
}
|
|
241
|
-
return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Text, { color: theme.table, bold: true, children: ["\u250C\u2500", title, "─".repeat(Math.max(0, chartWidth - title.length - 2)), "\u2510"] }), rows.map((row, i) => _jsxs(Text, { color: theme.code, children: ["\u2502", row] }, i)), _jsxs(Text, { color: theme.table, bold: true, children: ["\u2514", "─".repeat(chartWidth), "\u2518"] })] }));
|
|
242
|
-
}
|
|
243
|
-
function timelinePreview(events, width) {
|
|
244
|
-
const detailWidth = Math.max(16, width - 18);
|
|
245
|
-
const icon = (status) => status === "done" ? "✓" : status === "error" ? "✗" : status === "active" ? "◆" : "○";
|
|
246
|
-
return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Text, { color: theme.table, bold: true, children: [" timeline \u00B7 ", events.length, " event", events.length === 1 ? "" : "s"] }), events.map((event, i) => (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: [_jsx(Text, { color: event.status === "error" ? theme.error : event.status === "done" ? theme.success : theme.accent, bold: true, children: icon(event.status) }), _jsxs(Text, { dimColor: true, children: [" ", clip(event.time || "—", 16).padEnd(16), " "] }), _jsx(Text, { bold: true, children: clip(event.title, detailWidth) })] }), event.detail && _jsxs(Text, { dimColor: true, children: [" ", i === events.length - 1 ? " " : "│", " ", clip(event.detail, detailWidth)] }), i < events.length - 1 && _jsx(Text, { dimColor: true, children: " \u2502" })] }, i)))] }));
|
|
247
|
-
}
|
|
248
|
-
/** Render inline markdown tokens as nested Ink <Text> elements. */
|
|
249
|
-
function renderInline(text, keyPrefix) {
|
|
250
|
-
const parts = [];
|
|
251
|
-
const re = /(`[^`\n]+`)|(\*\*[^*\n]+\*\*)|(\[[^\]\n]+\]\([^)\n]+\))/g;
|
|
252
|
-
let last = 0;
|
|
253
|
-
let k = 0;
|
|
254
|
-
let m;
|
|
255
|
-
while ((m = re.exec(text)) !== null) {
|
|
256
|
-
if (m.index > last)
|
|
257
|
-
parts.push(text.slice(last, m.index));
|
|
258
|
-
const tok = m[0];
|
|
259
|
-
if (tok.startsWith("`")) {
|
|
260
|
-
parts.push(_jsx(Text, { color: theme.code, children: tok.slice(1, -1) }, `${keyPrefix}-c${k++}`));
|
|
261
|
-
}
|
|
262
|
-
else if (tok.startsWith("**")) {
|
|
263
|
-
parts.push(_jsx(Text, { bold: true, children: tok.slice(2, -2) }, `${keyPrefix}-b${k++}`));
|
|
264
|
-
}
|
|
265
|
-
else {
|
|
266
|
-
const lm = /\[([^\]]+)\]\(([^)]+)\)/.exec(tok);
|
|
267
|
-
if (lm) {
|
|
268
|
-
parts.push(_jsxs(Text, { children: [_jsx(Text, { color: "blue", children: lm[1] }), _jsxs(Text, { dimColor: true, children: [" (", lm[2], ")"] })] }, `${keyPrefix}-l${k++}`));
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
last = m.index + tok.length;
|
|
272
|
-
}
|
|
273
|
-
if (last < text.length)
|
|
274
|
-
parts.push(text.slice(last));
|
|
275
|
-
return parts;
|
|
12
|
+
/**
|
|
13
|
+
* Parse cache: finalized assistant messages are immutable, so re-renders
|
|
14
|
+
* (sidebar tick, scroll, resize) must not re-parse. Small LRU keyed by text;
|
|
15
|
+
* parse depends only on the text, not the width.
|
|
16
|
+
*/
|
|
17
|
+
const PARSE_CACHE_MAX = 50;
|
|
18
|
+
const parseCache = new Map();
|
|
19
|
+
function parseCached(text) {
|
|
20
|
+
const hit = parseCache.get(text);
|
|
21
|
+
if (hit) {
|
|
22
|
+
// Refresh recency (LRU).
|
|
23
|
+
parseCache.delete(text);
|
|
24
|
+
parseCache.set(text, hit);
|
|
25
|
+
return hit;
|
|
26
|
+
}
|
|
27
|
+
const blocks = parseMarkdown(text);
|
|
28
|
+
parseCache.set(text, blocks);
|
|
29
|
+
if (parseCache.size > PARSE_CACHE_MAX) {
|
|
30
|
+
const oldest = parseCache.keys().next().value;
|
|
31
|
+
if (oldest !== undefined)
|
|
32
|
+
parseCache.delete(oldest);
|
|
33
|
+
}
|
|
34
|
+
return blocks;
|
|
276
35
|
}
|
|
277
36
|
export function Markdown({ text, width }) {
|
|
278
37
|
const paneWidth = contentWidth(width);
|
|
279
|
-
const
|
|
280
|
-
|
|
281
|
-
let i = 0;
|
|
282
|
-
let key = 0;
|
|
283
|
-
while (i < lines.length) {
|
|
284
|
-
const line = lines[i];
|
|
285
|
-
// Fenced code block
|
|
286
|
-
if (line.startsWith("```")) {
|
|
287
|
-
const lang = line.slice(3).trim().toLowerCase();
|
|
288
|
-
const code = [];
|
|
289
|
-
i++;
|
|
290
|
-
while (i < lines.length && !lines[i].startsWith("```"))
|
|
291
|
-
code.push(lines[i++]);
|
|
292
|
-
i++; // consume closing fence (or EOF)
|
|
293
|
-
// T-15: mermaid diagram → visual ASCII preview
|
|
294
|
-
if (lang === "mermaid" && code.length > 0) {
|
|
295
|
-
blocks.push(_jsxs(Box, { marginTop: 1, children: [_jsx(Text, { dimColor: true, children: " \u25C9 mermaid diagram" }), mermaidPreview(code)] }, key++));
|
|
296
|
-
continue;
|
|
297
|
-
}
|
|
298
|
-
if ((lang === "chart" || lang.startsWith("chart:")) && code.length > 0) {
|
|
299
|
-
const chart = parseChartSpec(code.join("\n"), lang);
|
|
300
|
-
blocks.push(chart ? _jsx(Box, { children: chartPreview(chart, paneWidth) }, key++) : _jsx(Text, { dimColor: true, children: " (invalid chart data)" }, key++));
|
|
301
|
-
continue;
|
|
302
|
-
}
|
|
303
|
-
if ((lang === "timeline" || lang === "events") && code.length > 0) {
|
|
304
|
-
const events = parseTimelineSpec(code.join("\n"));
|
|
305
|
-
blocks.push(events.length > 0 ? _jsx(Box, { children: timelinePreview(events, paneWidth) }, key++) : _jsx(Text, { dimColor: true, children: " (empty timeline)" }, key++));
|
|
306
|
-
continue;
|
|
307
|
-
}
|
|
308
|
-
// T-15: syntax-highlighted code block
|
|
309
|
-
blocks.push(_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: theme.border, paddingX: 1, marginTop: 1, children: [lang && _jsx(Text, { dimColor: true, bold: true, children: lang }), code.length === 0 ? (_jsx(Text, { dimColor: true, children: "(empty)" })) : (highlightCode(code, paneWidth)), code.length > MAX_CODE_LINES && (_jsxs(Text, { dimColor: true, children: [" \u2026 +", code.length - MAX_CODE_LINES, " more lines"] }))] }, key++));
|
|
310
|
-
continue;
|
|
311
|
-
}
|
|
312
|
-
// Headers
|
|
313
|
-
const h = /^(#{1,4})\s+(.*)$/.exec(line);
|
|
314
|
-
if (h) {
|
|
315
|
-
blocks.push(_jsx(Text, { bold: true, color: h[1].length <= 2 ? "magenta" : "cyan", children: renderInline(h[2], `h${key}`) }, key++));
|
|
316
|
-
i++;
|
|
317
|
-
continue;
|
|
318
|
-
}
|
|
319
|
-
// Table (GFM): header | separator | rows — collect the whole block.
|
|
320
|
-
if (/^\s*\|.*\|\s*$/.test(line) && i + 1 < lines.length && /^\s*\|[\s:|-]+\|\s*$/.test(lines[i + 1])) {
|
|
321
|
-
const MAX_CELL = 40;
|
|
322
|
-
const parseRow = (l) => l.trim().replace(/^\|/, "").replace(/\|$/, "").split("|").map((c) => c.trim());
|
|
323
|
-
const header = parseRow(line);
|
|
324
|
-
i += 2; // header + separator
|
|
325
|
-
const rows = [];
|
|
326
|
-
while (i < lines.length && /^\s*\|.*\|\s*$/.test(lines[i]))
|
|
327
|
-
rows.push(parseRow(lines[i++]));
|
|
328
|
-
// Column widths (bounded — wide cells clip via clip()).
|
|
329
|
-
const available = Math.max(20, paneWidth - 4);
|
|
330
|
-
const naturalWidths = header.map((h, c) => Math.min(MAX_CELL, Math.max(h.length, ...rows.map((r) => (r[c] ?? "").length))));
|
|
331
|
-
const separators = Math.max(0, header.length - 1) * 3 + 4;
|
|
332
|
-
const totalNatural = naturalWidths.reduce((sum, n) => sum + n, 0);
|
|
333
|
-
const widths = naturalWidths.map((n) => totalNatural + separators <= available ? n : Math.max(3, Math.floor((n / Math.max(1, totalNatural)) * (available - separators))));
|
|
334
|
-
const renderRow = (cells, chars, color, bold, rowKey) => {
|
|
335
|
-
const line = chars.l + widths.map((width, ci) => {
|
|
336
|
-
const cell = cells[ci] ?? "";
|
|
337
|
-
return (cell.length > width ? cell.slice(0, Math.max(1, width - 1)) + "…" : cell).padEnd(width, " ");
|
|
338
|
-
}).join(chars.m) + chars.r;
|
|
339
|
-
return _jsx(Text, { color: color, bold: bold, children: clip(line, paneWidth) }, rowKey);
|
|
340
|
-
};
|
|
341
|
-
blocks.push(_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [renderRow(header, { l: "┌ ", m: " ┬ ", r: " ┐" }, theme.table, true, `th${key}`), renderRow(header.map(() => ""), { l: "├", m: "┼", r: "┤" }, "gray", false, `ts${key}`), rows.map((r, ri) => renderRow(r, { l: "│ ", m: " │ ", r: " │" }, undefined, false, `tr${key}-${ri}`))] }, key++));
|
|
342
|
-
continue;
|
|
343
|
-
}
|
|
344
|
-
// Bullets + task lists: - [ ] pending · - [x] completed
|
|
345
|
-
const b = /^(\s*)[-*]\s+(.*)$/.exec(line);
|
|
346
|
-
if (b) {
|
|
347
|
-
const body = b[2];
|
|
348
|
-
const taskMatch = /^\[([ xX])\]\s+(.*)$/.exec(body);
|
|
349
|
-
const indent = " ".repeat(Math.floor(b[1].length / 2));
|
|
350
|
-
if (taskMatch) {
|
|
351
|
-
const checked = taskMatch[1].toLowerCase() === "x";
|
|
352
|
-
const taskBody = taskMatch[2];
|
|
353
|
-
blocks.push(_jsxs(Text, { children: [_jsxs(Text, { color: theme.accent, children: [indent, checked ? "☑" : "☐", " "] }), checked
|
|
354
|
-
? _jsx(Text, { dimColor: true, strikethrough: true, children: renderInline(taskBody, `b${key}`) })
|
|
355
|
-
: renderInline(taskBody, `b${key}`)] }, key++));
|
|
356
|
-
}
|
|
357
|
-
else {
|
|
358
|
-
blocks.push(_jsxs(Text, { children: [_jsxs(Text, { color: theme.accent, children: [indent, "\u2022 "] }), renderInline(body, `b${key}`)] }, key++));
|
|
359
|
-
}
|
|
360
|
-
i++;
|
|
361
|
-
continue;
|
|
362
|
-
}
|
|
363
|
-
// Numbered list — keep the numbering as-is; nested indent preserved.
|
|
364
|
-
const n = /^(\s*)(\d+\.)\s+(.*)$/.exec(line);
|
|
365
|
-
if (n) {
|
|
366
|
-
const indent = " ".repeat(Math.floor(n[1].length / 2));
|
|
367
|
-
blocks.push(_jsxs(Text, { children: [_jsx(Text, { color: "cyan", children: indent }), renderInline(`${n[2]} ${n[3]}`, `n${key}`)] }, key++));
|
|
368
|
-
i++;
|
|
369
|
-
continue;
|
|
370
|
-
}
|
|
371
|
-
// Quote
|
|
372
|
-
if (/^>\s?/.test(line)) {
|
|
373
|
-
blocks.push(_jsxs(Text, { dimColor: true, children: [_jsx(Text, { color: theme.dim, children: "\u2502" }), " ", _jsx(Text, { italic: true, children: clip(line.replace(/^>\s?/, "")) })] }, key++));
|
|
374
|
-
i++;
|
|
375
|
-
continue;
|
|
376
|
-
}
|
|
377
|
-
// Blank line → single spacer
|
|
378
|
-
if (line.trim() === "") {
|
|
379
|
-
blocks.push(_jsx(Text, { children: " " }, key++));
|
|
380
|
-
i++;
|
|
381
|
-
continue;
|
|
382
|
-
}
|
|
383
|
-
blocks.push(_jsx(Text, { children: renderInline(clip(line, paneWidth), `t${key}`) }, key++));
|
|
384
|
-
i++;
|
|
385
|
-
}
|
|
386
|
-
return _jsx(Box, { flexDirection: "column", children: blocks });
|
|
38
|
+
const blocks = parseCached(text);
|
|
39
|
+
return (_jsx(Box, { flexDirection: "column", children: blocks.map((block, i) => renderBlock(block, paneWidth, i)) }));
|
|
387
40
|
}
|
|
@@ -8,6 +8,7 @@ import { useState } from "react";
|
|
|
8
8
|
import { Box, Text, useInput } from "ink";
|
|
9
9
|
import { DiffLine } from "./chat-views.js";
|
|
10
10
|
import { theme } from "./theme.js";
|
|
11
|
+
import { boxBorderStyle } from "./charset.js";
|
|
11
12
|
const MODAL_DIFF_WINDOW = 20; // rows hiển thị cùng lúc; scroll bằng pgup/pgdn
|
|
12
13
|
/**
|
|
13
14
|
* Permission request modal — elevated visual with a bold colored header
|
|
@@ -30,5 +31,5 @@ export function PermissionModal({ prompt }) {
|
|
|
30
31
|
const shown = lines.slice(start, start + MODAL_DIFF_WINDOW);
|
|
31
32
|
const hasMoreAbove = start > 0;
|
|
32
33
|
const hasMoreBelow = start + MODAL_DIFF_WINDOW < lines.length;
|
|
33
|
-
return (_jsxs(Box, { flexDirection: "column", borderStyle:
|
|
34
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: boxBorderStyle(), borderColor: theme.warn, paddingX: 1, paddingY: 1, children: [_jsxs(Box, { justifyContent: "space-between", marginBottom: 1, children: [_jsxs(Text, { color: theme.warn, bold: true, children: ["\u26A0 permission \u2014 ", prompt.kind] }), _jsxs(Text, { dimColor: true, children: ["timeout ", prompt.timeoutSecs, "s \u2192 deny"] })] }), _jsx(Text, { bold: true, children: prompt.label }), prompt.diff && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [hasMoreAbove && _jsxs(Text, { dimColor: true, children: ["\u2191 ", start, " earlier lines"] }), shown.map((l, i) => (_jsx(DiffLine, { line: l.length > 160 ? l.slice(0, 160) + "…" : l }, start + i))), hasMoreBelow && (_jsxs(Text, { dimColor: true, children: ["\u2193 ", lines.length - start - MODAL_DIFF_WINDOW, " more lines"] }))] })), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { dimColor: true, children: "─".repeat(28) }), _jsx(Text, { dimColor: true, children: " y \u00B7 a \u00B7 n \u00B7 q to answer" }), prompt.diff && _jsx(Text, { dimColor: true, children: " pgup \u00B7 pgdn \u00B7 home \u00B7 end scroll diff" })] })] }));
|
|
34
35
|
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { parseMarkdown } from "./markdown/parse.js";
|
|
2
|
+
import { measure } from "./text-width.js";
|
|
3
|
+
const EST_WIDTH = 80;
|
|
4
|
+
/** Ước lượng số dòng wrap của một đoạn text ở width cho trước. */
|
|
5
|
+
function wrapEstimate(text, width) {
|
|
6
|
+
return text.split("\n").reduce((sum, line) => sum + Math.max(1, Math.ceil(measure(line) / width)), 0);
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Block-aware estimate cho assistant message: parse markdown rồi cộng chiều
|
|
10
|
+
* cao từng block. Chính xác hơn nhiều so với heuristic len/80 cũ — bảng (+3
|
|
11
|
+
* viền), code box (+2 viền + gutter wrap), mermaid (~14 dòng) trước đây làm
|
|
12
|
+
* virtualization window lệch lớn.
|
|
13
|
+
*/
|
|
14
|
+
export function estimateMarkdownRows(text, width = EST_WIDTH) {
|
|
15
|
+
if (!text)
|
|
16
|
+
return 1;
|
|
17
|
+
let rows = 0;
|
|
18
|
+
for (const block of parseMarkdown(text)) {
|
|
19
|
+
switch (block.kind) {
|
|
20
|
+
case "paragraph":
|
|
21
|
+
rows += wrapEstimate(block.text, width);
|
|
22
|
+
break;
|
|
23
|
+
case "heading":
|
|
24
|
+
case "hr":
|
|
25
|
+
case "spacer":
|
|
26
|
+
rows += 1;
|
|
27
|
+
break;
|
|
28
|
+
case "code": {
|
|
29
|
+
const gutter = String(Math.min(block.lines.length, 60)).length;
|
|
30
|
+
const codeWidth = Math.max(8, width - gutter - 6);
|
|
31
|
+
const shown = Math.min(block.lines.length, 60);
|
|
32
|
+
rows += wrapEstimate(block.lines.slice(0, shown).join("\n"), codeWidth)
|
|
33
|
+
+ 2 // border top/bottom
|
|
34
|
+
+ (block.lang ? 1 : 0)
|
|
35
|
+
+ (block.lines.length > 60 ? 1 : 0)
|
|
36
|
+
+ 1; // marginTop
|
|
37
|
+
break;
|
|
38
|
+
}
|
|
39
|
+
case "table":
|
|
40
|
+
// top + header + separator + rows + bottom (+ cap note) + margin
|
|
41
|
+
rows += Math.min(block.rows.length, 50) + 4 + (block.rows.length > 50 ? 1 : 0) + 1;
|
|
42
|
+
break;
|
|
43
|
+
case "list":
|
|
44
|
+
rows += block.items.reduce((sum, it) => sum + wrapEstimate(it.text, Math.max(8, width - 4 - it.depth * 2)), 0);
|
|
45
|
+
break;
|
|
46
|
+
case "quote":
|
|
47
|
+
rows += wrapEstimate(block.text, Math.max(8, width - 2));
|
|
48
|
+
break;
|
|
49
|
+
case "mermaid":
|
|
50
|
+
// summary box: ~4 header lines + ≤6 nodes + ≤8 edges + hint + 2 border
|
|
51
|
+
rows += Math.min(block.lines.length, 12) + 6;
|
|
52
|
+
break;
|
|
53
|
+
case "chart":
|
|
54
|
+
rows += 2 + 40 + 1; // ContentBox max: borders + ≤40 data rows
|
|
55
|
+
break;
|
|
56
|
+
case "timeline":
|
|
57
|
+
rows += 1 + block.source.split("\n").length * 2;
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return rows;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* T4: ước lượng chiều cao (rows) của một ChatItem khi render — dùng cho
|
|
65
|
+
* virtualization (chỉ render window quanh viewport thay vì cap cứng).
|
|
66
|
+
* Over-estimate nhẹ an toàn hơn under-estimate (viewport luôn đủ phủ).
|
|
67
|
+
* Assistant items dùng estimateMarkdownRows (block-aware); các kind khác giữ
|
|
68
|
+
* heuristic đơn giản vì render gần như 1:1 với text.
|
|
69
|
+
*/
|
|
70
|
+
export function estimateItemRows(item) {
|
|
71
|
+
switch (item.kind) {
|
|
72
|
+
case "user":
|
|
73
|
+
return 2 + Math.ceil(item.text.length / 80);
|
|
74
|
+
case "assistant":
|
|
75
|
+
// +1 marginTop; streaming raw text dùng wrap đơn giản (chưa qua markdown).
|
|
76
|
+
return item.streaming
|
|
77
|
+
? wrapEstimate(item.text ?? "", EST_WIDTH)
|
|
78
|
+
: 1 + estimateMarkdownRows(item.text ?? "", EST_WIDTH);
|
|
79
|
+
case "thinking":
|
|
80
|
+
// Render: collapse to last 4 lines.
|
|
81
|
+
return 1 + Math.min(4, item.text ? item.text.split("\n").length : 0);
|
|
82
|
+
case "tool":
|
|
83
|
+
return 1 + 2; // title + preview 2 lines
|
|
84
|
+
case "compaction":
|
|
85
|
+
return 1;
|
|
86
|
+
case "diff":
|
|
87
|
+
return 3 + Math.min(24, (item.text?.split("\n").length ?? 0)); // header + ≤24 lines + border
|
|
88
|
+
case "todo":
|
|
89
|
+
// TodoItemView render null (plan hiển thị ở sidebar qua state.plan) —
|
|
90
|
+
// chiếm 0 dòng thực tế trong transcript.
|
|
91
|
+
return 0;
|
|
92
|
+
case "build":
|
|
93
|
+
return 1;
|
|
94
|
+
case "notice":
|
|
95
|
+
return 1 + Math.ceil(item.text.length / 2000);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* T4: chọn window item để render: lấy từ cuối lên tới khi tổng rows ước
|
|
100
|
+
* lượng ≥ viewportRows + margin (render dư phía trên để scroll mượt), nhưng
|
|
101
|
+
* giữ toàn bộ khi tổng nhỏ. Trả về slice của items (không mutate).
|
|
102
|
+
*/
|
|
103
|
+
export function visibleWindow(items, viewportRows, margin = 50) {
|
|
104
|
+
const budget = viewportRows + margin;
|
|
105
|
+
let acc = 0;
|
|
106
|
+
let start = items.length;
|
|
107
|
+
while (start > 0 && acc < budget) {
|
|
108
|
+
start--;
|
|
109
|
+
acc += estimateItemRows(items[start]);
|
|
110
|
+
}
|
|
111
|
+
return start === 0 ? items : items.slice(start);
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Plan mới nhất cho sidebar — todo items được append mỗi lần update nhưng
|
|
115
|
+
* sidebar chỉ cần snapshot hiện tại (last-wins), null khi chưa có plan.
|
|
116
|
+
*
|
|
117
|
+
* Reset realtime: mỗi lần user gửi message mới, plan reset — chỉ hiện plan
|
|
118
|
+
* được viết SAU user message cuối cùng. Tránh stale plan từ task trước hiện
|
|
119
|
+
* trong task mới.
|
|
120
|
+
*/
|
|
121
|
+
export function latestPlan(items) {
|
|
122
|
+
// Tìm user message cuối cùng — plan chỉ reset tại đây.
|
|
123
|
+
let lastUserIdx = -1;
|
|
124
|
+
for (let i = items.length - 1; i >= 0; i--) {
|
|
125
|
+
if (items[i].kind === "user") {
|
|
126
|
+
lastUserIdx = i;
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
// Chỉ tìm todo SAU user cuối.
|
|
131
|
+
for (let i = items.length - 1; i > lastUserIdx; i--) {
|
|
132
|
+
if (items[i].kind === "todo")
|
|
133
|
+
return items[i];
|
|
134
|
+
}
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Map tên tool → phase của breadcrumb (plan→explore→edit→build→verify).
|
|
139
|
+
* Trả về null với tool không thuộc phase nào (mcp__*...).
|
|
140
|
+
*/
|
|
141
|
+
export function toolPhase(name) {
|
|
142
|
+
if (name === "write_todos")
|
|
143
|
+
return "plan";
|
|
144
|
+
if (name === "list_files" || name === "read_file" || name === "grep" || name === "glob" || name === "web_fetch" || name === "load_skill" || name === "repo_map")
|
|
145
|
+
return "explore";
|
|
146
|
+
if (name === "write_file" || name === "edit_file" || name === "apply_patch")
|
|
147
|
+
return "edit";
|
|
148
|
+
if (name === "run_command")
|
|
149
|
+
return "build";
|
|
150
|
+
if (name === "task")
|
|
151
|
+
return "delegate";
|
|
152
|
+
if (name === "ask_user")
|
|
153
|
+
return "clear";
|
|
154
|
+
if (name.startsWith("mcp__"))
|
|
155
|
+
return "mcp";
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
@@ -9,8 +9,9 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
9
9
|
*/
|
|
10
10
|
import { useEffect, useState } from "react";
|
|
11
11
|
import { Box, Text, useInput } from "ink";
|
|
12
|
-
import { listSessions } from "../session.js";
|
|
12
|
+
import { listSessions } from "../session/session.js";
|
|
13
13
|
import { theme, fit } from "./theme.js";
|
|
14
|
+
import { boxBorderStyle } from "./charset.js";
|
|
14
15
|
const DISPLAY_LIMIT = 12;
|
|
15
16
|
export function SessionPicker({ workdir, onSelect, onClose }) {
|
|
16
17
|
const [sessions, setSessions] = useState([]);
|
|
@@ -57,9 +58,9 @@ export function SessionPicker({ workdir, onSelect, onClose }) {
|
|
|
57
58
|
}
|
|
58
59
|
});
|
|
59
60
|
if (sessions.length === 0) {
|
|
60
|
-
return (_jsxs(Box, { flexDirection: "column", borderStyle:
|
|
61
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: boxBorderStyle(), borderColor: theme.brand, paddingX: 1, paddingY: 1, children: [_jsx(Text, { color: theme.brand, bold: true, children: "\u2190 saved sessions" }), _jsx(Text, { dimColor: true, children: "no saved sessions found" }), _jsx(Text, { dimColor: true, children: "esc to close" })] }));
|
|
61
62
|
}
|
|
62
|
-
return (_jsxs(Box, { flexDirection: "column", borderStyle:
|
|
63
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: boxBorderStyle(), borderColor: theme.brand, paddingX: 1, paddingY: 1, children: [_jsxs(Box, { justifyContent: "space-between", marginBottom: 1, children: [_jsx(Text, { color: theme.brand, bold: true, children: "\u2190 saved sessions" }), _jsxs(Text, { dimColor: true, children: [sessions.length, " session", sessions.length !== 1 ? "s" : ""] })] }), _jsx(Box, { children: _jsx(Text, { dimColor: true, children: "─".repeat(38) }) }), sessions.map((s, i) => {
|
|
63
64
|
const isSelected = i === selected;
|
|
64
65
|
const idPart = s.id.slice(0, 14);
|
|
65
66
|
const dateStr = s.updatedAt ? s.updatedAt.slice(0, 16).replace("T", " ") : "—";
|