@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.
Files changed (88) hide show
  1. package/CHANGELOG.md +85 -0
  2. package/README.md +72 -109
  3. package/README.ru.md +73 -109
  4. package/dist/api/index.d.ts +5 -0
  5. package/dist/api/index.d.ts.map +1 -1
  6. package/dist/api/index.js +42 -4
  7. package/dist/api/index.js.map +1 -1
  8. package/dist/cli/index.d.ts +1 -0
  9. package/dist/cli/index.d.ts.map +1 -1
  10. package/dist/cli/index.js +15 -8
  11. package/dist/cli/index.js.map +1 -1
  12. package/dist/cli/interactive.d.ts.map +1 -1
  13. package/dist/cli/interactive.js +65 -3
  14. package/dist/cli/interactive.js.map +1 -1
  15. package/dist/commands/index.d.ts.map +1 -1
  16. package/dist/commands/index.js +26 -21
  17. package/dist/commands/index.js.map +1 -1
  18. package/dist/config/defaults.d.ts +9 -0
  19. package/dist/config/defaults.d.ts.map +1 -1
  20. package/dist/config/defaults.js +25 -7
  21. package/dist/config/defaults.js.map +1 -1
  22. package/dist/core/agent-loop.d.ts +56 -3
  23. package/dist/core/agent-loop.d.ts.map +1 -1
  24. package/dist/core/agent-loop.js +458 -104
  25. package/dist/core/agent-loop.js.map +1 -1
  26. package/dist/core/i18n.d.ts +3 -0
  27. package/dist/core/i18n.d.ts.map +1 -1
  28. package/dist/core/i18n.js +9 -0
  29. package/dist/core/i18n.js.map +1 -1
  30. package/dist/core/mcp-tools.d.ts +15 -0
  31. package/dist/core/mcp-tools.d.ts.map +1 -0
  32. package/dist/core/mcp-tools.js +94 -0
  33. package/dist/core/mcp-tools.js.map +1 -0
  34. package/dist/core/metrics.d.ts +9 -2
  35. package/dist/core/metrics.d.ts.map +1 -1
  36. package/dist/core/metrics.js +51 -9
  37. package/dist/core/metrics.js.map +1 -1
  38. package/dist/tools/bash.d.ts.map +1 -1
  39. package/dist/tools/bash.js +317 -23
  40. package/dist/tools/bash.js.map +1 -1
  41. package/dist/tools/chrome-manager.d.ts.map +1 -1
  42. package/dist/tools/chrome-manager.js +5 -2
  43. package/dist/tools/chrome-manager.js.map +1 -1
  44. package/dist/tools/chrome.d.ts.map +1 -1
  45. package/dist/tools/chrome.js +8 -3
  46. package/dist/tools/chrome.js.map +1 -1
  47. package/dist/tools/glob.d.ts.map +1 -1
  48. package/dist/tools/glob.js +40 -3
  49. package/dist/tools/glob.js.map +1 -1
  50. package/dist/tools/grep.d.ts.map +1 -1
  51. package/dist/tools/grep.js +69 -13
  52. package/dist/tools/grep.js.map +1 -1
  53. package/dist/tools/process-manager.d.ts +17 -0
  54. package/dist/tools/process-manager.d.ts.map +1 -0
  55. package/dist/tools/process-manager.js +94 -0
  56. package/dist/tools/process-manager.js.map +1 -0
  57. package/dist/tools/read.d.ts.map +1 -1
  58. package/dist/tools/read.js +94 -0
  59. package/dist/tools/read.js.map +1 -1
  60. package/dist/tools/shell.d.ts +20 -0
  61. package/dist/tools/shell.d.ts.map +1 -0
  62. package/dist/tools/shell.js +100 -0
  63. package/dist/tools/shell.js.map +1 -0
  64. package/dist/tools/types.d.ts +27 -1
  65. package/dist/tools/types.d.ts.map +1 -1
  66. package/dist/tools/types.js +43 -1
  67. package/dist/tools/types.js.map +1 -1
  68. package/dist/ui/app.d.ts.map +1 -1
  69. package/dist/ui/app.js +219 -178
  70. package/dist/ui/app.js.map +1 -1
  71. package/dist/ui/chat-view.d.ts +24 -3
  72. package/dist/ui/chat-view.d.ts.map +1 -1
  73. package/dist/ui/chat-view.js +116 -58
  74. package/dist/ui/chat-view.js.map +1 -1
  75. package/dist/ui/input-bar.d.ts.map +1 -1
  76. package/dist/ui/input-bar.js +38 -4
  77. package/dist/ui/input-bar.js.map +1 -1
  78. package/dist/ui/setup-wizard.js +1 -1
  79. package/dist/ui/setup-wizard.js.map +1 -1
  80. package/dist/ui/status-bar.d.ts +5 -1
  81. package/dist/ui/status-bar.d.ts.map +1 -1
  82. package/dist/ui/status-bar.js +10 -4
  83. package/dist/ui/status-bar.js.map +1 -1
  84. package/dist/utils/logger.d.ts +15 -0
  85. package/dist/utils/logger.d.ts.map +1 -1
  86. package/dist/utils/logger.js +47 -0
  87. package/dist/utils/logger.js.map +1 -1
  88. 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, useStdin } from 'ink';
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
- const [chatScrollOffset, setChatScrollOffset] = useState(0);
72
- const [scrollMode, setScrollMode] = useState('follow');
73
- const [newMessagesWhilePaused, setNewMessagesWhilePaused] = useState(false);
74
- const visibleMessageCountRef = useRef(0);
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 [pendingImage, setPendingImage] = useState(null);
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
- // Register soft-cancel hook for the SIGINT handler in interactive.ts.
171
- // While isProcessing, interactive.ts's onSIGINT calls this instead of process.exit().
172
- useEffect(() => {
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
- let userContent = input;
308
- if (pendingImage) {
309
- userContent = [
310
- { type: 'text', text: input },
311
- { type: 'image_url', image_url: { url: `data:${pendingImage.mimeType};base64,${pendingImage.base64}` } },
312
- ];
313
- setPendingImage(null);
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
- setChatScrollOffset(0);
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 in chat messages
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: updatedCalls, status: 'live' };
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
- setMessages(prev => {
368
- const last = prev[prev.length - 1];
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
- // Convert live tool activity card to compact summary
444
- if (liveToolMessageIndexRef.current >= 0 && toolHistory.length > 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: toolHistory, status: 'compact' }),
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
- // Don't show error on cancellation
463
- if (error.name === 'AbortError' || error.message.includes('abort') || error.message.includes('cancel')) {
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 msg = error.message || '';
467
- let friendlyMsg;
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
- setStatusText(i18n.t('ready'));
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: delegate to interactive.ts SIGINT handler.
531
- // - During processing: soft cancel (via __agentSoftCancel)
532
- // - During Ready: double Ctrl+C guard (first shows hint, second exits)
533
- // - Never call exit() here that would bypass the double-Ctrl+C guard.
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 (isProcessing && abortControllerRef.current) {
536
- abortControllerRef.current.abort();
537
- setStatusText(i18n.t('cancelled'));
538
- if (pendingApprovalResolveRef.current) {
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
- // When not processing: set flag so SIGINT handler can exit immediately
546
- // (interactive.ts checks __pendingExit for immediate exit after agent finishes)
547
- const proc = process;
548
- proc.__pendingExit = true;
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
- // Scroll chat history always works regardless of processing state.
636
- // PageUp: scroll up by ~half a screen
637
- if (key.pageUp) {
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
- // ArrowUp/ArrowDown: scroll by 1 line, but only when InputBar is disabled (processing)
742
- // When InputBar is active, arrows belong to input history/suggestions.
743
- if (key.upArrow && isProcessing) {
744
- const visibleCount = messages.filter(m => m.role !== 'tool').length;
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
- setPendingImage(null);
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', height: '100%', children: [setupStep !== 'done'
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', flexGrow: 1, children: [_jsx(Logo, {}), _jsx(ChatView, { messages: messages, scrollOffset: chatScrollOffset, hasNewMessages: newMessagesWhilePaused }), 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: [[
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: isProcessing, onClear: handleClear, onExit: handleExit, isMasked: setupStep === 'apikey', isSetupMode: setupStep !== 'done', blockInput: setupStep === 'done' && (pendingApproval !== null || pendingClear), emptyHint: emptyInputHint, onImagePaste: (base64, mimeType) => {
907
- const model = config.model ?? '';
908
- if (!model.includes('vl') && !model.includes('vision')) {
909
- setMessages(prev => [...prev, {
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