attocode 0.1.7 → 0.1.8

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 (74) hide show
  1. package/CHANGELOG.md +13 -1
  2. package/dist/src/adapters.d.ts.map +1 -1
  3. package/dist/src/adapters.js +6 -0
  4. package/dist/src/adapters.js.map +1 -1
  5. package/dist/src/agent.d.ts +52 -1
  6. package/dist/src/agent.d.ts.map +1 -1
  7. package/dist/src/agent.js +628 -82
  8. package/dist/src/agent.js.map +1 -1
  9. package/dist/src/commands/handler.d.ts.map +1 -1
  10. package/dist/src/commands/handler.js +6 -0
  11. package/dist/src/commands/handler.js.map +1 -1
  12. package/dist/src/defaults.d.ts +3 -1
  13. package/dist/src/defaults.d.ts.map +1 -1
  14. package/dist/src/defaults.js +9 -1
  15. package/dist/src/defaults.js.map +1 -1
  16. package/dist/src/integrations/agent-registry.d.ts +14 -0
  17. package/dist/src/integrations/agent-registry.d.ts.map +1 -1
  18. package/dist/src/integrations/agent-registry.js.map +1 -1
  19. package/dist/src/integrations/cancellation.d.ts +62 -0
  20. package/dist/src/integrations/cancellation.d.ts.map +1 -1
  21. package/dist/src/integrations/cancellation.js +174 -0
  22. package/dist/src/integrations/cancellation.js.map +1 -1
  23. package/dist/src/integrations/dead-letter-queue.js +1 -1
  24. package/dist/src/integrations/dead-letter-queue.js.map +1 -1
  25. package/dist/src/integrations/economics.d.ts +41 -0
  26. package/dist/src/integrations/economics.d.ts.map +1 -1
  27. package/dist/src/integrations/economics.js +114 -8
  28. package/dist/src/integrations/economics.js.map +1 -1
  29. package/dist/src/integrations/history.d.ts +72 -0
  30. package/dist/src/integrations/history.d.ts.map +1 -0
  31. package/dist/src/integrations/history.js +165 -0
  32. package/dist/src/integrations/history.js.map +1 -0
  33. package/dist/src/integrations/index.d.ts +4 -3
  34. package/dist/src/integrations/index.d.ts.map +1 -1
  35. package/dist/src/integrations/index.js +4 -2
  36. package/dist/src/integrations/index.js.map +1 -1
  37. package/dist/src/integrations/resources.d.ts +5 -0
  38. package/dist/src/integrations/resources.d.ts.map +1 -1
  39. package/dist/src/integrations/resources.js +7 -0
  40. package/dist/src/integrations/resources.js.map +1 -1
  41. package/dist/src/integrations/safety.d.ts +3 -1
  42. package/dist/src/integrations/safety.d.ts.map +1 -1
  43. package/dist/src/integrations/safety.js +22 -5
  44. package/dist/src/integrations/safety.js.map +1 -1
  45. package/dist/src/modes/tui.d.ts.map +1 -1
  46. package/dist/src/modes/tui.js +6 -0
  47. package/dist/src/modes/tui.js.map +1 -1
  48. package/dist/src/providers/adapters/openrouter.js +2 -2
  49. package/dist/src/providers/adapters/openrouter.js.map +1 -1
  50. package/dist/src/tools/agent.d.ts.map +1 -1
  51. package/dist/src/tools/agent.js +13 -1
  52. package/dist/src/tools/agent.js.map +1 -1
  53. package/dist/src/tui/app.d.ts.map +1 -1
  54. package/dist/src/tui/app.js +213 -103
  55. package/dist/src/tui/app.js.map +1 -1
  56. package/dist/src/tui/components/DebugPanel.d.ts +41 -0
  57. package/dist/src/tui/components/DebugPanel.d.ts.map +1 -0
  58. package/dist/src/tui/components/DebugPanel.js +104 -0
  59. package/dist/src/tui/components/DebugPanel.js.map +1 -0
  60. package/dist/src/tui/components/ErrorDetailPanel.d.ts +49 -0
  61. package/dist/src/tui/components/ErrorDetailPanel.d.ts.map +1 -0
  62. package/dist/src/tui/components/ErrorDetailPanel.js +109 -0
  63. package/dist/src/tui/components/ErrorDetailPanel.js.map +1 -0
  64. package/dist/src/tui/components/ToolCallItem.d.ts +3 -4
  65. package/dist/src/tui/components/ToolCallItem.d.ts.map +1 -1
  66. package/dist/src/tui/components/ToolCallItem.js +51 -15
  67. package/dist/src/tui/components/ToolCallItem.js.map +1 -1
  68. package/dist/src/tui/components/index.d.ts +2 -0
  69. package/dist/src/tui/components/index.d.ts.map +1 -1
  70. package/dist/src/tui/components/index.js +4 -0
  71. package/dist/src/tui/components/index.js.map +1 -1
  72. package/dist/src/types.d.ts +77 -0
  73. package/dist/src/types.d.ts.map +1 -1
  74. package/package.json +1 -1
@@ -10,9 +10,10 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
10
10
  */
11
11
  import { useState, useCallback, useEffect, memo, useRef, useMemo } from 'react';
12
12
  import { Box, Text, useApp, useInput, Static } from 'ink';
13
- import { DiffView } from './components/DiffView.js';
14
13
  import { ActiveAgentsPanel } from './components/ActiveAgentsPanel.js';
15
14
  import { TasksPanel } from './components/TasksPanel.js';
15
+ import { ToolCallItem } from './components/ToolCallItem.js';
16
+ import { DebugPanel, useDebugBuffer } from './components/DebugPanel.js';
16
17
  import { getTheme, getThemeNames } from './theme/index.js';
17
18
  import { ControlledCommandPalette } from './input/CommandPalette.js';
18
19
  import { ApprovalDialog } from './components/ApprovalDialog.js';
@@ -20,6 +21,7 @@ import { TransparencyAggregator } from './transparency-aggregator.js';
20
21
  import { handleSkillsCommand } from '../commands/skills-commands.js';
21
22
  import { handleAgentsCommand } from '../commands/agents-commands.js';
22
23
  import { handleInitCommand } from '../commands/init-commands.js';
24
+ import { createHistoryManager } from '../integrations/history.js';
23
25
  // =============================================================================
24
26
  // PATTERN GENERATION FOR ALWAYS-ALLOW
25
27
  // =============================================================================
@@ -59,98 +61,34 @@ const MessageItem = memo(function MessageItem({ msg, colors }) {
59
61
  const label = isUser ? 'You' : isAssistant ? 'Assistant' : isError ? 'Error' : 'System';
60
62
  return (_jsxs(Box, { marginBottom: 1, flexDirection: "column", children: [_jsxs(Box, { gap: 1, children: [_jsx(Text, { color: roleColor, bold: true, children: icon }), _jsx(Text, { color: roleColor, bold: true, children: label }), _jsx(Text, { color: colors.textMuted, dimColor: true, children: ` ${msg.ts.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' })}` })] }), _jsx(Box, { marginLeft: 2, children: _jsx(Text, { wrap: "wrap", color: isError ? colors.error : colors.text, children: msg.content }) })] }));
61
63
  });
62
- const ToolCallItem = memo(function ToolCallItem({ tc, expanded, colors }) {
63
- const icon = tc.status === 'success' ? '[OK]' : tc.status === 'error' ? '[X]' : tc.status === 'running' ? '[~]' : '[ ]';
64
- const statusColor = tc.status === 'success' ? '#98FB98' : tc.status === 'error' ? '#FF6B6B' : tc.status === 'running' ? '#87CEEB' : colors.textMuted;
65
- // Compact formatting for collapsed view
66
- const formatToolArgsCompact = (args) => {
67
- const entries = Object.entries(args);
68
- if (entries.length === 0)
69
- return '';
70
- if (entries.length === 1) {
71
- const [key, val] = entries[0];
72
- const valStr = typeof val === 'string' ? val : JSON.stringify(val);
73
- return valStr.length > 50 ? `${key}: ${valStr.slice(0, 47)}...` : `${key}: ${valStr}`;
74
- }
75
- return `{${entries.length} args}`;
76
- };
77
- // Expanded formatting - each arg on its own line with proper handling
78
- const formatToolArgsExpanded = (args) => {
79
- const entries = Object.entries(args);
80
- if (entries.length === 0)
81
- return [];
82
- return entries.map(([key, val]) => {
83
- let valStr;
84
- if (typeof val === 'string') {
85
- // For strings, show with quotes, handle multiline
86
- if (val.includes('\n')) {
87
- const lines = val.split('\n');
88
- if (lines.length > 3) {
89
- valStr = `"${lines.slice(0, 3).join('\\n')}..." (${lines.length} lines)`;
90
- }
91
- else {
92
- valStr = `"${val.replace(/\n/g, '\\n')}"`;
93
- }
94
- }
95
- else if (val.length > 100) {
96
- valStr = `"${val.slice(0, 97)}..."`;
97
- }
98
- else {
99
- valStr = `"${val}"`;
100
- }
101
- }
102
- else if (typeof val === 'object' && val !== null) {
103
- const json = JSON.stringify(val, null, 2);
104
- if (json.length > 200) {
105
- valStr = JSON.stringify(val).slice(0, 197) + '...';
106
- }
107
- else {
108
- valStr = json;
109
- }
110
- }
111
- else {
112
- valStr = String(val);
113
- }
114
- return `${key}: ${valStr}`;
115
- });
116
- };
117
- const argsStr = formatToolArgsCompact(tc.args);
118
- if (expanded) {
119
- const expandedArgs = formatToolArgsExpanded(tc.args);
120
- return (_jsxs(Box, { marginLeft: 2, flexDirection: "column", children: [_jsxs(Box, { gap: 1, children: [_jsx(Text, { color: statusColor, children: icon }), _jsx(Text, { color: "#DDA0DD", bold: true, children: tc.name }), tc.duration ? _jsxs(Text, { color: colors.textMuted, dimColor: true, children: ["(", tc.duration, "ms)"] }) : null] }), expandedArgs.map((argLine, i) => (_jsx(Box, { marginLeft: 3, children: _jsx(Text, { color: "#87CEEB", dimColor: true, children: argLine }) }, i))), tc.status === 'success' && tc.result !== undefined && tc.result !== null ? (_jsx(Box, { marginLeft: 3, flexDirection: "column", children: (tc.name === 'edit_file' || tc.name === 'write_file') &&
121
- typeof tc.result === 'object' && tc.result !== null &&
122
- 'metadata' in tc.result &&
123
- typeof tc.result.metadata?.diff === 'string' ? (_jsx(DiffView, { diff: tc.result.metadata.diff, expanded: true, maxLines: 15 })) : (_jsx(Text, { color: "#98FB98", dimColor: true, children: `-> ${String(tc.result).slice(0, 150)}${String(tc.result).length > 150 ? '...' : ''}` })) })) : null, tc.status === 'error' && tc.error && (_jsx(Box, { marginLeft: 3, children: _jsx(Text, { color: "#FF6B6B", children: `x ${tc.error}` }) }))] }));
124
- }
125
- // Check if result has diff metadata for collapsed summary
126
- const hasDiff = tc.status === 'success' &&
127
- (tc.name === 'edit_file' || tc.name === 'write_file') &&
128
- typeof tc.result === 'object' && tc.result !== null &&
129
- 'metadata' in tc.result &&
130
- typeof tc.result.metadata?.diff === 'string';
131
- return (_jsxs(Box, { marginLeft: 2, gap: 1, children: [_jsx(Text, { color: statusColor, children: icon }), _jsx(Text, { color: "#DDA0DD", bold: true, children: tc.name }), argsStr ? _jsx(Text, { color: colors.textMuted, dimColor: true, children: argsStr }) : null, hasDiff && (_jsx(DiffView, { diff: tc.result.metadata.diff, expanded: false })), tc.duration ? _jsxs(Text, { color: colors.textMuted, dimColor: true, children: ["(", tc.duration, "ms)"] }) : null] }));
132
- });
133
- const MemoizedInputArea = memo(function MemoizedInputArea({ onSubmit, disabled, borderColor, textColor, cursorColor, onCtrlC, onCtrlL, onCtrlP, onEscape, onToggleToolExpand, onToggleThinking, onToggleTransparency, onToggleActiveAgents, onToggleTasks, onPageUp, onPageDown, onHome, onEnd, commandPaletteOpen, onCommandPaletteInput, approvalDialogOpen, approvalDenyReasonMode, onApprovalApprove, onApprovalAlwaysAllow, onApprovalDeny, onApprovalDenyWithReason, onApprovalCancelDenyReason, onApprovalDenyReasonInput, }) {
64
+ const MemoizedInputArea = memo(function MemoizedInputArea({ onSubmit, disabled, borderColor, textColor, cursorColor, onCtrlC, onCtrlL, onCtrlP, onEscape, onToggleToolExpand, onToggleThinking, onToggleTransparency, onToggleActiveAgents, onToggleTasks, onToggleDebug, onPageUp, onPageDown, onHome, onEnd, commandPaletteOpen, onCommandPaletteInput, approvalDialogOpen, approvalDenyReasonMode, onApprovalApprove, onApprovalAlwaysAllow, onApprovalDeny, onApprovalDenyWithReason, onApprovalCancelDenyReason, onApprovalDenyReasonInput, history = [], onHistorySearch, }) {
134
65
  const [value, setValue] = useState('');
135
66
  const [cursorPos, setCursorPos] = useState(0);
67
+ // History navigation state
68
+ const [historyIndex, setHistoryIndex] = useState(-1); // -1 = current input (not browsing history)
69
+ const [savedInput, setSavedInput] = useState(''); // Preserve current input when browsing
70
+ const historyRef = useRef(history);
71
+ historyRef.current = history;
136
72
  // Store callbacks in refs so useInput doesn't re-subscribe on prop changes
137
73
  const callbacksRef = useRef({
138
74
  onSubmit, onCtrlC, onCtrlL, onCtrlP, onEscape,
139
- onToggleToolExpand, onToggleThinking, onToggleTransparency, onToggleActiveAgents, onToggleTasks,
75
+ onToggleToolExpand, onToggleThinking, onToggleTransparency, onToggleActiveAgents, onToggleTasks, onToggleDebug,
140
76
  onPageUp, onPageDown, onHome, onEnd,
141
77
  commandPaletteOpen, onCommandPaletteInput,
142
78
  approvalDialogOpen, approvalDenyReasonMode,
143
79
  onApprovalApprove, onApprovalAlwaysAllow, onApprovalDeny, onApprovalDenyWithReason,
144
80
  onApprovalCancelDenyReason, onApprovalDenyReasonInput,
81
+ onHistorySearch,
145
82
  });
146
83
  callbacksRef.current = {
147
84
  onSubmit, onCtrlC, onCtrlL, onCtrlP, onEscape,
148
- onToggleToolExpand, onToggleThinking, onToggleTransparency, onToggleActiveAgents, onToggleTasks,
85
+ onToggleToolExpand, onToggleThinking, onToggleTransparency, onToggleActiveAgents, onToggleTasks, onToggleDebug,
149
86
  onPageUp, onPageDown, onHome, onEnd,
150
87
  commandPaletteOpen, onCommandPaletteInput,
151
88
  approvalDialogOpen, approvalDenyReasonMode,
152
89
  onApprovalApprove, onApprovalAlwaysAllow, onApprovalDeny, onApprovalDenyWithReason,
153
90
  onApprovalCancelDenyReason, onApprovalDenyReasonInput,
91
+ onHistorySearch,
154
92
  };
155
93
  const disabledRef = useRef(disabled);
156
94
  disabledRef.current = disabled;
@@ -198,6 +136,11 @@ const MemoizedInputArea = memo(function MemoizedInputArea({ onSubmit, disabled,
198
136
  cb.onToggleTasks?.();
199
137
  return;
200
138
  }
139
+ // Alt+D / Option+D - Toggle debug panel
140
+ if (input === '\u2202' || (key.meta && input === 'd')) {
141
+ cb.onToggleDebug?.();
142
+ return;
143
+ }
201
144
  // Command palette keyboard handling (when open)
202
145
  if (cb.commandPaletteOpen && cb.onCommandPaletteInput) {
203
146
  cb.onCommandPaletteInput(input, key);
@@ -261,10 +204,18 @@ const MemoizedInputArea = memo(function MemoizedInputArea({ onSubmit, disabled,
261
204
  // Input handling (only when not disabled)
262
205
  if (disabledRef.current)
263
206
  return;
207
+ // Shift+Enter for multiline input (insert newline)
208
+ if (key.return && key.shift) {
209
+ setValue(v => v.slice(0, cursorPos) + '\n' + v.slice(cursorPos));
210
+ setCursorPos(p => p + 1);
211
+ return;
212
+ }
264
213
  if (key.return && value.trim()) {
265
214
  cb.onSubmit(value);
266
215
  setValue('');
267
216
  setCursorPos(0);
217
+ setHistoryIndex(-1); // Reset history navigation
218
+ setSavedInput('');
268
219
  return;
269
220
  }
270
221
  if (key.backspace || key.delete) {
@@ -282,6 +233,54 @@ const MemoizedInputArea = memo(function MemoizedInputArea({ onSubmit, disabled,
282
233
  setCursorPos(p => Math.min(value.length, p + 1));
283
234
  return;
284
235
  }
236
+ // History navigation with up/down arrows
237
+ if (key.upArrow && historyRef.current.length > 0) {
238
+ setHistoryIndex(prevIndex => {
239
+ const maxIndex = historyRef.current.length - 1;
240
+ if (prevIndex === -1) {
241
+ // First press - save current input and go to most recent history
242
+ setSavedInput(value);
243
+ const newValue = historyRef.current[maxIndex] || '';
244
+ setValue(newValue);
245
+ setCursorPos(newValue.length);
246
+ return maxIndex;
247
+ }
248
+ else if (prevIndex > 0) {
249
+ // Go to older entry
250
+ const newIndex = prevIndex - 1;
251
+ const newValue = historyRef.current[newIndex] || '';
252
+ setValue(newValue);
253
+ setCursorPos(newValue.length);
254
+ return newIndex;
255
+ }
256
+ return prevIndex; // Already at oldest
257
+ });
258
+ return;
259
+ }
260
+ if (key.downArrow && historyRef.current.length > 0) {
261
+ setHistoryIndex(prevIndex => {
262
+ if (prevIndex === -1) {
263
+ // Not browsing history, do nothing
264
+ return -1;
265
+ }
266
+ else if (prevIndex < historyRef.current.length - 1) {
267
+ // Go to newer entry
268
+ const newIndex = prevIndex + 1;
269
+ const newValue = historyRef.current[newIndex] || '';
270
+ setValue(newValue);
271
+ setCursorPos(newValue.length);
272
+ return newIndex;
273
+ }
274
+ else {
275
+ // At most recent - restore saved input
276
+ setValue(savedInput);
277
+ setCursorPos(savedInput.length);
278
+ setSavedInput('');
279
+ return -1;
280
+ }
281
+ });
282
+ return;
283
+ }
285
284
  if (key.ctrl && input === 'a') {
286
285
  setCursorPos(0);
287
286
  return;
@@ -300,7 +299,10 @@ const MemoizedInputArea = memo(function MemoizedInputArea({ onSubmit, disabled,
300
299
  setCursorPos(p => p + input.length);
301
300
  }
302
301
  });
303
- return (_jsxs(Box, { borderStyle: "round", borderColor: disabledRef.current ? '#666' : borderColor, paddingX: 1, children: [_jsxs(Text, { color: textColor, bold: true, children: ['>', " "] }), _jsx(Text, { children: value.slice(0, cursorPos) }), !disabled && (_jsx(Text, { backgroundColor: cursorColor, color: "#1a1a2e", children: value[cursorPos] ?? ' ' })), _jsx(Text, { children: value.slice(cursorPos + 1) })] }));
302
+ // Check if multiline (for visual indicator)
303
+ const isMultiline = value.includes('\n');
304
+ const lineCount = value.split('\n').length;
305
+ return (_jsx(Box, { borderStyle: "round", borderColor: disabledRef.current ? '#666' : borderColor, paddingX: 1, flexDirection: "column", children: _jsxs(Box, { children: [_jsxs(Text, { color: textColor, bold: true, children: [isMultiline ? '»' : '>', " "] }), _jsx(Text, { children: value.slice(0, cursorPos).replace(/\n/g, '⏎') }), !disabled && (_jsx(Text, { backgroundColor: cursorColor, color: "#1a1a2e", children: value[cursorPos] === '\n' ? '⏎' : (value[cursorPos] ?? ' ') })), _jsx(Text, { children: value.slice(cursorPos + 1).replace(/\n/g, '⏎') }), isMultiline && (_jsxs(Text, { color: "#666", dimColor: true, children: [" (", lineCount, " lines)"] }))] }) }));
304
306
  }, (prevProps, nextProps) => {
305
307
  // Custom comparison: only re-render if visual props change
306
308
  return prevProps.disabled === nextProps.disabled &&
@@ -326,6 +328,14 @@ export function TUIApp({ agent, sessionStore, mcpClient, compactor, lspManager,
326
328
  const [contextTokens, setContextTokens] = useState(0);
327
329
  const [elapsedTime, setElapsedTime] = useState(0);
328
330
  const processingStartRef = useRef(null);
331
+ // Command history manager (persistent)
332
+ const historyManagerRef = useRef(null);
333
+ if (!historyManagerRef.current) {
334
+ historyManagerRef.current = createHistoryManager();
335
+ }
336
+ const [historyEntries, setHistoryEntries] = useState(() => historyManagerRef.current?.getHistory() || []);
337
+ // Debug buffer for debug panel
338
+ const debugBuffer = useDebugBuffer(100);
329
339
  const [executionMode, setExecutionMode] = useState('idle');
330
340
  const executionModeRef = useRef('idle');
331
341
  // Display toggles
@@ -334,6 +344,7 @@ export function TUIApp({ agent, sessionStore, mcpClient, compactor, lspManager,
334
344
  const [transparencyExpanded, setTransparencyExpanded] = useState(false);
335
345
  const [activeAgentsExpanded, setActiveAgentsExpanded] = useState(true);
336
346
  const [tasksExpanded, setTasksExpanded] = useState(true);
347
+ const [debugExpanded, setDebugExpanded] = useState(false);
337
348
  // Active agents tracking (for Active Agents Panel)
338
349
  const [activeAgents, setActiveAgents] = useState([]);
339
350
  // Tasks tracking (for Tasks Panel)
@@ -351,6 +362,9 @@ export function TUIApp({ agent, sessionStore, mcpClient, compactor, lspManager,
351
362
  // Transparency state
352
363
  const [transparencyState, setTransparencyState] = useState(null);
353
364
  const transparencyAggregatorRef = useRef(null);
365
+ // Consecutive Ctrl+C tracking for force exit
366
+ const [ctrlCCount, setCtrlCCount] = useState(0);
367
+ const ctrlCTimerRef = useRef(null);
354
368
  // Refs for stable callbacks
355
369
  const isProcessingRef = useRef(isProcessing);
356
370
  const messagesLengthRef = useRef(messages.length);
@@ -366,6 +380,22 @@ export function TUIApp({ agent, sessionStore, mcpClient, compactor, lspManager,
366
380
  const uniqueId = `${role}-${Date.now()}-${++messageIdCounter.current}`;
367
381
  setMessages(prev => [...prev, { id: uniqueId, role, content, ts: new Date() }]);
368
382
  }, []);
383
+ const persistPendingPlanToStore = useCallback(() => {
384
+ if (!agent.hasPendingPlan())
385
+ return;
386
+ if (!('savePendingPlan' in sessionStore) || typeof sessionStore.savePendingPlan !== 'function') {
387
+ return;
388
+ }
389
+ const pendingPlan = agent.getPendingPlan();
390
+ if (!pendingPlan)
391
+ return;
392
+ sessionStore.savePendingPlan(pendingPlan, currentSessionId);
393
+ persistenceDebug.log('Pending plan saved', {
394
+ planId: pendingPlan.id,
395
+ changes: pendingPlan.proposedChanges.length,
396
+ sessionId: currentSessionId,
397
+ });
398
+ }, [agent, sessionStore, currentSessionId, persistenceDebug]);
369
399
  // =========================================================================
370
400
  // APPROVAL DIALOG HANDLERS
371
401
  // =========================================================================
@@ -438,6 +468,10 @@ export function TUIApp({ agent, sessionStore, mcpClient, compactor, lspManager,
438
468
  const mode = executionModeRef.current;
439
469
  if (mode === 'idle')
440
470
  return; // No active execution, ignore events
471
+ // Log event to debug buffer
472
+ if (debugExpanded) {
473
+ debugBuffer.debug(`Event: ${event.type}`, event);
474
+ }
441
475
  // Extract subagent from event if present (not all events have it)
442
476
  const eventWithSubagent = event;
443
477
  const subagentPrefix = eventWithSubagent.subagent ? `[${eventWithSubagent.subagent}] ` : '';
@@ -478,41 +512,44 @@ export function TUIApp({ agent, sessionStore, mcpClient, compactor, lspManager,
478
512
  if (event.type === 'agent.complete') {
479
513
  const e = event;
480
514
  const statusText = e.success ? 'completed' : 'failed';
481
- addMessage('system', `[AGENT] ${e.agentId} ${statusText}`);
482
- // Show output preview if substantive
515
+ const displayName = e.agentType || e.agentId;
516
+ addMessage('system', `[AGENT] ${displayName} ${statusText}`);
517
+ // Show output preview if substantive (increased from 300 to 1000 chars)
483
518
  if (e.output && e.output.length > 50) {
484
- const preview = e.output.slice(0, 300);
485
- addMessage('system', `[AGENT OUTPUT]\n${preview}${e.output.length > 300 ? '\n...(truncated)' : ''}`);
519
+ const preview = e.output.slice(0, 1000);
520
+ const truncated = e.output.length > 1000;
521
+ addMessage('system', `[AGENT OUTPUT]\n${preview}${truncated ? `\n...(full output: ${e.output.length} chars)` : ''}`);
486
522
  }
487
- // Update active agents panel
488
- setActiveAgents(prev => prev.map(a => a.type === e.agentId || a.id.includes(e.agentId)
523
+ // Update active agents panel - use strict ID matching
524
+ setActiveAgents(prev => prev.map(a => a.id === e.agentId
489
525
  ? { ...a, status: e.success ? 'completed' : 'error' }
490
526
  : a));
491
527
  return;
492
528
  }
493
529
  if (event.type === 'agent.error') {
494
530
  const e = event;
495
- addMessage('system', `[AGENT] ${e.agentId} error: ${e.error}`);
531
+ const displayName = e.agentType || e.agentId;
532
+ addMessage('system', `[AGENT] ${displayName} error: ${e.error}`);
496
533
  // For timeout errors, use 'timing_out' status first to indicate the agent
497
534
  // is in the process of stopping. Then transition to 'timeout' after a delay.
498
535
  // This provides better UX than immediately showing "failed" while tokens accumulate.
499
536
  const isTimeout = e.error.includes('timed out') || e.error.includes('Timed out');
500
537
  if (isTimeout) {
501
- // First, mark as timing_out
502
- setActiveAgents(prev => prev.map(a => a.type === e.agentId || a.id.includes(e.agentId)
538
+ // First, mark as timing_out - use strict ID matching
539
+ setActiveAgents(prev => prev.map(a => a.id === e.agentId
503
540
  ? { ...a, status: 'timing_out' }
504
541
  : a));
505
542
  // After 3 seconds, transition to final timeout status
506
543
  // (agent should have stopped by then due to cancellation token check)
507
544
  setTimeout(() => {
508
- setActiveAgents(prev => prev.map(a => (a.type === e.agentId || a.id.includes(e.agentId)) && a.status === 'timing_out'
545
+ setActiveAgents(prev => prev.map(a => a.id === e.agentId && a.status === 'timing_out'
509
546
  ? { ...a, status: 'timeout' }
510
547
  : a));
511
548
  }, 3000);
512
549
  }
513
550
  else {
514
- // Regular error - set immediately
515
- setActiveAgents(prev => prev.map(a => a.type === e.agentId || a.id.includes(e.agentId)
551
+ // Regular error - set immediately with strict ID matching
552
+ setActiveAgents(prev => prev.map(a => a.id === e.agentId
516
553
  ? { ...a, status: 'error' }
517
554
  : a));
518
555
  }
@@ -595,9 +632,14 @@ export function TUIApp({ agent, sessionStore, mcpClient, compactor, lspManager,
595
632
  // These agents should have stopped, and any lingering events are from
596
633
  // zombie processes that we don't want to count.
597
634
  if (e.subagent || eventWithSubagent.subagent) {
635
+ const subagentId = e.subagentId || eventWithSubagent.subagentId;
598
636
  const agentName = e.subagent || eventWithSubagent.subagent;
599
637
  setActiveAgents(prev => prev.map(a => {
600
- const matchesAgent = a.type === agentName || a.id.includes(agentName || '');
638
+ // Use strict ID matching when subagentId is available (prevents duplicate counting
639
+ // when multiple agents of the same type run in parallel)
640
+ const matchesAgent = subagentId
641
+ ? a.id === subagentId
642
+ : (a.type === agentName || a.id.includes(agentName || ''));
601
643
  const isStillRunning = a.status === 'running';
602
644
  // Only update tokens if agent is still running
603
645
  if (matchesAgent && isStillRunning) {
@@ -624,18 +666,26 @@ export function TUIApp({ agent, sessionStore, mcpClient, compactor, lspManager,
624
666
  const e = event;
625
667
  setStatus(s => ({ ...s, mode: `${e.agentId} iter ${e.iteration}/${e.maxIterations}` }));
626
668
  // Update active agents panel with iteration info
627
- setActiveAgents(prev => prev.map(a => a.type === e.agentId || a.id.includes(e.agentId)
628
- ? { ...a, iteration: e.iteration, maxIterations: e.maxIterations }
629
- : a));
669
+ // Use subagentId for strict matching when available (parallel same-type agents)
670
+ setActiveAgents(prev => prev.map(a => {
671
+ const matches = e.subagentId
672
+ ? a.id === e.subagentId
673
+ : (a.type === e.agentId || a.id.includes(e.agentId));
674
+ return matches ? { ...a, iteration: e.iteration, maxIterations: e.maxIterations } : a;
675
+ }));
630
676
  return;
631
677
  }
632
678
  if (event.type === 'subagent.phase') {
633
679
  const e = event;
634
680
  setStatus(s => ({ ...s, mode: `${e.agentId} ${e.phase}` }));
635
681
  // Update active agents panel with phase info
636
- setActiveAgents(prev => prev.map(a => a.type === e.agentId || a.id.includes(e.agentId)
637
- ? { ...a, currentPhase: e.phase }
638
- : a));
682
+ // Use subagentId for strict matching when available (parallel same-type agents)
683
+ setActiveAgents(prev => prev.map(a => {
684
+ const matches = e.subagentId
685
+ ? a.id === e.subagentId
686
+ : (a.type === e.agentId || a.id.includes(e.agentId));
687
+ return matches ? { ...a, currentPhase: e.phase } : a;
688
+ }));
639
689
  return;
640
690
  }
641
691
  // Task events - update Tasks Panel
@@ -825,6 +875,7 @@ export function TUIApp({ agent, sessionStore, mcpClient, compactor, lspManager,
825
875
  ' Alt+O Toggle thinking',
826
876
  ' Alt+I Toggle transparency panel',
827
877
  ' Alt+K Toggle tasks panel',
878
+ ' Alt+D Toggle debug panel',
828
879
  '========================',
829
880
  ].join('\n'));
830
881
  return;
@@ -848,6 +899,7 @@ export function TUIApp({ agent, sessionStore, mcpClient, compactor, lspManager,
848
899
  plan: agentState.plan,
849
900
  memoryContext: agentState.memoryContext,
850
901
  });
902
+ persistPendingPlanToStore();
851
903
  addMessage('system', `Session saved: ${currentSessionId} (checkpoint: ${ckptId})`);
852
904
  }
853
905
  catch (e) {
@@ -1559,7 +1611,7 @@ export function TUIApp({ agent, sessionStore, mcpClient, compactor, lspManager,
1559
1611
  default:
1560
1612
  addMessage('system', `Unknown: /${cmd}. Try /help`);
1561
1613
  }
1562
- }, [addMessage, exit, agent, mcpClient, lspManager, sessionStore, compactor, model, currentThemeName, currentSessionId, formatSessionsTable, saveCheckpointToStore, showThinking]);
1614
+ }, [addMessage, exit, agent, mcpClient, lspManager, sessionStore, compactor, model, currentThemeName, currentSessionId, formatSessionsTable, saveCheckpointToStore, showThinking, persistPendingPlanToStore]);
1563
1615
  // =========================================================================
1564
1616
  // SUBMIT HANDLER
1565
1617
  // =========================================================================
@@ -1567,6 +1619,11 @@ export function TUIApp({ agent, sessionStore, mcpClient, compactor, lspManager,
1567
1619
  const trimmed = input.trim();
1568
1620
  if (!trimmed)
1569
1621
  return;
1622
+ // Add to history (persistent)
1623
+ if (historyManagerRef.current) {
1624
+ historyManagerRef.current.addEntry(trimmed);
1625
+ setHistoryEntries(historyManagerRef.current.getHistory());
1626
+ }
1570
1627
  addMessage('user', trimmed);
1571
1628
  if (trimmed.startsWith('/')) {
1572
1629
  const parts = trimmed.slice(1).split(/\s+/);
@@ -1578,6 +1635,8 @@ export function TUIApp({ agent, sessionStore, mcpClient, compactor, lspManager,
1578
1635
  executionModeRef.current = 'processing';
1579
1636
  setExecutionMode('processing');
1580
1637
  setStatus(s => ({ ...s, mode: 'thinking' }));
1638
+ // Reset CPU time counter for per-prompt resource limits (prevents session-wide timeout)
1639
+ agent.resetResourceTimer();
1581
1640
  try {
1582
1641
  const result = await agent.run(trimmed);
1583
1642
  const metrics = agent.getMetrics();
@@ -1620,6 +1679,7 @@ export function TUIApp({ agent, sessionStore, mcpClient, compactor, lspManager,
1620
1679
  plan: checkpoint.state.plan,
1621
1680
  memoryContext: checkpoint.state.memoryContext,
1622
1681
  });
1682
+ persistPendingPlanToStore();
1623
1683
  }
1624
1684
  catch (e) {
1625
1685
  persistenceDebug.error('[TUI] Checkpoint failed', e);
@@ -1635,7 +1695,7 @@ export function TUIApp({ agent, sessionStore, mcpClient, compactor, lspManager,
1635
1695
  setIsProcessing(false);
1636
1696
  setToolCalls([]);
1637
1697
  }
1638
- }, [addMessage, handleCommand, agent, sessionStore, saveCheckpointToStore, persistenceDebug]);
1698
+ }, [addMessage, handleCommand, agent, sessionStore, saveCheckpointToStore, persistenceDebug, persistPendingPlanToStore]);
1639
1699
  // =========================================================================
1640
1700
  // COMMAND PALETTE ITEMS
1641
1701
  // =========================================================================
@@ -1729,8 +1789,33 @@ export function TUIApp({ agent, sessionStore, mcpClient, compactor, lspManager,
1729
1789
  // KEYBOARD CALLBACKS
1730
1790
  // =========================================================================
1731
1791
  const handleCtrlC = useCallback(() => {
1732
- agent.cleanup().then(() => mcpClient.cleanup()).then(() => lspManager.cleanup()).then(() => exit());
1733
- }, [agent, mcpClient, lspManager, exit]);
1792
+ // Clear any existing timer
1793
+ if (ctrlCTimerRef.current) {
1794
+ clearTimeout(ctrlCTimerRef.current);
1795
+ ctrlCTimerRef.current = null;
1796
+ }
1797
+ setCtrlCCount(prevCount => {
1798
+ const newCount = prevCount + 1;
1799
+ if (newCount >= 2) {
1800
+ // Second Ctrl+C within timeout window - force exit immediately
1801
+ process.exit(1);
1802
+ }
1803
+ // First Ctrl+C - show warning and start graceful cleanup
1804
+ addMessage('system', '[CTRL+C] Press again within 1s to force exit...');
1805
+ // Start graceful cleanup in background
1806
+ agent.cleanup()
1807
+ .then(() => mcpClient.cleanup())
1808
+ .then(() => lspManager.cleanup())
1809
+ .then(() => exit())
1810
+ .catch(() => exit()); // Exit even if cleanup fails
1811
+ // Reset counter after 1 second
1812
+ ctrlCTimerRef.current = setTimeout(() => {
1813
+ setCtrlCCount(0);
1814
+ ctrlCTimerRef.current = null;
1815
+ }, 1000);
1816
+ return newCount;
1817
+ });
1818
+ }, [agent, mcpClient, lspManager, exit, addMessage]);
1734
1819
  const handleCtrlL = useCallback(() => {
1735
1820
  setMessages([]);
1736
1821
  setToolCalls([]);
@@ -1740,7 +1825,7 @@ export function TUIApp({ agent, sessionStore, mcpClient, compactor, lspManager,
1740
1825
  setCommandPaletteQuery('');
1741
1826
  setCommandPaletteIndex(0);
1742
1827
  }, []);
1743
- const handleEscape = useCallback(() => {
1828
+ const handleEscape = useCallback(async () => {
1744
1829
  // Close command palette first if open
1745
1830
  if (commandPaletteOpen) {
1746
1831
  setCommandPaletteOpen(false);
@@ -1750,11 +1835,30 @@ export function TUIApp({ agent, sessionStore, mcpClient, compactor, lspManager,
1750
1835
  }
1751
1836
  // Otherwise cancel processing
1752
1837
  if (isProcessingRef.current) {
1838
+ // Immediate visual feedback
1839
+ addMessage('system', '[ESC] Stopping agent...');
1840
+ // Autosave checkpoint before cancel (async, don't block)
1841
+ try {
1842
+ const agentState = agent.getState();
1843
+ saveCheckpointToStore(sessionStore, {
1844
+ sessionId: currentSessionId,
1845
+ reason: 'user_cancel',
1846
+ messages: agentState.messages,
1847
+ iteration: agentState.iteration,
1848
+ timestamp: Date.now(),
1849
+ });
1850
+ persistPendingPlanToStore();
1851
+ persistenceDebug.log('Checkpoint saved before cancel');
1852
+ }
1853
+ catch (e) {
1854
+ persistenceDebug.error('Failed to save checkpoint before cancel', e);
1855
+ }
1856
+ // Cancel the agent
1753
1857
  agent.cancel('Cancelled by ESC');
1754
1858
  setIsProcessing(false);
1755
- addMessage('system', '[STOP] Cancelled');
1859
+ addMessage('system', '[STOP] Cancelled (checkpoint saved)');
1756
1860
  }
1757
- }, [agent, addMessage, commandPaletteOpen]);
1861
+ }, [agent, addMessage, commandPaletteOpen, sessionStore, currentSessionId, saveCheckpointToStore, persistenceDebug, persistPendingPlanToStore]);
1758
1862
  const handleToggleToolExpand = useCallback(() => {
1759
1863
  setToolCallsExpanded(prev => {
1760
1864
  addMessage('system', !prev ? '[*] Tool details: expanded' : '[ ] Tool details: collapsed');
@@ -1785,6 +1889,12 @@ export function TUIApp({ agent, sessionStore, mcpClient, compactor, lspManager,
1785
1889
  return !prev;
1786
1890
  });
1787
1891
  }, [addMessage]);
1892
+ const handleToggleDebug = useCallback(() => {
1893
+ setDebugExpanded(prev => {
1894
+ addMessage('system', !prev ? '[v] Debug panel: visible (Alt+D)' : '[^] Debug panel: hidden');
1895
+ return !prev;
1896
+ });
1897
+ }, [addMessage]);
1788
1898
  // Update context tokens
1789
1899
  useEffect(() => {
1790
1900
  const agentState = agent.getState();
@@ -1826,7 +1936,7 @@ export function TUIApp({ agent, sessionStore, mcpClient, compactor, lspManager,
1826
1936
  args: pendingApproval.args || {},
1827
1937
  risk: pendingApproval.risk,
1828
1938
  context: pendingApproval.context,
1829
- }, onApprove: handleApprove, onDeny: handleDeny, colors: colors, denyReasonMode: denyReasonMode, denyReason: denyReason })), _jsx(TasksPanel, { tasks: tasks, colors: colors, expanded: tasksExpanded }), _jsx(ActiveAgentsPanel, { agents: activeAgents, colors: colors, expanded: activeAgentsExpanded }), _jsx(MemoizedInputArea, { onSubmit: handleSubmit, disabled: isProcessing || !!pendingApproval, borderColor: pendingApproval ? '#FFD700' : '#87CEEB', textColor: "#98FB98", cursorColor: "#87CEEB", onCtrlC: handleCtrlC, onCtrlL: handleCtrlL, onCtrlP: handleCtrlP, onEscape: handleEscape, onToggleToolExpand: handleToggleToolExpand, onToggleThinking: handleToggleThinking, onToggleTransparency: handleToggleTransparency, onToggleActiveAgents: handleToggleActiveAgents, onToggleTasks: handleToggleTasks, commandPaletteOpen: commandPaletteOpen, onCommandPaletteInput: handleCommandPaletteInput, approvalDialogOpen: !!pendingApproval, approvalDenyReasonMode: denyReasonMode, onApprovalApprove: handleApprove, onApprovalAlwaysAllow: handleAlwaysAllow, onApprovalDeny: handleDeny, onApprovalDenyWithReason: handleDenyWithReason, onApprovalCancelDenyReason: handleCancelDenyReason, onApprovalDenyReasonInput: handleApprovalDenyReasonInput }), commandPaletteOpen && (_jsx(ControlledCommandPalette, { theme: selectedTheme, items: filteredCommandItems, visible: commandPaletteOpen, query: commandPaletteQuery, selectedIndex: commandPaletteIndex, onQueryChange: setCommandPaletteQuery, onSelectItem: (item) => {
1939
+ }, onApprove: handleApprove, onDeny: handleDeny, colors: colors, denyReasonMode: denyReasonMode, denyReason: denyReason })), _jsx(DebugPanel, { entries: debugBuffer.entries, expanded: debugExpanded, colors: colors }), _jsx(TasksPanel, { tasks: tasks, colors: colors, expanded: tasksExpanded }), _jsx(ActiveAgentsPanel, { agents: activeAgents, colors: colors, expanded: activeAgentsExpanded }), _jsx(MemoizedInputArea, { onSubmit: handleSubmit, disabled: isProcessing || !!pendingApproval, borderColor: pendingApproval ? '#FFD700' : '#87CEEB', textColor: "#98FB98", cursorColor: "#87CEEB", onCtrlC: handleCtrlC, onCtrlL: handleCtrlL, onCtrlP: handleCtrlP, onEscape: handleEscape, onToggleToolExpand: handleToggleToolExpand, onToggleThinking: handleToggleThinking, onToggleTransparency: handleToggleTransparency, onToggleActiveAgents: handleToggleActiveAgents, onToggleTasks: handleToggleTasks, onToggleDebug: handleToggleDebug, commandPaletteOpen: commandPaletteOpen, onCommandPaletteInput: handleCommandPaletteInput, approvalDialogOpen: !!pendingApproval, approvalDenyReasonMode: denyReasonMode, onApprovalApprove: handleApprove, onApprovalAlwaysAllow: handleAlwaysAllow, onApprovalDeny: handleDeny, onApprovalDenyWithReason: handleDenyWithReason, onApprovalCancelDenyReason: handleCancelDenyReason, onApprovalDenyReasonInput: handleApprovalDenyReasonInput, history: historyEntries }), commandPaletteOpen && (_jsx(ControlledCommandPalette, { theme: selectedTheme, items: filteredCommandItems, visible: commandPaletteOpen, query: commandPaletteQuery, selectedIndex: commandPaletteIndex, onQueryChange: setCommandPaletteQuery, onSelectItem: (item) => {
1830
1940
  setCommandPaletteOpen(false);
1831
1941
  setCommandPaletteQuery('');
1832
1942
  setCommandPaletteIndex(0);