@serjm/deepseek-code 0.4.2 → 0.4.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/CHANGELOG.md +75 -0
  2. package/README.md +72 -108
  3. package/README.ru.md +73 -108
  4. package/dist/api/index.d.ts +9 -0
  5. package/dist/api/index.d.ts.map +1 -1
  6. package/dist/api/index.js +65 -2
  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 +63 -41
  17. package/dist/commands/index.js.map +1 -1
  18. package/dist/config/defaults.d.ts.map +1 -1
  19. package/dist/config/defaults.js +8 -7
  20. package/dist/config/defaults.js.map +1 -1
  21. package/dist/core/agent-loop.d.ts +44 -2
  22. package/dist/core/agent-loop.d.ts.map +1 -1
  23. package/dist/core/agent-loop.js +318 -59
  24. package/dist/core/agent-loop.js.map +1 -1
  25. package/dist/core/i18n.d.ts +3 -0
  26. package/dist/core/i18n.d.ts.map +1 -1
  27. package/dist/core/i18n.js +9 -0
  28. package/dist/core/i18n.js.map +1 -1
  29. package/dist/core/metrics.d.ts +3 -1
  30. package/dist/core/metrics.d.ts.map +1 -1
  31. package/dist/core/metrics.js +34 -5
  32. package/dist/core/metrics.js.map +1 -1
  33. package/dist/tools/bash.d.ts.map +1 -1
  34. package/dist/tools/bash.js +299 -20
  35. package/dist/tools/bash.js.map +1 -1
  36. package/dist/tools/chrome.d.ts.map +1 -1
  37. package/dist/tools/chrome.js +1 -0
  38. package/dist/tools/chrome.js.map +1 -1
  39. package/dist/tools/glob.d.ts.map +1 -1
  40. package/dist/tools/glob.js +40 -3
  41. package/dist/tools/glob.js.map +1 -1
  42. package/dist/tools/grep.d.ts.map +1 -1
  43. package/dist/tools/grep.js +69 -13
  44. package/dist/tools/grep.js.map +1 -1
  45. package/dist/tools/read.d.ts.map +1 -1
  46. package/dist/tools/read.js +91 -0
  47. package/dist/tools/read.js.map +1 -1
  48. package/dist/tools/types.d.ts +21 -1
  49. package/dist/tools/types.d.ts.map +1 -1
  50. package/dist/tools/types.js +34 -0
  51. package/dist/tools/types.js.map +1 -1
  52. package/dist/ui/app.d.ts.map +1 -1
  53. package/dist/ui/app.js +234 -162
  54. package/dist/ui/app.js.map +1 -1
  55. package/dist/ui/chat-view.d.ts +24 -3
  56. package/dist/ui/chat-view.d.ts.map +1 -1
  57. package/dist/ui/chat-view.js +116 -58
  58. package/dist/ui/chat-view.js.map +1 -1
  59. package/dist/ui/input-bar.d.ts.map +1 -1
  60. package/dist/ui/input-bar.js +38 -4
  61. package/dist/ui/input-bar.js.map +1 -1
  62. package/dist/ui/setup-wizard.js +1 -1
  63. package/dist/ui/setup-wizard.js.map +1 -1
  64. package/dist/ui/status-bar.d.ts +5 -1
  65. package/dist/ui/status-bar.d.ts.map +1 -1
  66. package/dist/ui/status-bar.js +10 -4
  67. package/dist/ui/status-bar.js.map +1 -1
  68. package/dist/utils/logger.d.ts +15 -0
  69. package/dist/utils/logger.d.ts.map +1 -1
  70. package/dist/utils/logger.js +47 -0
  71. package/dist/utils/logger.js.map +1 -1
  72. 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';
@@ -14,11 +14,13 @@ import { subAgentManager } from '../core/subagent.js';
14
14
  import { skillsManager } from '../core/skills.js';
15
15
  import { lspManager } from '../core/lsp.js';
16
16
  import { scheduler } from '../core/scheduler.js';
17
+ import { chromeManager } from '../tools/chrome-manager.js';
17
18
  import { themeManager } from '../core/themes.js';
18
19
  import { i18n } from '../core/i18n.js';
19
20
  import { Logo, SetupWizard, useSetupWizard } from './setup-wizard.js';
20
21
  import { executeSlashCommand } from '../commands/index.js';
21
22
  import { checkLatestVersion } from '../commands/update-checker.js';
23
+ import { logEvent } from '../utils/logger.js';
22
24
  /** Empty input hint timeout in ms before showing the guide text */
23
25
  const EMPTY_INPUT_HINT_DELAY = 2000;
24
26
  function stripExecutionSummary(content) {
@@ -42,6 +44,33 @@ function historyForModel(messages) {
42
44
  return [message];
43
45
  });
44
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
+ }
45
74
  // Setup wizard step components are now in ./setup-wizard.tsx
46
75
  // Logo, SetupWizard, useSetupWizard imported from there
47
76
  export function App({ config, options }) {
@@ -49,12 +78,20 @@ export function App({ config, options }) {
49
78
  const [approvalMode, setApprovalMode] = useState(options.approvalMode ?? (options.turbo ? 'turbo' : config.approvalMode));
50
79
  const [messages, setMessages] = useState([]);
51
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]);
52
85
  const [statusText, setStatusText] = useState(i18n.t('ready'));
53
86
  const [localApiKey, setLocalApiKey] = useState(config.apiKey || '');
54
87
  const agentLoopRef = useRef(null);
55
88
  const abortControllerRef = useRef(null);
56
89
  const pendingApprovalResolveRef = useRef(null);
57
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([]);
58
95
  const prevToolCallsRef = useRef([]);
59
96
  const [toolCalls, setToolCalls] = useState([]);
60
97
  const [pendingApproval, setPendingApproval] = useState(null);
@@ -67,20 +104,69 @@ export function App({ config, options }) {
67
104
  const initializedRef = useRef(false);
68
105
  const [emptyInputHint, setEmptyInputHint] = useState(false);
69
106
  const emptyInputTimerRef = useRef(null);
70
- const [chatScrollOffset, setChatScrollOffset] = useState(0);
71
- const [scrollMode, setScrollMode] = useState('follow');
72
- const [newMessagesWhilePaused, setNewMessagesWhilePaused] = useState(false);
73
- 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);
74
111
  const [contextPercent, setContextPercent] = useState(0);
112
+ const [compactProgress, setCompactProgress] = useState(0);
75
113
  const [totalTokens, setTotalTokens] = useState(0);
76
114
  const [estimatedCost, setEstimatedCost] = useState(0);
77
115
  const [pendingImage, setPendingImage] = useState(null);
116
+ const [followUpCount, setFollowUpCount] = useState(0);
78
117
  const [themePicker, setThemePicker] = useState(null);
79
118
  const [modelPicker, setModelPicker] = useState(null);
80
119
  const [langPicker, setLangPicker] = useState(null);
81
120
  const [serviceNotice, setServiceNotice] = useState(null);
82
121
  const serviceNoticeTimerRef = useRef(null);
83
122
  const budgetRef = useRef(undefined);
123
+ // Double Ctrl+C to exit when idle/paused (raw mode owns Ctrl+C on all platforms).
124
+ const ctrlCExitArmedRef = useRef(false);
125
+ const ctrlCExitTimerRef = useRef(null);
126
+ // ── Stream chunk batching ──────────────────────────────────────────────
127
+ const pendingChunksRef = useRef('');
128
+ const flushTimerRef = useRef(null);
129
+ // Flush buffered assistant text into the message list. Starting a new text
130
+ // block also finalizes the previous live tool card (→ compact) so it commits
131
+ // to the Static scrollback as a completed item.
132
+ const flushPending = useCallback(() => {
133
+ if (flushTimerRef.current) {
134
+ clearTimeout(flushTimerRef.current);
135
+ flushTimerRef.current = null;
136
+ }
137
+ const buffered = pendingChunksRef.current;
138
+ if (!buffered)
139
+ return;
140
+ pendingChunksRef.current = '';
141
+ setMessages(prev => {
142
+ const last = prev[prev.length - 1];
143
+ if (last?.role === 'assistant') {
144
+ const updated = [...prev];
145
+ updated[updated.length - 1] = { ...last, content: last.content + buffered };
146
+ return updated;
147
+ }
148
+ const updated = [...prev];
149
+ const cardIdx = liveToolMessageIndexRef.current;
150
+ if (cardIdx >= 0 && cardIdx < updated.length && updated[cardIdx]?.role === 'tool' && currentCardToolsRef.current.length > 0) {
151
+ updated[cardIdx] = {
152
+ role: 'tool',
153
+ content: JSON.stringify({ type: 'tool_activity_card', toolCalls: currentCardToolsRef.current, status: 'compact' }),
154
+ };
155
+ }
156
+ liveToolMessageIndexRef.current = -1;
157
+ currentCardToolsRef.current = [];
158
+ updated.push({ role: 'assistant', content: buffered });
159
+ return updated;
160
+ });
161
+ }, []);
162
+ const scheduleFlush = useCallback(() => {
163
+ if (flushTimerRef.current)
164
+ return;
165
+ flushTimerRef.current = setTimeout(() => {
166
+ flushTimerRef.current = null;
167
+ flushPending();
168
+ }, 75);
169
+ }, [flushPending]);
84
170
  const addServiceNotice = useCallback((text) => {
85
171
  setServiceNotice(text);
86
172
  if (serviceNoticeTimerRef.current)
@@ -139,6 +225,10 @@ export function App({ config, options }) {
139
225
  subAgentManager.loadFromDir(),
140
226
  scheduler.load(),
141
227
  ]);
228
+ // Применяем сохранённый режим Chrome (headed/headless) из конфига.
229
+ // Только обновляет headlessMode в chrome-manager, НЕ запускает браузер.
230
+ // Браузер стартует только при реальном вызове chrome tool, browser-test или /chrome.
231
+ chromeManager.setHeadlessMode(config.chromeHeadless ?? false);
142
232
  // Chrome не инициализируется при старте.
143
233
  // Он запускается только когда:
144
234
  // - агент вызывает chrome tool
@@ -162,60 +252,13 @@ export function App({ config, options }) {
162
252
  setStatusText(i18n.t('ready'));
163
253
  })();
164
254
  }, []);
165
- // Register soft-cancel hook for the SIGINT handler in interactive.ts.
166
- // While isProcessing, interactive.ts's onSIGINT calls this instead of process.exit().
167
- useEffect(() => {
168
- const proc = process;
169
- if (isProcessing) {
170
- proc.__agentSoftCancel = () => {
171
- abortControllerRef.current?.abort();
172
- if (pendingApprovalResolveRef.current) {
173
- pendingApprovalResolveRef.current(false);
174
- pendingApprovalResolveRef.current = null;
175
- }
176
- setPendingApproval(null);
177
- setStatusText(i18n.t('cancelled'));
178
- };
179
- }
180
- else {
181
- proc.__agentSoftCancel = undefined;
182
- }
183
- return () => { proc.__agentSoftCancel = undefined; };
184
- }, [isProcessing]);
185
- const { stdin } = useStdin();
186
- // Compensate scroll offset when new visible messages arrive while paused
187
- useEffect(() => {
188
- if (setupStepRef.current !== 'done')
189
- return;
190
- const visible = messages.filter(m => m.role !== 'tool').length;
191
- if (scrollMode === 'paused' && visible > visibleMessageCountRef.current) {
192
- const diff = visible - visibleMessageCountRef.current;
193
- setChatScrollOffset(prev => prev + diff);
194
- setNewMessagesWhilePaused(true);
195
- }
196
- visibleMessageCountRef.current = visible;
197
- }, [messages.length, scrollMode]);
255
+ // __agentSoftCancel is now set synchronously in handleSubmit (before agentLoop.run())
256
+ // to eliminate the async useEffect race window on Windows where SIGINT fires
257
+ // before the effect commits. Cleanup is handled in finally block.
198
258
  // Sync prevToolCallsRef with toolCalls state
199
259
  useEffect(() => {
200
260
  prevToolCallsRef.current = toolCalls;
201
261
  }, [toolCalls]);
202
- // Detect End key from raw stdin (Ink does not expose it in useInput)
203
- useEffect(() => {
204
- if (!stdin)
205
- return;
206
- const handler = (data) => {
207
- if (setupStepRef.current !== 'done')
208
- return;
209
- const seq = data.toString();
210
- if (seq === '\x1b[F' || seq === '\x1b[4~' || seq === '\x1b[8~' || seq === '\x1bOF') {
211
- setChatScrollOffset(0);
212
- setScrollMode('follow');
213
- setNewMessagesWhilePaused(false);
214
- }
215
- };
216
- stdin.on('data', handler);
217
- return () => { stdin.off('data', handler); };
218
- }, [stdin]);
219
262
  // Slash commands — delegated to commands/index.ts
220
263
  const handleSlashCommand = useCallback(async (input) => {
221
264
  const ctx = {
@@ -256,8 +299,18 @@ export function App({ config, options }) {
256
299
  emptyInputTimerRef.current = setTimeout(() => setEmptyInputHint(false), EMPTY_INPUT_HINT_DELAY);
257
300
  return;
258
301
  }
259
- if (isProcessing)
302
+ if (isProcessing) {
303
+ // Live follow-up: inject into current active AgentLoop
304
+ if (agentLoopRef.current) {
305
+ agentLoopRef.current.addUserFollowUp(input.trim());
306
+ setFollowUpCount(c => c + 1);
307
+ addServiceNotice('[queue] Follow-up #' + (followUpCount + 1) + ' added. Agent will continue after current step.');
308
+ }
309
+ else {
310
+ addServiceNotice('Agent is not active. Send it as a new message.');
311
+ }
260
312
  return;
313
+ }
261
314
  // Handle setup wizard steps
262
315
  if (setupStep === 'apikey') {
263
316
  await handleApiKeySubmit(input);
@@ -273,6 +326,7 @@ export function App({ config, options }) {
273
326
  }]);
274
327
  return;
275
328
  }
329
+ setIsPaused(false);
276
330
  if (input.startsWith('/')) {
277
331
  try {
278
332
  const handled = await handleSlashCommand(input);
@@ -299,6 +353,23 @@ export function App({ config, options }) {
299
353
  }
300
354
  const abortController = new AbortController();
301
355
  abortControllerRef.current = abortController;
356
+ // SYNCHRONOUS: register soft-cancel hook before agentLoop.run().
357
+ // The old useEffect-based registration had a race window on Windows where
358
+ // SIGINT could fire between isProcessing=true and the effect commit.
359
+ const proc = process;
360
+ proc.__agentSoftCancel = () => {
361
+ logEvent('__agentSoftCancel invoked');
362
+ abortController.abort();
363
+ setIsPaused(true);
364
+ if (pendingApprovalResolveRef.current) {
365
+ pendingApprovalResolveRef.current(false);
366
+ pendingApprovalResolveRef.current = null;
367
+ }
368
+ setPendingApproval(null);
369
+ setStatusText(i18n.t('paused'));
370
+ };
371
+ proc.__agentAbortController = abortController;
372
+ logEvent('agent run: softCancel registered');
302
373
  let userContent = input;
303
374
  if (pendingImage) {
304
375
  userContent = [
@@ -320,9 +391,7 @@ export function App({ config, options }) {
320
391
  setStatusText(i18n.t('working'));
321
392
  setToolCalls([]);
322
393
  liveToolMessageIndexRef.current = -1;
323
- setChatScrollOffset(0);
324
- setScrollMode('follow');
325
- setNewMessagesWhilePaused(false);
394
+ currentCardToolsRef.current = [];
326
395
  try {
327
396
  await hooksManager.execute('UserPromptSubmit', {
328
397
  event: 'UserPromptSubmit',
@@ -335,21 +404,22 @@ export function App({ config, options }) {
335
404
  signal: abortController.signal,
336
405
  budget: budgetRef.current,
337
406
  onToolCall: (tc) => {
407
+ // Commit any buffered assistant text first so it lands above the card.
408
+ flushPending();
338
409
  const updatedCalls = [...prevToolCallsRef.current, tc];
339
410
  prevToolCallsRef.current = updatedCalls;
340
411
  setToolCalls(updatedCalls);
412
+ currentCardToolsRef.current = [...currentCardToolsRef.current, tc];
341
413
  setStatusText(`[tool] ${tc.name}...`);
342
- // Add/update live tool activity card in chat messages
414
+ // Add/update the live tool activity card (one card per tool batch).
343
415
  setMessages(prev => {
344
416
  const idx = liveToolMessageIndexRef.current;
345
- const card = { type: 'tool_activity_card', toolCalls: updatedCalls, status: 'live' };
417
+ const card = { type: 'tool_activity_card', toolCalls: currentCardToolsRef.current, status: 'live' };
346
418
  if (idx >= 0 && idx < prev.length && prev[idx]?.role === 'tool') {
347
- // Update existing card
348
419
  const updated = [...prev];
349
420
  updated[idx] = { role: 'tool', content: JSON.stringify(card) };
350
421
  return updated;
351
422
  }
352
- // Add new card
353
423
  liveToolMessageIndexRef.current = prev.length;
354
424
  return [...prev, { role: 'tool', content: JSON.stringify(card) }];
355
425
  });
@@ -359,16 +429,8 @@ export function App({ config, options }) {
359
429
  },
360
430
  onReasoningChunk: () => { },
361
431
  onStreamChunk: (chunk) => {
362
- setMessages(prev => {
363
- const last = prev[prev.length - 1];
364
- if (last?.role === 'assistant') {
365
- // If last content is empty, replace with chunk; otherwise append
366
- const updated = [...prev];
367
- updated[updated.length - 1] = { ...last, content: last.content + chunk };
368
- return updated;
369
- }
370
- return [...prev, { role: 'assistant', content: chunk }];
371
- });
432
+ pendingChunksRef.current += chunk;
433
+ scheduleFlush();
372
434
  },
373
435
  // onResponse intentionally removed — onStreamChunk handles all text
374
436
  // Avoids duplicate "assistant" message push that caused text doubling
@@ -377,6 +439,18 @@ export function App({ config, options }) {
377
439
  // Handled by handleSubmit catch block — adding here would create duplicate
378
440
  // assistant messages, breaking the conversation structure for the next request
379
441
  },
442
+ onCompactStart: () => {
443
+ setCompactProgress(5);
444
+ setStatusText('Compacting context...');
445
+ },
446
+ onCompactProgress: (event) => {
447
+ setCompactProgress(event.progress);
448
+ setStatusText(`Compacting context ${event.progress}%...`);
449
+ },
450
+ onCompactEnd: (event) => {
451
+ setCompactProgress(0);
452
+ setStatusText(event.phase === 'done' ? 'Context compacted' : 'Context compact failed');
453
+ },
380
454
  onApprovalRequest: async (toolName, args) => {
381
455
  if (approvalModeRef.current === 'turbo')
382
456
  return true;
@@ -394,6 +468,10 @@ export function App({ config, options }) {
394
468
  },
395
469
  });
396
470
  const finalResponse = await agentLoopRef.current.run(input, historyForModel(messages));
471
+ // Abort during run: skip session save, let finally handle UI reset
472
+ if (abortController.signal.aborted) {
473
+ return;
474
+ }
397
475
  const toolHistory = agentLoopRef.current.getToolCallHistory();
398
476
  const bundleFile = await writeExecutionBundle({
399
477
  sessionId: sessionIdRef.current,
@@ -435,15 +513,16 @@ export function App({ config, options }) {
435
513
  // Single batch: all final UI updates at once (no await between setState calls)
436
514
  setIsProcessing(false);
437
515
  setStatusText(i18n.t('ready'));
438
- // Convert live tool activity card to compact summary
439
- if (liveToolMessageIndexRef.current >= 0 && toolHistory.length > 0) {
516
+ // Finalize the last live tool batch (if the turn ended on tools) to compact.
517
+ if (liveToolMessageIndexRef.current >= 0 && currentCardToolsRef.current.length > 0) {
518
+ const cardTools = currentCardToolsRef.current;
440
519
  setMessages(prev => {
441
520
  const idx = liveToolMessageIndexRef.current;
442
521
  if (idx >= 0 && idx < prev.length && prev[idx]?.role === 'tool') {
443
522
  const updated = [...prev];
444
523
  updated[idx] = {
445
524
  role: 'tool',
446
- content: JSON.stringify({ type: 'tool_activity_card', toolCalls: toolHistory, status: 'compact' }),
525
+ content: JSON.stringify({ type: 'tool_activity_card', toolCalls: cardTools, status: 'compact' }),
447
526
  };
448
527
  return updated;
449
528
  }
@@ -451,33 +530,16 @@ export function App({ config, options }) {
451
530
  });
452
531
  }
453
532
  liveToolMessageIndexRef.current = -1;
533
+ currentCardToolsRef.current = [];
454
534
  }
455
535
  catch (err) {
456
536
  const error = err;
457
- // Don't show error on cancellation
458
- if (error.name === 'AbortError' || error.message.includes('abort') || error.message.includes('cancel')) {
537
+ // User-triggered cancellation is expected and should not become an error message.
538
+ if (isUserCancellationError(error, abortController.signal)) {
459
539
  return;
460
540
  }
461
- const msg = error.message || '';
462
- let friendlyMsg;
463
- if (msg.includes('401') || msg.includes('403') || msg.includes('Unauthorized')) {
464
- friendlyMsg = i18n.t('apiErrorAuth');
465
- }
466
- else if (msg.includes('429') || msg.includes('rate limit')) {
467
- friendlyMsg = i18n.t('apiErrorRateLimit');
468
- }
469
- else if (/5\d{2}|server error|Service Unavailable/i.test(msg)) {
470
- friendlyMsg = i18n.t('apiErrorServer');
471
- }
472
- else if (/ECONNRESET|ECONNREFUSED|ENOTFOUND/i.test(msg)) {
473
- friendlyMsg = i18n.t('apiErrorNetwork');
474
- }
475
- else if (/ETIMEDOUT|timed out/i.test(msg)) {
476
- friendlyMsg = i18n.t('apiErrorTimeout');
477
- }
478
- else {
479
- friendlyMsg = `${i18n.t('error')}: ${msg}`;
480
- }
541
+ const friendlyMsg = formatAgentError(error);
542
+ const toolHistory = agentLoopRef.current?.getToolCallHistory() ?? [];
481
543
  setMessages(prev => [...prev, {
482
544
  role: 'assistant',
483
545
  content: friendlyMsg,
@@ -487,16 +549,31 @@ export function App({ config, options }) {
487
549
  prompt: input,
488
550
  error: friendlyMsg,
489
551
  approvalMode,
552
+ toolCalls: toolHistory.map(toolCall => ({
553
+ name: toolCall.name,
554
+ status: toolCall.status,
555
+ durationMs: toolCall.durationMs,
556
+ error: toolCall.error,
557
+ })),
490
558
  });
491
559
  const bundleFile = await writeExecutionBundle({
492
560
  sessionId: sessionIdRef.current,
493
561
  prompt: input,
494
562
  error: friendlyMsg,
495
563
  approvalMode,
564
+ toolCalls: toolHistory.map(toolCall => ({
565
+ id: toolCall.id,
566
+ name: toolCall.name,
567
+ status: toolCall.status,
568
+ durationMs: toolCall.durationMs,
569
+ error: toolCall.error,
570
+ result: toolCall.result,
571
+ })),
496
572
  });
497
573
  await saveSession({
498
574
  id: sessionIdRef.current,
499
575
  messageCount: messages.length + 2,
576
+ toolCallCount: toolHistory.length,
500
577
  approvalMode,
501
578
  lastPrompt: input,
502
579
  lastError: friendlyMsg,
@@ -506,9 +583,14 @@ export function App({ config, options }) {
506
583
  });
507
584
  }
508
585
  finally {
586
+ logEvent('agent run: finally (clearing softCancel)');
509
587
  // Safety net: ensure UI is always reset regardless of exit path
588
+ flushPending();
510
589
  setIsProcessing(false);
511
- setStatusText(i18n.t('ready'));
590
+ // Don't overwrite status if user paused the agent
591
+ if (!isPausedRef.current) {
592
+ setStatusText(i18n.t('ready'));
593
+ }
512
594
  // Clear any pending approval (covers error/abort exit paths)
513
595
  if (pendingApprovalResolveRef.current) {
514
596
  pendingApprovalResolveRef.current(false);
@@ -518,29 +600,58 @@ export function App({ config, options }) {
518
600
  if (abortControllerRef.current === abortController) {
519
601
  abortControllerRef.current = null;
520
602
  }
603
+ // Cleanup synchronous SIGINT hooks
604
+ const proc2 = process;
605
+ if (proc2.__agentSoftCancel)
606
+ proc2.__agentSoftCancel = undefined;
607
+ if (proc2.__agentAbortController === abortController)
608
+ proc2.__agentAbortController = undefined;
521
609
  }
522
610
  }, [messages, isProcessing, setupStep, handleApiKeySubmit, handleSlashCommand, approvalMode, config, localApiKey]);
523
611
  useInput((_input, key) => {
524
612
  const step = setupStepRef.current;
525
- // Ctrl+C: delegate to interactive.ts SIGINT handler.
526
- // - During processing: soft cancel (via __agentSoftCancel)
527
- // - During Ready: double Ctrl+C guard (first shows hint, second exits)
528
- // - Never call exit() here that would bypass the double-Ctrl+C guard.
613
+ // Ctrl+C is handled here because Ink raw mode delivers it as input (not a
614
+ // SIGINT signal) on all platforms when exitOnCtrlC is false. The SIGINT
615
+ // handler in interactive.ts is only a fallback for non-TTY runs.
616
+ // - While the agent runs: first Ctrl+C aborts/pauses, never exits.
617
+ // - When idle or already paused: double Ctrl+C within 3s exits.
529
618
  if (key.ctrl && _input === 'c') {
530
- if (isProcessing && abortControllerRef.current) {
619
+ logEvent('useInput Ctrl+C', { isProcessing, isPaused, hasAbort: !!abortControllerRef.current, step });
620
+ if (isProcessing && abortControllerRef.current && !isPaused) {
621
+ logEvent('useInput Ctrl+C -> abort+pause');
531
622
  abortControllerRef.current.abort();
532
- setStatusText(i18n.t('cancelled'));
623
+ setIsPaused(true);
624
+ setStatusText(i18n.t('paused'));
533
625
  if (pendingApprovalResolveRef.current) {
534
626
  pendingApprovalResolveRef.current(false);
535
627
  pendingApprovalResolveRef.current = null;
536
628
  }
537
629
  setPendingApproval(null);
630
+ // Reset exit-arming: the interrupt is the user's action this press.
631
+ ctrlCExitArmedRef.current = false;
632
+ if (ctrlCExitTimerRef.current) {
633
+ clearTimeout(ctrlCExitTimerRef.current);
634
+ ctrlCExitTimerRef.current = null;
635
+ }
538
636
  return;
539
637
  }
540
- // When not processing: set flag so SIGINT handler can exit immediately
541
- // (interactive.ts checks __pendingExit for immediate exit after agent finishes)
542
- const proc = process;
543
- proc.__pendingExit = true;
638
+ // Idle or paused second press within the window exits.
639
+ if (ctrlCExitArmedRef.current) {
640
+ logEvent('useInput Ctrl+C -> doublePress -> exit()');
641
+ if (ctrlCExitTimerRef.current)
642
+ clearTimeout(ctrlCExitTimerRef.current);
643
+ exit();
644
+ return;
645
+ }
646
+ logEvent('useInput Ctrl+C -> firstPress armed');
647
+ ctrlCExitArmedRef.current = true;
648
+ addServiceNotice(i18n.t('ctrlCHint'));
649
+ if (ctrlCExitTimerRef.current)
650
+ clearTimeout(ctrlCExitTimerRef.current);
651
+ ctrlCExitTimerRef.current = setTimeout(() => {
652
+ ctrlCExitArmedRef.current = false;
653
+ ctrlCExitTimerRef.current = null;
654
+ }, 3000);
544
655
  return;
545
656
  }
546
657
  // When not in setup mode, let InputBar handle all keyboard input
@@ -627,26 +738,9 @@ export function App({ config, options }) {
627
738
  });
628
739
  return;
629
740
  }
630
- // Scroll chat history always works regardless of processing state.
631
- // PageUp: scroll up by ~half a screen
632
- if (key.pageUp) {
633
- const visibleCount = messages.filter(m => m.role !== 'tool').length;
634
- const next = Math.min(chatScrollOffset + 10, Math.max(0, visibleCount - 1));
635
- if (next > 0)
636
- setScrollMode('paused');
637
- setChatScrollOffset(next);
638
- return;
639
- }
640
- // PageDown: scroll down by ~half a screen
641
- if (key.pageDown) {
642
- const next = Math.max(0, chatScrollOffset - 10);
643
- setChatScrollOffset(next);
644
- if (next === 0) {
645
- setScrollMode('follow');
646
- setNewMessagesWhilePaused(false);
647
- }
648
- return;
649
- }
741
+ // Chat history scrolling is handled natively by the terminal (messages are
742
+ // committed to the scrollback via Ink <Static>), so PageUp/PageDown are no
743
+ // longer intercepted here.
650
744
  // Theme picker: interactive selection
651
745
  if (themePicker) {
652
746
  if (key.escape) {
@@ -733,23 +827,10 @@ export function App({ config, options }) {
733
827
  }
734
828
  return;
735
829
  }
736
- // ArrowUp/ArrowDown: scroll by 1 line, but only when InputBar is disabled (processing)
737
- // When InputBar is active, arrows belong to input history/suggestions.
738
- if (key.upArrow && isProcessing) {
739
- const visibleCount = messages.filter(m => m.role !== 'tool').length;
740
- const next = Math.min(chatScrollOffset + 1, Math.max(0, visibleCount - 1));
741
- if (next > 0)
742
- setScrollMode('paused');
743
- setChatScrollOffset(next);
744
- return;
745
- }
746
- if (key.downArrow && isProcessing) {
747
- const next = Math.max(0, chatScrollOffset - 1);
748
- setChatScrollOffset(next);
749
- if (next === 0) {
750
- setScrollMode('follow');
751
- setNewMessagesWhilePaused(false);
752
- }
830
+ // Esc when paused: dismiss pause, return to ready
831
+ if (key.escape && isPaused && !pendingApproval && !pendingClear && !themePicker && !modelPicker && !langPicker) {
832
+ setIsPaused(false);
833
+ setStatusText(i18n.t('ready'));
753
834
  return;
754
835
  }
755
836
  return;
@@ -861,9 +942,7 @@ export function App({ config, options }) {
861
942
  setToolCalls([]);
862
943
  setPendingApproval(null);
863
944
  setPendingImage(null);
864
- setScrollMode('follow');
865
- setNewMessagesWhilePaused(false);
866
- setChatScrollOffset(0);
945
+ setChatEpoch(e => e + 1); // remount Static so the scrollback resets
867
946
  liveToolMessageIndexRef.current = -1;
868
947
  setServiceNotice(null);
869
948
  if (serviceNoticeTimerRef.current) {
@@ -882,7 +961,7 @@ export function App({ config, options }) {
882
961
  }, [messages.length, toolCalls.length, executeClear]);
883
962
  const handleExit = useCallback(() => { exit(); }, [exit]);
884
963
  const colors = themeManager.getColors();
885
- return (_jsxs(Box, { flexDirection: 'column', height: '100%', children: [setupStep !== 'done'
964
+ return (_jsxs(Box, { flexDirection: 'column', children: [setupStep !== 'done'
886
965
  ? _jsx(SetupWizard, { state: {
887
966
  step: setupStep,
888
967
  apiKeyError,
@@ -893,21 +972,14 @@ export function App({ config, options }) {
893
972
  langOptions,
894
973
  modeOptions,
895
974
  } })
896
- : (_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: [[
975
+ : (_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: [[
897
976
  '[ok] Подтвердить',
898
977
  '[no] Отклонить',
899
978
  `[mute] Не спрашивать для "${pendingApproval.toolName}"`,
900
979
  '[turbo] Выполнять всё без вопросов',
901
- ].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) => {
902
- const model = config.model ?? '';
903
- if (!model.includes('vl') && !model.includes('vision')) {
904
- setMessages(prev => [...prev, {
905
- role: 'assistant',
906
- content: `[warn] Вставка изображения требует модель с поддержкой vision.\nТекущая модель: ${model || 'неизвестно'}\nИспользуйте модель с "vl" или "vision" в названии.`,
907
- }]);
908
- return;
909
- }
910
- setPendingImage({ base64, mimeType });
911
- } }), _jsx(StatusBar, { mode: approvalMode, status: statusText, messageCount: messages.length, isProcessing: isProcessing, contextPercent: contextPercent, totalTokens: totalTokens, estimatedCost: estimatedCost, model: config.model })] }));
980
+ ].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,
981
+ // disabled is no longer tied to isProcessing — input is allowed during processing.
982
+ // blockInput still handles approval/clear dialogs.
983
+ onClear: handleClear, onExit: handleExit, isMasked: setupStep === 'apikey', isSetupMode: setupStep !== 'done', blockInput: setupStep === 'done' && (pendingApproval !== null || pendingClear), emptyHint: emptyInputHint, onImagePaste: (base64, mimeType) => setPendingImage({ base64, mimeType }) }), _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 })] }));
912
984
  }
913
985
  //# sourceMappingURL=app.js.map