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.
- illusion/__init__.py +24 -0
- illusion/__main__.py +15 -0
- illusion/_frontend/dist/index.mjs +39208 -0
- illusion/_frontend/package.json +27 -0
- illusion/_frontend/src/App.tsx +624 -0
- illusion/_frontend/src/components/CommandPicker.tsx +98 -0
- illusion/_frontend/src/components/Composer.tsx +55 -0
- illusion/_frontend/src/components/ComposerController.tsx +128 -0
- illusion/_frontend/src/components/ConversationView.tsx +750 -0
- illusion/_frontend/src/components/Footer.tsx +25 -0
- illusion/_frontend/src/components/MarkdownContent.tsx +537 -0
- illusion/_frontend/src/components/MarkdownTable.tsx +245 -0
- illusion/_frontend/src/components/ModalHost.tsx +425 -0
- illusion/_frontend/src/components/MultilineTextInput.tsx +250 -0
- illusion/_frontend/src/components/PromptInput.tsx +64 -0
- illusion/_frontend/src/components/SelectModal.tsx +78 -0
- illusion/_frontend/src/components/SidePanel.tsx +175 -0
- illusion/_frontend/src/components/Spinner.tsx +77 -0
- illusion/_frontend/src/components/StatusBar.tsx +142 -0
- illusion/_frontend/src/components/SwarmPanel.tsx +141 -0
- illusion/_frontend/src/components/TodoPanel.tsx +126 -0
- illusion/_frontend/src/components/ToolCallDisplay.tsx +202 -0
- illusion/_frontend/src/components/TranscriptPane.tsx +79 -0
- illusion/_frontend/src/components/WelcomeBanner.tsx +37 -0
- illusion/_frontend/src/hooks/useBackendSession.ts +468 -0
- illusion/_frontend/src/hooks/useTerminalSize.ts +9 -0
- illusion/_frontend/src/i18n.ts +78 -0
- illusion/_frontend/src/index.tsx +42 -0
- illusion/_frontend/src/theme/ThemeContext.tsx +19 -0
- illusion/_frontend/src/theme/builtinThemes.ts +89 -0
- illusion/_frontend/src/types.ts +110 -0
- illusion/_frontend/src/utils/markdown.ts +33 -0
- illusion/_frontend/src/utils/thinking.ts +191 -0
- illusion/_frontend/tsconfig.json +13 -0
- illusion/_web_dist/assets/index-BseIw-ik.css +10 -0
- illusion/_web_dist/assets/index-C_0ZWMuW.js +82 -0
- illusion/_web_dist/index.html +16 -0
- illusion/api/__init__.py +36 -0
- illusion/api/client.py +568 -0
- illusion/api/codex_client.py +563 -0
- illusion/api/compat.py +138 -0
- illusion/api/effort.py +128 -0
- illusion/api/errors.py +57 -0
- illusion/api/openai_client.py +819 -0
- illusion/api/provider.py +148 -0
- illusion/api/registry.py +479 -0
- illusion/api/usage.py +45 -0
- illusion/auth/__init__.py +50 -0
- illusion/auth/copilot.py +419 -0
- illusion/auth/external.py +612 -0
- illusion/auth/flows.py +58 -0
- illusion/auth/manager.py +214 -0
- illusion/auth/storage.py +372 -0
- illusion/bridge/__init__.py +38 -0
- illusion/bridge/manager.py +190 -0
- illusion/bridge/session_runner.py +84 -0
- illusion/bridge/types.py +113 -0
- illusion/bridge/work_secret.py +131 -0
- illusion/cli.py +1228 -0
- illusion/commands/__init__.py +32 -0
- illusion/commands/registry.py +1934 -0
- illusion/config/__init__.py +39 -0
- illusion/config/i18n.py +522 -0
- illusion/config/paths.py +259 -0
- illusion/config/settings.py +564 -0
- illusion/coordinator/__init__.py +41 -0
- illusion/coordinator/agent_definitions.py +1093 -0
- illusion/coordinator/coordinator_mode.py +127 -0
- illusion/engine/__init__.py +95 -0
- illusion/engine/cost_tracker.py +55 -0
- illusion/engine/messages.py +369 -0
- illusion/engine/query.py +632 -0
- illusion/engine/query_engine.py +343 -0
- illusion/engine/stream_events.py +169 -0
- illusion/hooks/__init__.py +67 -0
- illusion/hooks/events.py +43 -0
- illusion/hooks/executor.py +397 -0
- illusion/hooks/hot_reload.py +74 -0
- illusion/hooks/loader.py +133 -0
- illusion/hooks/schemas.py +121 -0
- illusion/hooks/types.py +86 -0
- illusion/mcp/__init__.py +104 -0
- illusion/mcp/client.py +377 -0
- illusion/mcp/config.py +140 -0
- illusion/mcp/types.py +175 -0
- illusion/memory/__init__.py +36 -0
- illusion/memory/manager.py +94 -0
- illusion/memory/memdir.py +58 -0
- illusion/memory/paths.py +57 -0
- illusion/memory/scan.py +120 -0
- illusion/memory/search.py +83 -0
- illusion/memory/types.py +43 -0
- illusion/output_styles/__init__.py +15 -0
- illusion/output_styles/loader.py +64 -0
- illusion/permissions/__init__.py +39 -0
- illusion/permissions/checker.py +174 -0
- illusion/permissions/modes.py +38 -0
- illusion/platforms.py +148 -0
- illusion/plugins/__init__.py +71 -0
- illusion/plugins/bundled/__init__.py +0 -0
- illusion/plugins/installer.py +59 -0
- illusion/plugins/loader.py +301 -0
- illusion/plugins/schemas.py +51 -0
- illusion/plugins/types.py +56 -0
- illusion/prompts/__init__.py +29 -0
- illusion/prompts/claudemd.py +74 -0
- illusion/prompts/context.py +187 -0
- illusion/prompts/environment.py +189 -0
- illusion/prompts/system_prompt.py +155 -0
- illusion/py.typed +0 -0
- illusion/sandbox/__init__.py +29 -0
- illusion/sandbox/adapter.py +174 -0
- illusion/services/__init__.py +59 -0
- illusion/services/compact/__init__.py +1015 -0
- illusion/services/cron.py +338 -0
- illusion/services/cron_scheduler.py +715 -0
- illusion/services/file_history.py +258 -0
- illusion/services/lsp/__init__.py +455 -0
- illusion/services/session_storage.py +237 -0
- illusion/services/token_estimation.py +72 -0
- illusion/skills/__init__.py +60 -0
- illusion/skills/bundled/__init__.py +110 -0
- illusion/skills/bundled/content/batch.md +86 -0
- illusion/skills/bundled/content/coding-guidelines.md +70 -0
- illusion/skills/bundled/content/debug.md +38 -0
- illusion/skills/bundled/content/loop.md +82 -0
- illusion/skills/bundled/content/remember.md +105 -0
- illusion/skills/bundled/content/simplify.md +53 -0
- illusion/skills/bundled/content/skillify.md +113 -0
- illusion/skills/bundled/content/stuck.md +54 -0
- illusion/skills/bundled/content/update-config.md +329 -0
- illusion/skills/bundled/content/verify.md +74 -0
- illusion/skills/loader.py +219 -0
- illusion/skills/registry.py +40 -0
- illusion/skills/types.py +24 -0
- illusion/state/__init__.py +18 -0
- illusion/state/app_state.py +67 -0
- illusion/state/store.py +93 -0
- illusion/swarm/__init__.py +71 -0
- illusion/swarm/agent_executor.py +857 -0
- illusion/swarm/in_process.py +259 -0
- illusion/swarm/subprocess_backend.py +136 -0
- illusion/swarm/team_helpers.py +123 -0
- illusion/swarm/types.py +159 -0
- illusion/swarm/worktree.py +347 -0
- illusion/tasks/__init__.py +33 -0
- illusion/tasks/local_agent_task.py +42 -0
- illusion/tasks/local_shell_task.py +27 -0
- illusion/tasks/manager.py +377 -0
- illusion/tasks/stop_task.py +21 -0
- illusion/tasks/types.py +88 -0
- illusion/tools/__init__.py +126 -0
- illusion/tools/agent_tool.py +388 -0
- illusion/tools/ask_user_question_tool.py +186 -0
- illusion/tools/base.py +149 -0
- illusion/tools/bash_tool.py +413 -0
- illusion/tools/config_tool.py +90 -0
- illusion/tools/cron_tool.py +473 -0
- illusion/tools/enter_plan_mode_tool.py +147 -0
- illusion/tools/enter_worktree_tool.py +188 -0
- illusion/tools/exit_plan_mode_tool.py +69 -0
- illusion/tools/exit_worktree_tool.py +225 -0
- illusion/tools/file_edit_tool.py +283 -0
- illusion/tools/file_read_tool.py +294 -0
- illusion/tools/file_write_tool.py +184 -0
- illusion/tools/glob_tool.py +165 -0
- illusion/tools/grep_tool.py +190 -0
- illusion/tools/list_mcp_resources_tool.py +80 -0
- illusion/tools/lsp_tool.py +333 -0
- illusion/tools/mcp_auth_tool.py +100 -0
- illusion/tools/mcp_tool.py +75 -0
- illusion/tools/notebook_edit_tool.py +242 -0
- illusion/tools/powershell_tool.py +334 -0
- illusion/tools/read_mcp_resource_tool.py +63 -0
- illusion/tools/repl_tool.py +100 -0
- illusion/tools/send_message_tool.py +112 -0
- illusion/tools/shell_common.py +187 -0
- illusion/tools/skill_tool.py +86 -0
- illusion/tools/sleep_tool.py +62 -0
- illusion/tools/structured_output_tool.py +58 -0
- illusion/tools/task_create_tool.py +98 -0
- illusion/tools/task_get_tool.py +94 -0
- illusion/tools/task_list_tool.py +94 -0
- illusion/tools/task_output_tool.py +55 -0
- illusion/tools/task_stop_tool.py +52 -0
- illusion/tools/task_update_tool.py +224 -0
- illusion/tools/team_create_tool.py +236 -0
- illusion/tools/team_delete_tool.py +104 -0
- illusion/tools/todo_write_tool.py +198 -0
- illusion/tools/tool_search_tool.py +156 -0
- illusion/tools/web_fetch_tool.py +264 -0
- illusion/tools/web_search_tool.py +186 -0
- illusion/ui/__init__.py +23 -0
- illusion/ui/app.py +258 -0
- illusion/ui/backend_host.py +1180 -0
- illusion/ui/input.py +86 -0
- illusion/ui/output.py +363 -0
- illusion/ui/permission_dialog.py +47 -0
- illusion/ui/permission_store.py +99 -0
- illusion/ui/protocol.py +384 -0
- illusion/ui/react_launcher.py +280 -0
- illusion/ui/runtime.py +787 -0
- illusion/ui/textual_app.py +603 -0
- illusion/ui/web/__init__.py +10 -0
- illusion/ui/web/server.py +87 -0
- illusion/ui/web/ws_host.py +1197 -0
- illusion/utils/__init__.py +0 -0
- illusion/utils/ripgrep.py +299 -0
- illusion/utils/shell.py +248 -0
- illusion_code-0.1.0.dist-info/METADATA +1159 -0
- illusion_code-0.1.0.dist-info/RECORD +214 -0
- illusion_code-0.1.0.dist-info/WHEEL +4 -0
- illusion_code-0.1.0.dist-info/entry_points.txt +2 -0
- 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,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
|