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
illusion/ui/input.py
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Input 输入模块
|
|
3
|
+
=============
|
|
4
|
+
|
|
5
|
+
本模块实现基于 prompt_toolkit 的异步输入会话功能。
|
|
6
|
+
|
|
7
|
+
主要功能:
|
|
8
|
+
- 异步命令行输入会话
|
|
9
|
+
- 用户提示和问答支持
|
|
10
|
+
|
|
11
|
+
类说明:
|
|
12
|
+
- InputSession: 异步输入会话封装类
|
|
13
|
+
|
|
14
|
+
使用示例:
|
|
15
|
+
>>> from illusion.ui.input import InputSession
|
|
16
|
+
>>>
|
|
17
|
+
>>> # 创建输入会话
|
|
18
|
+
>>> session = InputSession()
|
|
19
|
+
>>>
|
|
20
|
+
>>> # 获取用户输入
|
|
21
|
+
>>> user_input = await session.prompt()
|
|
22
|
+
>>>
|
|
23
|
+
>>> # 提问并获取答案
|
|
24
|
+
>>> answer = await session.ask("请输入您的姓名")
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
from prompt_toolkit import PromptSession
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class InputSession:
|
|
33
|
+
"""异步输入会话封装类。
|
|
34
|
+
|
|
35
|
+
基于 prompt_toolkit 的 PromptSession 实现异步命令行输入。
|
|
36
|
+
支持普通提示输入和自定义问答。
|
|
37
|
+
|
|
38
|
+
Attributes:
|
|
39
|
+
_session: prompt_toolkit 会话实例
|
|
40
|
+
_prompt: 提示符字符串
|
|
41
|
+
|
|
42
|
+
使用示例:
|
|
43
|
+
>>> session = InputSession()
|
|
44
|
+
>>> user_input = await session.prompt()
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
def __init__(self) -> None:
|
|
48
|
+
# 创建 prompt_toolkit 会话实例
|
|
49
|
+
self._session = PromptSession()
|
|
50
|
+
# 设置默认提示符
|
|
51
|
+
self._prompt = "> "
|
|
52
|
+
|
|
53
|
+
def set_modes(self, *, vim_enabled: bool = False, voice_enabled: bool = False) -> None:
|
|
54
|
+
"""设置输入模式。
|
|
55
|
+
|
|
56
|
+
注意:vim 和 voice 模式已从 UI 提示符中移除,此方法保留用于兼容性。
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
vim_enabled: Vim 模式开关(已废弃)
|
|
60
|
+
voice_enabled: 语音模式开关(已废弃)
|
|
61
|
+
"""
|
|
62
|
+
# 忽略废弃参数以保持 API 兼容性
|
|
63
|
+
del vim_enabled, voice_enabled
|
|
64
|
+
# 重置为默认提示符
|
|
65
|
+
self._prompt = "> "
|
|
66
|
+
|
|
67
|
+
async def prompt(self) -> str:
|
|
68
|
+
"""提示用户输入一行文本。
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
str: 用户输入的文本(已去除首尾空白)
|
|
72
|
+
"""
|
|
73
|
+
return await self._session.prompt_async(self._prompt)
|
|
74
|
+
|
|
75
|
+
async def ask(self, question: str) -> str:
|
|
76
|
+
"""提示用户回答一个问题。
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
question: 要询问用户的问题
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
str: 用户输入的答案(已去除首尾空白)
|
|
83
|
+
"""
|
|
84
|
+
# 构建问答提示符,格式为 "[question] 问题内容\n> "
|
|
85
|
+
prompt = f"[question] {question}\n> "
|
|
86
|
+
return await self._session.prompt_async(prompt)
|
illusion/ui/output.py
ADDED
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Output 输出模块
|
|
3
|
+
=============
|
|
4
|
+
|
|
5
|
+
本模块实现基于 rich 的终端渲染辅助功能。
|
|
6
|
+
|
|
7
|
+
主要功能:
|
|
8
|
+
- Markdown 渲染
|
|
9
|
+
- 语法高亮
|
|
10
|
+
- 加载动画(spinner)
|
|
11
|
+
- 工具执行状态显示
|
|
12
|
+
|
|
13
|
+
类说明:
|
|
14
|
+
- OutputRenderer: 终端渲染器类
|
|
15
|
+
|
|
16
|
+
使用示例:
|
|
17
|
+
>>> from illusion.ui.output import OutputRenderer
|
|
18
|
+
>>>
|
|
19
|
+
>>> # 创建渲染器
|
|
20
|
+
>>> renderer = OutputRenderer(style_name="default")
|
|
21
|
+
>>>
|
|
22
|
+
>>> # 处理流式事件
|
|
23
|
+
>>> renderer.render_event(event)
|
|
24
|
+
>>>
|
|
25
|
+
>>> # 打印系统消息
|
|
26
|
+
>>> renderer.print_system("任务已完成")
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
from rich.console import Console
|
|
32
|
+
from rich.markdown import Markdown
|
|
33
|
+
from rich.panel import Panel
|
|
34
|
+
from rich.syntax import Syntax
|
|
35
|
+
|
|
36
|
+
from illusion.engine.stream_events import (
|
|
37
|
+
AssistantTextDelta,
|
|
38
|
+
AssistantTurnComplete,
|
|
39
|
+
StreamEvent,
|
|
40
|
+
ToolExecutionCompleted,
|
|
41
|
+
ToolExecutionStarted,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class OutputRenderer:
|
|
46
|
+
"""终端渲染器类。
|
|
47
|
+
|
|
48
|
+
使用 rich 格式化渲染模型和工具事件到终端。
|
|
49
|
+
支持 Markdown、语法高亮和加载动画。
|
|
50
|
+
|
|
51
|
+
Attributes:
|
|
52
|
+
console: Rich Console 实例
|
|
53
|
+
_assistant_line_open: 助手行是否处于打开状态
|
|
54
|
+
_assistant_buffer: 助手输出缓冲区
|
|
55
|
+
_style_name: 样式名称
|
|
56
|
+
_spinner_status: 加载动画状态
|
|
57
|
+
_last_tool_input: 上一个工具输入参数
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
def __init__(self, style_name: str = "default") -> None:
|
|
61
|
+
# 创建 Rich Console 实例
|
|
62
|
+
self.console = Console()
|
|
63
|
+
# 初始化状态
|
|
64
|
+
self._assistant_line_open = False
|
|
65
|
+
self._assistant_buffer = ""
|
|
66
|
+
self._style_name = style_name
|
|
67
|
+
self._spinner_status = None
|
|
68
|
+
self._last_tool_input: dict | None = None
|
|
69
|
+
|
|
70
|
+
def set_style(self, style_name: str) -> None:
|
|
71
|
+
"""设置输出样式。
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
style_name: 样式名称
|
|
75
|
+
"""
|
|
76
|
+
self._style_name = style_name
|
|
77
|
+
|
|
78
|
+
def show_thinking(self) -> None:
|
|
79
|
+
"""在第一个助手 token 到达之前显示思考加载动画。"""
|
|
80
|
+
# 如果已有加载动画,则跳过
|
|
81
|
+
if self._spinner_status is not None:
|
|
82
|
+
return
|
|
83
|
+
# minimal 样式不显示加载动画
|
|
84
|
+
if self._style_name == "minimal":
|
|
85
|
+
return
|
|
86
|
+
# 创建并启动加载动画
|
|
87
|
+
self._spinner_status = self.console.status(
|
|
88
|
+
"[cyan]Thinking...[/cyan]", spinner="dots"
|
|
89
|
+
)
|
|
90
|
+
self._spinner_status.start()
|
|
91
|
+
|
|
92
|
+
def start_assistant_turn(self) -> None:
|
|
93
|
+
"""开始助手回合。
|
|
94
|
+
|
|
95
|
+
停止加载动画,准备输出助手文本。
|
|
96
|
+
"""
|
|
97
|
+
# 停止加载动画
|
|
98
|
+
self._stop_spinner()
|
|
99
|
+
# 如果助手行已打开,先换行
|
|
100
|
+
if self._assistant_line_open:
|
|
101
|
+
self.console.print()
|
|
102
|
+
# 重置缓冲区
|
|
103
|
+
self._assistant_buffer = ""
|
|
104
|
+
self._assistant_line_open = True
|
|
105
|
+
# 根据样式打印提示符
|
|
106
|
+
if self._style_name == "minimal":
|
|
107
|
+
self.console.print("a> ", end="", style="green")
|
|
108
|
+
else:
|
|
109
|
+
self.console.print("[green bold]\u23fa[/green bold] ", end="")
|
|
110
|
+
|
|
111
|
+
def render_event(self, event: StreamEvent) -> None:
|
|
112
|
+
"""渲染流式事件。
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
event: 流式事件对象
|
|
116
|
+
"""
|
|
117
|
+
# 助手文本增量事件
|
|
118
|
+
if isinstance(event, AssistantTextDelta):
|
|
119
|
+
self._assistant_buffer += event.text
|
|
120
|
+
# 流式输出原始文本以保持响应性
|
|
121
|
+
self.console.print(event.text, end="", markup=False, highlight=False)
|
|
122
|
+
return
|
|
123
|
+
|
|
124
|
+
# 助手回合完成事件
|
|
125
|
+
if isinstance(event, AssistantTurnComplete):
|
|
126
|
+
if self._assistant_line_open:
|
|
127
|
+
self.console.print()
|
|
128
|
+
# 如果缓冲区包含 Markdown 指示符,则使用 Markdown 重新渲染
|
|
129
|
+
if _has_markdown(self._assistant_buffer) and self._style_name != "minimal":
|
|
130
|
+
self.console.print()
|
|
131
|
+
self.console.print(Markdown(self._assistant_buffer.strip()))
|
|
132
|
+
self._assistant_line_open = False
|
|
133
|
+
self._assistant_buffer = ""
|
|
134
|
+
return
|
|
135
|
+
|
|
136
|
+
# 工具开始执行事件
|
|
137
|
+
if isinstance(event, ToolExecutionStarted):
|
|
138
|
+
self._stop_spinner()
|
|
139
|
+
if self._assistant_line_open:
|
|
140
|
+
self.console.print()
|
|
141
|
+
self._assistant_line_open = False
|
|
142
|
+
# 获取工具名称和输入摘要
|
|
143
|
+
tool_name = event.tool_name
|
|
144
|
+
summary = _summarize_tool_input(tool_name, event.tool_input)
|
|
145
|
+
self._last_tool_input = event.tool_input
|
|
146
|
+
# 根据样式渲染
|
|
147
|
+
if self._style_name == "minimal":
|
|
148
|
+
self.console.print(f" > {tool_name} {summary}")
|
|
149
|
+
else:
|
|
150
|
+
self.console.print(
|
|
151
|
+
f" [bold cyan]\u23f5 {tool_name}[/bold cyan] [dim]{summary}[/dim]"
|
|
152
|
+
)
|
|
153
|
+
self._start_spinner(tool_name)
|
|
154
|
+
return
|
|
155
|
+
|
|
156
|
+
# 工具执行完成事件
|
|
157
|
+
if isinstance(event, ToolExecutionCompleted):
|
|
158
|
+
self._stop_spinner()
|
|
159
|
+
tool_name = event.tool_name
|
|
160
|
+
output = event.output
|
|
161
|
+
is_error = event.is_error
|
|
162
|
+
# minimal 样式简单输出
|
|
163
|
+
if self._style_name == "minimal":
|
|
164
|
+
self.console.print(f" {output}")
|
|
165
|
+
return
|
|
166
|
+
# 错误输出显示为红色面板
|
|
167
|
+
if is_error:
|
|
168
|
+
self.console.print(Panel(output, title=f"{tool_name} error", border_style="red", padding=(0, 1)))
|
|
169
|
+
return
|
|
170
|
+
# 根据工具类型渲染输出
|
|
171
|
+
tool_input = getattr(event, "tool_input", None) or self._last_tool_input
|
|
172
|
+
self._render_tool_output(tool_name, tool_input, output)
|
|
173
|
+
|
|
174
|
+
def print_system(self, message: str) -> None:
|
|
175
|
+
"""打印系统消息。
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
message: 系统消息文本
|
|
179
|
+
"""
|
|
180
|
+
self._stop_spinner()
|
|
181
|
+
if self._assistant_line_open:
|
|
182
|
+
self.console.print()
|
|
183
|
+
self._assistant_line_open = False
|
|
184
|
+
if self._style_name == "minimal":
|
|
185
|
+
self.console.print(message)
|
|
186
|
+
else:
|
|
187
|
+
self.console.print(f"[yellow]\u2139 {message}[/yellow]")
|
|
188
|
+
|
|
189
|
+
def print_status_line(
|
|
190
|
+
self,
|
|
191
|
+
*,
|
|
192
|
+
model: str = "unknown",
|
|
193
|
+
input_tokens: int = 0,
|
|
194
|
+
output_tokens: int = 0,
|
|
195
|
+
permission_mode: str = "default",
|
|
196
|
+
) -> None:
|
|
197
|
+
"""在每回合后打印紧凑状态行。
|
|
198
|
+
|
|
199
|
+
Args:
|
|
200
|
+
model: 模型名称
|
|
201
|
+
input_tokens: 输入 token 数量
|
|
202
|
+
output_tokens: 输出 token 数量
|
|
203
|
+
permission_mode: 权限模式
|
|
204
|
+
"""
|
|
205
|
+
parts = [f"[cyan]model: {model}[/cyan]"]
|
|
206
|
+
if input_tokens > 0 or output_tokens > 0:
|
|
207
|
+
down = "\u2193"
|
|
208
|
+
up = "\u2191"
|
|
209
|
+
parts.append(f"tokens: {_fmt_num(input_tokens)}{down} {_fmt_num(output_tokens)}{up}")
|
|
210
|
+
parts.append(f"mode: {permission_mode}")
|
|
211
|
+
sep = " \u2502 "
|
|
212
|
+
line = sep.join(parts)
|
|
213
|
+
self.console.print(f"[dim]{line}[/dim]")
|
|
214
|
+
|
|
215
|
+
def clear(self) -> None:
|
|
216
|
+
"""清空终端屏幕。"""
|
|
217
|
+
self.console.clear()
|
|
218
|
+
|
|
219
|
+
def _start_spinner(self, tool_name: str) -> None:
|
|
220
|
+
"""启动工具执行加载动画。
|
|
221
|
+
|
|
222
|
+
Args:
|
|
223
|
+
tool_name: 工具名称
|
|
224
|
+
"""
|
|
225
|
+
if self._style_name == "minimal":
|
|
226
|
+
return
|
|
227
|
+
self._spinner_status = self.console.status(f"Running {tool_name}...", spinner="dots")
|
|
228
|
+
self._spinner_status.start()
|
|
229
|
+
|
|
230
|
+
def _stop_spinner(self) -> None:
|
|
231
|
+
"""停止加载动画。"""
|
|
232
|
+
if self._spinner_status is not None:
|
|
233
|
+
self._spinner_status.stop()
|
|
234
|
+
self._spinner_status = None
|
|
235
|
+
|
|
236
|
+
def _render_tool_output(self, tool_name: str, tool_input: dict | None, output: str) -> None:
|
|
237
|
+
"""渲染工具输出。
|
|
238
|
+
|
|
239
|
+
Args:
|
|
240
|
+
tool_name: 工具名称
|
|
241
|
+
tool_input: 工具输入参数
|
|
242
|
+
output: 工具输出文本
|
|
243
|
+
"""
|
|
244
|
+
lower = tool_name.lower()
|
|
245
|
+
# Bash:在面板中显示
|
|
246
|
+
if lower == "bash":
|
|
247
|
+
cmd = (tool_input or {}).get("command", "")
|
|
248
|
+
title = f"$ {cmd[:80]}" if cmd else "Bash"
|
|
249
|
+
self.console.print(Panel(output[:2000], title=title, border_style="dim", padding=(0, 1)))
|
|
250
|
+
return
|
|
251
|
+
# Read/FileRead:根据文件扩展名进行语法高亮
|
|
252
|
+
if lower in ("read", "fileread", "file_read"):
|
|
253
|
+
file_path = str((tool_input or {}).get("file_path", ""))
|
|
254
|
+
ext = file_path.rsplit(".", 1)[-1] if "." in file_path else ""
|
|
255
|
+
lexer = _ext_to_lexer(ext)
|
|
256
|
+
if lexer and len(output) < 5000:
|
|
257
|
+
self.console.print(Syntax(output, lexer, theme="monokai", line_numbers=True, word_wrap=True))
|
|
258
|
+
else:
|
|
259
|
+
self.console.print(Panel(output[:2000], title=file_path, border_style="dim", padding=(0, 1)))
|
|
260
|
+
return
|
|
261
|
+
# Edit/FileEdit:显示为 diff 风格
|
|
262
|
+
if lower in ("edit", "fileedit", "file_edit"):
|
|
263
|
+
file_path = str((tool_input or {}).get("file_path", ""))
|
|
264
|
+
self.console.print(Panel(output[:2000], title=f"Edit: {file_path}", border_style="green", padding=(0, 1)))
|
|
265
|
+
return
|
|
266
|
+
# Grep:显示搜索结果
|
|
267
|
+
if lower in ("grep", "greptool"):
|
|
268
|
+
self.console.print(Panel(output[:2000], title="Search results", border_style="cyan", padding=(0, 1)))
|
|
269
|
+
return
|
|
270
|
+
# Default:变暗文本并截断
|
|
271
|
+
lines = output.split("\n")
|
|
272
|
+
if len(lines) > 15:
|
|
273
|
+
display = "\n".join(lines[:12]) + f"\n... ({len(lines) - 12} more lines)"
|
|
274
|
+
else:
|
|
275
|
+
display = output
|
|
276
|
+
self.console.print(f" [dim]{display}[/dim]")
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def _has_markdown(text: str) -> bool:
|
|
280
|
+
"""检查文本是否可能包含 Markdown 格式化。
|
|
281
|
+
|
|
282
|
+
Args:
|
|
283
|
+
text: 要检查的文本
|
|
284
|
+
|
|
285
|
+
Returns:
|
|
286
|
+
bool: 是否可能包含 Markdown
|
|
287
|
+
"""
|
|
288
|
+
indicators = ["```", "## ", "### ", "- ", "* ", "1. ", "**", "__", "> "]
|
|
289
|
+
return any(ind in text for ind in indicators)
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def _summarize_tool_input(tool_name: str, tool_input: dict | None) -> str:
|
|
293
|
+
"""生成工具输入的紧凑摘要。
|
|
294
|
+
|
|
295
|
+
Args:
|
|
296
|
+
tool_name: 工具名称
|
|
297
|
+
tool_input: 工具输入参数
|
|
298
|
+
|
|
299
|
+
Returns:
|
|
300
|
+
str: 工具输入摘要
|
|
301
|
+
"""
|
|
302
|
+
if not tool_input:
|
|
303
|
+
return ""
|
|
304
|
+
lower = tool_name.lower()
|
|
305
|
+
# Bash:显示命令
|
|
306
|
+
if lower == "bash" and "command" in tool_input:
|
|
307
|
+
return str(tool_input["command"])[:120]
|
|
308
|
+
# Read:显示文���路���
|
|
309
|
+
if lower in ("read", "fileread", "file_read") and "file_path" in tool_input:
|
|
310
|
+
return str(tool_input["file_path"])
|
|
311
|
+
# Write:显示文件路径
|
|
312
|
+
if lower in ("write", "filewrite", "file_write") and "file_path" in tool_input:
|
|
313
|
+
return str(tool_input["file_path"])
|
|
314
|
+
# Edit:显示文件路径
|
|
315
|
+
if lower in ("edit", "fileedit", "file_edit") and "file_path" in tool_input:
|
|
316
|
+
return str(tool_input["file_path"])
|
|
317
|
+
# Grep:显示搜索模式
|
|
318
|
+
if lower in ("grep", "greptool") and "pattern" in tool_input:
|
|
319
|
+
return f"/{tool_input['pattern']}/"
|
|
320
|
+
# Glob:显示 glob 模式
|
|
321
|
+
if lower in ("glob", "globtool") and "pattern" in tool_input:
|
|
322
|
+
return str(tool_input["pattern"])
|
|
323
|
+
# Default:显示第一个键值对
|
|
324
|
+
entries = list(tool_input.items())
|
|
325
|
+
if entries:
|
|
326
|
+
k, v = entries[0]
|
|
327
|
+
return f"{k}={str(v)[:60]}"
|
|
328
|
+
return ""
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def _ext_to_lexer(ext: str) -> str | None:
|
|
332
|
+
"""将文件扩展名映射到语法高亮lexer。
|
|
333
|
+
|
|
334
|
+
Args:
|
|
335
|
+
ext: 文件扩展名
|
|
336
|
+
|
|
337
|
+
Returns:
|
|
338
|
+
str | None: lexer 名称
|
|
339
|
+
"""
|
|
340
|
+
mapping = {
|
|
341
|
+
"py": "python", "js": "javascript", "ts": "typescript", "tsx": "tsx",
|
|
342
|
+
"jsx": "jsx", "rs": "rust", "go": "go", "rb": "ruby", "java": "java",
|
|
343
|
+
"c": "c", "cpp": "cpp", "h": "c", "hpp": "cpp", "cs": "csharp",
|
|
344
|
+
"sh": "bash", "bash": "bash", "zsh": "bash", "json": "json",
|
|
345
|
+
"yaml": "yaml", "yml": "yaml", "toml": "toml", "xml": "xml",
|
|
346
|
+
"html": "html", "css": "css", "sql": "sql", "md": "markdown",
|
|
347
|
+
"txt": None,
|
|
348
|
+
}
|
|
349
|
+
return mapping.get(ext.lower())
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def _fmt_num(n: int) -> str:
|
|
353
|
+
"""格式化数字(添加千位分隔符)。
|
|
354
|
+
|
|
355
|
+
Args:
|
|
356
|
+
n: 数字
|
|
357
|
+
|
|
358
|
+
Returns:
|
|
359
|
+
str: 格式化的数字字符串
|
|
360
|
+
"""
|
|
361
|
+
if n >= 1000:
|
|
362
|
+
return f"{n / 1000:.1f}k"
|
|
363
|
+
return str(n)
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Permission Dialog 权限对话框模块
|
|
3
|
+
=========================
|
|
4
|
+
|
|
5
|
+
本模块实现基于 prompt_toolkit 的交互式权限确认对话框。
|
|
6
|
+
|
|
7
|
+
主要功能:
|
|
8
|
+
- 异步权限确认提示
|
|
9
|
+
- 用户授权决策
|
|
10
|
+
|
|
11
|
+
函数说明:
|
|
12
|
+
- ask_permission: 提示用户批准变异工具
|
|
13
|
+
|
|
14
|
+
使用示例:
|
|
15
|
+
>>> from illusion.ui.permission_dialog import ask_permission
|
|
16
|
+
>>>
|
|
17
|
+
>>> # 请求权限
|
|
18
|
+
>>> allowed = await ask_permission("Bash", "需要执行 shell 命令")
|
|
19
|
+
>>> if allowed:
|
|
20
|
+
... print("已授权")
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
from prompt_toolkit import PromptSession
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
async def ask_permission(tool_name: str, reason: str) -> bool:
|
|
29
|
+
"""提示用户批准变异工具。
|
|
30
|
+
|
|
31
|
+
显示工具名称和原因,询问用户是否允许执行。
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
tool_name: 工具名称
|
|
35
|
+
reason: 工具请求的原因说明
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
bool: 用户是否允许执行该工具
|
|
39
|
+
"""
|
|
40
|
+
# 创建 prompt 会话
|
|
41
|
+
session = PromptSession()
|
|
42
|
+
# 发送提示并获取用户响应
|
|
43
|
+
response = await session.prompt_async(
|
|
44
|
+
f"Allow tool '{tool_name}'? [{reason}] [y/N]: "
|
|
45
|
+
)
|
|
46
|
+
# 检查用户输入是否为肯定回答
|
|
47
|
+
return response.strip().lower() in {"y", "yes"}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Permission Store 权限存储模块
|
|
3
|
+
=====================
|
|
4
|
+
|
|
5
|
+
本模块实现工作空间本地的"总是允许"工具权限持久化存储。
|
|
6
|
+
|
|
7
|
+
主要功能:
|
|
8
|
+
- 从工作空间加载"总是允许"的工具列表
|
|
9
|
+
- 保存"总是允许"的工具列表到工作空间
|
|
10
|
+
- 添加单个工具到"总是允许"列表
|
|
11
|
+
|
|
12
|
+
使用示例:
|
|
13
|
+
>>> from illusion.ui.permission_store import load_always_allowed_tools, save_always_allowed_tools, add_always_allowed_tool
|
|
14
|
+
>>>
|
|
15
|
+
>>> # 加载工具列表
|
|
16
|
+
>>> tools = load_always_allowed_tools("/path/to/workspace")
|
|
17
|
+
>>>
|
|
18
|
+
>>> # 添加工具
|
|
19
|
+
>>> tools = add_always_allowed_tool("/path/to/workspace", "Bash")
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import json
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
|
|
27
|
+
# 权限文件路径:.illusion/permissions.json
|
|
28
|
+
_PERMISSIONS_PATH = Path(".illusion") / "permissions.json"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _file_path(cwd: str | Path) -> Path:
|
|
32
|
+
"""获取权限文件的完整路径。
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
cwd: 工作目录路径
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
Path: 权限文件的完整路径
|
|
39
|
+
"""
|
|
40
|
+
return Path(cwd).resolve() / _PERMISSIONS_PATH
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def load_always_allowed_tools(cwd: str | Path) -> set[str]:
|
|
44
|
+
"""从工作空间权限文件加载"总是允许"的工具列表。
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
cwd: 工作目录路径
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
set[str]: "总是允许"的工具名称集合
|
|
51
|
+
"""
|
|
52
|
+
path = _file_path(cwd)
|
|
53
|
+
if not path.exists():
|
|
54
|
+
return set()
|
|
55
|
+
try:
|
|
56
|
+
payload = json.loads(path.read_text(encoding="utf-8"))
|
|
57
|
+
except (json.JSONDecodeError, OSError):
|
|
58
|
+
return set()
|
|
59
|
+
tools = payload.get("always_allow_tools", [])
|
|
60
|
+
if not isinstance(tools, list):
|
|
61
|
+
return set()
|
|
62
|
+
return {str(item).strip() for item in tools if str(item).strip()}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def save_always_allowed_tools(cwd: str | Path, tools: set[str]) -> None:
|
|
66
|
+
"""将"总是允许"的工具列表持久化到工作空间权限文件。
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
cwd: 工作目录路径
|
|
70
|
+
tools: "总是允许"的工具名称集合
|
|
71
|
+
"""
|
|
72
|
+
path = _file_path(cwd)
|
|
73
|
+
# 创建父目录(如果不存在)
|
|
74
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
75
|
+
# 序列化并写入文件
|
|
76
|
+
payload = {"always_allow_tools": sorted({tool.strip() for tool in tools if tool.strip()})}
|
|
77
|
+
path.write_text(json.dumps(payload, indent=2) + "\n", encoding="utf-8")
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def add_always_allowed_tool(cwd: str | Path, tool_name: str) -> set[str]:
|
|
81
|
+
"""将一个工具添加到工作空间"总是允许"权限列表并持久化。
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
cwd: 工作目录路径
|
|
85
|
+
tool_name: 工具名称
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
set[str]: 更新后的"总是允许"工具集合
|
|
89
|
+
"""
|
|
90
|
+
# 加载现有工具列表
|
|
91
|
+
tools = load_always_allowed_tools(cwd)
|
|
92
|
+
name = tool_name.strip()
|
|
93
|
+
if not name:
|
|
94
|
+
return tools
|
|
95
|
+
# 添加新工具
|
|
96
|
+
tools.add(name)
|
|
97
|
+
# 保存更新后的列表
|
|
98
|
+
save_always_allowed_tools(cwd, tools)
|
|
99
|
+
return tools
|