zerg-ztc 0.1.11 → 0.1.13
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/bin/ztc-audio-darwin-arm64 +0 -0
- package/dist/utils/dictation_native.d.ts.map +1 -1
- package/dist/utils/dictation_native.js +43 -23
- package/dist/utils/dictation_native.js.map +1 -1
- package/package.json +5 -4
- package/packages/ztc-dictation/Cargo.toml +0 -43
- package/packages/ztc-dictation/README.md +0 -65
- package/packages/ztc-dictation/index.d.ts +0 -16
- package/packages/ztc-dictation/index.js +0 -74
- package/packages/ztc-dictation/package.json +0 -41
- package/packages/ztc-dictation/src/main.rs +0 -430
- package/src/App.tsx +0 -910
- package/src/agent/agent.ts +0 -534
- package/src/agent/backends/anthropic.ts +0 -86
- package/src/agent/backends/gemini.ts +0 -119
- package/src/agent/backends/inception.ts +0 -23
- package/src/agent/backends/index.ts +0 -17
- package/src/agent/backends/openai.ts +0 -23
- package/src/agent/backends/openai_compatible.ts +0 -143
- package/src/agent/backends/types.ts +0 -83
- package/src/agent/commands/clipboard.ts +0 -77
- package/src/agent/commands/config.ts +0 -204
- package/src/agent/commands/debug.ts +0 -23
- package/src/agent/commands/dictation.ts +0 -11
- package/src/agent/commands/emulation.ts +0 -80
- package/src/agent/commands/execution.ts +0 -9
- package/src/agent/commands/help.ts +0 -20
- package/src/agent/commands/history.ts +0 -13
- package/src/agent/commands/index.ts +0 -48
- package/src/agent/commands/input_mode.ts +0 -22
- package/src/agent/commands/keybindings.ts +0 -40
- package/src/agent/commands/model.ts +0 -11
- package/src/agent/commands/models.ts +0 -116
- package/src/agent/commands/permissions.ts +0 -64
- package/src/agent/commands/retry.ts +0 -9
- package/src/agent/commands/shell.ts +0 -68
- package/src/agent/commands/skills.ts +0 -54
- package/src/agent/commands/status.ts +0 -19
- package/src/agent/commands/types.ts +0 -88
- package/src/agent/commands/update.ts +0 -32
- package/src/agent/factory.ts +0 -60
- package/src/agent/index.ts +0 -20
- package/src/agent/runtime/capabilities.ts +0 -7
- package/src/agent/runtime/memory.ts +0 -23
- package/src/agent/runtime/policy.ts +0 -48
- package/src/agent/runtime/session.ts +0 -18
- package/src/agent/runtime/tracing.ts +0 -23
- package/src/agent/tools/file.ts +0 -178
- package/src/agent/tools/index.ts +0 -52
- package/src/agent/tools/screenshot.ts +0 -821
- package/src/agent/tools/search.ts +0 -138
- package/src/agent/tools/shell.ts +0 -69
- package/src/agent/tools/skills.ts +0 -28
- package/src/agent/tools/types.ts +0 -14
- package/src/agent/tools/zerg.ts +0 -50
- package/src/cli.tsx +0 -163
- package/src/components/ActivityLine.tsx +0 -23
- package/src/components/FullScreen.tsx +0 -79
- package/src/components/Header.tsx +0 -27
- package/src/components/InputArea.tsx +0 -1660
- package/src/components/MessageList.tsx +0 -71
- package/src/components/SingleMessage.tsx +0 -298
- package/src/components/StatusBar.tsx +0 -55
- package/src/components/index.tsx +0 -8
- package/src/config/types.ts +0 -19
- package/src/config.ts +0 -186
- package/src/debug/logger.ts +0 -14
- package/src/emulation/README.md +0 -24
- package/src/emulation/catalog.ts +0 -82
- package/src/emulation/trace_style.ts +0 -8
- package/src/emulation/types.ts +0 -7
- package/src/skills/index.ts +0 -36
- package/src/skills/loader.ts +0 -135
- package/src/skills/registry.ts +0 -6
- package/src/skills/types.ts +0 -10
- package/src/types.ts +0 -84
- package/src/ui/README.md +0 -44
- package/src/ui/core/factory.ts +0 -9
- package/src/ui/core/index.ts +0 -4
- package/src/ui/core/input.ts +0 -38
- package/src/ui/core/input_segments.ts +0 -410
- package/src/ui/core/input_state.ts +0 -17
- package/src/ui/core/layout_yoga.ts +0 -122
- package/src/ui/core/style.ts +0 -38
- package/src/ui/core/types.ts +0 -54
- package/src/ui/ink/index.tsx +0 -1
- package/src/ui/ink/render.tsx +0 -60
- package/src/ui/views/activity_line.ts +0 -33
- package/src/ui/views/app.ts +0 -111
- package/src/ui/views/header.ts +0 -44
- package/src/ui/views/input_area.ts +0 -255
- package/src/ui/views/message_list.ts +0 -443
- package/src/ui/views/status_bar.ts +0 -114
- package/src/ui/vue/index.ts +0 -53
- package/src/ui/web/frame_render.tsx +0 -148
- package/src/ui/web/index.tsx +0 -1
- package/src/ui/web/render.tsx +0 -41
- package/src/utils/clipboard.ts +0 -39
- package/src/utils/clipboard_image.ts +0 -40
- package/src/utils/dictation.ts +0 -467
- package/src/utils/dictation_native.ts +0 -258
- package/src/utils/diff.ts +0 -52
- package/src/utils/image_preview.ts +0 -36
- package/src/utils/models.ts +0 -98
- package/src/utils/path_complete.ts +0 -173
- package/src/utils/path_format.ts +0 -99
- package/src/utils/shell.ts +0 -72
- package/src/utils/spinner_frames.ts +0 -1
- package/src/utils/spinner_verbs.ts +0 -23
- package/src/utils/table.ts +0 -171
- package/src/utils/tool_summary.ts +0 -56
- package/src/utils/tool_trace.ts +0 -346
- package/src/utils/update.ts +0 -44
- package/src/utils/version.ts +0 -15
- package/src/web/index.html +0 -352
- package/src/web/mirror-favicon.svg +0 -4
- package/src/web/mirror.html +0 -641
- package/src/web/mirror_hook.ts +0 -25
- package/src/web/mirror_server.ts +0 -204
- package/tsconfig.json +0 -22
- package/vite.config.ts +0 -363
- /package/{packages/ztc-dictation/bin → bin}/.gitkeep +0 -0
package/src/App.tsx
DELETED
|
@@ -1,910 +0,0 @@
|
|
|
1
|
-
import React, { useState, useCallback, useMemo, useRef } from 'react';
|
|
2
|
-
import { Box, Text, useApp, useInput, Static } from 'ink';
|
|
3
|
-
import { Header, MessageList, SingleMessage, InputArea, StatusBar, FullScreen, ActivityLine, useScreenSize } from './components/index.js';
|
|
4
|
-
import { buildAppView } from './ui/views/app.js';
|
|
5
|
-
import { useMirror } from './web/mirror_hook.js';
|
|
6
|
-
import { Agent } from './agent/index.js';
|
|
7
|
-
import { commands, type CommandContext } from './agent/commands/index.js';
|
|
8
|
-
import { Message, AgentState, InputMode } from './types.js';
|
|
9
|
-
import { InputState } from './ui/core/input_state.js';
|
|
10
|
-
import { estimateInputLines } from './ui/views/input_area.js';
|
|
11
|
-
import { configStore } from './config.js';
|
|
12
|
-
import { createInputBus } from './ui/core/input.js';
|
|
13
|
-
import { loadEmulationProfiles, getEmulationProfile } from './emulation/catalog.js';
|
|
14
|
-
import { debugLog } from './debug/logger.js';
|
|
15
|
-
import { runShellCommand, resolveWorkingDir } from './utils/shell.js';
|
|
16
|
-
import { writeClipboard } from './utils/clipboard.js';
|
|
17
|
-
import { listModels } from './utils/models.js';
|
|
18
|
-
import { formatToolStart, formatToolEnd, formatToolError, buildToolOutputMessage } from './utils/tool_trace.js';
|
|
19
|
-
import { getTraceStyle } from './emulation/trace_style.js';
|
|
20
|
-
import { autoActivateSkills, buildSkillPrompt } from './skills/index.js';
|
|
21
|
-
import { getSkillRegistry } from './skills/registry.js';
|
|
22
|
-
import { Skill } from './skills/types.js';
|
|
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';
|
|
28
|
-
import { getRecordingDeviceName } from './utils/dictation.js';
|
|
29
|
-
import {
|
|
30
|
-
isNativeDictationAvailable,
|
|
31
|
-
isNativeRecording,
|
|
32
|
-
onDictationEvent,
|
|
33
|
-
startNativeRecording,
|
|
34
|
-
stopNativeRecording,
|
|
35
|
-
cancelNativeRecording,
|
|
36
|
-
DictationEvent
|
|
37
|
-
} from './utils/dictation_native.js';
|
|
38
|
-
|
|
39
|
-
// --- Utilities ---
|
|
40
|
-
|
|
41
|
-
const generateId = () => Math.random().toString(36).slice(2, 11);
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Render a VU meter from dB level
|
|
45
|
-
* @param db Audio level in dB (-60 to 0)
|
|
46
|
-
* @returns ASCII VU meter string
|
|
47
|
-
*/
|
|
48
|
-
function renderVUMeter(db: number): string {
|
|
49
|
-
// Map -60dB..0dB to 0..10 bars
|
|
50
|
-
const normalized = Math.max(0, Math.min(1, (db + 60) / 60));
|
|
51
|
-
const bars = Math.round(normalized * 10);
|
|
52
|
-
const filled = '█'.repeat(bars);
|
|
53
|
-
const empty = '░'.repeat(10 - bars);
|
|
54
|
-
return `[${filled}${empty}]`;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// --- Initial welcome message ---
|
|
58
|
-
|
|
59
|
-
function getWelcomeMessage(): Message {
|
|
60
|
-
const hasKey = configStore.hasApiKey();
|
|
61
|
-
return {
|
|
62
|
-
id: generateId(),
|
|
63
|
-
role: 'system',
|
|
64
|
-
content: hasKey
|
|
65
|
-
? 'Welcome to ZTC - Zerg Terminal Client.\nType a message to begin, or /help for commands.'
|
|
66
|
-
: 'Welcome to ZTC - Zerg Terminal Client.\n\n⚠️ No API key configured.\n\nSet your Anthropic API key with:\n /config key sk-ant-your-key-here\n\nOr set the ANTHROPIC_API_KEY environment variable.',
|
|
67
|
-
timestamp: new Date()
|
|
68
|
-
};
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// --- Create agent if possible ---
|
|
72
|
-
|
|
73
|
-
function createAgent(): Agent | null {
|
|
74
|
-
if (!configStore.hasApiKey()) return null;
|
|
75
|
-
const provider = configStore.getProvider();
|
|
76
|
-
const apiKey = configStore.getApiKey(provider);
|
|
77
|
-
return createAgentFromConfig({
|
|
78
|
-
config: configStore.get(),
|
|
79
|
-
provider,
|
|
80
|
-
apiKey,
|
|
81
|
-
openaiCompatibleBaseUrl: configStore.getOpenAICompatibleBaseUrl(),
|
|
82
|
-
emulationId: configStore.getEmulationId()
|
|
83
|
-
});
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// --- Main App ---
|
|
87
|
-
|
|
88
|
-
export const App: React.FC = () => {
|
|
89
|
-
const renderCount = React.useRef(0);
|
|
90
|
-
const { exit } = useApp();
|
|
91
|
-
const { rows, columns } = useScreenSize();
|
|
92
|
-
const mirrorEnabled = process.env.ZTC_WEB_MIRROR === '1';
|
|
93
|
-
const provider = configStore.getProvider();
|
|
94
|
-
const model = configStore.get().model;
|
|
95
|
-
const emulationId = configStore.getEmulationId();
|
|
96
|
-
const shellCwdRef = React.useRef(process.cwd());
|
|
97
|
-
|
|
98
|
-
// State
|
|
99
|
-
const [messages, setMessages] = useState<Message[]>([getWelcomeMessage()]);
|
|
100
|
-
const messagesRef = useRef<Message[]>(messages);
|
|
101
|
-
const [agentState, setAgentState] = useState<AgentState>({ status: 'idle' });
|
|
102
|
-
const [sessionId] = useState(generateId());
|
|
103
|
-
const [agent, setAgent] = useState<Agent | null>(createAgent);
|
|
104
|
-
const [expandToolOutputs, setExpandToolOutputs] = useState(false);
|
|
105
|
-
const [skills, setSkills] = useState<Skill[]>([]);
|
|
106
|
-
const [inputMode, setInputMode] = useState<InputMode>('queue');
|
|
107
|
-
const queueRef = useRef<string[]>([]);
|
|
108
|
-
const activeRunIdRef = useRef<string | null>(null);
|
|
109
|
-
const runCounterRef = useRef(0);
|
|
110
|
-
const lastRunDurationRef = useRef<number | null>(null);
|
|
111
|
-
|
|
112
|
-
React.useEffect(() => {
|
|
113
|
-
let active = true;
|
|
114
|
-
configStore.load(true).then(() => {
|
|
115
|
-
if (!active) return;
|
|
116
|
-
setMessages(prev => {
|
|
117
|
-
if (
|
|
118
|
-
prev.length === 1 &&
|
|
119
|
-
prev[0]?.role === 'system' &&
|
|
120
|
-
prev[0].content.startsWith('Welcome to ZTC')
|
|
121
|
-
) {
|
|
122
|
-
return [getWelcomeMessage()];
|
|
123
|
-
}
|
|
124
|
-
return prev;
|
|
125
|
-
});
|
|
126
|
-
}).catch(() => {});
|
|
127
|
-
return () => { active = false; };
|
|
128
|
-
}, []);
|
|
129
|
-
const [inputSnapshot, setInputSnapshot] = useState<InputState>({
|
|
130
|
-
segments: [],
|
|
131
|
-
cursor: { index: 0, offset: 0 },
|
|
132
|
-
history: [],
|
|
133
|
-
historyIdx: -1
|
|
134
|
-
});
|
|
135
|
-
const lastRequestRef = useRef<{ messages: Message[]; agent: Agent } | null>(null);
|
|
136
|
-
const [retryAvailable, setRetryAvailable] = useState(false);
|
|
137
|
-
const [toast, setToast] = useState<string | null>(null);
|
|
138
|
-
const toastTimerRef = useRef<NodeJS.Timeout | null>(null);
|
|
139
|
-
const [isRecording, setIsRecording] = useState(false);
|
|
140
|
-
const [recordingDevice, setRecordingDevice] = useState<string | null>(null);
|
|
141
|
-
const [isTranscribing, setIsTranscribing] = useState(false);
|
|
142
|
-
const [audioLevel, setAudioLevel] = useState<number>(-60); // dB
|
|
143
|
-
const [partialTranscript, setPartialTranscript] = useState<string>('');
|
|
144
|
-
const [useNativeDictation] = useState(isNativeDictationAvailable());
|
|
145
|
-
const [spinnerLabel, setSpinnerLabel] = useState<string | null>(null);
|
|
146
|
-
const [spinnerFrame, setSpinnerFrame] = useState<string | null>(null);
|
|
147
|
-
const streamingMessageId = React.useRef<string | null>(null);
|
|
148
|
-
const streamedResponse = React.useRef(false);
|
|
149
|
-
const toolStartTimes = React.useRef<Map<string, number[]>>(new Map());
|
|
150
|
-
const activeSkillIds = React.useRef<string[]>([]);
|
|
151
|
-
const inputBus = useMemo(() => createInputBus(), []);
|
|
152
|
-
const fallbackContextLength = useMemo(
|
|
153
|
-
() => messages.reduce((sum, msg) => sum + msg.content.length, 0),
|
|
154
|
-
[messages]
|
|
155
|
-
);
|
|
156
|
-
const contextLength = agentState.contextTokens ?? fallbackContextLength;
|
|
157
|
-
const contextEstimated = agentState.contextTokens ? agentState.tokensEstimated : true;
|
|
158
|
-
const spinnerVerbs = configStore.get().spinnerVerbs || DEFAULT_SPINNER_VERBS;
|
|
159
|
-
const spinnerVerbsKey = spinnerVerbs.join('|');
|
|
160
|
-
|
|
161
|
-
React.useEffect(() => {
|
|
162
|
-
renderCount.current += 1;
|
|
163
|
-
debugLog(`App render #${renderCount.current} (messages=${messages.length}, state=${agentState.status})`);
|
|
164
|
-
});
|
|
165
|
-
React.useEffect(() => {
|
|
166
|
-
messagesRef.current = messages;
|
|
167
|
-
}, [messages]);
|
|
168
|
-
const [debug, setDebug] = useState(false);
|
|
169
|
-
const scrollback = process.env.ZTC_SCROLLBACK === '1' || process.env.ZTC_ALT_SCREEN !== '1';
|
|
170
|
-
|
|
171
|
-
React.useEffect(() => {
|
|
172
|
-
if (agentState.status !== 'thinking' && agentState.status !== 'streaming') {
|
|
173
|
-
setSpinnerLabel(null);
|
|
174
|
-
setSpinnerFrame(null);
|
|
175
|
-
return;
|
|
176
|
-
}
|
|
177
|
-
if (spinnerVerbs.length === 0) {
|
|
178
|
-
setSpinnerLabel(null);
|
|
179
|
-
setSpinnerFrame(null);
|
|
180
|
-
return;
|
|
181
|
-
}
|
|
182
|
-
const verb = spinnerVerbs[Math.floor(Math.random() * spinnerVerbs.length)];
|
|
183
|
-
setSpinnerLabel(verb);
|
|
184
|
-
}, [agentState.status, spinnerVerbsKey]);
|
|
185
|
-
|
|
186
|
-
React.useEffect(() => {
|
|
187
|
-
if (agentState.status !== 'thinking' && agentState.status !== 'streaming') {
|
|
188
|
-
setSpinnerFrame(null);
|
|
189
|
-
return;
|
|
190
|
-
}
|
|
191
|
-
// Animation is now safe in scrollback mode because we use Ink's Static component
|
|
192
|
-
// for completed messages - only the live section below Static re-renders
|
|
193
|
-
let index = 0;
|
|
194
|
-
setSpinnerFrame(SPINNER_FRAMES[index]);
|
|
195
|
-
const interval = setInterval(() => {
|
|
196
|
-
index = (index + 1) % SPINNER_FRAMES.length;
|
|
197
|
-
setSpinnerFrame(SPINNER_FRAMES[index]);
|
|
198
|
-
}, 120);
|
|
199
|
-
return () => {
|
|
200
|
-
clearInterval(interval);
|
|
201
|
-
};
|
|
202
|
-
}, [agentState.status]);
|
|
203
|
-
|
|
204
|
-
const headerHeight = scrollback ? 0 : 4; // border (2) + content (1) + margin (1)
|
|
205
|
-
const statusHeight = 1;
|
|
206
|
-
const suggestionLines = 4;
|
|
207
|
-
|
|
208
|
-
const estimateBadgePreviewLines = useCallback((state: InputState): number => {
|
|
209
|
-
const segment = state.segments[state.cursor.index];
|
|
210
|
-
if (!segment || segment.type === 'text') return 0;
|
|
211
|
-
if (segment.type === 'paste') {
|
|
212
|
-
const lines = segment.text.split('\n');
|
|
213
|
-
return Math.min(3, lines.length) + (lines.length > 3 ? 1 : 0);
|
|
214
|
-
}
|
|
215
|
-
if (segment.type === 'image') {
|
|
216
|
-
return 16;
|
|
217
|
-
}
|
|
218
|
-
return 1;
|
|
219
|
-
}, []);
|
|
220
|
-
|
|
221
|
-
const inputHeight = useMemo(() => {
|
|
222
|
-
const inputLineCount = estimateInputLines(inputSnapshot.segments, columns);
|
|
223
|
-
const previewLines = estimateBadgePreviewLines(inputSnapshot);
|
|
224
|
-
const maxInputLines = Math.max(1, rows - (headerHeight + statusHeight + suggestionLines + 5));
|
|
225
|
-
return Math.min(Math.max(1, inputLineCount + previewLines), maxInputLines) + suggestionLines;
|
|
226
|
-
}, [columns, estimateBadgePreviewLines, inputSnapshot, rows]);
|
|
227
|
-
|
|
228
|
-
// Calculate content height (total - header - input - status)
|
|
229
|
-
const contentHeight = useMemo(
|
|
230
|
-
() => scrollback ? undefined : Math.max(rows - (headerHeight + inputHeight + statusHeight), 5),
|
|
231
|
-
[rows, scrollback, inputHeight]
|
|
232
|
-
);
|
|
233
|
-
|
|
234
|
-
// Reload agent when config changes
|
|
235
|
-
const reloadAgent = useCallback(() => {
|
|
236
|
-
setAgent(createAgent());
|
|
237
|
-
}, []);
|
|
238
|
-
|
|
239
|
-
React.useEffect(() => {
|
|
240
|
-
let active = true;
|
|
241
|
-
getSkillRegistry()
|
|
242
|
-
.then(registry => {
|
|
243
|
-
if (!active) return;
|
|
244
|
-
setSkills(registry);
|
|
245
|
-
})
|
|
246
|
-
.catch(() => {});
|
|
247
|
-
return () => { active = false; };
|
|
248
|
-
}, []);
|
|
249
|
-
|
|
250
|
-
// Keyboard shortcuts
|
|
251
|
-
useInput((input, key) => {
|
|
252
|
-
if (key.ctrl && input === 'c') {
|
|
253
|
-
exit();
|
|
254
|
-
}
|
|
255
|
-
if (key.ctrl && input === 'l') {
|
|
256
|
-
setMessages([{
|
|
257
|
-
id: generateId(),
|
|
258
|
-
role: 'system',
|
|
259
|
-
content: 'Screen cleared.',
|
|
260
|
-
timestamp: new Date()
|
|
261
|
-
}]);
|
|
262
|
-
}
|
|
263
|
-
if (key.ctrl && input === 'o') {
|
|
264
|
-
setExpandToolOutputs(prev => !prev);
|
|
265
|
-
}
|
|
266
|
-
});
|
|
267
|
-
|
|
268
|
-
// Message helpers
|
|
269
|
-
const addMessage = useCallback((msg: Omit<Message, 'id' | 'timestamp'>) => {
|
|
270
|
-
setMessages(prev => [...prev, {
|
|
271
|
-
...msg,
|
|
272
|
-
id: generateId(),
|
|
273
|
-
timestamp: new Date()
|
|
274
|
-
}]);
|
|
275
|
-
}, []);
|
|
276
|
-
|
|
277
|
-
React.useEffect(() => {
|
|
278
|
-
if (process.env.ZTC_DISABLE_UPDATE === '1') return;
|
|
279
|
-
if (process.env.ZTC_WEB_MIRROR === '1') return;
|
|
280
|
-
let active = true;
|
|
281
|
-
const current = getVersion();
|
|
282
|
-
checkForUpdate(current).then(info => {
|
|
283
|
-
if (!active || !info.hasUpdate) return;
|
|
284
|
-
addMessage({
|
|
285
|
-
role: 'system',
|
|
286
|
-
content: `Update available: v${info.latest} (current v${info.current}). Run /update to install.`
|
|
287
|
-
});
|
|
288
|
-
}).catch(() => {});
|
|
289
|
-
return () => { active = false; };
|
|
290
|
-
}, [addMessage]);
|
|
291
|
-
|
|
292
|
-
const clearMessages = useCallback(() => {
|
|
293
|
-
setMessages([]);
|
|
294
|
-
}, []);
|
|
295
|
-
|
|
296
|
-
const getMessages = useCallback(() => messages, [messages]);
|
|
297
|
-
|
|
298
|
-
const shellController = useMemo(() => ({
|
|
299
|
-
getCwd: () => shellCwdRef.current,
|
|
300
|
-
setCwd: async (path: string) => {
|
|
301
|
-
const next = await resolveWorkingDir(shellCwdRef.current, path);
|
|
302
|
-
shellCwdRef.current = next;
|
|
303
|
-
// Also update agent's working directory so tools use it
|
|
304
|
-
if (agent) {
|
|
305
|
-
agent.setCwd(next);
|
|
306
|
-
}
|
|
307
|
-
return next;
|
|
308
|
-
},
|
|
309
|
-
run: async (command: string) => runShellCommand(command, shellCwdRef.current)
|
|
310
|
-
}), [agent]);
|
|
311
|
-
|
|
312
|
-
const isRetryableError = useCallback((message: string) => {
|
|
313
|
-
const lower = message.toLowerCase();
|
|
314
|
-
return lower.includes('overloaded') || lower.includes('529') || lower.includes('429') || lower.includes('rate limit');
|
|
315
|
-
}, []);
|
|
316
|
-
|
|
317
|
-
const runWithRetry = useCallback(async (
|
|
318
|
-
requestMessages: Message[],
|
|
319
|
-
runAgent: Agent,
|
|
320
|
-
isManual = false,
|
|
321
|
-
isActive?: () => boolean
|
|
322
|
-
) => {
|
|
323
|
-
const maxRetries = 3;
|
|
324
|
-
let attempt = 0;
|
|
325
|
-
setRetryAvailable(false);
|
|
326
|
-
while (attempt < maxRetries) {
|
|
327
|
-
attempt += 1;
|
|
328
|
-
if (isActive && !isActive()) return;
|
|
329
|
-
try {
|
|
330
|
-
streamedResponse.current = false;
|
|
331
|
-
const result = await runAgent.run(requestMessages);
|
|
332
|
-
if (isActive && !isActive()) return;
|
|
333
|
-
if (!streamedResponse.current) {
|
|
334
|
-
// Only add assistant message if there's actual text content.
|
|
335
|
-
// Tool calls are already displayed via tool_start/tool_end events.
|
|
336
|
-
// When the model returns empty content after tool execution, don't show empty message.
|
|
337
|
-
const hasContent = result.content && result.content.trim().length > 0;
|
|
338
|
-
if (hasContent) {
|
|
339
|
-
addMessage({
|
|
340
|
-
role: 'assistant',
|
|
341
|
-
content: result.content,
|
|
342
|
-
toolCalls: result.toolCalls.length > 0 ? result.toolCalls : undefined,
|
|
343
|
-
metadata: result.usage ? { usage: result.usage } : undefined
|
|
344
|
-
});
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
setAgentState({
|
|
348
|
-
status: 'idle',
|
|
349
|
-
tokensUsed: result.usage?.totalTokens || 0,
|
|
350
|
-
contextTokens: result.usage?.inputTokens,
|
|
351
|
-
tokensEstimated: result.usage?.estimated,
|
|
352
|
-
authError: false
|
|
353
|
-
});
|
|
354
|
-
return;
|
|
355
|
-
} catch (err) {
|
|
356
|
-
const errorMsg = (err as Error).message || 'Agent error';
|
|
357
|
-
if (isActive && !isActive()) return;
|
|
358
|
-
if (!isRetryableError(errorMsg) || attempt >= maxRetries) {
|
|
359
|
-
throw err;
|
|
360
|
-
}
|
|
361
|
-
const delayMs = 500 * Math.pow(2, attempt - 1);
|
|
362
|
-
addMessage({
|
|
363
|
-
role: 'system',
|
|
364
|
-
content: `Overloaded. Retrying in ${Math.round(delayMs / 100) / 10}s... (attempt ${attempt + 1}/${maxRetries})`
|
|
365
|
-
});
|
|
366
|
-
await new Promise(resolve => setTimeout(resolve, delayMs));
|
|
367
|
-
if (isManual) {
|
|
368
|
-
setAgentState({ status: 'thinking', startedAt: new Date() });
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
}, [addMessage, isRetryableError, setAgentState]);
|
|
373
|
-
|
|
374
|
-
const retryLast = useCallback(() => {
|
|
375
|
-
const last = lastRequestRef.current;
|
|
376
|
-
if (!last) {
|
|
377
|
-
addMessage({ role: 'system', content: 'No previous request to retry.' });
|
|
378
|
-
return;
|
|
379
|
-
}
|
|
380
|
-
const runId = `${Date.now()}_${runCounterRef.current++}`;
|
|
381
|
-
activeRunIdRef.current = runId;
|
|
382
|
-
const isActive = () => activeRunIdRef.current === runId;
|
|
383
|
-
const runStartedAt = Date.now();
|
|
384
|
-
setAgentState({ status: 'thinking', startedAt: new Date() });
|
|
385
|
-
void runWithRetry(last.messages, last.agent, true, isActive).catch((err) => {
|
|
386
|
-
const message = (err as Error).message || 'Agent error';
|
|
387
|
-
if (!isActive()) return;
|
|
388
|
-
addMessage({ role: 'system', content: `Error: ${message}` });
|
|
389
|
-
if (isRetryableError(message)) {
|
|
390
|
-
addMessage({ role: 'system', content: 'Retries exhausted. Use /retry to try again.' });
|
|
391
|
-
setRetryAvailable(true);
|
|
392
|
-
}
|
|
393
|
-
setAgentState({ status: 'error', error: message });
|
|
394
|
-
});
|
|
395
|
-
}, [addMessage, isRetryableError, runWithRetry, setAgentState]);
|
|
396
|
-
|
|
397
|
-
// App context for commands
|
|
398
|
-
const appContext: CommandContext = useMemo(() => ({
|
|
399
|
-
addMessage,
|
|
400
|
-
clearMessages,
|
|
401
|
-
getMessages,
|
|
402
|
-
setAgentState,
|
|
403
|
-
reloadAgent,
|
|
404
|
-
setDebug,
|
|
405
|
-
retry: retryLast,
|
|
406
|
-
exit,
|
|
407
|
-
config: configStore,
|
|
408
|
-
emulation: {
|
|
409
|
-
list: loadEmulationProfiles,
|
|
410
|
-
get: getEmulationProfile
|
|
411
|
-
},
|
|
412
|
-
shell: shellController,
|
|
413
|
-
clipboard: {
|
|
414
|
-
writeText: writeClipboard
|
|
415
|
-
},
|
|
416
|
-
models: {
|
|
417
|
-
list: async (provider?: string) => listModels({
|
|
418
|
-
provider: provider || configStore.getProvider(),
|
|
419
|
-
apiKey: configStore.getApiKey(provider || configStore.getProvider()),
|
|
420
|
-
baseUrl: configStore.getOpenAICompatibleBaseUrl()
|
|
421
|
-
})
|
|
422
|
-
},
|
|
423
|
-
skills: {
|
|
424
|
-
list: async () => getSkillRegistry()
|
|
425
|
-
},
|
|
426
|
-
getInputMode: () => inputMode,
|
|
427
|
-
setInputMode: (mode) => setInputMode(mode)
|
|
428
|
-
}), [addMessage, clearMessages, getMessages, reloadAgent, exit, shellController, retryLast, inputMode]);
|
|
429
|
-
|
|
430
|
-
// Handle commands
|
|
431
|
-
const handleCommand = useCallback((cmd: string, args: string[]) => {
|
|
432
|
-
const command = commands.find(c => c.name === cmd.toLowerCase());
|
|
433
|
-
if (command) {
|
|
434
|
-
const result = command.handler(args, appContext);
|
|
435
|
-
if (result && typeof (result as Promise<void>).catch === 'function') {
|
|
436
|
-
(result as Promise<void>).catch((err) => {
|
|
437
|
-
const message = err instanceof Error ? err.message : 'Command failed';
|
|
438
|
-
addMessage({ role: 'system', content: `Command failed: ${message}` });
|
|
439
|
-
});
|
|
440
|
-
}
|
|
441
|
-
} else {
|
|
442
|
-
addMessage({
|
|
443
|
-
role: 'system',
|
|
444
|
-
content: `Unknown command: /${cmd}. Type /help for available commands.`
|
|
445
|
-
});
|
|
446
|
-
}
|
|
447
|
-
}, [appContext, addMessage]);
|
|
448
|
-
|
|
449
|
-
// Handle message submission
|
|
450
|
-
const handleSubmit = useCallback(async (text: string) => {
|
|
451
|
-
if (text.startsWith('!')) {
|
|
452
|
-
const commandText = text.slice(1).trim();
|
|
453
|
-
if (commandText) {
|
|
454
|
-
// Intercept 'cd' commands to use the proper /cd handler that persists
|
|
455
|
-
if (commandText === 'cd' || commandText.startsWith('cd ')) {
|
|
456
|
-
const path = commandText.slice(2).trim();
|
|
457
|
-
handleCommand('cd', path ? [path] : []);
|
|
458
|
-
} else {
|
|
459
|
-
handleCommand('shell', [commandText]);
|
|
460
|
-
}
|
|
461
|
-
}
|
|
462
|
-
return;
|
|
463
|
-
}
|
|
464
|
-
if (!configStore.hasApiKey()) {
|
|
465
|
-
addMessage({ role: 'user', content: text });
|
|
466
|
-
addMessage({
|
|
467
|
-
role: 'system',
|
|
468
|
-
content: '⚠️ No API key configured.\n\nSet your key with: /config key sk-ant-your-key-here'
|
|
469
|
-
});
|
|
470
|
-
return;
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
const busy = agentState.status !== 'idle' && agentState.status !== 'error';
|
|
474
|
-
if (busy) {
|
|
475
|
-
if (inputMode === 'queue') {
|
|
476
|
-
queueRef.current.push(text);
|
|
477
|
-
addMessage({
|
|
478
|
-
role: 'system',
|
|
479
|
-
content: `Queued (${queueRef.current.length})`
|
|
480
|
-
});
|
|
481
|
-
return;
|
|
482
|
-
}
|
|
483
|
-
addMessage({
|
|
484
|
-
role: 'system',
|
|
485
|
-
content: 'Interrupting current response...'
|
|
486
|
-
});
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
let currentAgent = agent;
|
|
490
|
-
|
|
491
|
-
if (skills.length > 0) {
|
|
492
|
-
const activated = autoActivateSkills(text, skills);
|
|
493
|
-
const ids = activated.map(skill => skill.id).sort();
|
|
494
|
-
const previous = activeSkillIds.current.join(',');
|
|
495
|
-
const next = ids.join(',');
|
|
496
|
-
if (previous !== next) {
|
|
497
|
-
activeSkillIds.current = ids;
|
|
498
|
-
const prompt = buildSkillPrompt(activated);
|
|
499
|
-
const nextAgent = createAgentFromConfig({
|
|
500
|
-
config: configStore.get(),
|
|
501
|
-
provider: configStore.getProvider(),
|
|
502
|
-
apiKey: configStore.getApiKey(configStore.getProvider()),
|
|
503
|
-
openaiCompatibleBaseUrl: configStore.getOpenAICompatibleBaseUrl(),
|
|
504
|
-
emulationId: configStore.getEmulationId(),
|
|
505
|
-
skillPrompt: prompt
|
|
506
|
-
});
|
|
507
|
-
if (nextAgent) {
|
|
508
|
-
setAgent(nextAgent);
|
|
509
|
-
currentAgent = nextAgent;
|
|
510
|
-
}
|
|
511
|
-
if (ids.length > 0) {
|
|
512
|
-
addMessage({
|
|
513
|
-
role: 'system',
|
|
514
|
-
content: `Skills activated: ${activated.map(skill => skill.name).join(', ')}`
|
|
515
|
-
});
|
|
516
|
-
}
|
|
517
|
-
}
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
if (!currentAgent) {
|
|
521
|
-
currentAgent = createAgent();
|
|
522
|
-
setAgent(currentAgent);
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
if (!currentAgent) {
|
|
526
|
-
addMessage({
|
|
527
|
-
role: 'system',
|
|
528
|
-
content: 'Failed to initialize agent. Please check your configuration.'
|
|
529
|
-
});
|
|
530
|
-
return;
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
const userMsg: Message = {
|
|
534
|
-
id: generateId(),
|
|
535
|
-
role: 'user',
|
|
536
|
-
content: text,
|
|
537
|
-
timestamp: new Date()
|
|
538
|
-
};
|
|
539
|
-
setMessages(prev => [...prev, userMsg]);
|
|
540
|
-
const requestMessages = [...messagesRef.current, userMsg];
|
|
541
|
-
lastRequestRef.current = { messages: requestMessages, agent: currentAgent };
|
|
542
|
-
|
|
543
|
-
const runId = `${Date.now()}_${runCounterRef.current++}`;
|
|
544
|
-
activeRunIdRef.current = runId;
|
|
545
|
-
const isActive = () => activeRunIdRef.current === runId;
|
|
546
|
-
const runStartedAt = Date.now();
|
|
547
|
-
setAgentState({ status: 'thinking', startedAt: new Date() });
|
|
548
|
-
streamingMessageId.current = null;
|
|
549
|
-
streamedResponse.current = false;
|
|
550
|
-
|
|
551
|
-
const cleanup = currentAgent.on((event) => {
|
|
552
|
-
if (!isActive()) return;
|
|
553
|
-
switch (event.type) {
|
|
554
|
-
case 'thinking_start':
|
|
555
|
-
setAgentState(s => ({ ...s, status: 'thinking' }));
|
|
556
|
-
break;
|
|
557
|
-
case 'tool_start':
|
|
558
|
-
setAgentState(s => ({ ...s, status: 'tool_use', currentTool: event.tool }));
|
|
559
|
-
{
|
|
560
|
-
const key = event.tool;
|
|
561
|
-
const bucket = toolStartTimes.current.get(key) || [];
|
|
562
|
-
bucket.push(Date.now());
|
|
563
|
-
toolStartTimes.current.set(key, bucket);
|
|
564
|
-
addMessage({
|
|
565
|
-
role: 'tool',
|
|
566
|
-
content: formatToolStart(event.tool, event.args, configStore.getEmulationId())
|
|
567
|
-
});
|
|
568
|
-
}
|
|
569
|
-
break;
|
|
570
|
-
case 'stream_start':
|
|
571
|
-
streamedResponse.current = true;
|
|
572
|
-
setAgentState(s => ({ ...s, status: 'streaming' }));
|
|
573
|
-
if (!streamingMessageId.current) {
|
|
574
|
-
const id = generateId();
|
|
575
|
-
streamingMessageId.current = id;
|
|
576
|
-
setMessages(prev => [...prev, {
|
|
577
|
-
id,
|
|
578
|
-
role: 'assistant',
|
|
579
|
-
content: '',
|
|
580
|
-
timestamp: new Date(),
|
|
581
|
-
isStreaming: true
|
|
582
|
-
}]);
|
|
583
|
-
}
|
|
584
|
-
break;
|
|
585
|
-
case 'stream_delta':
|
|
586
|
-
setAgentState(s => ({ ...s, status: 'streaming' }));
|
|
587
|
-
if (streamingMessageId.current) {
|
|
588
|
-
setMessages(prev => prev.map(msg => {
|
|
589
|
-
if (msg.id !== streamingMessageId.current) return msg;
|
|
590
|
-
return { ...msg, content: `${msg.content}${event.content}`, isStreaming: true };
|
|
591
|
-
}));
|
|
592
|
-
}
|
|
593
|
-
break;
|
|
594
|
-
case 'stream_end':
|
|
595
|
-
if (streamingMessageId.current) {
|
|
596
|
-
setMessages(prev => {
|
|
597
|
-
// Remove the streaming message if it ended up empty (tool-only response)
|
|
598
|
-
const streamingMsg = prev.find(m => m.id === streamingMessageId.current);
|
|
599
|
-
if (streamingMsg && (!streamingMsg.content || streamingMsg.content.trim() === '')) {
|
|
600
|
-
return prev.filter(m => m.id !== streamingMessageId.current);
|
|
601
|
-
}
|
|
602
|
-
// Otherwise mark it as no longer streaming
|
|
603
|
-
return prev.map(msg => {
|
|
604
|
-
if (msg.id !== streamingMessageId.current) return msg;
|
|
605
|
-
return { ...msg, isStreaming: false };
|
|
606
|
-
});
|
|
607
|
-
});
|
|
608
|
-
}
|
|
609
|
-
streamingMessageId.current = null;
|
|
610
|
-
break;
|
|
611
|
-
case 'tool_end': {
|
|
612
|
-
const key = event.tool;
|
|
613
|
-
const bucket = toolStartTimes.current.get(key) || [];
|
|
614
|
-
const started = bucket.shift();
|
|
615
|
-
toolStartTimes.current.set(key, bucket);
|
|
616
|
-
const duration = started ? Date.now() - started : undefined;
|
|
617
|
-
const emulationId = configStore.getEmulationId();
|
|
618
|
-
if (getTraceStyle(emulationId) === 'claude_code' || getTraceStyle(emulationId) === 'codex') {
|
|
619
|
-
const output = buildToolOutputMessage(event.tool, event.result, duration, emulationId);
|
|
620
|
-
addMessage({
|
|
621
|
-
role: 'tool',
|
|
622
|
-
content: output.preview,
|
|
623
|
-
metadata: {
|
|
624
|
-
toolOutput: output
|
|
625
|
-
}
|
|
626
|
-
});
|
|
627
|
-
} else {
|
|
628
|
-
addMessage({
|
|
629
|
-
role: 'tool',
|
|
630
|
-
content: formatToolEnd(event.tool, event.result, duration, emulationId)
|
|
631
|
-
});
|
|
632
|
-
}
|
|
633
|
-
break;
|
|
634
|
-
}
|
|
635
|
-
case 'tool_error': {
|
|
636
|
-
addMessage({
|
|
637
|
-
role: 'tool',
|
|
638
|
-
content: formatToolError(event.tool, event.error, configStore.getEmulationId())
|
|
639
|
-
});
|
|
640
|
-
break;
|
|
641
|
-
}
|
|
642
|
-
case 'token_usage':
|
|
643
|
-
setAgentState(s => ({
|
|
644
|
-
...s,
|
|
645
|
-
tokensUsed: event.usage.totalTokens,
|
|
646
|
-
contextTokens: event.usage.inputTokens,
|
|
647
|
-
tokensEstimated: event.usage.estimated
|
|
648
|
-
}));
|
|
649
|
-
break;
|
|
650
|
-
case 'error':
|
|
651
|
-
setAgentState({ status: 'error', error: event.error });
|
|
652
|
-
break;
|
|
653
|
-
}
|
|
654
|
-
});
|
|
655
|
-
|
|
656
|
-
try {
|
|
657
|
-
await runWithRetry(requestMessages, currentAgent, false, isActive);
|
|
658
|
-
} catch (err) {
|
|
659
|
-
const errorMsg = (err as Error).message;
|
|
660
|
-
if (!isActive()) return;
|
|
661
|
-
|
|
662
|
-
const isAuthError = errorMsg.includes('401') || errorMsg.includes('authentication');
|
|
663
|
-
if (isAuthError) {
|
|
664
|
-
addMessage({
|
|
665
|
-
role: 'system',
|
|
666
|
-
content: `Authentication failed. Your API key may be invalid.\n\nUpdate it with: /config key sk-ant-your-key-here`
|
|
667
|
-
});
|
|
668
|
-
} else {
|
|
669
|
-
addMessage({
|
|
670
|
-
role: 'system',
|
|
671
|
-
content: `Error: ${errorMsg}`
|
|
672
|
-
});
|
|
673
|
-
if (isRetryableError(errorMsg)) {
|
|
674
|
-
addMessage({
|
|
675
|
-
role: 'system',
|
|
676
|
-
content: 'Retries exhausted. Use /retry to try again.'
|
|
677
|
-
});
|
|
678
|
-
setRetryAvailable(true);
|
|
679
|
-
}
|
|
680
|
-
}
|
|
681
|
-
setAgentState({ status: 'error', error: errorMsg, authError: isAuthError });
|
|
682
|
-
|
|
683
|
-
setTimeout(() => {
|
|
684
|
-
setAgentState({ status: 'idle' });
|
|
685
|
-
}, 3000);
|
|
686
|
-
} finally {
|
|
687
|
-
if (isActive()) {
|
|
688
|
-
streamedResponse.current = false;
|
|
689
|
-
streamingMessageId.current = null;
|
|
690
|
-
activeRunIdRef.current = null;
|
|
691
|
-
lastRunDurationRef.current = Date.now() - runStartedAt;
|
|
692
|
-
if (getTraceStyle(configStore.getEmulationId()) === 'codex') {
|
|
693
|
-
const seconds = Math.max(0, Math.round((lastRunDurationRef.current || 0) / 1000));
|
|
694
|
-
addMessage({
|
|
695
|
-
role: 'tool',
|
|
696
|
-
content: `✻ Worked for ${seconds}s`
|
|
697
|
-
});
|
|
698
|
-
}
|
|
699
|
-
cleanup();
|
|
700
|
-
if (inputMode === 'queue' && queueRef.current.length > 0) {
|
|
701
|
-
const next = queueRef.current.shift();
|
|
702
|
-
if (next) {
|
|
703
|
-
void handleSubmit(next);
|
|
704
|
-
}
|
|
705
|
-
}
|
|
706
|
-
} else {
|
|
707
|
-
cleanup();
|
|
708
|
-
}
|
|
709
|
-
}
|
|
710
|
-
}, [agent, agentState.status, addMessage, handleCommand, inputMode, isRetryableError, runWithRetry]);
|
|
711
|
-
|
|
712
|
-
const layoutTree = useMemo(() => {
|
|
713
|
-
if (!mirrorEnabled) return null;
|
|
714
|
-
return buildAppView({
|
|
715
|
-
messages,
|
|
716
|
-
agentState,
|
|
717
|
-
inputState: inputSnapshot,
|
|
718
|
-
sessionId,
|
|
719
|
-
cols: columns,
|
|
720
|
-
rows,
|
|
721
|
-
commands,
|
|
722
|
-
hasApiKey: configStore.hasApiKey(),
|
|
723
|
-
version: getVersion(),
|
|
724
|
-
contextLength,
|
|
725
|
-
contextEstimated,
|
|
726
|
-
provider,
|
|
727
|
-
model,
|
|
728
|
-
emulationId,
|
|
729
|
-
inputMode,
|
|
730
|
-
toast,
|
|
731
|
-
spinnerLabel,
|
|
732
|
-
spinnerFrame,
|
|
733
|
-
debug,
|
|
734
|
-
expandToolOutputs
|
|
735
|
-
});
|
|
736
|
-
}, [mirrorEnabled, messages, agentState, inputSnapshot, sessionId, rows, contextLength, contextEstimated, provider, model, emulationId, inputMode, toast, spinnerLabel, spinnerFrame, debug, expandToolOutputs]);
|
|
737
|
-
|
|
738
|
-
const showToast = useCallback((message: string) => {
|
|
739
|
-
setToast(message);
|
|
740
|
-
if (toastTimerRef.current) {
|
|
741
|
-
clearTimeout(toastTimerRef.current);
|
|
742
|
-
}
|
|
743
|
-
toastTimerRef.current = setTimeout(() => {
|
|
744
|
-
setToast(null);
|
|
745
|
-
}, 2500);
|
|
746
|
-
}, []);
|
|
747
|
-
|
|
748
|
-
const handleDictationStateChange = useCallback((state: 'idle' | 'recording' | 'transcribing') => {
|
|
749
|
-
setIsRecording(state === 'recording');
|
|
750
|
-
setIsTranscribing(state === 'transcribing');
|
|
751
|
-
if (state === 'recording') {
|
|
752
|
-
setRecordingDevice(getRecordingDeviceName());
|
|
753
|
-
} else if (state === 'idle') {
|
|
754
|
-
setRecordingDevice(null);
|
|
755
|
-
setAudioLevel(-60);
|
|
756
|
-
setPartialTranscript('');
|
|
757
|
-
}
|
|
758
|
-
}, []);
|
|
759
|
-
|
|
760
|
-
// Native dictation event handler
|
|
761
|
-
React.useEffect(() => {
|
|
762
|
-
if (!useNativeDictation) return;
|
|
763
|
-
|
|
764
|
-
const cleanup = onDictationEvent((event: DictationEvent) => {
|
|
765
|
-
switch (event.type) {
|
|
766
|
-
case 'ready':
|
|
767
|
-
setRecordingDevice(event.device || 'Unknown');
|
|
768
|
-
break;
|
|
769
|
-
case 'level':
|
|
770
|
-
setAudioLevel(event.db ?? -60);
|
|
771
|
-
break;
|
|
772
|
-
case 'text':
|
|
773
|
-
if (event.partial) {
|
|
774
|
-
setPartialTranscript(event.content || '');
|
|
775
|
-
}
|
|
776
|
-
break;
|
|
777
|
-
case 'stopped':
|
|
778
|
-
setIsRecording(false);
|
|
779
|
-
setAudioLevel(-60);
|
|
780
|
-
break;
|
|
781
|
-
case 'error':
|
|
782
|
-
showToast(`Dictation error: ${event.message}`);
|
|
783
|
-
break;
|
|
784
|
-
}
|
|
785
|
-
});
|
|
786
|
-
|
|
787
|
-
return cleanup;
|
|
788
|
-
}, [useNativeDictation, showToast]);
|
|
789
|
-
|
|
790
|
-
useMirror(layoutTree, inputBus);
|
|
791
|
-
|
|
792
|
-
// Compute messages for Static (completed) vs live (streaming)
|
|
793
|
-
// Static component handles deduplication by key - it only renders new items
|
|
794
|
-
const { staticMessages, streamingMessage } = useMemo(() => {
|
|
795
|
-
if (!scrollback) {
|
|
796
|
-
return { staticMessages: [], streamingMessage: null };
|
|
797
|
-
}
|
|
798
|
-
|
|
799
|
-
// Completed messages go to Static (rendered once, kept in scrollback)
|
|
800
|
-
const completed = messages.filter(m => !m.isStreaming);
|
|
801
|
-
// Streaming message stays in live section (can update)
|
|
802
|
-
const streaming = messages.find(m => m.isStreaming) || null;
|
|
803
|
-
|
|
804
|
-
return {
|
|
805
|
-
staticMessages: completed,
|
|
806
|
-
streamingMessage: streaming
|
|
807
|
-
};
|
|
808
|
-
}, [messages, scrollback]);
|
|
809
|
-
|
|
810
|
-
return (
|
|
811
|
-
<FullScreen debug={debug} scrollback={scrollback}>
|
|
812
|
-
{!scrollback && <Header version={getVersion()} debug={debug} />}
|
|
813
|
-
|
|
814
|
-
{scrollback ? (
|
|
815
|
-
<Box flexDirection="column">
|
|
816
|
-
{/* Static: each message rendered ONCE, stays in scrollback buffer */}
|
|
817
|
-
<Static items={staticMessages}>
|
|
818
|
-
{(msg: Message) => (
|
|
819
|
-
<Box key={msg.id} flexDirection="column">
|
|
820
|
-
<SingleMessage message={msg} expandToolOutputs={expandToolOutputs} />
|
|
821
|
-
</Box>
|
|
822
|
-
)}
|
|
823
|
-
</Static>
|
|
824
|
-
|
|
825
|
-
{/* Live section: streaming message can update */}
|
|
826
|
-
{streamingMessage && (
|
|
827
|
-
<SingleMessage message={streamingMessage} expandToolOutputs={expandToolOutputs} />
|
|
828
|
-
)}
|
|
829
|
-
</Box>
|
|
830
|
-
) : (
|
|
831
|
-
<MessageList
|
|
832
|
-
messages={messages}
|
|
833
|
-
height={contentHeight}
|
|
834
|
-
debug={debug}
|
|
835
|
-
expandToolOutputs={expandToolOutputs}
|
|
836
|
-
scrollback={scrollback}
|
|
837
|
-
/>
|
|
838
|
-
)}
|
|
839
|
-
|
|
840
|
-
<ActivityLine
|
|
841
|
-
state={agentState}
|
|
842
|
-
spinnerLabel={spinnerLabel}
|
|
843
|
-
spinnerFrame={spinnerFrame}
|
|
844
|
-
inputMode={inputMode}
|
|
845
|
-
/>
|
|
846
|
-
|
|
847
|
-
{isRecording && (
|
|
848
|
-
<Box flexDirection="column" paddingLeft={2}>
|
|
849
|
-
<Box>
|
|
850
|
-
<Text color="red">● Recording</Text>
|
|
851
|
-
{recordingDevice && (
|
|
852
|
-
<Text color="gray"> [{recordingDevice}]</Text>
|
|
853
|
-
)}
|
|
854
|
-
<Text color="gray"> </Text>
|
|
855
|
-
<Text color="cyan">{renderVUMeter(audioLevel)}</Text>
|
|
856
|
-
<Text color="gray"> — Ctrl+R to stop</Text>
|
|
857
|
-
</Box>
|
|
858
|
-
{partialTranscript && (
|
|
859
|
-
<Box>
|
|
860
|
-
<Text color="gray" italic>"{partialTranscript}"</Text>
|
|
861
|
-
</Box>
|
|
862
|
-
)}
|
|
863
|
-
</Box>
|
|
864
|
-
)}
|
|
865
|
-
{isTranscribing && !useNativeDictation && (
|
|
866
|
-
<Box paddingLeft={2}>
|
|
867
|
-
<Text color="yellow">⠋ Transcribing...</Text>
|
|
868
|
-
</Box>
|
|
869
|
-
)}
|
|
870
|
-
|
|
871
|
-
<InputArea
|
|
872
|
-
onSubmit={handleSubmit}
|
|
873
|
-
onCommand={handleCommand}
|
|
874
|
-
commands={commands}
|
|
875
|
-
onStateChange={setInputSnapshot}
|
|
876
|
-
onToast={showToast}
|
|
877
|
-
onDictationStateChange={handleDictationStateChange}
|
|
878
|
-
cols={columns}
|
|
879
|
-
inputBus={inputBus}
|
|
880
|
-
disabled={false}
|
|
881
|
-
debug={debug}
|
|
882
|
-
cwd={shellCwdRef.current}
|
|
883
|
-
placeholder={
|
|
884
|
-
!configStore.hasApiKey() ? 'Set API key with /config key <key>' :
|
|
885
|
-
agentState.status === 'thinking' ? 'Thinking...' :
|
|
886
|
-
agentState.status === 'tool_use' ? `Running ${agentState.currentTool}...` :
|
|
887
|
-
agentState.status === 'streaming' ? 'Streaming response...' :
|
|
888
|
-
'Type a message or /help for commands...'
|
|
889
|
-
}
|
|
890
|
-
/>
|
|
891
|
-
|
|
892
|
-
<StatusBar
|
|
893
|
-
state={agentState}
|
|
894
|
-
sessionId={sessionId}
|
|
895
|
-
version={getVersion()}
|
|
896
|
-
connectionStatus={configStore.hasApiKey() ? 'connected' : 'disconnected'}
|
|
897
|
-
contextLength={contextLength}
|
|
898
|
-
contextEstimated={contextEstimated}
|
|
899
|
-
provider={provider}
|
|
900
|
-
model={model}
|
|
901
|
-
emulationId={emulationId}
|
|
902
|
-
inputMode={inputMode}
|
|
903
|
-
toast={toast}
|
|
904
|
-
debug={debug}
|
|
905
|
-
/>
|
|
906
|
-
</FullScreen>
|
|
907
|
-
);
|
|
908
|
-
};
|
|
909
|
-
|
|
910
|
-
export default App;
|