attocode 0.1.6 → 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 (139) hide show
  1. package/CHANGELOG.md +23 -2
  2. package/README.md +47 -0
  3. package/dist/src/adapters.d.ts +21 -1
  4. package/dist/src/adapters.d.ts.map +1 -1
  5. package/dist/src/adapters.js +35 -1
  6. package/dist/src/adapters.js.map +1 -1
  7. package/dist/src/agent.d.ts +80 -2
  8. package/dist/src/agent.d.ts.map +1 -1
  9. package/dist/src/agent.js +874 -96
  10. package/dist/src/agent.js.map +1 -1
  11. package/dist/src/commands/agents-commands.d.ts.map +1 -1
  12. package/dist/src/commands/agents-commands.js +18 -4
  13. package/dist/src/commands/agents-commands.js.map +1 -1
  14. package/dist/src/commands/handler.d.ts.map +1 -1
  15. package/dist/src/commands/handler.js +126 -5
  16. package/dist/src/commands/handler.js.map +1 -1
  17. package/dist/src/defaults.d.ts +33 -1
  18. package/dist/src/defaults.d.ts.map +1 -1
  19. package/dist/src/defaults.js +61 -3
  20. package/dist/src/defaults.js.map +1 -1
  21. package/dist/src/integrations/agent-registry.d.ts +14 -0
  22. package/dist/src/integrations/agent-registry.d.ts.map +1 -1
  23. package/dist/src/integrations/agent-registry.js +4 -4
  24. package/dist/src/integrations/agent-registry.js.map +1 -1
  25. package/dist/src/integrations/cancellation.d.ts +62 -0
  26. package/dist/src/integrations/cancellation.d.ts.map +1 -1
  27. package/dist/src/integrations/cancellation.js +174 -0
  28. package/dist/src/integrations/cancellation.js.map +1 -1
  29. package/dist/src/integrations/dead-letter-queue.js +1 -1
  30. package/dist/src/integrations/dead-letter-queue.js.map +1 -1
  31. package/dist/src/integrations/economics.d.ts +41 -0
  32. package/dist/src/integrations/economics.d.ts.map +1 -1
  33. package/dist/src/integrations/economics.js +114 -8
  34. package/dist/src/integrations/economics.js.map +1 -1
  35. package/dist/src/integrations/history.d.ts +72 -0
  36. package/dist/src/integrations/history.d.ts.map +1 -0
  37. package/dist/src/integrations/history.js +165 -0
  38. package/dist/src/integrations/history.js.map +1 -0
  39. package/dist/src/integrations/index.d.ts +5 -3
  40. package/dist/src/integrations/index.d.ts.map +1 -1
  41. package/dist/src/integrations/index.js +6 -2
  42. package/dist/src/integrations/index.js.map +1 -1
  43. package/dist/src/integrations/resources.d.ts +5 -0
  44. package/dist/src/integrations/resources.d.ts.map +1 -1
  45. package/dist/src/integrations/resources.js +7 -0
  46. package/dist/src/integrations/resources.js.map +1 -1
  47. package/dist/src/integrations/safety.d.ts +3 -1
  48. package/dist/src/integrations/safety.d.ts.map +1 -1
  49. package/dist/src/integrations/safety.js +22 -5
  50. package/dist/src/integrations/safety.js.map +1 -1
  51. package/dist/src/integrations/sqlite-store.d.ts.map +1 -1
  52. package/dist/src/integrations/sqlite-store.js +17 -1
  53. package/dist/src/integrations/sqlite-store.js.map +1 -1
  54. package/dist/src/integrations/task-manager.d.ts +132 -0
  55. package/dist/src/integrations/task-manager.d.ts.map +1 -0
  56. package/dist/src/integrations/task-manager.js +309 -0
  57. package/dist/src/integrations/task-manager.js.map +1 -0
  58. package/dist/src/modes/tui.d.ts.map +1 -1
  59. package/dist/src/modes/tui.js +15 -0
  60. package/dist/src/modes/tui.js.map +1 -1
  61. package/dist/src/modes.d.ts +23 -0
  62. package/dist/src/modes.d.ts.map +1 -1
  63. package/dist/src/modes.js +61 -0
  64. package/dist/src/modes.js.map +1 -1
  65. package/dist/src/providers/adapters/openai.d.ts +46 -2
  66. package/dist/src/providers/adapters/openai.d.ts.map +1 -1
  67. package/dist/src/providers/adapters/openai.js +221 -21
  68. package/dist/src/providers/adapters/openai.js.map +1 -1
  69. package/dist/src/providers/adapters/openrouter.js +2 -2
  70. package/dist/src/providers/adapters/openrouter.js.map +1 -1
  71. package/dist/src/tools/agent.d.ts +18 -1
  72. package/dist/src/tools/agent.d.ts.map +1 -1
  73. package/dist/src/tools/agent.js +51 -3
  74. package/dist/src/tools/agent.js.map +1 -1
  75. package/dist/src/tools/tasks.d.ts +32 -0
  76. package/dist/src/tools/tasks.d.ts.map +1 -0
  77. package/dist/src/tools/tasks.js +334 -0
  78. package/dist/src/tools/tasks.js.map +1 -0
  79. package/dist/src/tracing/trace-collector.d.ts +81 -0
  80. package/dist/src/tracing/trace-collector.d.ts.map +1 -1
  81. package/dist/src/tracing/trace-collector.js +216 -4
  82. package/dist/src/tracing/trace-collector.js.map +1 -1
  83. package/dist/src/tracing/types.d.ts +8 -0
  84. package/dist/src/tracing/types.d.ts.map +1 -1
  85. package/dist/src/tracing/types.js.map +1 -1
  86. package/dist/src/tui/app.d.ts.map +1 -1
  87. package/dist/src/tui/app.js +459 -114
  88. package/dist/src/tui/app.js.map +1 -1
  89. package/dist/src/tui/components/ActiveAgentsPanel.d.ts +45 -0
  90. package/dist/src/tui/components/ActiveAgentsPanel.d.ts.map +1 -0
  91. package/dist/src/tui/components/ActiveAgentsPanel.js +121 -0
  92. package/dist/src/tui/components/ActiveAgentsPanel.js.map +1 -0
  93. package/dist/src/tui/components/DebugPanel.d.ts +41 -0
  94. package/dist/src/tui/components/DebugPanel.d.ts.map +1 -0
  95. package/dist/src/tui/components/DebugPanel.js +104 -0
  96. package/dist/src/tui/components/DebugPanel.js.map +1 -0
  97. package/dist/src/tui/components/ErrorBoundary.d.ts +63 -0
  98. package/dist/src/tui/components/ErrorBoundary.d.ts.map +1 -0
  99. package/dist/src/tui/components/ErrorBoundary.js +88 -0
  100. package/dist/src/tui/components/ErrorBoundary.js.map +1 -0
  101. package/dist/src/tui/components/ErrorDetailPanel.d.ts +49 -0
  102. package/dist/src/tui/components/ErrorDetailPanel.d.ts.map +1 -0
  103. package/dist/src/tui/components/ErrorDetailPanel.js +109 -0
  104. package/dist/src/tui/components/ErrorDetailPanel.js.map +1 -0
  105. package/dist/src/tui/components/TasksPanel.d.ts +25 -0
  106. package/dist/src/tui/components/TasksPanel.d.ts.map +1 -0
  107. package/dist/src/tui/components/TasksPanel.js +101 -0
  108. package/dist/src/tui/components/TasksPanel.js.map +1 -0
  109. package/dist/src/tui/components/ToolCallItem.d.ts +3 -4
  110. package/dist/src/tui/components/ToolCallItem.d.ts.map +1 -1
  111. package/dist/src/tui/components/ToolCallItem.js +51 -15
  112. package/dist/src/tui/components/ToolCallItem.js.map +1 -1
  113. package/dist/src/tui/components/index.d.ts +5 -0
  114. package/dist/src/tui/components/index.d.ts.map +1 -1
  115. package/dist/src/tui/components/index.js +10 -0
  116. package/dist/src/tui/components/index.js.map +1 -1
  117. package/dist/src/tui/hooks/index.d.ts +7 -0
  118. package/dist/src/tui/hooks/index.d.ts.map +1 -0
  119. package/dist/src/tui/hooks/index.js +7 -0
  120. package/dist/src/tui/hooks/index.js.map +1 -0
  121. package/dist/src/tui/hooks/useMessagePruning.d.ts +114 -0
  122. package/dist/src/tui/hooks/useMessagePruning.d.ts.map +1 -0
  123. package/dist/src/tui/hooks/useMessagePruning.js +127 -0
  124. package/dist/src/tui/hooks/useMessagePruning.js.map +1 -0
  125. package/dist/src/tui/index.d.ts +3 -0
  126. package/dist/src/tui/index.d.ts.map +1 -1
  127. package/dist/src/tui/index.js +9 -0
  128. package/dist/src/tui/index.js.map +1 -1
  129. package/dist/src/tui/utils/index.d.ts +7 -0
  130. package/dist/src/tui/utils/index.d.ts.map +1 -0
  131. package/dist/src/tui/utils/index.js +7 -0
  132. package/dist/src/tui/utils/index.js.map +1 -0
  133. package/dist/src/tui/utils/keyboard.d.ts +123 -0
  134. package/dist/src/tui/utils/keyboard.d.ts.map +1 -0
  135. package/dist/src/tui/utils/keyboard.js +185 -0
  136. package/dist/src/tui/utils/keyboard.js.map +1 -0
  137. package/dist/src/types.d.ts +94 -0
  138. package/dist/src/types.d.ts.map +1 -1
  139. package/package.json +1 -1
@@ -10,7 +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';
13
+ import { ActiveAgentsPanel } from './components/ActiveAgentsPanel.js';
14
+ import { TasksPanel } from './components/TasksPanel.js';
15
+ import { ToolCallItem } from './components/ToolCallItem.js';
16
+ import { DebugPanel, useDebugBuffer } from './components/DebugPanel.js';
14
17
  import { getTheme, getThemeNames } from './theme/index.js';
15
18
  import { ControlledCommandPalette } from './input/CommandPalette.js';
16
19
  import { ApprovalDialog } from './components/ApprovalDialog.js';
@@ -18,6 +21,7 @@ import { TransparencyAggregator } from './transparency-aggregator.js';
18
21
  import { handleSkillsCommand } from '../commands/skills-commands.js';
19
22
  import { handleAgentsCommand } from '../commands/agents-commands.js';
20
23
  import { handleInitCommand } from '../commands/init-commands.js';
24
+ import { createHistoryManager } from '../integrations/history.js';
21
25
  // =============================================================================
22
26
  // PATTERN GENERATION FOR ALWAYS-ALLOW
23
27
  // =============================================================================
@@ -57,98 +61,34 @@ const MessageItem = memo(function MessageItem({ msg, colors }) {
57
61
  const label = isUser ? 'You' : isAssistant ? 'Assistant' : isError ? 'Error' : 'System';
58
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 }) })] }));
59
63
  });
60
- const ToolCallItem = memo(function ToolCallItem({ tc, expanded, colors }) {
61
- const icon = tc.status === 'success' ? '[OK]' : tc.status === 'error' ? '[X]' : tc.status === 'running' ? '[~]' : '[ ]';
62
- const statusColor = tc.status === 'success' ? '#98FB98' : tc.status === 'error' ? '#FF6B6B' : tc.status === 'running' ? '#87CEEB' : colors.textMuted;
63
- // Compact formatting for collapsed view
64
- const formatToolArgsCompact = (args) => {
65
- const entries = Object.entries(args);
66
- if (entries.length === 0)
67
- return '';
68
- if (entries.length === 1) {
69
- const [key, val] = entries[0];
70
- const valStr = typeof val === 'string' ? val : JSON.stringify(val);
71
- return valStr.length > 50 ? `${key}: ${valStr.slice(0, 47)}...` : `${key}: ${valStr}`;
72
- }
73
- return `{${entries.length} args}`;
74
- };
75
- // Expanded formatting - each arg on its own line with proper handling
76
- const formatToolArgsExpanded = (args) => {
77
- const entries = Object.entries(args);
78
- if (entries.length === 0)
79
- return [];
80
- return entries.map(([key, val]) => {
81
- let valStr;
82
- if (typeof val === 'string') {
83
- // For strings, show with quotes, handle multiline
84
- if (val.includes('\n')) {
85
- const lines = val.split('\n');
86
- if (lines.length > 3) {
87
- valStr = `"${lines.slice(0, 3).join('\\n')}..." (${lines.length} lines)`;
88
- }
89
- else {
90
- valStr = `"${val.replace(/\n/g, '\\n')}"`;
91
- }
92
- }
93
- else if (val.length > 100) {
94
- valStr = `"${val.slice(0, 97)}..."`;
95
- }
96
- else {
97
- valStr = `"${val}"`;
98
- }
99
- }
100
- else if (typeof val === 'object' && val !== null) {
101
- const json = JSON.stringify(val, null, 2);
102
- if (json.length > 200) {
103
- valStr = JSON.stringify(val).slice(0, 197) + '...';
104
- }
105
- else {
106
- valStr = json;
107
- }
108
- }
109
- else {
110
- valStr = String(val);
111
- }
112
- return `${key}: ${valStr}`;
113
- });
114
- };
115
- const argsStr = formatToolArgsCompact(tc.args);
116
- if (expanded) {
117
- const expandedArgs = formatToolArgsExpanded(tc.args);
118
- 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') &&
119
- typeof tc.result === 'object' && tc.result !== null &&
120
- 'metadata' in tc.result &&
121
- 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}` }) }))] }));
122
- }
123
- // Check if result has diff metadata for collapsed summary
124
- const hasDiff = tc.status === 'success' &&
125
- (tc.name === 'edit_file' || tc.name === 'write_file') &&
126
- typeof tc.result === 'object' && tc.result !== null &&
127
- 'metadata' in tc.result &&
128
- typeof tc.result.metadata?.diff === 'string';
129
- 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] }));
130
- });
131
- const MemoizedInputArea = memo(function MemoizedInputArea({ onSubmit, disabled, borderColor, textColor, cursorColor, onCtrlC, onCtrlL, onCtrlP, onEscape, onToggleToolExpand, onToggleThinking, onToggleTransparency, 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, }) {
132
65
  const [value, setValue] = useState('');
133
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;
134
72
  // Store callbacks in refs so useInput doesn't re-subscribe on prop changes
135
73
  const callbacksRef = useRef({
136
74
  onSubmit, onCtrlC, onCtrlL, onCtrlP, onEscape,
137
- onToggleToolExpand, onToggleThinking, onToggleTransparency,
75
+ onToggleToolExpand, onToggleThinking, onToggleTransparency, onToggleActiveAgents, onToggleTasks, onToggleDebug,
138
76
  onPageUp, onPageDown, onHome, onEnd,
139
77
  commandPaletteOpen, onCommandPaletteInput,
140
78
  approvalDialogOpen, approvalDenyReasonMode,
141
79
  onApprovalApprove, onApprovalAlwaysAllow, onApprovalDeny, onApprovalDenyWithReason,
142
80
  onApprovalCancelDenyReason, onApprovalDenyReasonInput,
81
+ onHistorySearch,
143
82
  });
144
83
  callbacksRef.current = {
145
84
  onSubmit, onCtrlC, onCtrlL, onCtrlP, onEscape,
146
- onToggleToolExpand, onToggleThinking, onToggleTransparency,
85
+ onToggleToolExpand, onToggleThinking, onToggleTransparency, onToggleActiveAgents, onToggleTasks, onToggleDebug,
147
86
  onPageUp, onPageDown, onHome, onEnd,
148
87
  commandPaletteOpen, onCommandPaletteInput,
149
88
  approvalDialogOpen, approvalDenyReasonMode,
150
89
  onApprovalApprove, onApprovalAlwaysAllow, onApprovalDeny, onApprovalDenyWithReason,
151
90
  onApprovalCancelDenyReason, onApprovalDenyReasonInput,
91
+ onHistorySearch,
152
92
  };
153
93
  const disabledRef = useRef(disabled);
154
94
  disabledRef.current = disabled;
@@ -186,6 +126,21 @@ const MemoizedInputArea = memo(function MemoizedInputArea({ onSubmit, disabled,
186
126
  cb.onToggleTransparency?.();
187
127
  return;
188
128
  }
129
+ // Alt+A / Option+A - Toggle active agents panel
130
+ if (input === '\u00e5' || (key.meta && input === 'a')) {
131
+ cb.onToggleActiveAgents?.();
132
+ return;
133
+ }
134
+ // Alt+K / Option+K - Toggle tasks panel
135
+ if (input === '\u02da' || (key.meta && input === 'k')) {
136
+ cb.onToggleTasks?.();
137
+ return;
138
+ }
139
+ // Alt+D / Option+D - Toggle debug panel
140
+ if (input === '\u2202' || (key.meta && input === 'd')) {
141
+ cb.onToggleDebug?.();
142
+ return;
143
+ }
189
144
  // Command palette keyboard handling (when open)
190
145
  if (cb.commandPaletteOpen && cb.onCommandPaletteInput) {
191
146
  cb.onCommandPaletteInput(input, key);
@@ -249,10 +204,18 @@ const MemoizedInputArea = memo(function MemoizedInputArea({ onSubmit, disabled,
249
204
  // Input handling (only when not disabled)
250
205
  if (disabledRef.current)
251
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
+ }
252
213
  if (key.return && value.trim()) {
253
214
  cb.onSubmit(value);
254
215
  setValue('');
255
216
  setCursorPos(0);
217
+ setHistoryIndex(-1); // Reset history navigation
218
+ setSavedInput('');
256
219
  return;
257
220
  }
258
221
  if (key.backspace || key.delete) {
@@ -270,6 +233,54 @@ const MemoizedInputArea = memo(function MemoizedInputArea({ onSubmit, disabled,
270
233
  setCursorPos(p => Math.min(value.length, p + 1));
271
234
  return;
272
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
+ }
273
284
  if (key.ctrl && input === 'a') {
274
285
  setCursorPos(0);
275
286
  return;
@@ -288,7 +299,10 @@ const MemoizedInputArea = memo(function MemoizedInputArea({ onSubmit, disabled,
288
299
  setCursorPos(p => p + input.length);
289
300
  }
290
301
  });
291
- 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)"] }))] }) }));
292
306
  }, (prevProps, nextProps) => {
293
307
  // Custom comparison: only re-render if visual props change
294
308
  return prevProps.disabled === nextProps.disabled &&
@@ -314,12 +328,27 @@ export function TUIApp({ agent, sessionStore, mcpClient, compactor, lspManager,
314
328
  const [contextTokens, setContextTokens] = useState(0);
315
329
  const [elapsedTime, setElapsedTime] = useState(0);
316
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);
317
339
  const [executionMode, setExecutionMode] = useState('idle');
318
340
  const executionModeRef = useRef('idle');
319
341
  // Display toggles
320
342
  const [toolCallsExpanded, setToolCallsExpanded] = useState(false);
321
343
  const [showThinking, setShowThinking] = useState(true);
322
344
  const [transparencyExpanded, setTransparencyExpanded] = useState(false);
345
+ const [activeAgentsExpanded, setActiveAgentsExpanded] = useState(true);
346
+ const [tasksExpanded, setTasksExpanded] = useState(true);
347
+ const [debugExpanded, setDebugExpanded] = useState(false);
348
+ // Active agents tracking (for Active Agents Panel)
349
+ const [activeAgents, setActiveAgents] = useState([]);
350
+ // Tasks tracking (for Tasks Panel)
351
+ const [tasks, setTasks] = useState([]);
323
352
  // Command palette state
324
353
  const [commandPaletteOpen, setCommandPaletteOpen] = useState(false);
325
354
  const [commandPaletteQuery, setCommandPaletteQuery] = useState('');
@@ -333,6 +362,9 @@ export function TUIApp({ agent, sessionStore, mcpClient, compactor, lspManager,
333
362
  // Transparency state
334
363
  const [transparencyState, setTransparencyState] = useState(null);
335
364
  const transparencyAggregatorRef = useRef(null);
365
+ // Consecutive Ctrl+C tracking for force exit
366
+ const [ctrlCCount, setCtrlCCount] = useState(0);
367
+ const ctrlCTimerRef = useRef(null);
336
368
  // Refs for stable callbacks
337
369
  const isProcessingRef = useRef(isProcessing);
338
370
  const messagesLengthRef = useRef(messages.length);
@@ -348,6 +380,22 @@ export function TUIApp({ agent, sessionStore, mcpClient, compactor, lspManager,
348
380
  const uniqueId = `${role}-${Date.now()}-${++messageIdCounter.current}`;
349
381
  setMessages(prev => [...prev, { id: uniqueId, role, content, ts: new Date() }]);
350
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]);
351
399
  // =========================================================================
352
400
  // APPROVAL DIALOG HANDLERS
353
401
  // =========================================================================
@@ -420,6 +468,10 @@ export function TUIApp({ agent, sessionStore, mcpClient, compactor, lspManager,
420
468
  const mode = executionModeRef.current;
421
469
  if (mode === 'idle')
422
470
  return; // No active execution, ignore events
471
+ // Log event to debug buffer
472
+ if (debugExpanded) {
473
+ debugBuffer.debug(`Event: ${event.type}`, event);
474
+ }
423
475
  // Extract subagent from event if present (not all events have it)
424
476
  const eventWithSubagent = event;
425
477
  const subagentPrefix = eventWithSubagent.subagent ? `[${eventWithSubagent.subagent}] ` : '';
@@ -441,26 +493,66 @@ export function TUIApp({ agent, sessionStore, mcpClient, compactor, lspManager,
441
493
  // -------------------------------------------------------------------------
442
494
  // Shared events (both processing and approving modes)
443
495
  // -------------------------------------------------------------------------
444
- // Subagent lifecycle events
496
+ // Subagent lifecycle events - also update Active Agents Panel
445
497
  if (event.type === 'agent.spawn') {
446
498
  const e = event;
499
+ const agentId = e.agentId || `spawn-${Date.now()}`;
447
500
  addMessage('system', `[AGENT] Spawning ${e.name}: ${e.task.slice(0, 100)}${e.task.length > 100 ? '...' : ''}`);
501
+ // Add to active agents panel
502
+ setActiveAgents(prev => [...prev, {
503
+ id: agentId,
504
+ type: e.name,
505
+ task: e.task,
506
+ status: 'running',
507
+ tokens: 0,
508
+ startTime: Date.now(),
509
+ }]);
448
510
  return;
449
511
  }
450
512
  if (event.type === 'agent.complete') {
451
513
  const e = event;
452
514
  const statusText = e.success ? 'completed' : 'failed';
453
- addMessage('system', `[AGENT] ${e.agentId} ${statusText}`);
454
- // 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)
455
518
  if (e.output && e.output.length > 50) {
456
- const preview = e.output.slice(0, 300);
457
- 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)` : ''}`);
458
522
  }
523
+ // Update active agents panel - use strict ID matching
524
+ setActiveAgents(prev => prev.map(a => a.id === e.agentId
525
+ ? { ...a, status: e.success ? 'completed' : 'error' }
526
+ : a));
459
527
  return;
460
528
  }
461
529
  if (event.type === 'agent.error') {
462
530
  const e = event;
463
- addMessage('system', `[AGENT] ${e.agentId} error: ${e.error}`);
531
+ const displayName = e.agentType || e.agentId;
532
+ addMessage('system', `[AGENT] ${displayName} error: ${e.error}`);
533
+ // For timeout errors, use 'timing_out' status first to indicate the agent
534
+ // is in the process of stopping. Then transition to 'timeout' after a delay.
535
+ // This provides better UX than immediately showing "failed" while tokens accumulate.
536
+ const isTimeout = e.error.includes('timed out') || e.error.includes('Timed out');
537
+ if (isTimeout) {
538
+ // First, mark as timing_out - use strict ID matching
539
+ setActiveAgents(prev => prev.map(a => a.id === e.agentId
540
+ ? { ...a, status: 'timing_out' }
541
+ : a));
542
+ // After 3 seconds, transition to final timeout status
543
+ // (agent should have stopped by then due to cancellation token check)
544
+ setTimeout(() => {
545
+ setActiveAgents(prev => prev.map(a => a.id === e.agentId && a.status === 'timing_out'
546
+ ? { ...a, status: 'timeout' }
547
+ : a));
548
+ }, 3000);
549
+ }
550
+ else {
551
+ // Regular error - set immediately with strict ID matching
552
+ setActiveAgents(prev => prev.map(a => a.id === e.agentId
553
+ ? { ...a, status: 'error' }
554
+ : a));
555
+ }
464
556
  return;
465
557
  }
466
558
  if (event.type === 'agent.pending_plan') {
@@ -529,10 +621,33 @@ export function TUIApp({ agent, sessionStore, mcpClient, compactor, lspManager,
529
621
  addMessage('error', `${prefix} ${errorMsg}`);
530
622
  return;
531
623
  }
532
- // Insight events
533
- if (event.type === 'insight.tokens' && showThinking) {
624
+ // Insight events - also track tokens for active agents
625
+ if (event.type === 'insight.tokens') {
534
626
  const e = event;
535
- addMessage('system', `${subagentPrefix}* ${e.inputTokens.toLocaleString()} in, ${e.outputTokens.toLocaleString()} out${e.cost ? ` $${e.cost.toFixed(6)}` : ''}`);
627
+ if (showThinking) {
628
+ addMessage('system', `${subagentPrefix}* ${e.inputTokens.toLocaleString()} in, ${e.outputTokens.toLocaleString()} out${e.cost ? ` $${e.cost.toFixed(6)}` : ''}`);
629
+ }
630
+ // Update tokens for active agent if this event is from a subagent
631
+ // IMPORTANT: Don't update tokens for agents that are timing_out/timeout/error
632
+ // These agents should have stopped, and any lingering events are from
633
+ // zombie processes that we don't want to count.
634
+ if (e.subagent || eventWithSubagent.subagent) {
635
+ const subagentId = e.subagentId || eventWithSubagent.subagentId;
636
+ const agentName = e.subagent || eventWithSubagent.subagent;
637
+ setActiveAgents(prev => prev.map(a => {
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 || ''));
643
+ const isStillRunning = a.status === 'running';
644
+ // Only update tokens if agent is still running
645
+ if (matchesAgent && isStillRunning) {
646
+ return { ...a, tokens: a.tokens + (e.inputTokens || 0) + (e.outputTokens || 0) };
647
+ }
648
+ return a;
649
+ }));
650
+ }
536
651
  return;
537
652
  }
538
653
  // Resilience events
@@ -546,15 +661,45 @@ export function TUIApp({ agent, sessionStore, mcpClient, compactor, lspManager,
546
661
  addMessage('system', `[RECOVERED] ${e.reason} after ${e.attempts} attempt(s)`);
547
662
  return;
548
663
  }
549
- // Subagent visibility events
664
+ // Subagent visibility events - also update Active Agents Panel
550
665
  if (event.type === 'subagent.iteration') {
551
666
  const e = event;
552
667
  setStatus(s => ({ ...s, mode: `${e.agentId} iter ${e.iteration}/${e.maxIterations}` }));
668
+ // Update active agents panel with iteration info
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
+ }));
553
676
  return;
554
677
  }
555
678
  if (event.type === 'subagent.phase') {
556
679
  const e = event;
557
680
  setStatus(s => ({ ...s, mode: `${e.agentId} ${e.phase}` }));
681
+ // Update active agents panel with phase info
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
+ }));
689
+ return;
690
+ }
691
+ // Task events - update Tasks Panel
692
+ if (event.type === 'task.created') {
693
+ const e = event;
694
+ setTasks(prev => [...prev, e.task]);
695
+ addMessage('system', `[TASK] Created: ${e.task.subject}`);
696
+ return;
697
+ }
698
+ if (event.type === 'task.updated') {
699
+ const e = event;
700
+ setTasks(prev => prev.map(t => t.id === e.task.id ? e.task : t));
701
+ // Only log status changes
702
+ addMessage('system', `[TASK] ${e.task.subject}: ${e.task.status}`);
558
703
  return;
559
704
  }
560
705
  // -------------------------------------------------------------------------
@@ -673,9 +818,11 @@ export function TUIApp({ agent, sessionStore, mcpClient, compactor, lspManager,
673
818
  ' /theme [name] Show/change theme',
674
819
  ' /tools List tools',
675
820
  '',
676
- '> SESSIONS',
821
+ '> SESSIONS & TASKS',
677
822
  ' /save Save session',
678
823
  ' /sessions List sessions',
824
+ ' /load <id> Load session by ID',
825
+ ' /tasks List tracked tasks',
679
826
  ' /checkpoint Create checkpoint',
680
827
  ' /checkpoints List checkpoints',
681
828
  ' /restore <id> Restore checkpoint',
@@ -727,6 +874,8 @@ export function TUIApp({ agent, sessionStore, mcpClient, compactor, lspManager,
727
874
  ' Alt+T Toggle tool details',
728
875
  ' Alt+O Toggle thinking',
729
876
  ' Alt+I Toggle transparency panel',
877
+ ' Alt+K Toggle tasks panel',
878
+ ' Alt+D Toggle debug panel',
730
879
  '========================',
731
880
  ].join('\n'));
732
881
  return;
@@ -750,6 +899,7 @@ export function TUIApp({ agent, sessionStore, mcpClient, compactor, lspManager,
750
899
  plan: agentState.plan,
751
900
  memoryContext: agentState.memoryContext,
752
901
  });
902
+ persistPendingPlanToStore();
753
903
  addMessage('system', `Session saved: ${currentSessionId} (checkpoint: ${ckptId})`);
754
904
  }
755
905
  catch (e) {
@@ -765,6 +915,98 @@ export function TUIApp({ agent, sessionStore, mcpClient, compactor, lspManager,
765
915
  addMessage('error', e.message);
766
916
  }
767
917
  return;
918
+ case 'load': {
919
+ const targetSessionId = args[0];
920
+ if (!targetSessionId) {
921
+ addMessage('system', 'Usage: /load <session-id>\n Use /sessions to list available sessions');
922
+ return;
923
+ }
924
+ // Check if session exists
925
+ const targetSession = sessionStore.getSessionMetadata(targetSessionId);
926
+ if (!targetSession) {
927
+ addMessage('error', `Session not found: ${targetSessionId}\n Use /sessions to list available sessions`);
928
+ return;
929
+ }
930
+ try {
931
+ addMessage('system', `Loading session: ${targetSession.id}\n Created: ${new Date(targetSession.createdAt).toLocaleString()}\n Messages: ${targetSession.messageCount}`);
932
+ // Try to load from checkpoint first
933
+ let loadCheckpointData;
934
+ if ('loadLatestCheckpoint' in sessionStore && typeof sessionStore.loadLatestCheckpoint === 'function') {
935
+ const sqliteCheckpoint = sessionStore.loadLatestCheckpoint(targetSession.id);
936
+ if (sqliteCheckpoint?.state) {
937
+ loadCheckpointData = sqliteCheckpoint.state;
938
+ }
939
+ }
940
+ // Fall back to loading from entries if no checkpoint
941
+ if (!loadCheckpointData) {
942
+ const entriesResult = sessionStore.loadSession(targetSession.id);
943
+ const entries = Array.isArray(entriesResult) ? entriesResult : await entriesResult;
944
+ const checkpoint = [...entries].reverse().find((e) => e.type === 'checkpoint');
945
+ if (checkpoint?.data) {
946
+ loadCheckpointData = checkpoint.data;
947
+ }
948
+ else {
949
+ const messages = entries
950
+ .filter((e) => e.type === 'message')
951
+ .map((e) => e.data);
952
+ if (messages.length > 0) {
953
+ agent.loadState({ messages });
954
+ addMessage('system', `+ Loaded ${messages.length} messages from session`);
955
+ }
956
+ else {
957
+ addMessage('system', 'No messages found in session');
958
+ }
959
+ return;
960
+ }
961
+ }
962
+ // Load from checkpoint data
963
+ if (loadCheckpointData?.messages) {
964
+ agent.loadState({
965
+ messages: loadCheckpointData.messages,
966
+ iteration: loadCheckpointData.iteration,
967
+ metrics: loadCheckpointData.metrics,
968
+ plan: loadCheckpointData.plan,
969
+ memoryContext: loadCheckpointData.memoryContext,
970
+ });
971
+ addMessage('system', `+ Loaded ${loadCheckpointData.messages.length} messages from session${loadCheckpointData.iteration ? `\n Iteration: ${loadCheckpointData.iteration}` : ''}${loadCheckpointData.plan ? '\n Plan restored' : ''}`);
972
+ }
973
+ }
974
+ catch (e) {
975
+ addMessage('error', `Error loading session: ${e.message}`);
976
+ }
977
+ return;
978
+ }
979
+ case 'tasks': {
980
+ // Filter out deleted tasks
981
+ const visibleTasks = tasks.filter(t => t.status !== 'deleted');
982
+ if (visibleTasks.length === 0) {
983
+ addMessage('system', 'No tasks. Tasks are created when the agent uses task_create tool.');
984
+ return;
985
+ }
986
+ // Count by status
987
+ const pending = visibleTasks.filter(t => t.status === 'pending').length;
988
+ const inProgress = visibleTasks.filter(t => t.status === 'in_progress').length;
989
+ const completed = visibleTasks.filter(t => t.status === 'completed').length;
990
+ // Format task list
991
+ const taskLines = visibleTasks.map(t => {
992
+ const isBlocked = t.blockedBy.some(id => {
993
+ const blocker = visibleTasks.find(bt => bt.id === id);
994
+ return blocker && blocker.status !== 'completed';
995
+ });
996
+ const icon = isBlocked ? '◌' : t.status === 'completed' ? '✓' : t.status === 'in_progress' ? '●' : '○';
997
+ const blockedInfo = isBlocked ? ` (blocked by: ${t.blockedBy.slice(0, 2).join(', ')})` : '';
998
+ const activeInfo = t.status === 'in_progress' && t.activeForm ? `\n └ ${t.activeForm}...` : '';
999
+ return ` ${icon} ${t.id} ${t.subject}${blockedInfo}${activeInfo}`;
1000
+ });
1001
+ addMessage('system', [
1002
+ `TASKS [${pending} pending, ${inProgress} in_progress, ${completed} completed]`,
1003
+ '',
1004
+ ...taskLines,
1005
+ '',
1006
+ 'Toggle panel: Alt+K',
1007
+ ].join('\n'));
1008
+ return;
1009
+ }
768
1010
  case 'context':
769
1011
  case 'ctx': {
770
1012
  const agentState = agent.getState();
@@ -1188,7 +1430,7 @@ export function TUIApp({ agent, sessionStore, mcpClient, compactor, lspManager,
1188
1430
  case 'trace': {
1189
1431
  const traceCollector = agent.getTraceCollector();
1190
1432
  if (args.length === 0) {
1191
- // Show current session trace summary
1433
+ // Show current session trace summary with subagent hierarchy
1192
1434
  if (!traceCollector) {
1193
1435
  addMessage('system', 'Tracing is not enabled. Start agent with --trace to enable.');
1194
1436
  return;
@@ -1198,24 +1440,56 @@ export function TUIApp({ agent, sessionStore, mcpClient, compactor, lspManager,
1198
1440
  addMessage('system', 'No trace data collected yet.');
1199
1441
  return;
1200
1442
  }
1201
- addMessage('system', [
1202
- 'Trace Summary:',
1203
- ` Session ID: ${data.sessionId}`,
1204
- ` Status: ${data.status}`,
1205
- ` Iterations: ${data.iterations.length}`,
1206
- ` Duration: ${data.durationMs ? `${Math.round(data.durationMs / 1000)}s` : 'ongoing'}`,
1207
- '',
1208
- 'Metrics:',
1209
- ` Input tokens: ${data.metrics.inputTokens.toLocaleString()}`,
1210
- ` Output tokens: ${data.metrics.outputTokens.toLocaleString()}`,
1211
- ` Cache hit: ${Math.round(data.metrics.avgCacheHitRate * 100)}%`,
1212
- ` Tool calls: ${data.metrics.toolCalls}`,
1213
- ` Errors: ${data.metrics.errors}`,
1214
- ` Est. Cost: $${data.metrics.estimatedCost.toFixed(4)}`,
1215
- '',
1216
- 'Use: /trace --analyze for efficiency analysis',
1217
- ' /trace issues to see detected inefficiencies',
1218
- ].join('\n'));
1443
+ // Get subagent hierarchy from JSONL file
1444
+ const hierarchy = await traceCollector.getSubagentHierarchy();
1445
+ if (hierarchy && hierarchy.subagents.length > 0) {
1446
+ // Show hierarchy view with subagents
1447
+ const lines = [
1448
+ 'Trace Summary:',
1449
+ ` Session ID: ${data.sessionId}`,
1450
+ ` Status: ${data.status}`,
1451
+ ` Duration: ${data.durationMs ? `${Math.round(data.durationMs / 1000)}s` : 'ongoing'}`,
1452
+ '',
1453
+ 'Main Agent:',
1454
+ ` Iterations: ${hierarchy.mainAgent.llmCalls}`,
1455
+ ` Input tokens: ${hierarchy.mainAgent.inputTokens.toLocaleString()}`,
1456
+ ` Output tokens: ${hierarchy.mainAgent.outputTokens.toLocaleString()}`,
1457
+ ` Tool calls: ${hierarchy.mainAgent.toolCalls}`,
1458
+ '',
1459
+ 'Subagent Tree:',
1460
+ ];
1461
+ // Sort subagents by spawn time
1462
+ const sortedSubagents = hierarchy.subagents.sort((a, b) => (a.spawnedAtIteration || 0) - (b.spawnedAtIteration || 0));
1463
+ for (const sub of sortedSubagents) {
1464
+ const durationSec = Math.round(sub.duration / 1000);
1465
+ lines.push(` └─ ${sub.agentId} (spawned iter ${sub.spawnedAtIteration || '?'})`);
1466
+ lines.push(` ├─ ${sub.inputTokens.toLocaleString()} in / ${sub.outputTokens.toLocaleString()} out tokens`);
1467
+ lines.push(` ├─ ${sub.toolCalls} tools | ${durationSec}s`);
1468
+ }
1469
+ lines.push('', 'TOTALS (all agents):', ` Input tokens: ${hierarchy.totals.inputTokens.toLocaleString()}`, ` Output tokens: ${hierarchy.totals.outputTokens.toLocaleString()}`, ` Tool calls: ${hierarchy.totals.toolCalls}`, ` LLM calls: ${hierarchy.totals.llmCalls}`, ` Est. Cost: $${hierarchy.totals.estimatedCost.toFixed(4)}`, ` Duration: ${Math.round(hierarchy.totals.duration / 1000)}s`, '', 'Use: /trace --analyze for efficiency analysis', ' /trace issues to see detected inefficiencies');
1470
+ addMessage('system', lines.join('\n'));
1471
+ }
1472
+ else {
1473
+ // Original simple view (no subagents)
1474
+ addMessage('system', [
1475
+ 'Trace Summary:',
1476
+ ` Session ID: ${data.sessionId}`,
1477
+ ` Status: ${data.status}`,
1478
+ ` Iterations: ${data.iterations.length}`,
1479
+ ` Duration: ${data.durationMs ? `${Math.round(data.durationMs / 1000)}s` : 'ongoing'}`,
1480
+ '',
1481
+ 'Metrics:',
1482
+ ` Input tokens: ${data.metrics.inputTokens.toLocaleString()}`,
1483
+ ` Output tokens: ${data.metrics.outputTokens.toLocaleString()}`,
1484
+ ` Cache hit: ${Math.round(data.metrics.avgCacheHitRate * 100)}%`,
1485
+ ` Tool calls: ${data.metrics.toolCalls}`,
1486
+ ` Errors: ${data.metrics.errors}`,
1487
+ ` Est. Cost: $${data.metrics.estimatedCost.toFixed(4)}`,
1488
+ '',
1489
+ 'Use: /trace --analyze for efficiency analysis',
1490
+ ' /trace issues to see detected inefficiencies',
1491
+ ].join('\n'));
1492
+ }
1219
1493
  }
1220
1494
  else if (args[0] === '--analyze' || args[0] === 'analyze') {
1221
1495
  if (!traceCollector) {
@@ -1337,7 +1611,7 @@ export function TUIApp({ agent, sessionStore, mcpClient, compactor, lspManager,
1337
1611
  default:
1338
1612
  addMessage('system', `Unknown: /${cmd}. Try /help`);
1339
1613
  }
1340
- }, [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]);
1341
1615
  // =========================================================================
1342
1616
  // SUBMIT HANDLER
1343
1617
  // =========================================================================
@@ -1345,6 +1619,11 @@ export function TUIApp({ agent, sessionStore, mcpClient, compactor, lspManager,
1345
1619
  const trimmed = input.trim();
1346
1620
  if (!trimmed)
1347
1621
  return;
1622
+ // Add to history (persistent)
1623
+ if (historyManagerRef.current) {
1624
+ historyManagerRef.current.addEntry(trimmed);
1625
+ setHistoryEntries(historyManagerRef.current.getHistory());
1626
+ }
1348
1627
  addMessage('user', trimmed);
1349
1628
  if (trimmed.startsWith('/')) {
1350
1629
  const parts = trimmed.slice(1).split(/\s+/);
@@ -1356,6 +1635,8 @@ export function TUIApp({ agent, sessionStore, mcpClient, compactor, lspManager,
1356
1635
  executionModeRef.current = 'processing';
1357
1636
  setExecutionMode('processing');
1358
1637
  setStatus(s => ({ ...s, mode: 'thinking' }));
1638
+ // Reset CPU time counter for per-prompt resource limits (prevents session-wide timeout)
1639
+ agent.resetResourceTimer();
1359
1640
  try {
1360
1641
  const result = await agent.run(trimmed);
1361
1642
  const metrics = agent.getMetrics();
@@ -1398,6 +1679,7 @@ export function TUIApp({ agent, sessionStore, mcpClient, compactor, lspManager,
1398
1679
  plan: checkpoint.state.plan,
1399
1680
  memoryContext: checkpoint.state.memoryContext,
1400
1681
  });
1682
+ persistPendingPlanToStore();
1401
1683
  }
1402
1684
  catch (e) {
1403
1685
  persistenceDebug.error('[TUI] Checkpoint failed', e);
@@ -1413,7 +1695,7 @@ export function TUIApp({ agent, sessionStore, mcpClient, compactor, lspManager,
1413
1695
  setIsProcessing(false);
1414
1696
  setToolCalls([]);
1415
1697
  }
1416
- }, [addMessage, handleCommand, agent, sessionStore, saveCheckpointToStore, persistenceDebug]);
1698
+ }, [addMessage, handleCommand, agent, sessionStore, saveCheckpointToStore, persistenceDebug, persistPendingPlanToStore]);
1417
1699
  // =========================================================================
1418
1700
  // COMMAND PALETTE ITEMS
1419
1701
  // =========================================================================
@@ -1423,6 +1705,7 @@ export function TUIApp({ agent, sessionStore, mcpClient, compactor, lspManager,
1423
1705
  { id: 'clear', label: 'Clear Screen', shortcut: 'Ctrl+L', category: 'General', action: () => { setMessages([]); setToolCalls([]); } },
1424
1706
  { id: 'save', label: 'Save Session', shortcut: '/save', category: 'Sessions', action: () => handleCommand('save', []) },
1425
1707
  { id: 'sessions', label: 'List Sessions', shortcut: '/sessions', category: 'Sessions', action: () => handleCommand('sessions', []) },
1708
+ { id: 'load', label: 'Load Session', shortcut: '/load <id>', category: 'Sessions', action: () => handleCommand('sessions', []) }, // Shows sessions, user types /load <id>
1426
1709
  { id: 'context', label: 'Context Info', shortcut: '/context', category: 'Context', action: () => handleCommand('context', []) },
1427
1710
  { id: 'compact', label: 'Compact Context', shortcut: '/compact', category: 'Context', action: () => handleCommand('compact', []) },
1428
1711
  { id: 'mcp', label: 'MCP Servers', shortcut: '/mcp', category: 'MCP', action: () => handleCommand('mcp', []) },
@@ -1506,8 +1789,33 @@ export function TUIApp({ agent, sessionStore, mcpClient, compactor, lspManager,
1506
1789
  // KEYBOARD CALLBACKS
1507
1790
  // =========================================================================
1508
1791
  const handleCtrlC = useCallback(() => {
1509
- agent.cleanup().then(() => mcpClient.cleanup()).then(() => lspManager.cleanup()).then(() => exit());
1510
- }, [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]);
1511
1819
  const handleCtrlL = useCallback(() => {
1512
1820
  setMessages([]);
1513
1821
  setToolCalls([]);
@@ -1517,7 +1825,7 @@ export function TUIApp({ agent, sessionStore, mcpClient, compactor, lspManager,
1517
1825
  setCommandPaletteQuery('');
1518
1826
  setCommandPaletteIndex(0);
1519
1827
  }, []);
1520
- const handleEscape = useCallback(() => {
1828
+ const handleEscape = useCallback(async () => {
1521
1829
  // Close command palette first if open
1522
1830
  if (commandPaletteOpen) {
1523
1831
  setCommandPaletteOpen(false);
@@ -1527,11 +1835,30 @@ export function TUIApp({ agent, sessionStore, mcpClient, compactor, lspManager,
1527
1835
  }
1528
1836
  // Otherwise cancel processing
1529
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
1530
1857
  agent.cancel('Cancelled by ESC');
1531
1858
  setIsProcessing(false);
1532
- addMessage('system', '[STOP] Cancelled');
1859
+ addMessage('system', '[STOP] Cancelled (checkpoint saved)');
1533
1860
  }
1534
- }, [agent, addMessage, commandPaletteOpen]);
1861
+ }, [agent, addMessage, commandPaletteOpen, sessionStore, currentSessionId, saveCheckpointToStore, persistenceDebug, persistPendingPlanToStore]);
1535
1862
  const handleToggleToolExpand = useCallback(() => {
1536
1863
  setToolCallsExpanded(prev => {
1537
1864
  addMessage('system', !prev ? '[*] Tool details: expanded' : '[ ] Tool details: collapsed');
@@ -1550,6 +1877,24 @@ export function TUIApp({ agent, sessionStore, mcpClient, compactor, lspManager,
1550
1877
  return !prev;
1551
1878
  });
1552
1879
  }, [addMessage]);
1880
+ const handleToggleActiveAgents = useCallback(() => {
1881
+ setActiveAgentsExpanded(prev => {
1882
+ addMessage('system', !prev ? '[v] Active agents: visible' : '[^] Active agents: hidden');
1883
+ return !prev;
1884
+ });
1885
+ }, [addMessage]);
1886
+ const handleToggleTasks = useCallback(() => {
1887
+ setTasksExpanded(prev => {
1888
+ addMessage('system', !prev ? '[v] Tasks: visible' : '[^] Tasks: hidden');
1889
+ return !prev;
1890
+ });
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]);
1553
1898
  // Update context tokens
1554
1899
  useEffect(() => {
1555
1900
  const agentState = agent.getState();
@@ -1591,7 +1936,7 @@ export function TUIApp({ agent, sessionStore, mcpClient, compactor, lspManager,
1591
1936
  args: pendingApproval.args || {},
1592
1937
  risk: pendingApproval.risk,
1593
1938
  context: pendingApproval.context,
1594
- }, onApprove: handleApprove, onDeny: handleDeny, colors: colors, denyReasonMode: denyReasonMode, denyReason: denyReason })), _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, 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) => {
1595
1940
  setCommandPaletteOpen(false);
1596
1941
  setCommandPaletteQuery('');
1597
1942
  setCommandPaletteIndex(0);