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,1934 @@
1
+ """
2
+ 斜杠命令注册模块
3
+ ==============
4
+
5
+ 本模块提供 IllusionCode 斜杠命令的注册和管理功能。
6
+
7
+ 主要功能:
8
+ - 注册和管理斜杠命令 (/xxx)
9
+ - 解析命令参数
10
+ - 提供内置命令处理器
11
+
12
+ 类说明:
13
+ - CommandResult: 命令执行结果
14
+ - CommandContext: 命令执行上下文
15
+ - SlashCommand: 斜杠命令定义
16
+ - CommandRegistry: 命令注册表
17
+
18
+ 函数说明:
19
+ - create_default_command_registry: 创建默认命令注册表
20
+
21
+ 内置命令列表:
22
+ - /exit, /clear, /version, /status, /context, /summary
23
+ - /compact, /memory, /hooks, /resume
24
+ - /export, /share, /copy, /rewind, /files
25
+ - /init, /bridge, /login, /logout, /feedback
26
+ - /skills, /config, /mcp, /plugin, /reload-plugins
27
+ - /permissions, /plan, /thinking, /fast, /effort, /passes, /turns
28
+ - /continue, /model, /language, /output-style
29
+ - /doctor, /diff, /branch, /commit
30
+ - /issue, /pr_comments, /privacy-settings
31
+ - /delete, /rules
32
+
33
+ 使用示例:
34
+ >>> from illusion.commands import create_default_command_registry
35
+ >>> registry = create_default_command_registry()
36
+ >>> result = registry.lookup("/version")
37
+ """
38
+
39
+ from __future__ import annotations
40
+
41
+ import importlib.metadata
42
+ import json
43
+ import subprocess
44
+ import sys
45
+
46
+ from illusion.config.i18n import (
47
+ COMMAND_DESCRIPTIONS_ZH,
48
+ _is_zh,
49
+ translate_command_message,
50
+ )
51
+ from datetime import datetime, timezone
52
+ from dataclasses import dataclass
53
+ from pathlib import Path
54
+ from typing import Any, TYPE_CHECKING, Awaitable, Callable, Literal, get_args
55
+
56
+ import pyperclip
57
+
58
+ from illusion.config.paths import (
59
+ get_config_dir,
60
+ get_data_dir,
61
+ get_feedback_log_path,
62
+ get_project_config_dir,
63
+ get_project_issue_file,
64
+ get_project_pr_comments_file,
65
+ )
66
+ from illusion.bridge import get_bridge_manager
67
+ from illusion.bridge.types import WorkSecret
68
+ from illusion.bridge.work_secret import build_sdk_url, decode_work_secret, encode_work_secret
69
+ from illusion.api.provider import auth_status, detect_provider
70
+ from illusion.config.settings import Settings, load_settings, save_settings
71
+ from illusion.engine.messages import ConversationMessage
72
+ from illusion.engine.query_engine import QueryEngine
73
+ from illusion.memory import (
74
+ add_memory_entry,
75
+ get_memory_entrypoint,
76
+ get_project_memory_dir,
77
+ list_memory_files,
78
+ remove_memory_entry,
79
+ )
80
+ from illusion.output_styles import load_output_styles
81
+ from illusion.permissions import PermissionChecker, PermissionMode
82
+ from illusion.plugins import load_plugins
83
+ from illusion.prompts import build_runtime_system_prompt
84
+ from illusion.plugins.installer import install_plugin_from_path, uninstall_plugin
85
+ from illusion.services import (
86
+ estimate_conversation_tokens,
87
+ export_session_markdown,
88
+ save_session_snapshot,
89
+ summarize_messages,
90
+ )
91
+ from illusion.services.session_storage import get_project_session_dir, load_session_snapshot
92
+ from illusion.services.file_history import rewind_to
93
+ from illusion.skills import load_skill_registry
94
+
95
+ if TYPE_CHECKING:
96
+ from illusion.state import AppStateStore
97
+ from illusion.tools.base import ToolRegistry
98
+
99
+
100
+ @dataclass
101
+ class CommandResult:
102
+ """斜杠命令执行结果
103
+
104
+ Attributes:
105
+ message: 返回给用户的消息
106
+ should_exit: 是否应该退出程序
107
+ clear_screen: 是否应该清除屏幕
108
+ replay_messages: 要在TUI中重放的消息列表
109
+ continue_pending: 是否继续待处理的工具循环
110
+ continue_turns: 继续的回合数
111
+ """
112
+
113
+ message: str | None = None # 返回消息
114
+ should_exit: bool = False # 退出标志
115
+ clear_screen: bool = False # 清屏标志
116
+ replay_messages: list | None = None # ConversationMessage列表用于TUI重放
117
+ needs_api_rebuild: bool = False # 需要重建 API 客户端(跨 env 切换模型时)
118
+ continue_pending: bool = False # 继续待处理标志
119
+ continue_turns: int | None = None # 继续回合数
120
+ reset_session: bool = False # 是否重置会话ID
121
+ restored_session_id: str | None = None # 恢复的会话ID
122
+
123
+
124
+ def _resolve_ui_language(context: "CommandContext | None") -> str:
125
+ if context is not None and context.app_state is not None:
126
+ value = str(context.app_state.get().ui_language or "")
127
+ if value:
128
+ return value
129
+ return str(load_settings().ui_language)
130
+
131
+
132
+
133
+
134
+ def _translate_command_message(message: str, *, locale: str) -> str:
135
+ """翻译命令消息(委托给 i18n 模块)"""
136
+ return translate_command_message(message, locale=locale)
137
+
138
+
139
+ @dataclass
140
+ class CommandContext:
141
+ """命令处理器可用的上下文
142
+
143
+ Attributes:
144
+ engine: 查询引擎实例
145
+ hooks_summary: hooks摘要
146
+ mcp_summary: MCP摘要
147
+ plugin_summary: 插件摘要
148
+ cwd: 当前工作目录
149
+ tool_registry: 工具注册表
150
+ app_state: 应用状态存储
151
+ """
152
+
153
+ engine: QueryEngine # 查询引擎
154
+ hooks_summary: str = "" # hooks摘要
155
+ mcp_summary: str = "" # MCP摘要
156
+ plugin_summary: str = "" # 插件摘要
157
+ cwd: str = "." # 当前工作目录
158
+ tool_registry: ToolRegistry | None = None # 工具注册表
159
+ app_state: AppStateStore | None = None # 应用状态
160
+ session_id: str = "" # 当前会话ID
161
+
162
+
163
+ # 命令处理器类型别名
164
+ CommandHandler = Callable[[str, CommandContext], Awaitable[CommandResult]]
165
+
166
+
167
+ @dataclass
168
+ class SlashCommand:
169
+ """斜杠命令定义
170
+
171
+ Attributes:
172
+ name: 命令名称 (不含前导/)
173
+ description: 命令描述
174
+ handler: 命令处理器函数
175
+ """
176
+
177
+ name: str # 命令名称
178
+ description: str # 命令描述
179
+ handler: CommandHandler # 处理器函数
180
+
181
+
182
+ class CommandRegistry:
183
+ """斜杠命令到处理器的映射容器
184
+
185
+ Attributes:
186
+ _commands: 命令名到SlashCommand的映射
187
+ """
188
+
189
+ def __init__(self) -> None:
190
+ self._commands: dict[str, SlashCommand] = {} # 命令映射初始化
191
+
192
+ def register(self, command: SlashCommand) -> None:
193
+ """注册命令
194
+
195
+ Args:
196
+ command: 要注册的SlashCommand
197
+ """
198
+ original_handler = command.handler
199
+
200
+ async def _localized_handler(args: str, context: CommandContext) -> CommandResult:
201
+ result = await original_handler(args, context)
202
+ if result.message:
203
+ result.message = _translate_command_message(
204
+ result.message,
205
+ locale=_resolve_ui_language(context),
206
+ )
207
+ return result
208
+
209
+ self._commands[command.name] = SlashCommand(
210
+ name=command.name,
211
+ description=command.description,
212
+ handler=_localized_handler,
213
+ )
214
+
215
+ def lookup(self, raw_input: str) -> tuple[SlashCommand, str] | None:
216
+ """解析斜杠命令并返回其处理器和原始参数
217
+
218
+ Args:
219
+ raw_input: 原始输入字符串
220
+
221
+ Returns:
222
+ tuple[SlashCommand, str] | None: (命令对象, 参数) 或 None
223
+ """
224
+ if not raw_input.startswith("/"): # 不是斜杠命令
225
+ return None
226
+ name, _, args = raw_input[1:].partition(" ") # 分割名称和参数
227
+ command = self._commands.get(name) # 查找命令
228
+ if command is None: # 未找到
229
+ return None
230
+ return command, args.strip() # 返回命令和参数
231
+
232
+ def help_text(self) -> str:
233
+ """返回所有已注册命令的格式化摘要
234
+
235
+ Returns:
236
+ str: 格式化的命令帮助文本
237
+ """
238
+ locale = _resolve_ui_language(None)
239
+ lines = ["可用命令:" if _is_zh(locale) else "Available commands:"] # 标题
240
+ for command in sorted(self._commands.values(), key=lambda item: item.name): # 遍历命令
241
+ description = command.description
242
+ if _is_zh(locale):
243
+ description = COMMAND_DESCRIPTIONS_ZH.get(command.name, description)
244
+ lines.append(f"/{command.name:<12} {description}") # 格式化输出
245
+ return "\n".join(lines)
246
+
247
+ def list_commands(self) -> list[SlashCommand]:
248
+ """按照注册顺序返回命令列表
249
+
250
+ Returns:
251
+ list[SlashCommand]: 命令列表
252
+ """
253
+ return list(self._commands.values())
254
+
255
+
256
+ def _run_git_command(cwd: str, *args: str) -> tuple[bool, str]:
257
+ """执行git命令并返回结果
258
+
259
+ Args:
260
+ cwd: 工作目录
261
+ args: git子命令和参数
262
+
263
+ Returns:
264
+ tuple[bool, str]: (是否成功, 输出内容)
265
+ """
266
+ try:
267
+ run_kwargs: dict = {}
268
+ if sys.platform == "win32":
269
+ run_kwargs["creationflags"] = subprocess.CREATE_NO_WINDOW
270
+ completed = subprocess.run(
271
+ ["git", *args],
272
+ cwd=cwd,
273
+ capture_output=True,
274
+ text=True,
275
+ check=False,
276
+ stdin=subprocess.DEVNULL,
277
+ **run_kwargs,
278
+ )
279
+ except FileNotFoundError: # git未安装
280
+ return False, "git is not installed."
281
+ output = (completed.stdout or completed.stderr).strip() # 合并输出
282
+ if completed.returncode != 0: # 失败
283
+ return False, output or f"git {' '.join(args)} failed"
284
+ return True, output # 成功
285
+
286
+
287
+ def _copy_to_clipboard(text: str) -> tuple[bool, str]:
288
+ """复制文本到剪贴板
289
+
290
+ 尝试多种复制方式: pyperclip, pbcopy, wl-copy, xclip, xsel
291
+
292
+ Args:
293
+ text: 要复制的文本
294
+
295
+ Returns:
296
+ tuple[bool, str]: (是否成功, 目标位置)
297
+ """
298
+ try:
299
+ pyperclip.copy(text)
300
+ return True, "clipboard"
301
+ except Exception:
302
+ clip_kwargs: dict = {}
303
+ if sys.platform == "win32":
304
+ clip_kwargs["creationflags"] = subprocess.CREATE_NO_WINDOW
305
+ for command in (["pbcopy"], ["wl-copy"], ["xclip", "-selection", "clipboard"], ["xsel", "--clipboard"]):
306
+ try:
307
+ subprocess.run(command, input=text, text=True, check=True, capture_output=True, **clip_kwargs)
308
+ return True, "clipboard"
309
+ except Exception:
310
+ continue
311
+ fallback = get_data_dir() / "last_copy.txt" # 后备方案:文件
312
+ fallback.write_text(text, encoding="utf-8")
313
+ return False, str(fallback)
314
+
315
+
316
+ def _last_message_text(messages: list[ConversationMessage]) -> str:
317
+ """获取最后一条有内容的用户消息
318
+
319
+ Args:
320
+ messages: 消息列表
321
+
322
+ Returns:
323
+ str: 消息文本,空字符串若无
324
+ """
325
+ for message in reversed(messages): # 反向遍历
326
+ if message.text.strip(): # 有内容
327
+ return message.text.strip()
328
+ return ""
329
+
330
+
331
+ def _rewind_turns(messages: list[ConversationMessage], turns: int) -> list[ConversationMessage]:
332
+ """回退指定数量的对话回合
333
+
334
+ 回退到上一个非空的、非斜杠命令的 user 消息
335
+
336
+ Args:
337
+ messages: 消息列表
338
+ turns: 回退回合数
339
+
340
+ Returns:
341
+ list[ConversationMessage]: 回退后的消息列表
342
+ """
343
+ updated = list(messages)
344
+ for _ in range(max(0, turns)):
345
+ if not updated:
346
+ break
347
+ while updated:
348
+ popped = updated.pop()
349
+ if popped.role == "user" and popped.text.strip() and not popped.text.strip().startswith("/"):
350
+ break
351
+ return updated
352
+
353
+
354
+ def _coerce_setting_value(settings: Settings, key: str, raw: str):
355
+ """将字符串值强制转换为设置字段的正确类型
356
+
357
+ Args:
358
+ settings: 设置对象
359
+ key: 字段名
360
+ raw: 原始字符串值
361
+
362
+ Returns:
363
+ 转换后的值
364
+
365
+ Raises:
366
+ KeyError: 字段不存在
367
+ ValueError: 值无效
368
+ """
369
+ field = Settings.model_fields.get(key) # 获取字段定义
370
+ if field is None: # 不存在
371
+ raise KeyError(key)
372
+ annotation = field.annotation # 类型注解
373
+ if annotation is bool: # 布尔类型
374
+ lowered = raw.lower()
375
+ if lowered in {"1", "true", "yes", "on"}: # 真值
376
+ return True
377
+ if lowered in {"0", "false", "no", "off"}: # 假值
378
+ return False
379
+ raise ValueError(f"Invalid boolean value for {key}: {raw}")
380
+ if annotation is int: # 整数类型
381
+ return int(raw)
382
+ if annotation is str: # 字符串类型
383
+ return raw
384
+ if annotation is Literal or getattr(annotation, "__origin__", None) is Literal: # 字面量类型
385
+ allowed = get_args(annotation)
386
+ if raw not in allowed: # 不在允许值中
387
+ raise ValueError(f"Invalid value for {key}: {raw}")
388
+ return raw
389
+ return raw
390
+
391
+
392
+ def _explore_codebase(root: Path) -> dict[str, Any]:
393
+ """探索代码库结构,识别项目类型和工具链
394
+
395
+ Args:
396
+ root: 项目根目录
397
+
398
+ Returns:
399
+ 包含项目信息的字典
400
+ """
401
+ findings: dict[str, Any] = {
402
+ "languages": [],
403
+ "frameworks": [],
404
+ "package_manager": None,
405
+ "build_commands": [],
406
+ "test_commands": [],
407
+ "lint_commands": [],
408
+ "format_commands": [],
409
+ "existing_configs": [],
410
+ "ci_config": None,
411
+ "has_gitignore": False,
412
+ "readme_summary": None,
413
+ }
414
+
415
+ # 扫描文件结构
416
+ try:
417
+ all_files = [
418
+ p for p in root.rglob("*")
419
+ if p.is_file()
420
+ and ".git" not in p.parts
421
+ and ".venv" not in p.parts
422
+ and "node_modules" not in p.parts
423
+ and "__pycache__" not in p.parts
424
+ ]
425
+ except Exception:
426
+ all_files = []
427
+
428
+ # 检测语言
429
+ lang_indicators = {
430
+ ".py": "Python",
431
+ ".js": "JavaScript",
432
+ ".ts": "TypeScript",
433
+ ".jsx": "React",
434
+ ".tsx": "React",
435
+ ".java": "Java",
436
+ ".go": "Go",
437
+ ".rs": "Rust",
438
+ ".rb": "Ruby",
439
+ ".php": "PHP",
440
+ ".cs": "C#",
441
+ ".cpp": "C++",
442
+ ".c": "C",
443
+ ".swift": "Swift",
444
+ ".kt": "Kotlin",
445
+ }
446
+
447
+ detected_langs = set()
448
+ for f in all_files:
449
+ if f.suffix in lang_indicators:
450
+ detected_langs.add(lang_indicators[f.suffix])
451
+ findings["languages"] = sorted(detected_langs)
452
+
453
+ # 检测框架和语言指示文件
454
+ framework_indicators = {
455
+ "package.json": None,
456
+ "requirements.txt": "Python",
457
+ "pyproject.toml": "Python",
458
+ "setup.py": "Python",
459
+ "Cargo.toml": "Rust",
460
+ "go.mod": "Go",
461
+ "pom.xml": "Java",
462
+ "build.gradle": "Java",
463
+ "Gemfile": "Ruby",
464
+ "composer.json": "PHP",
465
+ }
466
+
467
+ for indicator, lang in framework_indicators.items():
468
+ if (root / indicator).exists():
469
+ if lang and lang not in findings["languages"]:
470
+ findings["languages"].append(lang)
471
+
472
+ # 检测配置文件关联的语言
473
+ config_lang_indicators = {
474
+ "tsconfig.json": "TypeScript",
475
+ "jsconfig.json": "JavaScript",
476
+ "webpack.config.js": "JavaScript",
477
+ "vite.config.ts": "TypeScript",
478
+ "vite.config.js": "JavaScript",
479
+ "next.config.js": "Next.js",
480
+ "nuxt.config.js": "Nuxt",
481
+ "angular.json": "Angular",
482
+ "vue.config.js": "Vue",
483
+ "svelte.config.js": "Svelte",
484
+ }
485
+
486
+ for config_file, lang in config_lang_indicators.items():
487
+ if (root / config_file).exists():
488
+ if lang not in findings["languages"] and lang not in ["Next.js", "Nuxt", "Angular", "Vue", "Svelte"]:
489
+ findings["languages"].append(lang)
490
+ elif lang in ["Next.js", "Nuxt", "Angular", "Vue", "Svelte"] and lang not in findings["frameworks"]:
491
+ findings["frameworks"].append(lang)
492
+
493
+ # 检测包管理器
494
+ if (root / "package.json").exists():
495
+ if (root / "yarn.lock").exists():
496
+ findings["package_manager"] = "yarn"
497
+ elif (root / "pnpm-lock.yaml").exists():
498
+ findings["package_manager"] = "pnpm"
499
+ else:
500
+ findings["package_manager"] = "npm"
501
+ elif (root / "requirements.txt").exists() or (root / "pyproject.toml").exists():
502
+ findings["package_manager"] = "pip"
503
+ elif (root / "Cargo.toml").exists():
504
+ findings["package_manager"] = "cargo"
505
+ elif (root / "go.mod").exists():
506
+ findings["package_manager"] = "go"
507
+
508
+ # 读取 package.json 获取脚本
509
+ package_json = root / "package.json"
510
+ if package_json.exists():
511
+ try:
512
+ with open(package_json, encoding="utf-8") as f:
513
+ data = json.load(f)
514
+ scripts = data.get("scripts", {})
515
+ if "build" in scripts:
516
+ findings["build_commands"].append("npm run build")
517
+ if "test" in scripts:
518
+ findings["test_commands"].append("npm test")
519
+ if "lint" in scripts:
520
+ findings["lint_commands"].append("npm run lint")
521
+ if "dev" in scripts:
522
+ findings["build_commands"].append("npm run dev")
523
+ if "format" in scripts:
524
+ findings["format_commands"].append("npm run format")
525
+ except Exception:
526
+ pass
527
+
528
+ # 读取 pyproject.toml 获取命令
529
+ pyproject = root / "pyproject.toml"
530
+ if pyproject.exists():
531
+ try:
532
+ import tomllib
533
+ with open(pyproject, "rb") as f:
534
+ data = tomllib.load(f)
535
+ scripts = data.get("tool", {}).get("poetry", {}).get("scripts", {})
536
+ if scripts:
537
+ findings["build_commands"].append("poetry run <script>")
538
+ # 检测 ruff/black 格式化配置
539
+ if "tool" in data:
540
+ if "ruff" in data["tool"]:
541
+ findings["format_commands"].append("ruff format")
542
+ if "lint" not in findings["lint_commands"]:
543
+ findings["lint_commands"].append("ruff check")
544
+ if "black" in data["tool"]:
545
+ findings["format_commands"].append("black")
546
+ except Exception:
547
+ pass
548
+
549
+ # 检测 Makefile
550
+ makefile = root / "Makefile"
551
+ if makefile.exists():
552
+ try:
553
+ content = makefile.read_text(encoding="utf-8")
554
+ if "build:" in content:
555
+ findings["build_commands"].append("make build")
556
+ if "test:" in content:
557
+ findings["test_commands"].append("make test")
558
+ if "lint:" in content:
559
+ findings["lint_commands"].append("make lint")
560
+ if "fmt:" in content or "format:" in content:
561
+ findings["format_commands"].append("make fmt")
562
+ except Exception:
563
+ pass
564
+
565
+ # 检测格式化工具配置
566
+ format_configs = {
567
+ ".prettierrc": "prettier",
568
+ ".prettierrc.json": "prettier",
569
+ ".prettierrc.js": "prettier",
570
+ "prettier.config.js": "prettier",
571
+ "biome.json": "biome",
572
+ ".eslintrc": "eslint",
573
+ ".eslintrc.json": "eslint",
574
+ ".eslintrc.js": "eslint",
575
+ "eslint.config.js": "eslint",
576
+ ".golangci.yml": "golangci-lint",
577
+ ".golangci.yaml": "golangci-lint",
578
+ "rustfmt.toml": "rustfmt",
579
+ ".rustfmt.toml": "rustfmt",
580
+ }
581
+
582
+ for config_file, tool_name in format_configs.items():
583
+ if (root / config_file).exists():
584
+ if tool_name not in findings["format_commands"]:
585
+ findings["format_commands"].append(tool_name)
586
+
587
+ # 检测 CI 配置
588
+ ci_configs = {
589
+ ".github/workflows": "GitHub Actions",
590
+ ".gitlab-ci.yml": "GitLab CI",
591
+ "Jenkinsfile": "Jenkins",
592
+ ".circleci/config.yml": "CircleCI",
593
+ ".travis.yml": "Travis CI",
594
+ }
595
+
596
+ for ci_path, ci_name in ci_configs.items():
597
+ if (root / ci_path).exists():
598
+ findings["ci_config"] = ci_name
599
+ break
600
+
601
+ # 检测 .gitignore
602
+ findings["has_gitignore"] = (root / ".gitignore").exists()
603
+
604
+ # 检测现有 AI 配置
605
+ ai_configs = [
606
+ ".cursor/rules",
607
+ ".cursorrules",
608
+ ".github/copilot-instructions.md",
609
+ ".windsurfrules",
610
+ ".clinerules",
611
+ "AGENTS.md",
612
+ "CLAUDE.md",
613
+ "ILLUSION.md",
614
+ ]
615
+ for config in ai_configs:
616
+ if (root / config).exists():
617
+ findings["existing_configs"].append(config)
618
+
619
+ # 检测常用框架
620
+ if (root / "package.json").exists():
621
+ try:
622
+ with open(root / "package.json", encoding="utf-8") as f:
623
+ data = json.load(f)
624
+ deps = {**data.get("dependencies", {}), **data.get("devDependencies", {})}
625
+ if "react" in deps:
626
+ findings["frameworks"].append("React")
627
+ if "vue" in deps:
628
+ findings["frameworks"].append("Vue")
629
+ if "svelte" in deps:
630
+ findings["frameworks"].append("Svelte")
631
+ if "next" in deps:
632
+ findings["frameworks"].append("Next.js")
633
+ if "nuxt" in deps:
634
+ findings["frameworks"].append("Nuxt")
635
+ if "express" in deps:
636
+ findings["frameworks"].append("Express")
637
+ if "fastapi" in deps:
638
+ findings["frameworks"].append("FastAPI")
639
+ if "django" in deps:
640
+ findings["frameworks"].append("Django")
641
+ if "flask" in deps:
642
+ findings["frameworks"].append("Flask")
643
+ if "angular" in deps or "@angular/core" in deps:
644
+ findings["frameworks"].append("Angular")
645
+ except Exception:
646
+ pass
647
+
648
+ # 提取 README 摘要
649
+ readme = root / "README.md"
650
+ if readme.exists():
651
+ try:
652
+ content = readme.read_text(encoding="utf-8")
653
+ # 提取前几行作为项目描述,跳过 HTML 和图片
654
+ desc_lines = []
655
+ in_code_block = False
656
+ in_html_block = False
657
+ for line in content.split("\n")[:50]:
658
+ stripped = line.strip()
659
+ # 跳过代码块
660
+ if stripped.startswith("```"):
661
+ in_code_block = not in_code_block
662
+ continue
663
+ if in_code_block:
664
+ continue
665
+ # 跳过 HTML 块
666
+ if stripped.startswith("<") and not stripped.startswith("</"):
667
+ if stripped.endswith(">") and "/" not in stripped:
668
+ in_html_block = True
669
+ continue
670
+ if in_html_block:
671
+ if stripped.startswith("</") or stripped.endswith("/>"):
672
+ in_html_block = False
673
+ continue
674
+ # 跳过图片和空行
675
+ if stripped.startswith("![") or not stripped:
676
+ continue
677
+ # 跳过标题行
678
+ if stripped.startswith("#"):
679
+ continue
680
+ # 跳过 badge 和链接
681
+ if stripped.startswith("[") and "badges" in stripped.lower():
682
+ continue
683
+ # 保留有意义的文本
684
+ if len(stripped) > 10: # 过短的行通常是装饰
685
+ desc_lines.append(stripped)
686
+ if len(desc_lines) >= 3:
687
+ break
688
+ if desc_lines:
689
+ findings["readme_summary"] = " ".join(desc_lines)
690
+ except Exception:
691
+ pass
692
+
693
+ return findings
694
+
695
+
696
+ def _generate_claudemd(findings: dict[str, Any], root: Path) -> str:
697
+ """基于项目发现生成 CLAUDE.md 内容
698
+
699
+ Args:
700
+ findings: 项目探索结果
701
+ root: 项目根目录
702
+
703
+ Returns:
704
+ CLAUDE.md 内容
705
+ """
706
+ lines = [
707
+ "# CLAUDE.md",
708
+ "",
709
+ "This file provides guidance to Illusion Code when working with code in this repository.",
710
+ "",
711
+ ]
712
+
713
+ # 项目概述
714
+ if findings.get("readme_summary"):
715
+ lines.append("## 项目概述")
716
+ lines.append(findings["readme_summary"])
717
+ lines.append("")
718
+
719
+ # 技术栈
720
+ if findings["languages"] or findings["frameworks"] or findings["package_manager"]:
721
+ lines.append("## 技术栈")
722
+ if findings["languages"]:
723
+ lines.append(f"- 主要语言: {', '.join(findings['languages'])}")
724
+ if findings["frameworks"]:
725
+ lines.append(f"- 框架: {', '.join(findings['frameworks'])}")
726
+ if findings["package_manager"]:
727
+ lines.append(f"- 包管理器: {findings['package_manager']}")
728
+ if findings.get("ci_config"):
729
+ lines.append(f"- CI/CD: {findings['ci_config']}")
730
+ lines.append("")
731
+
732
+ # 常用命令
733
+ has_commands = any([
734
+ findings["build_commands"],
735
+ findings["test_commands"],
736
+ findings["lint_commands"],
737
+ findings.get("format_commands")
738
+ ])
739
+ if has_commands:
740
+ lines.append("## 常用命令")
741
+ if findings["build_commands"]:
742
+ lines.append(f"- 构建: `{findings['build_commands'][0]}`")
743
+ if findings["test_commands"]:
744
+ lines.append(f"- 测试: `{findings['test_commands'][0]}`")
745
+ if findings["lint_commands"]:
746
+ lines.append(f"- 代码检查: `{findings['lint_commands'][0]}`")
747
+ if findings.get("format_commands"):
748
+ lines.append(f"- 格式化: `{findings['format_commands'][0]}`")
749
+ lines.append("")
750
+
751
+ # 开发规范
752
+ lines.extend([
753
+ "## 开发规范",
754
+ "- 修改后请验证测试是否通过",
755
+ "- 保持代码变更最小化",
756
+ ])
757
+
758
+ # 如果有格式化工具,添加格式化规范
759
+ if findings.get("format_commands"):
760
+ lines.append(f"- 代码格式化: 使用 `{findings['format_commands'][0]}`")
761
+
762
+ lines.append("")
763
+
764
+ # 如果有现有配置,提示用户
765
+ if findings["existing_configs"]:
766
+ lines.append("## 现有 AI 配置")
767
+ lines.append("检测到以下 AI 配置文件,内容可能需要合并:")
768
+ for config in findings["existing_configs"]:
769
+ lines.append(f"- `{config}`")
770
+ lines.append("")
771
+
772
+ return "\n".join(lines)
773
+
774
+
775
+ def create_default_command_registry() -> CommandRegistry:
776
+ """Create the built-in command registry."""
777
+ registry = CommandRegistry()
778
+
779
+ async def _exit_handler(_: str, context: CommandContext) -> CommandResult:
780
+ del context
781
+ return CommandResult(should_exit=True)
782
+
783
+ async def _new_handler(_: str, context: CommandContext) -> CommandResult:
784
+ if context.session_id and context.engine.messages:
785
+ settings = load_settings()
786
+ system_prompt = build_runtime_system_prompt(settings, cwd=context.cwd)
787
+ save_session_snapshot(
788
+ cwd=context.cwd,
789
+ model=settings.active_model_name,
790
+ system_prompt=system_prompt,
791
+ messages=context.engine.messages,
792
+ usage=context.engine.total_usage,
793
+ session_id=context.session_id,
794
+ )
795
+ context.engine.clear()
796
+ return CommandResult(
797
+ message="Started a new conversation session.",
798
+ clear_screen=True,
799
+ reset_session=True,
800
+ )
801
+
802
+ async def _status_handler(_: str, context: CommandContext) -> CommandResult:
803
+ usage = context.engine.total_usage
804
+ state = context.app_state.get() if context.app_state is not None else None
805
+ return CommandResult(
806
+ message=(
807
+ f"Messages: {len(context.engine.messages)}\n"
808
+ f"Usage: input={usage.input_tokens} output={usage.output_tokens}\n"
809
+ f"Effort: {state.effort if state is not None else load_settings().effort}\n"
810
+ f"Passes: {state.passes if state is not None else load_settings().passes}"
811
+ )
812
+ )
813
+
814
+ async def _version_handler(_: str, context: CommandContext) -> CommandResult:
815
+ del context
816
+ try:
817
+ version = importlib.metadata.version("illusion")
818
+ except importlib.metadata.PackageNotFoundError:
819
+ version = "0.1.0"
820
+ return CommandResult(message=f"IllusionCode {version}")
821
+
822
+ async def _context_handler(args: str, context: CommandContext) -> CommandResult:
823
+ settings = load_settings()
824
+ tokens = args.split(maxsplit=1)
825
+ subcommand = tokens[0] if tokens else "prompt"
826
+
827
+ if subcommand == "prompt":
828
+ prompt = build_runtime_system_prompt(settings, cwd=context.cwd)
829
+ return CommandResult(message=prompt)
830
+ if subcommand == "window" or subcommand == "show":
831
+ return CommandResult(message=f"Context window: {settings.context_window:,} tokens")
832
+ if subcommand == "__usage__":
833
+ from illusion.services.compact import estimate_conversation_tokens, get_context_window
834
+ estimated = estimate_conversation_tokens(context.engine.messages)
835
+ usage = context.engine.total_usage
836
+ context_window = get_context_window(settings.active_model_name)
837
+ percentage = int(estimated * 100 / context_window) if context_window > 0 else 0
838
+ remaining = max(0, context_window - estimated)
839
+ return CommandResult(
840
+ message=(
841
+ f"Context Window: {context_window:,} tokens\n"
842
+ f"Estimated Used: ~{estimated:,} tokens ({percentage}%)\n"
843
+ f"Remaining: ~{remaining:,} tokens\n"
844
+ f"Actual API Usage: input={usage.input_tokens:,} output={usage.output_tokens:,}\n"
845
+ f"Messages: {len(context.engine.messages)}"
846
+ )
847
+ )
848
+ if subcommand == "set" and len(tokens) == 2:
849
+ try:
850
+ value = int(tokens[1])
851
+ if value <= 0:
852
+ return CommandResult(message="Error: context window must be positive")
853
+ settings.context_window = value
854
+ save_settings(settings)
855
+ return CommandResult(message=f"Context window set to {value:,} tokens")
856
+ except ValueError:
857
+ return CommandResult(message="Error: invalid number")
858
+ return CommandResult(message="Usage: /context [prompt|window|set N]")
859
+
860
+ async def _summary_handler(args: str, context: CommandContext) -> CommandResult:
861
+ max_messages = 8
862
+ if args:
863
+ try:
864
+ max_messages = max(1, int(args))
865
+ except ValueError:
866
+ return CommandResult(message="Usage: /summary [MAX_MESSAGES]")
867
+ summary = summarize_messages(context.engine.messages, max_messages=max_messages)
868
+ return CommandResult(message=summary or "No conversation content to summarize.")
869
+
870
+ async def _compact_handler(args: str, context: CommandContext) -> CommandResult:
871
+ from illusion.services.compact import compact_conversation, compact_messages
872
+
873
+ # 解析参数:/compact [PRESERVE_RECENT] 或 /compact [custom instructions text]
874
+ preserve_recent = 6
875
+ custom_instructions: str | None = None
876
+
877
+ if args:
878
+ stripped = args.strip()
879
+ # 尝试解析为数字(preserve_recent)
880
+ try:
881
+ preserve_recent = max(1, int(stripped))
882
+ except ValueError:
883
+ # 非数字则视为自定义指令
884
+ custom_instructions = stripped
885
+
886
+ before = len(context.engine.messages)
887
+ before_tokens = estimate_conversation_tokens(context.engine.messages)
888
+
889
+ # 优先尝试 LLM 摘要;如果 API 客户端不可用则回退到传统方法
890
+ try:
891
+ settings = load_settings()
892
+ system_prompt = build_runtime_system_prompt(settings, cwd=context.cwd)
893
+ compacted = await compact_conversation(
894
+ context.engine.messages,
895
+ api_client=context.engine._api_client,
896
+ model=context.engine._model,
897
+ system_prompt=system_prompt,
898
+ preserve_recent=preserve_recent,
899
+ custom_instructions=custom_instructions,
900
+ suppress_follow_up=False,
901
+ )
902
+ except Exception as exc:
903
+ # LLM 摘要失败,回退到传统方法
904
+ import logging
905
+ logging.getLogger(__name__).warning("LLM compact failed, falling back to simple compact: %s", exc)
906
+ compacted = compact_messages(context.engine.messages, preserve_recent=preserve_recent)
907
+
908
+ context.engine.load_messages(compacted)
909
+ after_tokens = estimate_conversation_tokens(compacted)
910
+ saved = max(0, before_tokens - after_tokens)
911
+ from illusion.config.i18n import t
912
+ return CommandResult(
913
+ message=t("compact_result", before=before, after=len(compacted), saved=f"{saved:,}")
914
+ )
915
+
916
+ async def _memory_handler(args: str, context: CommandContext) -> CommandResult:
917
+ tokens = args.split(maxsplit=1)
918
+ if not tokens:
919
+ memory_dir = get_project_memory_dir(context.cwd)
920
+ entrypoint = get_memory_entrypoint(context.cwd)
921
+ return CommandResult(
922
+ message=f"Memory directory: {memory_dir}\nEntrypoint: {entrypoint}"
923
+ )
924
+ action = tokens[0]
925
+ rest = tokens[1] if len(tokens) == 2 else ""
926
+ if action == "list":
927
+ memory_files = list_memory_files(context.cwd)
928
+ if not memory_files:
929
+ return CommandResult(message="No memory files.")
930
+ return CommandResult(message="\n".join(path.name for path in memory_files))
931
+ if action == "show" and rest:
932
+ memory_dir = get_project_memory_dir(context.cwd)
933
+ path = memory_dir / rest
934
+ if not path.exists():
935
+ path = memory_dir / f"{rest}.md"
936
+ if not path.exists():
937
+ return CommandResult(message=f"Memory entry not found: {rest}")
938
+ return CommandResult(message=path.read_text(encoding="utf-8"))
939
+ if action == "add" and rest:
940
+ title, separator, content = rest.partition("::")
941
+ if not separator or not title.strip() or not content.strip():
942
+ return CommandResult(message="Usage: /memory add TITLE :: CONTENT")
943
+ path = add_memory_entry(context.cwd, title.strip(), content.strip())
944
+ return CommandResult(message=f"Added memory entry {path.name}")
945
+ if action == "remove" and rest:
946
+ if remove_memory_entry(context.cwd, rest.strip()):
947
+ return CommandResult(message=f"Removed memory entry {rest.strip()}")
948
+ return CommandResult(message=f"Memory entry not found: {rest.strip()}")
949
+ return CommandResult(message="Usage: /memory [list|show NAME|add TITLE :: CONTENT|remove NAME]")
950
+
951
+ async def _hooks_handler(_: str, context: CommandContext) -> CommandResult:
952
+ return CommandResult(message=context.hooks_summary or "No hooks configured.")
953
+
954
+ async def _resume_handler(args: str, context: CommandContext) -> CommandResult:
955
+ from illusion.services.session_storage import list_session_snapshots, load_session_by_id
956
+
957
+ tokens = args.strip().split()
958
+
959
+ # /resume <session_id> — load a specific session
960
+ if tokens:
961
+ sid = tokens[0]
962
+ snapshot = load_session_by_id(context.cwd, sid)
963
+ if snapshot is None:
964
+ return CommandResult(message=f"Session not found: {sid}")
965
+ messages = [
966
+ ConversationMessage.model_validate(item)
967
+ for item in snapshot.get("messages", [])
968
+ ]
969
+ context.engine.load_messages(messages)
970
+ summary = snapshot.get("summary", "")[:60]
971
+ return CommandResult(
972
+ message=f"Restored {len(messages)} messages from session {sid}"
973
+ + (f" ({summary})" if summary else ""),
974
+ replay_messages=messages,
975
+ restored_session_id=str(snapshot.get("session_id") or sid),
976
+ )
977
+
978
+ # /resume — list sessions (for the TUI to show a picker)
979
+ sessions = list_session_snapshots(context.cwd, limit=10)
980
+ if not sessions:
981
+ # Fall back to latest.json
982
+ snapshot = load_session_snapshot(context.cwd)
983
+ if snapshot is None:
984
+ return CommandResult(message="No saved sessions found for this project.")
985
+ messages = [
986
+ ConversationMessage.model_validate(item)
987
+ for item in snapshot.get("messages", [])
988
+ ]
989
+ context.engine.load_messages(messages)
990
+ return CommandResult(
991
+ message=f"Restored {len(messages)} messages from the latest session.",
992
+ replay_messages=messages,
993
+ restored_session_id=str(snapshot.get("session_id", "")),
994
+ )
995
+
996
+ # Format session list for display / picker
997
+ import time
998
+ lines = ["Saved sessions:"]
999
+ for s in sessions:
1000
+ ts = time.strftime("%m/%d %H:%M", time.localtime(s["created_at"]))
1001
+ summary = s["summary"][:50] or "(no summary)"
1002
+ lines.append(f" {s['session_id']} {ts} {s['message_count']}msg {summary}")
1003
+ lines.append("")
1004
+ lines.append("Use /resume <session_id> to restore a specific session.")
1005
+ return CommandResult(message="\n".join(lines))
1006
+
1007
+ async def _export_handler(_: str, context: CommandContext) -> CommandResult:
1008
+ path = export_session_markdown(cwd=context.cwd, messages=context.engine.messages)
1009
+ return CommandResult(message=f"Exported transcript to {path}")
1010
+
1011
+ async def _share_handler(_: str, context: CommandContext) -> CommandResult:
1012
+ path = export_session_markdown(cwd=context.cwd, messages=context.engine.messages)
1013
+ return CommandResult(message=f"Created shareable transcript snapshot at {path}")
1014
+
1015
+ async def _copy_handler(args: str, context: CommandContext) -> CommandResult:
1016
+ text = args.strip() or _last_message_text(context.engine.messages)
1017
+ if not text:
1018
+ return CommandResult(message="Nothing to copy.")
1019
+ copied, target = _copy_to_clipboard(text)
1020
+ if copied:
1021
+ return CommandResult(message=f"Copied {len(text)} characters to the clipboard.")
1022
+ return CommandResult(message=f"Clipboard unavailable. Saved copied text to {target}")
1023
+
1024
+ async def _rewind_handler(args: str, context: CommandContext) -> CommandResult:
1025
+ turns = 1
1026
+ if args.strip():
1027
+ try:
1028
+ turns = max(1, int(args.strip()))
1029
+ except ValueError:
1030
+ return CommandResult(message="Usage: /rewind [TURNS]")
1031
+ before = len(context.engine.messages)
1032
+ updated = _rewind_turns(context.engine.messages, turns)
1033
+ removed = before - len(updated)
1034
+
1035
+ # 文件回退:找到目标快照并恢复文件
1036
+ reverted_count = 0
1037
+ fh = context.engine.file_history
1038
+ if fh is not None and fh.snapshots:
1039
+ # 目标轮次索引 = 当前快照数 - 回退轮次数
1040
+ target_turn = max(0, len(fh.snapshots) - turns)
1041
+ reverted_files = rewind_to(fh, target_turn)
1042
+ reverted_count = len(reverted_files)
1043
+
1044
+ # 硬删除:直接截断消息列表
1045
+ context.engine.load_messages(updated)
1046
+
1047
+ # 构建反馈消息
1048
+ lines = [f"Rewound {turns} turn(s); removed {removed} message(s)."]
1049
+ if reverted_count > 0:
1050
+ lines.append(f"Reverted {reverted_count} file(s).")
1051
+
1052
+ return CommandResult(
1053
+ clear_screen=True,
1054
+ replay_messages=list(updated),
1055
+ message="\n".join(lines),
1056
+ )
1057
+
1058
+ async def _files_handler(args: str, context: CommandContext) -> CommandResult:
1059
+ raw = args.strip()
1060
+ root = Path(context.cwd)
1061
+ max_items = 30
1062
+ tokens = raw.split(maxsplit=1)
1063
+ if tokens and tokens[0] == "dirs":
1064
+ dirs = [
1065
+ path
1066
+ for path in sorted(root.rglob("*"))
1067
+ if path.is_dir() and ".git" not in path.parts and ".venv" not in path.parts
1068
+ ]
1069
+ lines = [str(path.relative_to(root)) for path in dirs[:max_items]]
1070
+ if len(dirs) > max_items:
1071
+ lines.append(f"... {len(dirs) - max_items} more")
1072
+ return CommandResult(message="\n".join(lines) if lines else "(no directories)")
1073
+ if tokens and tokens[0].isdigit():
1074
+ max_items = max(1, min(int(tokens[0]), 200))
1075
+ raw = tokens[1] if len(tokens) == 2 else ""
1076
+ needle = raw.lower()
1077
+ files = [
1078
+ path
1079
+ for path in sorted(root.rglob("*"))
1080
+ if path.is_file() and ".git" not in path.parts and ".venv" not in path.parts
1081
+ ]
1082
+ if needle:
1083
+ files = [path for path in files if needle in str(path.relative_to(root)).lower()]
1084
+ lines = [str(path.relative_to(root)) for path in files[:max_items]]
1085
+ if len(files) > max_items:
1086
+ lines.append(f"... {len(files) - max_items} more")
1087
+ return CommandResult(
1088
+ message="\n".join(lines) if lines else "(no matching files)"
1089
+ )
1090
+
1091
+ async def _init_handler(args: str, context: CommandContext) -> CommandResult:
1092
+ """智能初始化项目配置
1093
+
1094
+ 流程:
1095
+ 1. 探索代码库结构
1096
+ 2. 识别项目类型和工具链
1097
+ 3. 生成有针对性的配置
1098
+ """
1099
+ del args
1100
+ root = Path(context.cwd)
1101
+ project_dir = get_project_config_dir(context.cwd)
1102
+ created: list[str] = []
1103
+ findings: dict[str, Any] = {}
1104
+
1105
+ # Phase 1: 探索代码库
1106
+ findings = _explore_codebase(root)
1107
+
1108
+ # Phase 2: 生成 CLAUDE.md
1109
+ claudemd = root / "CLAUDE.md"
1110
+ if not claudemd.exists():
1111
+ content = _generate_claudemd(findings, root)
1112
+ claudemd.write_text(content, encoding="utf-8")
1113
+ created.append("CLAUDE.md")
1114
+
1115
+ # Phase 3: 创建项目配置目录
1116
+ for relative, content in (
1117
+ (
1118
+ project_dir / "README.md",
1119
+ "# Project IllusionCode Config\n\nThis directory stores project-specific IllusionCode state.\n",
1120
+ ),
1121
+ (
1122
+ project_dir / "memory" / "MEMORY.md",
1123
+ "# Project Memory\n\nAdd reusable project knowledge here.\n",
1124
+ ),
1125
+ (
1126
+ project_dir / "plugins" / ".gitkeep",
1127
+ "",
1128
+ ),
1129
+ (
1130
+ project_dir / "skills" / ".gitkeep",
1131
+ "",
1132
+ ),
1133
+ ):
1134
+ relative.parent.mkdir(parents=True, exist_ok=True)
1135
+ if not relative.exists():
1136
+ relative.write_text(content, encoding="utf-8")
1137
+ created.append(str(relative.relative_to(root)))
1138
+
1139
+ # Phase 4: 生成报告
1140
+ if not created:
1141
+ return CommandResult(message="Project already initialized for IllusionCode.")
1142
+
1143
+ report_lines = [
1144
+ "✨ **Illusion Code 项目初始化完成**\n",
1145
+ "## 已创建文件",
1146
+ *[f"- {item}" for item in created],
1147
+ "",
1148
+ "## 项目分析",
1149
+ ]
1150
+
1151
+ if findings.get("languages"):
1152
+ report_lines.append(f"- **检测到语言**: {', '.join(findings['languages'])}")
1153
+ if findings.get("frameworks"):
1154
+ report_lines.append(f"- **检测到框架**: {', '.join(findings['frameworks'])}")
1155
+ if findings.get("package_manager"):
1156
+ report_lines.append(f"- **包管理器**: {findings['package_manager']}")
1157
+ if findings.get("build_commands"):
1158
+ report_lines.append(f"- **构建命令**: {', '.join(findings['build_commands'])}")
1159
+ if findings.get("test_commands"):
1160
+ report_lines.append(f"- **测试命令**: {', '.join(findings['test_commands'])}")
1161
+ if findings.get("lint_commands"):
1162
+ report_lines.append(f"- **代码检查**: {', '.join(findings['lint_commands'])}")
1163
+ if findings.get("format_commands"):
1164
+ report_lines.append(f"- **格式化工具**: {', '.join(findings['format_commands'])}")
1165
+ if findings.get("ci_config"):
1166
+ report_lines.append(f"- **CI/CD**: {findings['ci_config']}")
1167
+
1168
+ report_lines.extend([
1169
+ "",
1170
+ "## 下一步建议",
1171
+ "- 查看 `CLAUDE.md` 了解项目配置",
1172
+ "- 运行 `/memory` 管理项目记忆",
1173
+ "- 运行 `/skills` 查看可用技能",
1174
+ "- 根据需要调整 CLAUDE.md 中的配置",
1175
+ ])
1176
+
1177
+ return CommandResult(message="\n".join(report_lines))
1178
+
1179
+ async def _bridge_handler(args: str, context: CommandContext) -> CommandResult:
1180
+ tokens = args.split()
1181
+ if not tokens or tokens[0] == "show":
1182
+ sessions = get_bridge_manager().list_sessions()
1183
+ lines = [
1184
+ "Bridge summary:",
1185
+ "- backend host: available",
1186
+ f"- cwd: {context.cwd}",
1187
+ f"- sessions: {len(sessions)}",
1188
+ "- utilities: encode, decode, sdk, spawn, list, output, stop",
1189
+ ]
1190
+ return CommandResult(message="\n".join(lines))
1191
+ if tokens[0] == "encode" and len(tokens) == 3:
1192
+ encoded = encode_work_secret(
1193
+ WorkSecret(version=1, session_ingress_token=tokens[2], api_base_url=tokens[1])
1194
+ )
1195
+ return CommandResult(message=encoded)
1196
+ if tokens[0] == "decode" and len(tokens) == 2:
1197
+ secret = decode_work_secret(tokens[1])
1198
+ return CommandResult(message=json.dumps(secret.__dict__, indent=2))
1199
+ if tokens[0] == "sdk" and len(tokens) == 3:
1200
+ return CommandResult(message=build_sdk_url(tokens[1], tokens[2]))
1201
+ if tokens[0] == "spawn" and len(tokens) >= 2:
1202
+ command = args[len("spawn ") :]
1203
+ handle = await get_bridge_manager().spawn(
1204
+ session_id=f"bridge-{datetime.now(timezone.utc).strftime('%H%M%S')}",
1205
+ command=command,
1206
+ cwd=context.cwd,
1207
+ )
1208
+ return CommandResult(
1209
+ message=f"Spawned bridge session {handle.session_id} pid={handle.process.pid}"
1210
+ )
1211
+ if tokens[0] == "list":
1212
+ sessions = get_bridge_manager().list_sessions()
1213
+ if not sessions:
1214
+ return CommandResult(message="No bridge sessions.")
1215
+ return CommandResult(
1216
+ message="\n".join(
1217
+ f"{item.session_id} [{item.status}] pid={item.pid} {item.command}"
1218
+ for item in sessions
1219
+ )
1220
+ )
1221
+ if tokens[0] == "output" and len(tokens) == 2:
1222
+ return CommandResult(message=get_bridge_manager().read_output(tokens[1]) or "(no output)")
1223
+ if tokens[0] == "stop" and len(tokens) == 2:
1224
+ try:
1225
+ await get_bridge_manager().stop(tokens[1])
1226
+ except ValueError as exc:
1227
+ return CommandResult(message=str(exc))
1228
+ return CommandResult(message=f"Stopped bridge session {tokens[1]}")
1229
+ return CommandResult(
1230
+ message="Usage: /bridge [show|encode API_BASE_URL TOKEN|decode SECRET|sdk API_BASE_URL SESSION_ID|spawn CMD|list|output SESSION_ID|stop SESSION_ID]"
1231
+ )
1232
+
1233
+ async def _reload_plugins_handler(_: str, context: CommandContext) -> CommandResult:
1234
+ settings = load_settings()
1235
+ plugins = load_plugins(settings, context.cwd)
1236
+ if not plugins:
1237
+ return CommandResult(message="No plugins discovered.")
1238
+ lines = ["Reloaded plugins:"]
1239
+ for plugin in plugins:
1240
+ state = "enabled" if plugin.enabled else "disabled"
1241
+ lines.append(f"- {plugin.manifest.name} [{state}]")
1242
+ return CommandResult(message="\n".join(lines))
1243
+
1244
+ async def _skills_handler(args: str, context: CommandContext) -> CommandResult:
1245
+ skill_registry = load_skill_registry(context.cwd)
1246
+ if args:
1247
+ skill = skill_registry.get(args)
1248
+ if skill is None:
1249
+ return CommandResult(message=f"Skill not found: {args}")
1250
+ return CommandResult(message=skill.content)
1251
+ skills = skill_registry.list_skills()
1252
+ if not skills:
1253
+ return CommandResult(message="No skills available.")
1254
+ lines = ["Available skills:"]
1255
+ for skill in skills:
1256
+ source = f" [{skill.source}]"
1257
+ lines.append(f"- {skill.name}{source}: {skill.description}")
1258
+ return CommandResult(message="\n".join(lines))
1259
+
1260
+ async def _config_handler(args: str, context: CommandContext) -> CommandResult:
1261
+ del context
1262
+ settings = load_settings()
1263
+ tokens = args.split(maxsplit=2)
1264
+ if not tokens or tokens[0] == "show":
1265
+ return CommandResult(message=settings.model_dump_json(indent=2))
1266
+ if tokens[0] == "set" and len(tokens) == 3:
1267
+ key, value = tokens[1], tokens[2]
1268
+ if key not in Settings.model_fields:
1269
+ return CommandResult(message=f"Unknown config key: {key}")
1270
+ try:
1271
+ coerced = _coerce_setting_value(settings, key, value)
1272
+ except ValueError as exc:
1273
+ return CommandResult(message=str(exc))
1274
+ setattr(settings, key, coerced)
1275
+ save_settings(settings)
1276
+ return CommandResult(message=f"Updated {key}")
1277
+ return CommandResult(message="Usage: /config [show|set KEY VALUE]")
1278
+
1279
+ async def _login_handler(args: str, context: CommandContext) -> CommandResult:
1280
+ del context
1281
+ settings = load_settings()
1282
+ provider = detect_provider(settings)
1283
+ api_key = args.strip()
1284
+ if not api_key:
1285
+ masked = (
1286
+ f"{settings.api_key[:6]}...{settings.api_key[-4:]}"
1287
+ if settings.api_key
1288
+ else "(not configured)"
1289
+ )
1290
+ return CommandResult(
1291
+ message=(
1292
+ f"Auth status:\n"
1293
+ f"- provider: {provider.name}\n"
1294
+ f"- auth_status: {auth_status(settings)}\n"
1295
+ f"- base_url: {settings.base_url or '(default)'}\n"
1296
+ f"- model: {settings.model}\n"
1297
+ f"- api_key: {masked}\n"
1298
+ "Usage: /login API_KEY"
1299
+ )
1300
+ )
1301
+ env_key = settings._active_env_key
1302
+ env = settings._active_env
1303
+ env.api_key = api_key
1304
+ settings.model_extra[env_key] = env.model_dump()
1305
+ save_settings(settings)
1306
+ return CommandResult(message="Stored API key in ~/.illusion/settings.json")
1307
+
1308
+ async def _logout_handler(_: str, context: CommandContext) -> CommandResult:
1309
+ del context
1310
+ settings = load_settings()
1311
+ env_key = settings._active_env_key
1312
+ env = settings._active_env
1313
+ env.api_key = ""
1314
+ settings.model_extra[env_key] = env.model_dump()
1315
+ save_settings(settings)
1316
+ return CommandResult(message="Cleared stored API key.")
1317
+
1318
+ async def _feedback_handler(args: str, context: CommandContext) -> CommandResult:
1319
+ del context
1320
+ path = get_feedback_log_path()
1321
+ if not args.strip():
1322
+ return CommandResult(message=f"Feedback log: {path}\nUsage: /feedback TEXT")
1323
+ timestamp = datetime.now(timezone.utc).isoformat()
1324
+ with path.open("a", encoding="utf-8") as handle:
1325
+ handle.write(f"[{timestamp}] {args.strip()}\n")
1326
+ return CommandResult(message=f"Saved feedback to {path}")
1327
+
1328
+ async def _fast_handler(args: str, context: CommandContext) -> CommandResult:
1329
+ settings = load_settings()
1330
+ current = (
1331
+ context.app_state.get().fast_mode
1332
+ if context.app_state is not None
1333
+ else settings.fast_mode
1334
+ )
1335
+ action = args.strip() or "toggle"
1336
+ if action == "show":
1337
+ return CommandResult(message=f"Fast mode: {'on' if current else 'off'}")
1338
+ enabled = {"on": True, "off": False, "toggle": not current}.get(action)
1339
+ if enabled is None:
1340
+ return CommandResult(message="Usage: /fast [show|on|off|toggle]")
1341
+ settings.fast_mode = enabled
1342
+ save_settings(settings)
1343
+ if context.app_state is not None:
1344
+ context.app_state.set(fast_mode=enabled)
1345
+ return CommandResult(message=f"Fast mode {'enabled' if enabled else 'disabled'}.")
1346
+
1347
+ async def _thinking_handler(args: str, context: CommandContext) -> CommandResult:
1348
+ settings = load_settings()
1349
+ current = (
1350
+ context.app_state.get().show_thinking
1351
+ if context.app_state is not None
1352
+ else settings.show_thinking
1353
+ )
1354
+ action = args.strip() or "toggle"
1355
+ if action == "show":
1356
+ return CommandResult(message=f"Thinking mode: {'on' if current else 'off'}")
1357
+ enabled = {"on": True, "off": False, "toggle": not current}.get(action)
1358
+ if enabled is None:
1359
+ return CommandResult(message="Usage: /thinking [show|on|off|toggle]")
1360
+ settings.show_thinking = enabled
1361
+ save_settings(settings)
1362
+ if context.app_state is not None:
1363
+ context.app_state.set(show_thinking=enabled)
1364
+ return CommandResult(message=f"Thinking mode {'enabled' if enabled else 'disabled'}.")
1365
+
1366
+ async def _help_handler(args: str, context: CommandContext) -> CommandResult:
1367
+ return CommandResult(message=registry.help_text())
1368
+
1369
+ async def _effort_handler(args: str, context: CommandContext) -> CommandResult:
1370
+ settings = load_settings()
1371
+ current = context.app_state.get().effort if context.app_state is not None else settings.effort
1372
+ value = args.strip() or "show"
1373
+ if value == "show":
1374
+ return CommandResult(message=f"Reasoning effort: {current}")
1375
+ if value not in {"low", "medium", "high", "xhigh", "max"}:
1376
+ return CommandResult(message="Usage: /effort [show|low|medium|high|xhigh|max]")
1377
+ # 验证 effort 级别
1378
+ try:
1379
+ from illusion.api.effort import EffortMapper
1380
+ effort_level = EffortMapper.normalize(value)
1381
+ except ValueError:
1382
+ return CommandResult(message="Usage: /effort [show|low|medium|high|xhigh|max]")
1383
+ settings.effort = value
1384
+ save_settings(settings)
1385
+ # 更新 QueryEngine 的 effort 值
1386
+ context.engine.effort = effort_level
1387
+ context.engine.set_system_prompt(build_runtime_system_prompt(settings, cwd=context.cwd))
1388
+ if context.app_state is not None:
1389
+ context.app_state.set(effort=value)
1390
+ return CommandResult(message=f"Reasoning effort set to {value}.")
1391
+
1392
+ async def _passes_handler(args: str, context: CommandContext) -> CommandResult:
1393
+ settings = load_settings()
1394
+ current = context.app_state.get().passes if context.app_state is not None else settings.passes
1395
+ value = args.strip() or "show"
1396
+ if value == "show":
1397
+ return CommandResult(message=f"Passes: {current}")
1398
+ try:
1399
+ passes = max(1, min(int(value), 8))
1400
+ except ValueError:
1401
+ return CommandResult(message="Usage: /passes [show|COUNT]")
1402
+ settings.passes = passes
1403
+ save_settings(settings)
1404
+ context.engine.set_system_prompt(build_runtime_system_prompt(settings, cwd=context.cwd))
1405
+ if context.app_state is not None:
1406
+ context.app_state.set(passes=passes)
1407
+ return CommandResult(message=f"Pass count set to {passes}.")
1408
+
1409
+ async def _turns_handler(args: str, context: CommandContext) -> CommandResult:
1410
+ settings = load_settings()
1411
+ tokens = args.split()
1412
+ if not tokens or tokens[0] == "show":
1413
+ return CommandResult(
1414
+ message=(
1415
+ f"Max turns (engine): {context.engine.max_turns}\n"
1416
+ f"Max turns (config): {settings.max_turns}\n"
1417
+ "Usage: /turns [show|COUNT]"
1418
+ )
1419
+ )
1420
+ if tokens[0] == "set" and len(tokens) == 2:
1421
+ raw = tokens[1]
1422
+ elif len(tokens) == 1:
1423
+ raw = tokens[0]
1424
+ else:
1425
+ return CommandResult(message="Usage: /turns [show|COUNT]")
1426
+ try:
1427
+ turns = int(raw)
1428
+ except ValueError:
1429
+ return CommandResult(message="Usage: /turns [show|COUNT]")
1430
+ turns = max(1, min(turns, 512))
1431
+ settings.max_turns = turns
1432
+ save_settings(settings)
1433
+ context.engine.set_max_turns(turns)
1434
+ return CommandResult(message=f"Max turns set to {turns}.")
1435
+
1436
+ async def _continue_handler(args: str, context: CommandContext) -> CommandResult:
1437
+ raw = args.strip()
1438
+ if not context.engine.has_pending_continuation():
1439
+ return CommandResult(message="Nothing to continue (no pending tool results).")
1440
+
1441
+ turns: int | None = None
1442
+ if raw:
1443
+ tokens = raw.split()
1444
+ if tokens[0] == "set" and len(tokens) == 2:
1445
+ raw = tokens[1]
1446
+ try:
1447
+ turns = int(raw)
1448
+ except ValueError:
1449
+ return CommandResult(message="Usage: /continue [COUNT]")
1450
+ turns = max(1, min(turns, 512))
1451
+
1452
+ return CommandResult(
1453
+ message="Continuing pending tool loop...",
1454
+ continue_pending=True,
1455
+ continue_turns=turns,
1456
+ )
1457
+
1458
+ async def _issue_handler(args: str, context: CommandContext) -> CommandResult:
1459
+ path = get_project_issue_file(context.cwd)
1460
+ tokens = args.split(maxsplit=1)
1461
+ action = tokens[0] if tokens else "show"
1462
+ rest = tokens[1] if len(tokens) == 2 else ""
1463
+ if action == "show":
1464
+ if not path.exists():
1465
+ return CommandResult(message=f"No issue context. File path: {path}")
1466
+ return CommandResult(message=path.read_text(encoding="utf-8"))
1467
+ if action == "set" and rest:
1468
+ title, separator, body = rest.partition("::")
1469
+ if not separator or not title.strip() or not body.strip():
1470
+ return CommandResult(message="Usage: /issue set TITLE :: BODY")
1471
+ content = f"# {title.strip()}\n\n{body.strip()}\n"
1472
+ path.parent.mkdir(parents=True, exist_ok=True)
1473
+ path.write_text(content, encoding="utf-8")
1474
+ return CommandResult(message=f"Saved issue context to {path}")
1475
+ if action == "clear":
1476
+ if path.exists():
1477
+ path.unlink()
1478
+ return CommandResult(message="Cleared issue context.")
1479
+ return CommandResult(message="No issue context to clear.")
1480
+ return CommandResult(message="Usage: /issue [show|set TITLE :: BODY|clear]")
1481
+
1482
+ async def _pr_comments_handler(args: str, context: CommandContext) -> CommandResult:
1483
+ path = get_project_pr_comments_file(context.cwd)
1484
+ tokens = args.split(maxsplit=1)
1485
+ action = tokens[0] if tokens else "show"
1486
+ rest = tokens[1] if len(tokens) == 2 else ""
1487
+ if action == "show":
1488
+ if not path.exists():
1489
+ return CommandResult(message=f"No PR comments context. File path: {path}")
1490
+ return CommandResult(message=path.read_text(encoding="utf-8"))
1491
+ if action == "add" and rest:
1492
+ location, separator, comment = rest.partition("::")
1493
+ if not separator or not location.strip() or not comment.strip():
1494
+ return CommandResult(message="Usage: /pr_comments add FILE[:LINE] :: COMMENT")
1495
+ existing = path.read_text(encoding="utf-8") if path.exists() else "# PR Comments\n"
1496
+ if not existing.endswith("\n"):
1497
+ existing += "\n"
1498
+ existing += f"- {location.strip()}: {comment.strip()}\n"
1499
+ path.parent.mkdir(parents=True, exist_ok=True)
1500
+ path.write_text(existing, encoding="utf-8")
1501
+ return CommandResult(message=f"Added PR comment to {path}")
1502
+ if action == "clear":
1503
+ if path.exists():
1504
+ path.unlink()
1505
+ return CommandResult(message="Cleared PR comments context.")
1506
+ return CommandResult(message="No PR comments context to clear.")
1507
+ return CommandResult(message="Usage: /pr_comments [show|add FILE[:LINE] :: COMMENT|clear]")
1508
+
1509
+ async def _mcp_handler(args: str, context: CommandContext) -> CommandResult:
1510
+ settings = load_settings()
1511
+ tokens = args.split()
1512
+ if tokens and tokens[0] == "auth" and len(tokens) >= 3:
1513
+ server_name = tokens[1]
1514
+ config = settings.mcp_servers.get(server_name)
1515
+ if config is None:
1516
+ return CommandResult(message=f"Unknown MCP server: {server_name}")
1517
+
1518
+ if len(tokens) == 3:
1519
+ mode = "bearer"
1520
+ key = None
1521
+ value = tokens[2]
1522
+ elif len(tokens) == 4:
1523
+ mode = tokens[2]
1524
+ key = None
1525
+ value = tokens[3]
1526
+ elif len(tokens) == 5:
1527
+ mode = tokens[2]
1528
+ key = tokens[3]
1529
+ value = tokens[4]
1530
+ else:
1531
+ return CommandResult(
1532
+ message="Usage: /mcp auth SERVER TOKEN | /mcp auth SERVER [bearer|env] VALUE | /mcp auth SERVER header KEY VALUE"
1533
+ )
1534
+
1535
+ if hasattr(config, "headers"):
1536
+ if mode not in {"bearer", "header"}:
1537
+ return CommandResult(message="HTTP/WS MCP auth supports bearer or header modes.")
1538
+ header_key = key or "Authorization"
1539
+ header_value = (
1540
+ f"Bearer {value}" if mode == "bearer" and header_key == "Authorization" else value
1541
+ )
1542
+ headers = dict(getattr(config, "headers", {}) or {})
1543
+ headers[header_key] = header_value
1544
+ settings.mcp_servers[server_name] = config.model_copy(update={"headers": headers})
1545
+ elif hasattr(config, "env"):
1546
+ if mode not in {"bearer", "env"}:
1547
+ return CommandResult(message="stdio MCP auth supports bearer or env modes.")
1548
+ env_key = key or "MCP_AUTH_TOKEN"
1549
+ env_value = f"Bearer {value}" if mode == "bearer" else value
1550
+ env = dict(getattr(config, "env", {}) or {})
1551
+ env[env_key] = env_value
1552
+ settings.mcp_servers[server_name] = config.model_copy(update={"env": env})
1553
+ else:
1554
+ return CommandResult(message=f"Server {server_name} does not support auth updates")
1555
+ save_settings(settings)
1556
+ return CommandResult(message=f"Saved MCP auth for {server_name}. Restart session to reconnect.")
1557
+ return CommandResult(message=context.mcp_summary or "No MCP servers configured.")
1558
+
1559
+ async def _plugin_handler(args: str, context: CommandContext) -> CommandResult:
1560
+ settings = load_settings()
1561
+ tokens = args.split()
1562
+ if not tokens or tokens[0] == "list":
1563
+ return CommandResult(message=context.plugin_summary or "No plugins discovered.")
1564
+ if tokens[0] == "enable" and len(tokens) == 2:
1565
+ settings.enabled_plugins[tokens[1]] = True
1566
+ save_settings(settings)
1567
+ return CommandResult(message=f"Enabled plugin '{tokens[1]}'. Restart session to reload.")
1568
+ if tokens[0] == "disable" and len(tokens) == 2:
1569
+ settings.enabled_plugins[tokens[1]] = False
1570
+ save_settings(settings)
1571
+ return CommandResult(message=f"Disabled plugin '{tokens[1]}'. Restart session to reload.")
1572
+ if tokens[0] == "install" and len(tokens) == 2:
1573
+ path = install_plugin_from_path(tokens[1])
1574
+ return CommandResult(message=f"Installed plugin to {path}")
1575
+ if tokens[0] == "uninstall" and len(tokens) == 2:
1576
+ if uninstall_plugin(tokens[1]):
1577
+ return CommandResult(message=f"Uninstalled plugin '{tokens[1]}'")
1578
+ return CommandResult(message=f"Plugin '{tokens[1]}' not found")
1579
+ plugins = load_plugins(settings, context.cwd)
1580
+ if plugins:
1581
+ return CommandResult(message=context.plugin_summary)
1582
+ return CommandResult(message="Usage: /plugin [list|enable NAME|disable NAME|install PATH|uninstall NAME]")
1583
+
1584
+ _MODE_LABELS = {"default": "Default", "plan": "Plan Mode", "full_auto": "Auto"}
1585
+
1586
+ async def _permissions_handler(args: str, context: CommandContext) -> CommandResult:
1587
+ settings = load_settings()
1588
+ tokens = args.split()
1589
+ if not tokens or tokens[0] == "show":
1590
+ permission = settings.permission
1591
+ label = _MODE_LABELS.get(permission.mode.value, permission.mode.value)
1592
+ return CommandResult(
1593
+ message=(
1594
+ f"Mode: {label}\n"
1595
+ f"Allowed tools: {permission.allowed_tools}\n"
1596
+ f"Denied tools: {permission.denied_tools}"
1597
+ )
1598
+ )
1599
+ if tokens[0] == "set" and len(tokens) == 2:
1600
+ settings.permission.mode = PermissionMode(tokens[1])
1601
+ save_settings(settings)
1602
+ context.engine.set_permission_checker(PermissionChecker(settings.permission))
1603
+ if context.app_state is not None:
1604
+ context.app_state.set(permission_mode=settings.permission.mode.value)
1605
+ label = _MODE_LABELS.get(tokens[1], tokens[1])
1606
+ return CommandResult(message=f"Permission mode set to {label}")
1607
+ return CommandResult(message="Usage: /permissions [show|set MODE]")
1608
+
1609
+ async def _plan_handler(args: str, context: CommandContext) -> CommandResult:
1610
+ settings = load_settings()
1611
+ mode = args.strip() or "on"
1612
+ if mode in {"on", "enter"}:
1613
+ settings.permission.mode = PermissionMode.PLAN
1614
+ save_settings(settings)
1615
+ context.engine.set_permission_checker(PermissionChecker(settings.permission))
1616
+ if context.app_state is not None:
1617
+ context.app_state.set(permission_mode=settings.permission.mode.value)
1618
+ return CommandResult(message="Plan mode enabled.")
1619
+ if mode in {"off", "exit"}:
1620
+ settings.permission.mode = PermissionMode.DEFAULT
1621
+ save_settings(settings)
1622
+ context.engine.set_permission_checker(PermissionChecker(settings.permission))
1623
+ if context.app_state is not None:
1624
+ context.app_state.set(permission_mode=settings.permission.mode.value)
1625
+ return CommandResult(message="Plan mode disabled.")
1626
+ return CommandResult(message="Usage: /plan [on|off]")
1627
+
1628
+ async def _model_handler(args: str, context: CommandContext) -> CommandResult:
1629
+ from illusion.config.i18n import t as i18n_t
1630
+ settings = load_settings()
1631
+ tokens = args.split(maxsplit=1)
1632
+ if not tokens or tokens[0] == "show":
1633
+ env = settings._active_env
1634
+ return CommandResult(
1635
+ message=i18n_t("model_active", model=settings.model) + "\n" +
1636
+ i18n_t("model_env_model", name=settings.active_model_name) + "\n" +
1637
+ i18n_t("model_api_format", fmt=env.api_format) + "\n" +
1638
+ i18n_t("model_base_url", url=env.base_url or i18n_t("model_default_url"))
1639
+ )
1640
+ if tokens[0] == "list":
1641
+ lines = []
1642
+ for env_key, env in settings.list_envs().items():
1643
+ for model_key, model_name in env.list_models().items():
1644
+ ref = f"{env_key}.{model_key}"
1645
+ active = " (active)" if ref == settings.model else ""
1646
+ lines.append(f" {ref}{active}: {model_name} ({env.api_format})")
1647
+ return CommandResult(message=i18n_t("model_list_title") + "\n" + "\n".join(lines))
1648
+ # 切换模型
1649
+ model_ref = tokens[0] if tokens[0] != "set" else (tokens[1] if len(tokens) > 1 else "")
1650
+ if "." in model_ref:
1651
+ env_key, model_key = model_ref.split(".", 1)
1652
+ env = settings.get_env(env_key)
1653
+ if env and env.get_model(model_key):
1654
+ old_env_key = settings._active_env_key
1655
+ settings.model = model_ref
1656
+ save_settings(settings)
1657
+ context.engine.set_model(env.get_model(model_key))
1658
+ if context.app_state is not None:
1659
+ context.app_state.set(model=env.get_model(model_key))
1660
+ # 跨 env 切换时告知调用方需要重建 API 客户端
1661
+ needs_rebuild = env_key != old_env_key
1662
+ return CommandResult(
1663
+ message=i18n_t("model_set_to", ref=model_ref, name=env.get_model(model_key)),
1664
+ needs_api_rebuild=needs_rebuild,
1665
+ )
1666
+ return CommandResult(message=i18n_t("model_unknown", ref=model_ref))
1667
+
1668
+ async def _language_handler(args: str, context: CommandContext) -> CommandResult:
1669
+ settings = load_settings()
1670
+ current = (
1671
+ str(context.app_state.get().ui_language)
1672
+ if context.app_state is not None
1673
+ else settings.ui_language
1674
+ )
1675
+ tokens = args.split()
1676
+ if not tokens or tokens[0] == "show":
1677
+ return CommandResult(message=f"UI language: {current}")
1678
+ if tokens[0] == "list":
1679
+ return CommandResult(message="Available UI languages: zh-CN, en")
1680
+ if tokens[0] == "set" and len(tokens) == 2:
1681
+ value = tokens[1]
1682
+ if value not in {"zh-CN", "en"}:
1683
+ return CommandResult(message="Usage: /language [show|list|set zh-CN|set en]")
1684
+ settings.ui_language = value
1685
+ save_settings(settings)
1686
+ if context.app_state is not None:
1687
+ context.app_state.set(ui_language=value)
1688
+ return CommandResult(message=f"UI language set to {value}")
1689
+ return CommandResult(message="Usage: /language [show|list|set zh-CN|set en]")
1690
+
1691
+ async def _output_style_handler(args: str, context: CommandContext) -> CommandResult:
1692
+ settings = load_settings()
1693
+ tokens = args.split(maxsplit=1)
1694
+ styles = load_output_styles()
1695
+ available = {style.name: style for style in styles}
1696
+ current = (
1697
+ context.app_state.get().output_style
1698
+ if context.app_state is not None
1699
+ else settings.output_style
1700
+ )
1701
+ if not tokens or tokens[0] == "show":
1702
+ return CommandResult(message=f"Output style: {current}")
1703
+ if tokens[0] == "list":
1704
+ return CommandResult(
1705
+ message="\n".join(f"{style.name} [{style.source}]" for style in styles)
1706
+ )
1707
+ if tokens[0] == "set" and len(tokens) == 2:
1708
+ if tokens[1] not in available:
1709
+ return CommandResult(message=f"Unknown output style: {tokens[1]}")
1710
+ settings.output_style = tokens[1]
1711
+ save_settings(settings)
1712
+ if context.app_state is not None:
1713
+ context.app_state.set(output_style=tokens[1])
1714
+ return CommandResult(message=f"Output style set to {tokens[1]}")
1715
+ return CommandResult(message="Usage: /output-style [show|list|set NAME]")
1716
+
1717
+ async def _doctor_handler(_: str, context: CommandContext) -> CommandResult:
1718
+ settings = load_settings()
1719
+ memory_dir = get_project_memory_dir(context.cwd)
1720
+ state = context.app_state.get() if context.app_state is not None else None
1721
+ lines = [
1722
+ "Doctor summary:",
1723
+ f"- cwd: {context.cwd}",
1724
+ f"- model: {settings.model}",
1725
+ f"- permission_mode: {state.permission_mode if state is not None else settings.permission.mode}",
1726
+ f"- output_style: {state.output_style if state is not None else settings.output_style}",
1727
+ f"- ui_language: {state.ui_language if state is not None else settings.ui_language}",
1728
+ f"- effort: {state.effort if state is not None else settings.effort}",
1729
+ f"- passes: {state.passes if state is not None else settings.passes}",
1730
+ f"- memory_dir: {memory_dir}",
1731
+ f"- plugin_count: {max(len(context.plugin_summary.splitlines()) - 1, 0) if context.plugin_summary else 0}",
1732
+ f"- mcp_configured: {'yes' if context.mcp_summary and 'No MCP' not in context.mcp_summary else 'no'}",
1733
+ ]
1734
+ return CommandResult(message="\n".join(lines))
1735
+
1736
+ async def _privacy_settings_handler(_: str, context: CommandContext) -> CommandResult:
1737
+ settings = load_settings()
1738
+ session_dir = get_project_session_dir(context.cwd)
1739
+ lines = [
1740
+ "Privacy settings:",
1741
+ f"- user_config_dir: {get_config_dir()}",
1742
+ f"- project_config_dir: {get_project_config_dir(context.cwd)}",
1743
+ f"- session_dir: {session_dir}",
1744
+ f"- feedback_log: {get_feedback_log_path()}",
1745
+ f"- api_base_url: {settings.base_url or '(default Anthropic-compatible endpoint)'}",
1746
+ "- network: enabled only for provider and explicit web/MCP calls",
1747
+ "- storage: local files under ~/.illusion and project .illusion",
1748
+ ]
1749
+ return CommandResult(message="\n".join(lines))
1750
+
1751
+ async def _diff_handler(args: str, context: CommandContext) -> CommandResult:
1752
+ if args.strip() == "full":
1753
+ ok, output = _run_git_command(context.cwd, "diff", "HEAD")
1754
+ return CommandResult(message=output or "(no diff)")
1755
+ ok, output = _run_git_command(context.cwd, "diff", "--stat")
1756
+ if not ok:
1757
+ return CommandResult(message=output)
1758
+ return CommandResult(message=output or "(no diff)")
1759
+
1760
+ async def _branch_handler(args: str, context: CommandContext) -> CommandResult:
1761
+ action = args.strip() or "show"
1762
+ if action == "show":
1763
+ ok, current = _run_git_command(context.cwd, "branch", "--show-current")
1764
+ if not ok:
1765
+ return CommandResult(message=current)
1766
+ return CommandResult(message=f"Current branch: {current or '(detached HEAD)'}")
1767
+ if action == "list":
1768
+ ok, branches = _run_git_command(context.cwd, "branch", "--format", "%(refname:short)")
1769
+ return CommandResult(message=branches if ok else branches)
1770
+ return CommandResult(message="Usage: /branch [show|list]")
1771
+
1772
+ async def _commit_handler(args: str, context: CommandContext) -> CommandResult:
1773
+ message = args.strip()
1774
+ if not message:
1775
+ ok, status = _run_git_command(context.cwd, "status", "--short")
1776
+ return CommandResult(message=status if ok and status else "(working tree clean)")
1777
+ ok, status = _run_git_command(context.cwd, "status", "--short")
1778
+ if not ok:
1779
+ return CommandResult(message=status)
1780
+ if not status.strip():
1781
+ return CommandResult(message="Nothing to commit.")
1782
+ ok, output = _run_git_command(context.cwd, "add", "-A")
1783
+ if not ok:
1784
+ return CommandResult(message=output)
1785
+ ok, output = _run_git_command(context.cwd, "commit", "-m", message)
1786
+ return CommandResult(message=output if ok else output)
1787
+
1788
+ async def _delete_handler(args: str, context: CommandContext) -> CommandResult:
1789
+ from illusion.services.session_storage import (
1790
+ delete_all_sessions,
1791
+ delete_session_by_id,
1792
+ list_session_snapshots,
1793
+ )
1794
+ from illusion.services.file_history import cleanup_file_history, cleanup_all_file_histories
1795
+
1796
+ tokens = args.strip().split()
1797
+
1798
+ # /delete — 列出会话供选择
1799
+ if not tokens:
1800
+ sessions = list_session_snapshots(context.cwd, limit=10)
1801
+ if not sessions:
1802
+ return CommandResult(message="No saved sessions found for this project.")
1803
+ import time
1804
+ lines = ["Saved sessions:"]
1805
+ for s in sessions:
1806
+ ts = time.strftime("%m/%d %H:%M", time.localtime(s["created_at"]))
1807
+ summary = s["summary"][:50] or "(no summary)"
1808
+ lines.append(f" {s['session_id']} {ts} {s['message_count']}msg {summary}")
1809
+ lines.append("")
1810
+ lines.append("Usage: /delete <session_id> — delete a specific session")
1811
+ lines.append(" /delete all — delete all sessions")
1812
+ return CommandResult(message="\n".join(lines))
1813
+
1814
+ # /delete all / /delete __all__ — 清除所有会话
1815
+ if tokens[0] in ("all", "__all__"):
1816
+ count = delete_all_sessions(context.cwd)
1817
+ cleanup_all_file_histories()
1818
+ context.engine.clear()
1819
+ return CommandResult(
1820
+ message=f"Deleted {count} session file(s).",
1821
+ clear_screen=True,
1822
+ reset_session=True,
1823
+ )
1824
+
1825
+ # /delete <session_id> — 删除指定会话
1826
+ sid = tokens[0]
1827
+ if delete_session_by_id(context.cwd, sid):
1828
+ cleanup_file_history(sid)
1829
+ if sid == context.session_id:
1830
+ context.engine.clear()
1831
+ return CommandResult(
1832
+ message=f"Deleted current session: {sid}",
1833
+ clear_screen=True,
1834
+ reset_session=True,
1835
+ )
1836
+ return CommandResult(message=f"Deleted session: {sid}")
1837
+ return CommandResult(message=f"Session not found: {sid}")
1838
+
1839
+ async def _rules_handler(args: str, context: CommandContext) -> CommandResult:
1840
+ from illusion.skills.loader import get_project_rules_dir
1841
+
1842
+ rules_dir = get_project_rules_dir(context.cwd)
1843
+ rule_files = sorted(rules_dir.glob("*.md"))
1844
+
1845
+ if not rule_files:
1846
+ return CommandResult(message=f"No rules found in {rules_dir}")
1847
+
1848
+ tokens = args.strip().split()
1849
+
1850
+ # /rules — 列出所有规则
1851
+ if not tokens:
1852
+ lines = [f"Rules directory: {rules_dir}", ""]
1853
+ for i, path in enumerate(rule_files, 1):
1854
+ # 读取第一行作为预览
1855
+ content = path.read_text(encoding="utf-8", errors="replace").strip()
1856
+ first_line = content.split("\n", 1)[0][:60] if content else "(empty)"
1857
+ lines.append(f" {i}. {path.stem} — {first_line}")
1858
+ lines.append("")
1859
+ lines.append("Usage: /rules <name|number> — view a specific rule")
1860
+ return CommandResult(message="\n".join(lines))
1861
+
1862
+ # /rules <name|number> — 显示指定规则内容
1863
+ target = tokens[0]
1864
+ selected = None
1865
+
1866
+ # 按编号选择
1867
+ try:
1868
+ idx = int(target) - 1
1869
+ if 0 <= idx < len(rule_files):
1870
+ selected = rule_files[idx]
1871
+ except ValueError:
1872
+ pass
1873
+
1874
+ # 按名称选择
1875
+ if selected is None:
1876
+ for path in rule_files:
1877
+ if path.stem.lower() == target.lower():
1878
+ selected = path
1879
+ break
1880
+
1881
+ if selected is None:
1882
+ return CommandResult(message=f"Rule not found: {target}. Use /rules to list available rules.")
1883
+
1884
+ content = selected.read_text(encoding="utf-8", errors="replace").strip()
1885
+ return CommandResult(message=f"# {selected.stem}\n\n{content}")
1886
+
1887
+ registry.register(SlashCommand("exit", "Exit IllusionCode", _exit_handler))
1888
+ registry.register(SlashCommand("clear", "Clear conversation and start a new session", _new_handler))
1889
+ registry.register(SlashCommand("new", "Start a new conversation session", _new_handler))
1890
+ registry.register(SlashCommand("version", "Show the installed IllusionCode version", _version_handler))
1891
+ registry.register(SlashCommand("status", "Show session status", _status_handler))
1892
+ registry.register(SlashCommand("context", "Show active system prompt or manage context window", _context_handler))
1893
+ registry.register(SlashCommand("summary", "Summarize conversation history", _summary_handler))
1894
+ registry.register(SlashCommand("compact", "Compact older conversation history", _compact_handler))
1895
+ registry.register(SlashCommand("memory", "Inspect and manage project memory", _memory_handler))
1896
+ registry.register(SlashCommand("hooks", "Show configured hooks", _hooks_handler))
1897
+ registry.register(SlashCommand("resume", "Restore the latest saved session", _resume_handler))
1898
+ registry.register(SlashCommand("export", "Export the current transcript", _export_handler))
1899
+ registry.register(SlashCommand("share", "Create a shareable transcript snapshot", _share_handler))
1900
+ registry.register(SlashCommand("copy", "Copy the latest response or provided text", _copy_handler))
1901
+ registry.register(SlashCommand("rewind", "Remove the latest conversation turn(s)", _rewind_handler))
1902
+ registry.register(SlashCommand("files", "List files in the current workspace", _files_handler))
1903
+ registry.register(SlashCommand("init", "Initialize project IllusionCode files", _init_handler))
1904
+ registry.register(SlashCommand("bridge", "Inspect bridge helpers and spawn bridge sessions", _bridge_handler))
1905
+ registry.register(SlashCommand("login", "Show auth status or store an API key", _login_handler))
1906
+ registry.register(SlashCommand("logout", "Clear the stored API key", _logout_handler))
1907
+ registry.register(SlashCommand("feedback", "Save CLI feedback to the local feedback log", _feedback_handler))
1908
+ registry.register(SlashCommand("skills", "List or show available skills", _skills_handler))
1909
+ registry.register(SlashCommand("config", "Show or update configuration", _config_handler))
1910
+ registry.register(SlashCommand("mcp", "Show MCP status", _mcp_handler))
1911
+ registry.register(SlashCommand("plugin", "Manage plugins", _plugin_handler))
1912
+ registry.register(SlashCommand("reload-plugins", "Reload plugin discovery for this workspace", _reload_plugins_handler))
1913
+ registry.register(SlashCommand("permissions", "Show or update permission mode", _permissions_handler))
1914
+ registry.register(SlashCommand("plan", "Toggle plan permission mode", _plan_handler))
1915
+ registry.register(SlashCommand("thinking", "Show or update thinking mode", _thinking_handler))
1916
+ registry.register(SlashCommand("help", "Show available commands and their usage", _help_handler))
1917
+ registry.register(SlashCommand("fast", "Show or update fast mode", _fast_handler))
1918
+ registry.register(SlashCommand("effort", "Show or update reasoning effort", _effort_handler))
1919
+ registry.register(SlashCommand("passes", "Show or update reasoning pass count", _passes_handler))
1920
+ registry.register(SlashCommand("turns", "Show or update maximum agentic turn count", _turns_handler))
1921
+ registry.register(SlashCommand("continue", "Continue the previous tool loop if it was interrupted", _continue_handler))
1922
+ registry.register(SlashCommand("model", "Show or update the default model", _model_handler))
1923
+ registry.register(SlashCommand("language", "Show or update UI language", _language_handler))
1924
+ registry.register(SlashCommand("output-style", "Show or update output style", _output_style_handler))
1925
+ registry.register(SlashCommand("doctor", "Show environment diagnostics", _doctor_handler))
1926
+ registry.register(SlashCommand("diff", "Show git diff output", _diff_handler))
1927
+ registry.register(SlashCommand("branch", "Show git branch information", _branch_handler))
1928
+ registry.register(SlashCommand("commit", "Show status or create a git commit", _commit_handler))
1929
+ registry.register(SlashCommand("issue", "Show or update project issue context", _issue_handler))
1930
+ registry.register(SlashCommand("pr_comments", "Show or update project PR comments context", _pr_comments_handler))
1931
+ registry.register(SlashCommand("privacy-settings", "Show local privacy and storage settings", _privacy_settings_handler))
1932
+ registry.register(SlashCommand("delete", "Delete saved sessions", _delete_handler))
1933
+ registry.register(SlashCommand("rules", "View project rules", _rules_handler))
1934
+ return registry