aegiscode 3.1.7 → 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 +555 -556
  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,297 @@
1
+ /**
2
+ * SetupWizard - Interactive first-run setup guide
3
+ *
4
+ * Saves API keys to ~/.aegiscode/.env — config.json picks them up automatically.
5
+ * Flow: pick provider → enter key → add another? → done
6
+ */
7
+
8
+ import React, { useState, useCallback } from 'react';
9
+ import { Box, Text, useInput } from 'ink';
10
+ import * as fs from 'node:fs';
11
+ import * as path from 'node:path';
12
+ import * as os from 'node:os';
13
+ import { themeManager } from '../../themes/index.js';
14
+ import { appActions } from '../../../store/index.js';
15
+
16
+ interface Provider {
17
+ id: string;
18
+ label: string;
19
+ envVar: string;
20
+ keyUrl: string;
21
+ noKey?: boolean;
22
+ }
23
+
24
+ const PROVIDERS: Provider[] = [
25
+ {
26
+ id: 'anthropic',
27
+ label: 'Anthropic (Claude)',
28
+ envVar: 'ANTHROPIC_API_KEY',
29
+ keyUrl: 'console.anthropic.com/settings/keys',
30
+ },
31
+ {
32
+ id: 'openai',
33
+ label: 'OpenAI (GPT)',
34
+ envVar: 'OPENAI_API_KEY',
35
+ keyUrl: 'platform.openai.com/api-keys',
36
+ },
37
+ {
38
+ id: 'deepseek',
39
+ label: 'DeepSeek',
40
+ envVar: 'DEEPSEEK_API_KEY',
41
+ keyUrl: 'platform.deepseek.com/api_keys',
42
+ },
43
+ {
44
+ id: 'groq',
45
+ label: 'Groq',
46
+ envVar: 'GROQ_API_KEY',
47
+ keyUrl: 'console.groq.com/keys',
48
+ },
49
+ {
50
+ id: 'gemini',
51
+ label: 'Google Gemini',
52
+ envVar: 'GEMINI_API_KEY',
53
+ keyUrl: 'aistudio.google.com/app/apikey',
54
+ },
55
+ {
56
+ id: 'ollama',
57
+ label: 'Ollama (local)',
58
+ envVar: '',
59
+ keyUrl: '',
60
+ noKey: true,
61
+ },
62
+ ];
63
+
64
+ type Step = 'provider' | 'apikey' | 'another' | 'saving' | 'done';
65
+
66
+ interface SetupWizardProps {
67
+ onComplete: () => void;
68
+ }
69
+
70
+ function readEnv(envPath: string): Record<string, string> {
71
+ const result: Record<string, string> = {};
72
+ if (!fs.existsSync(envPath)) return result;
73
+ for (const line of fs.readFileSync(envPath, 'utf-8').split('\n')) {
74
+ const eq = line.indexOf('=');
75
+ if (eq < 0 || line.trim().startsWith('#')) continue;
76
+ result[line.slice(0, eq).trim()] = line.slice(eq + 1).trim();
77
+ }
78
+ return result;
79
+ }
80
+
81
+ function writeEnv(envPath: string, vars: Record<string, string>): void {
82
+ const lines = Object.entries(vars).map(([k, v]) => `${k}=${v}`);
83
+ fs.writeFileSync(envPath, lines.join('\n') + '\n', 'utf-8');
84
+ }
85
+
86
+ export const SetupWizard: React.FC<SetupWizardProps> = ({ onComplete }) => {
87
+ const theme = themeManager.getTheme();
88
+ const [step, setStep] = useState<Step>('provider');
89
+ const [selectedIdx, setSelectedIdx] = useState(0);
90
+ const [apiKey, setApiKey] = useState('');
91
+ const [error, setError] = useState<string | null>(null);
92
+ const [saved, setSaved] = useState<string[]>([]); // env var names already saved this session
93
+
94
+ const selectedProvider = PROVIDERS[selectedIdx];
95
+ const envPath = path.join(os.homedir(), '.aegiscode', '.env');
96
+
97
+ const saveKeyToEnv = useCallback((provider: Provider, key: string) => {
98
+ const configDir = path.join(os.homedir(), '.aegiscode');
99
+ if (!fs.existsSync(configDir)) fs.mkdirSync(configDir, { recursive: true });
100
+ const vars = readEnv(envPath);
101
+ vars[provider.envVar] = key;
102
+ writeEnv(envPath, vars);
103
+ }, [envPath]);
104
+
105
+ const finalize = useCallback(() => {
106
+ setStep('saving');
107
+ // Inject saved env vars into process.env so ConfigManager picks them up immediately
108
+ const vars = readEnv(envPath);
109
+ for (const [k, v] of Object.entries(vars)) {
110
+ if (v && !v.startsWith('YOUR_')) process.env[k] = v;
111
+ }
112
+ appActions().setInitializationStatus('ready');
113
+ setStep('done');
114
+ setTimeout(() => onComplete(), 300);
115
+ }, [envPath, onComplete]);
116
+
117
+ useInput((input, key) => {
118
+ if (step === 'provider') {
119
+ if (key.upArrow) {
120
+ setSelectedIdx(i => (i > 0 ? i - 1 : PROVIDERS.length - 1));
121
+ } else if (key.downArrow) {
122
+ setSelectedIdx(i => (i < PROVIDERS.length - 1 ? i + 1 : 0));
123
+ } else if (key.return) {
124
+ if (selectedProvider.noKey) {
125
+ // Ollama needs no key — mark as done and ask for another
126
+ setSaved(s => [...s, 'Ollama']);
127
+ setStep('another');
128
+ } else {
129
+ setApiKey('');
130
+ setError(null);
131
+ setStep('apikey');
132
+ }
133
+ } else if (key.escape) {
134
+ if (saved.length > 0) {
135
+ finalize();
136
+ } else {
137
+ process.exit(0);
138
+ }
139
+ }
140
+ } else if (step === 'apikey') {
141
+ if (key.return) {
142
+ const trimmed = apiKey.trim();
143
+ if (!trimmed) {
144
+ setError('Key cannot be empty.');
145
+ return;
146
+ }
147
+ try {
148
+ saveKeyToEnv(selectedProvider, trimmed);
149
+ setSaved(s => [...s, selectedProvider.envVar]);
150
+ setApiKey('');
151
+ setError(null);
152
+ setStep('another');
153
+ } catch (err) {
154
+ setError((err as Error).message);
155
+ }
156
+ } else if (key.escape) {
157
+ setApiKey('');
158
+ setError(null);
159
+ setStep('provider');
160
+ } else if (key.backspace || key.delete) {
161
+ setApiKey(prev => prev.slice(0, -1));
162
+ } else if (input && !key.ctrl && !key.meta && input.length === 1) {
163
+ setApiKey(prev => prev + input);
164
+ }
165
+ } else if (step === 'another') {
166
+ if (input === 'y' || input === 'Y') {
167
+ setStep('provider');
168
+ } else if (input === 'n' || input === 'N' || key.return || key.escape) {
169
+ finalize();
170
+ }
171
+ }
172
+ });
173
+
174
+ const primary = theme.colors.primary;
175
+ const muted = theme.colors.text?.muted ?? 'gray';
176
+ const textPrimary = theme.colors.text?.primary ?? 'white';
177
+
178
+ if (step === 'saving' || step === 'done') {
179
+ return (
180
+ <Box flexDirection="column" padding={1}>
181
+ <Text color={primary}>◆ Saving configuration...</Text>
182
+ </Box>
183
+ );
184
+ }
185
+
186
+ return (
187
+ <Box flexDirection="column" padding={1}>
188
+ {/* Header */}
189
+ <Box marginBottom={1}>
190
+ <Text bold color={primary}>◆ ÆGIS — Setup</Text>
191
+ </Box>
192
+ <Box marginBottom={1}>
193
+ <Text color={muted}>
194
+ Keys are saved to <Text color={textPrimary}>~/.aegiscode/.env</Text>
195
+ </Text>
196
+ </Box>
197
+
198
+ {/* Saved so far */}
199
+ {saved.length > 0 && (
200
+ <Box marginBottom={1} flexDirection="column">
201
+ {saved.map(v => (
202
+ <Text key={v} color="green">✓ {v}</Text>
203
+ ))}
204
+ </Box>
205
+ )}
206
+
207
+ {step === 'provider' && (
208
+ <>
209
+ <Box marginBottom={1}>
210
+ <Text bold color={textPrimary}>Select provider:</Text>
211
+ </Box>
212
+ <Box flexDirection="column" marginBottom={1}>
213
+ {PROVIDERS.map((p, i) => {
214
+ const isSelected = i === selectedIdx;
215
+ const alreadySaved = saved.includes(p.envVar || 'Ollama');
216
+ return (
217
+ <Box key={p.id}>
218
+ <Text color={isSelected ? primary : muted}>
219
+ {isSelected ? '❯ ' : ' '}
220
+ </Text>
221
+ <Text bold={isSelected} color={isSelected ? textPrimary : muted}>
222
+ {p.label}
223
+ </Text>
224
+ <Text color={muted}>
225
+ {' '}
226
+ </Text>
227
+ <Text color={alreadySaved ? 'green' : muted} dimColor={!alreadySaved}>
228
+ {p.noKey ? '(no key needed)' : p.envVar}
229
+ {alreadySaved ? ' ✓' : ''}
230
+ </Text>
231
+ </Box>
232
+ );
233
+ })}
234
+ </Box>
235
+ <Box>
236
+ <Text color={muted} dimColor>
237
+ ↑↓ navigate Enter select{saved.length > 0 ? ' Esc done' : ' Esc exit'}
238
+ </Text>
239
+ </Box>
240
+ </>
241
+ )}
242
+
243
+ {step === 'apikey' && (
244
+ <>
245
+ <Box marginBottom={1}>
246
+ <Text bold color={textPrimary}>
247
+ {selectedProvider.label}
248
+ </Text>
249
+ </Box>
250
+ <Box marginBottom={1}>
251
+ <Text color={muted} dimColor>
252
+ Get your key at: {selectedProvider.keyUrl}
253
+ </Text>
254
+ </Box>
255
+ <Box marginBottom={1}>
256
+ <Text color={muted}>{selectedProvider.envVar}=</Text>
257
+ </Box>
258
+ <Box marginBottom={1} borderStyle="round" borderColor={primary} paddingX={1}>
259
+ <Text color={textPrimary}>
260
+ {apiKey.length > 0
261
+ ? apiKey.slice(0, 8) + '•'.repeat(Math.max(0, apiKey.length - 8))
262
+ : ''}
263
+ <Text color={primary}>▏</Text>
264
+ {apiKey.length === 0 && (
265
+ <Text color={muted} dimColor>paste your key here</Text>
266
+ )}
267
+ </Text>
268
+ </Box>
269
+ {error && (
270
+ <Box marginBottom={1}>
271
+ <Text color="red">✗ {error}</Text>
272
+ </Box>
273
+ )}
274
+ <Box>
275
+ <Text color={muted} dimColor>Enter confirm Esc back</Text>
276
+ </Box>
277
+ </>
278
+ )}
279
+
280
+ {step === 'another' && (
281
+ <>
282
+ <Box marginBottom={1}>
283
+ <Text color="green">✓ Saved to ~/.aegiscode/.env</Text>
284
+ </Box>
285
+ <Box>
286
+ <Text color={textPrimary}>Add another provider? </Text>
287
+ <Text color={primary} bold>y</Text>
288
+ <Text color={muted}>/</Text>
289
+ <Text color={primary} bold>n</Text>
290
+ </Box>
291
+ </>
292
+ )}
293
+ </Box>
294
+ );
295
+ };
296
+
297
+ export default SetupWizard;
@@ -0,0 +1,155 @@
1
+ /**
2
+ * UpdatePrompt - 版本更新提示组件
3
+ *
4
+ *
5
+ * - Update now: 立即执行升级
6
+ * - Skip: 跳过本次提示
7
+ * - Skip until next version: 跳过当前版本的提示
8
+ */
9
+
10
+ import React, { useState } from 'react';
11
+ import { Box, Text, useInput } from 'ink';
12
+ import type { VersionCheckResult } from '../../../services/VersionChecker.js';
13
+ import {
14
+ setSkipUntilVersion,
15
+ getUpgradeCommand,
16
+ performUpgrade,
17
+ restartApp,
18
+ } from '../../../services/VersionChecker.js';
19
+
20
+ interface UpdatePromptProps {
21
+ versionInfo: VersionCheckResult;
22
+ onComplete: () => void;
23
+ }
24
+
25
+ type MenuOption = 'update' | 'skip' | 'skipUntil';
26
+
27
+ const menuOptions: { key: MenuOption; label: string }[] = [
28
+ { key: 'update', label: 'Update now' },
29
+ { key: 'skip', label: 'Skip' },
30
+ { key: 'skipUntil', label: 'Skip until next version' },
31
+ ];
32
+
33
+ export const UpdatePrompt: React.FC<UpdatePromptProps> = ({
34
+ versionInfo,
35
+ onComplete,
36
+ }) => {
37
+ const [selectedIndex, setSelectedIndex] = useState(0);
38
+ const [isUpdating, setIsUpdating] = useState(false);
39
+ const [updateResult, setUpdateResult] = useState<string | null>(null);
40
+
41
+ useInput(async (input, key) => {
42
+ if (isUpdating) return;
43
+
44
+ // 上下键选
45
+ if (key.upArrow) {
46
+ setSelectedIndex((prev) => (prev > 0 ? prev - 1 : menuOptions.length - 1));
47
+ return;
48
+ }
49
+ if (key.downArrow) {
50
+ setSelectedIndex((prev) => (prev < menuOptions.length - 1 ? prev + 1 : 0));
51
+ return;
52
+ }
53
+
54
+ // 数字键快速选
55
+ const numKey = parseInt(input, 10);
56
+ if (numKey >= 1 && numKey <= menuOptions.length) {
57
+ setSelectedIndex(numKey - 1);
58
+ return;
59
+ }
60
+
61
+ // Enter 确认选
62
+ if (key.return) {
63
+ const selected = menuOptions[selectedIndex];
64
+ await handleSelection(selected.key);
65
+ }
66
+ });
67
+
68
+ const handleSelection = async (option: MenuOption) => {
69
+ switch (option) {
70
+ case 'update':
71
+ setIsUpdating(true);
72
+ const result = await performUpgrade();
73
+ setUpdateResult(result.message);
74
+ if (result.success) {
75
+ // 升级成功,自动重启应
76
+ setTimeout(() => restartApp(), 1500);
77
+ } else {
78
+ // 升级失败,继续进入应
79
+ setTimeout(() => onComplete(), 2000);
80
+ }
81
+ break;
82
+
83
+ case 'skip':
84
+ onComplete();
85
+ break;
86
+
87
+ case 'skipUntil':
88
+ if (versionInfo.latestVersion) {
89
+ await setSkipUntilVersion(versionInfo.latestVersion);
90
+ }
91
+ onComplete();
92
+ break;
93
+ }
94
+ };
95
+
96
+ // 显示升级结
97
+ if (updateResult) {
98
+ return (
99
+ <Box flexDirection="column" padding={1}>
100
+ <Text>{updateResult}</Text>
101
+ </Box>
102
+ );
103
+ }
104
+
105
+ // 显示升级
106
+ if (isUpdating) {
107
+ return (
108
+ <Box flexDirection="column" padding={1}>
109
+ <Text color="yellow">⏳ Upgrading...</Text>
110
+ <Text color="gray">{getUpgradeCommand()}</Text>
111
+ </Box>
112
+ );
113
+ }
114
+
115
+ return (
116
+ <Box flexDirection="column" padding={1}>
117
+ {/* 标题 */}
118
+ <Box marginBottom={1}>
119
+ <Text bold color="cyan">
120
+ 🎉 New version available!
121
+ </Text>
122
+ </Box>
123
+
124
+ {/* 版本信息 */}
125
+ <Box marginBottom={1}>
126
+ <Text>
127
+ <Text color="gray">{versionInfo.currentVersion}</Text>
128
+ <Text color="gray"> → </Text>
129
+ <Text color="green" bold>{versionInfo.latestVersion}</Text>
130
+ </Text>
131
+ </Box>
132
+
133
+ {/* 菜单选项 */}
134
+ <Box flexDirection="column" marginBottom={1}>
135
+ {menuOptions.map((option, index) => (
136
+ <Box key={option.key}>
137
+ <Text color={selectedIndex === index ? 'cyan' : 'white'}>
138
+ {selectedIndex === index ? '❯ ' : ' '}
139
+ {index + 1}. {option.label}
140
+ </Text>
141
+ </Box>
142
+ ))}
143
+ </Box>
144
+
145
+ {/* 提示 */}
146
+ <Box>
147
+ <Text color="gray">
148
+ Use ↑↓ to navigate, Enter to select, or press 1-3
149
+ </Text>
150
+ </Box>
151
+ </Box>
152
+ );
153
+ };
154
+
155
+ export default UpdatePrompt;
@@ -0,0 +1,8 @@
1
+ /**
2
+ *
3
+ */
4
+
5
+ export { ConfirmationPrompt } from './ConfirmationPrompt.js';
6
+ export { UpdatePrompt } from './UpdatePrompt.js';
7
+ export { InteractiveSelector, type SelectorOption } from './InteractiveSelector.js';
8
+ export { SetupWizard } from './SetupWizard.js';
@@ -0,0 +1,28 @@
1
+ /**
2
+ * UI 组件导出
3
+ *
4
+ *
5
+ * - common/ 通用组件 (ErrorBoundary, LoadingIndicator)
6
+ * - input/ 输入组件 (CustomTextInput, InputArea)
7
+ * - markdown/ Markdown 渲染 (MessageRenderer, CodeHighlighter, parser)
8
+ * - dialog/ 对话框组件 (ConfirmationPrompt, UpdatePrompt)
9
+ * - layout/ 布局组件 (ChatStatusBar, MessageArea)
10
+ */
11
+
12
+ // 通用组
13
+ export * from './common/index.js';
14
+
15
+ // 输入组
16
+ export * from './input/index.js';
17
+
18
+ // Markdown 组
19
+ export * from './markdown/index.js';
20
+
21
+ // 对话框组
22
+ export * from './dialog/index.js';
23
+
24
+ // 布局组
25
+ export * from './layout/index.js';
26
+
27
+ // 主界
28
+ export { AegisInterface } from './AegisInterface.js';
@@ -0,0 +1,139 @@
1
+ /**
2
+ * CommandSuggestions - Dropdown autocomplete for slash commands
3
+ *
4
+ * Shows fuzzy-matched command suggestions when user types "/..."
5
+ */
6
+
7
+ import React, { useMemo, useEffect, useRef } from 'react';
8
+ import { Box, Text, useInput } from 'ink';
9
+ import { getCommandCompletions } from '../../../slash-commands/index.js';
10
+ import { themeManager } from '../../themes/index.js';
11
+ import { FocusId, focusManager } from '../../focus/index.js';
12
+
13
+ interface CommandSuggestionsProps {
14
+ /** The current input value */
15
+ input: string;
16
+ /** Cursor position */
17
+ cursorPosition: number;
18
+ /** Callback to set input value (for tab-complete) */
19
+ onSelectSuggestion: (suggestion: string) => void;
20
+ /** Whether suggestions are visible */
21
+ visible: boolean;
22
+ }
23
+
24
+ export const CommandSuggestions: React.FC<CommandSuggestionsProps> = ({
25
+ input,
26
+ cursorPosition,
27
+ onSelectSuggestion,
28
+ visible,
29
+ }) => {
30
+ const [selectedIndex, setSelectedIndex] = React.useState(0);
31
+ const theme = themeManager.getTheme();
32
+
33
+ // Reset selection when suggestions change
34
+ const lastInputRef = useRef(input);
35
+ useEffect(() => {
36
+ if (lastInputRef.current !== input) {
37
+ setSelectedIndex(0);
38
+ lastInputRef.current = input;
39
+ }
40
+ }, [input]);
41
+
42
+ const suggestions = useMemo(() => {
43
+ if (!visible || !input.startsWith('/')) return [];
44
+
45
+ const partial = input.slice(0, cursorPosition);
46
+ const results = getCommandCompletions(partial);
47
+ const isListing = partial.trim() === '/';
48
+
49
+ if (isListing) {
50
+ // Alphabetical when showing all commands
51
+ results.sort((a, b) => a.command.localeCompare(b.command));
52
+ } else {
53
+ results.sort((a, b) => (b.matchScore ?? 0) - (a.matchScore ?? 0));
54
+ }
55
+ return results;
56
+ }, [input, cursorPosition, visible]);
57
+
58
+ // Clamp selected index
59
+ const clampedIndex = Math.min(selectedIndex, Math.max(0, suggestions.length - 1));
60
+
61
+ // Tab key: cycle through suggestions
62
+ useInput(
63
+ (_, key) => {
64
+ if (!visible || suggestions.length === 0) return;
65
+ if (focusManager.getCurrentFocus() !== FocusId.MAIN_INPUT) return;
66
+
67
+ if (key.tab && !key.shift) {
68
+ // Select next suggestion (cycle)
69
+ const next = (clampedIndex + 1) % suggestions.length;
70
+ setSelectedIndex(next);
71
+ return;
72
+ }
73
+
74
+ if (key.tab && key.shift) {
75
+ // Select previous suggestion
76
+ const prev = clampedIndex <= 0 ? suggestions.length - 1 : clampedIndex - 1;
77
+ setSelectedIndex(prev);
78
+ return;
79
+ }
80
+
81
+ // Enter on a suggestion: select it
82
+ if (key.return && suggestions.length > 0) {
83
+ const selected = suggestions[clampedIndex];
84
+ if (selected) {
85
+ // Replace the command part of the input
86
+ const beforeCursor = input.slice(0, cursorPosition);
87
+ const afterCursor = input.slice(cursorPosition);
88
+ const slashIdx = beforeCursor.lastIndexOf('/');
89
+
90
+ if (slashIdx !== -1) {
91
+ const newBefore = beforeCursor.slice(0, slashIdx);
92
+ const completed = `${newBefore}${selected.command} `;
93
+ onSelectSuggestion(completed + afterCursor);
94
+ }
95
+ }
96
+ }
97
+ },
98
+ { isActive: visible && suggestions.length > 0 }
99
+ );
100
+
101
+ if (!visible || suggestions.length === 0) return null;
102
+
103
+ return (
104
+ <Box
105
+ flexDirection="column"
106
+ marginLeft={1}
107
+ marginBottom={0}
108
+ borderStyle="round"
109
+ borderColor={theme.colors.border.light}
110
+ paddingX={1}
111
+ paddingY={0}
112
+ >
113
+ <Text dimColor>Commands ({suggestions.length}):</Text>
114
+ {suggestions.map((s, i) => {
115
+ const isSelected = i === clampedIndex;
116
+ return (
117
+ <Box key={s.command} flexDirection="row">
118
+ <Text>
119
+ {isSelected ? (
120
+ <Text color={theme.colors.primary} bold>{'>'}</Text>
121
+ ) : (
122
+ <Text> </Text>
123
+ )}{' '}
124
+ <Text color={isSelected ? theme.colors.primary : undefined} bold={isSelected}>
125
+ {s.command}
126
+ </Text>
127
+ <Text color={theme.colors.text.muted}> — {s.description}</Text>
128
+ </Text>
129
+ </Box>
130
+ );
131
+ })}
132
+ <Text dimColor>
133
+ Tab/Shift+Tab to navigate · Enter to select
134
+ </Text>
135
+ </Box>
136
+ );
137
+ };
138
+
139
+ export default CommandSuggestions;