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,603 @@
1
+ """
2
+ Textual 终端 UI 模块
3
+ ==================
4
+
5
+ 本模块实现基于 Textual 框架的默认终端用户界面。
6
+
7
+ 主要功能:
8
+ - 交互式对话界面(Transcript 显示对话历史)
9
+ - 实时流式输出(Assistant 输出流式显示)
10
+ - 工具执行状态显示
11
+ - 侧边栏(状态、任务、MCP 服务器信息)
12
+ - 权限确认对话框(PermissionScreen)
13
+ - 用户问答对话框(QuestionScreen)
14
+
15
+ 类说明:
16
+ - AppConfig: 终端应用配置数据类
17
+ - PermissionScreen: 权限确认模态对话框
18
+ - QuestionScreen: 用户问答模态对话框
19
+ - illusionTerminalApp: 主终端应用类
20
+
21
+ 使用示例:
22
+ >>> from illusion.ui.textual_app import illusionTerminalApp
23
+ >>> app = illusionTerminalApp()
24
+ >>> app.run()
25
+ """
26
+
27
+ from __future__ import annotations
28
+
29
+ import asyncio
30
+ import json
31
+ from dataclasses import dataclass
32
+
33
+ from rich.panel import Panel
34
+ from textual import on
35
+ from textual.app import App, ComposeResult
36
+ from textual.binding import Binding
37
+ from textual.containers import Container, Horizontal, Vertical
38
+ from textual.screen import ModalScreen
39
+ from textual.widgets import Button, Checkbox, Footer, Header, Input, RadioButton, RadioSet, RichLog, Static
40
+
41
+ from illusion.api.client import SupportsStreamingMessages
42
+ from illusion.engine.stream_events import (
43
+ AssistantTextDelta,
44
+ AssistantTurnComplete,
45
+ ErrorEvent,
46
+ StatusEvent,
47
+ StreamEvent,
48
+ ToolExecutionCompleted,
49
+ ToolExecutionStarted,
50
+ )
51
+ from illusion.swarm.agent_executor import list_active_agents
52
+ from illusion.tasks import get_task_manager
53
+ from illusion.tasks.types import to_task_display_status
54
+ from illusion.ui.runtime import build_runtime, close_runtime, handle_line, start_runtime
55
+
56
+ # Agent 状态指示器颜色(与 agent_definitions.py 中的 AGENT_COLORS 一致)
57
+ _AGENT_INDICATOR_COLOR = "purple"
58
+
59
+
60
+ @dataclass(frozen=True)
61
+ class AppConfig:
62
+ """终端应用配置数据类。
63
+
64
+ 用于存储终端应用会话的配置参数。
65
+
66
+ Attributes:
67
+ prompt: 初始用户提示词
68
+ model: 使用的模型名称
69
+ base_url: API 基础 URL
70
+ system_prompt: 系统提示词
71
+ api_key: API 密钥
72
+ api_client: 流式 API 客户端实例
73
+ """
74
+
75
+ prompt: str | None = None
76
+ model: str | None = None
77
+ base_url: str | None = None
78
+ system_prompt: str | None = None
79
+ api_key: str | None = None
80
+ api_client: SupportsStreamingMessages | None = None
81
+
82
+
83
+ class PermissionScreen(ModalScreen[bool]):
84
+ """权限确认模态对话框。
85
+
86
+ 当工具需要用户确认时显示此对话框,让用户决定是否允许执行该工具。
87
+ 支持快捷键:Y=允许,N=拒绝,Escape=拒绝。
88
+
89
+ Attributes:
90
+ _tool_name: 请求执行的工具名称
91
+ _reason: 工具请求的原因说明
92
+ """
93
+
94
+ BINDINGS = [
95
+ Binding("escape", "deny", "Deny"),
96
+ Binding("y", "allow", "Allow"),
97
+ Binding("n", "deny", "Deny"),
98
+ ]
99
+
100
+ def __init__(self, tool_name: str, reason: str) -> None:
101
+ super().__init__()
102
+ self._tool_name = tool_name # 存储工具名称
103
+ self._reason = reason # 存储原因说明
104
+
105
+ def compose(self) -> ComposeResult:
106
+ yield Container(
107
+ Static(
108
+ Panel.fit(
109
+ f"Allow tool [bold]{self._tool_name}[/bold]?\n\n{self._reason}",
110
+ title="Permission Required",
111
+ )
112
+ ),
113
+ Horizontal(
114
+ Button("Allow", id="allow", variant="success"),
115
+ Button("Deny", id="deny", variant="error"),
116
+ classes="permission-actions",
117
+ ),
118
+ id="permission-dialog",
119
+ )
120
+
121
+ @on(Button.Pressed)
122
+ def handle_button_press(self, event: Button.Pressed) -> None:
123
+ # 根据按钮ID决定是否允许:allow=True, deny=False
124
+ self.dismiss(event.button.id == "allow")
125
+
126
+ def action_allow(self) -> None:
127
+ self.dismiss(True) # 允许执行
128
+
129
+ def action_deny(self) -> None:
130
+ self.dismiss(False) # 拒绝执行
131
+
132
+
133
+ class QuestionScreen(ModalScreen):
134
+ """用户问答模态对话框。
135
+
136
+ 支持两种模式:
137
+ - 结构化模式:当提供 questions 数据时,渲染单选(RadioSet)/多选(Checkbox)UI
138
+ - 文本模式:无结构化数据时,回退为文本输入框
139
+ """
140
+
141
+ BINDINGS = [
142
+ Binding("escape", "cancel", "Cancel"),
143
+ # Enter 不再全局绑定提交。多选时 Space 切换,Tab 到 Submit 按钮后回车提交。
144
+ ]
145
+
146
+ def __init__(self, question: str, questions_data: list | None = None) -> None:
147
+ super().__init__()
148
+ self._question = question
149
+ self._questions_data: list[dict] = []
150
+ if questions_data:
151
+ self._questions_data = [
152
+ q.model_dump() if hasattr(q, "model_dump") else q
153
+ for q in questions_data
154
+ ]
155
+
156
+ def compose(self) -> ComposeResult:
157
+ if self._questions_data:
158
+ yield from self._compose_structured()
159
+ else:
160
+ yield from self._compose_fallback()
161
+
162
+ def _compose_fallback(self) -> ComposeResult:
163
+ """文本输入模式(兼容旧行为)"""
164
+ yield Container(
165
+ Static(Panel.fit(self._question, title="Question")),
166
+ Input(placeholder="Type your answer", id="question-input"),
167
+ Horizontal(
168
+ Button("Submit", id="submit", variant="primary"),
169
+ Button("Cancel", id="cancel", variant="default"),
170
+ classes="permission-actions",
171
+ ),
172
+ id="permission-dialog",
173
+ )
174
+
175
+ def _compose_structured(self) -> ComposeResult:
176
+ """结构化选项模式:单选用 RadioSet,多选用 Checkbox"""
177
+ with Container(id="permission-dialog"):
178
+ for i, q in enumerate(self._questions_data):
179
+ header = q.get("header", "")
180
+ question_text = q.get("question", "")
181
+ options: list[dict] = q.get("options", [])
182
+ multi: bool = q.get("multiSelect", False)
183
+
184
+ title = f"[{header}] {question_text}" if header else question_text
185
+ yield Static(title, classes="question-title")
186
+ yield Static("─" * 40, classes="question-separator")
187
+
188
+ if multi:
189
+ for j, opt in enumerate(options):
190
+ desc = opt.get("description", "")
191
+ label = f"{opt['label']} — {desc}" if desc else opt["label"]
192
+ yield Checkbox(label, id=f"q_{i}_opt_{j}", classes="question-option")
193
+ else:
194
+ with RadioSet(id=f"q_{i}", classes="question-radioset"):
195
+ for j, opt in enumerate(options):
196
+ desc = opt.get("description", "")
197
+ label = f"{opt['label']} — {desc}" if desc else opt["label"]
198
+ yield RadioButton(label, id=f"q_{i}_opt_{j}")
199
+ yield Static("")
200
+
201
+ yield Horizontal(
202
+ Button("Submit", id="submit", variant="primary"),
203
+ Button("Cancel", id="cancel", variant="default"),
204
+ classes="permission-actions",
205
+ )
206
+ yield Static(
207
+ "空格=选中/取消 Tab=切换选项 聚焦[提交]后回车=确认 Esc=取消",
208
+ classes="question-hint",
209
+ )
210
+
211
+ def on_mount(self) -> None:
212
+ if not self._questions_data:
213
+ self.query_one("#question-input", Input).focus()
214
+
215
+ @on(Button.Pressed)
216
+ def handle_button_press(self, event: Button.Pressed) -> None:
217
+ if event.button.id == "cancel":
218
+ self.dismiss("")
219
+ return
220
+ if self._questions_data:
221
+ self.dismiss(self._collect_structured_answers())
222
+ else:
223
+ self.dismiss(self.query_one("#question-input", Input).value.strip())
224
+
225
+ @on(Input.Submitted, "#question-input")
226
+ def handle_input_submit(self, event: Input.Submitted) -> None:
227
+ self.dismiss(event.value.strip())
228
+
229
+ def action_submit(self) -> None:
230
+ if self._questions_data:
231
+ self.dismiss(self._collect_structured_answers())
232
+ else:
233
+ self.dismiss(self.query_one("#question-input", Input).value.strip())
234
+
235
+ def action_cancel(self) -> None:
236
+ self.dismiss("")
237
+
238
+ def _collect_structured_answers(self) -> dict[str, list[str] | str]:
239
+ """收集结构化问题的用户答案。"""
240
+ result: dict[str, list[str] | str] = {}
241
+ for i, q in enumerate(self._questions_data):
242
+ header = q.get("header", f"q{i}")
243
+ options: list[dict] = q.get("options", [])
244
+ multi: bool = q.get("multiSelect", False)
245
+
246
+ if multi:
247
+ selected: list[str] = []
248
+ for j, opt in enumerate(options):
249
+ cb = self.query_one(f"#q_{i}_opt_{j}", Checkbox)
250
+ if cb.value:
251
+ selected.append(opt["label"])
252
+ result[header] = selected
253
+ else:
254
+ rs = self.query_one(f"#q_{i}", RadioSet)
255
+ pressed = rs.pressed_button
256
+ if pressed is not None:
257
+ try:
258
+ opt_idx = int(str(pressed.id).rsplit("_", 1)[-1])
259
+ result[header] = options[opt_idx]["label"]
260
+ except (ValueError, IndexError):
261
+ result[header] = str(pressed.label).split(" — ")[0]
262
+ else:
263
+ result[header] = ""
264
+ return result
265
+
266
+
267
+ class illusionTerminalApp(App[None]):
268
+ """Textual 终端应用程序主类。
269
+
270
+ 提供基于 Textual 框架的交互式终端用户界面。
271
+ 支持快捷键:Ctrl+L 清空对话,Ctrl+R 刷新侧边栏,Ctrl+D 退出。
272
+
273
+ Attributes:
274
+ _config: 应用配置参数
275
+ _bundle: 运行时数据bundle
276
+ _assistant_buffer: 助手输出缓冲区(用于流式输出)
277
+ _busy: 当前是否正在处理请求
278
+ transcript_lines: 对话历史记录列表
279
+ """
280
+
281
+ # CSS 样式定义 - 终端布局
282
+ CSS = """
283
+ Screen {
284
+ layout: vertical;
285
+ }
286
+
287
+ #main-row {
288
+ height: 1fr;
289
+ }
290
+
291
+ #transcript-column {
292
+ width: 3fr;
293
+ min-width: 60;
294
+ }
295
+
296
+ #side-column {
297
+ width: 1fr;
298
+ min-width: 28;
299
+ }
300
+
301
+ #transcript {
302
+ height: 1fr;
303
+ border: solid $accent;
304
+ }
305
+
306
+ #current-response {
307
+ min-height: 3;
308
+ max-height: 8;
309
+ border: round $primary;
310
+ padding: 0 1;
311
+ }
312
+
313
+ #composer {
314
+ dock: bottom;
315
+ height: 3;
316
+ border: solid $accent;
317
+ }
318
+
319
+ #status-bar, #tasks-panel, #mcp-panel {
320
+ border: round $surface;
321
+ padding: 0 1;
322
+ margin-bottom: 1;
323
+ }
324
+
325
+ #permission-dialog {
326
+ width: 60;
327
+ height: auto;
328
+ padding: 1 2;
329
+ background: $panel;
330
+ border: round $accent;
331
+ }
332
+
333
+ .permission-actions {
334
+ align: center middle;
335
+ height: auto;
336
+ margin-top: 1;
337
+ }
338
+ """
339
+
340
+ # 快捷键绑定
341
+ BINDINGS = [
342
+ Binding("ctrl+l", "clear_conversation", "Clear"), # 清空对话
343
+ Binding("ctrl+r", "refresh_sidebars", "Refresh"), # 刷新侧边栏
344
+ Binding("ctrl+d", "quit_session", "Exit"), # 退出会话
345
+ ]
346
+
347
+ def __init__(
348
+ self,
349
+ *,
350
+ prompt: str | None = None,
351
+ model: str | None = None,
352
+ base_url: str | None = None,
353
+ system_prompt: str | None = None,
354
+ api_key: str | None = None,
355
+ api_client: SupportsStreamingMessages | None = None,
356
+ ) -> None:
357
+ super().__init__()
358
+ # 初始化应用配置
359
+ self._config = AppConfig(
360
+ prompt=prompt,
361
+ model=model,
362
+ base_url=base_url,
363
+ system_prompt=system_prompt,
364
+ api_key=api_key,
365
+ api_client=api_client,
366
+ )
367
+ self._bundle = None # 运行时数据bundle
368
+ self._assistant_buffer = "" # 助手输出缓冲区
369
+ self._busy = False # 当前是否正在处理请求
370
+ self.transcript_lines: list[str] = [] # 对话历史
371
+
372
+ def compose(self) -> ComposeResult:
373
+ """构建界面布局。"""
374
+ yield Header(show_clock=True) # 显示时钟的标题栏
375
+ with Horizontal(id="main-row"):
376
+ with Vertical(id="transcript-column"):
377
+ # 对话历史显示区域
378
+ yield RichLog(id="transcript", wrap=True, highlight=True, markup=True)
379
+ # 当前响应显示区域
380
+ yield Static("Ready.", id="current-response")
381
+ # 用户输入框
382
+ yield Input(placeholder="Ask illusion or enter a /command", id="composer")
383
+ with Vertical(id="side-column"):
384
+ # 状态栏
385
+ yield Static("Starting...", id="status-bar")
386
+ # 任务面板
387
+ yield Static("No tasks yet.", id="tasks-panel")
388
+ # MCP 服务器面板
389
+ yield Static("No MCP servers configured.", id="mcp-panel")
390
+ yield Footer()
391
+
392
+ async def on_mount(self) -> None:
393
+ """应用挂载时初始化运行时。"""
394
+ # 构建运行时环境
395
+ self._bundle = await build_runtime(
396
+ prompt=self._config.prompt,
397
+ model=self._config.model,
398
+ base_url=self._config.base_url,
399
+ system_prompt=self._config.system_prompt,
400
+ api_key=self._config.api_key,
401
+ api_client=self._config.api_client,
402
+ permission_prompt=self._ask_permission,
403
+ ask_user_prompt=self._ask_question,
404
+ )
405
+ await start_runtime(self._bundle) # 启动运行时(执行会话开始钩子)
406
+ # 聚焦输入框
407
+ self.query_one("#composer", Input).focus()
408
+ # 刷新侧边栏
409
+ self._refresh_sidebars()
410
+ # 设置定时刷新侧边栏(每秒)
411
+ self.set_interval(1.0, self._refresh_sidebars)
412
+ # 如果有初始提示词,自动执行
413
+ if self._config.prompt:
414
+ self.call_later(lambda: asyncio.create_task(self._process_line(self._config.prompt or "")))
415
+
416
+ async def on_unmount(self) -> None:
417
+ """应用卸载时清理资源。"""
418
+ if self._bundle is not None:
419
+ await close_runtime(self._bundle)
420
+
421
+ async def _ask_permission(self, tool_name: str, reason: str) -> bool:
422
+ """权限确认回调函数。"""
423
+ return bool(await self._open_modal(PermissionScreen(tool_name, reason)))
424
+
425
+ async def _ask_question(self, question: str, questions_data: object = None) -> str | dict:
426
+ """用户问答回调函数。"""
427
+ result = await self._open_modal(QuestionScreen(question, questions_data)) or ""
428
+ if isinstance(result, dict):
429
+ return result # 结构化答案(含多选 list)
430
+ return str(result)
431
+
432
+ async def _open_modal(self, screen: ModalScreen) -> object:
433
+ """打开模态对话框并等待用户响应。"""
434
+ loop = asyncio.get_running_loop()
435
+ future: asyncio.Future[object] = loop.create_future()
436
+
437
+ def _done(result: object) -> None:
438
+ if not future.done():
439
+ future.set_result(result)
440
+
441
+ self.push_screen(screen, callback=_done)
442
+ return await future
443
+
444
+ @on(Input.Submitted, "#composer")
445
+ async def handle_submit(self, event: Input.Submitted) -> None:
446
+ """处理用户提交输入事件。"""
447
+ event.input.value = ""
448
+ await self._process_line(event.value)
449
+
450
+ async def _process_line(self, line: str) -> None:
451
+ """处理用户输入的行内容。"""
452
+ # 空行或无运行时则忽略
453
+ if not line.strip() or self._bundle is None or self._busy:
454
+ return
455
+ self._busy = True # 设置忙碌状态
456
+ # 获取并禁用输入框
457
+ composer = self.query_one("#composer", Input)
458
+ composer.disabled = True
459
+ # 添加用户输入到对话历史
460
+ self._append_line(f"user> {line}")
461
+ self._set_current_response("[dim]Working...[/dim]")
462
+ try:
463
+ # 处理输入行
464
+ should_continue = await handle_line(
465
+ self._bundle,
466
+ line,
467
+ print_system=self._print_system,
468
+ render_event=self._render_event,
469
+ clear_output=self._clear_transcript,
470
+ )
471
+ self._refresh_sidebars()
472
+ # 如果会话结束则退出
473
+ if not should_continue:
474
+ self.exit()
475
+ finally:
476
+ self._busy = False
477
+ composer.disabled = False
478
+ composer.focus()
479
+
480
+ async def _print_system(self, message: str) -> None:
481
+ """打印系统消息。"""
482
+ self._append_line(f"system> {message}")
483
+ self._set_current_response("Ready.")
484
+
485
+ async def _render_event(self, event: StreamEvent) -> None:
486
+ """渲染流式事件。"""
487
+ # 助手文本增量事件
488
+ if isinstance(event, AssistantTextDelta):
489
+ self._assistant_buffer += event.text
490
+ self._set_current_response(f"[bold]assistant>[/bold] {self._assistant_buffer}")
491
+ return
492
+
493
+ # 助手回合完成事件
494
+ if isinstance(event, AssistantTurnComplete):
495
+ text = self._assistant_buffer or event.message.text or "(empty response)"
496
+ self._append_line(f"assistant> {text}")
497
+ self._assistant_buffer = ""
498
+ self._set_current_response("Ready.")
499
+ return
500
+
501
+ # 工具开始执行事件
502
+ if isinstance(event, ToolExecutionStarted):
503
+ payload = json.dumps(event.tool_input, ensure_ascii=False)
504
+ self._append_line(f"tool> {event.tool_name} {payload}")
505
+ return
506
+
507
+ # 工具执行完成事件
508
+ if isinstance(event, ToolExecutionCompleted):
509
+ prefix = "tool-error>" if event.is_error else "tool-result>"
510
+ self._append_line(f"{prefix} {event.tool_name}: {event.output}")
511
+ return
512
+
513
+ # 错误事件
514
+ if isinstance(event, ErrorEvent):
515
+ self._append_line(f"error> {event.message}")
516
+ self._assistant_buffer = ""
517
+ self._set_current_response("Ready.")
518
+ return
519
+ # 状态事件
520
+ if isinstance(event, StatusEvent):
521
+ self._append_line(f"system> {event.message}")
522
+
523
+ def action_clear_conversation(self) -> None:
524
+ """清空对话历史。"""
525
+ if self._bundle is None:
526
+ return
527
+ self._bundle.engine.clear() # 清空引擎对话历史
528
+ # 清空界面显示
529
+ self.query_one("#transcript", RichLog).clear()
530
+ self.transcript_lines.clear()
531
+ self._set_current_response("Conversation cleared.")
532
+ self._refresh_sidebars()
533
+
534
+ def action_refresh_sidebars(self) -> None:
535
+ """刷新侧边栏显示。"""
536
+ self._refresh_sidebars()
537
+
538
+ def action_quit_session(self) -> None:
539
+ """退出当前会话。"""
540
+ self.exit()
541
+
542
+ def _append_line(self, message: str) -> None:
543
+ """添加一行到对话历史。"""
544
+ self.transcript_lines.append(message)
545
+ self.query_one("#transcript", RichLog).write(message)
546
+
547
+ async def _clear_transcript(self) -> None:
548
+ """清空对话显示区域。"""
549
+ self.query_one("#transcript", RichLog).clear()
550
+ self.transcript_lines.clear()
551
+
552
+ def _set_current_response(self, message: str) -> None:
553
+ """设置当前响应显示。"""
554
+ self.query_one("#current-response", Static).update(message)
555
+
556
+ def _refresh_sidebars(self) -> None:
557
+ """刷新侧边栏信息。"""
558
+ if self._bundle is None:
559
+ return
560
+ # 获取状态信息
561
+ state = self._bundle.app_state.get()
562
+ usage = self._bundle.engine.total_usage
563
+ # 状态栏信息
564
+ agent_count = len(list_active_agents())
565
+ # Agent 状态指示器:使用主题色闪烁
566
+ agent_indicator = ""
567
+ if agent_count > 0:
568
+ import time
569
+ blink = int(time.time() * 2) % 2
570
+ style = f"bold {_AGENT_INDICATOR_COLOR}" if blink else _AGENT_INDICATOR_COLOR
571
+ agent_indicator = f" [{style}]· {agent_count} agent{'s' if agent_count > 1 else ''}[/{style}]"
572
+
573
+ status_lines = [
574
+ "[b]Status[/b]",
575
+ f"model: {state.model}{agent_indicator}",
576
+ f"permissions: {state.permission_mode}",
577
+ f"fast: {'on' if state.fast_mode else 'off'}",
578
+ f"language: {state.ui_language}",
579
+ f"style: {state.output_style}",
580
+ f"tokens: {usage.total_tokens}",
581
+ f"messages: {len(self._bundle.engine.messages)}",
582
+ ]
583
+ self.query_one("#status-bar", Static).update("\n".join(status_lines))
584
+
585
+ # 获取任务列表
586
+ tasks = get_task_manager().list_tasks()
587
+ if tasks:
588
+ task_lines = ["[b]Tasks[/b]"]
589
+ for task in tasks[:10]:
590
+ suffix: list[str] = []
591
+ if task.metadata.get("progress"):
592
+ suffix.append(f"{task.metadata['progress']}%")
593
+ if task.metadata.get("status_note"):
594
+ suffix.append(task.metadata["status_note"])
595
+ detail = f" ({' | '.join(suffix)})" if suffix else ""
596
+ task_lines.append(
597
+ f"{task.id} {to_task_display_status(task.status)} {task.description}{detail}"
598
+ )
599
+ else:
600
+ task_lines = ["[b]Tasks[/b]", "No background tasks."]
601
+ self.query_one("#tasks-panel", Static).update("\n".join(task_lines))
602
+ # 更新 MCP 服务器面板
603
+ self.query_one("#mcp-panel", Static).update(self._bundle.mcp_summary())
@@ -0,0 +1,10 @@
1
+ """
2
+ IllusionCode Web 前端模块
3
+ ========================
4
+
5
+ 本模块提供基于 FastAPI + WebSocket 的 Web 界面后端支持。
6
+
7
+ 主要组件:
8
+ - ws_host: WebBackendHost - WebSocket 通道的后端主机
9
+ - server: FastAPI 应用和路由配置
10
+ """
@@ -0,0 +1,87 @@
1
+ """
2
+ Web 服务器模块
3
+ =============
4
+
5
+ 本模块提供 FastAPI 应用和 WebSocket 端点,用于启动 Web 前端服务。
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import logging
11
+ from pathlib import Path
12
+
13
+ from fastapi import FastAPI, WebSocket, WebSocketDisconnect
14
+ from fastapi.middleware.cors import CORSMiddleware
15
+ from fastapi.staticfiles import StaticFiles
16
+
17
+ from illusion.ui.web.ws_host import WebBackendHost, WebHostConfig
18
+
19
+ log = logging.getLogger(__name__)
20
+
21
+
22
+ def _find_frontend_dist() -> Path | None:
23
+ """查找前端打包产物目录。"""
24
+ # server.py 位于 src/illusion/ui/web/server.py,需要向上 5 级到项目根目录
25
+ project_root = Path(__file__).parent.parent.parent.parent.parent
26
+ # illusion/ 包根目录(wheel 安装后为 site-packages/illusion/)
27
+ pkg_root = Path(__file__).parent.parent.parent.parent
28
+ candidates = [
29
+ # 开发模式:项目根目录下的 frontend/web/dist
30
+ project_root / "frontend" / "web" / "dist",
31
+ # pip 安装:包内打包的前端产物(illusion/_web_dist)
32
+ pkg_root / "_web_dist",
33
+ # 当前工作目录(可能从项目根目录运行)
34
+ Path.cwd() / "frontend" / "web" / "dist",
35
+ ]
36
+ for p in candidates:
37
+ if p.is_dir() and (p / "index.html").exists():
38
+ return p
39
+ return None
40
+
41
+
42
+ def create_app(
43
+ *,
44
+ dev: bool = False,
45
+ host_config: WebHostConfig | None = None,
46
+ ) -> FastAPI:
47
+ """创建 FastAPI 应用实例。"""
48
+ app = FastAPI(title="Illusion Code Web")
49
+
50
+ if dev:
51
+ app.add_middleware(
52
+ CORSMiddleware,
53
+ allow_origins=["http://localhost:5173", "http://127.0.0.1:5173"],
54
+ allow_credentials=True,
55
+ allow_methods=["*"],
56
+ allow_headers=["*"],
57
+ )
58
+
59
+ @app.websocket("/ws")
60
+ async def websocket_endpoint(websocket: WebSocket) -> None:
61
+ await websocket.accept()
62
+ config = host_config or WebHostConfig()
63
+ host = WebBackendHost(config, websocket)
64
+ try:
65
+ await host.run()
66
+ except WebSocketDisconnect:
67
+ log.info("WebSocket client disconnected")
68
+ except Exception as exc:
69
+ log.warning("WebSocket endpoint error: %s", exc)
70
+ # 尝试向前端发送错误事件
71
+ try:
72
+ from starlette.websockets import WebSocketState
73
+ if websocket.application_state == WebSocketState.CONNECTED:
74
+ import json
75
+ await websocket.send_text(json.dumps({
76
+ "type": "error",
77
+ "message": f"Backend error: {exc}",
78
+ }))
79
+ except Exception:
80
+ pass
81
+
82
+ if not dev:
83
+ dist_dir = _find_frontend_dist()
84
+ if dist_dir is not None:
85
+ app.mount("/", StaticFiles(directory=str(dist_dir), html=True), name="static")
86
+
87
+ return app