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,188 @@
1
+ """
2
+ 进入工作树工具
3
+ =============
4
+
5
+ 本模块提供创建和进入 git 工作树的功能,用于隔离开发环境。
6
+
7
+ 主要组件:
8
+ - EnterWorktreeTool: 创建并进入 git 工作树的工具
9
+
10
+ 使用示例:
11
+ >>> from illusion.tools import EnterWorktreeTool
12
+ >>> tool = EnterWorktreeTool()
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import re
18
+ import subprocess
19
+ import sys
20
+ from pathlib import Path
21
+ from uuid import uuid4
22
+
23
+ from pydantic import BaseModel, Field
24
+
25
+ from illusion.config.settings import load_settings
26
+ from illusion.tools.base import BaseTool, ToolExecutionContext, ToolResult
27
+
28
+
29
+ class EnterWorktreeToolInput(BaseModel):
30
+ """进入工作树参数。
31
+
32
+ 属性:
33
+ name: 工作树名称(可选,不提供则生成随机名称)
34
+ """
35
+
36
+ name: str | None = Field(
37
+ default=None,
38
+ description="A name for the worktree. If not provided, a random name is generated.",
39
+ )
40
+
41
+
42
+ class EnterWorktreeTool(BaseTool):
43
+ """创建 git 工作树。
44
+
45
+ 仅在用户明确要求使用工作树时使用。此工具创建隔离的 git 工作树并切换当前会话到其中。
46
+ """
47
+
48
+ name = "enter_worktree"
49
+ description = """Use this tool ONLY when the user explicitly asks to work in a worktree. This tool creates an isolated git worktree and switches the current session into it.
50
+
51
+ ## When to Use
52
+
53
+ - The user explicitly says "worktree" (e.g., "start a worktree", "work in a worktree", "create a worktree", "use a worktree")
54
+
55
+ ## When NOT to Use
56
+
57
+ - The user asks to create a branch, switch branches, or work on a different branch -- use git commands instead
58
+ - The user asks to fix a bug or work on a feature -- use normal git workflow unless they specifically mention worktrees
59
+ - Never use this tool unless the user explicitly mentions "worktree"
60
+
61
+ ## Requirements
62
+
63
+ - Must be in a git repository, OR have WorktreeCreate/WorktreeRemove hooks configured in settings.json
64
+ - Must not already be in a worktree
65
+
66
+ ## Behavior
67
+
68
+ - In a git repository: creates a new git worktree inside `.illusion/worktrees/` with a new branch based on HEAD
69
+ - Outside a git repository: delegates to WorktreeCreate/WorktreeRemove hooks for VCS-agnostic isolation
70
+ - Switches the session's working directory to the new worktree
71
+ - Use ExitWorktree to leave the worktree mid-session (keep or remove). On session exit, if still in the worktree, the user will be prompted to keep or remove it
72
+
73
+ ## Parameters
74
+
75
+ - `name` (optional): A name for the worktree. If not provided, a random name is generated."""
76
+ input_model = EnterWorktreeToolInput
77
+
78
+ async def execute(
79
+ self,
80
+ arguments: EnterWorktreeToolInput,
81
+ context: ToolExecutionContext,
82
+ ) -> ToolResult:
83
+ name = arguments.name or f"wt-{uuid4().hex[:8]}"
84
+
85
+ # 检查是否已在 worktree 中
86
+ if _is_in_worktree(context.cwd):
87
+ return ToolResult(
88
+ output="Already in a worktree session. Use exit_worktree to leave first.",
89
+ is_error=True,
90
+ )
91
+
92
+ branch_name = name
93
+
94
+ # 尝试 git 仓库路径
95
+ top_level = _git_output(context.cwd, "rev-parse", "--show-toplevel")
96
+ if top_level is not None:
97
+ # ---- Git 仓库模式 ----
98
+ repo_root = Path(top_level)
99
+ worktree_path = _resolve_worktree_path(repo_root, branch_name)
100
+ worktree_path.parent.mkdir(parents=True, exist_ok=True)
101
+ cmd = ["git", "worktree", "add", "-b", branch_name, str(worktree_path), "HEAD"]
102
+ result = subprocess.run(
103
+ cmd,
104
+ cwd=repo_root,
105
+ capture_output=True,
106
+ text=True,
107
+ check=False,
108
+ stdin=subprocess.DEVNULL,
109
+ **({"creationflags": subprocess.CREATE_NO_WINDOW} if sys.platform == "win32" else {}),
110
+ )
111
+ output = (result.stdout or result.stderr).strip() or f"Created worktree {worktree_path}"
112
+ if result.returncode != 0:
113
+ return ToolResult(output=output, is_error=True)
114
+ return ToolResult(
115
+ output=f"{output}\nPath: {worktree_path}",
116
+ metadata={"new_cwd": str(worktree_path)},
117
+ )
118
+ else:
119
+ # ---- 非 Git 仓库:检查 WorktreeCreate/WorktreeRemove hooks ----
120
+ settings = load_settings()
121
+ hooks = settings.hooks or {}
122
+ has_create = "WorktreeCreate" in hooks or "worktree_create" in hooks
123
+ has_remove = "WorktreeRemove" in hooks or "worktree_remove" in hooks
124
+ if has_create and has_remove:
125
+ worktree_path = _resolve_worktree_path(context.cwd.resolve(), branch_name)
126
+ worktree_path.mkdir(parents=True, exist_ok=True)
127
+ return ToolResult(
128
+ output=f"Created isolated worktree at {worktree_path}",
129
+ metadata={"new_cwd": str(worktree_path)},
130
+ )
131
+ return ToolResult(
132
+ output=(
133
+ "enter_worktree requires a git repository "
134
+ "or WorktreeCreate/WorktreeRemove hooks configured in settings.json"
135
+ ),
136
+ is_error=True,
137
+ )
138
+
139
+
140
+ def _git_output(cwd: Path, *args: str) -> str | None:
141
+ """执行 git 命令并返回输出。
142
+
143
+ 参数:
144
+ cwd: 工作目录
145
+ *args: git 命令参数
146
+
147
+ 返回:
148
+ 命令输出字符串,失败返回 None
149
+ """
150
+ result = subprocess.run(
151
+ ["git", *args],
152
+ cwd=cwd,
153
+ capture_output=True,
154
+ text=True,
155
+ check=False,
156
+ stdin=subprocess.DEVNULL,
157
+ **({"creationflags": subprocess.CREATE_NO_WINDOW} if sys.platform == "win32" else {}),
158
+ )
159
+ if result.returncode != 0:
160
+ return None
161
+ return (result.stdout or "").strip()
162
+
163
+
164
+ def _resolve_worktree_path(repo_root: Path, name: str) -> Path:
165
+ """解析工作树路径。
166
+
167
+ 参数:
168
+ repo_root: 仓库根目录
169
+ name: 工作树名称
170
+
171
+ 返回:
172
+ 解析后的工作树路径
173
+ """
174
+ slug = re.sub(r"[^A-Za-z0-9._-]+", "-", name).strip("-") or "worktree"
175
+ return (repo_root / ".illusion" / "worktrees" / slug).resolve()
176
+
177
+
178
+ def _is_in_worktree(cwd: Path) -> bool:
179
+ """检查当前工作目录是否已在 worktree 中。
180
+
181
+ 参数:
182
+ cwd: 当前工作目录
183
+
184
+ 返回:
185
+ 是否已在 worktree 中
186
+ """
187
+ cwd_str = str(cwd.resolve())
188
+ return "/.illusion/worktrees/" in cwd_str.replace("\\", "/")
@@ -0,0 +1,69 @@
1
+ """
2
+ 退出计划模式工具
3
+ ================
4
+
5
+ 本模块提供退出计划权限模式的功能,允许代理在完成计划编写后请求用户审批。
6
+
7
+ 主要组件:
8
+ - ExitPlanModeTool: 退出计划模式的工具
9
+
10
+ 使用示例:
11
+ >>> from illusion.tools import ExitPlanModeTool
12
+ >>> tool = ExitPlanModeTool()
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ from pydantic import BaseModel
18
+
19
+ from illusion.config.settings import load_settings, save_settings
20
+ from illusion.permissions import PermissionMode
21
+ from illusion.tools.base import BaseTool, ToolExecutionContext, ToolResult
22
+
23
+
24
+ class ExitPlanModeToolInput(BaseModel):
25
+ """无操作输入模型(工具不需要任何参数)。"""
26
+
27
+
28
+ class ExitPlanModeTool(BaseTool):
29
+ """将设置权限模式切换回默认值。
30
+
31
+ 当代理在计划模式下完成计划编写并准备好请求用户审批时,使用此工具。
32
+ 该工具会读取代理之前写入的计划文件内容,供用户在审批时查看。
33
+ """
34
+
35
+ name = "exit_plan_mode"
36
+ description = """Use this tool when you are in plan mode and have finished writing your plan to the plan file and are ready for user approval.
37
+
38
+ ## How This Tool Works
39
+ - You should have already written your plan to the plan file specified in the plan mode system message
40
+ - This tool does NOT take the plan content as a parameter - it will read the plan from the file you wrote
41
+ - This tool simply signals that you're done planning and ready for the user to review and approve
42
+ - The user will see the contents of your plan file when they review it
43
+
44
+ ## When to Use This Tool
45
+ IMPORTANT: Only use this tool when the task requires planning the implementation steps of a task that requires writing code. For research tasks where you're gathering information, searching files, reading files or in general trying to understand the codebase - do NOT use this tool.
46
+
47
+ ## Before Using This Tool
48
+ Ensure your plan is complete and unambiguous:
49
+ - If you have unresolved questions about requirements or approach, use AskUserQuestion first (in earlier phases)
50
+ - Once your plan is finalized, use THIS tool to request approval
51
+
52
+ **Important:** Do NOT use AskUserQuestion to ask "Is this plan okay?" or "Should I proceed?" - that's exactly what THIS tool does. ExitPlanMode inherently requests user approval of your plan.
53
+
54
+ ## Examples
55
+
56
+ 1. Initial task: "Search for and understand the implementation of input handling in the codebase" - Do not use the exit plan mode tool because you are not planning the implementation steps of a task.
57
+ 2. Initial task: "Help me implement a yank-like editor shortcut" - Use the exit plan mode tool after you have finished planning the implementation steps of the task.
58
+ 3. Initial task: "Add a new feature to handle user authentication" - If unsure about auth method (OAuth, JWT, etc.), use AskUserQuestion first, then use exit plan mode tool after clarifying the approach."""
59
+ input_model = ExitPlanModeToolInput
60
+
61
+ async def execute(self, arguments: ExitPlanModeToolInput, context: ToolExecutionContext) -> ToolResult:
62
+ del arguments, context
63
+ # 加载设置
64
+ settings = load_settings()
65
+ # 将权限模式设置为默认
66
+ settings.permission.mode = PermissionMode.DEFAULT
67
+ # 保存设置
68
+ save_settings(settings)
69
+ return ToolResult(output="Permission mode set to default")
@@ -0,0 +1,225 @@
1
+ """
2
+ 退出工作树工具
3
+ =============
4
+
5
+ 本模块提供移除 git 工作树的功能,用于结束工作树会话。
6
+
7
+ 主要组件:
8
+ - ExitWorktreeTool: 退出并可选删除工作树的工具
9
+
10
+ 使用示例:
11
+ >>> from illusion.tools import ExitWorktreeTool
12
+ >>> tool = ExitWorktreeTool()
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import subprocess
18
+ import sys
19
+ from pathlib import Path
20
+ from typing import Literal
21
+
22
+ from pydantic import BaseModel, Field
23
+
24
+ from illusion.tools.base import BaseTool, ToolExecutionContext, ToolResult
25
+
26
+
27
+ class ExitWorktreeToolInput(BaseModel):
28
+ """工作树移除参数。
29
+
30
+ 属性:
31
+ action: 操作类型,keep 或 remove
32
+ discard_changes: 是否放弃未提交的更改
33
+ """
34
+
35
+ action: Literal["keep", "remove"] = Field(
36
+ description='"keep" leaves the worktree directory and branch intact on disk; "remove" deletes both.',
37
+ )
38
+ discard_changes: bool = Field(
39
+ default=False,
40
+ description='Only meaningful with action "remove". If true, force-remove even with uncommitted changes.',
41
+ )
42
+
43
+
44
+ class ExitWorktreeTool(BaseTool):
45
+ """移除 git 工作树。
46
+
47
+ 退出由 EnterWorktree 创建的工作树会话,并将会话返回到原始工作目录。
48
+ """
49
+
50
+ name = "exit_worktree"
51
+ description = """Exit a worktree session created by EnterWorktree and return the session to the original working directory.
52
+
53
+ ## Scope
54
+
55
+ This tool ONLY operates on worktrees created by EnterWorktree in this session. It will NOT touch:
56
+ - Worktrees you created manually with `git worktree add`
57
+ - Worktrees from a previous session (even if created by EnterWorktree then)
58
+ - The directory you're in if EnterWorktree was never called
59
+
60
+ If called outside an EnterWorktree session, the tool is a **no-op**: it reports that no worktree session is active and takes no action. Filesystem state is unchanged.
61
+
62
+ ## When to Use
63
+
64
+ - The user explicitly asks to "exit the worktree", "leave the worktree", "go back", or otherwise end the worktree session
65
+ - Do NOT call this proactively — only when the user asks
66
+
67
+ ## Parameters
68
+
69
+ - `action` (required): `"keep"` or `"remove"`
70
+ - `"keep"` — leave the worktree directory and branch intact on disk. Use this if the user wants to come back to the work later, or if there are changes to preserve.
71
+ - `"remove"` — delete the worktree directory and its branch. Use this for a clean exit when the work is done or abandoned.
72
+ - `discard_changes` (optional, default false): only meaningful with `action: "remove"`. If the worktree has uncommitted files or commits not on the original branch, the tool will REFUSE to remove it unless this is set to `true`. If the tool returns an error listing changes, confirm with the user before re-invoking with `discard_changes: true`.
73
+
74
+ ## Behavior
75
+
76
+ - Restores the session's working directory to where it was before EnterWorktree
77
+ - Clears CWD-dependent caches (system prompt sections, memory files, plans directory) so the session state reflects the original directory
78
+ - If a tmux session was attached to the worktree: killed on `remove`, left running on `keep` (its name is returned so the user can reattach)
79
+ - Once exited, EnterWorktree can be called again to create a fresh worktree"""
80
+ input_model = ExitWorktreeToolInput
81
+
82
+ async def execute(
83
+ self,
84
+ arguments: ExitWorktreeToolInput,
85
+ context: ToolExecutionContext,
86
+ ) -> ToolResult:
87
+ run_kwargs: dict = {}
88
+ if sys.platform == "win32":
89
+ run_kwargs["creationflags"] = subprocess.CREATE_NO_WINDOW
90
+
91
+ # 获取主仓库的公共 .git 目录(worktree 内也能正确指向主仓库)
92
+ common_dir = _git_output(context.cwd, "rev-parse", "--git-common-dir")
93
+ if common_dir is None:
94
+ return ToolResult(output="Not in a git repository", is_error=True)
95
+
96
+ # 解析主仓库根目录
97
+ git_dir = Path(common_dir)
98
+ if not git_dir.is_absolute():
99
+ git_dir = (context.cwd / git_dir).resolve()
100
+ else:
101
+ git_dir = git_dir.resolve()
102
+ main_repo_root = git_dir.parent
103
+
104
+ # 检查当前 CWD 是否在 .illusion/worktrees/ 下
105
+ worktree_base = (main_repo_root / ".illusion" / "worktrees").resolve()
106
+ current_cwd = context.cwd.resolve()
107
+
108
+ if not _is_subpath(current_cwd, worktree_base) and current_cwd != worktree_base:
109
+ # 不在 worktree 中 — no-op(非错误)
110
+ return ToolResult(output="No active worktree session found")
111
+
112
+ worktree_path = current_cwd
113
+
114
+ # keep 操作 — 保留工作树,仅恢复 CWD
115
+ if arguments.action == "keep":
116
+ return ToolResult(
117
+ output=f"Worktree kept at {worktree_path}",
118
+ metadata={"new_cwd": str(main_repo_root)},
119
+ )
120
+
121
+ # remove 操作
122
+ # 获取分支名(在删除 worktree 之前)
123
+ branch = _git_output(worktree_path, "rev-parse", "--abbrev-ref", "HEAD")
124
+
125
+ # 检查未提交的更改,除非 discard_changes 为 true
126
+ if not arguments.discard_changes:
127
+ status_check = subprocess.run(
128
+ ["git", "status", "--porcelain"],
129
+ cwd=worktree_path,
130
+ capture_output=True,
131
+ text=True,
132
+ check=False,
133
+ stdin=subprocess.DEVNULL,
134
+ **run_kwargs,
135
+ )
136
+ if status_check.stdout.strip():
137
+ return ToolResult(
138
+ output=(
139
+ f"Worktree has uncommitted changes. "
140
+ f"Set discard_changes=true to force remove.\n"
141
+ f"Changes:\n{status_check.stdout.strip()[:500]}"
142
+ ),
143
+ is_error=True,
144
+ )
145
+
146
+ # 移除工作树
147
+ cmd = ["git", "worktree", "remove"]
148
+ if arguments.discard_changes:
149
+ cmd.append("--force")
150
+ cmd.append(str(worktree_path))
151
+
152
+ result = subprocess.run(
153
+ cmd,
154
+ cwd=main_repo_root,
155
+ capture_output=True,
156
+ text=True,
157
+ check=False,
158
+ stdin=subprocess.DEVNULL,
159
+ **run_kwargs,
160
+ )
161
+ if result.returncode != 0:
162
+ output = (result.stdout or result.stderr).strip() or f"Failed to remove worktree {worktree_path}"
163
+ return ToolResult(output=output, is_error=True)
164
+
165
+ # 删除对应的分支
166
+ if branch and branch != "HEAD":
167
+ subprocess.run(
168
+ ["git", "branch", "-D", branch],
169
+ cwd=main_repo_root,
170
+ capture_output=True,
171
+ text=True,
172
+ check=False,
173
+ stdin=subprocess.DEVNULL,
174
+ **run_kwargs,
175
+ )
176
+
177
+ output = (result.stdout or "").strip() or f"Removed worktree {worktree_path}"
178
+ return ToolResult(
179
+ output=output,
180
+ metadata={"new_cwd": str(main_repo_root)},
181
+ )
182
+
183
+
184
+ def _git_output(cwd: Path, *args: str) -> str | None:
185
+ """执行 git 命令并返回输出。
186
+
187
+ 参数:
188
+ cwd: 工作目录
189
+ *args: git 命令参数
190
+
191
+ 返回:
192
+ 命令输出字符串,失败返回 None
193
+ """
194
+ run_kwargs: dict = {}
195
+ if sys.platform == "win32":
196
+ run_kwargs["creationflags"] = subprocess.CREATE_NO_WINDOW
197
+ result = subprocess.run(
198
+ ["git", *args],
199
+ cwd=cwd,
200
+ capture_output=True,
201
+ text=True,
202
+ check=False,
203
+ stdin=subprocess.DEVNULL,
204
+ **run_kwargs,
205
+ )
206
+ if result.returncode != 0:
207
+ return None
208
+ return (result.stdout or "").strip()
209
+
210
+
211
+ def _is_subpath(path: Path, parent: Path) -> bool:
212
+ """检查 path 是否为 parent 的子路径。
213
+
214
+ 参数:
215
+ path: 要检查的路径
216
+ parent: 父路径
217
+
218
+ 返回:
219
+ 是否为子路径
220
+ """
221
+ try:
222
+ path.relative_to(parent)
223
+ return True
224
+ except ValueError:
225
+ return False