@serjm/deepseek-code 0.4.3 → 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 (68) hide show
  1. package/CHANGELOG.md +61 -0
  2. package/README.md +72 -109
  3. package/README.ru.md +73 -109
  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 +26 -21
  17. package/dist/commands/index.js.map +1 -1
  18. package/dist/config/defaults.js +7 -7
  19. package/dist/config/defaults.js.map +1 -1
  20. package/dist/core/agent-loop.d.ts +44 -2
  21. package/dist/core/agent-loop.d.ts.map +1 -1
  22. package/dist/core/agent-loop.js +317 -102
  23. package/dist/core/agent-loop.js.map +1 -1
  24. package/dist/core/i18n.d.ts +3 -0
  25. package/dist/core/i18n.d.ts.map +1 -1
  26. package/dist/core/i18n.js +9 -0
  27. package/dist/core/i18n.js.map +1 -1
  28. package/dist/core/metrics.d.ts +3 -1
  29. package/dist/core/metrics.d.ts.map +1 -1
  30. package/dist/core/metrics.js +34 -5
  31. package/dist/core/metrics.js.map +1 -1
  32. package/dist/tools/bash.d.ts.map +1 -1
  33. package/dist/tools/bash.js +299 -20
  34. package/dist/tools/bash.js.map +1 -1
  35. package/dist/tools/glob.d.ts.map +1 -1
  36. package/dist/tools/glob.js +40 -3
  37. package/dist/tools/glob.js.map +1 -1
  38. package/dist/tools/grep.d.ts.map +1 -1
  39. package/dist/tools/grep.js +69 -13
  40. package/dist/tools/grep.js.map +1 -1
  41. package/dist/tools/read.d.ts.map +1 -1
  42. package/dist/tools/read.js +91 -0
  43. package/dist/tools/read.js.map +1 -1
  44. package/dist/tools/types.d.ts +21 -1
  45. package/dist/tools/types.d.ts.map +1 -1
  46. package/dist/tools/types.js +34 -0
  47. package/dist/tools/types.js.map +1 -1
  48. package/dist/ui/app.d.ts.map +1 -1
  49. package/dist/ui/app.js +229 -162
  50. package/dist/ui/app.js.map +1 -1
  51. package/dist/ui/chat-view.d.ts +24 -3
  52. package/dist/ui/chat-view.d.ts.map +1 -1
  53. package/dist/ui/chat-view.js +116 -58
  54. package/dist/ui/chat-view.js.map +1 -1
  55. package/dist/ui/input-bar.d.ts.map +1 -1
  56. package/dist/ui/input-bar.js +38 -4
  57. package/dist/ui/input-bar.js.map +1 -1
  58. package/dist/ui/setup-wizard.js +1 -1
  59. package/dist/ui/setup-wizard.js.map +1 -1
  60. package/dist/ui/status-bar.d.ts +5 -1
  61. package/dist/ui/status-bar.d.ts.map +1 -1
  62. package/dist/ui/status-bar.js +10 -4
  63. package/dist/ui/status-bar.js.map +1 -1
  64. package/dist/utils/logger.d.ts +15 -0
  65. package/dist/utils/logger.d.ts.map +1 -1
  66. package/dist/utils/logger.js +47 -0
  67. package/dist/utils/logger.js.map +1 -1
  68. 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,69 @@ 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
115
  const [pendingImage, setPendingImage] = useState(null);
116
+ const [followUpCount, setFollowUpCount] = useState(0);
79
117
  const [themePicker, setThemePicker] = useState(null);
80
118
  const [modelPicker, setModelPicker] = useState(null);
81
119
  const [langPicker, setLangPicker] = useState(null);
82
120
  const [serviceNotice, setServiceNotice] = useState(null);
83
121
  const serviceNoticeTimerRef = useRef(null);
84
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]);
85
170
  const addServiceNotice = useCallback((text) => {
86
171
  setServiceNotice(text);
87
172
  if (serviceNoticeTimerRef.current)
@@ -167,60 +252,13 @@ export function App({ config, options }) {
167
252
  setStatusText(i18n.t('ready'));
168
253
  })();
169
254
  }, []);
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]);
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.
203
258
  // Sync prevToolCallsRef with toolCalls state
204
259
  useEffect(() => {
205
260
  prevToolCallsRef.current = toolCalls;
206
261
  }, [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
262
  // Slash commands — delegated to commands/index.ts
225
263
  const handleSlashCommand = useCallback(async (input) => {
226
264
  const ctx = {
@@ -261,8 +299,18 @@ export function App({ config, options }) {
261
299
  emptyInputTimerRef.current = setTimeout(() => setEmptyInputHint(false), EMPTY_INPUT_HINT_DELAY);
262
300
  return;
263
301
  }
264
- 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
+ }
265
312
  return;
313
+ }
266
314
  // Handle setup wizard steps
267
315
  if (setupStep === 'apikey') {
268
316
  await handleApiKeySubmit(input);
@@ -278,6 +326,7 @@ export function App({ config, options }) {
278
326
  }]);
279
327
  return;
280
328
  }
329
+ setIsPaused(false);
281
330
  if (input.startsWith('/')) {
282
331
  try {
283
332
  const handled = await handleSlashCommand(input);
@@ -304,6 +353,23 @@ export function App({ config, options }) {
304
353
  }
305
354
  const abortController = new AbortController();
306
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');
307
373
  let userContent = input;
308
374
  if (pendingImage) {
309
375
  userContent = [
@@ -325,9 +391,7 @@ export function App({ config, options }) {
325
391
  setStatusText(i18n.t('working'));
326
392
  setToolCalls([]);
327
393
  liveToolMessageIndexRef.current = -1;
328
- setChatScrollOffset(0);
329
- setScrollMode('follow');
330
- setNewMessagesWhilePaused(false);
394
+ currentCardToolsRef.current = [];
331
395
  try {
332
396
  await hooksManager.execute('UserPromptSubmit', {
333
397
  event: 'UserPromptSubmit',
@@ -340,21 +404,22 @@ export function App({ config, options }) {
340
404
  signal: abortController.signal,
341
405
  budget: budgetRef.current,
342
406
  onToolCall: (tc) => {
407
+ // Commit any buffered assistant text first so it lands above the card.
408
+ flushPending();
343
409
  const updatedCalls = [...prevToolCallsRef.current, tc];
344
410
  prevToolCallsRef.current = updatedCalls;
345
411
  setToolCalls(updatedCalls);
412
+ currentCardToolsRef.current = [...currentCardToolsRef.current, tc];
346
413
  setStatusText(`[tool] ${tc.name}...`);
347
- // Add/update live tool activity card in chat messages
414
+ // Add/update the live tool activity card (one card per tool batch).
348
415
  setMessages(prev => {
349
416
  const idx = liveToolMessageIndexRef.current;
350
- const card = { type: 'tool_activity_card', toolCalls: updatedCalls, status: 'live' };
417
+ const card = { type: 'tool_activity_card', toolCalls: currentCardToolsRef.current, status: 'live' };
351
418
  if (idx >= 0 && idx < prev.length && prev[idx]?.role === 'tool') {
352
- // Update existing card
353
419
  const updated = [...prev];
354
420
  updated[idx] = { role: 'tool', content: JSON.stringify(card) };
355
421
  return updated;
356
422
  }
357
- // Add new card
358
423
  liveToolMessageIndexRef.current = prev.length;
359
424
  return [...prev, { role: 'tool', content: JSON.stringify(card) }];
360
425
  });
@@ -364,16 +429,8 @@ export function App({ config, options }) {
364
429
  },
365
430
  onReasoningChunk: () => { },
366
431
  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
- });
432
+ pendingChunksRef.current += chunk;
433
+ scheduleFlush();
377
434
  },
378
435
  // onResponse intentionally removed — onStreamChunk handles all text
379
436
  // Avoids duplicate "assistant" message push that caused text doubling
@@ -382,6 +439,18 @@ export function App({ config, options }) {
382
439
  // Handled by handleSubmit catch block — adding here would create duplicate
383
440
  // assistant messages, breaking the conversation structure for the next request
384
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
+ },
385
454
  onApprovalRequest: async (toolName, args) => {
386
455
  if (approvalModeRef.current === 'turbo')
387
456
  return true;
@@ -399,6 +468,10 @@ export function App({ config, options }) {
399
468
  },
400
469
  });
401
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
+ }
402
475
  const toolHistory = agentLoopRef.current.getToolCallHistory();
403
476
  const bundleFile = await writeExecutionBundle({
404
477
  sessionId: sessionIdRef.current,
@@ -440,15 +513,16 @@ export function App({ config, options }) {
440
513
  // Single batch: all final UI updates at once (no await between setState calls)
441
514
  setIsProcessing(false);
442
515
  setStatusText(i18n.t('ready'));
443
- // Convert live tool activity card to compact summary
444
- 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;
445
519
  setMessages(prev => {
446
520
  const idx = liveToolMessageIndexRef.current;
447
521
  if (idx >= 0 && idx < prev.length && prev[idx]?.role === 'tool') {
448
522
  const updated = [...prev];
449
523
  updated[idx] = {
450
524
  role: 'tool',
451
- content: JSON.stringify({ type: 'tool_activity_card', toolCalls: toolHistory, status: 'compact' }),
525
+ content: JSON.stringify({ type: 'tool_activity_card', toolCalls: cardTools, status: 'compact' }),
452
526
  };
453
527
  return updated;
454
528
  }
@@ -456,33 +530,16 @@ export function App({ config, options }) {
456
530
  });
457
531
  }
458
532
  liveToolMessageIndexRef.current = -1;
533
+ currentCardToolsRef.current = [];
459
534
  }
460
535
  catch (err) {
461
536
  const error = err;
462
- // Don't show error on cancellation
463
- 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)) {
464
539
  return;
465
540
  }
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
- }
541
+ const friendlyMsg = formatAgentError(error);
542
+ const toolHistory = agentLoopRef.current?.getToolCallHistory() ?? [];
486
543
  setMessages(prev => [...prev, {
487
544
  role: 'assistant',
488
545
  content: friendlyMsg,
@@ -492,16 +549,31 @@ export function App({ config, options }) {
492
549
  prompt: input,
493
550
  error: friendlyMsg,
494
551
  approvalMode,
552
+ toolCalls: toolHistory.map(toolCall => ({
553
+ name: toolCall.name,
554
+ status: toolCall.status,
555
+ durationMs: toolCall.durationMs,
556
+ error: toolCall.error,
557
+ })),
495
558
  });
496
559
  const bundleFile = await writeExecutionBundle({
497
560
  sessionId: sessionIdRef.current,
498
561
  prompt: input,
499
562
  error: friendlyMsg,
500
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
+ })),
501
572
  });
502
573
  await saveSession({
503
574
  id: sessionIdRef.current,
504
575
  messageCount: messages.length + 2,
576
+ toolCallCount: toolHistory.length,
505
577
  approvalMode,
506
578
  lastPrompt: input,
507
579
  lastError: friendlyMsg,
@@ -511,9 +583,14 @@ export function App({ config, options }) {
511
583
  });
512
584
  }
513
585
  finally {
586
+ logEvent('agent run: finally (clearing softCancel)');
514
587
  // Safety net: ensure UI is always reset regardless of exit path
588
+ flushPending();
515
589
  setIsProcessing(false);
516
- setStatusText(i18n.t('ready'));
590
+ // Don't overwrite status if user paused the agent
591
+ if (!isPausedRef.current) {
592
+ setStatusText(i18n.t('ready'));
593
+ }
517
594
  // Clear any pending approval (covers error/abort exit paths)
518
595
  if (pendingApprovalResolveRef.current) {
519
596
  pendingApprovalResolveRef.current(false);
@@ -523,29 +600,58 @@ export function App({ config, options }) {
523
600
  if (abortControllerRef.current === abortController) {
524
601
  abortControllerRef.current = null;
525
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;
526
609
  }
527
610
  }, [messages, isProcessing, setupStep, handleApiKeySubmit, handleSlashCommand, approvalMode, config, localApiKey]);
528
611
  useInput((_input, key) => {
529
612
  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.
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.
534
618
  if (key.ctrl && _input === 'c') {
535
- 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');
536
622
  abortControllerRef.current.abort();
537
- setStatusText(i18n.t('cancelled'));
623
+ setIsPaused(true);
624
+ setStatusText(i18n.t('paused'));
538
625
  if (pendingApprovalResolveRef.current) {
539
626
  pendingApprovalResolveRef.current(false);
540
627
  pendingApprovalResolveRef.current = null;
541
628
  }
542
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
+ }
543
636
  return;
544
637
  }
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;
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);
549
655
  return;
550
656
  }
551
657
  // When not in setup mode, let InputBar handle all keyboard input
@@ -632,26 +738,9 @@ export function App({ config, options }) {
632
738
  });
633
739
  return;
634
740
  }
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
- }
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.
655
744
  // Theme picker: interactive selection
656
745
  if (themePicker) {
657
746
  if (key.escape) {
@@ -738,23 +827,10 @@ export function App({ config, options }) {
738
827
  }
739
828
  return;
740
829
  }
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
- }
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'));
758
834
  return;
759
835
  }
760
836
  return;
@@ -866,9 +942,7 @@ export function App({ config, options }) {
866
942
  setToolCalls([]);
867
943
  setPendingApproval(null);
868
944
  setPendingImage(null);
869
- setScrollMode('follow');
870
- setNewMessagesWhilePaused(false);
871
- setChatScrollOffset(0);
945
+ setChatEpoch(e => e + 1); // remount Static so the scrollback resets
872
946
  liveToolMessageIndexRef.current = -1;
873
947
  setServiceNotice(null);
874
948
  if (serviceNoticeTimerRef.current) {
@@ -887,7 +961,7 @@ export function App({ config, options }) {
887
961
  }, [messages.length, toolCalls.length, executeClear]);
888
962
  const handleExit = useCallback(() => { exit(); }, [exit]);
889
963
  const colors = themeManager.getColors();
890
- return (_jsxs(Box, { flexDirection: 'column', height: '100%', children: [setupStep !== 'done'
964
+ return (_jsxs(Box, { flexDirection: 'column', children: [setupStep !== 'done'
891
965
  ? _jsx(SetupWizard, { state: {
892
966
  step: setupStep,
893
967
  apiKeyError,
@@ -898,21 +972,14 @@ export function App({ config, options }) {
898
972
  langOptions,
899
973
  modeOptions,
900
974
  } })
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: [[
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: [[
902
976
  '[ok] Подтвердить',
903
977
  '[no] Отклонить',
904
978
  `[mute] Не спрашивать для "${pendingApproval.toolName}"`,
905
979
  '[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 })] }));
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 })] }));
917
984
  }
918
985
  //# sourceMappingURL=app.js.map