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.
- package/README.md +23 -15
- package/dist/main.js +549 -552
- package/package.json +4 -2
- package/src/agent/Agent.ts +903 -0
- package/src/agent/SimpleAgent.ts +48 -0
- package/src/agent/index.ts +54 -0
- package/src/agent/orchestrator/AppBuilder.ts +443 -0
- package/src/agent/orchestrator/CouncilAgent.ts +310 -0
- package/src/agent/orchestrator/DiscussionRoom.ts +462 -0
- package/src/agent/orchestrator/OrchestratorAgent.ts +637 -0
- package/src/agent/orchestrator/index.ts +38 -0
- package/src/agent/orchestrator/utils.ts +397 -0
- package/src/agent/pricing.ts +115 -0
- package/src/agent/router.ts +74 -0
- package/src/agent/routerStats.ts +121 -0
- package/src/agent/types.ts +318 -0
- package/src/auth/login.ts +383 -0
- package/src/cli/config.ts +189 -0
- package/src/cli/index.ts +17 -0
- package/src/cli/middleware.ts +119 -0
- package/src/cli/types.ts +75 -0
- package/src/config/ConfigManager.ts +587 -0
- package/src/config/index.ts +7 -0
- package/src/config/types.ts +584 -0
- package/src/context/CompactionService.ts +300 -0
- package/src/context/ContextManager.ts +450 -0
- package/src/context/FileAnalyzer.ts +267 -0
- package/src/context/TokenCounter.ts +265 -0
- package/src/context/index.ts +27 -0
- package/src/context/storage/CacheStore.ts +176 -0
- package/src/context/storage/JSONLStore.ts +201 -0
- package/src/context/storage/MemoryStore.ts +205 -0
- package/src/context/storage/PersistentStore.ts +327 -0
- package/src/context/storage/index.ts +9 -0
- package/src/context/storage/pathUtils.ts +114 -0
- package/src/context/test.ts +309 -0
- package/src/context/types.ts +268 -0
- package/src/hooks/HookExecutor.ts +434 -0
- package/src/hooks/HookManager.ts +596 -0
- package/src/hooks/HookService.ts +269 -0
- package/src/hooks/Matcher.ts +157 -0
- package/src/hooks/index.ts +63 -0
- package/src/hooks/types.ts +424 -0
- package/src/main.tsx +596 -0
- package/src/mcp/HealthMonitor.ts +150 -0
- package/src/mcp/McpClient.ts +491 -0
- package/src/mcp/McpRegistry.ts +321 -0
- package/src/mcp/createMcpTool.ts +251 -0
- package/src/mcp/index.ts +15 -0
- package/src/mcp/server.ts +334 -0
- package/src/mcp/test-server.ts +88 -0
- package/src/mcp/test.ts +372 -0
- package/src/mcp/types.ts +247 -0
- package/src/memory/AgentMemoryBus.ts +432 -0
- package/src/memory/CloudSync.ts +99 -0
- package/src/memory/DriveSync.ts +106 -0
- package/src/memory/SharedMemory.ts +951 -0
- package/src/memory/index.ts +14 -0
- package/src/memory/machineFingerprint.ts +40 -0
- package/src/orchestrator/SubAgentMetadata.ts +136 -0
- package/src/prompts/builder.ts +213 -0
- package/src/prompts/default.ts +144 -0
- package/src/prompts/index.ts +16 -0
- package/src/prompts/plan.ts +64 -0
- package/src/prompts/test.ts +78 -0
- package/src/services/AnthropicChatService.ts +341 -0
- package/src/services/ChatService.ts +347 -0
- package/src/services/ClaudeCliChatService.ts +256 -0
- package/src/services/CloudSync.ts +168 -0
- package/src/services/CostLedger.ts +211 -0
- package/src/services/Heartbeat.ts +135 -0
- package/src/services/LearningCollector.ts +291 -0
- package/src/services/OllamaInstaller.ts +342 -0
- package/src/services/VersionChecker.ts +445 -0
- package/src/services/index.ts +57 -0
- package/src/services/streaming/OpenAIEventAdapter.ts +126 -0
- package/src/services/streaming/RenderingProfile.ts +90 -0
- package/src/services/streaming/StreamEventParser.ts +181 -0
- package/src/services/streaming/ThrottledRenderer.ts +139 -0
- package/src/services/streaming/TranscriptBuffer.ts +574 -0
- package/src/services/streaming/eventStatusMap.ts +52 -0
- package/src/services/streaming/index.ts +46 -0
- package/src/services/streaming/renderFormatting.ts +79 -0
- package/src/services/streaming/types.ts +234 -0
- package/src/skills/SkillLoader.ts +126 -0
- package/src/skills/SkillRegistry.ts +366 -0
- package/src/skills/index.ts +48 -0
- package/src/skills/types.ts +146 -0
- package/src/slash-commands/billing.ts +70 -0
- package/src/slash-commands/build.ts +413 -0
- package/src/slash-commands/builtinCommands.ts +2733 -0
- package/src/slash-commands/clone.ts +242 -0
- package/src/slash-commands/council.ts +125 -0
- package/src/slash-commands/custom/CustomCommandExecutor.ts +143 -0
- package/src/slash-commands/custom/CustomCommandLoader.ts +251 -0
- package/src/slash-commands/custom/CustomCommandRegistry.ts +144 -0
- package/src/slash-commands/custom/index.ts +7 -0
- package/src/slash-commands/debate.ts +254 -0
- package/src/slash-commands/gmail.ts +105 -0
- package/src/slash-commands/index.ts +388 -0
- package/src/slash-commands/mcpCommand.ts +205 -0
- package/src/slash-commands/types.ts +201 -0
- package/src/store/index.ts +76 -0
- package/src/store/selectors.ts +246 -0
- package/src/store/slices/appSlice.ts +205 -0
- package/src/store/slices/commandSlice.ts +115 -0
- package/src/store/slices/configSlice.ts +46 -0
- package/src/store/slices/focusSlice.ts +64 -0
- package/src/store/slices/index.ts +9 -0
- package/src/store/slices/sessionSlice.ts +424 -0
- package/src/store/streaming-buffer.ts +425 -0
- package/src/store/test.ts +296 -0
- package/src/store/types.ts +274 -0
- package/src/store/vanilla.ts +186 -0
- package/src/tools/builtin/bash.ts +236 -0
- package/src/tools/builtin/council.ts +105 -0
- package/src/tools/builtin/edit.ts +213 -0
- package/src/tools/builtin/glob.ts +136 -0
- package/src/tools/builtin/grep.ts +263 -0
- package/src/tools/builtin/index.ts +61 -0
- package/src/tools/builtin/memory.ts +66 -0
- package/src/tools/builtin/read.ts +168 -0
- package/src/tools/builtin/skill.ts +97 -0
- package/src/tools/builtin/snapshot.ts +40 -0
- package/src/tools/builtin/task.ts +106 -0
- package/src/tools/builtin/write.ts +134 -0
- package/src/tools/createTool.ts +221 -0
- package/src/tools/execution/ExecutionPipeline.ts +263 -0
- package/src/tools/execution/index.ts +40 -0
- package/src/tools/execution/stages/CacheStage.ts +131 -0
- package/src/tools/execution/stages/ConfirmationStage.ts +203 -0
- package/src/tools/execution/stages/DiscoveryStage.ts +46 -0
- package/src/tools/execution/stages/ExecutionStage.ts +48 -0
- package/src/tools/execution/stages/FormattingStage.ts +44 -0
- package/src/tools/execution/stages/HookStage.ts +72 -0
- package/src/tools/execution/stages/PermissionStage.ts +287 -0
- package/src/tools/execution/stages/PostHookStage.ts +71 -0
- package/src/tools/execution/stages/index.ts +12 -0
- package/src/tools/execution/test.ts +266 -0
- package/src/tools/execution/types.ts +273 -0
- package/src/tools/index.ts +81 -0
- package/src/tools/registry.ts +304 -0
- package/src/tools/schemas.ts +109 -0
- package/src/tools/test.ts +220 -0
- package/src/tools/types.ts +175 -0
- package/src/tools/validation/PermissionChecker.ts +242 -0
- package/src/tools/validation/SensitiveFileDetector.ts +210 -0
- package/src/tools/validation/index.ts +11 -0
- package/src/ui/App.tsx +166 -0
- package/src/ui/components/AegisInterface.tsx +484 -0
- package/src/ui/components/common/ChatSearch.tsx +150 -0
- package/src/ui/components/common/ErrorBoundary.tsx +82 -0
- package/src/ui/components/common/ExitMessage.tsx +120 -0
- package/src/ui/components/common/LoadingIndicator.tsx +49 -0
- package/src/ui/components/common/index.ts +6 -0
- package/src/ui/components/dialog/ConfirmationPrompt.tsx +208 -0
- package/src/ui/components/dialog/InteractiveSelector.tsx +149 -0
- package/src/ui/components/dialog/SetupWizard.tsx +297 -0
- package/src/ui/components/dialog/UpdatePrompt.tsx +155 -0
- package/src/ui/components/dialog/index.ts +8 -0
- package/src/ui/components/index.ts +28 -0
- package/src/ui/components/input/CommandSuggestions.tsx +139 -0
- package/src/ui/components/input/CustomTextInput.tsx +220 -0
- package/src/ui/components/input/InputArea.tsx +361 -0
- package/src/ui/components/input/PromptSuggestions.tsx +66 -0
- package/src/ui/components/input/index.ts +6 -0
- package/src/ui/components/layout/ChatStatusBar.tsx +90 -0
- package/src/ui/components/layout/ContextBar.tsx +79 -0
- package/src/ui/components/layout/MessageArea.tsx +96 -0
- package/src/ui/components/layout/MessageList.tsx +647 -0
- package/src/ui/components/layout/MessageSeparator.tsx +26 -0
- package/src/ui/components/layout/WelcomeMessage.tsx +93 -0
- package/src/ui/components/layout/index.ts +7 -0
- package/src/ui/components/markdown/CodeHighlighter.tsx +292 -0
- package/src/ui/components/markdown/MessageRenderer.tsx +1211 -0
- package/src/ui/components/markdown/index.ts +8 -0
- package/src/ui/components/markdown/parser.ts +336 -0
- package/src/ui/components/markdown/types.ts +66 -0
- package/src/ui/focus/FocusManager.ts +137 -0
- package/src/ui/focus/index.ts +13 -0
- package/src/ui/focus/types.ts +54 -0
- package/src/ui/focus/useFocus.ts +75 -0
- package/src/ui/hooks/index.ts +11 -0
- package/src/ui/hooks/useAgent.ts +284 -0
- package/src/ui/hooks/useCommandHistory.ts +87 -0
- package/src/ui/hooks/useCommandProcessor.ts +443 -0
- package/src/ui/hooks/useConfirmation.ts +99 -0
- package/src/ui/hooks/useCtrlCHandler.ts +100 -0
- package/src/ui/hooks/useInputBuffer.ts +122 -0
- package/src/ui/hooks/useTerminalSize.ts +68 -0
- package/src/ui/hooks/useTerminalWidth.ts +5 -0
- package/src/ui/hooks/useWindowedList.ts +118 -0
- package/src/ui/render-debugger.ts +621 -0
- package/src/ui/test.ts +189 -0
- package/src/ui/themes/ThemeManager.ts +332 -0
- package/src/ui/themes/aegisTheme.ts +87 -0
- package/src/ui/themes/darkTheme.ts +87 -0
- package/src/ui/themes/defaultTheme.ts +85 -0
- package/src/ui/themes/index.ts +10 -0
- package/src/ui/themes/lightTheme.ts +89 -0
- package/src/ui/themes/popularThemes.ts +187 -0
- package/src/ui/themes/types.ts +130 -0
- package/src/utils/clipboard.ts +48 -0
- package/src/utils/debug.ts +43 -0
- package/src/utils/environment.ts +68 -0
- 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
|
+
};
|