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,484 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AegisInterface.tsx - Main CLI interface component
|
|
3
|
+
*
|
|
4
|
+
* Refactored to use extracted hooks: useAgent, useCommandProcessor, useTerminalSize.
|
|
5
|
+
* Previously 977 lines — now ~450 lines of orchestration, with logic in focused hooks.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import React, { useEffect, useCallback, useRef, useState, useLayoutEffect } from 'react';
|
|
9
|
+
import { Box, Text, useInput, measureElement, type DOMElement } from 'ink';
|
|
10
|
+
|
|
11
|
+
// Store
|
|
12
|
+
import {
|
|
13
|
+
useInitializationStatus,
|
|
14
|
+
useActiveModal,
|
|
15
|
+
useMessages,
|
|
16
|
+
usePendingCommands,
|
|
17
|
+
useAutoRouterActiveModel,
|
|
18
|
+
useRouterEnabled,
|
|
19
|
+
useWorkflow,
|
|
20
|
+
sessionActions,
|
|
21
|
+
configActions,
|
|
22
|
+
commandActions,
|
|
23
|
+
getState,
|
|
24
|
+
subscribe,
|
|
25
|
+
} from '../../store/index.js';
|
|
26
|
+
|
|
27
|
+
// Hooks
|
|
28
|
+
import { useTerminalSize } from '../hooks/useTerminalSize.js';
|
|
29
|
+
import { useCtrlCHandler } from '../hooks/useCtrlCHandler.js';
|
|
30
|
+
import { useConfirmation } from '../hooks/useConfirmation.js';
|
|
31
|
+
import { useAgent } from '../hooks/useAgent.js';
|
|
32
|
+
import { useCommandProcessor } from '../hooks/useCommandProcessor.js';
|
|
33
|
+
|
|
34
|
+
// Components
|
|
35
|
+
import { MessageRenderer } from './markdown/MessageRenderer.js';
|
|
36
|
+
import { InputArea } from './input/InputArea.js';
|
|
37
|
+
import { ChatStatusBar } from './layout/ChatStatusBar.js';
|
|
38
|
+
import { WelcomeMessage } from './layout/WelcomeMessage.js';
|
|
39
|
+
import { MessageList } from './layout/MessageList.js';
|
|
40
|
+
import { ContextBar } from './layout/ContextBar.js';
|
|
41
|
+
import { ConfirmationPrompt } from './dialog/ConfirmationPrompt.js';
|
|
42
|
+
import { InteractiveSelector, type SelectorOption } from './dialog/InteractiveSelector.js';
|
|
43
|
+
import { SetupWizard } from './dialog/SetupWizard.js';
|
|
44
|
+
import { ExitMessage } from './common/ExitMessage.js';
|
|
45
|
+
import { ErrorBoundary } from './common/ErrorBoundary.js';
|
|
46
|
+
|
|
47
|
+
// Focus
|
|
48
|
+
import { FocusId, focusActions } from '../focus/index.js';
|
|
49
|
+
import { copyToClipboard } from '../../utils/clipboard.js';
|
|
50
|
+
|
|
51
|
+
// Heartbeat (online status ping)
|
|
52
|
+
import { startHeartbeat, stopHeartbeat } from '../../services/Heartbeat.js';
|
|
53
|
+
|
|
54
|
+
// Theme
|
|
55
|
+
import { themeManager } from '../themes/index.js';
|
|
56
|
+
|
|
57
|
+
// ========== Types ==========
|
|
58
|
+
|
|
59
|
+
export interface AegisInterfaceProps {
|
|
60
|
+
apiKey: string;
|
|
61
|
+
baseURL?: string;
|
|
62
|
+
model?: string;
|
|
63
|
+
initialMessage?: string;
|
|
64
|
+
debug?: boolean;
|
|
65
|
+
resumeSessionId?: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ========== Memoized Sub-Components ==========
|
|
69
|
+
|
|
70
|
+
const QueuedCommands: React.FC = React.memo(() => {
|
|
71
|
+
const pendingCommands = usePendingCommands();
|
|
72
|
+
const theme = themeManager.getTheme();
|
|
73
|
+
|
|
74
|
+
if (pendingCommands.length === 0) return null;
|
|
75
|
+
|
|
76
|
+
return (
|
|
77
|
+
<Box flexDirection="column" marginTop={0} marginBottom={0}>
|
|
78
|
+
{pendingCommands.map((cmd, i) => (
|
|
79
|
+
<Box key={i} flexDirection="row" marginLeft={1}>
|
|
80
|
+
<Text color={theme.colors.text.muted} dimColor>
|
|
81
|
+
<Text color={theme.colors.primary}>#{i+1}</Text> {cmd.length > 60 ? cmd.slice(0, 60) + '...' : cmd}
|
|
82
|
+
</Text>
|
|
83
|
+
</Box>
|
|
84
|
+
))}
|
|
85
|
+
</Box>
|
|
86
|
+
);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
QueuedCommands.displayName = 'QueuedCommands';
|
|
90
|
+
|
|
91
|
+
const RecentMessagesPreview: React.FC<{ terminalWidth: number; count?: number }> = React.memo(
|
|
92
|
+
({ terminalWidth, count = 3 }) => {
|
|
93
|
+
const messages = getState().session.messages;
|
|
94
|
+
const recentMessages = messages.slice(-count);
|
|
95
|
+
|
|
96
|
+
return (
|
|
97
|
+
<Box flexDirection="column" marginBottom={1}>
|
|
98
|
+
{recentMessages.map((msg, index) => (
|
|
99
|
+
<MessageRenderer
|
|
100
|
+
key={msg.id || index}
|
|
101
|
+
content={msg.content}
|
|
102
|
+
role={msg.role}
|
|
103
|
+
terminalWidth={terminalWidth}
|
|
104
|
+
showPrefix={true}
|
|
105
|
+
/>
|
|
106
|
+
))}
|
|
107
|
+
</Box>
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
RecentMessagesPreview.displayName = 'RecentMessagesPreview';
|
|
113
|
+
|
|
114
|
+
// ========== Main Component ==========
|
|
115
|
+
|
|
116
|
+
export const AegisInterface: React.FC<AegisInterfaceProps> = ({
|
|
117
|
+
apiKey,
|
|
118
|
+
baseURL,
|
|
119
|
+
model,
|
|
120
|
+
initialMessage,
|
|
121
|
+
debug,
|
|
122
|
+
resumeSessionId,
|
|
123
|
+
}) => {
|
|
124
|
+
// ==================== Terminal Size ====================
|
|
125
|
+
const { width: terminalWidth, height: terminalHeight } = useTerminalSize();
|
|
126
|
+
|
|
127
|
+
// ==================== Store State ====================
|
|
128
|
+
const initializationStatus = useInitializationStatus();
|
|
129
|
+
const activeModal = useActiveModal();
|
|
130
|
+
const messages = useMessages();
|
|
131
|
+
|
|
132
|
+
const getMessages = useCallback(() => getState().session.messages, []);
|
|
133
|
+
|
|
134
|
+
// ==================== Agent Hook ====================
|
|
135
|
+
const {
|
|
136
|
+
agentRef,
|
|
137
|
+
contextManagerRef,
|
|
138
|
+
isInitializing,
|
|
139
|
+
initError,
|
|
140
|
+
currentModel,
|
|
141
|
+
handleSetupComplete,
|
|
142
|
+
} = useAgent({ apiKey, baseURL, model, debug, resumeSessionId });
|
|
143
|
+
|
|
144
|
+
const autoRouterActiveModel = useAutoRouterActiveModel();
|
|
145
|
+
const routerEnabled = useRouterEnabled();
|
|
146
|
+
const workflow = useWorkflow();
|
|
147
|
+
|
|
148
|
+
// ==================== Below-MessageList overhead measurement ====================
|
|
149
|
+
// MessageList budgets its viewport with a fixed UI_OVERHEAD constant that only
|
|
150
|
+
// accounts for the input area/status bar. ContextBar (workflow indicator) and
|
|
151
|
+
// QueuedCommands render below it and can take a variable number of extra rows
|
|
152
|
+
// (e.g. ContextBar only appears at all when workflow.visible) — without telling
|
|
153
|
+
// MessageList about that, its row budget and what's actually left on screen drift
|
|
154
|
+
// apart, which looks like messages getting clipped/disappearing. Measuring the
|
|
155
|
+
// actual rendered height post-layout keeps the two in sync regardless of what
|
|
156
|
+
// gets added below the message list in the future.
|
|
157
|
+
const belowMessageListRef = useRef<DOMElement>(null);
|
|
158
|
+
const [belowOverheadLines, setBelowOverheadLines] = useState(0);
|
|
159
|
+
const pendingCommandsForOverhead = usePendingCommands();
|
|
160
|
+
useLayoutEffect(() => {
|
|
161
|
+
if (!belowMessageListRef.current) return;
|
|
162
|
+
const { height } = measureElement(belowMessageListRef.current);
|
|
163
|
+
setBelowOverheadLines(height);
|
|
164
|
+
}, [workflow, pendingCommandsForOverhead.length]);
|
|
165
|
+
|
|
166
|
+
// ==================== Stable Refs ====================
|
|
167
|
+
const debugRef = useRef(debug);
|
|
168
|
+
debugRef.current = debug;
|
|
169
|
+
const modelRef = useRef(model);
|
|
170
|
+
modelRef.current = model;
|
|
171
|
+
const getMessagesRef = useRef(getMessages);
|
|
172
|
+
getMessagesRef.current = getMessages;
|
|
173
|
+
|
|
174
|
+
// ==================== Local State ====================
|
|
175
|
+
const [isExiting, setIsExiting] = useState(false);
|
|
176
|
+
const [exitSessionId, setExitSessionId] = useState<string | null>(null);
|
|
177
|
+
const [isScrolledUp, setIsScrolledUp] = useState(false);
|
|
178
|
+
const [renderLatency, setRenderLatency] = useState(0);
|
|
179
|
+
|
|
180
|
+
const renderLatencyTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
181
|
+
const handleRenderLatency = useCallback((ms: number) => {
|
|
182
|
+
// Debounce: only update state at most every 500ms
|
|
183
|
+
if (renderLatencyTimerRef.current) return;
|
|
184
|
+
renderLatencyTimerRef.current = setTimeout(() => {
|
|
185
|
+
renderLatencyTimerRef.current = null;
|
|
186
|
+
}, 500);
|
|
187
|
+
setRenderLatency(ms);
|
|
188
|
+
}, []);
|
|
189
|
+
|
|
190
|
+
// Clean up latency debounce timer on unmount
|
|
191
|
+
useEffect(() => {
|
|
192
|
+
return () => {
|
|
193
|
+
if (renderLatencyTimerRef.current) clearTimeout(renderLatencyTimerRef.current);
|
|
194
|
+
};
|
|
195
|
+
}, []);
|
|
196
|
+
|
|
197
|
+
// ==================== Heartbeat (online status) ====================
|
|
198
|
+
useEffect(() => {
|
|
199
|
+
startHeartbeat();
|
|
200
|
+
return () => stopHeartbeat();
|
|
201
|
+
}, []);
|
|
202
|
+
|
|
203
|
+
const [selectorState, setSelectorState] = useState<{
|
|
204
|
+
isVisible: boolean;
|
|
205
|
+
title: string;
|
|
206
|
+
options: SelectorOption[];
|
|
207
|
+
handler: 'theme' | 'model' | null;
|
|
208
|
+
}>({
|
|
209
|
+
isVisible: false,
|
|
210
|
+
title: '',
|
|
211
|
+
options: [],
|
|
212
|
+
handler: null,
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
// ==================== Hooks ====================
|
|
216
|
+
const { confirmationState, confirmationHandler, handleResponse } = useConfirmation();
|
|
217
|
+
const confirmationHandlerRef = useRef(confirmationHandler);
|
|
218
|
+
confirmationHandlerRef.current = confirmationHandler;
|
|
219
|
+
|
|
220
|
+
// Command processor — extracted hook
|
|
221
|
+
const { processCommand, handleSubmit, processQueue } = useCommandProcessor({
|
|
222
|
+
agentRef,
|
|
223
|
+
contextManagerRef,
|
|
224
|
+
modelRef,
|
|
225
|
+
debugRef,
|
|
226
|
+
getMessagesRef,
|
|
227
|
+
confirmationHandlerRef,
|
|
228
|
+
onSelectorRequest: (state) => {
|
|
229
|
+
setSelectorState({
|
|
230
|
+
isVisible: true,
|
|
231
|
+
title: state.title,
|
|
232
|
+
options: state.options,
|
|
233
|
+
handler: state.handler,
|
|
234
|
+
});
|
|
235
|
+
focusActions.setFocus(FocusId.SELECTOR);
|
|
236
|
+
},
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
// ESC interrupt (scrolling handled by MessageList internally)
|
|
240
|
+
useInput((_input, key) => {
|
|
241
|
+
if (key.escape && getState().session.isThinking) {
|
|
242
|
+
commandActions().abort();
|
|
243
|
+
sessionActions().setThinking(false);
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
// ==================== Alt+C: Copy last assistant reply ====================
|
|
248
|
+
useInput((_input, key) => {
|
|
249
|
+
if (key.meta && _input === 'c') {
|
|
250
|
+
const msgs = getState().session.messages.filter(m => m.role === 'assistant');
|
|
251
|
+
if (msgs.length === 0) return;
|
|
252
|
+
const last = msgs[msgs.length - 1];
|
|
253
|
+
// Strip markdown for clean copy
|
|
254
|
+
const plain = last.content
|
|
255
|
+
.replace(/```[\s\S]*?```/g, m => m.replace(/```\w*\n?/, '').replace(/\n?```/, ''))
|
|
256
|
+
.replace(/\*\*(.+?)\*\*/g, '$1')
|
|
257
|
+
.replace(/\*(.+?)\*/g, '$1')
|
|
258
|
+
.replace(/`([^`]+)`/g, '$1')
|
|
259
|
+
.replace(/~~(.+?)~~/g, '$1')
|
|
260
|
+
.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1')
|
|
261
|
+
.replace(/^#{1,6}\s+/gm, '')
|
|
262
|
+
.replace(/^>\s+/gm, '')
|
|
263
|
+
.trim();
|
|
264
|
+
if (!plain) return;
|
|
265
|
+
copyToClipboard(plain)
|
|
266
|
+
.then(async () => {
|
|
267
|
+
const { nanoid } = await import('nanoid');
|
|
268
|
+
sessionActions().addMessage({
|
|
269
|
+
id: nanoid(),
|
|
270
|
+
role: 'system',
|
|
271
|
+
content: '✓ Copied last reply to clipboard',
|
|
272
|
+
timestamp: Date.now(),
|
|
273
|
+
});
|
|
274
|
+
})
|
|
275
|
+
.catch(async (err) => {
|
|
276
|
+
const { nanoid } = await import('nanoid');
|
|
277
|
+
sessionActions().addMessage({
|
|
278
|
+
id: nanoid(),
|
|
279
|
+
role: 'system',
|
|
280
|
+
content: `✗ Copy failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
281
|
+
timestamp: Date.now(),
|
|
282
|
+
});
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
// ==================== Alt+R: Toggle auto-router ====================
|
|
288
|
+
useInput((_input, key) => {
|
|
289
|
+
if (key.meta && _input === 'r') {
|
|
290
|
+
const isEnabled = getState().config.config?.autoRouter?.enabled ?? false;
|
|
291
|
+
handleSubmit(isEnabled ? '/router off' : '/router on');
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
// ==================== Ctrl+C Handler ====================
|
|
296
|
+
useCtrlCHandler({
|
|
297
|
+
onInterrupt: () => {
|
|
298
|
+
commandActions().abort();
|
|
299
|
+
sessionActions().setThinking(false);
|
|
300
|
+
},
|
|
301
|
+
onBeforeExit: () => {
|
|
302
|
+
const currentMessageCount = getState().session.messages.length;
|
|
303
|
+
const currentSessionId = contextManagerRef.current?.getCurrentSessionId() || getState().session.sessionId;
|
|
304
|
+
if (currentSessionId && currentMessageCount > 0) {
|
|
305
|
+
// Flush pending saves before exit so the last messages are persisted
|
|
306
|
+
contextManagerRef.current?.flush().catch(() => {});
|
|
307
|
+
setExitSessionId(currentSessionId);
|
|
308
|
+
setIsExiting(true);
|
|
309
|
+
return true;
|
|
310
|
+
}
|
|
311
|
+
return false;
|
|
312
|
+
},
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
// ==================== Focus Management ====================
|
|
316
|
+
useEffect(() => {
|
|
317
|
+
if (confirmationState.isVisible) return;
|
|
318
|
+
if (selectorState.isVisible) {
|
|
319
|
+
focusActions.setFocus(FocusId.SELECTOR);
|
|
320
|
+
} else if (activeModal === 'themeSelector') {
|
|
321
|
+
focusActions.setFocus(FocusId.THEME_SELECTOR);
|
|
322
|
+
} else {
|
|
323
|
+
focusActions.setFocus(FocusId.MAIN_INPUT);
|
|
324
|
+
}
|
|
325
|
+
}, [confirmationState.isVisible, selectorState.isVisible, activeModal]);
|
|
326
|
+
|
|
327
|
+
// ==================== Selector Handlers ====================
|
|
328
|
+
const handleSelectorSelect = useCallback(async (value: string) => {
|
|
329
|
+
const { handler } = selectorState;
|
|
330
|
+
focusActions.setFocus(FocusId.MAIN_INPUT);
|
|
331
|
+
setSelectorState({ isVisible: false, title: '', options: [], handler: null });
|
|
332
|
+
|
|
333
|
+
if (handler === 'theme') {
|
|
334
|
+
themeManager.setTheme(value);
|
|
335
|
+
sessionActions().addAssistantMessage('✓ ' + value);
|
|
336
|
+
} else if (handler === 'model') {
|
|
337
|
+
if (value.startsWith('__ollama__')) {
|
|
338
|
+
const modelName = value.slice('__ollama__'.length);
|
|
339
|
+
const id = modelName.replace(/[^a-z0-9_-]/gi, '-').toLowerCase();
|
|
340
|
+
try {
|
|
341
|
+
const fs = await import('fs');
|
|
342
|
+
const path = await import('path');
|
|
343
|
+
const os = await import('os');
|
|
344
|
+
const cfgPath = path.join(os.homedir(), '.aegiscode', 'config.json');
|
|
345
|
+
try {
|
|
346
|
+
const cfg = JSON.parse(fs.readFileSync(cfgPath, 'utf8'));
|
|
347
|
+
cfg.models = cfg.models || [];
|
|
348
|
+
if (!cfg.models.find((m: any) => m.id === id)) {
|
|
349
|
+
cfg.models.push({ id, name: modelName, model: modelName, baseURL: 'http://localhost:11434/v1', apiKey: 'ollama' });
|
|
350
|
+
}
|
|
351
|
+
cfg.currentModelId = id;
|
|
352
|
+
fs.writeFileSync(cfgPath, JSON.stringify(cfg, null, 2));
|
|
353
|
+
} catch { /* non-fatal */ }
|
|
354
|
+
} catch { /* non-fatal */ }
|
|
355
|
+
configActions().updateConfig({ currentModelId: id });
|
|
356
|
+
} else {
|
|
357
|
+
configActions().updateConfig({ currentModelId: value });
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}, [selectorState]);
|
|
361
|
+
|
|
362
|
+
const handleSelectorCancel = useCallback(() => {
|
|
363
|
+
setSelectorState({ isVisible: false, title: '', options: [], handler: null });
|
|
364
|
+
focusActions.setFocus(FocusId.MAIN_INPUT);
|
|
365
|
+
}, []);
|
|
366
|
+
|
|
367
|
+
// ==================== Queue Processor ====================
|
|
368
|
+
useEffect(() => {
|
|
369
|
+
let prevIsThinking = getState().session.isThinking;
|
|
370
|
+
const unsubscribe = subscribe((state) => {
|
|
371
|
+
const currentIsThinking = state.session.isThinking;
|
|
372
|
+
const hasPending = state.command.pendingCommands.length > 0;
|
|
373
|
+
if (prevIsThinking && !currentIsThinking && hasPending) {
|
|
374
|
+
processQueue();
|
|
375
|
+
}
|
|
376
|
+
prevIsThinking = currentIsThinking;
|
|
377
|
+
});
|
|
378
|
+
return unsubscribe;
|
|
379
|
+
}, [processQueue]);
|
|
380
|
+
|
|
381
|
+
// ==================== Initial Message ====================
|
|
382
|
+
const initialMessageSent = useRef(false);
|
|
383
|
+
useEffect(() => {
|
|
384
|
+
if (initialMessage && !initialMessageSent.current && !isInitializing && agentRef.current) {
|
|
385
|
+
initialMessageSent.current = true;
|
|
386
|
+
handleSubmit(initialMessage);
|
|
387
|
+
}
|
|
388
|
+
}, [initialMessage, handleSubmit, isInitializing]);
|
|
389
|
+
|
|
390
|
+
// ==================== Scroll State Notification ====================
|
|
391
|
+
const handleScrolledUpChange = useCallback((scrolledUp: boolean) => {
|
|
392
|
+
setIsScrolledUp(scrolledUp);
|
|
393
|
+
}, []);
|
|
394
|
+
|
|
395
|
+
// ==================== Render ====================
|
|
396
|
+
|
|
397
|
+
if (isInitializing) {
|
|
398
|
+
return (
|
|
399
|
+
<Box flexDirection="column" padding={1}>
|
|
400
|
+
<Box>
|
|
401
|
+
<Text color="yellow"> Initializing...</Text>
|
|
402
|
+
</Box>
|
|
403
|
+
</Box>
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
if (initError) {
|
|
408
|
+
return (
|
|
409
|
+
<Box flexDirection="column" padding={1}>
|
|
410
|
+
<Text color="red">Agent initialization failed:</Text>
|
|
411
|
+
<Text color="red">{initError}</Text>
|
|
412
|
+
</Box>
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
if (initializationStatus === 'needsSetup') {
|
|
417
|
+
return <SetupWizard onComplete={handleSetupComplete} />;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
const hasPendingInitialMessage = !!(initialMessage && !initialMessageSent.current);
|
|
421
|
+
|
|
422
|
+
return (
|
|
423
|
+
<Box flexDirection="column" width="100%" paddingX={0} flexGrow={1}>
|
|
424
|
+
{messages.length === 0 && <WelcomeMessage terminalWidth={terminalWidth - 2} />}
|
|
425
|
+
|
|
426
|
+
{selectorState.isVisible ? (
|
|
427
|
+
<>
|
|
428
|
+
<RecentMessagesPreview terminalWidth={terminalWidth - 2} count={3} />
|
|
429
|
+
<InteractiveSelector
|
|
430
|
+
title={selectorState.title}
|
|
431
|
+
options={selectorState.options}
|
|
432
|
+
onSelect={handleSelectorSelect}
|
|
433
|
+
onCancel={handleSelectorCancel}
|
|
434
|
+
focusId={FocusId.SELECTOR}
|
|
435
|
+
/>
|
|
436
|
+
</>
|
|
437
|
+
) : (
|
|
438
|
+
<>
|
|
439
|
+
{messages.length > 0 && (
|
|
440
|
+
<ErrorBoundary name="MessageList" fallback={<Text color="red">Message list error</Text>}>
|
|
441
|
+
<Box flexGrow={1} minHeight={0}>
|
|
442
|
+
<MessageList
|
|
443
|
+
terminalWidth={terminalWidth - 2}
|
|
444
|
+
terminalHeight={terminalHeight}
|
|
445
|
+
onScrolledUpChange={handleScrolledUpChange}
|
|
446
|
+
onRenderLatency={handleRenderLatency}
|
|
447
|
+
extraOverheadLines={belowOverheadLines}
|
|
448
|
+
/>
|
|
449
|
+
</Box>
|
|
450
|
+
</ErrorBoundary>
|
|
451
|
+
)}
|
|
452
|
+
<Box ref={belowMessageListRef} flexDirection="column">
|
|
453
|
+
<ContextBar workflow={workflow} />
|
|
454
|
+
<QueuedCommands />
|
|
455
|
+
</Box>
|
|
456
|
+
|
|
457
|
+
{confirmationState.isVisible && confirmationState.details && (
|
|
458
|
+
<ErrorBoundary name="ConfirmationPrompt" fallback={null}>
|
|
459
|
+
<ConfirmationPrompt
|
|
460
|
+
details={confirmationState.details}
|
|
461
|
+
onResponse={handleResponse}
|
|
462
|
+
/>
|
|
463
|
+
</ErrorBoundary>
|
|
464
|
+
)}
|
|
465
|
+
|
|
466
|
+
<ErrorBoundary name="InputArea" fallback={<Text color="red">Input error — restart app</Text>}>
|
|
467
|
+
<InputArea onSubmit={handleSubmit} />
|
|
468
|
+
</ErrorBoundary>
|
|
469
|
+
<ChatStatusBar
|
|
470
|
+
model={autoRouterActiveModel || currentModel}
|
|
471
|
+
modelIsAuto={!!autoRouterActiveModel}
|
|
472
|
+
isScrolledUp={messages.length > 0 && isScrolledUp}
|
|
473
|
+
renderLatency={renderLatency}
|
|
474
|
+
routerEnabled={routerEnabled}
|
|
475
|
+
/>
|
|
476
|
+
</>
|
|
477
|
+
)}
|
|
478
|
+
|
|
479
|
+
{isExiting && exitSessionId && (
|
|
480
|
+
<ExitMessage sessionId={exitSessionId} />
|
|
481
|
+
)}
|
|
482
|
+
</Box>
|
|
483
|
+
);
|
|
484
|
+
};
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ChatSearch - Ctrl+F inline search within messages
|
|
3
|
+
*
|
|
4
|
+
* Shows a search bar overlay when activated via Ctrl+F.
|
|
5
|
+
* Highlights matching messages and allows cycling through results.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import React, { useState, useCallback, useEffect, useRef, useMemo } from 'react';
|
|
9
|
+
import { Box, Text, useInput } from 'ink';
|
|
10
|
+
import { themeManager } from '../../themes/index.js';
|
|
11
|
+
import { vanillaStore } from '../../../store/vanilla.js';
|
|
12
|
+
import type { FocusId } from '../../focus/index.js';
|
|
13
|
+
|
|
14
|
+
interface ChatSearchProps {
|
|
15
|
+
/** Called when search is dismissed */
|
|
16
|
+
onDismiss: () => void;
|
|
17
|
+
/** Called with indices of matching messages */
|
|
18
|
+
onResults?: (indices: number[], currentIndex: number) => void;
|
|
19
|
+
/** The focus ID for input capture */
|
|
20
|
+
focusId?: FocusId;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const ChatSearch: React.FC<ChatSearchProps> = ({
|
|
24
|
+
onDismiss,
|
|
25
|
+
onResults,
|
|
26
|
+
}) => {
|
|
27
|
+
const theme = themeManager.getTheme();
|
|
28
|
+
const [query, setQuery] = useState('');
|
|
29
|
+
const [cursorPos, setCursorPos] = useState(0);
|
|
30
|
+
const [currentMatch, setCurrentMatch] = useState(0);
|
|
31
|
+
const queryRef = useRef('');
|
|
32
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
|
33
|
+
|
|
34
|
+
const messages = useMemo(
|
|
35
|
+
() => vanillaStore.getState().session.messages,
|
|
36
|
+
[],
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
const matchIndices = useMemo(() => {
|
|
40
|
+
if (!query.trim()) return [];
|
|
41
|
+
const q = query.toLowerCase();
|
|
42
|
+
return messages.reduce<number[]>((acc, msg, idx) => {
|
|
43
|
+
if (msg.content.toLowerCase().includes(q)) acc.push(idx);
|
|
44
|
+
return acc;
|
|
45
|
+
}, []);
|
|
46
|
+
}, [query, messages]);
|
|
47
|
+
|
|
48
|
+
const totalMatches = matchIndices.length;
|
|
49
|
+
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
setCurrentMatch(0);
|
|
52
|
+
if (onResults) {
|
|
53
|
+
onResults(matchIndices, 0);
|
|
54
|
+
}
|
|
55
|
+
}, [query]);
|
|
56
|
+
|
|
57
|
+
const handleChange = useCallback((value: string) => {
|
|
58
|
+
queryRef.current = value;
|
|
59
|
+
setQuery(value);
|
|
60
|
+
setCursorPos(value.length);
|
|
61
|
+
}, []);
|
|
62
|
+
|
|
63
|
+
const handlePrev = useCallback(() => {
|
|
64
|
+
if (matchIndices.length === 0) return;
|
|
65
|
+
const next = currentMatch > 0 ? currentMatch - 1 : matchIndices.length - 1;
|
|
66
|
+
setCurrentMatch(next);
|
|
67
|
+
if (onResults) onResults(matchIndices, next);
|
|
68
|
+
}, [currentMatch, matchIndices, onResults]);
|
|
69
|
+
|
|
70
|
+
const handleNext = useCallback(() => {
|
|
71
|
+
if (matchIndices.length === 0) return;
|
|
72
|
+
const next = currentMatch < matchIndices.length - 1 ? currentMatch + 1 : 0;
|
|
73
|
+
setCurrentMatch(next);
|
|
74
|
+
if (onResults) onResults(matchIndices, next);
|
|
75
|
+
}, [currentMatch, matchIndices, onResults]);
|
|
76
|
+
|
|
77
|
+
useInput((input, key) => {
|
|
78
|
+
if (key.escape) {
|
|
79
|
+
onDismiss();
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
if (key.return) {
|
|
83
|
+
handleNext();
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (key.backspace || key.delete) {
|
|
87
|
+
handleChange(queryRef.current.slice(0, -1));
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
if (key.tab) {
|
|
91
|
+
handleNext();
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if (key.shift && key.tab) {
|
|
95
|
+
handlePrev();
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
if (key.upArrow) {
|
|
99
|
+
handlePrev();
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
if (key.downArrow) {
|
|
103
|
+
handleNext();
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
if (input && !key.ctrl && !key.meta && input.length === 1) {
|
|
107
|
+
handleChange(queryRef.current + input);
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
return (
|
|
112
|
+
<Box
|
|
113
|
+
flexDirection="row"
|
|
114
|
+
paddingX={1}
|
|
115
|
+
paddingY={0}
|
|
116
|
+
borderStyle="round"
|
|
117
|
+
borderColor={theme.colors.accent}
|
|
118
|
+
marginBottom={0}
|
|
119
|
+
>
|
|
120
|
+
<Box marginRight={1}>
|
|
121
|
+
<Text color={theme.colors.info} bold>
|
|
122
|
+
{'\u2315'}
|
|
123
|
+
</Text>
|
|
124
|
+
</Box>
|
|
125
|
+
|
|
126
|
+
<Box flexGrow={1}>
|
|
127
|
+
<Text>
|
|
128
|
+
<Text color={theme.colors.text.primary}>{query}</Text>
|
|
129
|
+
<Text color={theme.colors.text.muted}>{'\u2502'}</Text>
|
|
130
|
+
</Text>
|
|
131
|
+
</Box>
|
|
132
|
+
|
|
133
|
+
<Box marginRight={1}>
|
|
134
|
+
<Text color={theme.colors.text.muted} dimColor>
|
|
135
|
+
{totalMatches > 0
|
|
136
|
+
? `${currentMatch + 1}/${totalMatches}`
|
|
137
|
+
: query.trim()
|
|
138
|
+
? '0/0'
|
|
139
|
+
: ''}
|
|
140
|
+
</Text>
|
|
141
|
+
</Box>
|
|
142
|
+
|
|
143
|
+
<Text color={theme.colors.text.muted} dimColor>
|
|
144
|
+
{'n/N to navigate, Esc to close'}
|
|
145
|
+
</Text>
|
|
146
|
+
</Box>
|
|
147
|
+
);
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
export default ChatSearch;
|