codemini-cli 0.2.7 → 0.2.8
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/package.json +1 -1
- package/src/cli.js +1 -1
- package/src/core/agent-loop.js +101 -3
- package/src/core/default-system-prompt.js +38 -0
- package/src/core/provider/openai-compatible.js +34 -4
- package/src/core/shell-profile.js +14 -3
- package/src/core/tools.js +163 -55
- package/src/tui/chat-app.js +441 -13
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -4,7 +4,7 @@ import { handleConfig } from './commands/config.js';
|
|
|
4
4
|
import { handleDoctor } from './commands/doctor.js';
|
|
5
5
|
import { handleSkill } from './commands/skill.js';
|
|
6
6
|
|
|
7
|
-
const VERSION = '0.2.
|
|
7
|
+
const VERSION = '0.2.8';
|
|
8
8
|
|
|
9
9
|
function printHelp() {
|
|
10
10
|
console.log(`codemini ${VERSION}
|
package/src/core/agent-loop.js
CHANGED
|
@@ -14,6 +14,101 @@ function safeJsonParse(raw) {
|
|
|
14
14
|
}
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
function parseInlineRangePath(value) {
|
|
18
|
+
const text = String(value || '').trim();
|
|
19
|
+
if (!text) return null;
|
|
20
|
+
const match = text.match(/^(.*?):(\d+)(?:-(\d+))?$/);
|
|
21
|
+
if (!match) return null;
|
|
22
|
+
const [, maybePath, startRaw, endRaw] = match;
|
|
23
|
+
if (!maybePath || /^(?:[A-Za-z])$/.test(maybePath)) return null;
|
|
24
|
+
const start = Number(startRaw);
|
|
25
|
+
const end = Number(endRaw || startRaw);
|
|
26
|
+
if (!Number.isFinite(start) || start <= 0) return null;
|
|
27
|
+
if (!Number.isFinite(end) || end < start) return null;
|
|
28
|
+
return { path: maybePath, start_line: start, end_line: end };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function normalizeToolArguments(toolName, args, rawArguments) {
|
|
32
|
+
const rawText = typeof rawArguments === 'string' ? rawArguments.trim() : '';
|
|
33
|
+
const primitive =
|
|
34
|
+
args == null || Array.isArray(args) || typeof args !== 'object'
|
|
35
|
+
? args
|
|
36
|
+
: null;
|
|
37
|
+
const source =
|
|
38
|
+
args && typeof args === 'object' && !Array.isArray(args)
|
|
39
|
+
? { ...args }
|
|
40
|
+
: {};
|
|
41
|
+
|
|
42
|
+
if (primitive != null && typeof primitive !== 'object') {
|
|
43
|
+
source._raw = rawText || String(primitive);
|
|
44
|
+
} else if (!source._raw && rawText && source._invalid_json) {
|
|
45
|
+
source._raw = rawText;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const stringValue =
|
|
49
|
+
typeof primitive === 'string'
|
|
50
|
+
? primitive.trim()
|
|
51
|
+
: String(source._raw || '').trim();
|
|
52
|
+
|
|
53
|
+
if (toolName === 'read') {
|
|
54
|
+
const value = String(source.path || source.file_path || source.file || stringValue || '').trim();
|
|
55
|
+
if (value) source.path = value;
|
|
56
|
+
if (source.offset != null && source.start_line == null) source.start_line = source.offset;
|
|
57
|
+
if (source.limit != null && source.end_line == null && Number(source.start_line) > 0) {
|
|
58
|
+
source.end_line = Number(source.start_line) + Number(source.limit) - 1;
|
|
59
|
+
}
|
|
60
|
+
const range = parseInlineRangePath(source.path);
|
|
61
|
+
if (range) {
|
|
62
|
+
source.path = range.path;
|
|
63
|
+
if (source.start_line == null) source.start_line = range.start_line;
|
|
64
|
+
if (source.end_line == null) source.end_line = range.end_line;
|
|
65
|
+
}
|
|
66
|
+
return source;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (toolName === 'list') {
|
|
70
|
+
const value = String(source.path || source.dir || source.directory || stringValue || '.').trim();
|
|
71
|
+
return { ...source, path: value || '.' };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (toolName === 'glob') {
|
|
75
|
+
const pattern = String(source.pattern || source.glob || source.query || stringValue || '').trim();
|
|
76
|
+
if (pattern) source.pattern = pattern;
|
|
77
|
+
if (!source.path && source.directory) source.path = source.directory;
|
|
78
|
+
return source;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (toolName === 'grep') {
|
|
82
|
+
const pattern = String(source.pattern || source.query || source.symbol || source.q || stringValue || '').trim();
|
|
83
|
+
if (pattern) source.pattern = pattern;
|
|
84
|
+
if (!source.path && (source.directory || source.dir || source.cwd)) {
|
|
85
|
+
source.path = source.directory || source.dir || source.cwd;
|
|
86
|
+
}
|
|
87
|
+
return source;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (toolName === 'write') {
|
|
91
|
+
const value = String(source.path || source.file_path || source.file || stringValue || '').trim();
|
|
92
|
+
if (value) source.path = value;
|
|
93
|
+
if (source.content == null && source.text != null) source.content = source.text;
|
|
94
|
+
if (source.content == null && source.new_content != null) source.content = source.new_content;
|
|
95
|
+
return source;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (toolName === 'edit') {
|
|
99
|
+
const value = String(source.path || source.file || source.file_path || '').trim();
|
|
100
|
+
if (value && !source.path) source.path = value;
|
|
101
|
+
return source;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return source;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function emptyToolResultMarker(toolName) {
|
|
108
|
+
const name = String(toolName || 'tool').trim() || 'tool';
|
|
109
|
+
return `(${name} completed with no output)`;
|
|
110
|
+
}
|
|
111
|
+
|
|
17
112
|
function clipToolResult(result, maxChars = 12000) {
|
|
18
113
|
const raw = typeof result === 'string' ? result : JSON.stringify(result);
|
|
19
114
|
if (!maxChars || raw.length <= maxChars) return raw;
|
|
@@ -374,9 +469,12 @@ function formatToolDisplayName(name, args) {
|
|
|
374
469
|
function formatToolResult(toolResult, toolName, args, toolFormatters, toolResultMaxChars) {
|
|
375
470
|
if (toolFormatters && typeof toolFormatters[toolName] === 'function') {
|
|
376
471
|
const formatted = toolFormatters[toolName](toolResult, args);
|
|
377
|
-
if (typeof formatted === 'string')
|
|
472
|
+
if (typeof formatted === 'string') {
|
|
473
|
+
return formatted.trim() ? formatted : emptyToolResultMarker(toolName);
|
|
474
|
+
}
|
|
378
475
|
}
|
|
379
|
-
|
|
476
|
+
const fallback = compactToolResult(toolResult, toolName, args, toolResultMaxChars);
|
|
477
|
+
return String(fallback || '').trim() ? fallback : emptyToolResultMarker(toolName);
|
|
380
478
|
}
|
|
381
479
|
|
|
382
480
|
// ─── Main agent loop ────────────────────────────────────────────────
|
|
@@ -469,8 +567,8 @@ export async function runAgentLoop({
|
|
|
469
567
|
// ─── P1a: Partition into read-only (parallel) and write (serial) ──
|
|
470
568
|
|
|
471
569
|
const callsWithMeta = toolCalls.map((call) => {
|
|
472
|
-
const args = safeJsonParse(call.arguments);
|
|
473
570
|
const toolName = normalizeToolCallName(call.name);
|
|
571
|
+
const args = normalizeToolArguments(toolName, safeJsonParse(call.arguments), call.arguments);
|
|
474
572
|
const displayName = formatToolDisplayName(toolName, args);
|
|
475
573
|
const isReadOnly = READ_ONLY_TOOLS.has(toolName);
|
|
476
574
|
return { call, args, toolName, displayName, isReadOnly };
|
|
@@ -2,6 +2,42 @@ import os from 'node:os';
|
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import { getShellSystemPrompt } from './shell-profile.js';
|
|
4
4
|
|
|
5
|
+
function getToolFewShotBlock() {
|
|
6
|
+
const cwd = process.cwd();
|
|
7
|
+
return `# Tool Examples
|
|
8
|
+
|
|
9
|
+
Use these as style examples for tool calls:
|
|
10
|
+
|
|
11
|
+
Current working directory: ${cwd}
|
|
12
|
+
When a tool takes file_path, build it from the current working directory and prefer absolute paths.
|
|
13
|
+
If the user mentions a project-relative path like src/app.ts, resolve it from ${cwd} instead of guessing parent directories.
|
|
14
|
+
|
|
15
|
+
1. File discovery then read
|
|
16
|
+
User: compare the auth flow
|
|
17
|
+
Assistant: first locate the relevant files
|
|
18
|
+
Tool: glob({"pattern":"src/**/*auth*.ts"})
|
|
19
|
+
Tool: read({"file_path":"${cwd}/src/auth/service.ts"})
|
|
20
|
+
|
|
21
|
+
2. Targeted search then exact text edit
|
|
22
|
+
User: rename loginUser to signInUser
|
|
23
|
+
Assistant: first find the exact occurrences
|
|
24
|
+
Tool: grep({"pattern":"loginUser","path":"src"})
|
|
25
|
+
Tool: edit({"file_path":"${cwd}/src/auth/service.ts","old_string":"loginUser","new_string":"signInUser"})
|
|
26
|
+
|
|
27
|
+
3. Read a specific range
|
|
28
|
+
User: inspect the reducer around line 120
|
|
29
|
+
Assistant: read only the needed range
|
|
30
|
+
Tool: read({"path":"${cwd}/src/store/reducer.ts:110-150"})
|
|
31
|
+
|
|
32
|
+
4. Create a new file
|
|
33
|
+
User: add a notes file
|
|
34
|
+
Assistant: create the file directly
|
|
35
|
+
Tool: write({"file":"${cwd}/notes.txt","text":"todo\\n"})
|
|
36
|
+
|
|
37
|
+
Prefer these direct tool shapes over multi-step metadata reads or shell fallbacks.
|
|
38
|
+
Prefer explicit absolute file_path values when the current working directory is known.`;
|
|
39
|
+
}
|
|
40
|
+
|
|
5
41
|
function getEnvBlock() {
|
|
6
42
|
const cwd = process.cwd();
|
|
7
43
|
let isGitRepo = false;
|
|
@@ -22,5 +58,7 @@ OS Version: ${os.version || os.release()}
|
|
|
22
58
|
export function buildDefaultSystemPrompt(config = {}) {
|
|
23
59
|
return `${getShellSystemPrompt(config?.shell?.default)}
|
|
24
60
|
|
|
61
|
+
${getToolFewShotBlock()}
|
|
62
|
+
|
|
25
63
|
${getEnvBlock()}`;
|
|
26
64
|
}
|
|
@@ -47,6 +47,16 @@ function normalizeToolCallArguments(argumentsText) {
|
|
|
47
47
|
return '{}';
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
function normalizeIncomingToolCallArguments(argumentsValue) {
|
|
51
|
+
if (typeof argumentsValue === 'string') return argumentsValue;
|
|
52
|
+
if (argumentsValue == null) return '{}';
|
|
53
|
+
try {
|
|
54
|
+
return JSON.stringify(argumentsValue);
|
|
55
|
+
} catch {
|
|
56
|
+
return '{}';
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
50
60
|
function sanitizeGatewayMessages(messages) {
|
|
51
61
|
const source = Array.isArray(messages) ? messages : [];
|
|
52
62
|
return source
|
|
@@ -115,7 +125,18 @@ function buildPayload({ model, temperature, messages, tools, stream = false }) {
|
|
|
115
125
|
return payload;
|
|
116
126
|
}
|
|
117
127
|
|
|
118
|
-
function
|
|
128
|
+
function hasTrailingToolContext(messages) {
|
|
129
|
+
const source = Array.isArray(messages) ? messages : [];
|
|
130
|
+
for (let index = source.length - 1; index >= 0; index -= 1) {
|
|
131
|
+
const message = source[index];
|
|
132
|
+
if (!message || typeof message !== 'object') continue;
|
|
133
|
+
if (message.role === 'tool') return true;
|
|
134
|
+
if (message.role === 'assistant' || message.role === 'user') return false;
|
|
135
|
+
}
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function buildFinalStreamResult(text, toolCallsByIndex, usage, messages) {
|
|
119
140
|
const toolCalls = Array.from(toolCallsByIndex.entries())
|
|
120
141
|
.sort((a, b) => a[0] - b[0])
|
|
121
142
|
.map(([, tc], i) => ({
|
|
@@ -126,6 +147,13 @@ function buildFinalStreamResult(text, toolCallsByIndex, usage) {
|
|
|
126
147
|
.filter((tc) => tc.name);
|
|
127
148
|
|
|
128
149
|
if (!text && toolCalls.length === 0) {
|
|
150
|
+
if (hasTrailingToolContext(messages)) {
|
|
151
|
+
return {
|
|
152
|
+
text: '',
|
|
153
|
+
toolCalls: [],
|
|
154
|
+
usage
|
|
155
|
+
};
|
|
156
|
+
}
|
|
129
157
|
throw new Error('Gateway stream returned empty assistant response');
|
|
130
158
|
}
|
|
131
159
|
|
|
@@ -216,7 +244,7 @@ export async function createChatCompletion({
|
|
|
216
244
|
const toolCalls = (message.tool_calls || []).map((tc) => ({
|
|
217
245
|
id: tc.id,
|
|
218
246
|
name: tc.function?.name,
|
|
219
|
-
arguments: tc.function?.arguments
|
|
247
|
+
arguments: normalizeIncomingToolCallArguments(tc.function?.arguments)
|
|
220
248
|
}));
|
|
221
249
|
|
|
222
250
|
if (!text && toolCalls.length === 0) {
|
|
@@ -280,7 +308,9 @@ export async function createChatCompletionStream({
|
|
|
280
308
|
const current = toolCallsByIndex.get(idx) || emptyToolCall(idx);
|
|
281
309
|
if (td.id) current.id = td.id;
|
|
282
310
|
if (td.function?.name) current.name = `${current.name}${td.function.name}`;
|
|
283
|
-
if (td.function?.arguments
|
|
311
|
+
if (td.function?.arguments !== undefined) {
|
|
312
|
+
current.arguments = `${current.arguments}${normalizeIncomingToolCallArguments(td.function.arguments)}`;
|
|
313
|
+
}
|
|
284
314
|
toolCallsByIndex.set(idx, current);
|
|
285
315
|
if (onToolCallDelta) {
|
|
286
316
|
onToolCallDelta({
|
|
@@ -293,5 +323,5 @@ export async function createChatCompletionStream({
|
|
|
293
323
|
}
|
|
294
324
|
}
|
|
295
325
|
|
|
296
|
-
return buildFinalStreamResult(text, toolCallsByIndex, usage);
|
|
326
|
+
return buildFinalStreamResult(text, toolCallsByIndex, usage, messages);
|
|
297
327
|
}
|
|
@@ -123,11 +123,11 @@ export function getShellSystemPrompt(value) {
|
|
|
123
123
|
# Using your tools
|
|
124
124
|
|
|
125
125
|
ALWAYS prefer dedicated tools over raw shell commands:
|
|
126
|
-
- Use read to inspect files — NEVER use cat, head, or tail via run
|
|
126
|
+
- Use read to inspect files — NEVER use cat, head, or tail via run. read returns content directly by default; demo-style shapes like {file_path:"src/app.ts"}, {path:"src/app.ts:10-40"}, or {file_path:"src/app.ts", offset:10, limit:30} are accepted
|
|
127
127
|
- Use grep to search file contents — NEVER use grep or rg via run
|
|
128
128
|
- Use glob to find files by pattern — NEVER use find via run
|
|
129
|
-
- Use edit to modify existing files — this is the DEFAULT path for code changes
|
|
130
|
-
- Use write only for creating new files or complete rewrites (set full_file_rewrite=true for existing code files)
|
|
129
|
+
- Use edit to modify existing files — this is the DEFAULT path for code changes. Demo-style aliases like {file_path:"src/app.ts", old_string:"foo", new_string:"bar"} are accepted
|
|
130
|
+
- Use write only for creating new files or complete rewrites (set full_file_rewrite=true for existing code files). Aliases like {file:"notes.txt", text:"..."} are accepted
|
|
131
131
|
- Use patch to apply unified diffs
|
|
132
132
|
- Use run for one-shot shell commands: install, build, test, or other finite tasks
|
|
133
133
|
- For long-running processes (dev servers, watchers), use start_service instead of run
|
|
@@ -140,6 +140,17 @@ For services: use start_service to launch, list_services/get_service_status/get_
|
|
|
140
140
|
|
|
141
141
|
Some tools are loaded on demand. If a needed tool is not listed, call tool_search first to load it.
|
|
142
142
|
|
|
143
|
+
Common tool call patterns:
|
|
144
|
+
- Read a file: {path:"src/app.ts"} or {file_path:"src/app.ts", offset:20, limit:40}
|
|
145
|
+
- Read a specific range inline: {path:"src/app.ts:20-60"}
|
|
146
|
+
- Search text: {pattern:"loginUser", path:"src"} or {query:"loginUser", directory:"src"}
|
|
147
|
+
- Find files: {pattern:"src/**/*.ts"} or {query:"src/**/*.ts"}
|
|
148
|
+
- Edit exact text: {file_path:"src/app.ts", old_string:"foo", new_string:"bar"}
|
|
149
|
+
- Edit with shorthand: {path:"src/app.ts", old_text:"foo", content:"bar"}
|
|
150
|
+
- Write a new file: {file:"notes.txt", text:"..."} or {path:"src/page.tsx", content:"..."}
|
|
151
|
+
- When the environment provides a Working directory, prefer absolute file_path values rooted there instead of guessing prefixes
|
|
152
|
+
- If the user gives a relative path like src/app.ts, resolve it from the current Working directory rather than inventing ../ or sibling folders
|
|
153
|
+
|
|
143
154
|
# Doing tasks
|
|
144
155
|
|
|
145
156
|
- You are a terminal-first CLI coding agent, not a generic chat assistant
|
package/src/core/tools.js
CHANGED
|
@@ -119,6 +119,111 @@ function splitLines(text) {
|
|
|
119
119
|
return String(text || '').split('\n');
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
+
function parseInlineReadRange(value) {
|
|
123
|
+
const text = String(value || '').trim();
|
|
124
|
+
if (!text) return null;
|
|
125
|
+
const match = text.match(/^(.*?):(\d+)(?:-(\d+))?$/);
|
|
126
|
+
if (!match) return null;
|
|
127
|
+
const [, maybePath, startRaw, endRaw] = match;
|
|
128
|
+
if (!maybePath || /^(?:[A-Za-z])$/.test(maybePath)) return null;
|
|
129
|
+
const startLine = Number(startRaw);
|
|
130
|
+
const endLine = Number(endRaw || startRaw);
|
|
131
|
+
if (!Number.isFinite(startLine) || startLine <= 0) return null;
|
|
132
|
+
if (!Number.isFinite(endLine) || endLine < startLine) return null;
|
|
133
|
+
return {
|
|
134
|
+
path: maybePath,
|
|
135
|
+
start_line: startLine,
|
|
136
|
+
end_line: endLine
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function normalizeReadArgs(rawArgs) {
|
|
141
|
+
const source =
|
|
142
|
+
rawArgs && typeof rawArgs === 'object' && !Array.isArray(rawArgs)
|
|
143
|
+
? { ...rawArgs }
|
|
144
|
+
: { path: typeof rawArgs === 'string' ? rawArgs : '' };
|
|
145
|
+
|
|
146
|
+
const normalized = { ...source };
|
|
147
|
+
const aliasPath = String(source.path || source.file_path || source.file || source.target || '').trim();
|
|
148
|
+
if (aliasPath) normalized.path = aliasPath;
|
|
149
|
+
|
|
150
|
+
if (!Number.isFinite(Number(normalized.start_line)) && Number.isFinite(Number(source.offset))) {
|
|
151
|
+
normalized.start_line = Number(source.offset);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (!Number.isFinite(Number(normalized.end_line)) && Number.isFinite(Number(source.limit))) {
|
|
155
|
+
const startLine = Number(normalized.start_line);
|
|
156
|
+
const limit = Number(source.limit);
|
|
157
|
+
if (startLine > 0 && limit > 0) {
|
|
158
|
+
normalized.end_line = startLine + limit - 1;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const inlineRange = parseInlineReadRange(normalized.path);
|
|
163
|
+
if (inlineRange) {
|
|
164
|
+
normalized.path = inlineRange.path;
|
|
165
|
+
if (!Number.isFinite(Number(normalized.start_line))) normalized.start_line = inlineRange.start_line;
|
|
166
|
+
if (!Number.isFinite(Number(normalized.end_line))) normalized.end_line = inlineRange.end_line;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return normalized;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function normalizePathArgs(rawArgs, aliases = []) {
|
|
173
|
+
const source =
|
|
174
|
+
rawArgs && typeof rawArgs === 'object' && !Array.isArray(rawArgs)
|
|
175
|
+
? { ...rawArgs }
|
|
176
|
+
: { path: typeof rawArgs === 'string' ? rawArgs : '' };
|
|
177
|
+
const normalized = { ...source };
|
|
178
|
+
const keys = ['path', ...aliases];
|
|
179
|
+
for (const key of keys) {
|
|
180
|
+
const value = String(source?.[key] || '').trim();
|
|
181
|
+
if (value) {
|
|
182
|
+
normalized.path = value;
|
|
183
|
+
break;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return normalized;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function normalizePatternArgs(rawArgs, aliases = [], defaultPathAliases = []) {
|
|
190
|
+
const source =
|
|
191
|
+
rawArgs && typeof rawArgs === 'object' && !Array.isArray(rawArgs)
|
|
192
|
+
? { ...rawArgs }
|
|
193
|
+
: { pattern: typeof rawArgs === 'string' ? rawArgs : '' };
|
|
194
|
+
const normalized = { ...source };
|
|
195
|
+
for (const key of ['pattern', ...aliases]) {
|
|
196
|
+
const value = String(source?.[key] || '').trim();
|
|
197
|
+
if (value) {
|
|
198
|
+
normalized.pattern = value;
|
|
199
|
+
break;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
for (const key of ['path', ...defaultPathAliases]) {
|
|
203
|
+
const value = String(source?.[key] || '').trim();
|
|
204
|
+
if (value) {
|
|
205
|
+
normalized.path = value;
|
|
206
|
+
break;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return normalized;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function normalizeWriteArgs(rawArgs) {
|
|
213
|
+
const source =
|
|
214
|
+
rawArgs && typeof rawArgs === 'object' && !Array.isArray(rawArgs)
|
|
215
|
+
? { ...rawArgs }
|
|
216
|
+
: { path: typeof rawArgs === 'string' ? rawArgs : '' };
|
|
217
|
+
const normalized = { ...source };
|
|
218
|
+
const filePath = String(source.path || source.file_path || source.file || '').trim();
|
|
219
|
+
if (filePath) normalized.path = filePath;
|
|
220
|
+
if (normalized.content == null) {
|
|
221
|
+
if (source.text != null) normalized.content = source.text;
|
|
222
|
+
if (source.new_content != null) normalized.content = source.new_content;
|
|
223
|
+
}
|
|
224
|
+
return normalized;
|
|
225
|
+
}
|
|
226
|
+
|
|
122
227
|
function findUniqueLineBlock(lines, blockContent) {
|
|
123
228
|
const probeLines = splitLines(blockContent);
|
|
124
229
|
if (probeLines.length === 0 || (probeLines.length === 1 && probeLines[0] === '')) return null;
|
|
@@ -665,16 +770,17 @@ async function getFileState(root, relativePath) {
|
|
|
665
770
|
}
|
|
666
771
|
|
|
667
772
|
async function readFile(root, args) {
|
|
668
|
-
const
|
|
773
|
+
const normalizedArgs = normalizeReadArgs(args);
|
|
774
|
+
const target = resolveInWorkspace(root, normalizedArgs?.path);
|
|
669
775
|
const stat = await fs.stat(target);
|
|
670
776
|
const text = await fs.readFile(target, 'utf8');
|
|
671
777
|
const lines = splitLines(text);
|
|
672
778
|
const totalLines = lines.length;
|
|
673
|
-
const startLineRaw = Number(
|
|
674
|
-
const endLineRaw = Number(
|
|
675
|
-
const defaultLines = Number(
|
|
676
|
-
const maxChars = Number(
|
|
677
|
-
const
|
|
779
|
+
const startLineRaw = Number(normalizedArgs?.start_line);
|
|
780
|
+
const endLineRaw = Number(normalizedArgs?.end_line);
|
|
781
|
+
const defaultLines = Number(normalizedArgs?.default_lines || 220);
|
|
782
|
+
const maxChars = Number(normalizedArgs?.max_chars || 24000);
|
|
783
|
+
const wantsMetadataOnly = normalizedArgs?.metadata_only === true || normalizedArgs?.include_content === false;
|
|
678
784
|
|
|
679
785
|
let startLine = Number.isFinite(startLineRaw) && startLineRaw > 0 ? startLineRaw : 1;
|
|
680
786
|
let endLine =
|
|
@@ -684,12 +790,12 @@ async function readFile(root, args) {
|
|
|
684
790
|
startLine = Math.max(1, Math.min(startLine, totalLines));
|
|
685
791
|
endLine = Math.max(startLine, Math.min(endLine, totalLines));
|
|
686
792
|
|
|
687
|
-
const tokenSeed = `${
|
|
793
|
+
const tokenSeed = `${normalizedArgs?.path}|${stat.size}|${stat.mtimeMs}|${startLine}|${endLine}`;
|
|
688
794
|
const readToken = sha1(tokenSeed).slice(0, 16);
|
|
689
795
|
|
|
690
|
-
if (
|
|
796
|
+
if (wantsMetadataOnly) {
|
|
691
797
|
return {
|
|
692
|
-
path:
|
|
798
|
+
path: normalizedArgs?.path,
|
|
693
799
|
phase: 'metadata',
|
|
694
800
|
size_bytes: stat.size,
|
|
695
801
|
modified_at: new Date(stat.mtimeMs).toISOString(),
|
|
@@ -701,21 +807,6 @@ async function readFile(root, args) {
|
|
|
701
807
|
};
|
|
702
808
|
}
|
|
703
809
|
|
|
704
|
-
if (String(args?.read_token || '') !== readToken) {
|
|
705
|
-
return {
|
|
706
|
-
path: args?.path,
|
|
707
|
-
phase: 'metadata',
|
|
708
|
-
error: 'read_token mismatch or missing',
|
|
709
|
-
size_bytes: stat.size,
|
|
710
|
-
modified_at: new Date(stat.mtimeMs).toISOString(),
|
|
711
|
-
total_lines: totalLines,
|
|
712
|
-
suggested_start_line: startLine,
|
|
713
|
-
suggested_end_line: endLine,
|
|
714
|
-
read_token: readToken,
|
|
715
|
-
next: 'Retry with include_content=true and read_token from latest metadata'
|
|
716
|
-
};
|
|
717
|
-
}
|
|
718
|
-
|
|
719
810
|
let content = lines.slice(startLine - 1, endLine).join('\n');
|
|
720
811
|
let truncated = false;
|
|
721
812
|
if (maxChars > 0 && content.length > maxChars) {
|
|
@@ -725,14 +816,14 @@ async function readFile(root, args) {
|
|
|
725
816
|
|
|
726
817
|
// Read deduplication: if same path+range+mtime was read before, return a short stub
|
|
727
818
|
const isDuplicate = checkReadDedup(
|
|
728
|
-
|
|
819
|
+
normalizedArgs?.path,
|
|
729
820
|
startLine,
|
|
730
821
|
endLine,
|
|
731
822
|
stat.mtimeMs
|
|
732
823
|
);
|
|
733
824
|
if (isDuplicate) {
|
|
734
825
|
return {
|
|
735
|
-
path:
|
|
826
|
+
path: normalizedArgs?.path,
|
|
736
827
|
phase: 'content',
|
|
737
828
|
start_line: startLine,
|
|
738
829
|
end_line: endLine,
|
|
@@ -744,7 +835,7 @@ async function readFile(root, args) {
|
|
|
744
835
|
}
|
|
745
836
|
|
|
746
837
|
return {
|
|
747
|
-
path:
|
|
838
|
+
path: normalizedArgs?.path,
|
|
748
839
|
phase: 'content',
|
|
749
840
|
start_line: startLine,
|
|
750
841
|
end_line: endLine,
|
|
@@ -755,7 +846,8 @@ async function readFile(root, args) {
|
|
|
755
846
|
}
|
|
756
847
|
|
|
757
848
|
async function writeFile(root, args) {
|
|
758
|
-
const
|
|
849
|
+
const normalizedArgs = normalizeWriteArgs(args);
|
|
850
|
+
const rawPath = String(normalizedArgs?.path || '').trim();
|
|
759
851
|
if (!rawPath) {
|
|
760
852
|
throw new Error('write requires a file path like weather/WeatherForecast.js');
|
|
761
853
|
}
|
|
@@ -778,18 +870,18 @@ async function writeFile(root, args) {
|
|
|
778
870
|
} catch {
|
|
779
871
|
existed = false;
|
|
780
872
|
}
|
|
781
|
-
if (existed && !
|
|
873
|
+
if (existed && !normalizedArgs?.append && !normalizedArgs?.full_file_rewrite && isCodeLikePath(rawPath)) {
|
|
782
874
|
throw new Error(
|
|
783
875
|
'write blocks full overwrite for existing code files by default. Use grep/read -> edit for minimal edits, or pass full_file_rewrite=true when a whole-file rewrite is truly intended.'
|
|
784
876
|
);
|
|
785
877
|
}
|
|
786
878
|
await fs.mkdir(path.dirname(target), { recursive: true });
|
|
787
|
-
if (
|
|
788
|
-
await fs.appendFile(target,
|
|
879
|
+
if (normalizedArgs?.append) {
|
|
880
|
+
await fs.appendFile(target, normalizedArgs?.content || '', 'utf8');
|
|
789
881
|
} else {
|
|
790
|
-
await fs.writeFile(target,
|
|
882
|
+
await fs.writeFile(target, normalizedArgs?.content || '', 'utf8');
|
|
791
883
|
}
|
|
792
|
-
const after =
|
|
884
|
+
const after = normalizedArgs?.append ? `${before}${normalizedArgs?.content || ''}` : normalizedArgs?.content || '';
|
|
793
885
|
const beforeLines = splitLines(before);
|
|
794
886
|
const afterLines = splitLines(after);
|
|
795
887
|
let changeLine = 0;
|
|
@@ -805,7 +897,7 @@ async function writeFile(root, args) {
|
|
|
805
897
|
return {
|
|
806
898
|
ok: true,
|
|
807
899
|
path: rawPath,
|
|
808
|
-
action:
|
|
900
|
+
action: normalizedArgs?.append ? 'append' : existed ? 'overwrite' : 'create',
|
|
809
901
|
changed_line: changeLine || Math.max(1, afterLines.length),
|
|
810
902
|
diff_preview: previewLines.map((line, idx) => `${previewStart + idx + 1}| ${line}`).join('\n')
|
|
811
903
|
};
|
|
@@ -1208,12 +1300,13 @@ async function searchCode(root, args) {
|
|
|
1208
1300
|
}
|
|
1209
1301
|
|
|
1210
1302
|
async function grep(root, args) {
|
|
1211
|
-
const
|
|
1303
|
+
const normalizedArgs = normalizePatternArgs(args, ['query', 'symbol', 'q'], ['directory', 'dir', 'cwd']);
|
|
1304
|
+
const pattern = String(normalizedArgs?.pattern || '').trim();
|
|
1212
1305
|
if (!pattern) throw new Error('grep requires pattern');
|
|
1213
|
-
const maxResults = Math.max(1, Math.min(200, Number(
|
|
1214
|
-
const caseSensitive = Boolean(
|
|
1215
|
-
const files = await walkTextFiles(root,
|
|
1216
|
-
const regex =
|
|
1306
|
+
const maxResults = Math.max(1, Math.min(200, Number(normalizedArgs?.max_results || 50)));
|
|
1307
|
+
const caseSensitive = Boolean(normalizedArgs?.case_sensitive);
|
|
1308
|
+
const files = await walkTextFiles(root, normalizedArgs?.path || '.', normalizeFileTypes(normalizedArgs));
|
|
1309
|
+
const regex = normalizedArgs?.regex
|
|
1217
1310
|
? new RegExp(pattern, caseSensitive ? 'g' : 'gi')
|
|
1218
1311
|
: new RegExp(escapeRegex(pattern), caseSensitive ? 'g' : 'gi');
|
|
1219
1312
|
const matches = [];
|
|
@@ -1242,12 +1335,13 @@ async function grep(root, args) {
|
|
|
1242
1335
|
}
|
|
1243
1336
|
|
|
1244
1337
|
async function glob(root, args) {
|
|
1245
|
-
const
|
|
1338
|
+
const normalizedArgs = normalizePatternArgs(args, ['glob', 'query'], ['directory', 'dir', 'cwd']);
|
|
1339
|
+
const pattern = String(normalizedArgs?.pattern || '').trim();
|
|
1246
1340
|
if (!pattern) throw new Error('glob requires pattern');
|
|
1247
|
-
const maxResults = Math.max(1, Math.min(500, Number(
|
|
1341
|
+
const maxResults = Math.max(1, Math.min(500, Number(normalizedArgs?.max_results || 200)));
|
|
1248
1342
|
const regex = globToRegex(pattern);
|
|
1249
|
-
const entries = await walkWorkspaceEntries(root,
|
|
1250
|
-
includeHidden: Boolean(
|
|
1343
|
+
const entries = await walkWorkspaceEntries(root, normalizedArgs?.path || '.', {
|
|
1344
|
+
includeHidden: Boolean(normalizedArgs?.include_hidden)
|
|
1251
1345
|
});
|
|
1252
1346
|
const matches = entries
|
|
1253
1347
|
.filter((entry) => entry.type === 'file' && regex.test(entry.path))
|
|
@@ -1261,10 +1355,11 @@ async function glob(root, args) {
|
|
|
1261
1355
|
}
|
|
1262
1356
|
|
|
1263
1357
|
async function list(root, args) {
|
|
1264
|
-
const
|
|
1358
|
+
const normalizedArgs = normalizePathArgs(args, ['dir', 'directory', 'target']);
|
|
1359
|
+
const relativePath = String(normalizedArgs?.path || '.').trim() || '.';
|
|
1265
1360
|
const target = resolveInWorkspace(root, relativePath);
|
|
1266
1361
|
const entries = await fs.readdir(target, { withFileTypes: true });
|
|
1267
|
-
const includeHidden = Boolean(
|
|
1362
|
+
const includeHidden = Boolean(normalizedArgs?.include_hidden);
|
|
1268
1363
|
const items = entries
|
|
1269
1364
|
.filter((entry) => includeHidden || !entry.name.startsWith('.'))
|
|
1270
1365
|
.map((entry) => ({
|
|
@@ -1552,6 +1647,8 @@ function normalizeEditTargetArgs(args = {}) {
|
|
|
1552
1647
|
edit: normalizedEdit
|
|
1553
1648
|
};
|
|
1554
1649
|
}
|
|
1650
|
+
const topLevelOldText = args?.old_text ?? args?.old_string;
|
|
1651
|
+
const topLevelContent = args?.content;
|
|
1555
1652
|
return {
|
|
1556
1653
|
file,
|
|
1557
1654
|
ast_target: args?.ast_target,
|
|
@@ -1560,7 +1657,7 @@ function normalizeEditTargetArgs(args = {}) {
|
|
|
1560
1657
|
target: args?.target,
|
|
1561
1658
|
new_content: args?.new_content ?? args?.content,
|
|
1562
1659
|
old_text: args?.old_text,
|
|
1563
|
-
new_text: args?.new_text,
|
|
1660
|
+
new_text: args?.new_text ?? (topLevelOldText != null && topLevelContent != null ? topLevelContent : undefined),
|
|
1564
1661
|
old_string: args?.old_string,
|
|
1565
1662
|
new_string: args?.new_string,
|
|
1566
1663
|
anchor_text: args?.anchor_text,
|
|
@@ -1754,18 +1851,22 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
|
|
|
1754
1851
|
function: {
|
|
1755
1852
|
name: 'read',
|
|
1756
1853
|
description:
|
|
1757
|
-
'Inspect a file
|
|
1854
|
+
'Inspect a file and return content directly by default. Demo-style aliases like file_path, offset, and limit are accepted. Use metadata_only=true only when you want file metadata without content. Do not use run with cat, head, or tail for file reads.',
|
|
1758
1855
|
parameters: {
|
|
1759
1856
|
type: 'object',
|
|
1760
1857
|
properties: {
|
|
1761
|
-
path: { type: 'string', description: 'File path to read' },
|
|
1858
|
+
path: { type: 'string', description: 'File path to read. You can also include an inline range like src/app.ts:10-40.' },
|
|
1859
|
+
file_path: { type: 'string', description: 'Alias for path' },
|
|
1762
1860
|
start_line: { type: 'number', description: '1-based start line' },
|
|
1763
1861
|
end_line: { type: 'number', description: 'Inclusive end line' },
|
|
1862
|
+
offset: { type: 'number', description: 'Alias for start_line' },
|
|
1863
|
+
limit: { type: 'number', description: 'Number of lines to read starting from offset/start_line' },
|
|
1764
1864
|
max_chars: { type: 'number', description: 'Max chars to return' },
|
|
1765
|
-
include_content: { type: 'boolean', description: '
|
|
1766
|
-
read_token: { type: 'string', description: '
|
|
1865
|
+
include_content: { type: 'boolean', description: 'Legacy compatibility flag. Content is returned by default.' },
|
|
1866
|
+
read_token: { type: 'string', description: 'Legacy compatibility token. No longer required for content reads.' },
|
|
1867
|
+
metadata_only: { type: 'boolean', description: 'Set true to return metadata without content.' }
|
|
1767
1868
|
},
|
|
1768
|
-
required: [
|
|
1869
|
+
required: []
|
|
1769
1870
|
}
|
|
1770
1871
|
}
|
|
1771
1872
|
},
|
|
@@ -1774,13 +1875,14 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
|
|
|
1774
1875
|
function: {
|
|
1775
1876
|
name: 'grep',
|
|
1776
1877
|
description:
|
|
1777
|
-
'Search file contents. Use this for code search before read or edit. Do not use run with grep or rg for normal code search.',
|
|
1878
|
+
'Search file contents. Use this for code search before read or edit. Aliases like query and directory are accepted. Do not use run with grep or rg for normal code search.',
|
|
1778
1879
|
parameters: {
|
|
1779
1880
|
type: 'object',
|
|
1780
1881
|
properties: {
|
|
1781
1882
|
pattern: { type: 'string', description: 'Search pattern' },
|
|
1782
1883
|
query: { type: 'string', description: 'Alias for pattern' },
|
|
1783
1884
|
path: { type: 'string', description: 'Directory or file to search' },
|
|
1885
|
+
directory: { type: 'string', description: 'Alias for path' },
|
|
1784
1886
|
regex: { type: 'boolean', description: 'Treat pattern as regex' },
|
|
1785
1887
|
case_sensitive: { type: 'boolean', description: 'Case-sensitive matching' },
|
|
1786
1888
|
max_results: { type: 'number', description: 'Max matches to return' },
|
|
@@ -1796,12 +1898,14 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
|
|
|
1796
1898
|
function: {
|
|
1797
1899
|
name: 'glob',
|
|
1798
1900
|
description:
|
|
1799
|
-
'Find files by glob pattern. Use this for file discovery before read. Do not use run with find for normal file lookup.',
|
|
1901
|
+
'Find files by glob pattern. Use this for file discovery before read. Aliases like query and directory are accepted. Do not use run with find for normal file lookup.',
|
|
1800
1902
|
parameters: {
|
|
1801
1903
|
type: 'object',
|
|
1802
1904
|
properties: {
|
|
1803
1905
|
pattern: { type: 'string', description: 'Glob pattern' },
|
|
1804
1906
|
path: { type: 'string', description: 'Directory to search' },
|
|
1907
|
+
query: { type: 'string', description: 'Alias for pattern' },
|
|
1908
|
+
directory: { type: 'string', description: 'Alias for path' },
|
|
1805
1909
|
include_hidden: { type: 'boolean', description: 'Include dotfiles' },
|
|
1806
1910
|
max_results: { type: 'number', description: 'Max results' }
|
|
1807
1911
|
},
|
|
@@ -1813,11 +1917,12 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
|
|
|
1813
1917
|
type: 'function',
|
|
1814
1918
|
function: {
|
|
1815
1919
|
name: 'list',
|
|
1816
|
-
description: 'List files and directories in a workspace path. Use this for quick directory discovery before deeper reads.',
|
|
1920
|
+
description: 'List files and directories in a workspace path. Use this for quick directory discovery before deeper reads. Aliases like directory are accepted, and plain string paths are tolerated by the runtime.',
|
|
1817
1921
|
parameters: {
|
|
1818
1922
|
type: 'object',
|
|
1819
1923
|
properties: {
|
|
1820
1924
|
path: { type: 'string', description: 'Directory path to list' },
|
|
1925
|
+
directory: { type: 'string', description: 'Alias for path' },
|
|
1821
1926
|
include_hidden: { type: 'boolean', description: 'Include dotfiles' }
|
|
1822
1927
|
}
|
|
1823
1928
|
}
|
|
@@ -1859,13 +1964,16 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
|
|
|
1859
1964
|
function: {
|
|
1860
1965
|
name: 'write',
|
|
1861
1966
|
description:
|
|
1862
|
-
'Create a new file or overwrite a file. Always include path
|
|
1967
|
+
'Create a new file or overwrite a file. Always include path and content. Aliases like file, file_path, text, and new_content are accepted. Use this for new files or explicit full rewrites only. Example: {path:"src/page.html", content:"..."} . If the file path is not decided yet, do not call write yet. Prefer edit for existing code changes.',
|
|
1863
1968
|
parameters: {
|
|
1864
1969
|
type: 'object',
|
|
1865
1970
|
properties: {
|
|
1866
1971
|
path: { type: 'string', description: 'Required file path like src/app.js or pages/index.html. Never omit this.' },
|
|
1867
1972
|
file_path: { type: 'string', description: 'Alias for path, compatible with simpler demo-style tool calls' },
|
|
1973
|
+
file: { type: 'string', description: 'Alias for path' },
|
|
1868
1974
|
content: { type: 'string', description: 'Content to write' },
|
|
1975
|
+
text: { type: 'string', description: 'Alias for content' },
|
|
1976
|
+
new_content: { type: 'string', description: 'Alias for content' },
|
|
1869
1977
|
append: { type: 'boolean', description: 'Append instead of overwrite' },
|
|
1870
1978
|
full_file_rewrite: { type: 'boolean', description: 'Set true for whole-file rewrites' }
|
|
1871
1979
|
},
|
package/src/tui/chat-app.js
CHANGED
|
@@ -88,6 +88,7 @@ const TUI_COPY = {
|
|
|
88
88
|
startupHint: '使用 /help、/commands、/compact、/exit、!<shell>。Tab 可自动补全 slash 命令。',
|
|
89
89
|
toolSummaryExpanded: '工具摘要:已展开',
|
|
90
90
|
toolSummaryCollapsed: '工具摘要:已收起',
|
|
91
|
+
toolChainCollapsed: (count) => `已折叠更早的 ${count} 个工具调用,按 Ctrl+T 展开全部`,
|
|
91
92
|
toggleToolSummary: 'Ctrl+T 切换',
|
|
92
93
|
scrollHint: '使用终端自己的滚动条或 scrollback',
|
|
93
94
|
keyboardDebugEnabled: '键盘调试已开启',
|
|
@@ -219,6 +220,7 @@ const TUI_COPY = {
|
|
|
219
220
|
startupHint: 'Use /help, /commands, /compact, /exit, !<shell>. Tab for slash autocomplete.',
|
|
220
221
|
toolSummaryExpanded: 'Tool summary: expanded',
|
|
221
222
|
toolSummaryCollapsed: 'Tool summary: collapsed',
|
|
223
|
+
toolChainCollapsed: (count) => `${count} earlier tool calls hidden, press Ctrl+T to expand`,
|
|
222
224
|
toggleToolSummary: 'Ctrl+T to toggle',
|
|
223
225
|
scrollHint: 'Scroll with your terminal scrollbar or scrollback',
|
|
224
226
|
keyboardDebugEnabled: 'Keyboard debug enabled',
|
|
@@ -353,6 +355,308 @@ function trimText(value, maxLen = 88) {
|
|
|
353
355
|
return `${text.slice(0, maxLen - 3)}...`;
|
|
354
356
|
}
|
|
355
357
|
|
|
358
|
+
export function splitMarkdownTableCells(line) {
|
|
359
|
+
const text = String(line || '').trim();
|
|
360
|
+
if (!text.includes('|')) return [];
|
|
361
|
+
return text
|
|
362
|
+
.replace(/^\|/, '')
|
|
363
|
+
.replace(/\|$/, '')
|
|
364
|
+
.split('|')
|
|
365
|
+
.map((cell) => String(cell || '').trim());
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
export function isMarkdownTableSeparator(line) {
|
|
369
|
+
const cells = splitMarkdownTableCells(line);
|
|
370
|
+
return cells.length > 0 && cells.every((cell) => /^:?-{3,}:?$/.test(cell));
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
export function isMarkdownTableHeader(line, nextLine) {
|
|
374
|
+
const cells = splitMarkdownTableCells(line);
|
|
375
|
+
return cells.length > 1 && isMarkdownTableSeparator(nextLine);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function getMarkdownTableAlignments(separatorLine, columnCount) {
|
|
379
|
+
const cells = splitMarkdownTableCells(separatorLine);
|
|
380
|
+
return Array.from({ length: columnCount }, (_, index) => {
|
|
381
|
+
const cell = String(cells[index] || '').trim();
|
|
382
|
+
if (/^:-{3,}:$/.test(cell)) return 'center';
|
|
383
|
+
if (/^-{3,}:$/.test(cell)) return 'right';
|
|
384
|
+
return 'left';
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function stringWidthLite(value) {
|
|
389
|
+
return Array.from(String(value || '')).reduce((sum, ch) => sum + charDisplayWidth(ch), 0);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function splitTableWrapUnits(text) {
|
|
393
|
+
return String(text || '')
|
|
394
|
+
.split(/([\s,.;:!?/\\|()[\]{}<>,。;:!?、()【】《》]+)/)
|
|
395
|
+
.filter(Boolean);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function wrapPlainText(text, width, hard = false) {
|
|
399
|
+
const normalized = String(text || '').replace(/\s+/g, ' ').trim();
|
|
400
|
+
if (!normalized) return [''];
|
|
401
|
+
if (width <= 1) return [normalized];
|
|
402
|
+
|
|
403
|
+
const words = splitTableWrapUnits(normalized);
|
|
404
|
+
const lines = [];
|
|
405
|
+
let current = '';
|
|
406
|
+
|
|
407
|
+
const pushWord = (word) => {
|
|
408
|
+
if (stringWidthLite(word) <= width) {
|
|
409
|
+
if (!current) {
|
|
410
|
+
current = word;
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
const needsSpacer =
|
|
414
|
+
!/\s$/.test(current) &&
|
|
415
|
+
!/^\s/.test(word) &&
|
|
416
|
+
!/^[,.;:!?/\\|)\]},。;:!?、】【》]/.test(word);
|
|
417
|
+
const next = needsSpacer ? `${current} ${word}` : `${current}${word}`;
|
|
418
|
+
if (stringWidthLite(next) <= width) {
|
|
419
|
+
current = next;
|
|
420
|
+
} else {
|
|
421
|
+
lines.push(current);
|
|
422
|
+
current = word;
|
|
423
|
+
}
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
if (!hard) {
|
|
428
|
+
if (current) {
|
|
429
|
+
lines.push(current);
|
|
430
|
+
current = '';
|
|
431
|
+
}
|
|
432
|
+
lines.push(word);
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
if (current) {
|
|
437
|
+
lines.push(current);
|
|
438
|
+
current = '';
|
|
439
|
+
}
|
|
440
|
+
let rest = word;
|
|
441
|
+
while (stringWidthLite(rest) > width) {
|
|
442
|
+
lines.push(Array.from(rest).slice(0, width).join(''));
|
|
443
|
+
rest = Array.from(rest).slice(width).join('');
|
|
444
|
+
}
|
|
445
|
+
current = rest;
|
|
446
|
+
};
|
|
447
|
+
|
|
448
|
+
for (const word of words) pushWord(word);
|
|
449
|
+
if (current) lines.push(current);
|
|
450
|
+
return lines.length > 0 ? lines : [''];
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function padAlignedText(text, width, align = 'left') {
|
|
454
|
+
const value = String(text || '');
|
|
455
|
+
const visible = stringWidthLite(value);
|
|
456
|
+
if (visible >= width) return value;
|
|
457
|
+
const gap = width - visible;
|
|
458
|
+
if (align === 'right') return `${' '.repeat(gap)}${value}`;
|
|
459
|
+
if (align === 'center') {
|
|
460
|
+
const left = Math.floor(gap / 2);
|
|
461
|
+
const right = gap - left;
|
|
462
|
+
return `${' '.repeat(left)}${value}${' '.repeat(right)}`;
|
|
463
|
+
}
|
|
464
|
+
return `${value}${' '.repeat(gap)}`;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function normalizeTableCellText(value) {
|
|
468
|
+
return String(value || '')
|
|
469
|
+
.replace(/`([^`]+)`/g, '$1')
|
|
470
|
+
.replace(/\*\*([^*]+)\*\*/g, '$1')
|
|
471
|
+
.replace(/\*([^*]+)\*/g, '$1')
|
|
472
|
+
.trim();
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
export function formatMarkdownTableBlock(lines, contentWidth = 72) {
|
|
476
|
+
const sourceLines = Array.isArray(lines) ? lines : [];
|
|
477
|
+
if (sourceLines.length < 2) return [];
|
|
478
|
+
|
|
479
|
+
const headerCells = splitMarkdownTableCells(sourceLines[0]);
|
|
480
|
+
const separatorLine = sourceLines[1];
|
|
481
|
+
const bodyRows = sourceLines.slice(2).map(splitMarkdownTableCells).filter((cells) => cells.length > 0);
|
|
482
|
+
if (headerCells.length === 0) return [];
|
|
483
|
+
|
|
484
|
+
const columnCount = Math.max(headerCells.length, ...bodyRows.map((cells) => cells.length));
|
|
485
|
+
const headers = Array.from({ length: columnCount }, (_, index) => normalizeTableCellText(headerCells[index] || ''));
|
|
486
|
+
const rows = bodyRows.map((cells) =>
|
|
487
|
+
Array.from({ length: columnCount }, (_, index) => normalizeTableCellText(cells[index] || ''))
|
|
488
|
+
);
|
|
489
|
+
const alignments = getMarkdownTableAlignments(separatorLine, columnCount);
|
|
490
|
+
|
|
491
|
+
const minColumnWidth = 3;
|
|
492
|
+
const maxRowLines = 6;
|
|
493
|
+
const safetyMargin = 4;
|
|
494
|
+
const borderOverhead = 1 + columnCount * 3;
|
|
495
|
+
const availableWidth = Math.max(contentWidth - borderOverhead - safetyMargin, columnCount * minColumnWidth);
|
|
496
|
+
|
|
497
|
+
const getMinWidth = (text) => {
|
|
498
|
+
const words = splitTableWrapUnits(String(text || '')).filter((word) => !/^\s+$/.test(word));
|
|
499
|
+
if (words.length === 0) return minColumnWidth;
|
|
500
|
+
return Math.max(...words.map((word) => stringWidthLite(word)), minColumnWidth);
|
|
501
|
+
};
|
|
502
|
+
|
|
503
|
+
const getIdealWidth = (text) => Math.max(stringWidthLite(String(text || '').trim()), minColumnWidth);
|
|
504
|
+
|
|
505
|
+
const minWidths = headers.map((header, index) =>
|
|
506
|
+
Math.max(getMinWidth(header), ...rows.map((row) => getMinWidth(row[index])))
|
|
507
|
+
);
|
|
508
|
+
const idealWidths = headers.map((header, index) =>
|
|
509
|
+
Math.max(getIdealWidth(header), ...rows.map((row) => getIdealWidth(row[index])))
|
|
510
|
+
);
|
|
511
|
+
|
|
512
|
+
const totalMin = minWidths.reduce((sum, width) => sum + width, 0);
|
|
513
|
+
const totalIdeal = idealWidths.reduce((sum, width) => sum + width, 0);
|
|
514
|
+
let needsHardWrap = false;
|
|
515
|
+
let columnWidths;
|
|
516
|
+
|
|
517
|
+
if (totalIdeal <= availableWidth) {
|
|
518
|
+
columnWidths = idealWidths.slice();
|
|
519
|
+
} else if (totalMin <= availableWidth) {
|
|
520
|
+
const extraSpace = availableWidth - totalMin;
|
|
521
|
+
const overflows = idealWidths.map((ideal, index) => ideal - minWidths[index]);
|
|
522
|
+
const totalOverflow = overflows.reduce((sum, width) => sum + width, 0);
|
|
523
|
+
columnWidths = minWidths.map((min, index) => {
|
|
524
|
+
if (totalOverflow === 0) return min;
|
|
525
|
+
return min + Math.floor((overflows[index] / totalOverflow) * extraSpace);
|
|
526
|
+
});
|
|
527
|
+
} else {
|
|
528
|
+
needsHardWrap = true;
|
|
529
|
+
const scale = availableWidth / Math.max(totalMin, 1);
|
|
530
|
+
columnWidths = minWidths.map((width) => Math.max(Math.floor(width * scale), minColumnWidth));
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
const wrapCell = (text, width) => wrapPlainText(text, width, needsHardWrap);
|
|
534
|
+
|
|
535
|
+
const computeMaxWrappedLines = () => {
|
|
536
|
+
let maxLines = 1;
|
|
537
|
+
for (let index = 0; index < headers.length; index += 1) {
|
|
538
|
+
maxLines = Math.max(maxLines, wrapCell(headers[index], columnWidths[index]).length);
|
|
539
|
+
}
|
|
540
|
+
for (const row of rows) {
|
|
541
|
+
for (let index = 0; index < columnCount; index += 1) {
|
|
542
|
+
maxLines = Math.max(maxLines, wrapCell(row[index], columnWidths[index]).length);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
return maxLines;
|
|
546
|
+
};
|
|
547
|
+
|
|
548
|
+
const renderVerticalRows = () => {
|
|
549
|
+
const rendered = [];
|
|
550
|
+
const separatorWidth = Math.min(Math.max(contentWidth - 2, 12), 40);
|
|
551
|
+
const separator = '─'.repeat(separatorWidth);
|
|
552
|
+
rows.forEach((row, rowIndex) => {
|
|
553
|
+
if (rowIndex > 0) rendered.push({ kind: 'table-vertical-separator', text: separator });
|
|
554
|
+
row.forEach((cell, cellIndex) => {
|
|
555
|
+
const label = headers[cellIndex] || `Column ${cellIndex + 1}`;
|
|
556
|
+
const firstWidth = Math.max(contentWidth - stringWidthLite(label) - 3, 10);
|
|
557
|
+
const nextWidth = Math.max(contentWidth - 3, 10);
|
|
558
|
+
const firstPass = wrapPlainText(cell, firstWidth, true);
|
|
559
|
+
const firstLine = firstPass[0] || '';
|
|
560
|
+
const remaining = firstPass.slice(1).join(' ');
|
|
561
|
+
const rest = remaining ? wrapPlainText(remaining, nextWidth, true) : [];
|
|
562
|
+
const wrapped = [firstLine, ...rest].filter((line, idx) => idx === 0 || line.trim());
|
|
563
|
+
rendered.push({
|
|
564
|
+
kind: 'table-vertical',
|
|
565
|
+
label,
|
|
566
|
+
text: wrapped[0] || ''
|
|
567
|
+
});
|
|
568
|
+
for (const line of wrapped.slice(1)) {
|
|
569
|
+
rendered.push({
|
|
570
|
+
kind: 'table-vertical-continuation',
|
|
571
|
+
text: line
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
});
|
|
575
|
+
});
|
|
576
|
+
return rendered;
|
|
577
|
+
};
|
|
578
|
+
|
|
579
|
+
if (computeMaxWrappedLines() > maxRowLines && contentWidth < 80) {
|
|
580
|
+
return renderVerticalRows();
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
const renderBorder = (type) => {
|
|
584
|
+
const chars = {
|
|
585
|
+
top: ['┌', '─', '┬', '┐'],
|
|
586
|
+
middle: ['├', '─', '┼', '┤'],
|
|
587
|
+
bottom: ['└', '─', '┴', '┘']
|
|
588
|
+
}[type];
|
|
589
|
+
let line = chars[0];
|
|
590
|
+
columnWidths.forEach((width, index) => {
|
|
591
|
+
line += chars[1].repeat(width + 2);
|
|
592
|
+
line += index < columnWidths.length - 1 ? chars[2] : chars[3];
|
|
593
|
+
});
|
|
594
|
+
return line;
|
|
595
|
+
};
|
|
596
|
+
|
|
597
|
+
const renderRowLines = (cells, isHeader = false) => {
|
|
598
|
+
const wrappedColumns = cells.map((cell, index) => wrapCell(cell, columnWidths[index]));
|
|
599
|
+
const maxLines = Math.max(...wrappedColumns.map((entry) => entry.length), 1);
|
|
600
|
+
const verticalOffsets = wrappedColumns.map((entry) => Math.floor((maxLines - entry.length) / 2));
|
|
601
|
+
const rendered = [];
|
|
602
|
+
for (let lineIndex = 0; lineIndex < maxLines; lineIndex += 1) {
|
|
603
|
+
let line = '│';
|
|
604
|
+
for (let columnIndex = 0; columnIndex < columnCount; columnIndex += 1) {
|
|
605
|
+
const wrapped = wrappedColumns[columnIndex];
|
|
606
|
+
const offset = verticalOffsets[columnIndex];
|
|
607
|
+
const contentIndex = lineIndex - offset;
|
|
608
|
+
const text = contentIndex >= 0 && contentIndex < wrapped.length ? wrapped[contentIndex] : '';
|
|
609
|
+
const align = isHeader ? 'center' : alignments[columnIndex];
|
|
610
|
+
line += ` ${padAlignedText(text, columnWidths[columnIndex], align)} │`;
|
|
611
|
+
}
|
|
612
|
+
rendered.push({
|
|
613
|
+
kind: 'table',
|
|
614
|
+
text: line,
|
|
615
|
+
isHeader
|
|
616
|
+
});
|
|
617
|
+
}
|
|
618
|
+
return rendered;
|
|
619
|
+
};
|
|
620
|
+
|
|
621
|
+
const tableLines = [
|
|
622
|
+
{ kind: 'table-separator', text: renderBorder('top') },
|
|
623
|
+
...renderRowLines(headers, true),
|
|
624
|
+
{ kind: 'table-separator', text: renderBorder('middle') }
|
|
625
|
+
];
|
|
626
|
+
|
|
627
|
+
rows.forEach((row, index) => {
|
|
628
|
+
tableLines.push(...renderRowLines(row, false));
|
|
629
|
+
if (index < rows.length - 1) {
|
|
630
|
+
tableLines.push({ kind: 'table-separator', text: renderBorder('middle') });
|
|
631
|
+
}
|
|
632
|
+
});
|
|
633
|
+
tableLines.push({ kind: 'table-separator', text: renderBorder('bottom') });
|
|
634
|
+
|
|
635
|
+
const maxLineWidth = Math.max(...tableLines.map((entry) => stringWidthLite(entry.text)));
|
|
636
|
+
if (maxLineWidth > contentWidth - safetyMargin) {
|
|
637
|
+
return renderVerticalRows();
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
return tableLines;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
function parseRichTextSegments(line, baseColor) {
|
|
644
|
+
const parts = String(line || '').split(/(`[^`]+`|\*\*[^*]+\*\*)/g);
|
|
645
|
+
return parts.map((part, idx) => {
|
|
646
|
+
if (part.startsWith('`') && part.endsWith('`') && part.length >= 2) {
|
|
647
|
+
return h(
|
|
648
|
+
Text,
|
|
649
|
+
{ key: `ic-${idx}`, color: 'black', backgroundColor: 'yellow' },
|
|
650
|
+
part.slice(1, -1)
|
|
651
|
+
);
|
|
652
|
+
}
|
|
653
|
+
if (part.startsWith('**') && part.endsWith('**') && part.length >= 4) {
|
|
654
|
+
return h(Text, { key: `bd-${idx}`, color: 'cyanBright', bold: true }, part.slice(2, -2));
|
|
655
|
+
}
|
|
656
|
+
return h(Text, { key: `tx-${idx}`, color: baseColor }, part);
|
|
657
|
+
});
|
|
658
|
+
}
|
|
659
|
+
|
|
356
660
|
function safeJsonParse(raw) {
|
|
357
661
|
try {
|
|
358
662
|
return JSON.parse(String(raw || '{}'));
|
|
@@ -880,20 +1184,31 @@ function Header({ sessionId, model, shellName, safeMode = true }) {
|
|
|
880
1184
|
}
|
|
881
1185
|
|
|
882
1186
|
function renderInlineCode(line, baseColor) {
|
|
883
|
-
|
|
884
|
-
return parts.map((part, idx) => {
|
|
885
|
-
if (part.startsWith('`') && part.endsWith('`') && part.length >= 2) {
|
|
886
|
-
return h(
|
|
887
|
-
Text,
|
|
888
|
-
{ key: `ic-${idx}`, color: 'black', backgroundColor: 'yellow' },
|
|
889
|
-
part.slice(1, -1)
|
|
890
|
-
);
|
|
891
|
-
}
|
|
892
|
-
return h(Text, { key: `tx-${idx}`, color: baseColor }, part);
|
|
893
|
-
});
|
|
1187
|
+
return parseRichTextSegments(line, baseColor);
|
|
894
1188
|
}
|
|
895
1189
|
|
|
896
1190
|
function renderTextLine(msg, line, idx, color) {
|
|
1191
|
+
const headingMatch = String(line || '').match(/^\s{0,3}(#{1,3})\s+(.*)$/);
|
|
1192
|
+
if (headingMatch) {
|
|
1193
|
+
const level = headingMatch[1].length;
|
|
1194
|
+
const title = headingMatch[2].trim();
|
|
1195
|
+
const accent = level === 1 ? 'cyanBright' : level === 2 ? 'greenBright' : 'yellowBright';
|
|
1196
|
+
return h(
|
|
1197
|
+
Box,
|
|
1198
|
+
{ key: `ln-wrap-${msg.id}-${idx}` },
|
|
1199
|
+
h(Text, { color: accent, bold: true }, title)
|
|
1200
|
+
);
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
const boldTitleMatch = String(line || '').match(/^\s*\*\*(.+)\*\*\s*$/);
|
|
1204
|
+
if (boldTitleMatch) {
|
|
1205
|
+
return h(
|
|
1206
|
+
Box,
|
|
1207
|
+
{ key: `ln-wrap-${msg.id}-${idx}` },
|
|
1208
|
+
h(Text, { key: `ln-${msg.id}-${idx}`, color: 'cyanBright', bold: true }, boldTitleMatch[1].trim())
|
|
1209
|
+
);
|
|
1210
|
+
}
|
|
1211
|
+
|
|
897
1212
|
return h(
|
|
898
1213
|
Box,
|
|
899
1214
|
{ key: `ln-wrap-${msg.id}-${idx}` },
|
|
@@ -1544,12 +1859,70 @@ export function mergeActivitySummary(previousSummary, nextSummary, activityName)
|
|
|
1544
1859
|
return lines.join('\n');
|
|
1545
1860
|
}
|
|
1546
1861
|
|
|
1862
|
+
export function collapseActivityChainRows(inputRows, showToolDetails, copy, maxVisibleActivities = 3) {
|
|
1863
|
+
const rows = Array.isArray(inputRows) ? inputRows : [];
|
|
1864
|
+
if (showToolDetails) return rows.slice();
|
|
1865
|
+
const maxVisible = Math.max(1, Number(maxVisibleActivities) || 3);
|
|
1866
|
+
const collapsed = [];
|
|
1867
|
+
|
|
1868
|
+
const isCollapsibleActivity = (row) =>
|
|
1869
|
+
row?.kind === 'activity' &&
|
|
1870
|
+
['tool', 'skill', 'system_tool'].includes(String(row?.activityType || 'tool'));
|
|
1871
|
+
|
|
1872
|
+
let index = 0;
|
|
1873
|
+
while (index < rows.length) {
|
|
1874
|
+
const row = rows[index];
|
|
1875
|
+
if (!isCollapsibleActivity(row)) {
|
|
1876
|
+
collapsed.push(row);
|
|
1877
|
+
index += 1;
|
|
1878
|
+
continue;
|
|
1879
|
+
}
|
|
1880
|
+
|
|
1881
|
+
const group = [];
|
|
1882
|
+
while (index < rows.length) {
|
|
1883
|
+
const next = rows[index];
|
|
1884
|
+
if (isCollapsibleActivity(next)) {
|
|
1885
|
+
group.push([next]);
|
|
1886
|
+
index += 1;
|
|
1887
|
+
continue;
|
|
1888
|
+
}
|
|
1889
|
+
if (next?.kind === 'activity-summary' && group.length > 0) {
|
|
1890
|
+
group[group.length - 1].push(next);
|
|
1891
|
+
index += 1;
|
|
1892
|
+
continue;
|
|
1893
|
+
}
|
|
1894
|
+
break;
|
|
1895
|
+
}
|
|
1896
|
+
|
|
1897
|
+
if (group.length <= maxVisible) {
|
|
1898
|
+
for (const item of group) collapsed.push(...item);
|
|
1899
|
+
continue;
|
|
1900
|
+
}
|
|
1901
|
+
|
|
1902
|
+
const hiddenCount = group.length - maxVisible;
|
|
1903
|
+
collapsed.push({
|
|
1904
|
+
kind: 'activity-collapsed',
|
|
1905
|
+
hiddenCount,
|
|
1906
|
+
text:
|
|
1907
|
+
copy?.generic?.toolChainCollapsed != null
|
|
1908
|
+
? copy.generic.toolChainCollapsed(hiddenCount)
|
|
1909
|
+
: `${hiddenCount} earlier tool calls hidden`
|
|
1910
|
+
});
|
|
1911
|
+
for (const item of group.slice(-maxVisible)) {
|
|
1912
|
+
collapsed.push(...item);
|
|
1913
|
+
}
|
|
1914
|
+
}
|
|
1915
|
+
|
|
1916
|
+
return collapsed;
|
|
1917
|
+
}
|
|
1918
|
+
|
|
1547
1919
|
function buildMessageRows(msg, showToolDetails, contentWidth = 72, copy) {
|
|
1548
1920
|
const rows = [];
|
|
1549
1921
|
const pushTextRows = (text) => {
|
|
1550
1922
|
const lines = String(text || '').split('\n');
|
|
1551
1923
|
let codeFence = false;
|
|
1552
|
-
for (
|
|
1924
|
+
for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
|
|
1925
|
+
const line = lines[lineIndex];
|
|
1553
1926
|
const trimmed = line.trim();
|
|
1554
1927
|
const planProgress = parsePlanProgressLine(trimmed);
|
|
1555
1928
|
if (planProgress) {
|
|
@@ -1570,6 +1943,16 @@ function buildMessageRows(msg, showToolDetails, contentWidth = 72, copy) {
|
|
|
1570
1943
|
pushWrappedRow(rows, { kind: 'code', text: line || ' ', color: 'gray' }, contentWidth);
|
|
1571
1944
|
continue;
|
|
1572
1945
|
}
|
|
1946
|
+
if (isMarkdownTableHeader(line, lines[lineIndex + 1])) {
|
|
1947
|
+
const tableLines = [line];
|
|
1948
|
+
lineIndex += 1; // skip separator
|
|
1949
|
+
while (lineIndex + 1 < lines.length && splitMarkdownTableCells(lines[lineIndex + 1]).length > 1) {
|
|
1950
|
+
tableLines.push(lines[lineIndex + 1]);
|
|
1951
|
+
lineIndex += 1;
|
|
1952
|
+
}
|
|
1953
|
+
rows.push(...formatMarkdownTableBlock(tableLines, contentWidth));
|
|
1954
|
+
continue;
|
|
1955
|
+
}
|
|
1573
1956
|
let color = msg.color || roleStyle(msg.label).text || 'white';
|
|
1574
1957
|
if (line.startsWith('#')) color = 'cyanBright';
|
|
1575
1958
|
else if (trimmed.startsWith('- ') || trimmed.startsWith('* ')) color = 'magentaBright';
|
|
@@ -1651,7 +2034,9 @@ function buildMessageRows(msg, showToolDetails, contentWidth = 72, copy) {
|
|
|
1651
2034
|
syntheticRows.push(...statusRows);
|
|
1652
2035
|
}
|
|
1653
2036
|
|
|
1654
|
-
return normalizeActivitySpacingRows(
|
|
2037
|
+
return normalizeActivitySpacingRows(
|
|
2038
|
+
insertRowsAfterLastCodeRow(collapseActivityChainRows(rows, showToolDetails, copy), syntheticRows)
|
|
2039
|
+
);
|
|
1655
2040
|
}
|
|
1656
2041
|
|
|
1657
2042
|
function renderMessageRow(msg, row, idx, loaderTick) {
|
|
@@ -1695,6 +2080,49 @@ function renderMessageRow(msg, row, idx, loaderTick) {
|
|
|
1695
2080
|
h(Text, { color: 'gray' }, `└ ${row.text}`)
|
|
1696
2081
|
);
|
|
1697
2082
|
}
|
|
2083
|
+
if (row.kind === 'table') {
|
|
2084
|
+
return h(
|
|
2085
|
+
Box,
|
|
2086
|
+
{ key: `row-table-${msg.id}-${idx}`, marginLeft: 1 },
|
|
2087
|
+
h(Text, { color: row.isHeader ? 'cyanBright' : 'gray', bold: Boolean(row.isHeader) }, row.text)
|
|
2088
|
+
);
|
|
2089
|
+
}
|
|
2090
|
+
if (row.kind === 'table-separator') {
|
|
2091
|
+
return h(
|
|
2092
|
+
Box,
|
|
2093
|
+
{ key: `row-table-sep-${msg.id}-${idx}`, marginLeft: 1 },
|
|
2094
|
+
h(Text, { color: 'gray' }, row.text)
|
|
2095
|
+
);
|
|
2096
|
+
}
|
|
2097
|
+
if (row.kind === 'table-vertical') {
|
|
2098
|
+
return h(
|
|
2099
|
+
Box,
|
|
2100
|
+
{ key: `row-table-v-${msg.id}-${idx}`, marginLeft: 1 },
|
|
2101
|
+
h(Text, { color: 'cyanBright', bold: true }, `${row.label}:`),
|
|
2102
|
+
h(Text, { color: 'gray' }, row.text ? ` ${row.text}` : '')
|
|
2103
|
+
);
|
|
2104
|
+
}
|
|
2105
|
+
if (row.kind === 'table-vertical-continuation') {
|
|
2106
|
+
return h(
|
|
2107
|
+
Box,
|
|
2108
|
+
{ key: `row-table-vc-${msg.id}-${idx}`, marginLeft: 3 },
|
|
2109
|
+
h(Text, { color: 'gray' }, row.text)
|
|
2110
|
+
);
|
|
2111
|
+
}
|
|
2112
|
+
if (row.kind === 'table-vertical-separator') {
|
|
2113
|
+
return h(
|
|
2114
|
+
Box,
|
|
2115
|
+
{ key: `row-table-vs-${msg.id}-${idx}`, marginLeft: 1 },
|
|
2116
|
+
h(Text, { color: 'gray' }, row.text)
|
|
2117
|
+
);
|
|
2118
|
+
}
|
|
2119
|
+
if (row.kind === 'activity-collapsed') {
|
|
2120
|
+
return h(
|
|
2121
|
+
Box,
|
|
2122
|
+
{ key: `row-tool-collapsed-${msg.id}-${idx}`, marginLeft: 1 },
|
|
2123
|
+
h(Text, { color: 'gray' }, `└ ${row.text}`)
|
|
2124
|
+
);
|
|
2125
|
+
}
|
|
1698
2126
|
if (row.kind === 'plan-progress') {
|
|
1699
2127
|
return h(
|
|
1700
2128
|
Box,
|