aegiscode 3.1.8 → 3.1.10

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 (206) hide show
  1. package/README.md +23 -15
  2. package/dist/main.js +549 -552
  3. package/package.json +4 -2
  4. package/src/agent/Agent.ts +903 -0
  5. package/src/agent/SimpleAgent.ts +48 -0
  6. package/src/agent/index.ts +54 -0
  7. package/src/agent/orchestrator/AppBuilder.ts +443 -0
  8. package/src/agent/orchestrator/CouncilAgent.ts +310 -0
  9. package/src/agent/orchestrator/DiscussionRoom.ts +462 -0
  10. package/src/agent/orchestrator/OrchestratorAgent.ts +637 -0
  11. package/src/agent/orchestrator/index.ts +38 -0
  12. package/src/agent/orchestrator/utils.ts +397 -0
  13. package/src/agent/pricing.ts +115 -0
  14. package/src/agent/router.ts +74 -0
  15. package/src/agent/routerStats.ts +121 -0
  16. package/src/agent/types.ts +318 -0
  17. package/src/auth/login.ts +383 -0
  18. package/src/cli/config.ts +189 -0
  19. package/src/cli/index.ts +17 -0
  20. package/src/cli/middleware.ts +119 -0
  21. package/src/cli/types.ts +75 -0
  22. package/src/config/ConfigManager.ts +587 -0
  23. package/src/config/index.ts +7 -0
  24. package/src/config/types.ts +584 -0
  25. package/src/context/CompactionService.ts +300 -0
  26. package/src/context/ContextManager.ts +450 -0
  27. package/src/context/FileAnalyzer.ts +267 -0
  28. package/src/context/TokenCounter.ts +265 -0
  29. package/src/context/index.ts +27 -0
  30. package/src/context/storage/CacheStore.ts +176 -0
  31. package/src/context/storage/JSONLStore.ts +201 -0
  32. package/src/context/storage/MemoryStore.ts +205 -0
  33. package/src/context/storage/PersistentStore.ts +327 -0
  34. package/src/context/storage/index.ts +9 -0
  35. package/src/context/storage/pathUtils.ts +114 -0
  36. package/src/context/test.ts +309 -0
  37. package/src/context/types.ts +268 -0
  38. package/src/hooks/HookExecutor.ts +434 -0
  39. package/src/hooks/HookManager.ts +596 -0
  40. package/src/hooks/HookService.ts +269 -0
  41. package/src/hooks/Matcher.ts +157 -0
  42. package/src/hooks/index.ts +63 -0
  43. package/src/hooks/types.ts +424 -0
  44. package/src/main.tsx +596 -0
  45. package/src/mcp/HealthMonitor.ts +150 -0
  46. package/src/mcp/McpClient.ts +491 -0
  47. package/src/mcp/McpRegistry.ts +321 -0
  48. package/src/mcp/createMcpTool.ts +251 -0
  49. package/src/mcp/index.ts +15 -0
  50. package/src/mcp/server.ts +334 -0
  51. package/src/mcp/test-server.ts +88 -0
  52. package/src/mcp/test.ts +372 -0
  53. package/src/mcp/types.ts +247 -0
  54. package/src/memory/AgentMemoryBus.ts +432 -0
  55. package/src/memory/CloudSync.ts +99 -0
  56. package/src/memory/DriveSync.ts +106 -0
  57. package/src/memory/SharedMemory.ts +951 -0
  58. package/src/memory/index.ts +14 -0
  59. package/src/memory/machineFingerprint.ts +40 -0
  60. package/src/orchestrator/SubAgentMetadata.ts +136 -0
  61. package/src/prompts/builder.ts +213 -0
  62. package/src/prompts/default.ts +144 -0
  63. package/src/prompts/index.ts +16 -0
  64. package/src/prompts/plan.ts +64 -0
  65. package/src/prompts/test.ts +78 -0
  66. package/src/services/AnthropicChatService.ts +341 -0
  67. package/src/services/ChatService.ts +347 -0
  68. package/src/services/ClaudeCliChatService.ts +256 -0
  69. package/src/services/CloudSync.ts +168 -0
  70. package/src/services/CostLedger.ts +211 -0
  71. package/src/services/Heartbeat.ts +135 -0
  72. package/src/services/LearningCollector.ts +291 -0
  73. package/src/services/OllamaInstaller.ts +342 -0
  74. package/src/services/VersionChecker.ts +445 -0
  75. package/src/services/index.ts +57 -0
  76. package/src/services/streaming/OpenAIEventAdapter.ts +126 -0
  77. package/src/services/streaming/RenderingProfile.ts +90 -0
  78. package/src/services/streaming/StreamEventParser.ts +181 -0
  79. package/src/services/streaming/ThrottledRenderer.ts +139 -0
  80. package/src/services/streaming/TranscriptBuffer.ts +574 -0
  81. package/src/services/streaming/eventStatusMap.ts +52 -0
  82. package/src/services/streaming/index.ts +46 -0
  83. package/src/services/streaming/renderFormatting.ts +79 -0
  84. package/src/services/streaming/types.ts +234 -0
  85. package/src/skills/SkillLoader.ts +126 -0
  86. package/src/skills/SkillRegistry.ts +366 -0
  87. package/src/skills/index.ts +48 -0
  88. package/src/skills/types.ts +146 -0
  89. package/src/slash-commands/billing.ts +70 -0
  90. package/src/slash-commands/build.ts +413 -0
  91. package/src/slash-commands/builtinCommands.ts +2733 -0
  92. package/src/slash-commands/clone.ts +242 -0
  93. package/src/slash-commands/council.ts +125 -0
  94. package/src/slash-commands/custom/CustomCommandExecutor.ts +143 -0
  95. package/src/slash-commands/custom/CustomCommandLoader.ts +251 -0
  96. package/src/slash-commands/custom/CustomCommandRegistry.ts +144 -0
  97. package/src/slash-commands/custom/index.ts +7 -0
  98. package/src/slash-commands/debate.ts +254 -0
  99. package/src/slash-commands/gmail.ts +105 -0
  100. package/src/slash-commands/index.ts +388 -0
  101. package/src/slash-commands/mcpCommand.ts +205 -0
  102. package/src/slash-commands/types.ts +201 -0
  103. package/src/store/index.ts +76 -0
  104. package/src/store/selectors.ts +246 -0
  105. package/src/store/slices/appSlice.ts +205 -0
  106. package/src/store/slices/commandSlice.ts +115 -0
  107. package/src/store/slices/configSlice.ts +46 -0
  108. package/src/store/slices/focusSlice.ts +64 -0
  109. package/src/store/slices/index.ts +9 -0
  110. package/src/store/slices/sessionSlice.ts +424 -0
  111. package/src/store/streaming-buffer.ts +425 -0
  112. package/src/store/test.ts +296 -0
  113. package/src/store/types.ts +274 -0
  114. package/src/store/vanilla.ts +186 -0
  115. package/src/tools/builtin/bash.ts +236 -0
  116. package/src/tools/builtin/council.ts +105 -0
  117. package/src/tools/builtin/edit.ts +213 -0
  118. package/src/tools/builtin/glob.ts +136 -0
  119. package/src/tools/builtin/grep.ts +263 -0
  120. package/src/tools/builtin/index.ts +61 -0
  121. package/src/tools/builtin/memory.ts +66 -0
  122. package/src/tools/builtin/read.ts +168 -0
  123. package/src/tools/builtin/skill.ts +97 -0
  124. package/src/tools/builtin/snapshot.ts +40 -0
  125. package/src/tools/builtin/task.ts +106 -0
  126. package/src/tools/builtin/write.ts +134 -0
  127. package/src/tools/createTool.ts +221 -0
  128. package/src/tools/execution/ExecutionPipeline.ts +263 -0
  129. package/src/tools/execution/index.ts +40 -0
  130. package/src/tools/execution/stages/CacheStage.ts +131 -0
  131. package/src/tools/execution/stages/ConfirmationStage.ts +203 -0
  132. package/src/tools/execution/stages/DiscoveryStage.ts +46 -0
  133. package/src/tools/execution/stages/ExecutionStage.ts +48 -0
  134. package/src/tools/execution/stages/FormattingStage.ts +44 -0
  135. package/src/tools/execution/stages/HookStage.ts +72 -0
  136. package/src/tools/execution/stages/PermissionStage.ts +287 -0
  137. package/src/tools/execution/stages/PostHookStage.ts +71 -0
  138. package/src/tools/execution/stages/index.ts +12 -0
  139. package/src/tools/execution/test.ts +266 -0
  140. package/src/tools/execution/types.ts +273 -0
  141. package/src/tools/index.ts +81 -0
  142. package/src/tools/registry.ts +304 -0
  143. package/src/tools/schemas.ts +109 -0
  144. package/src/tools/test.ts +220 -0
  145. package/src/tools/types.ts +175 -0
  146. package/src/tools/validation/PermissionChecker.ts +242 -0
  147. package/src/tools/validation/SensitiveFileDetector.ts +210 -0
  148. package/src/tools/validation/index.ts +11 -0
  149. package/src/ui/App.tsx +166 -0
  150. package/src/ui/components/AegisInterface.tsx +484 -0
  151. package/src/ui/components/common/ChatSearch.tsx +150 -0
  152. package/src/ui/components/common/ErrorBoundary.tsx +82 -0
  153. package/src/ui/components/common/ExitMessage.tsx +120 -0
  154. package/src/ui/components/common/LoadingIndicator.tsx +49 -0
  155. package/src/ui/components/common/index.ts +6 -0
  156. package/src/ui/components/dialog/ConfirmationPrompt.tsx +208 -0
  157. package/src/ui/components/dialog/InteractiveSelector.tsx +149 -0
  158. package/src/ui/components/dialog/SetupWizard.tsx +297 -0
  159. package/src/ui/components/dialog/UpdatePrompt.tsx +155 -0
  160. package/src/ui/components/dialog/index.ts +8 -0
  161. package/src/ui/components/index.ts +28 -0
  162. package/src/ui/components/input/CommandSuggestions.tsx +139 -0
  163. package/src/ui/components/input/CustomTextInput.tsx +220 -0
  164. package/src/ui/components/input/InputArea.tsx +361 -0
  165. package/src/ui/components/input/PromptSuggestions.tsx +66 -0
  166. package/src/ui/components/input/index.ts +6 -0
  167. package/src/ui/components/layout/ChatStatusBar.tsx +90 -0
  168. package/src/ui/components/layout/ContextBar.tsx +79 -0
  169. package/src/ui/components/layout/MessageArea.tsx +96 -0
  170. package/src/ui/components/layout/MessageList.tsx +647 -0
  171. package/src/ui/components/layout/MessageSeparator.tsx +26 -0
  172. package/src/ui/components/layout/WelcomeMessage.tsx +93 -0
  173. package/src/ui/components/layout/index.ts +7 -0
  174. package/src/ui/components/markdown/CodeHighlighter.tsx +292 -0
  175. package/src/ui/components/markdown/MessageRenderer.tsx +1211 -0
  176. package/src/ui/components/markdown/index.ts +8 -0
  177. package/src/ui/components/markdown/parser.ts +336 -0
  178. package/src/ui/components/markdown/types.ts +66 -0
  179. package/src/ui/focus/FocusManager.ts +137 -0
  180. package/src/ui/focus/index.ts +13 -0
  181. package/src/ui/focus/types.ts +54 -0
  182. package/src/ui/focus/useFocus.ts +75 -0
  183. package/src/ui/hooks/index.ts +11 -0
  184. package/src/ui/hooks/useAgent.ts +284 -0
  185. package/src/ui/hooks/useCommandHistory.ts +87 -0
  186. package/src/ui/hooks/useCommandProcessor.ts +443 -0
  187. package/src/ui/hooks/useConfirmation.ts +99 -0
  188. package/src/ui/hooks/useCtrlCHandler.ts +100 -0
  189. package/src/ui/hooks/useInputBuffer.ts +122 -0
  190. package/src/ui/hooks/useTerminalSize.ts +68 -0
  191. package/src/ui/hooks/useTerminalWidth.ts +5 -0
  192. package/src/ui/hooks/useWindowedList.ts +118 -0
  193. package/src/ui/render-debugger.ts +621 -0
  194. package/src/ui/test.ts +189 -0
  195. package/src/ui/themes/ThemeManager.ts +332 -0
  196. package/src/ui/themes/aegisTheme.ts +87 -0
  197. package/src/ui/themes/darkTheme.ts +87 -0
  198. package/src/ui/themes/defaultTheme.ts +85 -0
  199. package/src/ui/themes/index.ts +10 -0
  200. package/src/ui/themes/lightTheme.ts +89 -0
  201. package/src/ui/themes/popularThemes.ts +187 -0
  202. package/src/ui/themes/types.ts +130 -0
  203. package/src/utils/clipboard.ts +48 -0
  204. package/src/utils/debug.ts +43 -0
  205. package/src/utils/environment.ts +68 -0
  206. package/src/utils/index.ts +10 -0
@@ -0,0 +1,443 @@
1
+ /**
2
+ * useCommandProcessor - Command processing hook
3
+ *
4
+ * Extracts the core command processing logic (slash commands, agent chat,
5
+ * streaming, tool calls, auto-compaction) from AegisInterface.
6
+ */
7
+
8
+ import { useCallback, useRef } from 'react';
9
+ import type { Agent } from '../../agent/Agent.js';
10
+ import type { ContextManager } from '../../context/index.js';
11
+ import { TokenCounter } from '../../context/index.js';
12
+ import type { Message, ToolCall, ToolResult } from '../../agent/types.js';
13
+ import type { SlashCommandResult } from '../../slash-commands/types.js';
14
+ import {
15
+ sessionActions,
16
+ commandActions,
17
+ appActions,
18
+ getState,
19
+ } from '../../store/index.js';
20
+ import { classifyComplexity, resolveModelForTier } from '../../agent/router.js';
21
+ import type { ComplexityTier } from '../../agent/router.js';
22
+ import {
23
+ applyStreamEvent,
24
+ finishToolCallInBuffer,
25
+ getBufferedToolCalls,
26
+ } from '../../store/streaming-buffer.js';
27
+ import type { ConfirmationHandler } from './useConfirmation.js';
28
+
29
+ export interface UseCommandProcessorOptions {
30
+ agentRef: React.MutableRefObject<Agent | null>;
31
+ contextManagerRef: React.MutableRefObject<ContextManager | null>;
32
+ modelRef: React.MutableRefObject<string | undefined>;
33
+ debugRef: React.MutableRefObject<boolean | undefined>;
34
+ getMessagesRef: React.MutableRefObject<() => any[]>;
35
+ confirmationHandlerRef: React.MutableRefObject<ConfirmationHandler>;
36
+ onSelectorRequest?: (state: {
37
+ title: string;
38
+ options: Array<{ value: string; label: string }>;
39
+ handler: 'theme' | 'model' | null;
40
+ }) => void;
41
+ }
42
+
43
+ export interface UseCommandProcessorResult {
44
+ processCommand: (value: string, options?: { silent?: boolean }) => Promise<void>;
45
+ handleSubmit: (value: string) => Promise<void>;
46
+ processQueue: () => Promise<void>;
47
+ }
48
+
49
+ export function useCommandProcessor(options: UseCommandProcessorOptions): UseCommandProcessorResult {
50
+ const {
51
+ agentRef,
52
+ contextManagerRef,
53
+ modelRef,
54
+ debugRef,
55
+ getMessagesRef,
56
+ confirmationHandlerRef,
57
+ onSelectorRequest,
58
+ } = options;
59
+
60
+ // Auto-router: lazily-created Agent instances per model id, keyed by config
61
+ // model id, reused across turns so picking the same tier twice in a row
62
+ // doesn't pay an Agent.create() again. Each entry also stores the settings
63
+ // snapshot it was built with — requireConfirmation/allowedTools/
64
+ // disallowedTools are frozen into the Agent at creation time, so if the
65
+ // user toggles e.g. /confirm for that model after it's cached, the stale
66
+ // Agent would keep prompting forever. Comparing the snapshot before reuse
67
+ // (mirrors the same fix already applied to the main agentRef in
68
+ // useAgent.ts's model-switch subscription) catches that drift.
69
+ const routerAgentCacheRef = useRef<Map<string, { agent: Agent; settingsSnapshot: string }>>(new Map());
70
+ // Which model id the auto-router last swapped agentRef.current to (or
71
+ // undefined if it's still whatever /model or initial setup left it as).
72
+ const routerActiveModelIdRef = useRef<string | undefined>(undefined);
73
+
74
+ const processCommand = useCallback(async (value: string, options?: { silent?: boolean }) => {
75
+ const { isSlashCommand, executeSlashCommand } = await import('../../slash-commands/index.js');
76
+ const { vanillaStore } = await import('../../store/vanilla.js');
77
+ const { startBatch, batchAddUserMessage, batchAddAssistantMessage, batchSetThinking, flushBatchWithStore, cancelBatch } = await import('../../store/streaming-buffer.js');
78
+ const streamingBuf = await import('../../store/streaming-buffer.js');
79
+
80
+ if (isSlashCommand(value)) {
81
+ // Phase 1: flush user message + thinking=true immediately
82
+ sessionActions().setCurrentCommand(value);
83
+ startBatch();
84
+ batchAddUserMessage(value);
85
+ batchSetThinking(true);
86
+ flushBatchWithStore(vanillaStore);
87
+
88
+ let streamingMsgId: string | null = null;
89
+ let streamingResult: SlashCommandResult | null = null;
90
+ try {
91
+ streamingMsgId = sessionActions().startStreamingMessage();
92
+ } catch { /* streaming not available */ }
93
+
94
+ const onContentDelta = streamingMsgId
95
+ ? (delta: string) => { streamingBuf.appendToBuffer(delta); }
96
+ : undefined;
97
+
98
+ try {
99
+ streamingResult = await executeSlashCommand(value, {
100
+ cwd: process.cwd(),
101
+ sessionId: getState().session.sessionId,
102
+ messages: getMessagesRef.current(),
103
+ contextManager: contextManagerRef.current,
104
+ chatService: agentRef.current?.getChatService(),
105
+ modelName: modelRef.current,
106
+ onContentDelta,
107
+ onThinkingDelta: streamingMsgId
108
+ ? (delta: string) => { streamingBuf.appendThinkingToBuffer(delta); }
109
+ : undefined,
110
+ confirmationHandler: confirmationHandlerRef.current,
111
+ });
112
+
113
+ if (streamingResult.type === 'selector' && streamingResult.selector) {
114
+ sessionActions().setCurrentCommand(null);
115
+ sessionActions().setThinking(false);
116
+ if (streamingMsgId) sessionActions().finishStreamingMessage(streamingMsgId);
117
+ // Rollback: remove user command message + empty streaming message
118
+ // so WelcomeScreen stays visible (messages.length === 0)
119
+ sessionActions().removeLastMessages(2);
120
+ onSelectorRequest?.(streamingResult.selector);
121
+ return;
122
+ }
123
+
124
+ if (streamingResult.sendToAgent && streamingResult.content) {
125
+ // Don't clear currentCommand — it continues via processCommand below
126
+ sessionActions().setThinking(false);
127
+ if (streamingMsgId) sessionActions().finishStreamingMessage(streamingMsgId);
128
+ await processCommand(streamingResult.content, { silent: true });
129
+ return;
130
+ }
131
+
132
+ if (streamingResult.type === 'silent') {
133
+ if (streamingMsgId) sessionActions().finishStreamingMessage(streamingMsgId);
134
+ sessionActions().setCurrentCommand(null);
135
+ sessionActions().setThinking(false);
136
+ return;
137
+ }
138
+
139
+ startBatch();
140
+ if (streamingResult.content) {
141
+ batchAddAssistantMessage(streamingResult.content);
142
+ } else if (streamingResult.message) {
143
+ batchAddAssistantMessage(streamingResult.message);
144
+ } else if (streamingResult.error) {
145
+ batchAddAssistantMessage('error: ' + streamingResult.error);
146
+ }
147
+ } catch (error) {
148
+ startBatch();
149
+ batchAddAssistantMessage(
150
+ 'error: ' + (error instanceof Error ? error.message : String(error))
151
+ );
152
+ } finally {
153
+ if (streamingMsgId && streamingResult?.type !== 'silent') {
154
+ try { sessionActions().finishStreamingMessage(streamingMsgId); } catch {}
155
+ }
156
+ sessionActions().setCurrentCommand(null);
157
+ batchSetThinking(false);
158
+ flushBatchWithStore(vanillaStore);
159
+ }
160
+ return;
161
+ }
162
+
163
+ if (!agentRef.current || !contextManagerRef.current) return;
164
+
165
+ const ctxManager = contextManagerRef.current;
166
+
167
+ // ==================== Auto-router ====================
168
+ // Pick a model for this turn based on task complexity, unless the user
169
+ // has manually chosen one with /model this session. Never throws —
170
+ // a classification or Agent.create failure just leaves the current
171
+ // agent/model in place.
172
+ // Captured here, recorded after the chat call resolves/aborts below —
173
+ // this is the learning loop's only outcome signal (see routerStats.ts).
174
+ let routedTier: ComplexityTier | undefined;
175
+ let routedModelId: string | undefined;
176
+ try {
177
+ const routerState = getState();
178
+ const autoRouter = routerState.config.config?.autoRouter;
179
+ const defaultModelId = routerState.config.config?.currentModelId;
180
+ const activeModelId = routerActiveModelIdRef.current ?? defaultModelId;
181
+
182
+ if (autoRouter?.enabled && !routerState.app.manualModelOverride) {
183
+ const models = routerState.config.config?.models || [];
184
+ const tier = classifyComplexity(value);
185
+ const targetModel = resolveModelForTier(tier, models, autoRouter.tiers);
186
+ const targetId = targetModel?.id;
187
+ const targetLabel = targetModel ? (targetModel.model || targetId) : undefined;
188
+
189
+ if (targetModel && targetId && targetId !== activeModelId) {
190
+ const settingsSnapshot = JSON.stringify({
191
+ requireConfirmation: targetModel.requireConfirmation,
192
+ allowedTools: targetModel.allowedTools,
193
+ disallowedTools: targetModel.disallowedTools,
194
+ });
195
+ const cached = routerAgentCacheRef.current.get(targetId);
196
+ let agent = cached && cached.settingsSnapshot === settingsSnapshot ? cached.agent : undefined;
197
+ if (!agent) {
198
+ const { Agent } = await import('../../agent/Agent.js');
199
+ agent = await Agent.create({
200
+ apiKey: targetModel.apiKey!,
201
+ baseURL: targetModel.baseURL,
202
+ model: targetLabel!,
203
+ requireConfirmation: targetModel.requireConfirmation,
204
+ });
205
+ routerAgentCacheRef.current.set(targetId, { agent, settingsSnapshot });
206
+ }
207
+ agentRef.current = agent;
208
+ modelRef.current = targetLabel;
209
+ routerActiveModelIdRef.current = targetId;
210
+ }
211
+
212
+ if (targetId) {
213
+ routedTier = tier;
214
+ routedModelId = targetId;
215
+ }
216
+
217
+ appActions().setAutoRouterActiveModel(
218
+ targetId && targetId !== defaultModelId ? (targetLabel ?? null) : null
219
+ );
220
+ }
221
+ } catch { /* non-fatal — keep using whatever agent is already active */ }
222
+
223
+ // Capture the agent for this turn by value now that routing is decided.
224
+ // Several awaits follow before chat() actually dispatches; reading
225
+ // `agentRef.current` fresh at each of those points would let a
226
+ // concurrent /model switch (its Agent.create() resolves async, on its
227
+ // own timeline) retarget a request that's already committed to this turn.
228
+ const dispatchAgent = agentRef.current;
229
+
230
+ if (!options?.silent) {
231
+ sessionActions().addUserMessage(value);
232
+ }
233
+
234
+ sessionActions().setCurrentCommand(value);
235
+ sessionActions().setThinking(true);
236
+
237
+ const { onUserPromptSubmit } = await import('../../hooks/index.js');
238
+ const injectedContext = await onUserPromptSubmit(value, getState().session.sessionId, process.cwd());
239
+
240
+ if (injectedContext) {
241
+ if (debugRef.current) {
242
+ console.log('[DEBUG] Hook injected context:', injectedContext);
243
+ }
244
+ sessionActions().addAssistantMessage('[Hook] ' + injectedContext);
245
+ }
246
+
247
+ await ctxManager.addMessage('user', value);
248
+
249
+ // Auto-compact when context approaches 80% of the token limit
250
+ {
251
+ const currentTokens = ctxManager.getTokenCount();
252
+ const runtimeConfig = getState().config.config;
253
+ const maxCtx = runtimeConfig?.maxContextTokens ?? 200000;
254
+ if (currentTokens > 0 && currentTokens >= maxCtx * 0.8) {
255
+ try {
256
+ sessionActions().setCompacting(true);
257
+ sessionActions().addAssistantMessage('⟳ Context near limit - auto-compacting...');
258
+ const { CompactionService } = await import('../../context/CompactionService.js');
259
+ const ctxMsgs = ctxManager.getMessages();
260
+ const msgs = ctxMsgs.map((m: { role: string; content: string }) => ({
261
+ role: m.role as Message['role'],
262
+ content: m.content,
263
+ }));
264
+ const result = await CompactionService.compact(msgs, {
265
+ modelName: modelRef.current || 'claude-sonnet-4-6',
266
+ maxContextTokens: maxCtx,
267
+ chatService: dispatchAgent?.getChatService(),
268
+ trigger: 'auto',
269
+ actualPreTokens: currentTokens,
270
+ });
271
+ if (result.success) {
272
+ const { nanoid } = await import('nanoid');
273
+ ctxManager.replaceMessages(result.compactedMessages.map((m: Message) => ({
274
+ id: nanoid(),
275
+ role: m.role as 'user' | 'assistant' | 'system' | 'tool',
276
+ content: m.content,
277
+ timestamp: Date.now(),
278
+ })));
279
+ ctxManager.updateTokenCount(result.postTokens);
280
+ const saved = result.preTokens - result.postTokens;
281
+ sessionActions().addAssistantMessage(
282
+ `✓ Auto-compact: ${result.preTokens.toLocaleString()} → ${result.postTokens.toLocaleString()} tokens (−${saved.toLocaleString()})`
283
+ );
284
+ }
285
+ } catch { /* non-fatal */ }
286
+ finally { sessionActions().setCompacting(false); }
287
+ }
288
+ }
289
+
290
+ // Dual-path streaming: mutable buffer + store
291
+ const streamingMessageId = sessionActions().startStreamingMessage();
292
+ // Wire to store so Escape/Ctrl+C can actually abort the agent
293
+ const abortController = commandActions().createAbortController();
294
+
295
+ try {
296
+ const contextMessages = ctxManager.getMessages();
297
+ const modelName = modelRef.current || 'claude-sonnet-4-6';
298
+
299
+ const inputTokens = TokenCounter.countTokens(
300
+ contextMessages.map(m => ({ role: m.role as Message['role'], content: m.content })),
301
+ modelName
302
+ );
303
+
304
+ const { nanoid } = await import('nanoid');
305
+
306
+ const chatContext = {
307
+ sessionId: ctxManager.getCurrentSessionId() || getState().session.sessionId,
308
+ messages: contextMessages.map(m => ({
309
+ role: m.role as Message['role'],
310
+ content: m.content,
311
+ })),
312
+ confirmationHandler: confirmationHandlerRef.current,
313
+ permissionMode: getState().config.config?.defaultPermissionMode || 'default',
314
+ };
315
+
316
+ const result = await dispatchAgent!.chat(value, chatContext, {
317
+ signal: abortController.signal,
318
+ onStreamEvent: (event) => {
319
+ if (!abortController.signal.aborted) {
320
+ applyStreamEvent(event);
321
+ }
322
+ },
323
+ onToolCallStart: (toolCall) => {
324
+ if (abortController.signal.aborted) return;
325
+ sessionActions().flushStreamBuffer(streamingMessageId);
326
+ const name = toolCall.function?.name || 'tool';
327
+ const toolId = toolCall.id || `${name}-${Date.now()}`;
328
+ sessionActions().addContentBlock(streamingMessageId, {
329
+ type: 'tool_use',
330
+ id: toolId,
331
+ name,
332
+ input: '',
333
+ status: 'running',
334
+ startedAt: Date.now(),
335
+ });
336
+ },
337
+ onToolResult: (_toolCall: ToolCall, toolResult: ToolResult) => {
338
+ if (abortController.signal.aborted) return;
339
+ const toolId = _toolCall.id;
340
+ const isError = !toolResult.success;
341
+ finishToolCallInBuffer(toolId, isError);
342
+ const bufferedCalls = getBufferedToolCalls();
343
+ const bufferedCall = bufferedCalls.find(tc => tc.id === toolId);
344
+ if (bufferedCall?.arguments) {
345
+ sessionActions().setToolCallInput(streamingMessageId, toolId, bufferedCall.arguments);
346
+ }
347
+ sessionActions().updateToolCallStatus(streamingMessageId, toolId, isError ? 'error' : 'success', Date.now());
348
+ const DISPLAY_CONTENT_CAP = 3000; // diffs need more room than plain text results
349
+ const resultContent = toolResult.error
350
+ ? (toolResult.error.length > 200 ? toolResult.error.slice(0, 200) + '...' : toolResult.error)
351
+ : (toolResult.displayContent
352
+ ? (toolResult.displayContent.length > DISPLAY_CONTENT_CAP ? toolResult.displayContent.slice(0, DISPLAY_CONTENT_CAP) + '...' : toolResult.displayContent)
353
+ : '');
354
+ sessionActions().addToolResultBlock(streamingMessageId, toolId, resultContent, isError);
355
+ },
356
+ });
357
+
358
+ sessionActions().finishStreamingMessage(streamingMessageId);
359
+ await ctxManager.addMessage('assistant', result);
360
+
361
+ const outputTokens = TokenCounter.countTextTokens(result, modelName);
362
+ const totalTokens = inputTokens + outputTokens;
363
+ ctxManager.updateTokenCount(totalTokens);
364
+
365
+ const currentTokenUsage = getState().session.tokenUsage;
366
+ const prevModel = currentTokenUsage.modelBreakdown[modelName] ?? { inputTokens: 0, outputTokens: 0 };
367
+ sessionActions().updateTokenUsage({
368
+ inputTokens: currentTokenUsage.inputTokens + inputTokens,
369
+ outputTokens: currentTokenUsage.outputTokens + outputTokens,
370
+ modelBreakdown: {
371
+ ...currentTokenUsage.modelBreakdown,
372
+ [modelName]: {
373
+ inputTokens: prevModel.inputTokens + inputTokens,
374
+ outputTokens: prevModel.outputTokens + outputTokens,
375
+ },
376
+ },
377
+ });
378
+
379
+ if (debugRef.current) {
380
+ console.log('[DEBUG] Token usage - input:', inputTokens, 'output:', outputTokens);
381
+ console.log('[DEBUG] Total context tokens:', ctxManager.getTokenCount());
382
+ }
383
+
384
+ if (routedTier && routedModelId) {
385
+ const { recordRouterOutcome } = await import('../../agent/routerStats.js');
386
+ recordRouterOutcome(routedTier, routedModelId, true);
387
+ }
388
+ } catch (error) {
389
+ if (routedTier && routedModelId) {
390
+ const { recordRouterOutcome } = await import('../../agent/routerStats.js');
391
+ recordRouterOutcome(routedTier, routedModelId, false);
392
+ }
393
+ if ((error as Error)?.name !== 'AbortError') {
394
+ const errorContent = 'Error: ' + (error as Error).message;
395
+ sessionActions().forceAppendToMessage(streamingMessageId, errorContent);
396
+ sessionActions().finishStreamingMessage(streamingMessageId);
397
+ await ctxManager.addMessage('assistant', errorContent);
398
+ } else {
399
+ sessionActions().finishStreamingMessage(streamingMessageId);
400
+ }
401
+ } finally {
402
+ sessionActions().setCurrentCommand(null);
403
+ sessionActions().setThinking(false);
404
+ }
405
+ }, [onSelectorRequest]);
406
+
407
+ const processQueue = useCallback(async () => {
408
+ const nextCommand = commandActions().dequeueCommand();
409
+ if (nextCommand) {
410
+ if (debugRef.current) {
411
+ console.log('[DEBUG] Processing queued command:', nextCommand);
412
+ }
413
+ await processCommand(nextCommand);
414
+ }
415
+ }, [processCommand]);
416
+
417
+ const handleSubmit = useCallback(async (value: string) => {
418
+ if (!value.trim()) return;
419
+
420
+ const currentState = getState();
421
+ const currentIsThinking = currentState.session.isThinking;
422
+ const currentPendingCount = currentState.command.pendingCommands.length;
423
+
424
+ if (currentIsThinking) {
425
+ commandActions().enqueueCommand(value);
426
+ if (debugRef.current) {
427
+ console.log('[DEBUG] Command queued:', value, 'Queue size:', currentPendingCount + 1);
428
+ }
429
+ return;
430
+ }
431
+
432
+ // Acquire the lock synchronously, before processCommand's first await
433
+ // (dynamic imports, auto-router's Agent.create, etc.). Without this,
434
+ // isThinking stays false across that gap and a second Enter press in
435
+ // the same window slips past the check above instead of queueing,
436
+ // producing two concurrent in-flight commands.
437
+ sessionActions().setThinking(true);
438
+
439
+ await processCommand(value);
440
+ }, [processCommand]);
441
+
442
+ return { processCommand, handleSubmit, processQueue };
443
+ }
@@ -0,0 +1,99 @@
1
+ /**
2
+ * useConfirmation - 确认对话框状态管理
3
+ *
4
+ *
5
+ *
6
+ */
7
+
8
+ import { useState, useCallback, useMemo } from 'react';
9
+ import { focusActions, FocusId } from '../focus/index.js';
10
+ import type { ConfirmationHandler, ConfirmationDetails, ConfirmationResponse } from '../../tools/execution/types.js';
11
+
12
+ // Re-export for useCommandProcessor
13
+ export type { ConfirmationHandler, ConfirmationDetails, ConfirmationResponse };
14
+
15
+ /**
16
+ *
17
+ */
18
+ interface ConfirmationState {
19
+ isVisible: boolean;
20
+ details: ConfirmationDetails | null;
21
+ resolver: ((response: ConfirmationResponse) => void) | null;
22
+ }
23
+
24
+ interface UseConfirmationResult {
25
+ /** 确认状态 */
26
+ confirmationState: ConfirmationState;
27
+ /** 确认处理器(供 Agent/Pipeline 使用) */
28
+ confirmationHandler: ConfirmationHandler;
29
+ /** 处理用户响应 */
30
+ handleResponse: (response: ConfirmationResponse) => void;
31
+ /** 显示确认对话框 */
32
+ showConfirmation: (details: ConfirmationDetails) => Promise<ConfirmationResponse>;
33
+ }
34
+
35
+ /**
36
+ *
37
+ *
38
+ *
39
+ */
40
+ export const useConfirmation = (): UseConfirmationResult => {
41
+ const [confirmationState, setConfirmationState] = useState<ConfirmationState>({
42
+ isVisible: false,
43
+ details: null,
44
+ resolver: null,
45
+ });
46
+
47
+ /**
48
+ *
49
+ *
50
+ */
51
+ const showConfirmation = useCallback(
52
+ (details: ConfirmationDetails): Promise<ConfirmationResponse> => {
53
+ return new Promise((resolve) => {
54
+ // 同步设置焦点 — 在 React 调度 render 之前就生
55
+ focusActions.setFocus(FocusId.CONFIRMATION_PROMPT);
56
+ setConfirmationState({
57
+ isVisible: true,
58
+ details,
59
+ resolver: resolve,
60
+ });
61
+ });
62
+ },
63
+ []
64
+ );
65
+
66
+ /**
67
+ *
68
+ *
69
+ */
70
+ const handleResponse = useCallback((response: ConfirmationResponse) => {
71
+ if (confirmationState.resolver) {
72
+ confirmationState.resolver(response);
73
+ }
74
+ // 同步恢复焦
75
+ focusActions.setFocus(FocusId.MAIN_INPUT);
76
+ setConfirmationState({
77
+ isVisible: false,
78
+ details: null,
79
+ resolver: null,
80
+ });
81
+ }, [confirmationState.resolver]);
82
+
83
+ /**
84
+ *
85
+ */
86
+ const confirmationHandler: ConfirmationHandler = useMemo(
87
+ () => ({
88
+ requestConfirmation: showConfirmation,
89
+ }),
90
+ [showConfirmation]
91
+ );
92
+
93
+ return {
94
+ confirmationState,
95
+ confirmationHandler,
96
+ handleResponse,
97
+ showConfirmation,
98
+ };
99
+ };
@@ -0,0 +1,100 @@
1
+ /**
2
+ * useCtrlCHandler - Ctrl+Z / Ctrl+C 处理
3
+ *
4
+ * Ctrl+Z 主退出键 (Kitty 中 Ctrl+C 用于复制)
5
+ * Ctrl+C 备用退出键
6
+ *
7
+ * - 有任务运行时:请求中断
8
+ * - 无任务时:退出应用
9
+ */
10
+
11
+ import { useCallback, useRef } from 'react';
12
+ import { useApp, useInput } from 'ink';
13
+ import { getState } from '../../store/index.js';
14
+
15
+ interface CtrlCHandlerOptions {
16
+ /** 中断回调 */
17
+ onInterrupt?: () => void;
18
+ /**
19
+ *
20
+ *
21
+ */
22
+ onBeforeExit?: () => boolean | void;
23
+ /** 强制退出前的确认时间(毫秒) */
24
+ forceExitDelay?: number;
25
+ }
26
+
27
+ interface CtrlCHandlerResult {
28
+ /** 处理退出信号 (Ctrl+Z / Ctrl+C) */
29
+ handleExit: () => void;
30
+ /** 重置强制退出状态 */
31
+ resetForceExit: () => void;
32
+ }
33
+
34
+ /**
35
+ * 退出处理 Hook
36
+ *
37
+ * Ctrl+Z = 主退出 (Kitty 中 Ctrl+C 用于复制)
38
+ * Ctrl+C = 备用退出
39
+ */
40
+ export const useCtrlCHandler = (options: CtrlCHandlerOptions): CtrlCHandlerResult => {
41
+ const { onInterrupt, onBeforeExit, forceExitDelay = 2000 } = options;
42
+ const { exit } = useApp();
43
+
44
+ const lastExitTime = useRef<number>(0);
45
+ const forceExitPending = useRef(false);
46
+
47
+ const doExit = useCallback(() => {
48
+ if (onBeforeExit) {
49
+ const handled = onBeforeExit();
50
+ if (handled === true) {
51
+ return;
52
+ }
53
+ }
54
+ exit();
55
+ setTimeout(() => process.exit(0), 50);
56
+ }, [onBeforeExit, exit]);
57
+
58
+ const handleExit = useCallback(() => {
59
+ const now = Date.now();
60
+ const timeSinceLastExit = now - lastExitTime.current;
61
+
62
+ const hasRunningTask = getState().session.isThinking;
63
+
64
+ if (hasRunningTask) {
65
+ if (forceExitPending.current && timeSinceLastExit < forceExitDelay) {
66
+ // 第二次退出信号:强制退出
67
+ doExit();
68
+ return;
69
+ }
70
+
71
+ // 第一次退出信号:请求中断
72
+ forceExitPending.current = true;
73
+ lastExitTime.current = now;
74
+
75
+ if (onInterrupt) {
76
+ onInterrupt();
77
+ }
78
+ } else {
79
+ // 没有任务,直接退出
80
+ doExit();
81
+ }
82
+ }, [onInterrupt, forceExitDelay, doExit]);
83
+
84
+ // Ctrl+Z = primary exit key (Ctrl+C reserved for terminal copy in Kitty & others)
85
+ useInput((input, key) => {
86
+ if (input === 'z' && key.ctrl) {
87
+ handleExit();
88
+ }
89
+ });
90
+
91
+ const resetForceExit = useCallback(() => {
92
+ forceExitPending.current = false;
93
+ lastExitTime.current = 0;
94
+ }, []);
95
+
96
+ return {
97
+ handleExit,
98
+ resetForceExit,
99
+ };
100
+ };