zerg-ztc 0.1.4 → 0.1.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/dist/App.d.ts.map +1 -1
- package/dist/App.js +141 -16
- package/dist/App.js.map +1 -1
- package/dist/agent/agent.d.ts +4 -0
- package/dist/agent/agent.d.ts.map +1 -1
- package/dist/agent/agent.js +21 -3
- package/dist/agent/agent.js.map +1 -1
- package/dist/agent/commands/config.d.ts.map +1 -1
- package/dist/agent/commands/config.js +68 -2
- package/dist/agent/commands/config.js.map +1 -1
- 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/update.d.ts +3 -0
- package/dist/agent/commands/update.d.ts.map +1 -0
- package/dist/agent/commands/update.js +33 -0
- package/dist/agent/commands/update.js.map +1 -0
- package/dist/agent/tools/file.d.ts.map +1 -1
- package/dist/agent/tools/file.js +10 -6
- package/dist/agent/tools/file.js.map +1 -1
- package/dist/agent/tools/index.d.ts +2 -2
- package/dist/agent/tools/index.d.ts.map +1 -1
- package/dist/agent/tools/index.js +2 -2
- package/dist/agent/tools/index.js.map +1 -1
- package/dist/agent/tools/search.d.ts.map +1 -1
- package/dist/agent/tools/search.js +5 -4
- package/dist/agent/tools/search.js.map +1 -1
- package/dist/agent/tools/shell.d.ts.map +1 -1
- package/dist/agent/tools/shell.js +7 -3
- package/dist/agent/tools/shell.js.map +1 -1
- package/dist/agent/tools/types.d.ts +4 -1
- package/dist/agent/tools/types.d.ts.map +1 -1
- package/dist/cli.js +46 -31
- package/dist/cli.js.map +1 -1
- package/dist/components/ActivityLine.d.ts +11 -0
- package/dist/components/ActivityLine.d.ts.map +1 -0
- package/dist/components/ActivityLine.js +9 -0
- package/dist/components/ActivityLine.js.map +1 -0
- package/dist/components/FullScreen.d.ts +1 -0
- package/dist/components/FullScreen.d.ts.map +1 -1
- package/dist/components/FullScreen.js +2 -2
- package/dist/components/FullScreen.js.map +1 -1
- package/dist/components/MessageList.d.ts +2 -1
- package/dist/components/MessageList.d.ts.map +1 -1
- package/dist/components/MessageList.js +41 -2
- package/dist/components/MessageList.js.map +1 -1
- package/dist/components/SingleMessage.d.ts +9 -0
- package/dist/components/SingleMessage.d.ts.map +1 -0
- package/dist/components/SingleMessage.js +27 -0
- package/dist/components/SingleMessage.js.map +1 -0
- package/dist/components/StatusBar.d.ts +1 -0
- package/dist/components/StatusBar.d.ts.map +1 -1
- package/dist/components/StatusBar.js +2 -1
- package/dist/components/StatusBar.js.map +1 -1
- package/dist/components/index.d.ts +2 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +2 -0
- package/dist/components/index.js.map +1 -1
- package/dist/config/types.d.ts +1 -0
- package/dist/config/types.d.ts.map +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +8 -0
- package/dist/config.js.map +1 -1
- package/dist/ui/views/activity_line.d.ts +11 -0
- package/dist/ui/views/activity_line.d.ts.map +1 -0
- package/dist/ui/views/activity_line.js +20 -0
- package/dist/ui/views/activity_line.js.map +1 -0
- package/dist/ui/views/app.d.ts +5 -2
- package/dist/ui/views/app.d.ts.map +1 -1
- package/dist/ui/views/app.js +17 -14
- package/dist/ui/views/app.js.map +1 -1
- package/dist/ui/views/header.d.ts.map +1 -1
- package/dist/ui/views/header.js +3 -4
- package/dist/ui/views/header.js.map +1 -1
- package/dist/ui/views/input_area.d.ts.map +1 -1
- package/dist/ui/views/input_area.js +25 -12
- package/dist/ui/views/input_area.js.map +1 -1
- package/dist/ui/views/message_list.d.ts +3 -2
- package/dist/ui/views/message_list.d.ts.map +1 -1
- package/dist/ui/views/message_list.js +33 -19
- package/dist/ui/views/message_list.js.map +1 -1
- package/dist/ui/views/status_bar.d.ts +2 -1
- package/dist/ui/views/status_bar.d.ts.map +1 -1
- package/dist/ui/views/status_bar.js +4 -2
- package/dist/ui/views/status_bar.js.map +1 -1
- package/dist/utils/shell.d.ts.map +1 -1
- package/dist/utils/shell.js +9 -1
- package/dist/utils/shell.js.map +1 -1
- package/dist/utils/spinner_frames.d.ts +2 -0
- package/dist/utils/spinner_frames.d.ts.map +1 -0
- package/dist/utils/spinner_frames.js +2 -0
- package/dist/utils/spinner_frames.js.map +1 -0
- package/dist/utils/spinner_verbs.d.ts +4 -0
- package/dist/utils/spinner_verbs.d.ts.map +1 -0
- package/dist/utils/spinner_verbs.js +22 -0
- package/dist/utils/spinner_verbs.js.map +1 -0
- package/dist/utils/tool_trace.d.ts.map +1 -1
- package/dist/utils/tool_trace.js +12 -2
- package/dist/utils/tool_trace.js.map +1 -1
- package/dist/utils/update.d.ts +9 -0
- package/dist/utils/update.d.ts.map +1 -0
- package/dist/utils/update.js +37 -0
- package/dist/utils/update.js.map +1 -0
- package/dist/utils/version.d.ts +2 -0
- package/dist/utils/version.d.ts.map +1 -0
- package/dist/utils/version.js +16 -0
- package/dist/utils/version.js.map +1 -0
- package/package.json +1 -1
- package/src/App.tsx +180 -26
- package/src/agent/agent.ts +26 -6
- package/src/agent/commands/config.ts +76 -2
- package/src/agent/commands/index.ts +2 -0
- package/src/agent/commands/update.ts +32 -0
- package/src/agent/tools/file.ts +24 -19
- package/src/agent/tools/index.ts +5 -4
- package/src/agent/tools/search.ts +6 -5
- package/src/agent/tools/shell.ts +13 -9
- package/src/agent/tools/types.ts +5 -1
- package/src/cli.tsx +50 -30
- package/src/components/ActivityLine.tsx +23 -0
- package/src/components/FullScreen.tsx +4 -3
- package/src/components/MessageList.tsx +52 -6
- package/src/components/SingleMessage.tsx +59 -0
- package/src/components/StatusBar.tsx +3 -0
- package/src/components/index.tsx +3 -1
- package/src/config/types.ts +1 -0
- package/src/config.ts +8 -0
- package/src/ui/views/activity_line.ts +33 -0
- package/src/ui/views/app.ts +23 -14
- package/src/ui/views/header.ts +3 -4
- package/src/ui/views/input_area.ts +28 -17
- package/src/ui/views/message_list.ts +36 -20
- package/src/ui/views/status_bar.ts +5 -1
- package/src/utils/shell.ts +10 -1
- package/src/utils/spinner_frames.ts +1 -0
- package/src/utils/spinner_verbs.ts +23 -0
- package/src/utils/tool_trace.ts +12 -2
- package/src/utils/update.ts +44 -0
- package/src/utils/version.ts +15 -0
package/src/agent/tools/file.ts
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
import { readFile, writeFile, readdir, stat } from 'fs/promises';
|
|
2
2
|
import { dirname, resolve } from 'path';
|
|
3
|
-
import { Tool } from './types.js';
|
|
3
|
+
import { Tool, ToolContext } from './types.js';
|
|
4
4
|
import { ToolCapability } from '../runtime/capabilities.js';
|
|
5
5
|
import { buildSimpleDiff } from '../../utils/diff.js';
|
|
6
6
|
|
|
7
|
+
function resolvePath(path: string, context?: ToolContext): string {
|
|
8
|
+
const base = context?.cwd || process.cwd();
|
|
9
|
+
return resolve(base, path);
|
|
10
|
+
}
|
|
11
|
+
|
|
7
12
|
// --- File Tools ---
|
|
8
13
|
|
|
9
14
|
export const readFileTool: Tool = {
|
|
@@ -27,10 +32,10 @@ export const readFileTool: Tool = {
|
|
|
27
32
|
required: ['path']
|
|
28
33
|
}
|
|
29
34
|
},
|
|
30
|
-
execute: async (args) => {
|
|
31
|
-
const path =
|
|
35
|
+
execute: async (args, context) => {
|
|
36
|
+
const path = resolvePath(String(args.path), context);
|
|
32
37
|
const encoding = (args.encoding as BufferEncoding) || 'utf-8';
|
|
33
|
-
|
|
38
|
+
|
|
34
39
|
try {
|
|
35
40
|
const content = await readFile(path, { encoding });
|
|
36
41
|
const stats = await stat(path);
|
|
@@ -70,16 +75,16 @@ export const writeFileTool: Tool = {
|
|
|
70
75
|
required: ['path', 'content']
|
|
71
76
|
}
|
|
72
77
|
},
|
|
73
|
-
execute: async (args) => {
|
|
74
|
-
const path =
|
|
78
|
+
execute: async (args, context) => {
|
|
79
|
+
const path = resolvePath(String(args.path), context);
|
|
75
80
|
const content = String(args.content);
|
|
76
81
|
const append = args.append === 'true';
|
|
77
|
-
|
|
82
|
+
|
|
78
83
|
try {
|
|
79
84
|
// Ensure directory exists
|
|
80
85
|
const { mkdir, appendFile } = await import('fs/promises');
|
|
81
86
|
await mkdir(dirname(path), { recursive: true });
|
|
82
|
-
|
|
87
|
+
|
|
83
88
|
let beforeContent = '';
|
|
84
89
|
try {
|
|
85
90
|
beforeContent = await readFile(path, { encoding: 'utf-8' });
|
|
@@ -92,7 +97,7 @@ export const writeFileTool: Tool = {
|
|
|
92
97
|
} else {
|
|
93
98
|
await writeFile(path, content, 'utf-8');
|
|
94
99
|
}
|
|
95
|
-
|
|
100
|
+
|
|
96
101
|
const stats = await stat(path);
|
|
97
102
|
const afterContent = await readFile(path, { encoding: 'utf-8' });
|
|
98
103
|
const diff = buildSimpleDiff(beforeContent, afterContent);
|
|
@@ -131,38 +136,38 @@ export const listDirectoryTool: Tool = {
|
|
|
131
136
|
required: ['path']
|
|
132
137
|
}
|
|
133
138
|
},
|
|
134
|
-
execute: async (args) => {
|
|
135
|
-
const path =
|
|
139
|
+
execute: async (args, context) => {
|
|
140
|
+
const path = resolvePath(String(args.path), context);
|
|
136
141
|
const recursive = args.recursive === 'true';
|
|
137
|
-
|
|
142
|
+
|
|
138
143
|
async function listDir(dir: string, depth = 0): Promise<object[]> {
|
|
139
144
|
if (depth > 3) return [];
|
|
140
|
-
|
|
145
|
+
|
|
141
146
|
const entries = await readdir(dir, { withFileTypes: true });
|
|
142
147
|
const results: object[] = [];
|
|
143
|
-
|
|
148
|
+
|
|
144
149
|
for (const entry of entries.slice(0, 100)) { // Limit entries
|
|
145
150
|
const entryPath = resolve(dir, entry.name);
|
|
146
151
|
const info: Record<string, unknown> = {
|
|
147
152
|
name: entry.name,
|
|
148
153
|
type: entry.isDirectory() ? 'directory' : 'file'
|
|
149
154
|
};
|
|
150
|
-
|
|
155
|
+
|
|
151
156
|
if (entry.isFile()) {
|
|
152
157
|
const s = await stat(entryPath);
|
|
153
158
|
info.size = s.size;
|
|
154
159
|
}
|
|
155
|
-
|
|
160
|
+
|
|
156
161
|
if (recursive && entry.isDirectory() && !entry.name.startsWith('.')) {
|
|
157
162
|
info.children = await listDir(entryPath, depth + 1);
|
|
158
163
|
}
|
|
159
|
-
|
|
164
|
+
|
|
160
165
|
results.push(info);
|
|
161
166
|
}
|
|
162
|
-
|
|
167
|
+
|
|
163
168
|
return results;
|
|
164
169
|
}
|
|
165
|
-
|
|
170
|
+
|
|
166
171
|
try {
|
|
167
172
|
const entries = await listDir(path);
|
|
168
173
|
return JSON.stringify({ path, entries });
|
package/src/agent/tools/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Tool } from './types.js';
|
|
1
|
+
import { Tool, ToolContext } from './types.js';
|
|
2
2
|
import { ToolDefinition } from '../../types.js';
|
|
3
3
|
import { readFileTool, writeFileTool, listDirectoryTool } from './file.js';
|
|
4
4
|
import { runCommandTool } from './shell.js';
|
|
@@ -27,15 +27,16 @@ export function getTool(name: string, tools: Tool[] = defaultTools): Tool | unde
|
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
export async function executeTool(
|
|
30
|
-
name: string,
|
|
30
|
+
name: string,
|
|
31
31
|
args: Record<string, unknown>,
|
|
32
|
-
tools: Tool[] = defaultTools
|
|
32
|
+
tools: Tool[] = defaultTools,
|
|
33
|
+
context?: ToolContext
|
|
33
34
|
): Promise<string> {
|
|
34
35
|
const tool = getTool(name, tools);
|
|
35
36
|
if (!tool) {
|
|
36
37
|
throw new Error(`Unknown tool: ${name}`);
|
|
37
38
|
}
|
|
38
|
-
return tool.execute(args);
|
|
39
|
+
return tool.execute(args, context);
|
|
39
40
|
}
|
|
40
41
|
|
|
41
42
|
export { readFileTool, writeFileTool, listDirectoryTool } from './file.js';
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { spawn } from 'child_process';
|
|
2
2
|
import { resolve } from 'path';
|
|
3
|
-
import { Tool } from './types.js';
|
|
3
|
+
import { Tool, ToolContext } from './types.js';
|
|
4
4
|
import { ToolCapability } from '../runtime/capabilities.js';
|
|
5
5
|
|
|
6
6
|
interface RunResult {
|
|
@@ -58,9 +58,10 @@ export const searchTool: Tool = {
|
|
|
58
58
|
required: ['pattern']
|
|
59
59
|
}
|
|
60
60
|
},
|
|
61
|
-
execute: async (args) => {
|
|
61
|
+
execute: async (args, context) => {
|
|
62
|
+
const baseCwd = context?.cwd || process.cwd();
|
|
62
63
|
const pattern = String(args.pattern || '');
|
|
63
|
-
const path = resolve(String(args.path || '.'));
|
|
64
|
+
const path = resolve(baseCwd, String(args.path || '.'));
|
|
64
65
|
const glob = args.glob ? String(args.glob) : undefined;
|
|
65
66
|
const outputMode = args.output_mode === 'files' ? 'files' : 'content';
|
|
66
67
|
const maxResults = Math.max(1, Math.min(200, parseInt(String(args.max_results || '20'), 10)));
|
|
@@ -82,14 +83,14 @@ export const searchTool: Tool = {
|
|
|
82
83
|
|
|
83
84
|
let result: RunResult | null = null;
|
|
84
85
|
try {
|
|
85
|
-
result = await runCommand('rg', rgArgs,
|
|
86
|
+
result = await runCommand('rg', rgArgs, baseCwd);
|
|
86
87
|
} catch {
|
|
87
88
|
result = null;
|
|
88
89
|
}
|
|
89
90
|
|
|
90
91
|
if (!result || (result.code !== 0 && result.code !== 1)) {
|
|
91
92
|
const grepArgs = ['-R', '-n', pattern, path];
|
|
92
|
-
const fallback = await runCommand('grep', grepArgs,
|
|
93
|
+
const fallback = await runCommand('grep', grepArgs, baseCwd);
|
|
93
94
|
if (fallback.code !== 0 && fallback.code !== 1) {
|
|
94
95
|
throw new Error(fallback.stderr || 'Search failed');
|
|
95
96
|
}
|
package/src/agent/tools/shell.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { exec } from 'child_process';
|
|
2
2
|
import { promisify } from 'util';
|
|
3
3
|
import { resolve } from 'path';
|
|
4
|
-
import { Tool } from './types.js';
|
|
4
|
+
import { Tool, ToolContext } from './types.js';
|
|
5
5
|
import { ToolCapability } from '../runtime/capabilities.js';
|
|
6
6
|
|
|
7
7
|
const execAsync = promisify(exec);
|
|
@@ -32,24 +32,25 @@ export const runCommandTool: Tool = {
|
|
|
32
32
|
required: ['command']
|
|
33
33
|
}
|
|
34
34
|
},
|
|
35
|
-
execute: async (args) => {
|
|
35
|
+
execute: async (args, context) => {
|
|
36
36
|
const command = String(args.command);
|
|
37
|
-
const
|
|
37
|
+
const baseCwd = context?.cwd || process.cwd();
|
|
38
|
+
const cwd = args.cwd ? resolve(baseCwd, String(args.cwd)) : baseCwd;
|
|
38
39
|
const timeout = parseInt(String(args.timeout || '30000'), 10);
|
|
39
|
-
|
|
40
|
+
|
|
40
41
|
// Basic safety checks
|
|
41
42
|
const dangerous = ['rm -rf /', 'mkfs', ':(){:|:&};:'];
|
|
42
43
|
if (dangerous.some(d => command.includes(d))) {
|
|
43
44
|
throw new Error('Command rejected for safety reasons');
|
|
44
45
|
}
|
|
45
|
-
|
|
46
|
+
|
|
46
47
|
try {
|
|
47
|
-
const { stdout, stderr } = await execAsync(command, {
|
|
48
|
-
cwd,
|
|
48
|
+
const { stdout, stderr } = await execAsync(command, {
|
|
49
|
+
cwd,
|
|
49
50
|
timeout,
|
|
50
51
|
maxBuffer: 1024 * 1024 // 1MB
|
|
51
52
|
});
|
|
52
|
-
|
|
53
|
+
|
|
53
54
|
return JSON.stringify({
|
|
54
55
|
command,
|
|
55
56
|
cwd,
|
|
@@ -59,7 +60,10 @@ export const runCommandTool: Tool = {
|
|
|
59
60
|
});
|
|
60
61
|
} catch (err) {
|
|
61
62
|
const e = err as Error & { stdout?: string; stderr?: string; code?: number };
|
|
62
|
-
|
|
63
|
+
// Include both stdout and stderr in error - many tools output errors to stdout
|
|
64
|
+
const output = [e.stdout, e.stderr].filter(Boolean).join('\n').trim();
|
|
65
|
+
const outputSnippet = output.slice(0, 2000) || '(no output)';
|
|
66
|
+
throw new Error(`Command failed (exit ${e.code || '?'}):\n${outputSnippet}`);
|
|
63
67
|
}
|
|
64
68
|
}
|
|
65
69
|
};
|
package/src/agent/tools/types.ts
CHANGED
|
@@ -3,8 +3,12 @@ import { ToolCapability } from '../runtime/capabilities.js';
|
|
|
3
3
|
|
|
4
4
|
// --- Tool Interface ---
|
|
5
5
|
|
|
6
|
+
export interface ToolContext {
|
|
7
|
+
cwd: string; // Working directory for path resolution
|
|
8
|
+
}
|
|
9
|
+
|
|
6
10
|
export interface Tool {
|
|
7
11
|
definition: ToolDefinition;
|
|
8
12
|
capabilities?: ToolCapability[];
|
|
9
|
-
execute: (args: Record<string, unknown
|
|
13
|
+
execute: (args: Record<string, unknown>, context?: ToolContext) => Promise<string>;
|
|
10
14
|
}
|
package/src/cli.tsx
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import React from 'react';
|
|
3
3
|
import { render } from 'ink';
|
|
4
|
-
import { resolve
|
|
5
|
-
import { statSync
|
|
6
|
-
import { fileURLToPath } from 'url';
|
|
4
|
+
import { resolve } from 'path';
|
|
5
|
+
import { statSync } from 'fs';
|
|
7
6
|
import { App } from './App.js';
|
|
8
7
|
import { configStore } from './config.js';
|
|
8
|
+
import { getVersion } from './utils/version.js';
|
|
9
9
|
|
|
10
10
|
// --- CLI Entry Point ---
|
|
11
11
|
|
|
@@ -76,15 +76,43 @@ async function main(): Promise<void> {
|
|
|
76
76
|
await configStore.load(true);
|
|
77
77
|
|
|
78
78
|
const isTty = Boolean(process.stdout.isTTY);
|
|
79
|
-
const useAltScreen = isTty
|
|
79
|
+
const useAltScreen = isTty
|
|
80
|
+
&& process.env.ZTC_ALT_SCREEN === '1'
|
|
81
|
+
&& process.env.ZTC_NO_ALT_SCREEN !== '1'
|
|
82
|
+
&& process.env.ZTC_SCROLLBACK !== '1';
|
|
80
83
|
|
|
81
84
|
if (useAltScreen) {
|
|
82
85
|
process.stdout.write('\x1b[?1049h');
|
|
83
86
|
process.stdout.write('\x1b[?25l');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (isTty) {
|
|
90
|
+
// Clear screen and move cursor home for a clean start
|
|
84
91
|
process.stdout.write('\x1b[2J');
|
|
85
92
|
process.stdout.write('\x1b[H');
|
|
86
93
|
}
|
|
87
94
|
|
|
95
|
+
// In scrollback mode, print a static header once at startup
|
|
96
|
+
const scrollback = !useAltScreen;
|
|
97
|
+
if (scrollback && isTty) {
|
|
98
|
+
const version = getVersion();
|
|
99
|
+
const dateLabel = new Date().toLocaleDateString('en-US', {
|
|
100
|
+
month: 'short',
|
|
101
|
+
day: '2-digit',
|
|
102
|
+
year: 'numeric'
|
|
103
|
+
});
|
|
104
|
+
const cols = process.stdout.columns || 80;
|
|
105
|
+
const title = 'Zerg Terminal Client';
|
|
106
|
+
const right = `${dateLabel} • v${version} • Ctrl+C exit • /help`;
|
|
107
|
+
const padding = Math.max(0, cols - title.length - right.length - 4);
|
|
108
|
+
const borderLine = '─'.repeat(cols - 2);
|
|
109
|
+
|
|
110
|
+
console.log(`┌${borderLine}┐`);
|
|
111
|
+
console.log(`│ ${title}${' '.repeat(padding)}${right} │`);
|
|
112
|
+
console.log(`└${borderLine}┘`);
|
|
113
|
+
console.log(''); // Extra line after header
|
|
114
|
+
}
|
|
115
|
+
|
|
88
116
|
// Enable bracketed paste mode so we can detect paste boundaries
|
|
89
117
|
process.stdout.write('\x1b[?2004h');
|
|
90
118
|
|
|
@@ -94,21 +122,23 @@ async function main(): Promise<void> {
|
|
|
94
122
|
// Using flags=1 for basic disambiguation which reports Cmd+V as CSI sequence
|
|
95
123
|
process.stdout.write('\x1b[>1u');
|
|
96
124
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
syncPending
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
125
|
+
if (useAltScreen) {
|
|
126
|
+
// Wrap stdout.write to use synchronized output (reduces flicker)
|
|
127
|
+
// DECSM/DECRM 2026 tells terminal to batch updates
|
|
128
|
+
const originalWrite = process.stdout.write.bind(process.stdout);
|
|
129
|
+
let syncPending = false;
|
|
130
|
+
process.stdout.write = function(chunk: any, encoding?: any, callback?: any): boolean {
|
|
131
|
+
if (!syncPending) {
|
|
132
|
+
syncPending = true;
|
|
133
|
+
originalWrite('\x1b[?2026h'); // Begin synchronized update
|
|
134
|
+
setImmediate(() => {
|
|
135
|
+
originalWrite('\x1b[?2026l'); // End synchronized update
|
|
136
|
+
syncPending = false;
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
return originalWrite(chunk, encoding, callback);
|
|
140
|
+
} as typeof process.stdout.write;
|
|
141
|
+
}
|
|
112
142
|
|
|
113
143
|
// Render the app
|
|
114
144
|
const { waitUntilExit } = render(<App />);
|
|
@@ -130,14 +160,4 @@ async function main(): Promise<void> {
|
|
|
130
160
|
|
|
131
161
|
void main();
|
|
132
162
|
|
|
133
|
-
|
|
134
|
-
try {
|
|
135
|
-
const here = dirname(fileURLToPath(import.meta.url));
|
|
136
|
-
const pkgPath = resolve(here, '../package.json');
|
|
137
|
-
const raw = readFileSync(pkgPath, 'utf-8');
|
|
138
|
-
const parsed = JSON.parse(raw) as { version?: string };
|
|
139
|
-
return parsed.version || '0.0.0';
|
|
140
|
-
} catch {
|
|
141
|
-
return '0.0.0';
|
|
142
|
-
}
|
|
143
|
-
}
|
|
163
|
+
// getVersion moved to utils/version.ts
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { AgentState } from '../types.js';
|
|
3
|
+
import { InkNode } from '../ui/ink/index.js';
|
|
4
|
+
import { buildActivityLineView } from '../ui/views/activity_line.js';
|
|
5
|
+
|
|
6
|
+
interface ActivityLineProps {
|
|
7
|
+
state: AgentState;
|
|
8
|
+
spinnerLabel?: string | null;
|
|
9
|
+
spinnerFrame?: string | null;
|
|
10
|
+
inputMode?: 'queue' | 'interrupt';
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const ActivityLine: React.FC<ActivityLineProps> = ({
|
|
14
|
+
state,
|
|
15
|
+
spinnerLabel,
|
|
16
|
+
spinnerFrame,
|
|
17
|
+
inputMode
|
|
18
|
+
}) => {
|
|
19
|
+
const node = buildActivityLineView({ state, spinnerLabel, spinnerFrame, inputMode });
|
|
20
|
+
return <InkNode node={node} />;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export default ActivityLine;
|
|
@@ -18,6 +18,7 @@ import { Box } from 'ink';
|
|
|
18
18
|
|
|
19
19
|
interface FullScreenProps extends PropsWithChildren {
|
|
20
20
|
debug?: boolean;
|
|
21
|
+
scrollback?: boolean;
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
function getTerminalSize() {
|
|
@@ -27,15 +28,15 @@ function getTerminalSize() {
|
|
|
27
28
|
};
|
|
28
29
|
}
|
|
29
30
|
|
|
30
|
-
export const FullScreen: React.FC<FullScreenProps> = ({ children, debug = false }) => {
|
|
31
|
+
export const FullScreen: React.FC<FullScreenProps> = ({ children, debug = false, scrollback = false }) => {
|
|
31
32
|
const { rows, columns } = useScreenSize();
|
|
32
33
|
|
|
33
34
|
return (
|
|
34
35
|
<Box
|
|
35
36
|
flexDirection="column"
|
|
36
37
|
width={columns}
|
|
37
|
-
height={rows}
|
|
38
|
-
overflow=
|
|
38
|
+
height={scrollback ? undefined : rows}
|
|
39
|
+
overflow={scrollback ? undefined : 'hidden'}
|
|
39
40
|
borderStyle={debug ? 'single' : undefined}
|
|
40
41
|
borderColor={debug ? 'red' : undefined}
|
|
41
42
|
>
|
|
@@ -1,24 +1,70 @@
|
|
|
1
|
-
import React from 'react';
|
|
1
|
+
import React, { useRef, useMemo } from 'react';
|
|
2
2
|
import { Message } from '../types.js';
|
|
3
3
|
import { InkNode } from '../ui/ink/index.js';
|
|
4
4
|
import { buildMessageListView } from '../ui/views/message_list.js';
|
|
5
5
|
|
|
6
6
|
interface MessageListProps {
|
|
7
7
|
messages: Message[];
|
|
8
|
-
height
|
|
8
|
+
height?: number;
|
|
9
9
|
maxMessages?: number;
|
|
10
10
|
debug?: boolean;
|
|
11
11
|
expandToolOutputs?: boolean;
|
|
12
|
+
scrollback?: boolean;
|
|
12
13
|
}
|
|
13
14
|
|
|
14
|
-
export const MessageList: React.FC<MessageListProps> = ({
|
|
15
|
-
messages,
|
|
15
|
+
export const MessageList: React.FC<MessageListProps> = ({
|
|
16
|
+
messages,
|
|
16
17
|
height,
|
|
17
18
|
maxMessages = 50,
|
|
18
19
|
debug = false,
|
|
19
|
-
expandToolOutputs = false
|
|
20
|
+
expandToolOutputs = false,
|
|
21
|
+
scrollback = false
|
|
20
22
|
}) => {
|
|
21
|
-
|
|
23
|
+
// In scrollback mode, track which messages have been rendered to avoid duplication
|
|
24
|
+
const renderedIdsRef = useRef<Set<string>>(new Set());
|
|
25
|
+
|
|
26
|
+
// Filter to only new, complete messages in scrollback mode
|
|
27
|
+
const messagesToRender = useMemo(() => {
|
|
28
|
+
if (!scrollback) {
|
|
29
|
+
return messages;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// In scrollback mode, only render messages that:
|
|
33
|
+
// 1. Are NOT currently streaming (wait for completion)
|
|
34
|
+
// 2. Haven't been rendered yet
|
|
35
|
+
const newMessages: Message[] = [];
|
|
36
|
+
for (const msg of messages) {
|
|
37
|
+
// Skip streaming messages - we'll render them when complete
|
|
38
|
+
if (msg.isStreaming) {
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Skip already rendered messages
|
|
43
|
+
if (renderedIdsRef.current.has(msg.id)) {
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// This is a new, complete message - render it
|
|
48
|
+
newMessages.push(msg);
|
|
49
|
+
// Mark as rendered immediately
|
|
50
|
+
renderedIdsRef.current.add(msg.id);
|
|
51
|
+
}
|
|
52
|
+
return newMessages;
|
|
53
|
+
}, [messages, scrollback]);
|
|
54
|
+
|
|
55
|
+
// Don't render anything if there are no new messages in scrollback mode
|
|
56
|
+
if (scrollback && messagesToRender.length === 0) {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const node = buildMessageListView({
|
|
61
|
+
messages: messagesToRender,
|
|
62
|
+
height,
|
|
63
|
+
maxMessages,
|
|
64
|
+
debug,
|
|
65
|
+
expandToolOutputs,
|
|
66
|
+
scrollback
|
|
67
|
+
});
|
|
22
68
|
return <InkNode node={node} />;
|
|
23
69
|
};
|
|
24
70
|
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
import { Message } from '../types.js';
|
|
4
|
+
|
|
5
|
+
interface SingleMessageProps {
|
|
6
|
+
message: Message;
|
|
7
|
+
expandToolOutputs?: boolean;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface RoleConfig {
|
|
11
|
+
icon: string;
|
|
12
|
+
color: string;
|
|
13
|
+
label: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const roleConfigs: Record<string, RoleConfig> = {
|
|
17
|
+
user: { icon: '❯', color: 'blue', label: 'You' },
|
|
18
|
+
assistant: { icon: '◆', color: 'magenta', label: 'Zerg' },
|
|
19
|
+
tool: { icon: '⏺', color: 'yellow', label: 'Trace' },
|
|
20
|
+
system: { icon: '●', color: 'gray', label: 'System' }
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
function formatTime(date: Date): string {
|
|
24
|
+
return date.toLocaleTimeString('en-US', {
|
|
25
|
+
hour: '2-digit',
|
|
26
|
+
minute: '2-digit',
|
|
27
|
+
hour12: false
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export const SingleMessage: React.FC<SingleMessageProps> = ({
|
|
32
|
+
message,
|
|
33
|
+
expandToolOutputs = false
|
|
34
|
+
}) => {
|
|
35
|
+
const config = roleConfigs[message.role] || roleConfigs.system;
|
|
36
|
+
const toolOutput = message.metadata && (message.metadata as Record<string, any>).toolOutput;
|
|
37
|
+
const content = toolOutput && expandToolOutputs && toolOutput.full
|
|
38
|
+
? toolOutput.full
|
|
39
|
+
: toolOutput && toolOutput.preview
|
|
40
|
+
? toolOutput.preview
|
|
41
|
+
: message.content;
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<Box flexDirection="column" marginY={1} paddingX={1}>
|
|
45
|
+
<Box flexDirection="row">
|
|
46
|
+
<Text color={config.color} bold>{config.icon} {config.label}</Text>
|
|
47
|
+
<Text color="gray" dimColor> • {formatTime(message.timestamp)}</Text>
|
|
48
|
+
{message.isStreaming && <Text color="yellow" bold> ▌</Text>}
|
|
49
|
+
</Box>
|
|
50
|
+
<Box flexDirection="column" marginLeft={2}>
|
|
51
|
+
{content.split('\n').map((line: string, i: number) => (
|
|
52
|
+
<Text key={i}>{line}</Text>
|
|
53
|
+
))}
|
|
54
|
+
</Box>
|
|
55
|
+
</Box>
|
|
56
|
+
);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export default SingleMessage;
|
|
@@ -15,6 +15,7 @@ interface StatusBarProps {
|
|
|
15
15
|
emulationId?: string;
|
|
16
16
|
inputMode?: 'queue' | 'interrupt';
|
|
17
17
|
toast?: string | null;
|
|
18
|
+
spinnerLabel?: string | null;
|
|
18
19
|
debug?: boolean;
|
|
19
20
|
}
|
|
20
21
|
|
|
@@ -30,6 +31,7 @@ export const StatusBar: React.FC<StatusBarProps> = ({
|
|
|
30
31
|
emulationId,
|
|
31
32
|
inputMode,
|
|
32
33
|
toast,
|
|
34
|
+
spinnerLabel,
|
|
33
35
|
debug = false
|
|
34
36
|
}) => {
|
|
35
37
|
const node = buildStatusBarView({
|
|
@@ -44,6 +46,7 @@ export const StatusBar: React.FC<StatusBarProps> = ({
|
|
|
44
46
|
emulationId,
|
|
45
47
|
inputMode,
|
|
46
48
|
toast,
|
|
49
|
+
spinnerLabel,
|
|
47
50
|
debug
|
|
48
51
|
});
|
|
49
52
|
return <InkNode node={node} />;
|
package/src/components/index.tsx
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
// Component exports
|
|
2
2
|
export { Header } from './Header.js';
|
|
3
3
|
export { MessageList } from './MessageList.js';
|
|
4
|
+
export { SingleMessage } from './SingleMessage.js';
|
|
4
5
|
export { InputArea } from './InputArea.js';
|
|
5
6
|
export { StatusBar } from './StatusBar.js';
|
|
6
|
-
export { FullScreen, useScreenSize } from './FullScreen.js';
|
|
7
|
+
export { FullScreen, useScreenSize } from './FullScreen.js';
|
|
8
|
+
export { ActivityLine } from './ActivityLine.js';
|
package/src/config/types.ts
CHANGED
package/src/config.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { join } from 'path';
|
|
|
3
3
|
import { readFile, writeFile, mkdir } from 'fs/promises';
|
|
4
4
|
|
|
5
5
|
import { ZTCConfig } from './config/types.js';
|
|
6
|
+
import { DEFAULT_SPINNER_VERBS } from './utils/spinner_verbs.js';
|
|
6
7
|
|
|
7
8
|
// --- Configuration Store ---
|
|
8
9
|
|
|
@@ -15,6 +16,7 @@ const DEFAULT_CONFIG: ZTCConfig = {
|
|
|
15
16
|
maxTokens: 4096,
|
|
16
17
|
zergEndpoint: undefined,
|
|
17
18
|
emulationId: undefined,
|
|
19
|
+
spinnerVerbs: DEFAULT_SPINNER_VERBS,
|
|
18
20
|
toolPermissions: {
|
|
19
21
|
file_read: true,
|
|
20
22
|
file_write: true,
|
|
@@ -46,6 +48,9 @@ class ConfigStore {
|
|
|
46
48
|
if (this.config.model === 'claude-opus-4-5-20250514') {
|
|
47
49
|
this.config.model = 'claude-opus-4-20250514';
|
|
48
50
|
}
|
|
51
|
+
if (!this.config.spinnerVerbs) {
|
|
52
|
+
this.config.spinnerVerbs = DEFAULT_SPINNER_VERBS;
|
|
53
|
+
}
|
|
49
54
|
if (!this.config.toolPermissions) {
|
|
50
55
|
this.config.toolPermissions = { ...DEFAULT_CONFIG.toolPermissions };
|
|
51
56
|
}
|
|
@@ -92,6 +97,9 @@ class ConfigStore {
|
|
|
92
97
|
if (this.config.model === 'claude-opus-4-5-20250514') {
|
|
93
98
|
this.config.model = 'claude-opus-4-20250514';
|
|
94
99
|
}
|
|
100
|
+
if (!this.config.spinnerVerbs) {
|
|
101
|
+
this.config.spinnerVerbs = DEFAULT_SPINNER_VERBS;
|
|
102
|
+
}
|
|
95
103
|
if (!this.config.toolPermissions) {
|
|
96
104
|
this.config.toolPermissions = { ...DEFAULT_CONFIG.toolPermissions };
|
|
97
105
|
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { AgentState } from '../../types.js';
|
|
2
|
+
import { box, text, LayoutNode } from '../core/index.js';
|
|
3
|
+
|
|
4
|
+
interface ActivityLineProps {
|
|
5
|
+
state: AgentState;
|
|
6
|
+
spinnerLabel?: string | null;
|
|
7
|
+
spinnerFrame?: string | null;
|
|
8
|
+
inputMode?: 'queue' | 'interrupt';
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function buildActivityLineView({
|
|
12
|
+
state,
|
|
13
|
+
spinnerLabel,
|
|
14
|
+
spinnerFrame,
|
|
15
|
+
inputMode
|
|
16
|
+
}: ActivityLineProps): LayoutNode {
|
|
17
|
+
const active = state.status === 'thinking' || state.status === 'streaming';
|
|
18
|
+
if (!active || !spinnerLabel) {
|
|
19
|
+
return box([], { height: 0 });
|
|
20
|
+
}
|
|
21
|
+
const frame = spinnerFrame || '✽';
|
|
22
|
+
const hint = inputMode === 'interrupt' ? ' (esc to interrupt)' : '';
|
|
23
|
+
return box([
|
|
24
|
+
text(`${frame} ${spinnerLabel}…${hint}`, { color: 'yellow' })
|
|
25
|
+
], {
|
|
26
|
+
flexDirection: 'row',
|
|
27
|
+
paddingX: 2,
|
|
28
|
+
paddingY: 0,
|
|
29
|
+
marginTop: 1,
|
|
30
|
+
marginBottom: 1,
|
|
31
|
+
height: 1
|
|
32
|
+
});
|
|
33
|
+
}
|