illusion-code 0.1.0__py3-none-any.whl

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 (214) hide show
  1. illusion/__init__.py +24 -0
  2. illusion/__main__.py +15 -0
  3. illusion/_frontend/dist/index.mjs +39208 -0
  4. illusion/_frontend/package.json +27 -0
  5. illusion/_frontend/src/App.tsx +624 -0
  6. illusion/_frontend/src/components/CommandPicker.tsx +98 -0
  7. illusion/_frontend/src/components/Composer.tsx +55 -0
  8. illusion/_frontend/src/components/ComposerController.tsx +128 -0
  9. illusion/_frontend/src/components/ConversationView.tsx +750 -0
  10. illusion/_frontend/src/components/Footer.tsx +25 -0
  11. illusion/_frontend/src/components/MarkdownContent.tsx +537 -0
  12. illusion/_frontend/src/components/MarkdownTable.tsx +245 -0
  13. illusion/_frontend/src/components/ModalHost.tsx +425 -0
  14. illusion/_frontend/src/components/MultilineTextInput.tsx +250 -0
  15. illusion/_frontend/src/components/PromptInput.tsx +64 -0
  16. illusion/_frontend/src/components/SelectModal.tsx +78 -0
  17. illusion/_frontend/src/components/SidePanel.tsx +175 -0
  18. illusion/_frontend/src/components/Spinner.tsx +77 -0
  19. illusion/_frontend/src/components/StatusBar.tsx +142 -0
  20. illusion/_frontend/src/components/SwarmPanel.tsx +141 -0
  21. illusion/_frontend/src/components/TodoPanel.tsx +126 -0
  22. illusion/_frontend/src/components/ToolCallDisplay.tsx +202 -0
  23. illusion/_frontend/src/components/TranscriptPane.tsx +79 -0
  24. illusion/_frontend/src/components/WelcomeBanner.tsx +37 -0
  25. illusion/_frontend/src/hooks/useBackendSession.ts +468 -0
  26. illusion/_frontend/src/hooks/useTerminalSize.ts +9 -0
  27. illusion/_frontend/src/i18n.ts +78 -0
  28. illusion/_frontend/src/index.tsx +42 -0
  29. illusion/_frontend/src/theme/ThemeContext.tsx +19 -0
  30. illusion/_frontend/src/theme/builtinThemes.ts +89 -0
  31. illusion/_frontend/src/types.ts +110 -0
  32. illusion/_frontend/src/utils/markdown.ts +33 -0
  33. illusion/_frontend/src/utils/thinking.ts +191 -0
  34. illusion/_frontend/tsconfig.json +13 -0
  35. illusion/_web_dist/assets/index-BseIw-ik.css +10 -0
  36. illusion/_web_dist/assets/index-C_0ZWMuW.js +82 -0
  37. illusion/_web_dist/index.html +16 -0
  38. illusion/api/__init__.py +36 -0
  39. illusion/api/client.py +568 -0
  40. illusion/api/codex_client.py +563 -0
  41. illusion/api/compat.py +138 -0
  42. illusion/api/effort.py +128 -0
  43. illusion/api/errors.py +57 -0
  44. illusion/api/openai_client.py +819 -0
  45. illusion/api/provider.py +148 -0
  46. illusion/api/registry.py +479 -0
  47. illusion/api/usage.py +45 -0
  48. illusion/auth/__init__.py +50 -0
  49. illusion/auth/copilot.py +419 -0
  50. illusion/auth/external.py +612 -0
  51. illusion/auth/flows.py +58 -0
  52. illusion/auth/manager.py +214 -0
  53. illusion/auth/storage.py +372 -0
  54. illusion/bridge/__init__.py +38 -0
  55. illusion/bridge/manager.py +190 -0
  56. illusion/bridge/session_runner.py +84 -0
  57. illusion/bridge/types.py +113 -0
  58. illusion/bridge/work_secret.py +131 -0
  59. illusion/cli.py +1228 -0
  60. illusion/commands/__init__.py +32 -0
  61. illusion/commands/registry.py +1934 -0
  62. illusion/config/__init__.py +39 -0
  63. illusion/config/i18n.py +522 -0
  64. illusion/config/paths.py +259 -0
  65. illusion/config/settings.py +564 -0
  66. illusion/coordinator/__init__.py +41 -0
  67. illusion/coordinator/agent_definitions.py +1093 -0
  68. illusion/coordinator/coordinator_mode.py +127 -0
  69. illusion/engine/__init__.py +95 -0
  70. illusion/engine/cost_tracker.py +55 -0
  71. illusion/engine/messages.py +369 -0
  72. illusion/engine/query.py +632 -0
  73. illusion/engine/query_engine.py +343 -0
  74. illusion/engine/stream_events.py +169 -0
  75. illusion/hooks/__init__.py +67 -0
  76. illusion/hooks/events.py +43 -0
  77. illusion/hooks/executor.py +397 -0
  78. illusion/hooks/hot_reload.py +74 -0
  79. illusion/hooks/loader.py +133 -0
  80. illusion/hooks/schemas.py +121 -0
  81. illusion/hooks/types.py +86 -0
  82. illusion/mcp/__init__.py +104 -0
  83. illusion/mcp/client.py +377 -0
  84. illusion/mcp/config.py +140 -0
  85. illusion/mcp/types.py +175 -0
  86. illusion/memory/__init__.py +36 -0
  87. illusion/memory/manager.py +94 -0
  88. illusion/memory/memdir.py +58 -0
  89. illusion/memory/paths.py +57 -0
  90. illusion/memory/scan.py +120 -0
  91. illusion/memory/search.py +83 -0
  92. illusion/memory/types.py +43 -0
  93. illusion/output_styles/__init__.py +15 -0
  94. illusion/output_styles/loader.py +64 -0
  95. illusion/permissions/__init__.py +39 -0
  96. illusion/permissions/checker.py +174 -0
  97. illusion/permissions/modes.py +38 -0
  98. illusion/platforms.py +148 -0
  99. illusion/plugins/__init__.py +71 -0
  100. illusion/plugins/bundled/__init__.py +0 -0
  101. illusion/plugins/installer.py +59 -0
  102. illusion/plugins/loader.py +301 -0
  103. illusion/plugins/schemas.py +51 -0
  104. illusion/plugins/types.py +56 -0
  105. illusion/prompts/__init__.py +29 -0
  106. illusion/prompts/claudemd.py +74 -0
  107. illusion/prompts/context.py +187 -0
  108. illusion/prompts/environment.py +189 -0
  109. illusion/prompts/system_prompt.py +155 -0
  110. illusion/py.typed +0 -0
  111. illusion/sandbox/__init__.py +29 -0
  112. illusion/sandbox/adapter.py +174 -0
  113. illusion/services/__init__.py +59 -0
  114. illusion/services/compact/__init__.py +1015 -0
  115. illusion/services/cron.py +338 -0
  116. illusion/services/cron_scheduler.py +715 -0
  117. illusion/services/file_history.py +258 -0
  118. illusion/services/lsp/__init__.py +455 -0
  119. illusion/services/session_storage.py +237 -0
  120. illusion/services/token_estimation.py +72 -0
  121. illusion/skills/__init__.py +60 -0
  122. illusion/skills/bundled/__init__.py +110 -0
  123. illusion/skills/bundled/content/batch.md +86 -0
  124. illusion/skills/bundled/content/coding-guidelines.md +70 -0
  125. illusion/skills/bundled/content/debug.md +38 -0
  126. illusion/skills/bundled/content/loop.md +82 -0
  127. illusion/skills/bundled/content/remember.md +105 -0
  128. illusion/skills/bundled/content/simplify.md +53 -0
  129. illusion/skills/bundled/content/skillify.md +113 -0
  130. illusion/skills/bundled/content/stuck.md +54 -0
  131. illusion/skills/bundled/content/update-config.md +329 -0
  132. illusion/skills/bundled/content/verify.md +74 -0
  133. illusion/skills/loader.py +219 -0
  134. illusion/skills/registry.py +40 -0
  135. illusion/skills/types.py +24 -0
  136. illusion/state/__init__.py +18 -0
  137. illusion/state/app_state.py +67 -0
  138. illusion/state/store.py +93 -0
  139. illusion/swarm/__init__.py +71 -0
  140. illusion/swarm/agent_executor.py +857 -0
  141. illusion/swarm/in_process.py +259 -0
  142. illusion/swarm/subprocess_backend.py +136 -0
  143. illusion/swarm/team_helpers.py +123 -0
  144. illusion/swarm/types.py +159 -0
  145. illusion/swarm/worktree.py +347 -0
  146. illusion/tasks/__init__.py +33 -0
  147. illusion/tasks/local_agent_task.py +42 -0
  148. illusion/tasks/local_shell_task.py +27 -0
  149. illusion/tasks/manager.py +377 -0
  150. illusion/tasks/stop_task.py +21 -0
  151. illusion/tasks/types.py +88 -0
  152. illusion/tools/__init__.py +126 -0
  153. illusion/tools/agent_tool.py +388 -0
  154. illusion/tools/ask_user_question_tool.py +186 -0
  155. illusion/tools/base.py +149 -0
  156. illusion/tools/bash_tool.py +413 -0
  157. illusion/tools/config_tool.py +90 -0
  158. illusion/tools/cron_tool.py +473 -0
  159. illusion/tools/enter_plan_mode_tool.py +147 -0
  160. illusion/tools/enter_worktree_tool.py +188 -0
  161. illusion/tools/exit_plan_mode_tool.py +69 -0
  162. illusion/tools/exit_worktree_tool.py +225 -0
  163. illusion/tools/file_edit_tool.py +283 -0
  164. illusion/tools/file_read_tool.py +294 -0
  165. illusion/tools/file_write_tool.py +184 -0
  166. illusion/tools/glob_tool.py +165 -0
  167. illusion/tools/grep_tool.py +190 -0
  168. illusion/tools/list_mcp_resources_tool.py +80 -0
  169. illusion/tools/lsp_tool.py +333 -0
  170. illusion/tools/mcp_auth_tool.py +100 -0
  171. illusion/tools/mcp_tool.py +75 -0
  172. illusion/tools/notebook_edit_tool.py +242 -0
  173. illusion/tools/powershell_tool.py +334 -0
  174. illusion/tools/read_mcp_resource_tool.py +63 -0
  175. illusion/tools/repl_tool.py +100 -0
  176. illusion/tools/send_message_tool.py +112 -0
  177. illusion/tools/shell_common.py +187 -0
  178. illusion/tools/skill_tool.py +86 -0
  179. illusion/tools/sleep_tool.py +62 -0
  180. illusion/tools/structured_output_tool.py +58 -0
  181. illusion/tools/task_create_tool.py +98 -0
  182. illusion/tools/task_get_tool.py +94 -0
  183. illusion/tools/task_list_tool.py +94 -0
  184. illusion/tools/task_output_tool.py +55 -0
  185. illusion/tools/task_stop_tool.py +52 -0
  186. illusion/tools/task_update_tool.py +224 -0
  187. illusion/tools/team_create_tool.py +236 -0
  188. illusion/tools/team_delete_tool.py +104 -0
  189. illusion/tools/todo_write_tool.py +198 -0
  190. illusion/tools/tool_search_tool.py +156 -0
  191. illusion/tools/web_fetch_tool.py +264 -0
  192. illusion/tools/web_search_tool.py +186 -0
  193. illusion/ui/__init__.py +23 -0
  194. illusion/ui/app.py +258 -0
  195. illusion/ui/backend_host.py +1180 -0
  196. illusion/ui/input.py +86 -0
  197. illusion/ui/output.py +363 -0
  198. illusion/ui/permission_dialog.py +47 -0
  199. illusion/ui/permission_store.py +99 -0
  200. illusion/ui/protocol.py +384 -0
  201. illusion/ui/react_launcher.py +280 -0
  202. illusion/ui/runtime.py +787 -0
  203. illusion/ui/textual_app.py +603 -0
  204. illusion/ui/web/__init__.py +10 -0
  205. illusion/ui/web/server.py +87 -0
  206. illusion/ui/web/ws_host.py +1197 -0
  207. illusion/utils/__init__.py +0 -0
  208. illusion/utils/ripgrep.py +299 -0
  209. illusion/utils/shell.py +248 -0
  210. illusion_code-0.1.0.dist-info/METADATA +1159 -0
  211. illusion_code-0.1.0.dist-info/RECORD +214 -0
  212. illusion_code-0.1.0.dist-info/WHEEL +4 -0
  213. illusion_code-0.1.0.dist-info/entry_points.txt +2 -0
  214. illusion_code-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "@illusion/terminal",
3
+ "private": true,
4
+ "type": "module",
5
+ "scripts": {
6
+ "start": "tsx src/index.tsx",
7
+ "build": "node build.mjs"
8
+ },
9
+ "dependencies": {
10
+ "chalk": "^5.6.2",
11
+ "ink": "^5.1.0",
12
+ "ink-text-input": "^6.0.0",
13
+ "marked": "^18.0.2",
14
+ "react": "^18.3.1",
15
+ "string-width": "^8.2.0",
16
+ "strip-ansi": "^7.2.0",
17
+ "wrap-ansi": "^10.0.0"
18
+ },
19
+ "devDependencies": {
20
+ "@types/node": "^22.13.10",
21
+ "@types/react": "^18.3.12",
22
+ "@types/strip-ansi": "^3.0.0",
23
+ "esbuild": "^0.25.0",
24
+ "tsx": "^4.19.2",
25
+ "typescript": "^5.7.3"
26
+ }
27
+ }
@@ -0,0 +1,624 @@
1
+ import React, {useEffect, useMemo, useState} from 'react';
2
+ import {Box, Text, useApp, useInput} from 'ink';
3
+
4
+ import {CommandPicker} from './components/CommandPicker.js';
5
+ import {ConversationView} from './components/ConversationView.js';
6
+ import {ModalHost} from './components/ModalHost.js';
7
+ import {PromptInput} from './components/PromptInput.js';
8
+ import {SelectModal, type SelectOption} from './components/SelectModal.js';
9
+ import {Spinner} from './components/Spinner.js';
10
+ import {StatusBar} from './components/StatusBar.js';
11
+ import {SwarmPanel} from './components/SwarmPanel.js';
12
+ import {TodoPanel} from './components/TodoPanel.js';
13
+ import {useBackendSession} from './hooks/useBackendSession.js';
14
+ import {normalizeLanguage, t} from './i18n.js';
15
+ import {ThemeProvider, useTheme} from './theme/ThemeContext.js';
16
+ import type {FrontendConfig} from './types.js';
17
+
18
+ const rawReturnSubmit = process.env.ILLUSION_FRONTEND_RAW_RETURN === '1';
19
+ const scriptedSteps = (() => {
20
+ const raw = process.env.ILLUSION_FRONTEND_SCRIPT;
21
+ if (!raw) {
22
+ return [] as string[];
23
+ }
24
+ try {
25
+ const parsed = JSON.parse(raw);
26
+ return Array.isArray(parsed) ? parsed.filter((item): item is string => typeof item === 'string') : [];
27
+ } catch {
28
+ return [];
29
+ }
30
+ })();
31
+
32
+ const PERMISSION_MODES: SelectOption[] = [
33
+ {value: 'default', label: 'Default', description: 'Ask before write/execute operations'},
34
+ {value: 'full_auto', label: 'Auto', description: 'Allow all tools automatically'},
35
+ {value: 'plan', label: 'Plan Mode', description: 'Block all write operations'},
36
+ ];
37
+
38
+ type SelectModalState = {
39
+ title: string;
40
+ options: SelectOption[];
41
+ onSelect: (value: string) => void;
42
+ } | null;
43
+
44
+ const PERMISSION_PROMPT_OPTIONS: SelectOption[] = [
45
+ {value: 'allow', label: 'Allow', description: 'Approve this tool execution'},
46
+ {value: 'always', label: 'Always Allow', description: 'Always allow this tool without asking again'},
47
+ {value: 'deny', label: 'Deny', description: 'Reject this tool execution'},
48
+ ];
49
+
50
+
51
+ export function App({config}: {config: FrontendConfig}): React.JSX.Element {
52
+ return (
53
+ <ThemeProvider>
54
+ <AppInner config={config} />
55
+ </ThemeProvider>
56
+ );
57
+ }
58
+
59
+ function AppInner({config}: {config: FrontendConfig}): React.JSX.Element {
60
+ const {exit} = useApp();
61
+ const theme = useTheme();
62
+ const [input, setInput] = useState('');
63
+ const [modalInput, setModalInput] = useState('');
64
+ const [scriptIndex, setScriptIndex] = useState(0);
65
+ const [pickerIndex, setPickerIndex] = useState(0);
66
+ const [selectModal, setSelectModal] = useState<SelectModalState>(null);
67
+ const [selectIndex, setSelectIndex] = useState(0);
68
+ const [permissionIndex, setPermissionIndex] = useState(2);
69
+ const [pendingPermissionAck, setPendingPermissionAck] = useState(false);
70
+ const [cursorReset, setCursorReset] = useState(0);
71
+ const session = useBackendSession(config, () => exit());
72
+ const isPermissionModal = session.modal?.kind === 'permission';
73
+ const language = normalizeLanguage(session.status.ui_language);
74
+ const permissionRequestId =
75
+ isPermissionModal && typeof session.modal?.request_id === 'string' ? String(session.modal.request_id) : '';
76
+ const localizedPermissionOptions = PERMISSION_PROMPT_OPTIONS.map((opt) => {
77
+ if (opt.value === 'allow') {
78
+ return {...opt, label: t(language, 'allow')};
79
+ }
80
+ if (opt.value === 'always') {
81
+ return {...opt, label: t(language, 'alwaysAllow')};
82
+ }
83
+ return {...opt, label: t(language, 'deny')};
84
+ });
85
+
86
+ // Current tool name for spinner
87
+ const currentToolName = useMemo(() => {
88
+ // 优先检查 pendingToolCalls(工具调用刚开始,参数尚未到达)
89
+ if (session.pendingToolCalls.length > 0) {
90
+ return session.pendingToolCalls[session.pendingToolCalls.length - 1].tool_name;
91
+ }
92
+ for (let i = session.staticItems.length - 1; i >= 0; i--) {
93
+ const item = session.staticItems[i];
94
+ if (item.role === 'tool') {
95
+ return item.tool_name ?? 'tool';
96
+ }
97
+ if (item.role === 'tool_result' || item.role === 'assistant') {
98
+ break;
99
+ }
100
+ }
101
+ return undefined;
102
+ }, [session.staticItems, session.pendingToolCalls]);
103
+
104
+ // Command hints
105
+ const commandHints = useMemo(() => {
106
+ if (!input.startsWith('/')) {
107
+ return [] as string[];
108
+ }
109
+ const value = input.trimEnd();
110
+ if (value === '') {
111
+ return [] as string[];
112
+ }
113
+ const matches = session.commands.filter((cmd) => cmd.startsWith(value));
114
+ if (value === '/') {
115
+ const preferred = ['/language'];
116
+ const boosted = preferred.filter((cmd) => matches.includes(cmd));
117
+ const rest = matches.filter((cmd) => !preferred.includes(cmd));
118
+ return [...boosted, ...rest];
119
+ }
120
+ return matches;
121
+ }, [session.commands, input]);
122
+
123
+ const canShowPicker = input.startsWith('/') && commandHints.length > 0;
124
+ const showPicker = canShowPicker && !session.busy && !session.modal && !selectModal;
125
+
126
+ useEffect(() => {
127
+ setPickerIndex(0);
128
+ }, [canShowPicker, commandHints.length, input]);
129
+
130
+ // Handle backend-initiated select requests (e.g. /resume session list)
131
+ useEffect(() => {
132
+ if (!session.selectRequest) {
133
+ return;
134
+ }
135
+ const req = session.selectRequest;
136
+ if (req.options.length === 0) {
137
+ session.setSelectRequest(null);
138
+ return;
139
+ }
140
+ setSelectIndex(0);
141
+ setSelectModal({
142
+ title: req.title,
143
+ options: req.options.map((o) => ({value: o.value, label: o.label, description: o.description})),
144
+ onSelect: (value) => {
145
+ session.sendRequest({type: 'apply_select_command', command: req.command, value});
146
+ session.setBusy(true);
147
+ setSelectModal(null);
148
+ },
149
+ });
150
+ session.setSelectRequest(null);
151
+ }, [session.selectRequest]);
152
+
153
+ useEffect(() => {
154
+ if (!isPermissionModal) {
155
+ setPendingPermissionAck(false);
156
+ return;
157
+ }
158
+ setPermissionIndex(1);
159
+ setPendingPermissionAck(false);
160
+ }, [permissionRequestId, isPermissionModal]);
161
+
162
+ // Intercept special commands that need interactive UI
163
+ const handleCommand = (cmd: string): boolean => {
164
+ const trimmed = cmd.trim();
165
+
166
+ // /permissions → show mode picker
167
+ if (trimmed === '/permissions' || trimmed === '/permissions show') {
168
+ const currentMode = String(session.status.permission_mode ?? 'default');
169
+ const options = PERMISSION_MODES.map((opt) => ({
170
+ ...opt,
171
+ active: opt.value === currentMode,
172
+ }));
173
+ const initialIndex = options.findIndex((o) => o.active);
174
+ setSelectIndex(initialIndex >= 0 ? initialIndex : 0);
175
+ setSelectModal({
176
+ title: 'Permission Mode',
177
+ options,
178
+ onSelect: (value) => {
179
+ session.sendRequest({type: 'submit_line', line: `/permissions set ${value}`});
180
+ session.setBusy(true);
181
+ setSelectModal(null);
182
+ },
183
+ });
184
+ return true;
185
+ }
186
+
187
+ if (trimmed === '/language' || trimmed === '/language show') {
188
+ const current = normalizeLanguage(session.status.ui_language);
189
+ const options: SelectOption[] = [
190
+ {value: 'set zh-CN', label: t(current, 'langZh'), description: '中文界面', active: current === 'zh-CN'},
191
+ {value: 'set en', label: t(current, 'langEn'), description: 'English UI', active: current === 'en'},
192
+ ];
193
+ const initialIndex = options.findIndex((o) => o.active);
194
+ setSelectIndex(initialIndex >= 0 ? initialIndex : 0);
195
+ setSelectModal({
196
+ title: t(current, 'language'),
197
+ options,
198
+ onSelect: (value) => {
199
+ session.sendRequest({type: 'submit_line', line: `/language ${value}`});
200
+ session.setBusy(true);
201
+ setSelectModal(null);
202
+ },
203
+ });
204
+ return true;
205
+ }
206
+
207
+ // /plan → toggle plan mode
208
+ if (trimmed === '/plan') {
209
+ const currentMode = String(session.status.permission_mode ?? 'default');
210
+ if (currentMode === 'plan') {
211
+ session.sendRequest({type: 'submit_line', line: '/plan off'});
212
+ } else {
213
+ session.sendRequest({type: 'submit_line', line: '/plan on'});
214
+ }
215
+ session.setBusy(true);
216
+ return true;
217
+ }
218
+
219
+ // /resume → request session list from backend (will trigger select_request)
220
+ if (trimmed === '/resume') {
221
+ session.sendRequest({type: 'list_sessions'});
222
+ return true;
223
+ }
224
+
225
+ // /model → show model selector dropdown
226
+ if (trimmed === '/model' || trimmed === '/model show') {
227
+ session.sendRequest({type: 'select_command', command: 'model'});
228
+ return true;
229
+ }
230
+
231
+ // /rewind → show message selector to pick rewind point
232
+ if (trimmed === '/rewind') {
233
+ session.sendRequest({type: 'select_command', command: 'rewind'});
234
+ return true;
235
+ }
236
+
237
+ // /delete → show session picker for deletion
238
+ if (trimmed === '/delete') {
239
+ session.sendRequest({type: 'select_command', command: 'delete'});
240
+ return true;
241
+ }
242
+
243
+ // /rules → show rule picker
244
+ if (trimmed === '/rules') {
245
+ session.sendRequest({type: 'select_command', command: 'rules'});
246
+ return true;
247
+ }
248
+
249
+ // /context → show context management selector
250
+ if (trimmed === '/context') {
251
+ session.sendRequest({type: 'select_command', command: 'context'});
252
+ return true;
253
+ }
254
+
255
+ // /new → clear conversation window and start fresh session
256
+ if (trimmed === '/new' || trimmed === '/clear') {
257
+ session.sendRequest({type: 'submit_line', line: '/new'});
258
+ session.setBusy(true);
259
+ return true;
260
+ }
261
+
262
+ // /version → 显示版本信息(前端处理,不发送到后端)
263
+ if (trimmed === '/version') {
264
+ session.setCommandResult({
265
+ text: 'IllusionCode 0.1.0',
266
+ type: 'info',
267
+ });
268
+ return true;
269
+ }
270
+
271
+ return false;
272
+ };
273
+
274
+ useInput((chunk, key) => {
275
+ // Ctrl+C → 退出程序
276
+ if (key.ctrl && chunk === 'c') {
277
+ session.sendRequest({type: 'shutdown'});
278
+ exit();
279
+ return;
280
+ }
281
+ // Ctrl+X → 停止当前任务
282
+ if (key.ctrl && chunk.toLowerCase() === 'x') {
283
+ if (session.busy) {
284
+ session.sendRequest({type: 'stop'});
285
+ session.pushStatic({role: 'system', text: ' '});
286
+ session.setCommandResult({
287
+ text: t(language, 'taskStopped'),
288
+ type: 'info',
289
+ });
290
+ }
291
+ return;
292
+ }
293
+ // Ctrl+O → 将完整结果内容显示在对话中(不发送到 AI)
294
+ if (key.ctrl && chunk.toLowerCase() === 'o' && session.commandResult) {
295
+ session.pushStatic({role: 'system', text: session.commandResult.text});
296
+ session.setCommandResult(null);
297
+ return;
298
+ }
299
+
300
+ // ESC → 清除指令结果
301
+ if (key.escape && session.commandResult) {
302
+ session.setCommandResult(null);
303
+ return;
304
+ }
305
+
306
+ // --- Select modal (permissions picker etc.) ---
307
+ if (selectModal) {
308
+ if (key.upArrow) {
309
+ setSelectIndex((i) => Math.max(0, i - 1));
310
+ return;
311
+ }
312
+ if (key.downArrow) {
313
+ setSelectIndex((i) => Math.min(selectModal.options.length - 1, i + 1));
314
+ return;
315
+ }
316
+ if (key.return) {
317
+ const selected = selectModal.options[selectIndex];
318
+ if (selected) {
319
+ selectModal.onSelect(selected.value);
320
+ }
321
+ return;
322
+ }
323
+ if (key.escape) {
324
+ setSelectModal(null);
325
+ session.setBusy(false);
326
+ return;
327
+ }
328
+ // Number keys for quick selection
329
+ const num = parseInt(chunk, 10);
330
+ if (num >= 1 && num <= selectModal.options.length) {
331
+ const selected = selectModal.options[num - 1];
332
+ if (selected) {
333
+ selectModal.onSelect(selected.value);
334
+ }
335
+ return;
336
+ }
337
+ return;
338
+ }
339
+
340
+ // --- Scripted raw return ---
341
+ if (rawReturnSubmit && key.return) {
342
+ if (session.modal?.kind === 'question') {
343
+ session.sendRequest({
344
+ type: 'question_response',
345
+ request_id: session.modal.request_id,
346
+ answer: modalInput,
347
+ });
348
+ session.setModal(null);
349
+ setModalInput('');
350
+ return;
351
+ }
352
+ if (!session.modal && !session.busy && input.trim()) {
353
+ onSubmit(input);
354
+ return;
355
+ }
356
+ }
357
+
358
+ // --- Permission modal (MUST be before busy check — modal appears while busy) ---
359
+ if (isPermissionModal) {
360
+ if (pendingPermissionAck) {
361
+ return;
362
+ }
363
+ if (key.upArrow || key.downArrow) {
364
+ setPermissionIndex((i) => {
365
+ if (key.upArrow) return i <= 0 ? 2 : i - 1;
366
+ return i >= 2 ? 0 : i + 1;
367
+ });
368
+ return;
369
+ }
370
+ if (key.return || key.escape) {
371
+ if (!permissionRequestId) {
372
+ return;
373
+ }
374
+ const selected = key.escape ? 'deny' : localizedPermissionOptions[permissionIndex]?.value;
375
+ const allowed = selected === 'allow' || selected === 'always';
376
+ session.sendRequest({
377
+ type: 'permission_response',
378
+ request_id: permissionRequestId,
379
+ allowed,
380
+ always_allow: selected === 'always',
381
+ tool_name: String(session.modal?.tool_name ?? ''),
382
+ });
383
+ setPendingPermissionAck(true);
384
+ return;
385
+ }
386
+ return;
387
+ }
388
+
389
+ // --- Question modal (also appears while busy) ---
390
+ if (session.modal?.kind === 'question') {
391
+ return;
392
+ }
393
+
394
+ // --- Ignore input while busy ---
395
+ if (session.busy) {
396
+ return;
397
+ }
398
+
399
+ // --- Command picker ---
400
+ if (showPicker) {
401
+ if (key.upArrow) {
402
+ setPickerIndex((i) => Math.max(0, i - 1));
403
+ return;
404
+ }
405
+ if (key.downArrow) {
406
+ setPickerIndex((i) => Math.min(commandHints.length - 1, i + 1));
407
+ return;
408
+ }
409
+ if (key.return) {
410
+ const selected = commandHints[pickerIndex];
411
+ if (selected) {
412
+ setInput('');
413
+ setCursorReset((c) => c + 1);
414
+ if (!handleCommand(selected)) {
415
+ onSubmit(selected);
416
+ }
417
+ }
418
+ return;
419
+ }
420
+ if (key.tab) {
421
+ const selected = commandHints[pickerIndex];
422
+ if (selected) {
423
+ setInput(selected + ' ');
424
+ setCursorReset((c) => c + 1);
425
+ }
426
+ return;
427
+ }
428
+ if (key.escape) {
429
+ setInput('');
430
+ setCursorReset((c) => c + 1);
431
+ return;
432
+ }
433
+ }
434
+
435
+ // Note: normal Enter submission is handled by TextInput's onSubmit in
436
+ // PromptInput. Do NOT duplicate it here — that causes double requests.
437
+ });
438
+
439
+ const onSubmit = (value: string): void => {
440
+ if (session.modal?.kind === 'question') {
441
+ session.sendRequest({
442
+ type: 'question_response',
443
+ request_id: session.modal.request_id,
444
+ answer: value,
445
+ });
446
+ session.setModal(null);
447
+ setModalInput('');
448
+ return;
449
+ }
450
+ const trimmed = value.trim();
451
+ if (!trimmed || session.busy || !session.ready) {
452
+ return;
453
+ }
454
+ // Check if it's an interactive command
455
+ if (handleCommand(trimmed)) {
456
+ setInput('');
457
+ return;
458
+ }
459
+ session.sendRequest({type: 'submit_line', line: trimmed});
460
+ setInput('');
461
+ session.setBusy(true);
462
+ };
463
+
464
+ // 指令结果自动消失:3 秒后清除
465
+ useEffect(() => {
466
+ if (!session.commandResult) {
467
+ return;
468
+ }
469
+ const timer = setTimeout(() => {
470
+ session.setCommandResult(null);
471
+ }, 3000);
472
+ return () => clearTimeout(timer);
473
+ }, [session.commandResult]);
474
+
475
+ // Scripted automation
476
+ useEffect(() => {
477
+ if (scriptIndex >= scriptedSteps.length) {
478
+ return;
479
+ }
480
+ if (session.busy || session.modal || selectModal) {
481
+ return;
482
+ }
483
+ const step = scriptedSteps[scriptIndex];
484
+ const timer = setTimeout(() => {
485
+ onSubmit(step);
486
+ setScriptIndex((index) => index + 1);
487
+ }, 200);
488
+ return () => clearTimeout(timer);
489
+ }, [scriptIndex, session.busy, session.modal, selectModal]);
490
+
491
+ return (
492
+ <Box flexDirection="column" height="100%">
493
+ {/* Conversation area */}
494
+ <Box flexDirection="column" flexGrow={1}>
495
+ <ConversationView
496
+ staticItems={session.staticItems}
497
+ clearCount={session.clearCount}
498
+ assistantBuffer={session.assistantBuffer}
499
+ showWelcome={session.ready}
500
+ showThinking={session.showThinking}
501
+ language={language}
502
+ pendingToolCalls={session.pendingToolCalls}
503
+ commandPickerOpen={showPicker}
504
+ />
505
+ </Box>
506
+
507
+ <Box flexDirection="column" paddingX={1}>
508
+ {/* Permission confirm modal */}
509
+ {isPermissionModal ? (
510
+ <SelectModal
511
+ title={`Allow ${String(session.modal?.tool_name ?? 'tool')}?`}
512
+ options={localizedPermissionOptions}
513
+ selectedIndex={permissionIndex}
514
+ />
515
+ ) : null}
516
+
517
+ {/* Backend modal (question, mcp auth) */}
518
+ {session.modal && !isPermissionModal ? (
519
+ <ModalHost
520
+ modal={session.modal}
521
+ modalInput={modalInput}
522
+ setModalInput={setModalInput}
523
+ onSubmit={onSubmit}
524
+ language={language}
525
+ />
526
+ ) : null}
527
+
528
+ {/* Frontend select modal (permissions picker, etc.) */}
529
+ {selectModal ? (
530
+ <SelectModal
531
+ title={selectModal.title}
532
+ options={selectModal.options}
533
+ selectedIndex={selectIndex}
534
+ />
535
+ ) : null}
536
+
537
+ {/* Command picker */}
538
+ {showPicker ? (
539
+ <CommandPicker hints={commandHints} selectedIndex={pickerIndex} totalCommands={session.commands.length} />
540
+ ) : null}
541
+
542
+ {/* Command result display */}
543
+ {session.commandResult ? (
544
+ <CommandPicker
545
+ mode="result"
546
+ result={session.commandResult.text}
547
+ resultType={session.commandResult.type}
548
+ />
549
+ ) : null}
550
+
551
+ {/* Todo panel */}
552
+ {session.ready && session.todoItems.length > 0 ? (
553
+ <TodoPanel items={session.todoItems} />
554
+ ) : null}
555
+
556
+ {/* Swarm panel */}
557
+ {session.ready && (session.swarmTeammates.length > 0 || session.swarmNotifications.length > 0) ? (
558
+ <SwarmPanel teammates={session.swarmTeammates} notifications={session.swarmNotifications} />
559
+ ) : null}
560
+
561
+ {/* Status bar (only after backend is ready) */}
562
+ {session.ready ? (
563
+ <StatusBar status={session.status} tasks={session.tasks} />
564
+ ) : null}
565
+
566
+ {/* Input — show loading indicator until backend is ready */}
567
+ {!session.ready ? (
568
+ <Box>
569
+ <Text color={theme.colors.warning}>{t(language, 'connecting')}</Text>
570
+ </Box>
571
+ ) : session.modal || selectModal || pendingPermissionAck ? null : session.busy ? (
572
+ <Box marginTop={1}>
573
+ <Spinner
574
+ label={session.bgAgentLabel ?? undefined}
575
+ todoItems={session.todoItems}
576
+ language={language}
577
+ toolName={currentToolName}
578
+ sessionId={String(session.status.session_id ?? '')}
579
+ />
580
+ </Box>
581
+ ) : (
582
+ <PromptInput
583
+ busy={session.busy}
584
+ input={input}
585
+ setInput={setInput}
586
+ onSubmit={onSubmit}
587
+ toolName={session.busy ? currentToolName : undefined}
588
+ suppressSubmit={showPicker}
589
+ cursorReset={cursorReset}
590
+ language={language}
591
+ todoItems={session.todoItems}
592
+ />
593
+ )}
594
+
595
+ {/* Keyboard hints (only after backend is ready) */}
596
+ {session.ready && !session.modal && !session.busy && !selectModal && !pendingPermissionAck ? (
597
+ <Box>
598
+ <Text dimColor>
599
+ <Text color={theme.colors.muted}>enter</Text> {t(language, 'send')}
600
+ <Text> {theme.icons.middleDot} </Text>
601
+ <Text color={theme.colors.muted}>/</Text> {t(language, 'commands')}
602
+ <Text> {theme.icons.middleDot} </Text>
603
+ <Text color={theme.colors.muted}>ctrl+c</Text> {t(language, 'exitProgram')}
604
+ <Text> {theme.icons.middleDot} </Text>
605
+ <Text color={theme.colors.muted}>ctrl+x</Text> {t(language, 'stopCurrentTask')}
606
+ <Text> {theme.icons.middleDot} </Text>
607
+ <Text color={theme.colors.muted}>ctrl+u</Text> {t(language, 'clearInput')}
608
+ <Text> {theme.icons.middleDot} </Text>
609
+ <Text color={theme.colors.muted}>ctrl+j</Text> {t(language, 'newline')}
610
+ </Text>
611
+ </Box>
612
+ ) : session.ready && session.busy && !session.modal && !selectModal ? (
613
+ <Box marginTop={1}>
614
+ <Text dimColor>
615
+ <Text color={theme.colors.muted}>ctrl+c</Text> {t(language, 'exitProgram')}
616
+ <Text> {theme.icons.middleDot} </Text>
617
+ <Text color={theme.colors.muted}>ctrl+x</Text> {t(language, 'stopCurrentTask')}
618
+ </Text>
619
+ </Box>
620
+ ) : null}
621
+ </Box>
622
+ </Box>
623
+ );
624
+ }