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/App.tsx
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import React, { useState, useCallback, useMemo, useRef } from 'react';
|
|
2
|
-
import { Box, useApp, useInput } from 'ink';
|
|
3
|
-
import { Header, MessageList, InputArea, StatusBar, FullScreen, useScreenSize } from './components/index.js';
|
|
2
|
+
import { Box, useApp, useInput, Static } from 'ink';
|
|
3
|
+
import { Header, MessageList, SingleMessage, InputArea, StatusBar, FullScreen, ActivityLine, useScreenSize } from './components/index.js';
|
|
4
4
|
import { buildAppView } from './ui/views/app.js';
|
|
5
5
|
import { useMirror } from './web/mirror_hook.js';
|
|
6
6
|
import { Agent } from './agent/index.js';
|
|
7
7
|
import { commands, type CommandContext } from './agent/commands/index.js';
|
|
8
8
|
import { Message, AgentState, InputMode } from './types.js';
|
|
9
9
|
import { InputState } from './ui/core/input_state.js';
|
|
10
|
+
import { estimateInputLines } from './ui/views/input_area.js';
|
|
10
11
|
import { configStore } from './config.js';
|
|
11
12
|
import { createInputBus } from './ui/core/input.js';
|
|
12
13
|
import { loadEmulationProfiles, getEmulationProfile } from './emulation/catalog.js';
|
|
@@ -20,6 +21,10 @@ import { autoActivateSkills, buildSkillPrompt } from './skills/index.js';
|
|
|
20
21
|
import { getSkillRegistry } from './skills/registry.js';
|
|
21
22
|
import { Skill } from './skills/types.js';
|
|
22
23
|
import { createAgentFromConfig } from './agent/factory.js';
|
|
24
|
+
import { checkForUpdate } from './utils/update.js';
|
|
25
|
+
import { getVersion } from './utils/version.js';
|
|
26
|
+
import { DEFAULT_SPINNER_VERBS } from './utils/spinner_verbs.js';
|
|
27
|
+
import { SPINNER_FRAMES } from './utils/spinner_frames.js';
|
|
23
28
|
|
|
24
29
|
// --- Utilities ---
|
|
25
30
|
|
|
@@ -78,6 +83,7 @@ export const App: React.FC = () => {
|
|
|
78
83
|
const queueRef = useRef<string[]>([]);
|
|
79
84
|
const activeRunIdRef = useRef<string | null>(null);
|
|
80
85
|
const runCounterRef = useRef(0);
|
|
86
|
+
const lastRunDurationRef = useRef<number | null>(null);
|
|
81
87
|
|
|
82
88
|
React.useEffect(() => {
|
|
83
89
|
let active = true;
|
|
@@ -106,6 +112,8 @@ export const App: React.FC = () => {
|
|
|
106
112
|
const [retryAvailable, setRetryAvailable] = useState(false);
|
|
107
113
|
const [toast, setToast] = useState<string | null>(null);
|
|
108
114
|
const toastTimerRef = useRef<NodeJS.Timeout | null>(null);
|
|
115
|
+
const [spinnerLabel, setSpinnerLabel] = useState<string | null>(null);
|
|
116
|
+
const [spinnerFrame, setSpinnerFrame] = useState<string | null>(null);
|
|
109
117
|
const streamingMessageId = React.useRef<string | null>(null);
|
|
110
118
|
const streamedResponse = React.useRef(false);
|
|
111
119
|
const toolStartTimes = React.useRef<Map<string, number[]>>(new Map());
|
|
@@ -117,6 +125,8 @@ export const App: React.FC = () => {
|
|
|
117
125
|
);
|
|
118
126
|
const contextLength = agentState.contextTokens ?? fallbackContextLength;
|
|
119
127
|
const contextEstimated = agentState.contextTokens ? agentState.tokensEstimated : true;
|
|
128
|
+
const spinnerVerbs = configStore.get().spinnerVerbs || DEFAULT_SPINNER_VERBS;
|
|
129
|
+
const spinnerVerbsKey = spinnerVerbs.join('|');
|
|
120
130
|
|
|
121
131
|
React.useEffect(() => {
|
|
122
132
|
renderCount.current += 1;
|
|
@@ -126,15 +136,69 @@ export const App: React.FC = () => {
|
|
|
126
136
|
messagesRef.current = messages;
|
|
127
137
|
}, [messages]);
|
|
128
138
|
const [debug, setDebug] = useState(false);
|
|
139
|
+
const scrollback = process.env.ZTC_SCROLLBACK === '1' || process.env.ZTC_ALT_SCREEN !== '1';
|
|
129
140
|
|
|
130
|
-
|
|
131
|
-
|
|
141
|
+
React.useEffect(() => {
|
|
142
|
+
if (agentState.status !== 'thinking' && agentState.status !== 'streaming') {
|
|
143
|
+
setSpinnerLabel(null);
|
|
144
|
+
setSpinnerFrame(null);
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
if (spinnerVerbs.length === 0) {
|
|
148
|
+
setSpinnerLabel(null);
|
|
149
|
+
setSpinnerFrame(null);
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
const verb = spinnerVerbs[Math.floor(Math.random() * spinnerVerbs.length)];
|
|
153
|
+
setSpinnerLabel(verb);
|
|
154
|
+
}, [agentState.status, spinnerVerbsKey]);
|
|
155
|
+
|
|
156
|
+
React.useEffect(() => {
|
|
157
|
+
if (agentState.status !== 'thinking' && agentState.status !== 'streaming') {
|
|
158
|
+
setSpinnerFrame(null);
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
// Animation is now safe in scrollback mode because we use Ink's Static component
|
|
162
|
+
// for completed messages - only the live section below Static re-renders
|
|
163
|
+
let index = 0;
|
|
164
|
+
setSpinnerFrame(SPINNER_FRAMES[index]);
|
|
165
|
+
const interval = setInterval(() => {
|
|
166
|
+
index = (index + 1) % SPINNER_FRAMES.length;
|
|
167
|
+
setSpinnerFrame(SPINNER_FRAMES[index]);
|
|
168
|
+
}, 120);
|
|
169
|
+
return () => {
|
|
170
|
+
clearInterval(interval);
|
|
171
|
+
};
|
|
172
|
+
}, [agentState.status]);
|
|
173
|
+
|
|
174
|
+
const headerHeight = scrollback ? 0 : 4; // border (2) + content (1) + margin (1)
|
|
132
175
|
const statusHeight = 1;
|
|
176
|
+
const suggestionLines = 4;
|
|
177
|
+
|
|
178
|
+
const estimateBadgePreviewLines = useCallback((state: InputState): number => {
|
|
179
|
+
const segment = state.segments[state.cursor.index];
|
|
180
|
+
if (!segment || segment.type === 'text') return 0;
|
|
181
|
+
if (segment.type === 'paste') {
|
|
182
|
+
const lines = segment.text.split('\n');
|
|
183
|
+
return Math.min(3, lines.length) + (lines.length > 3 ? 1 : 0);
|
|
184
|
+
}
|
|
185
|
+
if (segment.type === 'image') {
|
|
186
|
+
return 16;
|
|
187
|
+
}
|
|
188
|
+
return 1;
|
|
189
|
+
}, []);
|
|
190
|
+
|
|
191
|
+
const inputHeight = useMemo(() => {
|
|
192
|
+
const inputLineCount = estimateInputLines(inputSnapshot.segments, columns);
|
|
193
|
+
const previewLines = estimateBadgePreviewLines(inputSnapshot);
|
|
194
|
+
const maxInputLines = Math.max(1, rows - (headerHeight + statusHeight + suggestionLines + 5));
|
|
195
|
+
return Math.min(Math.max(1, inputLineCount + previewLines), maxInputLines) + suggestionLines;
|
|
196
|
+
}, [columns, estimateBadgePreviewLines, inputSnapshot, rows]);
|
|
133
197
|
|
|
134
198
|
// Calculate content height (total - header - input - status)
|
|
135
199
|
const contentHeight = useMemo(
|
|
136
|
-
() => Math.max(rows - (headerHeight + inputHeight + statusHeight), 5),
|
|
137
|
-
[rows]
|
|
200
|
+
() => scrollback ? undefined : Math.max(rows - (headerHeight + inputHeight + statusHeight), 5),
|
|
201
|
+
[rows, scrollback, inputHeight]
|
|
138
202
|
);
|
|
139
203
|
|
|
140
204
|
// Reload agent when config changes
|
|
@@ -180,6 +244,21 @@ export const App: React.FC = () => {
|
|
|
180
244
|
}]);
|
|
181
245
|
}, []);
|
|
182
246
|
|
|
247
|
+
React.useEffect(() => {
|
|
248
|
+
if (process.env.ZTC_DISABLE_UPDATE === '1') return;
|
|
249
|
+
if (process.env.ZTC_WEB_MIRROR === '1') return;
|
|
250
|
+
let active = true;
|
|
251
|
+
const current = getVersion();
|
|
252
|
+
checkForUpdate(current).then(info => {
|
|
253
|
+
if (!active || !info.hasUpdate) return;
|
|
254
|
+
addMessage({
|
|
255
|
+
role: 'system',
|
|
256
|
+
content: `Update available: v${info.latest} (current v${info.current}). Run /update to install.`
|
|
257
|
+
});
|
|
258
|
+
}).catch(() => {});
|
|
259
|
+
return () => { active = false; };
|
|
260
|
+
}, [addMessage]);
|
|
261
|
+
|
|
183
262
|
const clearMessages = useCallback(() => {
|
|
184
263
|
setMessages([]);
|
|
185
264
|
}, []);
|
|
@@ -191,10 +270,14 @@ export const App: React.FC = () => {
|
|
|
191
270
|
setCwd: async (path: string) => {
|
|
192
271
|
const next = await resolveWorkingDir(shellCwdRef.current, path);
|
|
193
272
|
shellCwdRef.current = next;
|
|
273
|
+
// Also update agent's working directory so tools use it
|
|
274
|
+
if (agent) {
|
|
275
|
+
agent.setCwd(next);
|
|
276
|
+
}
|
|
194
277
|
return next;
|
|
195
278
|
},
|
|
196
279
|
run: async (command: string) => runShellCommand(command, shellCwdRef.current)
|
|
197
|
-
}), []);
|
|
280
|
+
}), [agent]);
|
|
198
281
|
|
|
199
282
|
const isRetryableError = useCallback((message: string) => {
|
|
200
283
|
const lower = message.toLowerCase();
|
|
@@ -261,6 +344,7 @@ export const App: React.FC = () => {
|
|
|
261
344
|
const runId = `${Date.now()}_${runCounterRef.current++}`;
|
|
262
345
|
activeRunIdRef.current = runId;
|
|
263
346
|
const isActive = () => activeRunIdRef.current === runId;
|
|
347
|
+
const runStartedAt = Date.now();
|
|
264
348
|
setAgentState({ status: 'thinking', startedAt: new Date() });
|
|
265
349
|
void runWithRetry(last.messages, last.agent, true, isActive).catch((err) => {
|
|
266
350
|
const message = (err as Error).message || 'Agent error';
|
|
@@ -331,7 +415,13 @@ export const App: React.FC = () => {
|
|
|
331
415
|
if (text.startsWith('!')) {
|
|
332
416
|
const commandText = text.slice(1).trim();
|
|
333
417
|
if (commandText) {
|
|
334
|
-
|
|
418
|
+
// Intercept 'cd' commands to use the proper /cd handler that persists
|
|
419
|
+
if (commandText === 'cd' || commandText.startsWith('cd ')) {
|
|
420
|
+
const path = commandText.slice(2).trim();
|
|
421
|
+
handleCommand('cd', path ? [path] : []);
|
|
422
|
+
} else {
|
|
423
|
+
handleCommand('shell', [commandText]);
|
|
424
|
+
}
|
|
335
425
|
}
|
|
336
426
|
return;
|
|
337
427
|
}
|
|
@@ -417,6 +507,7 @@ export const App: React.FC = () => {
|
|
|
417
507
|
const runId = `${Date.now()}_${runCounterRef.current++}`;
|
|
418
508
|
activeRunIdRef.current = runId;
|
|
419
509
|
const isActive = () => activeRunIdRef.current === runId;
|
|
510
|
+
const runStartedAt = Date.now();
|
|
420
511
|
setAgentState({ status: 'thinking', startedAt: new Date() });
|
|
421
512
|
streamingMessageId.current = null;
|
|
422
513
|
streamedResponse.current = false;
|
|
@@ -466,10 +557,18 @@ export const App: React.FC = () => {
|
|
|
466
557
|
break;
|
|
467
558
|
case 'stream_end':
|
|
468
559
|
if (streamingMessageId.current) {
|
|
469
|
-
setMessages(prev =>
|
|
470
|
-
if
|
|
471
|
-
|
|
472
|
-
|
|
560
|
+
setMessages(prev => {
|
|
561
|
+
// Remove the streaming message if it ended up empty (tool-only response)
|
|
562
|
+
const streamingMsg = prev.find(m => m.id === streamingMessageId.current);
|
|
563
|
+
if (streamingMsg && (!streamingMsg.content || streamingMsg.content.trim() === '')) {
|
|
564
|
+
return prev.filter(m => m.id !== streamingMessageId.current);
|
|
565
|
+
}
|
|
566
|
+
// Otherwise mark it as no longer streaming
|
|
567
|
+
return prev.map(msg => {
|
|
568
|
+
if (msg.id !== streamingMessageId.current) return msg;
|
|
569
|
+
return { ...msg, isStreaming: false };
|
|
570
|
+
});
|
|
571
|
+
});
|
|
473
572
|
}
|
|
474
573
|
streamingMessageId.current = null;
|
|
475
574
|
break;
|
|
@@ -480,7 +579,7 @@ export const App: React.FC = () => {
|
|
|
480
579
|
toolStartTimes.current.set(key, bucket);
|
|
481
580
|
const duration = started ? Date.now() - started : undefined;
|
|
482
581
|
const emulationId = configStore.getEmulationId();
|
|
483
|
-
if (getTraceStyle(emulationId) === 'claude_code') {
|
|
582
|
+
if (getTraceStyle(emulationId) === 'claude_code' || getTraceStyle(emulationId) === 'codex') {
|
|
484
583
|
const output = buildToolOutputMessage(event.tool, event.result, duration, emulationId);
|
|
485
584
|
addMessage({
|
|
486
585
|
role: 'tool',
|
|
@@ -553,6 +652,14 @@ export const App: React.FC = () => {
|
|
|
553
652
|
streamedResponse.current = false;
|
|
554
653
|
streamingMessageId.current = null;
|
|
555
654
|
activeRunIdRef.current = null;
|
|
655
|
+
lastRunDurationRef.current = Date.now() - runStartedAt;
|
|
656
|
+
if (getTraceStyle(configStore.getEmulationId()) === 'codex') {
|
|
657
|
+
const seconds = Math.max(0, Math.round((lastRunDurationRef.current || 0) / 1000));
|
|
658
|
+
addMessage({
|
|
659
|
+
role: 'tool',
|
|
660
|
+
content: `✻ Worked for ${seconds}s`
|
|
661
|
+
});
|
|
662
|
+
}
|
|
556
663
|
cleanup();
|
|
557
664
|
if (inputMode === 'queue' && queueRef.current.length > 0) {
|
|
558
665
|
const next = queueRef.current.shift();
|
|
@@ -577,6 +684,7 @@ export const App: React.FC = () => {
|
|
|
577
684
|
rows,
|
|
578
685
|
commands,
|
|
579
686
|
hasApiKey: configStore.hasApiKey(),
|
|
687
|
+
version: getVersion(),
|
|
580
688
|
contextLength,
|
|
581
689
|
contextEstimated,
|
|
582
690
|
provider,
|
|
@@ -584,10 +692,12 @@ export const App: React.FC = () => {
|
|
|
584
692
|
emulationId,
|
|
585
693
|
inputMode,
|
|
586
694
|
toast,
|
|
695
|
+
spinnerLabel,
|
|
696
|
+
spinnerFrame,
|
|
587
697
|
debug,
|
|
588
698
|
expandToolOutputs
|
|
589
699
|
});
|
|
590
|
-
}, [mirrorEnabled, messages, agentState, inputSnapshot, sessionId, rows, contextLength, contextEstimated, provider, model, emulationId, inputMode, toast, debug, expandToolOutputs]);
|
|
700
|
+
}, [mirrorEnabled, messages, agentState, inputSnapshot, sessionId, rows, contextLength, contextEstimated, provider, model, emulationId, inputMode, toast, spinnerLabel, spinnerFrame, debug, expandToolOutputs]);
|
|
591
701
|
|
|
592
702
|
const showToast = useCallback((message: string) => {
|
|
593
703
|
setToast(message);
|
|
@@ -601,22 +711,66 @@ export const App: React.FC = () => {
|
|
|
601
711
|
|
|
602
712
|
useMirror(layoutTree, inputBus);
|
|
603
713
|
|
|
714
|
+
// Compute messages for Static (completed) vs live (streaming)
|
|
715
|
+
// Static component handles deduplication by key - it only renders new items
|
|
716
|
+
const { staticMessages, streamingMessage } = useMemo(() => {
|
|
717
|
+
if (!scrollback) {
|
|
718
|
+
return { staticMessages: [], streamingMessage: null };
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
// Completed messages go to Static (rendered once, kept in scrollback)
|
|
722
|
+
const completed = messages.filter(m => !m.isStreaming);
|
|
723
|
+
// Streaming message stays in live section (can update)
|
|
724
|
+
const streaming = messages.find(m => m.isStreaming) || null;
|
|
725
|
+
|
|
726
|
+
return {
|
|
727
|
+
staticMessages: completed,
|
|
728
|
+
streamingMessage: streaming
|
|
729
|
+
};
|
|
730
|
+
}, [messages, scrollback]);
|
|
731
|
+
|
|
604
732
|
return (
|
|
605
|
-
<FullScreen debug={debug}>
|
|
606
|
-
<Header version=
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
733
|
+
<FullScreen debug={debug} scrollback={scrollback}>
|
|
734
|
+
{!scrollback && <Header version={getVersion()} debug={debug} />}
|
|
735
|
+
|
|
736
|
+
{scrollback ? (
|
|
737
|
+
<Box flexDirection="column">
|
|
738
|
+
{/* Static: each message rendered ONCE, stays in scrollback buffer */}
|
|
739
|
+
<Static items={staticMessages}>
|
|
740
|
+
{(msg: Message) => (
|
|
741
|
+
<Box key={msg.id} flexDirection="column">
|
|
742
|
+
<SingleMessage message={msg} expandToolOutputs={expandToolOutputs} />
|
|
743
|
+
</Box>
|
|
744
|
+
)}
|
|
745
|
+
</Static>
|
|
746
|
+
|
|
747
|
+
{/* Live section: streaming message can update */}
|
|
748
|
+
{streamingMessage && (
|
|
749
|
+
<SingleMessage message={streamingMessage} expandToolOutputs={expandToolOutputs} />
|
|
750
|
+
)}
|
|
751
|
+
</Box>
|
|
752
|
+
) : (
|
|
753
|
+
<MessageList
|
|
754
|
+
messages={messages}
|
|
755
|
+
height={contentHeight}
|
|
756
|
+
debug={debug}
|
|
757
|
+
expandToolOutputs={expandToolOutputs}
|
|
758
|
+
scrollback={scrollback}
|
|
759
|
+
/>
|
|
760
|
+
)}
|
|
761
|
+
|
|
762
|
+
<ActivityLine
|
|
763
|
+
state={agentState}
|
|
764
|
+
spinnerLabel={spinnerLabel}
|
|
765
|
+
spinnerFrame={spinnerFrame}
|
|
766
|
+
inputMode={inputMode}
|
|
613
767
|
/>
|
|
614
|
-
|
|
768
|
+
|
|
615
769
|
<InputArea
|
|
616
770
|
onSubmit={handleSubmit}
|
|
617
771
|
onCommand={handleCommand}
|
|
618
772
|
commands={commands}
|
|
619
|
-
onStateChange={
|
|
773
|
+
onStateChange={setInputSnapshot}
|
|
620
774
|
onToast={showToast}
|
|
621
775
|
cols={columns}
|
|
622
776
|
inputBus={inputBus}
|
|
@@ -630,11 +784,11 @@ export const App: React.FC = () => {
|
|
|
630
784
|
'Type a message or /help for commands...'
|
|
631
785
|
}
|
|
632
786
|
/>
|
|
633
|
-
|
|
787
|
+
|
|
634
788
|
<StatusBar
|
|
635
789
|
state={agentState}
|
|
636
790
|
sessionId={sessionId}
|
|
637
|
-
version=
|
|
791
|
+
version={getVersion()}
|
|
638
792
|
connectionStatus={configStore.hasApiKey() ? 'connected' : 'disconnected'}
|
|
639
793
|
contextLength={contextLength}
|
|
640
794
|
contextEstimated={contextEstimated}
|
package/src/agent/agent.ts
CHANGED
|
@@ -22,6 +22,7 @@ export interface AgentConfig {
|
|
|
22
22
|
backend?: AgentBackend;
|
|
23
23
|
policy?: Policy;
|
|
24
24
|
tracer?: Tracer;
|
|
25
|
+
cwd?: string; // Working directory for tool execution
|
|
25
26
|
}
|
|
26
27
|
|
|
27
28
|
export class AgentError extends Error {
|
|
@@ -42,10 +43,11 @@ export class Agent {
|
|
|
42
43
|
private policy: Policy;
|
|
43
44
|
private tracer: Tracer;
|
|
44
45
|
private streamChunkSize = 32;
|
|
45
|
-
|
|
46
|
+
private _cwd: string;
|
|
47
|
+
|
|
46
48
|
constructor(config: AgentConfig = {}) {
|
|
47
49
|
const apiKey = config.apiKey || process.env.ANTHROPIC_API_KEY || '';
|
|
48
|
-
|
|
50
|
+
|
|
49
51
|
this.config = {
|
|
50
52
|
model: config.model || 'claude-opus-4-20250514',
|
|
51
53
|
apiKey,
|
|
@@ -56,18 +58,28 @@ export class Agent {
|
|
|
56
58
|
systemPrompt: config.systemPrompt || this.getDefaultSystemPrompt(),
|
|
57
59
|
backend: config.backend || new AnthropicBackend({ apiKey, apiEndpoint: config.apiEndpoint }),
|
|
58
60
|
policy: config.policy || new AllowAllPolicy(),
|
|
59
|
-
tracer: config.tracer || new NoopTracer()
|
|
61
|
+
tracer: config.tracer || new NoopTracer(),
|
|
62
|
+
cwd: config.cwd || process.cwd()
|
|
60
63
|
};
|
|
61
64
|
|
|
62
65
|
this.backend = this.config.backend;
|
|
63
66
|
this.policy = this.config.policy;
|
|
64
67
|
this.tracer = this.config.tracer;
|
|
68
|
+
this._cwd = this.config.cwd;
|
|
65
69
|
}
|
|
66
70
|
|
|
67
71
|
hasApiKey(): boolean {
|
|
68
72
|
return !!this.config.apiKey && this.config.apiKey.length > 0;
|
|
69
73
|
}
|
|
70
74
|
|
|
75
|
+
setCwd(cwd: string): void {
|
|
76
|
+
this._cwd = cwd;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
getCwd(): string {
|
|
80
|
+
return this._cwd;
|
|
81
|
+
}
|
|
82
|
+
|
|
71
83
|
private getDefaultSystemPrompt(): string {
|
|
72
84
|
return `You are ZTC (Zerg Terminal Client), an AI assistant that helps users interact with the Zerg continual AI system and manage local development tasks.
|
|
73
85
|
|
|
@@ -97,7 +109,14 @@ When a user intent maps to an available slash command, invoke the command direct
|
|
|
97
109
|
// Convert messages to API format
|
|
98
110
|
private formatMessages(messages: Message[]): LlmMessage[] {
|
|
99
111
|
return messages
|
|
100
|
-
.filter((m): m is Message & { role: 'user' | 'assistant' } =>
|
|
112
|
+
.filter((m): m is Message & { role: 'user' | 'assistant' } => {
|
|
113
|
+
// Only include user and assistant messages
|
|
114
|
+
if (m.role !== 'user' && m.role !== 'assistant') return false;
|
|
115
|
+
// Filter out assistant messages with empty content (from tool-only responses)
|
|
116
|
+
// The API rejects empty content for non-final assistant messages
|
|
117
|
+
if (m.role === 'assistant' && (!m.content || m.content.trim() === '')) return false;
|
|
118
|
+
return true;
|
|
119
|
+
})
|
|
101
120
|
.map(m => ({
|
|
102
121
|
role: m.role,
|
|
103
122
|
content: m.role === 'user' ? this.buildContentBlocks(m.content) : m.content
|
|
@@ -336,9 +355,10 @@ When a user intent maps to an available slash command, invoke the command direct
|
|
|
336
355
|
|
|
337
356
|
try {
|
|
338
357
|
const result = await executeTool(
|
|
339
|
-
toolBlock.name,
|
|
358
|
+
toolBlock.name,
|
|
340
359
|
toolBlock.input,
|
|
341
|
-
this.config.tools
|
|
360
|
+
this.config.tools,
|
|
361
|
+
{ cwd: this._cwd }
|
|
342
362
|
);
|
|
343
363
|
|
|
344
364
|
toolCall.status = 'complete';
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { Command } from './types.js';
|
|
2
|
+
import { DEFAULT_SPINNER_VERBS, formatSpinnerVerbs, parseSpinnerVerbs } from '../../utils/spinner_verbs.js';
|
|
2
3
|
|
|
3
4
|
export const configCommand: Command = {
|
|
4
5
|
name: 'config',
|
|
5
6
|
description: 'Manage configuration',
|
|
6
|
-
usage: '<show|key|provider|endpoint|model> [value]',
|
|
7
|
+
usage: '<show|key|provider|endpoint|model|spinner> [value]',
|
|
7
8
|
handler: async (args, ctx) => {
|
|
8
9
|
const [subCmd, ...rest] = args;
|
|
9
10
|
const value = rest.join(' ');
|
|
@@ -24,6 +25,7 @@ export const configCommand: Command = {
|
|
|
24
25
|
` Max Tokens: ${config.maxTokens}`,
|
|
25
26
|
` Zerg Endpoint: ${config.zergEndpoint || '(not set)'}`,
|
|
26
27
|
` Emulation: ${ctx.config.getEmulationId() || '(none)'}`,
|
|
28
|
+
` Spinner verbs: ${formatSpinnerVerbs(config.spinnerVerbs || DEFAULT_SPINNER_VERBS)}`,
|
|
27
29
|
'',
|
|
28
30
|
`Config storage: ${ctx.config.locationLabel || '~/.ztc/config.json'}`
|
|
29
31
|
].join('\n')
|
|
@@ -120,10 +122,82 @@ export const configCommand: Command = {
|
|
|
120
122
|
});
|
|
121
123
|
break;
|
|
122
124
|
|
|
125
|
+
case 'spinner': {
|
|
126
|
+
const [action, ...restParts] = rest;
|
|
127
|
+
const actionValue = restParts.join(' ');
|
|
128
|
+
const current = ctx.config.get().spinnerVerbs || DEFAULT_SPINNER_VERBS;
|
|
129
|
+
|
|
130
|
+
if (!action || action === 'show') {
|
|
131
|
+
ctx.addMessage({
|
|
132
|
+
role: 'system',
|
|
133
|
+
content: `Spinner verbs:\n ${formatSpinnerVerbs(current)}\n\nUsage:\n /config spinner show\n /config spinner set <verb1, verb2, ...>\n /config spinner add <verb>\n /config spinner remove <verb>\n /config spinner reset\n /config spinner off`
|
|
134
|
+
});
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (action === 'reset') {
|
|
139
|
+
ctx.config.set('spinnerVerbs', DEFAULT_SPINNER_VERBS);
|
|
140
|
+
ctx.config.save();
|
|
141
|
+
ctx.addMessage({ role: 'system', content: '✓ Spinner verbs reset to defaults.' });
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (action === 'off') {
|
|
146
|
+
ctx.config.set('spinnerVerbs', []);
|
|
147
|
+
ctx.config.save();
|
|
148
|
+
ctx.addMessage({ role: 'system', content: '✓ Spinner verbs disabled.' });
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (action === 'set') {
|
|
153
|
+
const verbs = parseSpinnerVerbs(actionValue);
|
|
154
|
+
if (verbs.length === 0) {
|
|
155
|
+
ctx.addMessage({ role: 'system', content: 'Usage: /config spinner set <verb1, verb2, ...>' });
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
ctx.config.set('spinnerVerbs', verbs);
|
|
159
|
+
ctx.config.save();
|
|
160
|
+
ctx.addMessage({ role: 'system', content: `✓ Spinner verbs updated: ${formatSpinnerVerbs(verbs)}` });
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (action === 'add') {
|
|
165
|
+
const verbs = parseSpinnerVerbs(actionValue);
|
|
166
|
+
if (verbs.length === 0) {
|
|
167
|
+
ctx.addMessage({ role: 'system', content: 'Usage: /config spinner add <verb>' });
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
const next = [...current, ...verbs];
|
|
171
|
+
ctx.config.set('spinnerVerbs', next);
|
|
172
|
+
ctx.config.save();
|
|
173
|
+
ctx.addMessage({ role: 'system', content: `✓ Added spinner verbs: ${formatSpinnerVerbs(verbs)}` });
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (action === 'remove') {
|
|
178
|
+
const verbs = parseSpinnerVerbs(actionValue).map(v => v.toLowerCase());
|
|
179
|
+
if (verbs.length === 0) {
|
|
180
|
+
ctx.addMessage({ role: 'system', content: 'Usage: /config spinner remove <verb>' });
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
const next = current.filter(v => !verbs.includes(v.toLowerCase()));
|
|
184
|
+
ctx.config.set('spinnerVerbs', next);
|
|
185
|
+
ctx.config.save();
|
|
186
|
+
ctx.addMessage({ role: 'system', content: `✓ Removed spinner verbs: ${formatSpinnerVerbs(verbs)}` });
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
ctx.addMessage({
|
|
191
|
+
role: 'system',
|
|
192
|
+
content: 'Usage: /config spinner <show|set|add|remove|reset|off> [value]'
|
|
193
|
+
});
|
|
194
|
+
break;
|
|
195
|
+
}
|
|
196
|
+
|
|
123
197
|
default:
|
|
124
198
|
ctx.addMessage({
|
|
125
199
|
role: 'system',
|
|
126
|
-
content: 'Usage: /config <show|key|provider|endpoint|model> [value]\n\nExamples:\n /config show Show current config\n /config key sk-ant-... Set API key (current provider)\n /config key openai sk-... Set API key for provider\n /config provider openai Set provider\n /config endpoint https://api.example.com/v1 Set OpenAI-compatible base URL\n /config model claude-opus-4-20250514 Set model'
|
|
200
|
+
content: 'Usage: /config <show|key|provider|endpoint|model|spinner> [value]\n\nExamples:\n /config show Show current config\n /config key sk-ant-... Set API key (current provider)\n /config key openai sk-... Set API key for provider\n /config provider openai Set provider\n /config endpoint https://api.example.com/v1 Set OpenAI-compatible base URL\n /config model claude-opus-4-20250514 Set model\n /config spinner set Reticulating splines, Organizing thoughts'
|
|
127
201
|
});
|
|
128
202
|
}
|
|
129
203
|
}
|
|
@@ -14,6 +14,7 @@ import { skillsCommand } from './skills.js';
|
|
|
14
14
|
import { retryCommand } from './retry.js';
|
|
15
15
|
import { inputModeCommand } from './input_mode.js';
|
|
16
16
|
import { keybindingsCommand } from './keybindings.js';
|
|
17
|
+
import { updateCommand } from './update.js';
|
|
17
18
|
import { Command } from './types.js';
|
|
18
19
|
|
|
19
20
|
const commandList: Command[] = [];
|
|
@@ -35,6 +36,7 @@ commandList.push(
|
|
|
35
36
|
permissionsCommand,
|
|
36
37
|
skillsCommand,
|
|
37
38
|
keybindingsCommand,
|
|
39
|
+
updateCommand,
|
|
38
40
|
inputModeCommand,
|
|
39
41
|
retryCommand,
|
|
40
42
|
exitCommand
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Command } from './types.js';
|
|
2
|
+
import { getVersion } from '../../utils/version.js';
|
|
3
|
+
import { checkForUpdate } from '../../utils/update.js';
|
|
4
|
+
|
|
5
|
+
export const updateCommand: Command = {
|
|
6
|
+
name: 'update',
|
|
7
|
+
description: 'Check for updates and install the latest version',
|
|
8
|
+
handler: async (_args, ctx) => {
|
|
9
|
+
try {
|
|
10
|
+
ctx.addMessage({ role: 'system', content: 'Checking for updates...' });
|
|
11
|
+
const current = getVersion();
|
|
12
|
+
const info = await checkForUpdate(current);
|
|
13
|
+
if (!info.hasUpdate) {
|
|
14
|
+
ctx.addMessage({ role: 'system', content: `ZTC is up to date (v${current}).` });
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
ctx.addMessage({ role: 'system', content: `Updating to v${info.latest}...` });
|
|
18
|
+
const result = await ctx.shell.run('npm install -g zerg-ztc@latest');
|
|
19
|
+
if (result.exitCode === 0) {
|
|
20
|
+
ctx.addMessage({ role: 'system', content: `✓ Updated to v${info.latest}. Restart ZTC to use the new version.` });
|
|
21
|
+
} else {
|
|
22
|
+
ctx.addMessage({
|
|
23
|
+
role: 'system',
|
|
24
|
+
content: `Update failed (exit ${result.exitCode}).\n${result.stderr || result.stdout || ''}`.trim()
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
} catch (err) {
|
|
28
|
+
const message = err instanceof Error ? err.message : 'Update failed';
|
|
29
|
+
ctx.addMessage({ role: 'system', content: `Update failed: ${message}` });
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
};
|