@serjm/deepseek-code 0.4.3 → 0.4.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/CHANGELOG.md +85 -0
- package/README.md +72 -109
- package/README.ru.md +73 -109
- package/dist/api/index.d.ts +5 -0
- package/dist/api/index.d.ts.map +1 -1
- package/dist/api/index.js +42 -4
- package/dist/api/index.js.map +1 -1
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +15 -8
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/interactive.d.ts.map +1 -1
- package/dist/cli/interactive.js +65 -3
- package/dist/cli/interactive.js.map +1 -1
- package/dist/commands/index.d.ts.map +1 -1
- package/dist/commands/index.js +26 -21
- package/dist/commands/index.js.map +1 -1
- package/dist/config/defaults.d.ts +9 -0
- package/dist/config/defaults.d.ts.map +1 -1
- package/dist/config/defaults.js +25 -7
- package/dist/config/defaults.js.map +1 -1
- package/dist/core/agent-loop.d.ts +56 -3
- package/dist/core/agent-loop.d.ts.map +1 -1
- package/dist/core/agent-loop.js +458 -104
- package/dist/core/agent-loop.js.map +1 -1
- package/dist/core/i18n.d.ts +3 -0
- package/dist/core/i18n.d.ts.map +1 -1
- package/dist/core/i18n.js +9 -0
- package/dist/core/i18n.js.map +1 -1
- package/dist/core/mcp-tools.d.ts +15 -0
- package/dist/core/mcp-tools.d.ts.map +1 -0
- package/dist/core/mcp-tools.js +94 -0
- package/dist/core/mcp-tools.js.map +1 -0
- package/dist/core/metrics.d.ts +9 -2
- package/dist/core/metrics.d.ts.map +1 -1
- package/dist/core/metrics.js +51 -9
- package/dist/core/metrics.js.map +1 -1
- package/dist/tools/bash.d.ts.map +1 -1
- package/dist/tools/bash.js +317 -23
- package/dist/tools/bash.js.map +1 -1
- package/dist/tools/chrome-manager.d.ts.map +1 -1
- package/dist/tools/chrome-manager.js +5 -2
- package/dist/tools/chrome-manager.js.map +1 -1
- package/dist/tools/chrome.d.ts.map +1 -1
- package/dist/tools/chrome.js +8 -3
- package/dist/tools/chrome.js.map +1 -1
- package/dist/tools/glob.d.ts.map +1 -1
- package/dist/tools/glob.js +40 -3
- package/dist/tools/glob.js.map +1 -1
- package/dist/tools/grep.d.ts.map +1 -1
- package/dist/tools/grep.js +69 -13
- package/dist/tools/grep.js.map +1 -1
- package/dist/tools/process-manager.d.ts +17 -0
- package/dist/tools/process-manager.d.ts.map +1 -0
- package/dist/tools/process-manager.js +94 -0
- package/dist/tools/process-manager.js.map +1 -0
- package/dist/tools/read.d.ts.map +1 -1
- package/dist/tools/read.js +94 -0
- package/dist/tools/read.js.map +1 -1
- package/dist/tools/shell.d.ts +20 -0
- package/dist/tools/shell.d.ts.map +1 -0
- package/dist/tools/shell.js +100 -0
- package/dist/tools/shell.js.map +1 -0
- package/dist/tools/types.d.ts +27 -1
- package/dist/tools/types.d.ts.map +1 -1
- package/dist/tools/types.js +43 -1
- package/dist/tools/types.js.map +1 -1
- package/dist/ui/app.d.ts.map +1 -1
- package/dist/ui/app.js +219 -178
- package/dist/ui/app.js.map +1 -1
- package/dist/ui/chat-view.d.ts +24 -3
- package/dist/ui/chat-view.d.ts.map +1 -1
- package/dist/ui/chat-view.js +116 -58
- package/dist/ui/chat-view.js.map +1 -1
- package/dist/ui/input-bar.d.ts.map +1 -1
- package/dist/ui/input-bar.js +38 -4
- package/dist/ui/input-bar.js.map +1 -1
- package/dist/ui/setup-wizard.js +1 -1
- package/dist/ui/setup-wizard.js.map +1 -1
- package/dist/ui/status-bar.d.ts +5 -1
- package/dist/ui/status-bar.d.ts.map +1 -1
- package/dist/ui/status-bar.js +10 -4
- package/dist/ui/status-bar.js.map +1 -1
- package/dist/utils/logger.d.ts +15 -0
- package/dist/utils/logger.d.ts.map +1 -1
- package/dist/utils/logger.js +47 -0
- package/dist/utils/logger.js.map +1 -1
- package/package.json +3 -2
package/dist/ui/app.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useState, useCallback, useRef, useEffect } from 'react';
|
|
3
|
-
import { Box, Text, useInput, useApp
|
|
3
|
+
import { Box, Text, useInput, useApp } from 'ink';
|
|
4
4
|
import { ChatView } from './chat-view.js';
|
|
5
5
|
import { InputBar } from './input-bar.js';
|
|
6
6
|
import { StatusBar } from './status-bar.js';
|
|
@@ -20,6 +20,7 @@ import { i18n } from '../core/i18n.js';
|
|
|
20
20
|
import { Logo, SetupWizard, useSetupWizard } from './setup-wizard.js';
|
|
21
21
|
import { executeSlashCommand } from '../commands/index.js';
|
|
22
22
|
import { checkLatestVersion } from '../commands/update-checker.js';
|
|
23
|
+
import { logEvent } from '../utils/logger.js';
|
|
23
24
|
/** Empty input hint timeout in ms before showing the guide text */
|
|
24
25
|
const EMPTY_INPUT_HINT_DELAY = 2000;
|
|
25
26
|
function stripExecutionSummary(content) {
|
|
@@ -43,6 +44,33 @@ function historyForModel(messages) {
|
|
|
43
44
|
return [message];
|
|
44
45
|
});
|
|
45
46
|
}
|
|
47
|
+
function isUserCancellationError(error, signal) {
|
|
48
|
+
if (!signal.aborted)
|
|
49
|
+
return false;
|
|
50
|
+
if (error.name === 'StreamTimeoutError')
|
|
51
|
+
return false;
|
|
52
|
+
const msg = error.message || '';
|
|
53
|
+
return error.name === 'AbortError' || /abort|cancel/i.test(msg);
|
|
54
|
+
}
|
|
55
|
+
function formatAgentError(error) {
|
|
56
|
+
const msg = error.message || '';
|
|
57
|
+
if (msg.includes('401') || msg.includes('403') || msg.includes('Unauthorized')) {
|
|
58
|
+
return i18n.t('apiErrorAuth');
|
|
59
|
+
}
|
|
60
|
+
if (msg.includes('429') || msg.includes('rate limit')) {
|
|
61
|
+
return i18n.t('apiErrorRateLimit');
|
|
62
|
+
}
|
|
63
|
+
if (/5\d{2}|server error|Service Unavailable/i.test(msg)) {
|
|
64
|
+
return i18n.t('apiErrorServer');
|
|
65
|
+
}
|
|
66
|
+
if (/ECONNRESET|ECONNREFUSED|ENOTFOUND/i.test(msg)) {
|
|
67
|
+
return i18n.t('apiErrorNetwork');
|
|
68
|
+
}
|
|
69
|
+
if (error.name === 'StreamTimeoutError' || /ETIMEDOUT|timed out|Stream timeout/i.test(msg)) {
|
|
70
|
+
return i18n.t('apiErrorTimeout');
|
|
71
|
+
}
|
|
72
|
+
return `${i18n.t('error')}: ${msg}`;
|
|
73
|
+
}
|
|
46
74
|
// Setup wizard step components are now in ./setup-wizard.tsx
|
|
47
75
|
// Logo, SetupWizard, useSetupWizard imported from there
|
|
48
76
|
export function App({ config, options }) {
|
|
@@ -50,12 +78,20 @@ export function App({ config, options }) {
|
|
|
50
78
|
const [approvalMode, setApprovalMode] = useState(options.approvalMode ?? (options.turbo ? 'turbo' : config.approvalMode));
|
|
51
79
|
const [messages, setMessages] = useState([]);
|
|
52
80
|
const [isProcessing, setIsProcessing] = useState(false);
|
|
81
|
+
const [isPaused, setIsPaused] = useState(false);
|
|
82
|
+
const isPausedRef = useRef(false);
|
|
83
|
+
// Keep isPausedRef in sync so finally block can read it without stale closure
|
|
84
|
+
useEffect(() => { isPausedRef.current = isPaused; }, [isPaused]);
|
|
53
85
|
const [statusText, setStatusText] = useState(i18n.t('ready'));
|
|
54
86
|
const [localApiKey, setLocalApiKey] = useState(config.apiKey || '');
|
|
55
87
|
const agentLoopRef = useRef(null);
|
|
56
88
|
const abortControllerRef = useRef(null);
|
|
57
89
|
const pendingApprovalResolveRef = useRef(null);
|
|
58
90
|
const liveToolMessageIndexRef = useRef(-1);
|
|
91
|
+
// Tools belonging to the CURRENT (still-live) tool card. Each tool batch gets
|
|
92
|
+
// its own card so completed batches can be committed to the Static scrollback
|
|
93
|
+
// while only the active batch stays in the live region.
|
|
94
|
+
const currentCardToolsRef = useRef([]);
|
|
59
95
|
const prevToolCallsRef = useRef([]);
|
|
60
96
|
const [toolCalls, setToolCalls] = useState([]);
|
|
61
97
|
const [pendingApproval, setPendingApproval] = useState(null);
|
|
@@ -68,20 +104,68 @@ export function App({ config, options }) {
|
|
|
68
104
|
const initializedRef = useRef(false);
|
|
69
105
|
const [emptyInputHint, setEmptyInputHint] = useState(false);
|
|
70
106
|
const emptyInputTimerRef = useRef(null);
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
const
|
|
107
|
+
// Chat history is rendered via Ink <Static> (see ChatView): finalized messages
|
|
108
|
+
// are printed once to the terminal scrollback (no flicker, native mouse scroll).
|
|
109
|
+
// `chatEpoch` remounts Static to reset the scrollback on /clear.
|
|
110
|
+
const [chatEpoch, setChatEpoch] = useState(0);
|
|
75
111
|
const [contextPercent, setContextPercent] = useState(0);
|
|
112
|
+
const [compactProgress, setCompactProgress] = useState(0);
|
|
76
113
|
const [totalTokens, setTotalTokens] = useState(0);
|
|
77
114
|
const [estimatedCost, setEstimatedCost] = useState(0);
|
|
78
|
-
const [
|
|
115
|
+
const [followUpCount, setFollowUpCount] = useState(0);
|
|
79
116
|
const [themePicker, setThemePicker] = useState(null);
|
|
80
117
|
const [modelPicker, setModelPicker] = useState(null);
|
|
81
118
|
const [langPicker, setLangPicker] = useState(null);
|
|
82
119
|
const [serviceNotice, setServiceNotice] = useState(null);
|
|
83
120
|
const serviceNoticeTimerRef = useRef(null);
|
|
84
121
|
const budgetRef = useRef(undefined);
|
|
122
|
+
// Double Ctrl+C to exit when idle/paused (raw mode owns Ctrl+C on all platforms).
|
|
123
|
+
const ctrlCExitArmedRef = useRef(false);
|
|
124
|
+
const ctrlCExitTimerRef = useRef(null);
|
|
125
|
+
// ── Stream chunk batching ──────────────────────────────────────────────
|
|
126
|
+
const pendingChunksRef = useRef('');
|
|
127
|
+
const flushTimerRef = useRef(null);
|
|
128
|
+
// Flush buffered assistant text into the message list. Starting a new text
|
|
129
|
+
// block also finalizes the previous live tool card (→ compact) so it commits
|
|
130
|
+
// to the Static scrollback as a completed item.
|
|
131
|
+
const flushPending = useCallback(() => {
|
|
132
|
+
if (flushTimerRef.current) {
|
|
133
|
+
clearTimeout(flushTimerRef.current);
|
|
134
|
+
flushTimerRef.current = null;
|
|
135
|
+
}
|
|
136
|
+
const buffered = pendingChunksRef.current;
|
|
137
|
+
if (!buffered)
|
|
138
|
+
return;
|
|
139
|
+
pendingChunksRef.current = '';
|
|
140
|
+
setMessages(prev => {
|
|
141
|
+
const last = prev[prev.length - 1];
|
|
142
|
+
if (last?.role === 'assistant') {
|
|
143
|
+
const updated = [...prev];
|
|
144
|
+
updated[updated.length - 1] = { ...last, content: last.content + buffered };
|
|
145
|
+
return updated;
|
|
146
|
+
}
|
|
147
|
+
const updated = [...prev];
|
|
148
|
+
const cardIdx = liveToolMessageIndexRef.current;
|
|
149
|
+
if (cardIdx >= 0 && cardIdx < updated.length && updated[cardIdx]?.role === 'tool' && currentCardToolsRef.current.length > 0) {
|
|
150
|
+
updated[cardIdx] = {
|
|
151
|
+
role: 'tool',
|
|
152
|
+
content: JSON.stringify({ type: 'tool_activity_card', toolCalls: currentCardToolsRef.current, status: 'compact' }),
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
liveToolMessageIndexRef.current = -1;
|
|
156
|
+
currentCardToolsRef.current = [];
|
|
157
|
+
updated.push({ role: 'assistant', content: buffered });
|
|
158
|
+
return updated;
|
|
159
|
+
});
|
|
160
|
+
}, []);
|
|
161
|
+
const scheduleFlush = useCallback(() => {
|
|
162
|
+
if (flushTimerRef.current)
|
|
163
|
+
return;
|
|
164
|
+
flushTimerRef.current = setTimeout(() => {
|
|
165
|
+
flushTimerRef.current = null;
|
|
166
|
+
flushPending();
|
|
167
|
+
}, 75);
|
|
168
|
+
}, [flushPending]);
|
|
85
169
|
const addServiceNotice = useCallback((text) => {
|
|
86
170
|
setServiceNotice(text);
|
|
87
171
|
if (serviceNoticeTimerRef.current)
|
|
@@ -167,60 +251,13 @@ export function App({ config, options }) {
|
|
|
167
251
|
setStatusText(i18n.t('ready'));
|
|
168
252
|
})();
|
|
169
253
|
}, []);
|
|
170
|
-
//
|
|
171
|
-
//
|
|
172
|
-
|
|
173
|
-
const proc = process;
|
|
174
|
-
if (isProcessing) {
|
|
175
|
-
proc.__agentSoftCancel = () => {
|
|
176
|
-
abortControllerRef.current?.abort();
|
|
177
|
-
if (pendingApprovalResolveRef.current) {
|
|
178
|
-
pendingApprovalResolveRef.current(false);
|
|
179
|
-
pendingApprovalResolveRef.current = null;
|
|
180
|
-
}
|
|
181
|
-
setPendingApproval(null);
|
|
182
|
-
setStatusText(i18n.t('cancelled'));
|
|
183
|
-
};
|
|
184
|
-
}
|
|
185
|
-
else {
|
|
186
|
-
proc.__agentSoftCancel = undefined;
|
|
187
|
-
}
|
|
188
|
-
return () => { proc.__agentSoftCancel = undefined; };
|
|
189
|
-
}, [isProcessing]);
|
|
190
|
-
const { stdin } = useStdin();
|
|
191
|
-
// Compensate scroll offset when new visible messages arrive while paused
|
|
192
|
-
useEffect(() => {
|
|
193
|
-
if (setupStepRef.current !== 'done')
|
|
194
|
-
return;
|
|
195
|
-
const visible = messages.filter(m => m.role !== 'tool').length;
|
|
196
|
-
if (scrollMode === 'paused' && visible > visibleMessageCountRef.current) {
|
|
197
|
-
const diff = visible - visibleMessageCountRef.current;
|
|
198
|
-
setChatScrollOffset(prev => prev + diff);
|
|
199
|
-
setNewMessagesWhilePaused(true);
|
|
200
|
-
}
|
|
201
|
-
visibleMessageCountRef.current = visible;
|
|
202
|
-
}, [messages.length, scrollMode]);
|
|
254
|
+
// __agentSoftCancel is now set synchronously in handleSubmit (before agentLoop.run())
|
|
255
|
+
// to eliminate the async useEffect race window on Windows where SIGINT fires
|
|
256
|
+
// before the effect commits. Cleanup is handled in finally block.
|
|
203
257
|
// Sync prevToolCallsRef with toolCalls state
|
|
204
258
|
useEffect(() => {
|
|
205
259
|
prevToolCallsRef.current = toolCalls;
|
|
206
260
|
}, [toolCalls]);
|
|
207
|
-
// Detect End key from raw stdin (Ink does not expose it in useInput)
|
|
208
|
-
useEffect(() => {
|
|
209
|
-
if (!stdin)
|
|
210
|
-
return;
|
|
211
|
-
const handler = (data) => {
|
|
212
|
-
if (setupStepRef.current !== 'done')
|
|
213
|
-
return;
|
|
214
|
-
const seq = data.toString();
|
|
215
|
-
if (seq === '\x1b[F' || seq === '\x1b[4~' || seq === '\x1b[8~' || seq === '\x1bOF') {
|
|
216
|
-
setChatScrollOffset(0);
|
|
217
|
-
setScrollMode('follow');
|
|
218
|
-
setNewMessagesWhilePaused(false);
|
|
219
|
-
}
|
|
220
|
-
};
|
|
221
|
-
stdin.on('data', handler);
|
|
222
|
-
return () => { stdin.off('data', handler); };
|
|
223
|
-
}, [stdin]);
|
|
224
261
|
// Slash commands — delegated to commands/index.ts
|
|
225
262
|
const handleSlashCommand = useCallback(async (input) => {
|
|
226
263
|
const ctx = {
|
|
@@ -261,8 +298,18 @@ export function App({ config, options }) {
|
|
|
261
298
|
emptyInputTimerRef.current = setTimeout(() => setEmptyInputHint(false), EMPTY_INPUT_HINT_DELAY);
|
|
262
299
|
return;
|
|
263
300
|
}
|
|
264
|
-
if (isProcessing)
|
|
301
|
+
if (isProcessing) {
|
|
302
|
+
// Live follow-up: inject into current active AgentLoop
|
|
303
|
+
if (agentLoopRef.current) {
|
|
304
|
+
agentLoopRef.current.addUserFollowUp(input.trim());
|
|
305
|
+
setFollowUpCount(c => c + 1);
|
|
306
|
+
addServiceNotice('[queue] Follow-up #' + (followUpCount + 1) + ' added. Agent will continue after current step.');
|
|
307
|
+
}
|
|
308
|
+
else {
|
|
309
|
+
addServiceNotice('Agent is not active. Send it as a new message.');
|
|
310
|
+
}
|
|
265
311
|
return;
|
|
312
|
+
}
|
|
266
313
|
// Handle setup wizard steps
|
|
267
314
|
if (setupStep === 'apikey') {
|
|
268
315
|
await handleApiKeySubmit(input);
|
|
@@ -278,6 +325,7 @@ export function App({ config, options }) {
|
|
|
278
325
|
}]);
|
|
279
326
|
return;
|
|
280
327
|
}
|
|
328
|
+
setIsPaused(false);
|
|
281
329
|
if (input.startsWith('/')) {
|
|
282
330
|
try {
|
|
283
331
|
const handled = await handleSlashCommand(input);
|
|
@@ -304,14 +352,27 @@ export function App({ config, options }) {
|
|
|
304
352
|
}
|
|
305
353
|
const abortController = new AbortController();
|
|
306
354
|
abortControllerRef.current = abortController;
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
355
|
+
// SYNCHRONOUS: register soft-cancel hook before agentLoop.run().
|
|
356
|
+
// The old useEffect-based registration had a race window on Windows where
|
|
357
|
+
// SIGINT could fire between isProcessing=true and the effect commit.
|
|
358
|
+
const proc = process;
|
|
359
|
+
proc.__agentSoftCancel = () => {
|
|
360
|
+
logEvent('__agentSoftCancel invoked');
|
|
361
|
+
abortController.abort();
|
|
362
|
+
setIsPaused(true);
|
|
363
|
+
if (pendingApprovalResolveRef.current) {
|
|
364
|
+
pendingApprovalResolveRef.current(false);
|
|
365
|
+
pendingApprovalResolveRef.current = null;
|
|
366
|
+
}
|
|
367
|
+
setPendingApproval(null);
|
|
368
|
+
setStatusText(i18n.t('paused'));
|
|
369
|
+
};
|
|
370
|
+
proc.__agentAbortController = abortController;
|
|
371
|
+
logEvent('agent run: softCancel registered');
|
|
372
|
+
// Images are not sent to the model: the DeepSeek API has no vision and
|
|
373
|
+
// rejects image_url content blocks (a single one bricks the whole session
|
|
374
|
+
// with a 400). Image paste shows a notice instead (see onImagePaste).
|
|
375
|
+
const userContent = input;
|
|
315
376
|
// Show local diagnostic notice for large prompts (not sent to model)
|
|
316
377
|
const charCount = input.length;
|
|
317
378
|
const lineCount = (input.match(/\r\n|\r|\n/g) || []).length + 1;
|
|
@@ -325,9 +386,7 @@ export function App({ config, options }) {
|
|
|
325
386
|
setStatusText(i18n.t('working'));
|
|
326
387
|
setToolCalls([]);
|
|
327
388
|
liveToolMessageIndexRef.current = -1;
|
|
328
|
-
|
|
329
|
-
setScrollMode('follow');
|
|
330
|
-
setNewMessagesWhilePaused(false);
|
|
389
|
+
currentCardToolsRef.current = [];
|
|
331
390
|
try {
|
|
332
391
|
await hooksManager.execute('UserPromptSubmit', {
|
|
333
392
|
event: 'UserPromptSubmit',
|
|
@@ -340,21 +399,22 @@ export function App({ config, options }) {
|
|
|
340
399
|
signal: abortController.signal,
|
|
341
400
|
budget: budgetRef.current,
|
|
342
401
|
onToolCall: (tc) => {
|
|
402
|
+
// Commit any buffered assistant text first so it lands above the card.
|
|
403
|
+
flushPending();
|
|
343
404
|
const updatedCalls = [...prevToolCallsRef.current, tc];
|
|
344
405
|
prevToolCallsRef.current = updatedCalls;
|
|
345
406
|
setToolCalls(updatedCalls);
|
|
407
|
+
currentCardToolsRef.current = [...currentCardToolsRef.current, tc];
|
|
346
408
|
setStatusText(`[tool] ${tc.name}...`);
|
|
347
|
-
// Add/update live tool activity card
|
|
409
|
+
// Add/update the live tool activity card (one card per tool batch).
|
|
348
410
|
setMessages(prev => {
|
|
349
411
|
const idx = liveToolMessageIndexRef.current;
|
|
350
|
-
const card = { type: 'tool_activity_card', toolCalls:
|
|
412
|
+
const card = { type: 'tool_activity_card', toolCalls: currentCardToolsRef.current, status: 'live' };
|
|
351
413
|
if (idx >= 0 && idx < prev.length && prev[idx]?.role === 'tool') {
|
|
352
|
-
// Update existing card
|
|
353
414
|
const updated = [...prev];
|
|
354
415
|
updated[idx] = { role: 'tool', content: JSON.stringify(card) };
|
|
355
416
|
return updated;
|
|
356
417
|
}
|
|
357
|
-
// Add new card
|
|
358
418
|
liveToolMessageIndexRef.current = prev.length;
|
|
359
419
|
return [...prev, { role: 'tool', content: JSON.stringify(card) }];
|
|
360
420
|
});
|
|
@@ -364,16 +424,8 @@ export function App({ config, options }) {
|
|
|
364
424
|
},
|
|
365
425
|
onReasoningChunk: () => { },
|
|
366
426
|
onStreamChunk: (chunk) => {
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
if (last?.role === 'assistant') {
|
|
370
|
-
// If last content is empty, replace with chunk; otherwise append
|
|
371
|
-
const updated = [...prev];
|
|
372
|
-
updated[updated.length - 1] = { ...last, content: last.content + chunk };
|
|
373
|
-
return updated;
|
|
374
|
-
}
|
|
375
|
-
return [...prev, { role: 'assistant', content: chunk }];
|
|
376
|
-
});
|
|
427
|
+
pendingChunksRef.current += chunk;
|
|
428
|
+
scheduleFlush();
|
|
377
429
|
},
|
|
378
430
|
// onResponse intentionally removed — onStreamChunk handles all text
|
|
379
431
|
// Avoids duplicate "assistant" message push that caused text doubling
|
|
@@ -382,6 +434,18 @@ export function App({ config, options }) {
|
|
|
382
434
|
// Handled by handleSubmit catch block — adding here would create duplicate
|
|
383
435
|
// assistant messages, breaking the conversation structure for the next request
|
|
384
436
|
},
|
|
437
|
+
onCompactStart: () => {
|
|
438
|
+
setCompactProgress(5);
|
|
439
|
+
setStatusText('Compacting context...');
|
|
440
|
+
},
|
|
441
|
+
onCompactProgress: (event) => {
|
|
442
|
+
setCompactProgress(event.progress);
|
|
443
|
+
setStatusText(`Compacting context ${event.progress}%...`);
|
|
444
|
+
},
|
|
445
|
+
onCompactEnd: (event) => {
|
|
446
|
+
setCompactProgress(0);
|
|
447
|
+
setStatusText(event.phase === 'done' ? 'Context compacted' : 'Context compact failed');
|
|
448
|
+
},
|
|
385
449
|
onApprovalRequest: async (toolName, args) => {
|
|
386
450
|
if (approvalModeRef.current === 'turbo')
|
|
387
451
|
return true;
|
|
@@ -399,6 +463,10 @@ export function App({ config, options }) {
|
|
|
399
463
|
},
|
|
400
464
|
});
|
|
401
465
|
const finalResponse = await agentLoopRef.current.run(input, historyForModel(messages));
|
|
466
|
+
// Abort during run: skip session save, let finally handle UI reset
|
|
467
|
+
if (abortController.signal.aborted) {
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
402
470
|
const toolHistory = agentLoopRef.current.getToolCallHistory();
|
|
403
471
|
const bundleFile = await writeExecutionBundle({
|
|
404
472
|
sessionId: sessionIdRef.current,
|
|
@@ -440,15 +508,16 @@ export function App({ config, options }) {
|
|
|
440
508
|
// Single batch: all final UI updates at once (no await between setState calls)
|
|
441
509
|
setIsProcessing(false);
|
|
442
510
|
setStatusText(i18n.t('ready'));
|
|
443
|
-
//
|
|
444
|
-
if (liveToolMessageIndexRef.current >= 0 &&
|
|
511
|
+
// Finalize the last live tool batch (if the turn ended on tools) to compact.
|
|
512
|
+
if (liveToolMessageIndexRef.current >= 0 && currentCardToolsRef.current.length > 0) {
|
|
513
|
+
const cardTools = currentCardToolsRef.current;
|
|
445
514
|
setMessages(prev => {
|
|
446
515
|
const idx = liveToolMessageIndexRef.current;
|
|
447
516
|
if (idx >= 0 && idx < prev.length && prev[idx]?.role === 'tool') {
|
|
448
517
|
const updated = [...prev];
|
|
449
518
|
updated[idx] = {
|
|
450
519
|
role: 'tool',
|
|
451
|
-
content: JSON.stringify({ type: 'tool_activity_card', toolCalls:
|
|
520
|
+
content: JSON.stringify({ type: 'tool_activity_card', toolCalls: cardTools, status: 'compact' }),
|
|
452
521
|
};
|
|
453
522
|
return updated;
|
|
454
523
|
}
|
|
@@ -456,33 +525,16 @@ export function App({ config, options }) {
|
|
|
456
525
|
});
|
|
457
526
|
}
|
|
458
527
|
liveToolMessageIndexRef.current = -1;
|
|
528
|
+
currentCardToolsRef.current = [];
|
|
459
529
|
}
|
|
460
530
|
catch (err) {
|
|
461
531
|
const error = err;
|
|
462
|
-
//
|
|
463
|
-
if (error
|
|
532
|
+
// User-triggered cancellation is expected and should not become an error message.
|
|
533
|
+
if (isUserCancellationError(error, abortController.signal)) {
|
|
464
534
|
return;
|
|
465
535
|
}
|
|
466
|
-
const
|
|
467
|
-
|
|
468
|
-
if (msg.includes('401') || msg.includes('403') || msg.includes('Unauthorized')) {
|
|
469
|
-
friendlyMsg = i18n.t('apiErrorAuth');
|
|
470
|
-
}
|
|
471
|
-
else if (msg.includes('429') || msg.includes('rate limit')) {
|
|
472
|
-
friendlyMsg = i18n.t('apiErrorRateLimit');
|
|
473
|
-
}
|
|
474
|
-
else if (/5\d{2}|server error|Service Unavailable/i.test(msg)) {
|
|
475
|
-
friendlyMsg = i18n.t('apiErrorServer');
|
|
476
|
-
}
|
|
477
|
-
else if (/ECONNRESET|ECONNREFUSED|ENOTFOUND/i.test(msg)) {
|
|
478
|
-
friendlyMsg = i18n.t('apiErrorNetwork');
|
|
479
|
-
}
|
|
480
|
-
else if (/ETIMEDOUT|timed out/i.test(msg)) {
|
|
481
|
-
friendlyMsg = i18n.t('apiErrorTimeout');
|
|
482
|
-
}
|
|
483
|
-
else {
|
|
484
|
-
friendlyMsg = `${i18n.t('error')}: ${msg}`;
|
|
485
|
-
}
|
|
536
|
+
const friendlyMsg = formatAgentError(error);
|
|
537
|
+
const toolHistory = agentLoopRef.current?.getToolCallHistory() ?? [];
|
|
486
538
|
setMessages(prev => [...prev, {
|
|
487
539
|
role: 'assistant',
|
|
488
540
|
content: friendlyMsg,
|
|
@@ -492,16 +544,31 @@ export function App({ config, options }) {
|
|
|
492
544
|
prompt: input,
|
|
493
545
|
error: friendlyMsg,
|
|
494
546
|
approvalMode,
|
|
547
|
+
toolCalls: toolHistory.map(toolCall => ({
|
|
548
|
+
name: toolCall.name,
|
|
549
|
+
status: toolCall.status,
|
|
550
|
+
durationMs: toolCall.durationMs,
|
|
551
|
+
error: toolCall.error,
|
|
552
|
+
})),
|
|
495
553
|
});
|
|
496
554
|
const bundleFile = await writeExecutionBundle({
|
|
497
555
|
sessionId: sessionIdRef.current,
|
|
498
556
|
prompt: input,
|
|
499
557
|
error: friendlyMsg,
|
|
500
558
|
approvalMode,
|
|
559
|
+
toolCalls: toolHistory.map(toolCall => ({
|
|
560
|
+
id: toolCall.id,
|
|
561
|
+
name: toolCall.name,
|
|
562
|
+
status: toolCall.status,
|
|
563
|
+
durationMs: toolCall.durationMs,
|
|
564
|
+
error: toolCall.error,
|
|
565
|
+
result: toolCall.result,
|
|
566
|
+
})),
|
|
501
567
|
});
|
|
502
568
|
await saveSession({
|
|
503
569
|
id: sessionIdRef.current,
|
|
504
570
|
messageCount: messages.length + 2,
|
|
571
|
+
toolCallCount: toolHistory.length,
|
|
505
572
|
approvalMode,
|
|
506
573
|
lastPrompt: input,
|
|
507
574
|
lastError: friendlyMsg,
|
|
@@ -511,9 +578,14 @@ export function App({ config, options }) {
|
|
|
511
578
|
});
|
|
512
579
|
}
|
|
513
580
|
finally {
|
|
581
|
+
logEvent('agent run: finally (clearing softCancel)');
|
|
514
582
|
// Safety net: ensure UI is always reset regardless of exit path
|
|
583
|
+
flushPending();
|
|
515
584
|
setIsProcessing(false);
|
|
516
|
-
|
|
585
|
+
// Don't overwrite status if user paused the agent
|
|
586
|
+
if (!isPausedRef.current) {
|
|
587
|
+
setStatusText(i18n.t('ready'));
|
|
588
|
+
}
|
|
517
589
|
// Clear any pending approval (covers error/abort exit paths)
|
|
518
590
|
if (pendingApprovalResolveRef.current) {
|
|
519
591
|
pendingApprovalResolveRef.current(false);
|
|
@@ -523,29 +595,38 @@ export function App({ config, options }) {
|
|
|
523
595
|
if (abortControllerRef.current === abortController) {
|
|
524
596
|
abortControllerRef.current = null;
|
|
525
597
|
}
|
|
598
|
+
// Cleanup synchronous SIGINT hooks
|
|
599
|
+
const proc2 = process;
|
|
600
|
+
if (proc2.__agentSoftCancel)
|
|
601
|
+
proc2.__agentSoftCancel = undefined;
|
|
602
|
+
if (proc2.__agentAbortController === abortController)
|
|
603
|
+
proc2.__agentAbortController = undefined;
|
|
526
604
|
}
|
|
527
605
|
}, [messages, isProcessing, setupStep, handleApiKeySubmit, handleSlashCommand, approvalMode, config, localApiKey]);
|
|
528
606
|
useInput((_input, key) => {
|
|
529
607
|
const step = setupStepRef.current;
|
|
530
|
-
// Ctrl+C
|
|
531
|
-
//
|
|
532
|
-
//
|
|
533
|
-
//
|
|
608
|
+
// Ctrl+C = exit only after a double confirmation. Ink raw mode delivers it
|
|
609
|
+
// as input (not a SIGINT) when exitOnCtrlC is false. We intentionally do NOT
|
|
610
|
+
// abort the running agent on Ctrl+C — aborting the in-flight work was what
|
|
611
|
+
// dropped the process to the shell. To steer or stop the agent mid-run, type
|
|
612
|
+
// a follow-up message instead (it is queued into the active run).
|
|
613
|
+
// - First press: show a hint, do nothing else (agent keeps running).
|
|
614
|
+
// - Second press within 3s: exit cleanly.
|
|
534
615
|
if (key.ctrl && _input === 'c') {
|
|
535
|
-
if (
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
pendingApprovalResolveRef.current(false);
|
|
540
|
-
pendingApprovalResolveRef.current = null;
|
|
541
|
-
}
|
|
542
|
-
setPendingApproval(null);
|
|
616
|
+
if (ctrlCExitArmedRef.current) {
|
|
617
|
+
if (ctrlCExitTimerRef.current)
|
|
618
|
+
clearTimeout(ctrlCExitTimerRef.current);
|
|
619
|
+
exit();
|
|
543
620
|
return;
|
|
544
621
|
}
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
622
|
+
ctrlCExitArmedRef.current = true;
|
|
623
|
+
addServiceNotice(i18n.t('ctrlCHint'));
|
|
624
|
+
if (ctrlCExitTimerRef.current)
|
|
625
|
+
clearTimeout(ctrlCExitTimerRef.current);
|
|
626
|
+
ctrlCExitTimerRef.current = setTimeout(() => {
|
|
627
|
+
ctrlCExitArmedRef.current = false;
|
|
628
|
+
ctrlCExitTimerRef.current = null;
|
|
629
|
+
}, 3000);
|
|
549
630
|
return;
|
|
550
631
|
}
|
|
551
632
|
// When not in setup mode, let InputBar handle all keyboard input
|
|
@@ -632,26 +713,9 @@ export function App({ config, options }) {
|
|
|
632
713
|
});
|
|
633
714
|
return;
|
|
634
715
|
}
|
|
635
|
-
//
|
|
636
|
-
//
|
|
637
|
-
|
|
638
|
-
const visibleCount = messages.filter(m => m.role !== 'tool').length;
|
|
639
|
-
const next = Math.min(chatScrollOffset + 10, Math.max(0, visibleCount - 1));
|
|
640
|
-
if (next > 0)
|
|
641
|
-
setScrollMode('paused');
|
|
642
|
-
setChatScrollOffset(next);
|
|
643
|
-
return;
|
|
644
|
-
}
|
|
645
|
-
// PageDown: scroll down by ~half a screen
|
|
646
|
-
if (key.pageDown) {
|
|
647
|
-
const next = Math.max(0, chatScrollOffset - 10);
|
|
648
|
-
setChatScrollOffset(next);
|
|
649
|
-
if (next === 0) {
|
|
650
|
-
setScrollMode('follow');
|
|
651
|
-
setNewMessagesWhilePaused(false);
|
|
652
|
-
}
|
|
653
|
-
return;
|
|
654
|
-
}
|
|
716
|
+
// Chat history scrolling is handled natively by the terminal (messages are
|
|
717
|
+
// committed to the scrollback via Ink <Static>), so PageUp/PageDown are no
|
|
718
|
+
// longer intercepted here.
|
|
655
719
|
// Theme picker: interactive selection
|
|
656
720
|
if (themePicker) {
|
|
657
721
|
if (key.escape) {
|
|
@@ -738,23 +802,10 @@ export function App({ config, options }) {
|
|
|
738
802
|
}
|
|
739
803
|
return;
|
|
740
804
|
}
|
|
741
|
-
//
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
const next = Math.min(chatScrollOffset + 1, Math.max(0, visibleCount - 1));
|
|
746
|
-
if (next > 0)
|
|
747
|
-
setScrollMode('paused');
|
|
748
|
-
setChatScrollOffset(next);
|
|
749
|
-
return;
|
|
750
|
-
}
|
|
751
|
-
if (key.downArrow && isProcessing) {
|
|
752
|
-
const next = Math.max(0, chatScrollOffset - 1);
|
|
753
|
-
setChatScrollOffset(next);
|
|
754
|
-
if (next === 0) {
|
|
755
|
-
setScrollMode('follow');
|
|
756
|
-
setNewMessagesWhilePaused(false);
|
|
757
|
-
}
|
|
805
|
+
// Esc when paused: dismiss pause, return to ready
|
|
806
|
+
if (key.escape && isPaused && !pendingApproval && !pendingClear && !themePicker && !modelPicker && !langPicker) {
|
|
807
|
+
setIsPaused(false);
|
|
808
|
+
setStatusText(i18n.t('ready'));
|
|
758
809
|
return;
|
|
759
810
|
}
|
|
760
811
|
return;
|
|
@@ -865,10 +916,7 @@ export function App({ config, options }) {
|
|
|
865
916
|
setMessages([]);
|
|
866
917
|
setToolCalls([]);
|
|
867
918
|
setPendingApproval(null);
|
|
868
|
-
|
|
869
|
-
setScrollMode('follow');
|
|
870
|
-
setNewMessagesWhilePaused(false);
|
|
871
|
-
setChatScrollOffset(0);
|
|
919
|
+
setChatEpoch(e => e + 1); // remount Static so the scrollback resets
|
|
872
920
|
liveToolMessageIndexRef.current = -1;
|
|
873
921
|
setServiceNotice(null);
|
|
874
922
|
if (serviceNoticeTimerRef.current) {
|
|
@@ -887,7 +935,7 @@ export function App({ config, options }) {
|
|
|
887
935
|
}, [messages.length, toolCalls.length, executeClear]);
|
|
888
936
|
const handleExit = useCallback(() => { exit(); }, [exit]);
|
|
889
937
|
const colors = themeManager.getColors();
|
|
890
|
-
return (_jsxs(Box, { flexDirection: 'column',
|
|
938
|
+
return (_jsxs(Box, { flexDirection: 'column', children: [setupStep !== 'done'
|
|
891
939
|
? _jsx(SetupWizard, { state: {
|
|
892
940
|
step: setupStep,
|
|
893
941
|
apiKeyError,
|
|
@@ -898,21 +946,14 @@ export function App({ config, options }) {
|
|
|
898
946
|
langOptions,
|
|
899
947
|
modeOptions,
|
|
900
948
|
} })
|
|
901
|
-
: (_jsxs(Box, { flexDirection: 'column',
|
|
949
|
+
: (_jsxs(Box, { flexDirection: 'column', children: [_jsx(ChatView, { messages: messages, isProcessing: isProcessing, epoch: chatEpoch, header: _jsx(Logo, {}) }), serviceNotice && (_jsx(Box, { marginLeft: 2, marginBottom: 1, children: _jsx(Text, { color: colors.primary, children: serviceNotice }) })), themePicker && (_jsxs(Box, { flexDirection: 'column', marginLeft: 2, marginBottom: 1, borderStyle: 'round', borderColor: colors.primary, children: [_jsx(Box, { marginLeft: 1, marginTop: 1, children: _jsx(Text, { bold: true, color: colors.primary, children: "\u0412\u044B\u0431\u0435\u0440\u0438\u0442\u0435 \u0442\u0435\u043C\u0443" }) }), _jsx(Box, { marginLeft: 1, marginTop: 1, flexDirection: 'column', children: themePicker.themes.map((t, i) => (_jsx(Box, { children: _jsxs(Text, { color: i === themePicker.selectedIndex ? colors.primary : colors.textMuted, children: [i === themePicker.selectedIndex ? '▸ ' : ' ', t.name, t.name === themeManager.theme.name ? ' (текущая)' : ''] }) }, t.name))) }), _jsx(Box, { marginLeft: 1, marginBottom: 1, marginTop: 1, children: _jsx(Text, { color: colors.textMuted, children: "\u2191\u2193 \u2014 \u043D\u0430\u0432\u0438\u0433\u0430\u0446\u0438\u044F Enter \u2014 \u043F\u0440\u0438\u043C\u0435\u043D\u0438\u0442\u044C Esc \u2014 \u043E\u0442\u043C\u0435\u043D\u0430" }) })] })), modelPicker && (_jsxs(Box, { flexDirection: 'column', marginLeft: 2, marginBottom: 1, borderStyle: 'round', borderColor: colors.primary, children: [_jsx(Box, { marginLeft: 1, marginTop: 1, children: _jsx(Text, { bold: true, color: colors.primary, children: "\u0412\u044B\u0431\u0435\u0440\u0438\u0442\u0435 \u043C\u043E\u0434\u0435\u043B\u044C" }) }), _jsx(Box, { marginLeft: 1, marginTop: 1, flexDirection: 'column', children: DEEPSEEK_MODELS.map((m, i) => (_jsxs(Box, { flexDirection: 'column', children: [_jsxs(Text, { color: i === modelPicker.selectedIndex ? colors.primary : colors.textMuted, children: [i === modelPicker.selectedIndex ? '▸ ' : ' ', _jsx(Text, { bold: i === modelPicker.selectedIndex, children: m.label }), m.id === config.model ? _jsx(Text, { dimColor: true, children: " (\u0442\u0435\u043A\u0443\u0449\u0430\u044F)" }) : null] }), _jsxs(Text, { dimColor: true, children: [' ', m.description] })] }, m.id))) }), _jsx(Box, { marginLeft: 1, marginBottom: 1, marginTop: 1, children: _jsx(Text, { color: colors.textMuted, children: "\u2191\u2193 \u2014 \u043D\u0430\u0432\u0438\u0433\u0430\u0446\u0438\u044F Enter \u2014 \u043F\u0440\u0438\u043C\u0435\u043D\u0438\u0442\u044C Esc \u2014 \u043E\u0442\u043C\u0435\u043D\u0430" }) })] })), langPicker && (_jsxs(Box, { flexDirection: 'column', marginLeft: 2, marginBottom: 1, borderStyle: 'round', borderColor: colors.primary, children: [_jsx(Box, { marginLeft: 1, marginTop: 1, children: _jsx(Text, { bold: true, color: colors.primary, children: "\u0412\u044B\u0431\u0435\u0440\u0438\u0442\u0435 \u044F\u0437\u044B\u043A / Select language / \u9009\u62E9\u8BED\u8A00" }) }), _jsx(Box, { marginLeft: 1, marginTop: 1, flexDirection: 'column', children: i18n.listLocales().map((loc, i) => (_jsx(Box, { children: _jsxs(Text, { color: i === langPicker.selectedIndex ? colors.primary : colors.textMuted, children: [i === langPicker.selectedIndex ? '▸ ' : ' ', loc.name, loc.code === i18n.getLocale() ? _jsx(Text, { dimColor: true, children: " (\u0442\u0435\u043A\u0443\u0449\u0438\u0439)" }) : null] }) }, loc.code))) }), _jsx(Box, { marginLeft: 1, marginBottom: 1, marginTop: 1, children: _jsx(Text, { color: colors.textMuted, children: "\u2191\u2193 \u2014 \u043D\u0430\u0432\u0438\u0433\u0430\u0446\u0438\u044F Enter \u2014 \u043F\u0440\u0438\u043C\u0435\u043D\u0438\u0442\u044C Esc \u2014 \u043E\u0442\u043C\u0435\u043D\u0430" }) })] })), pendingApproval && (_jsxs(Box, { flexDirection: 'column', marginLeft: 2, marginBottom: 1, borderStyle: 'round', borderColor: colors.warning, children: [_jsx(Box, { children: _jsx(Text, { bold: true, color: colors.warning, children: "\u041F\u043E\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u044C \u0432\u044B\u0437\u043E\u0432 \u0438\u043D\u0441\u0442\u0440\u0443\u043C\u0435\u043D\u0442\u0430?" }) }), _jsx(Box, { marginLeft: 1, children: _jsx(Text, { bold: true, color: colors.text, children: pendingApproval.toolName }) }), _jsx(Box, { marginLeft: 1, children: _jsx(Text, { color: colors.textMuted, children: JSON.stringify(pendingApproval.args, null, 2).slice(0, 200) }) }), _jsxs(Box, { flexDirection: 'column', marginTop: 1, children: [[
|
|
902
950
|
'[ok] Подтвердить',
|
|
903
951
|
'[no] Отклонить',
|
|
904
952
|
`[mute] Не спрашивать для "${pendingApproval.toolName}"`,
|
|
905
953
|
'[turbo] Выполнять всё без вопросов',
|
|
906
|
-
].map((label, i) => (_jsx(Box, { marginLeft: 1, children: _jsxs(Text, { color: approvalCursor === i ? colors.primary : colors.text, children: [approvalCursor === i ? '> ' : ' ', label] }) }, i))), _jsx(Box, { marginLeft: 1, marginTop: 1, children: _jsx(Text, { color: colors.textMuted, children: "\u2191\u2193 \u2014 \u043D\u0430\u0432\u0438\u0433\u0430\u0446\u0438\u044F Enter \u2014 \u0432\u044B\u0431\u0440\u0430\u0442\u044C Esc \u2014 \u043E\u0442\u043A\u043B\u043E\u043D\u0438\u0442\u044C" }) })] })] })), pendingClear && (_jsxs(Box, { flexDirection: 'column', marginLeft: 2, marginBottom: 1, borderStyle: 'round', borderColor: colors.warning, children: [_jsx(Box, { children: _jsx(Text, { bold: true, color: colors.warning, children: "\u041E\u0447\u0438\u0441\u0442\u0438\u0442\u044C \u0438\u0441\u0442\u043E\u0440\u0438\u044E \u0447\u0430\u0442\u0430?" }) }), _jsx(Box, { marginLeft: 1, children: _jsxs(Text, { color: colors.textMuted, children: [messages.length, " \u0441\u043E\u043E\u0431\u0449\u0435\u043D\u0438\u0439 \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043B\u0435\u043D\u043E. \u041E\u0442\u043C\u0435\u043D\u0443 \u043D\u0435\u043B\u044C\u0437\u044F."] }) }), _jsxs(Box, { flexDirection: 'column', marginTop: 1, children: [['[ok] Да, очистить', '[no] Отмена'].map((label, i) => (_jsx(Box, { marginLeft: 1, children: _jsxs(Text, { color: clearCursor === i ? colors.primary : colors.text, children: [clearCursor === i ? '> ' : ' ', label] }) }, i))), _jsx(Box, { marginLeft: 1, marginTop: 1, children: _jsx(Text, { color: colors.textMuted, children: "\u2191\u2193 \u2014 \u043D\u0430\u0432\u0438\u0433\u0430\u0446\u0438\u044F Enter \u2014 \u0432\u044B\u0431\u0440\u0430\u0442\u044C Esc \u2014 \u043E\u0442\u043C\u0435\u043D\u0430" }) })] })] }))] })), _jsx(InputBar, { onSubmit: handleSubmit, disabled:
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
role: 'assistant',
|
|
911
|
-
content: `[warn] Вставка изображения требует модель с поддержкой vision.\nТекущая модель: ${model || 'неизвестно'}\nИспользуйте модель с "vl" или "vision" в названии.`,
|
|
912
|
-
}]);
|
|
913
|
-
return;
|
|
914
|
-
}
|
|
915
|
-
setPendingImage({ base64, mimeType });
|
|
916
|
-
} }), _jsx(StatusBar, { mode: approvalMode, status: statusText, messageCount: messages.length, isProcessing: isProcessing, contextPercent: contextPercent, totalTokens: totalTokens, estimatedCost: estimatedCost, model: config.model })] }));
|
|
954
|
+
].map((label, i) => (_jsx(Box, { marginLeft: 1, children: _jsxs(Text, { color: approvalCursor === i ? colors.primary : colors.text, children: [approvalCursor === i ? '> ' : ' ', label] }) }, i))), _jsx(Box, { marginLeft: 1, marginTop: 1, children: _jsx(Text, { color: colors.textMuted, children: "\u2191\u2193 \u2014 \u043D\u0430\u0432\u0438\u0433\u0430\u0446\u0438\u044F Enter \u2014 \u0432\u044B\u0431\u0440\u0430\u0442\u044C Esc \u2014 \u043E\u0442\u043A\u043B\u043E\u043D\u0438\u0442\u044C" }) })] })] })), pendingClear && (_jsxs(Box, { flexDirection: 'column', marginLeft: 2, marginBottom: 1, borderStyle: 'round', borderColor: colors.warning, children: [_jsx(Box, { children: _jsx(Text, { bold: true, color: colors.warning, children: "\u041E\u0447\u0438\u0441\u0442\u0438\u0442\u044C \u0438\u0441\u0442\u043E\u0440\u0438\u044E \u0447\u0430\u0442\u0430?" }) }), _jsx(Box, { marginLeft: 1, children: _jsxs(Text, { color: colors.textMuted, children: [messages.length, " \u0441\u043E\u043E\u0431\u0449\u0435\u043D\u0438\u0439 \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043B\u0435\u043D\u043E. \u041E\u0442\u043C\u0435\u043D\u0443 \u043D\u0435\u043B\u044C\u0437\u044F."] }) }), _jsxs(Box, { flexDirection: 'column', marginTop: 1, children: [['[ok] Да, очистить', '[no] Отмена'].map((label, i) => (_jsx(Box, { marginLeft: 1, children: _jsxs(Text, { color: clearCursor === i ? colors.primary : colors.text, children: [clearCursor === i ? '> ' : ' ', label] }) }, i))), _jsx(Box, { marginLeft: 1, marginTop: 1, children: _jsx(Text, { color: colors.textMuted, children: "\u2191\u2193 \u2014 \u043D\u0430\u0432\u0438\u0433\u0430\u0446\u0438\u044F Enter \u2014 \u0432\u044B\u0431\u0440\u0430\u0442\u044C Esc \u2014 \u043E\u0442\u043C\u0435\u043D\u0430" }) })] })] }))] })), _jsx(InputBar, { onSubmit: handleSubmit, disabled: false,
|
|
955
|
+
// disabled is no longer tied to isProcessing — input is allowed during processing.
|
|
956
|
+
// blockInput still handles approval/clear dialogs.
|
|
957
|
+
onClear: handleClear, onExit: handleExit, isMasked: setupStep === 'apikey', isSetupMode: setupStep !== 'done', blockInput: setupStep === 'done' && (pendingApproval !== null || pendingClear), emptyHint: emptyInputHint, onImagePaste: () => addServiceNotice('[image] Эта модель не поддерживает изображения — вставка пропущена. Для проверки UI используйте /browser-test или опишите задачу текстом.') }), _jsx(StatusBar, { mode: approvalMode, status: statusText, messageCount: messages.length, isProcessing: isProcessing, isPaused: isPaused, contextPercent: contextPercent, compactProgress: compactProgress, totalTokens: totalTokens, estimatedCost: estimatedCost, model: config.model, followUpCount: followUpCount })] }));
|
|
917
958
|
}
|
|
918
959
|
//# sourceMappingURL=app.js.map
|