zerg-ztc 0.1.10 → 0.1.11
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/dist/App.d.ts.map +1 -1
- package/dist/App.js +63 -2
- package/dist/App.js.map +1 -1
- package/dist/agent/commands/dictation.d.ts +3 -0
- package/dist/agent/commands/dictation.d.ts.map +1 -0
- package/dist/agent/commands/dictation.js +10 -0
- package/dist/agent/commands/dictation.js.map +1 -0
- package/dist/agent/commands/index.d.ts.map +1 -1
- package/dist/agent/commands/index.js +2 -1
- package/dist/agent/commands/index.js.map +1 -1
- package/dist/agent/commands/types.d.ts +7 -0
- package/dist/agent/commands/types.d.ts.map +1 -1
- package/dist/components/InputArea.d.ts +1 -0
- package/dist/components/InputArea.d.ts.map +1 -1
- package/dist/components/InputArea.js +591 -43
- package/dist/components/InputArea.js.map +1 -1
- package/dist/components/SingleMessage.d.ts.map +1 -1
- package/dist/components/SingleMessage.js +157 -7
- package/dist/components/SingleMessage.js.map +1 -1
- package/dist/config/types.d.ts +6 -0
- package/dist/config/types.d.ts.map +1 -1
- package/dist/ui/views/status_bar.js +2 -2
- package/dist/ui/views/status_bar.js.map +1 -1
- package/dist/utils/dictation.d.ts +46 -0
- package/dist/utils/dictation.d.ts.map +1 -0
- package/dist/utils/dictation.js +409 -0
- package/dist/utils/dictation.js.map +1 -0
- package/dist/utils/dictation_native.d.ts +51 -0
- package/dist/utils/dictation_native.d.ts.map +1 -0
- package/dist/utils/dictation_native.js +216 -0
- package/dist/utils/dictation_native.js.map +1 -0
- package/dist/utils/path_format.d.ts +20 -0
- package/dist/utils/path_format.d.ts.map +1 -0
- package/dist/utils/path_format.js +90 -0
- package/dist/utils/path_format.js.map +1 -0
- package/dist/utils/table.d.ts +38 -0
- package/dist/utils/table.d.ts.map +1 -0
- package/dist/utils/table.js +133 -0
- package/dist/utils/table.js.map +1 -0
- package/dist/utils/tool_trace.d.ts +7 -2
- package/dist/utils/tool_trace.d.ts.map +1 -1
- package/dist/utils/tool_trace.js +156 -51
- package/dist/utils/tool_trace.js.map +1 -1
- package/package.json +4 -1
- package/packages/ztc-dictation/Cargo.toml +43 -0
- package/packages/ztc-dictation/README.md +65 -0
- package/packages/ztc-dictation/bin/.gitkeep +0 -0
- package/packages/ztc-dictation/index.d.ts +16 -0
- package/packages/ztc-dictation/index.js +74 -0
- package/packages/ztc-dictation/package.json +41 -0
- package/packages/ztc-dictation/src/main.rs +430 -0
- package/src/App.tsx +98 -1
- package/src/agent/commands/dictation.ts +11 -0
- package/src/agent/commands/index.ts +2 -0
- package/src/agent/commands/types.ts +8 -0
- package/src/components/InputArea.tsx +606 -42
- package/src/components/SingleMessage.tsx +248 -9
- package/src/config/types.ts +7 -0
- package/src/ui/views/status_bar.ts +2 -2
- package/src/utils/dictation.ts +467 -0
- package/src/utils/dictation_native.ts +258 -0
- package/src/utils/path_format.ts +99 -0
- package/src/utils/table.ts +171 -0
- package/src/utils/tool_trace.ts +184 -54
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { homedir } from 'os';
|
|
2
|
+
import { basename, dirname } from 'path';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Shorten a path for display:
|
|
6
|
+
* - Replace home directory with ~
|
|
7
|
+
* - Truncate middle of long paths
|
|
8
|
+
* - Show basename prominently
|
|
9
|
+
*/
|
|
10
|
+
export function shortenPath(path: string, maxLength = 60): string {
|
|
11
|
+
if (!path) return '';
|
|
12
|
+
|
|
13
|
+
// Replace home directory with ~
|
|
14
|
+
const home = homedir();
|
|
15
|
+
let shortened = path;
|
|
16
|
+
if (path.startsWith(home)) {
|
|
17
|
+
shortened = '~' + path.slice(home.length);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// If short enough, return as-is
|
|
21
|
+
if (shortened.length <= maxLength) {
|
|
22
|
+
return shortened;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Truncate middle, keeping start and end
|
|
26
|
+
const base = basename(shortened);
|
|
27
|
+
const dir = dirname(shortened);
|
|
28
|
+
|
|
29
|
+
// Always show at least the basename
|
|
30
|
+
if (base.length >= maxLength - 4) {
|
|
31
|
+
return '…' + base.slice(-(maxLength - 1));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Show as much of the directory as we can
|
|
35
|
+
const availableForDir = maxLength - base.length - 2; // 2 for /…
|
|
36
|
+
if (availableForDir < 10) {
|
|
37
|
+
return '…/' + base;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Split dir and keep start + end
|
|
41
|
+
const startLen = Math.floor(availableForDir * 0.4);
|
|
42
|
+
const endLen = availableForDir - startLen - 1; // 1 for …
|
|
43
|
+
|
|
44
|
+
const dirStart = dir.slice(0, startLen);
|
|
45
|
+
const dirEnd = dir.slice(-endLen);
|
|
46
|
+
|
|
47
|
+
return `${dirStart}…${dirEnd}/${base}`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Format a path for trace display - very compact
|
|
52
|
+
*/
|
|
53
|
+
export function formatTracePath(path: string): string {
|
|
54
|
+
if (!path) return '""';
|
|
55
|
+
|
|
56
|
+
const home = homedir();
|
|
57
|
+
let formatted = path;
|
|
58
|
+
|
|
59
|
+
// Replace home with ~
|
|
60
|
+
if (path.startsWith(home)) {
|
|
61
|
+
formatted = '~' + path.slice(home.length);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// For very long paths, just show basename with hint
|
|
65
|
+
if (formatted.length > 50) {
|
|
66
|
+
const base = basename(formatted);
|
|
67
|
+
const dir = dirname(formatted);
|
|
68
|
+
// Show abbreviated dir + full basename
|
|
69
|
+
if (dir.length > 30) {
|
|
70
|
+
const dirParts = dir.split('/').filter(Boolean);
|
|
71
|
+
if (dirParts.length > 3) {
|
|
72
|
+
const abbreviated = dirParts.slice(0, 2).join('/') + '/…/' + dirParts.slice(-1).join('/');
|
|
73
|
+
return abbreviated + '/' + base;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return formatted;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Format bytes in human readable form
|
|
83
|
+
*/
|
|
84
|
+
export function formatBytes(bytes: number): string {
|
|
85
|
+
if (bytes < 1024) return `${bytes} bytes`;
|
|
86
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
87
|
+
return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Format duration in human readable form
|
|
92
|
+
*/
|
|
93
|
+
export function formatDuration(ms: number): string {
|
|
94
|
+
if (ms < 1000) return `${ms}ms`;
|
|
95
|
+
if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
|
|
96
|
+
const mins = Math.floor(ms / 60000);
|
|
97
|
+
const secs = Math.round((ms % 60000) / 1000);
|
|
98
|
+
return `${mins}m${secs}s`;
|
|
99
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Box-drawing table utilities
|
|
3
|
+
* Uses Unicode box-drawing characters for clean terminal tables
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// Box-drawing characters
|
|
7
|
+
const BOX = {
|
|
8
|
+
topLeft: '┌',
|
|
9
|
+
topRight: '┐',
|
|
10
|
+
bottomLeft: '└',
|
|
11
|
+
bottomRight: '┘',
|
|
12
|
+
horizontal: '─',
|
|
13
|
+
vertical: '│',
|
|
14
|
+
leftT: '├',
|
|
15
|
+
rightT: '┤',
|
|
16
|
+
topT: '┬',
|
|
17
|
+
bottomT: '┴',
|
|
18
|
+
cross: '┼'
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
// Light box (for subtler tables)
|
|
22
|
+
const BOX_LIGHT = {
|
|
23
|
+
topLeft: '╭',
|
|
24
|
+
topRight: '╮',
|
|
25
|
+
bottomLeft: '╰',
|
|
26
|
+
bottomRight: '╯',
|
|
27
|
+
horizontal: '─',
|
|
28
|
+
vertical: '│',
|
|
29
|
+
leftT: '├',
|
|
30
|
+
rightT: '┤',
|
|
31
|
+
topT: '┬',
|
|
32
|
+
bottomT: '┴',
|
|
33
|
+
cross: '┼'
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export interface TableColumn {
|
|
37
|
+
header: string;
|
|
38
|
+
key: string;
|
|
39
|
+
width?: number;
|
|
40
|
+
align?: 'left' | 'right' | 'center';
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface TableOptions {
|
|
44
|
+
rounded?: boolean;
|
|
45
|
+
headerSeparator?: boolean;
|
|
46
|
+
compact?: boolean;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function padString(str: string, width: number, align: 'left' | 'right' | 'center' = 'left'): string {
|
|
50
|
+
const visibleLength = str.replace(/\x1b\[[0-9;]*m/g, '').length;
|
|
51
|
+
const padding = Math.max(0, width - visibleLength);
|
|
52
|
+
|
|
53
|
+
if (align === 'right') {
|
|
54
|
+
return ' '.repeat(padding) + str;
|
|
55
|
+
}
|
|
56
|
+
if (align === 'center') {
|
|
57
|
+
const left = Math.floor(padding / 2);
|
|
58
|
+
const right = padding - left;
|
|
59
|
+
return ' '.repeat(left) + str + ' '.repeat(right);
|
|
60
|
+
}
|
|
61
|
+
return str + ' '.repeat(padding);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function getColumnWidths(columns: TableColumn[], rows: Record<string, unknown>[]): number[] {
|
|
65
|
+
return columns.map(col => {
|
|
66
|
+
const headerWidth = col.header.length;
|
|
67
|
+
const dataWidth = rows.reduce((max, row) => {
|
|
68
|
+
const value = String(row[col.key] || '');
|
|
69
|
+
const visibleLength = value.replace(/\x1b\[[0-9;]*m/g, '').length;
|
|
70
|
+
return Math.max(max, visibleLength);
|
|
71
|
+
}, 0);
|
|
72
|
+
return col.width || Math.max(headerWidth, dataWidth);
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Format data as a box-drawing table
|
|
78
|
+
*/
|
|
79
|
+
export function formatTable(
|
|
80
|
+
columns: TableColumn[],
|
|
81
|
+
rows: Record<string, unknown>[],
|
|
82
|
+
options: TableOptions = {}
|
|
83
|
+
): string {
|
|
84
|
+
const { rounded = false, headerSeparator = true, compact = false } = options;
|
|
85
|
+
const box = rounded ? BOX_LIGHT : BOX;
|
|
86
|
+
const widths = getColumnWidths(columns, rows);
|
|
87
|
+
const lines: string[] = [];
|
|
88
|
+
|
|
89
|
+
// Top border
|
|
90
|
+
const topBorder = box.topLeft +
|
|
91
|
+
widths.map(w => box.horizontal.repeat(w + 2)).join(box.topT) +
|
|
92
|
+
box.topRight;
|
|
93
|
+
lines.push(topBorder);
|
|
94
|
+
|
|
95
|
+
// Header row
|
|
96
|
+
const headerRow = box.vertical +
|
|
97
|
+
columns.map((col, i) => ' ' + padString(col.header, widths[i], 'center') + ' ').join(box.vertical) +
|
|
98
|
+
box.vertical;
|
|
99
|
+
lines.push(headerRow);
|
|
100
|
+
|
|
101
|
+
// Header separator
|
|
102
|
+
if (headerSeparator) {
|
|
103
|
+
const separator = box.leftT +
|
|
104
|
+
widths.map(w => box.horizontal.repeat(w + 2)).join(box.cross) +
|
|
105
|
+
box.rightT;
|
|
106
|
+
lines.push(separator);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Data rows
|
|
110
|
+
for (const row of rows) {
|
|
111
|
+
const dataRow = box.vertical +
|
|
112
|
+
columns.map((col, i) => {
|
|
113
|
+
const value = String(row[col.key] || '');
|
|
114
|
+
return ' ' + padString(value, widths[i], col.align) + ' ';
|
|
115
|
+
}).join(box.vertical) +
|
|
116
|
+
box.vertical;
|
|
117
|
+
lines.push(dataRow);
|
|
118
|
+
|
|
119
|
+
// Row separator (if not compact)
|
|
120
|
+
if (!compact && rows.indexOf(row) < rows.length - 1) {
|
|
121
|
+
const rowSep = box.leftT +
|
|
122
|
+
widths.map(w => box.horizontal.repeat(w + 2)).join(box.cross) +
|
|
123
|
+
box.rightT;
|
|
124
|
+
lines.push(rowSep);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Bottom border
|
|
129
|
+
const bottomBorder = box.bottomLeft +
|
|
130
|
+
widths.map(w => box.horizontal.repeat(w + 2)).join(box.bottomT) +
|
|
131
|
+
box.bottomRight;
|
|
132
|
+
lines.push(bottomBorder);
|
|
133
|
+
|
|
134
|
+
return lines.join('\n');
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Simple two-column table (key-value style)
|
|
139
|
+
*/
|
|
140
|
+
export function formatKeyValueTable(
|
|
141
|
+
data: Array<{ label: string; value: string }>,
|
|
142
|
+
options: TableOptions = {}
|
|
143
|
+
): string {
|
|
144
|
+
const columns: TableColumn[] = [
|
|
145
|
+
{ header: 'Key', key: 'label', align: 'left' },
|
|
146
|
+
{ header: 'Value', key: 'value', align: 'left' }
|
|
147
|
+
];
|
|
148
|
+
const rows = data.map(d => ({ label: d.label, value: d.value }));
|
|
149
|
+
return formatTable(columns, rows, { ...options, headerSeparator: false });
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Format a schedule/calendar table
|
|
154
|
+
*/
|
|
155
|
+
export function formatScheduleTable(
|
|
156
|
+
events: Array<{ time: string; event: string }>,
|
|
157
|
+
options: TableOptions = {}
|
|
158
|
+
): string {
|
|
159
|
+
const columns: TableColumn[] = [
|
|
160
|
+
{ header: 'Time', key: 'time', align: 'left', width: 7 },
|
|
161
|
+
{ header: 'Event', key: 'event', align: 'left' }
|
|
162
|
+
];
|
|
163
|
+
return formatTable(columns, events, { rounded: true, compact: true, ...options });
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Simple horizontal line/divider
|
|
168
|
+
*/
|
|
169
|
+
export function formatDivider(width = 80, char = '─'): string {
|
|
170
|
+
return char.repeat(width);
|
|
171
|
+
}
|
package/src/utils/tool_trace.ts
CHANGED
|
@@ -1,45 +1,65 @@
|
|
|
1
|
-
import { getTraceStyle
|
|
1
|
+
import { getTraceStyle } from '../emulation/trace_style.js';
|
|
2
|
+
import { formatTracePath, formatBytes, formatDuration } from './path_format.js';
|
|
2
3
|
|
|
3
4
|
const toolLabels: Record<string, string> = {
|
|
4
5
|
run_command: 'Bash',
|
|
5
6
|
read_file: 'Read',
|
|
6
7
|
write_file: 'Update',
|
|
7
8
|
list_directory: 'List',
|
|
8
|
-
search: 'Search'
|
|
9
|
+
search: 'Search',
|
|
10
|
+
screenshot: 'Screenshot',
|
|
11
|
+
list_windows: 'ListWindows',
|
|
12
|
+
run_and_capture: 'RunCapture'
|
|
9
13
|
};
|
|
10
14
|
|
|
11
|
-
function formatValue(value: unknown): string {
|
|
12
|
-
if (typeof value === 'string') {
|
|
13
|
-
const escaped = value.replace(/\n/g, '\\n');
|
|
14
|
-
return `"${escaped}"`;
|
|
15
|
-
}
|
|
16
|
-
if (typeof value === 'number' || typeof value === 'boolean') {
|
|
17
|
-
return String(value);
|
|
18
|
-
}
|
|
19
|
-
return JSON.stringify(value);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
15
|
function formatArgs(tool: string, args: Record<string, unknown>): string {
|
|
16
|
+
// Compact arg formatting - just show the key values
|
|
23
17
|
if (tool === 'run_command') {
|
|
24
18
|
const command = args.command ? String(args.command) : '';
|
|
25
|
-
|
|
26
|
-
const
|
|
27
|
-
return `(${
|
|
19
|
+
// Truncate long commands
|
|
20
|
+
const truncated = command.length > 60 ? command.slice(0, 57) + '...' : command;
|
|
21
|
+
return `(${truncated})`;
|
|
28
22
|
}
|
|
23
|
+
|
|
29
24
|
if (tool === 'search') {
|
|
30
|
-
const pattern = args.pattern ?
|
|
31
|
-
const path = args.path ?
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
25
|
+
const pattern = args.pattern ? String(args.pattern) : '';
|
|
26
|
+
const path = args.path ? formatTracePath(String(args.path)) : '';
|
|
27
|
+
if (path) {
|
|
28
|
+
return `(${pattern}, ${path})`;
|
|
29
|
+
}
|
|
30
|
+
return `(${pattern})`;
|
|
35
31
|
}
|
|
32
|
+
|
|
36
33
|
if (tool === 'read_file' || tool === 'write_file' || tool === 'list_directory') {
|
|
37
|
-
const path = args.path ?
|
|
34
|
+
const path = args.path ? formatTracePath(String(args.path)) : '';
|
|
38
35
|
return `(${path})`;
|
|
39
36
|
}
|
|
40
37
|
|
|
41
|
-
|
|
42
|
-
|
|
38
|
+
if (tool === 'screenshot') {
|
|
39
|
+
if (args.app) return `(app: "${args.app}")`;
|
|
40
|
+
if (args.pid) return `(pid: ${args.pid})`;
|
|
41
|
+
if (args.windowId) return `(windowId: ${args.windowId})`;
|
|
42
|
+
return '';
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (tool === 'list_windows') {
|
|
46
|
+
if (args.filter) return `(filter: "${args.filter}")`;
|
|
47
|
+
return '';
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Generic: show first few args compactly
|
|
51
|
+
const entries = Object.entries(args).slice(0, 3);
|
|
52
|
+
if (entries.length === 0) return '';
|
|
53
|
+
|
|
54
|
+
const parts = entries.map(([key, value]) => {
|
|
55
|
+
if (typeof value === 'string') {
|
|
56
|
+
const v = value.length > 30 ? value.slice(0, 27) + '...' : value;
|
|
57
|
+
return `${key}: "${v}"`;
|
|
58
|
+
}
|
|
59
|
+
return `${key}: ${value}`;
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
return `(${parts.join(', ')})`;
|
|
43
63
|
}
|
|
44
64
|
|
|
45
65
|
function parseResult(raw: string): Record<string, unknown> | null {
|
|
@@ -52,37 +72,96 @@ function parseResult(raw: string): Record<string, unknown> | null {
|
|
|
52
72
|
return null;
|
|
53
73
|
}
|
|
54
74
|
|
|
75
|
+
interface ParsedDiff {
|
|
76
|
+
addedLines: number;
|
|
77
|
+
removedLines: number;
|
|
78
|
+
hunks: Array<{
|
|
79
|
+
header: string;
|
|
80
|
+
lines: Array<{ type: 'add' | 'remove' | 'context'; lineNum?: number; text: string }>;
|
|
81
|
+
}>;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function parseDiff(diff: string): ParsedDiff | null {
|
|
85
|
+
if (!diff || !diff.includes('@@')) return null;
|
|
86
|
+
|
|
87
|
+
const lines = diff.split('\n');
|
|
88
|
+
let addedLines = 0;
|
|
89
|
+
let removedLines = 0;
|
|
90
|
+
const hunks: ParsedDiff['hunks'] = [];
|
|
91
|
+
let currentHunk: ParsedDiff['hunks'][0] | null = null;
|
|
92
|
+
|
|
93
|
+
for (const line of lines) {
|
|
94
|
+
if (line.startsWith('@@')) {
|
|
95
|
+
if (currentHunk) hunks.push(currentHunk);
|
|
96
|
+
currentHunk = { header: line, lines: [] };
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (!currentHunk) continue;
|
|
101
|
+
|
|
102
|
+
if (line.startsWith('+') && !line.startsWith('+++')) {
|
|
103
|
+
addedLines++;
|
|
104
|
+
currentHunk.lines.push({ type: 'add', text: line.slice(1) });
|
|
105
|
+
} else if (line.startsWith('-') && !line.startsWith('---')) {
|
|
106
|
+
removedLines++;
|
|
107
|
+
currentHunk.lines.push({ type: 'remove', text: line.slice(1) });
|
|
108
|
+
} else if (line.startsWith(' ')) {
|
|
109
|
+
currentHunk.lines.push({ type: 'context', text: line.slice(1) });
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (currentHunk) hunks.push(currentHunk);
|
|
114
|
+
|
|
115
|
+
return { addedLines, removedLines, hunks };
|
|
116
|
+
}
|
|
117
|
+
|
|
55
118
|
function formatOutcome(tool: string, result: string, durationMs?: number): string {
|
|
56
119
|
const parsed = parseResult(result);
|
|
57
|
-
const duration = typeof durationMs === 'number' ? ` in ${(durationMs
|
|
120
|
+
const duration = typeof durationMs === 'number' ? ` in ${formatDuration(durationMs)}` : '';
|
|
58
121
|
|
|
59
122
|
if (parsed && tool === 'search') {
|
|
60
123
|
const count = typeof parsed.count === 'number' ? parsed.count : 0;
|
|
61
124
|
const label = count === 1 ? 'match' : 'matches';
|
|
62
125
|
return `Found ${count} ${label}${duration}`;
|
|
63
126
|
}
|
|
127
|
+
|
|
64
128
|
if (parsed && tool === 'run_command') {
|
|
65
129
|
const stdout = String(parsed.stdout || '');
|
|
66
130
|
const stderr = String(parsed.stderr || '');
|
|
67
131
|
const lines = (stdout || stderr).trim().split('\n').filter(Boolean).length;
|
|
68
132
|
const label = lines === 1 ? 'line' : 'lines';
|
|
69
|
-
return lines > 0 ? `Output ${lines} ${label}${duration}` : `
|
|
133
|
+
return lines > 0 ? `Output ${lines} ${label}${duration}` : `Done${duration}`;
|
|
70
134
|
}
|
|
135
|
+
|
|
71
136
|
if (parsed && tool === 'read_file') {
|
|
72
|
-
const path = String(parsed.path || '');
|
|
73
|
-
const size = parsed.size ?
|
|
137
|
+
const path = formatTracePath(String(parsed.path || ''));
|
|
138
|
+
const size = typeof parsed.size === 'number' ? formatBytes(parsed.size) : '';
|
|
74
139
|
const truncated = parsed.truncated ? ' (truncated)' : '';
|
|
75
|
-
return
|
|
140
|
+
return `${path} (${size})${duration}${truncated}`;
|
|
76
141
|
}
|
|
142
|
+
|
|
77
143
|
if (parsed && tool === 'write_file') {
|
|
78
|
-
const path = String(parsed.path || '');
|
|
79
|
-
const
|
|
80
|
-
|
|
144
|
+
const path = formatTracePath(String(parsed.path || ''));
|
|
145
|
+
const diff = parseDiff(String(parsed.diff || ''));
|
|
146
|
+
if (diff) {
|
|
147
|
+
const parts: string[] = [];
|
|
148
|
+
if (diff.addedLines > 0) parts.push(`+${diff.addedLines}`);
|
|
149
|
+
if (diff.removedLines > 0) parts.push(`-${diff.removedLines}`);
|
|
150
|
+
const summary = parts.length > 0 ? ` (${parts.join(', ')} lines)` : '';
|
|
151
|
+
return `${path}${summary}${duration}`;
|
|
152
|
+
}
|
|
153
|
+
return `${path}${duration}`;
|
|
81
154
|
}
|
|
155
|
+
|
|
82
156
|
if (parsed && tool === 'list_directory') {
|
|
83
|
-
const path = String(parsed.path || '');
|
|
157
|
+
const path = formatTracePath(String(parsed.path || ''));
|
|
84
158
|
const entries = Array.isArray(parsed.entries) ? parsed.entries.length : 0;
|
|
85
|
-
return
|
|
159
|
+
return `${path} (${entries} items)${duration}`;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (parsed && tool === 'screenshot') {
|
|
163
|
+
const desc = String(parsed.description || 'Screenshot captured');
|
|
164
|
+
return `${desc}${duration}`;
|
|
86
165
|
}
|
|
87
166
|
|
|
88
167
|
return `Done${duration}`;
|
|
@@ -107,7 +186,7 @@ function buildOutputLines(tool: string, result: string): { lines: string[]; trun
|
|
|
107
186
|
const lines = matches.map((entry: unknown) => {
|
|
108
187
|
if (!entry || typeof entry !== 'object') return '';
|
|
109
188
|
const row = entry as Record<string, unknown>;
|
|
110
|
-
const file = String(row.path || '');
|
|
189
|
+
const file = formatTracePath(String(row.path || ''));
|
|
111
190
|
const line = row.line ? `:${row.line}` : '';
|
|
112
191
|
const text = String(row.text || '');
|
|
113
192
|
return `${file}${line} ${text}`.trim();
|
|
@@ -122,9 +201,24 @@ function buildOutputLines(tool: string, result: string): { lines: string[]; trun
|
|
|
122
201
|
}
|
|
123
202
|
|
|
124
203
|
if (tool === 'write_file') {
|
|
125
|
-
|
|
126
|
-
const
|
|
127
|
-
|
|
204
|
+
// Return formatted diff lines
|
|
205
|
+
const diff = parseDiff(String(parsed.diff || ''));
|
|
206
|
+
if (diff && diff.hunks.length > 0) {
|
|
207
|
+
const lines: string[] = [];
|
|
208
|
+
for (const hunk of diff.hunks) {
|
|
209
|
+
for (const line of hunk.lines) {
|
|
210
|
+
if (line.type === 'add') {
|
|
211
|
+
lines.push(`+ ${line.text}`);
|
|
212
|
+
} else if (line.type === 'remove') {
|
|
213
|
+
lines.push(`- ${line.text}`);
|
|
214
|
+
} else {
|
|
215
|
+
lines.push(` ${line.text}`);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return { lines, truncated: Boolean(parsed.truncated) };
|
|
220
|
+
}
|
|
221
|
+
return { lines: [], truncated: false };
|
|
128
222
|
}
|
|
129
223
|
|
|
130
224
|
return { lines: [], truncated: false };
|
|
@@ -134,6 +228,7 @@ export function formatToolStart(tool: string, args: Record<string, unknown>, emu
|
|
|
134
228
|
const style = getTraceStyle(emulationId);
|
|
135
229
|
const label = toolLabels[tool] || tool;
|
|
136
230
|
const argsText = formatArgs(tool, args);
|
|
231
|
+
|
|
137
232
|
if (style === 'claude_code') {
|
|
138
233
|
return `⏺ ${label}${argsText}`;
|
|
139
234
|
}
|
|
@@ -150,8 +245,9 @@ export function formatToolStart(tool: string, args: Record<string, unknown>, emu
|
|
|
150
245
|
export function formatToolEnd(tool: string, result: string, durationMs?: number, emulationId?: string): string {
|
|
151
246
|
const style = getTraceStyle(emulationId);
|
|
152
247
|
const summary = formatOutcome(tool, result, durationMs);
|
|
248
|
+
|
|
153
249
|
if (style === 'claude_code') {
|
|
154
|
-
return `⎿
|
|
250
|
+
return `⎿ ${summary}`;
|
|
155
251
|
}
|
|
156
252
|
if (style === 'codex') {
|
|
157
253
|
if (tool === 'run_command') {
|
|
@@ -167,13 +263,28 @@ export function formatToolEnd(tool: string, result: string, durationMs?: number,
|
|
|
167
263
|
|
|
168
264
|
export function formatToolError(tool: string, error: string, emulationId?: string): string {
|
|
169
265
|
const style = getTraceStyle(emulationId);
|
|
266
|
+
const label = toolLabels[tool] || tool;
|
|
267
|
+
|
|
268
|
+
// Provide helpful error messages
|
|
269
|
+
let displayError = error;
|
|
270
|
+
if (error.includes('Screen Recording permission')) {
|
|
271
|
+
displayError = 'Screen Recording permission required (System Settings > Privacy & Security)';
|
|
272
|
+
}
|
|
273
|
+
|
|
170
274
|
if (style === 'claude_code') {
|
|
171
|
-
return `⎿
|
|
275
|
+
return `⎿ ${label} failed: ${displayError}`;
|
|
172
276
|
}
|
|
173
277
|
if (style === 'codex') {
|
|
174
|
-
return ` ${
|
|
278
|
+
return ` ${label} failed: ${displayError}`;
|
|
175
279
|
}
|
|
176
|
-
return `< ${
|
|
280
|
+
return `< ${label} failed: ${displayError}`;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
export interface ToolOutputMessage {
|
|
284
|
+
preview: string;
|
|
285
|
+
full: string;
|
|
286
|
+
truncated: boolean;
|
|
287
|
+
diffLines?: Array<{ type: 'add' | 'remove' | 'context'; text: string }>;
|
|
177
288
|
}
|
|
178
289
|
|
|
179
290
|
export function buildToolOutputMessage(
|
|
@@ -181,9 +292,25 @@ export function buildToolOutputMessage(
|
|
|
181
292
|
result: string,
|
|
182
293
|
durationMs?: number,
|
|
183
294
|
emulationId?: string
|
|
184
|
-
):
|
|
295
|
+
): ToolOutputMessage {
|
|
185
296
|
const { lines, truncated } = buildOutputLines(tool, result);
|
|
186
|
-
|
|
297
|
+
const parsed = parseResult(result);
|
|
298
|
+
|
|
299
|
+
// For write_file, extract diff info
|
|
300
|
+
let diffLines: ToolOutputMessage['diffLines'];
|
|
301
|
+
if (tool === 'write_file' && parsed) {
|
|
302
|
+
const diff = parseDiff(String(parsed.diff || ''));
|
|
303
|
+
if (diff && diff.hunks.length > 0) {
|
|
304
|
+
diffLines = [];
|
|
305
|
+
for (const hunk of diff.hunks) {
|
|
306
|
+
for (const line of hunk.lines) {
|
|
307
|
+
diffLines.push({ type: line.type, text: line.text });
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (lines.length === 0 && !diffLines) {
|
|
187
314
|
const summary = formatToolEnd(tool, result, durationMs, emulationId);
|
|
188
315
|
return { preview: summary, full: summary, truncated: false };
|
|
189
316
|
}
|
|
@@ -194,23 +321,26 @@ export function buildToolOutputMessage(
|
|
|
194
321
|
const expandHint = truncated || remaining > 0 ? ' (ctrl+o to expand)' : '';
|
|
195
322
|
|
|
196
323
|
const summary = formatToolEnd(tool, result, durationMs, emulationId);
|
|
197
|
-
const
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
324
|
+
const indent = ' ';
|
|
325
|
+
|
|
326
|
+
const previewBlock = [summary];
|
|
327
|
+
for (const line of previewLines) {
|
|
328
|
+
previewBlock.push(`${indent}${line}`);
|
|
329
|
+
}
|
|
201
330
|
if (expandHint) {
|
|
202
331
|
const extraLabel = remaining > 0 ? `… +${remaining} lines` : '…';
|
|
203
|
-
previewBlock.push(
|
|
332
|
+
previewBlock.push(`${indent}${extraLabel}${expandHint}`);
|
|
204
333
|
}
|
|
205
334
|
|
|
206
|
-
const fullBlock = [
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
335
|
+
const fullBlock = [summary];
|
|
336
|
+
for (const line of lines) {
|
|
337
|
+
fullBlock.push(`${indent}${line}`);
|
|
338
|
+
}
|
|
210
339
|
|
|
211
340
|
return {
|
|
212
341
|
preview: previewBlock.join('\n'),
|
|
213
342
|
full: fullBlock.join('\n'),
|
|
214
|
-
truncated: Boolean(expandHint)
|
|
343
|
+
truncated: Boolean(expandHint),
|
|
344
|
+
diffLines
|
|
215
345
|
};
|
|
216
346
|
}
|