zerg-ztc 0.1.3 → 0.1.5
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 +183 -19
- package/dist/App.js.map +1 -1
- package/dist/agent/agent.d.ts.map +1 -1
- package/dist/agent/agent.js +3 -1
- 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 +4 -1
- package/dist/agent/commands/index.js.map +1 -1
- package/dist/agent/commands/input_mode.d.ts +3 -0
- package/dist/agent/commands/input_mode.d.ts.map +1 -0
- package/dist/agent/commands/input_mode.js +21 -0
- package/dist/agent/commands/input_mode.js.map +1 -0
- package/dist/agent/commands/keybindings.d.ts +3 -0
- package/dist/agent/commands/keybindings.d.ts.map +1 -0
- package/dist/agent/commands/keybindings.js +38 -0
- package/dist/agent/commands/keybindings.js.map +1 -0
- package/dist/agent/commands/types.d.ts +2 -0
- package/dist/agent/commands/types.d.ts.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/cli.js +68 -16
- 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 +30 -30
- package/dist/components/FullScreen.js.map +1 -1
- package/dist/components/InputArea.d.ts.map +1 -1
- package/dist/components/InputArea.js +476 -19
- package/dist/components/InputArea.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 +2 -0
- package/dist/components/StatusBar.d.ts.map +1 -1
- package/dist/components/StatusBar.js +3 -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/types.d.ts +1 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/ui/core/input_segments.d.ts +1 -0
- package/dist/ui/core/input_segments.d.ts.map +1 -1
- package/dist/ui/core/input_segments.js +46 -14
- package/dist/ui/core/input_segments.js.map +1 -1
- package/dist/ui/core/types.d.ts +1 -0
- package/dist/ui/core/types.d.ts.map +1 -1
- package/dist/ui/ink/render.d.ts +3 -1
- package/dist/ui/ink/render.d.ts.map +1 -1
- package/dist/ui/ink/render.js +7 -5
- package/dist/ui/ink/render.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 -1
- package/dist/ui/views/app.d.ts.map +1 -1
- package/dist/ui/views/app.js +18 -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 +7 -5
- 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 +3 -1
- package/dist/ui/views/status_bar.d.ts.map +1 -1
- package/dist/ui/views/status_bar.js +8 -2
- package/dist/ui/views/status_bar.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 +226 -32
- package/src/agent/agent.ts +3 -1
- package/src/agent/commands/config.ts +76 -2
- package/src/agent/commands/index.ts +6 -0
- package/src/agent/commands/input_mode.ts +22 -0
- package/src/agent/commands/keybindings.ts +40 -0
- package/src/agent/commands/types.ts +2 -0
- package/src/agent/commands/update.ts +32 -0
- package/src/cli.tsx +77 -15
- package/src/components/ActivityLine.tsx +23 -0
- package/src/components/FullScreen.tsx +41 -35
- package/src/components/InputArea.tsx +489 -19
- package/src/components/MessageList.tsx +52 -6
- package/src/components/SingleMessage.tsx +59 -0
- package/src/components/StatusBar.tsx +6 -0
- package/src/components/index.tsx +3 -1
- package/src/config/types.ts +1 -0
- package/src/config.ts +8 -0
- package/src/types.ts +1 -0
- package/src/ui/core/input_segments.ts +49 -14
- package/src/ui/core/types.ts +1 -0
- package/src/ui/ink/render.tsx +16 -5
- package/src/ui/views/activity_line.ts +33 -0
- package/src/ui/views/app.ts +25 -13
- package/src/ui/views/header.ts +7 -5
- 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 +11 -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
|
-
import { Message, AgentState } from './types.js';
|
|
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
|
|
|
@@ -68,11 +73,17 @@ export const App: React.FC = () => {
|
|
|
68
73
|
|
|
69
74
|
// State
|
|
70
75
|
const [messages, setMessages] = useState<Message[]>([getWelcomeMessage()]);
|
|
76
|
+
const messagesRef = useRef<Message[]>(messages);
|
|
71
77
|
const [agentState, setAgentState] = useState<AgentState>({ status: 'idle' });
|
|
72
78
|
const [sessionId] = useState(generateId());
|
|
73
79
|
const [agent, setAgent] = useState<Agent | null>(createAgent);
|
|
74
80
|
const [expandToolOutputs, setExpandToolOutputs] = useState(false);
|
|
75
81
|
const [skills, setSkills] = useState<Skill[]>([]);
|
|
82
|
+
const [inputMode, setInputMode] = useState<InputMode>('queue');
|
|
83
|
+
const queueRef = useRef<string[]>([]);
|
|
84
|
+
const activeRunIdRef = useRef<string | null>(null);
|
|
85
|
+
const runCounterRef = useRef(0);
|
|
86
|
+
const lastRunDurationRef = useRef<number | null>(null);
|
|
76
87
|
|
|
77
88
|
React.useEffect(() => {
|
|
78
89
|
let active = true;
|
|
@@ -101,6 +112,8 @@ export const App: React.FC = () => {
|
|
|
101
112
|
const [retryAvailable, setRetryAvailable] = useState(false);
|
|
102
113
|
const [toast, setToast] = useState<string | null>(null);
|
|
103
114
|
const toastTimerRef = useRef<NodeJS.Timeout | null>(null);
|
|
115
|
+
const [spinnerLabel, setSpinnerLabel] = useState<string | null>(null);
|
|
116
|
+
const [spinnerFrame, setSpinnerFrame] = useState<string | null>(null);
|
|
104
117
|
const streamingMessageId = React.useRef<string | null>(null);
|
|
105
118
|
const streamedResponse = React.useRef(false);
|
|
106
119
|
const toolStartTimes = React.useRef<Map<string, number[]>>(new Map());
|
|
@@ -112,21 +125,80 @@ export const App: React.FC = () => {
|
|
|
112
125
|
);
|
|
113
126
|
const contextLength = agentState.contextTokens ?? fallbackContextLength;
|
|
114
127
|
const contextEstimated = agentState.contextTokens ? agentState.tokensEstimated : true;
|
|
128
|
+
const spinnerVerbs = configStore.get().spinnerVerbs || DEFAULT_SPINNER_VERBS;
|
|
129
|
+
const spinnerVerbsKey = spinnerVerbs.join('|');
|
|
115
130
|
|
|
116
131
|
React.useEffect(() => {
|
|
117
132
|
renderCount.current += 1;
|
|
118
133
|
debugLog(`App render #${renderCount.current} (messages=${messages.length}, state=${agentState.status})`);
|
|
119
134
|
});
|
|
135
|
+
React.useEffect(() => {
|
|
136
|
+
messagesRef.current = messages;
|
|
137
|
+
}, [messages]);
|
|
120
138
|
const [debug, setDebug] = useState(false);
|
|
139
|
+
const scrollback = process.env.ZTC_SCROLLBACK === '1' || process.env.ZTC_ALT_SCREEN !== '1';
|
|
140
|
+
|
|
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]);
|
|
121
155
|
|
|
122
|
-
|
|
123
|
-
|
|
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)
|
|
124
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]);
|
|
125
197
|
|
|
126
198
|
// Calculate content height (total - header - input - status)
|
|
127
199
|
const contentHeight = useMemo(
|
|
128
|
-
() => Math.max(rows - (headerHeight + inputHeight + statusHeight), 5),
|
|
129
|
-
[rows]
|
|
200
|
+
() => scrollback ? undefined : Math.max(rows - (headerHeight + inputHeight + statusHeight), 5),
|
|
201
|
+
[rows, scrollback, inputHeight]
|
|
130
202
|
);
|
|
131
203
|
|
|
132
204
|
// Reload agent when config changes
|
|
@@ -172,6 +244,21 @@ export const App: React.FC = () => {
|
|
|
172
244
|
}]);
|
|
173
245
|
}, []);
|
|
174
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
|
+
|
|
175
262
|
const clearMessages = useCallback(() => {
|
|
176
263
|
setMessages([]);
|
|
177
264
|
}, []);
|
|
@@ -193,15 +280,22 @@ export const App: React.FC = () => {
|
|
|
193
280
|
return lower.includes('overloaded') || lower.includes('529') || lower.includes('429') || lower.includes('rate limit');
|
|
194
281
|
}, []);
|
|
195
282
|
|
|
196
|
-
const runWithRetry = useCallback(async (
|
|
283
|
+
const runWithRetry = useCallback(async (
|
|
284
|
+
requestMessages: Message[],
|
|
285
|
+
runAgent: Agent,
|
|
286
|
+
isManual = false,
|
|
287
|
+
isActive?: () => boolean
|
|
288
|
+
) => {
|
|
197
289
|
const maxRetries = 3;
|
|
198
290
|
let attempt = 0;
|
|
199
291
|
setRetryAvailable(false);
|
|
200
292
|
while (attempt < maxRetries) {
|
|
201
293
|
attempt += 1;
|
|
294
|
+
if (isActive && !isActive()) return;
|
|
202
295
|
try {
|
|
203
296
|
streamedResponse.current = false;
|
|
204
297
|
const result = await runAgent.run(requestMessages);
|
|
298
|
+
if (isActive && !isActive()) return;
|
|
205
299
|
if (!streamedResponse.current) {
|
|
206
300
|
addMessage({
|
|
207
301
|
role: 'assistant',
|
|
@@ -220,6 +314,7 @@ export const App: React.FC = () => {
|
|
|
220
314
|
return;
|
|
221
315
|
} catch (err) {
|
|
222
316
|
const errorMsg = (err as Error).message || 'Agent error';
|
|
317
|
+
if (isActive && !isActive()) return;
|
|
223
318
|
if (!isRetryableError(errorMsg) || attempt >= maxRetries) {
|
|
224
319
|
throw err;
|
|
225
320
|
}
|
|
@@ -242,9 +337,14 @@ export const App: React.FC = () => {
|
|
|
242
337
|
addMessage({ role: 'system', content: 'No previous request to retry.' });
|
|
243
338
|
return;
|
|
244
339
|
}
|
|
340
|
+
const runId = `${Date.now()}_${runCounterRef.current++}`;
|
|
341
|
+
activeRunIdRef.current = runId;
|
|
342
|
+
const isActive = () => activeRunIdRef.current === runId;
|
|
343
|
+
const runStartedAt = Date.now();
|
|
245
344
|
setAgentState({ status: 'thinking', startedAt: new Date() });
|
|
246
|
-
void runWithRetry(last.messages, last.agent, true).catch((err) => {
|
|
345
|
+
void runWithRetry(last.messages, last.agent, true, isActive).catch((err) => {
|
|
247
346
|
const message = (err as Error).message || 'Agent error';
|
|
347
|
+
if (!isActive()) return;
|
|
248
348
|
addMessage({ role: 'system', content: `Error: ${message}` });
|
|
249
349
|
if (isRetryableError(message)) {
|
|
250
350
|
addMessage({ role: 'system', content: 'Retries exhausted. Use /retry to try again.' });
|
|
@@ -282,8 +382,10 @@ export const App: React.FC = () => {
|
|
|
282
382
|
},
|
|
283
383
|
skills: {
|
|
284
384
|
list: async () => getSkillRegistry()
|
|
285
|
-
}
|
|
286
|
-
|
|
385
|
+
},
|
|
386
|
+
getInputMode: () => inputMode,
|
|
387
|
+
setInputMode: (mode) => setInputMode(mode)
|
|
388
|
+
}), [addMessage, clearMessages, getMessages, reloadAgent, exit, shellController, retryLast, inputMode]);
|
|
287
389
|
|
|
288
390
|
// Handle commands
|
|
289
391
|
const handleCommand = useCallback((cmd: string, args: string[]) => {
|
|
@@ -322,6 +424,22 @@ export const App: React.FC = () => {
|
|
|
322
424
|
return;
|
|
323
425
|
}
|
|
324
426
|
|
|
427
|
+
const busy = agentState.status !== 'idle' && agentState.status !== 'error';
|
|
428
|
+
if (busy) {
|
|
429
|
+
if (inputMode === 'queue') {
|
|
430
|
+
queueRef.current.push(text);
|
|
431
|
+
addMessage({
|
|
432
|
+
role: 'system',
|
|
433
|
+
content: `Queued (${queueRef.current.length})`
|
|
434
|
+
});
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
addMessage({
|
|
438
|
+
role: 'system',
|
|
439
|
+
content: 'Interrupting current response...'
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
|
|
325
443
|
let currentAgent = agent;
|
|
326
444
|
|
|
327
445
|
if (skills.length > 0) {
|
|
@@ -373,12 +491,19 @@ export const App: React.FC = () => {
|
|
|
373
491
|
timestamp: new Date()
|
|
374
492
|
};
|
|
375
493
|
setMessages(prev => [...prev, userMsg]);
|
|
376
|
-
const requestMessages = [...
|
|
494
|
+
const requestMessages = [...messagesRef.current, userMsg];
|
|
377
495
|
lastRequestRef.current = { messages: requestMessages, agent: currentAgent };
|
|
378
496
|
|
|
497
|
+
const runId = `${Date.now()}_${runCounterRef.current++}`;
|
|
498
|
+
activeRunIdRef.current = runId;
|
|
499
|
+
const isActive = () => activeRunIdRef.current === runId;
|
|
500
|
+
const runStartedAt = Date.now();
|
|
379
501
|
setAgentState({ status: 'thinking', startedAt: new Date() });
|
|
502
|
+
streamingMessageId.current = null;
|
|
503
|
+
streamedResponse.current = false;
|
|
380
504
|
|
|
381
505
|
const cleanup = currentAgent.on((event) => {
|
|
506
|
+
if (!isActive()) return;
|
|
382
507
|
switch (event.type) {
|
|
383
508
|
case 'thinking_start':
|
|
384
509
|
setAgentState(s => ({ ...s, status: 'thinking' }));
|
|
@@ -436,7 +561,7 @@ export const App: React.FC = () => {
|
|
|
436
561
|
toolStartTimes.current.set(key, bucket);
|
|
437
562
|
const duration = started ? Date.now() - started : undefined;
|
|
438
563
|
const emulationId = configStore.getEmulationId();
|
|
439
|
-
if (getTraceStyle(emulationId) === 'claude_code') {
|
|
564
|
+
if (getTraceStyle(emulationId) === 'claude_code' || getTraceStyle(emulationId) === 'codex') {
|
|
440
565
|
const output = buildToolOutputMessage(event.tool, event.result, duration, emulationId);
|
|
441
566
|
addMessage({
|
|
442
567
|
role: 'tool',
|
|
@@ -475,9 +600,10 @@ export const App: React.FC = () => {
|
|
|
475
600
|
});
|
|
476
601
|
|
|
477
602
|
try {
|
|
478
|
-
await runWithRetry(requestMessages, currentAgent);
|
|
603
|
+
await runWithRetry(requestMessages, currentAgent, false, isActive);
|
|
479
604
|
} catch (err) {
|
|
480
605
|
const errorMsg = (err as Error).message;
|
|
606
|
+
if (!isActive()) return;
|
|
481
607
|
|
|
482
608
|
const isAuthError = errorMsg.includes('401') || errorMsg.includes('authentication');
|
|
483
609
|
if (isAuthError) {
|
|
@@ -504,11 +630,30 @@ export const App: React.FC = () => {
|
|
|
504
630
|
setAgentState({ status: 'idle' });
|
|
505
631
|
}, 3000);
|
|
506
632
|
} finally {
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
633
|
+
if (isActive()) {
|
|
634
|
+
streamedResponse.current = false;
|
|
635
|
+
streamingMessageId.current = null;
|
|
636
|
+
activeRunIdRef.current = null;
|
|
637
|
+
lastRunDurationRef.current = Date.now() - runStartedAt;
|
|
638
|
+
if (getTraceStyle(configStore.getEmulationId()) === 'codex') {
|
|
639
|
+
const seconds = Math.max(0, Math.round((lastRunDurationRef.current || 0) / 1000));
|
|
640
|
+
addMessage({
|
|
641
|
+
role: 'tool',
|
|
642
|
+
content: `✻ Worked for ${seconds}s`
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
cleanup();
|
|
646
|
+
if (inputMode === 'queue' && queueRef.current.length > 0) {
|
|
647
|
+
const next = queueRef.current.shift();
|
|
648
|
+
if (next) {
|
|
649
|
+
void handleSubmit(next);
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
} else {
|
|
653
|
+
cleanup();
|
|
654
|
+
}
|
|
510
655
|
}
|
|
511
|
-
}, [
|
|
656
|
+
}, [agent, agentState.status, addMessage, handleCommand, inputMode, isRetryableError, runWithRetry]);
|
|
512
657
|
|
|
513
658
|
const layoutTree = useMemo(() => {
|
|
514
659
|
if (!mirrorEnabled) return null;
|
|
@@ -521,16 +666,20 @@ export const App: React.FC = () => {
|
|
|
521
666
|
rows,
|
|
522
667
|
commands,
|
|
523
668
|
hasApiKey: configStore.hasApiKey(),
|
|
669
|
+
version: getVersion(),
|
|
524
670
|
contextLength,
|
|
525
671
|
contextEstimated,
|
|
526
672
|
provider,
|
|
527
673
|
model,
|
|
528
674
|
emulationId,
|
|
675
|
+
inputMode,
|
|
529
676
|
toast,
|
|
677
|
+
spinnerLabel,
|
|
678
|
+
spinnerFrame,
|
|
530
679
|
debug,
|
|
531
680
|
expandToolOutputs
|
|
532
681
|
});
|
|
533
|
-
}, [mirrorEnabled, messages, agentState, inputSnapshot, sessionId, rows, contextLength, contextEstimated, provider, model, emulationId, toast, debug, expandToolOutputs]);
|
|
682
|
+
}, [mirrorEnabled, messages, agentState, inputSnapshot, sessionId, rows, contextLength, contextEstimated, provider, model, emulationId, inputMode, toast, spinnerLabel, spinnerFrame, debug, expandToolOutputs]);
|
|
534
683
|
|
|
535
684
|
const showToast = useCallback((message: string) => {
|
|
536
685
|
setToast(message);
|
|
@@ -544,26 +693,70 @@ export const App: React.FC = () => {
|
|
|
544
693
|
|
|
545
694
|
useMirror(layoutTree, inputBus);
|
|
546
695
|
|
|
696
|
+
// Compute messages for Static (completed) vs live (streaming)
|
|
697
|
+
// Static component handles deduplication by key - it only renders new items
|
|
698
|
+
const { staticMessages, streamingMessage } = useMemo(() => {
|
|
699
|
+
if (!scrollback) {
|
|
700
|
+
return { staticMessages: [], streamingMessage: null };
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
// Completed messages go to Static (rendered once, kept in scrollback)
|
|
704
|
+
const completed = messages.filter(m => !m.isStreaming);
|
|
705
|
+
// Streaming message stays in live section (can update)
|
|
706
|
+
const streaming = messages.find(m => m.isStreaming) || null;
|
|
707
|
+
|
|
708
|
+
return {
|
|
709
|
+
staticMessages: completed,
|
|
710
|
+
streamingMessage: streaming
|
|
711
|
+
};
|
|
712
|
+
}, [messages, scrollback]);
|
|
713
|
+
|
|
547
714
|
return (
|
|
548
|
-
<FullScreen debug={debug}>
|
|
549
|
-
<Header version=
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
715
|
+
<FullScreen debug={debug} scrollback={scrollback}>
|
|
716
|
+
{!scrollback && <Header version={getVersion()} debug={debug} />}
|
|
717
|
+
|
|
718
|
+
{scrollback ? (
|
|
719
|
+
<Box flexDirection="column">
|
|
720
|
+
{/* Static: each message rendered ONCE, stays in scrollback buffer */}
|
|
721
|
+
<Static items={staticMessages}>
|
|
722
|
+
{(msg: Message) => (
|
|
723
|
+
<Box key={msg.id} flexDirection="column">
|
|
724
|
+
<SingleMessage message={msg} expandToolOutputs={expandToolOutputs} />
|
|
725
|
+
</Box>
|
|
726
|
+
)}
|
|
727
|
+
</Static>
|
|
728
|
+
|
|
729
|
+
{/* Live section: streaming message can update */}
|
|
730
|
+
{streamingMessage && (
|
|
731
|
+
<SingleMessage message={streamingMessage} expandToolOutputs={expandToolOutputs} />
|
|
732
|
+
)}
|
|
733
|
+
</Box>
|
|
734
|
+
) : (
|
|
735
|
+
<MessageList
|
|
736
|
+
messages={messages}
|
|
737
|
+
height={contentHeight}
|
|
738
|
+
debug={debug}
|
|
739
|
+
expandToolOutputs={expandToolOutputs}
|
|
740
|
+
scrollback={scrollback}
|
|
741
|
+
/>
|
|
742
|
+
)}
|
|
743
|
+
|
|
744
|
+
<ActivityLine
|
|
745
|
+
state={agentState}
|
|
746
|
+
spinnerLabel={spinnerLabel}
|
|
747
|
+
spinnerFrame={spinnerFrame}
|
|
748
|
+
inputMode={inputMode}
|
|
556
749
|
/>
|
|
557
|
-
|
|
750
|
+
|
|
558
751
|
<InputArea
|
|
559
752
|
onSubmit={handleSubmit}
|
|
560
753
|
onCommand={handleCommand}
|
|
561
754
|
commands={commands}
|
|
562
|
-
onStateChange={
|
|
755
|
+
onStateChange={setInputSnapshot}
|
|
563
756
|
onToast={showToast}
|
|
564
757
|
cols={columns}
|
|
565
758
|
inputBus={inputBus}
|
|
566
|
-
disabled={
|
|
759
|
+
disabled={false}
|
|
567
760
|
debug={debug}
|
|
568
761
|
placeholder={
|
|
569
762
|
!configStore.hasApiKey() ? 'Set API key with /config key <key>' :
|
|
@@ -573,17 +766,18 @@ export const App: React.FC = () => {
|
|
|
573
766
|
'Type a message or /help for commands...'
|
|
574
767
|
}
|
|
575
768
|
/>
|
|
576
|
-
|
|
769
|
+
|
|
577
770
|
<StatusBar
|
|
578
771
|
state={agentState}
|
|
579
772
|
sessionId={sessionId}
|
|
580
|
-
version=
|
|
773
|
+
version={getVersion()}
|
|
581
774
|
connectionStatus={configStore.hasApiKey() ? 'connected' : 'disconnected'}
|
|
582
775
|
contextLength={contextLength}
|
|
583
776
|
contextEstimated={contextEstimated}
|
|
584
777
|
provider={provider}
|
|
585
778
|
model={model}
|
|
586
779
|
emulationId={emulationId}
|
|
780
|
+
inputMode={inputMode}
|
|
587
781
|
toast={toast}
|
|
588
782
|
debug={debug}
|
|
589
783
|
/>
|
package/src/agent/agent.ts
CHANGED
|
@@ -77,7 +77,9 @@ You have access to tools for:
|
|
|
77
77
|
- Running shell commands
|
|
78
78
|
- Querying the Zerg system
|
|
79
79
|
|
|
80
|
-
Be concise and helpful. When using tools, explain what you're doing briefly. If a task requires multiple steps, proceed through them systematically
|
|
80
|
+
Be concise and helpful. When using tools, explain what you're doing briefly. If a task requires multiple steps, proceed through them systematically.
|
|
81
|
+
|
|
82
|
+
When a user intent maps to an available slash command, invoke the command directly (just the command) instead of explaining how to do it. Prefer executing commands over describing them.`;
|
|
81
83
|
}
|
|
82
84
|
|
|
83
85
|
// Event handling
|
|
@@ -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
|
}
|
|
@@ -12,6 +12,9 @@ import { modelCommand } from './model.js';
|
|
|
12
12
|
import { permissionsCommand } from './permissions.js';
|
|
13
13
|
import { skillsCommand } from './skills.js';
|
|
14
14
|
import { retryCommand } from './retry.js';
|
|
15
|
+
import { inputModeCommand } from './input_mode.js';
|
|
16
|
+
import { keybindingsCommand } from './keybindings.js';
|
|
17
|
+
import { updateCommand } from './update.js';
|
|
15
18
|
import { Command } from './types.js';
|
|
16
19
|
|
|
17
20
|
const commandList: Command[] = [];
|
|
@@ -32,6 +35,9 @@ commandList.push(
|
|
|
32
35
|
modelCommand,
|
|
33
36
|
permissionsCommand,
|
|
34
37
|
skillsCommand,
|
|
38
|
+
keybindingsCommand,
|
|
39
|
+
updateCommand,
|
|
40
|
+
inputModeCommand,
|
|
35
41
|
retryCommand,
|
|
36
42
|
exitCommand
|
|
37
43
|
);
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Command } from './types.js';
|
|
2
|
+
|
|
3
|
+
export const inputModeCommand: Command = {
|
|
4
|
+
name: 'inputmode',
|
|
5
|
+
description: 'Set input mode while an agent is running',
|
|
6
|
+
usage: '<queue|interrupt>',
|
|
7
|
+
handler: (args, ctx) => {
|
|
8
|
+
const mode = (args[0] || '').toLowerCase();
|
|
9
|
+
if (mode !== 'queue' && mode !== 'interrupt') {
|
|
10
|
+
ctx.addMessage({
|
|
11
|
+
role: 'system',
|
|
12
|
+
content: `Current input mode: ${ctx.getInputMode()}\n\nUsage: /inputmode <queue|interrupt>`
|
|
13
|
+
});
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
ctx.setInputMode(mode);
|
|
17
|
+
ctx.addMessage({
|
|
18
|
+
role: 'system',
|
|
19
|
+
content: `✓ Input mode set: ${mode}`
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { Command } from './types.js';
|
|
2
|
+
|
|
3
|
+
const lines = [
|
|
4
|
+
'Keybindings (readline-style):',
|
|
5
|
+
'',
|
|
6
|
+
'Movement:',
|
|
7
|
+
' Ctrl+A start of line',
|
|
8
|
+
' Ctrl+E end of line',
|
|
9
|
+
' Ctrl+B move left',
|
|
10
|
+
' Ctrl+F move right',
|
|
11
|
+
' Alt+B word left',
|
|
12
|
+
' Alt+F word right',
|
|
13
|
+
'',
|
|
14
|
+
'Editing:',
|
|
15
|
+
' Ctrl+U kill to start',
|
|
16
|
+
' Ctrl+K kill to end',
|
|
17
|
+
' Ctrl+W kill previous word',
|
|
18
|
+
' Alt+D kill next word',
|
|
19
|
+
' Ctrl+D delete forward',
|
|
20
|
+
' Ctrl+Y yank',
|
|
21
|
+
' Alt+Y yank-pop',
|
|
22
|
+
' Ctrl+T transpose chars',
|
|
23
|
+
' Alt+T transpose words',
|
|
24
|
+
'',
|
|
25
|
+
'History:',
|
|
26
|
+
' Ctrl+P previous',
|
|
27
|
+
' Ctrl+N next',
|
|
28
|
+
' Up/Down arrows also work',
|
|
29
|
+
'',
|
|
30
|
+
'Input mode:',
|
|
31
|
+
' /inputmode queue|interrupt'
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
export const keybindingsCommand: Command = {
|
|
35
|
+
name: 'keybindings',
|
|
36
|
+
description: 'Show keybindings',
|
|
37
|
+
handler: (_args, ctx) => {
|
|
38
|
+
ctx.addMessage({ role: 'system', content: lines.join('\n') });
|
|
39
|
+
}
|
|
40
|
+
};
|
|
@@ -68,6 +68,8 @@ export interface CommandContext {
|
|
|
68
68
|
clipboard: ClipboardController;
|
|
69
69
|
models: ModelsController;
|
|
70
70
|
skills: SkillsController;
|
|
71
|
+
getInputMode: () => 'queue' | 'interrupt';
|
|
72
|
+
setInputMode: (mode: 'queue' | 'interrupt') => void;
|
|
71
73
|
}
|
|
72
74
|
|
|
73
75
|
export interface Command {
|