klaude-code 2.0.1__py3-none-any.whl → 2.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 (160) hide show
  1. klaude_code/app/__init__.py +12 -0
  2. klaude_code/app/runtime.py +215 -0
  3. klaude_code/cli/auth_cmd.py +2 -2
  4. klaude_code/cli/config_cmd.py +2 -2
  5. klaude_code/cli/cost_cmd.py +1 -1
  6. klaude_code/cli/debug.py +12 -36
  7. klaude_code/cli/list_model.py +3 -3
  8. klaude_code/cli/main.py +17 -60
  9. klaude_code/cli/self_update.py +2 -187
  10. klaude_code/cli/session_cmd.py +2 -2
  11. klaude_code/config/config.py +1 -1
  12. klaude_code/config/select_model.py +1 -1
  13. klaude_code/const.py +10 -1
  14. klaude_code/core/agent.py +9 -62
  15. klaude_code/core/agent_profile.py +284 -0
  16. klaude_code/core/executor.py +343 -230
  17. klaude_code/core/manager/llm_clients_builder.py +1 -1
  18. klaude_code/core/manager/sub_agent_manager.py +16 -29
  19. klaude_code/core/reminders.py +107 -155
  20. klaude_code/core/task.py +12 -20
  21. klaude_code/core/tool/__init__.py +5 -19
  22. klaude_code/core/tool/context.py +84 -0
  23. klaude_code/core/tool/file/apply_patch_tool.py +18 -21
  24. klaude_code/core/tool/file/edit_tool.py +42 -44
  25. klaude_code/core/tool/file/read_tool.py +14 -9
  26. klaude_code/core/tool/file/write_tool.py +12 -13
  27. klaude_code/core/tool/report_back_tool.py +4 -1
  28. klaude_code/core/tool/shell/bash_tool.py +6 -11
  29. klaude_code/core/tool/skill/skill_tool.py +3 -1
  30. klaude_code/core/tool/sub_agent_tool.py +8 -7
  31. klaude_code/core/tool/todo/todo_write_tool.py +3 -9
  32. klaude_code/core/tool/todo/update_plan_tool.py +3 -5
  33. klaude_code/core/tool/tool_abc.py +2 -1
  34. klaude_code/core/tool/tool_registry.py +2 -33
  35. klaude_code/core/tool/tool_runner.py +13 -10
  36. klaude_code/core/tool/web/mermaid_tool.py +3 -1
  37. klaude_code/core/tool/web/web_fetch_tool.py +5 -3
  38. klaude_code/core/tool/web/web_search_tool.py +5 -3
  39. klaude_code/core/turn.py +86 -26
  40. klaude_code/llm/anthropic/client.py +1 -1
  41. klaude_code/llm/bedrock/client.py +1 -1
  42. klaude_code/llm/claude/client.py +1 -1
  43. klaude_code/llm/codex/client.py +1 -1
  44. klaude_code/llm/google/client.py +1 -1
  45. klaude_code/llm/openai_compatible/client.py +1 -1
  46. klaude_code/llm/openai_compatible/tool_call_accumulator.py +1 -1
  47. klaude_code/llm/openrouter/client.py +1 -1
  48. klaude_code/llm/openrouter/reasoning.py +1 -1
  49. klaude_code/llm/responses/client.py +1 -1
  50. klaude_code/protocol/events/__init__.py +57 -0
  51. klaude_code/protocol/events/base.py +18 -0
  52. klaude_code/protocol/events/chat.py +20 -0
  53. klaude_code/protocol/events/lifecycle.py +22 -0
  54. klaude_code/protocol/events/metadata.py +15 -0
  55. klaude_code/protocol/events/streaming.py +43 -0
  56. klaude_code/protocol/events/system.py +53 -0
  57. klaude_code/protocol/events/tools.py +23 -0
  58. klaude_code/protocol/message.py +3 -11
  59. klaude_code/protocol/model.py +78 -9
  60. klaude_code/protocol/op.py +5 -0
  61. klaude_code/protocol/sub_agent/explore.py +0 -15
  62. klaude_code/protocol/sub_agent/task.py +1 -1
  63. klaude_code/protocol/sub_agent/web.py +1 -17
  64. klaude_code/protocol/tools.py +0 -1
  65. klaude_code/session/session.py +6 -5
  66. klaude_code/skill/assets/create-plan/SKILL.md +76 -0
  67. klaude_code/skill/loader.py +1 -1
  68. klaude_code/skill/system_skills.py +1 -1
  69. klaude_code/tui/__init__.py +8 -0
  70. klaude_code/{command → tui/command}/clear_cmd.py +2 -1
  71. klaude_code/{command → tui/command}/debug_cmd.py +4 -3
  72. klaude_code/{command → tui/command}/export_cmd.py +2 -1
  73. klaude_code/{command → tui/command}/export_online_cmd.py +6 -5
  74. klaude_code/{command → tui/command}/fork_session_cmd.py +10 -9
  75. klaude_code/{command → tui/command}/help_cmd.py +3 -2
  76. klaude_code/{command → tui/command}/model_cmd.py +5 -4
  77. klaude_code/{command → tui/command}/model_select.py +2 -2
  78. klaude_code/{command → tui/command}/prompt_command.py +4 -3
  79. klaude_code/{command → tui/command}/refresh_cmd.py +3 -1
  80. klaude_code/{command → tui/command}/registry.py +16 -6
  81. klaude_code/{command → tui/command}/release_notes_cmd.py +3 -2
  82. klaude_code/{command → tui/command}/resume_cmd.py +6 -5
  83. klaude_code/{command → tui/command}/status_cmd.py +4 -3
  84. klaude_code/{command → tui/command}/terminal_setup_cmd.py +4 -3
  85. klaude_code/{command → tui/command}/thinking_cmd.py +4 -3
  86. klaude_code/tui/commands.py +164 -0
  87. klaude_code/{ui/renderers → tui/components}/assistant.py +3 -3
  88. klaude_code/{ui/renderers → tui/components}/bash_syntax.py +2 -2
  89. klaude_code/{ui/renderers → tui/components}/common.py +1 -1
  90. klaude_code/tui/components/developer.py +231 -0
  91. klaude_code/{ui/renderers → tui/components}/diffs.py +2 -2
  92. klaude_code/{ui/renderers → tui/components}/errors.py +2 -2
  93. klaude_code/{ui/renderers → tui/components}/metadata.py +34 -21
  94. klaude_code/{ui → tui/components}/rich/markdown.py +78 -34
  95. klaude_code/{ui → tui/components}/rich/status.py +2 -2
  96. klaude_code/{ui → tui/components}/rich/theme.py +12 -5
  97. klaude_code/{ui/renderers → tui/components}/sub_agent.py +23 -43
  98. klaude_code/{ui/renderers → tui/components}/thinking.py +3 -3
  99. klaude_code/{ui/renderers → tui/components}/tools.py +11 -48
  100. klaude_code/{ui/renderers → tui/components}/user_input.py +3 -20
  101. klaude_code/tui/display.py +85 -0
  102. klaude_code/{ui/modes/repl → tui/input}/__init__.py +1 -1
  103. klaude_code/{ui/modes/repl → tui/input}/completers.py +1 -1
  104. klaude_code/{ui/modes/repl/input_prompt_toolkit.py → tui/input/prompt_toolkit.py} +11 -7
  105. klaude_code/tui/machine.py +606 -0
  106. klaude_code/tui/renderer.py +707 -0
  107. klaude_code/tui/runner.py +321 -0
  108. klaude_code/tui/terminal/__init__.py +56 -0
  109. klaude_code/{ui → tui}/terminal/color.py +1 -1
  110. klaude_code/{ui → tui}/terminal/control.py +1 -1
  111. klaude_code/{ui → tui}/terminal/notifier.py +1 -1
  112. klaude_code/{ui → tui}/terminal/selector.py +36 -17
  113. klaude_code/ui/__init__.py +6 -50
  114. klaude_code/ui/core/display.py +3 -3
  115. klaude_code/ui/core/input.py +2 -1
  116. klaude_code/ui/{modes/debug/display.py → debug_mode.py} +1 -1
  117. klaude_code/ui/{modes/exec/display.py → exec_mode.py} +1 -4
  118. klaude_code/ui/terminal/__init__.py +6 -54
  119. klaude_code/ui/terminal/title.py +31 -0
  120. klaude_code/update.py +163 -0
  121. {klaude_code-2.0.1.dist-info → klaude_code-2.1.0.dist-info}/METADATA +1 -1
  122. klaude_code-2.1.0.dist-info/RECORD +235 -0
  123. klaude_code/cli/runtime.py +0 -525
  124. klaude_code/core/prompt.py +0 -108
  125. klaude_code/core/tool/file/move_tool.md +0 -41
  126. klaude_code/core/tool/file/move_tool.py +0 -435
  127. klaude_code/core/tool/tool_context.py +0 -148
  128. klaude_code/protocol/events.py +0 -194
  129. klaude_code/skill/assets/dev-docs/SKILL.md +0 -108
  130. klaude_code/trace/__init__.py +0 -21
  131. klaude_code/ui/core/stage_manager.py +0 -48
  132. klaude_code/ui/modes/__init__.py +0 -1
  133. klaude_code/ui/modes/debug/__init__.py +0 -1
  134. klaude_code/ui/modes/exec/__init__.py +0 -1
  135. klaude_code/ui/modes/repl/display.py +0 -61
  136. klaude_code/ui/modes/repl/event_handler.py +0 -634
  137. klaude_code/ui/modes/repl/renderer.py +0 -463
  138. klaude_code/ui/renderers/developer.py +0 -215
  139. klaude_code/ui/utils/__init__.py +0 -1
  140. klaude_code-2.0.1.dist-info/RECORD +0 -229
  141. /klaude_code/{trace/log.py → log.py} +0 -0
  142. /klaude_code/{command → tui/command}/__init__.py +0 -0
  143. /klaude_code/{command → tui/command}/command_abc.py +0 -0
  144. /klaude_code/{command → tui/command}/prompt-commit.md +0 -0
  145. /klaude_code/{command → tui/command}/prompt-init.md +0 -0
  146. /klaude_code/{ui/renderers → tui/components}/__init__.py +0 -0
  147. /klaude_code/{ui/renderers → tui/components}/mermaid_viewer.py +0 -0
  148. /klaude_code/{ui → tui/components}/rich/__init__.py +0 -0
  149. /klaude_code/{ui → tui/components}/rich/cjk_wrap.py +0 -0
  150. /klaude_code/{ui → tui/components}/rich/code_panel.py +0 -0
  151. /klaude_code/{ui → tui/components}/rich/live.py +0 -0
  152. /klaude_code/{ui → tui/components}/rich/quote.py +0 -0
  153. /klaude_code/{ui → tui/components}/rich/searchable_text.py +0 -0
  154. /klaude_code/{ui/modes/repl → tui/input}/clipboard.py +0 -0
  155. /klaude_code/{ui/modes/repl → tui/input}/key_bindings.py +0 -0
  156. /klaude_code/{ui → tui}/terminal/image.py +0 -0
  157. /klaude_code/{ui → tui}/terminal/progress_bar.py +0 -0
  158. /klaude_code/ui/{utils/common.py → common.py} +0 -0
  159. {klaude_code-2.0.1.dist-info → klaude_code-2.1.0.dist-info}/WHEEL +0 -0
  160. {klaude_code-2.0.1.dist-info → klaude_code-2.1.0.dist-info}/entry_points.txt +0 -0
@@ -1,435 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import asyncio
4
- import contextlib
5
- import os
6
- from pathlib import Path
7
-
8
- from pydantic import BaseModel, Field
9
-
10
- from klaude_code.core.tool.file._utils import file_exists, hash_text_sha256, is_directory, read_text, write_text
11
- from klaude_code.core.tool.file.diff_builder import build_structured_diff
12
- from klaude_code.core.tool.tool_abc import ToolABC, load_desc
13
- from klaude_code.core.tool.tool_context import get_current_file_tracker
14
- from klaude_code.core.tool.tool_registry import register
15
- from klaude_code.protocol import llm_param, message, model, tools
16
-
17
-
18
- class MoveArguments(BaseModel):
19
- source_file_path: str
20
- start_line: int = Field(ge=1, description="Start line number (1-indexed, inclusive)")
21
- end_line: int = Field(ge=1, description="End line number (1-indexed, inclusive)")
22
- target_file_path: str
23
- insert_line: int = Field(ge=1, description="Line number to insert before (1-indexed)")
24
-
25
-
26
- def _build_context_snippet(
27
- all_lines: list[str],
28
- start_line: int,
29
- end_line: int,
30
- context_lines: int = 3,
31
- marker: str = "cut here",
32
- ) -> str:
33
- """Build a snippet showing context around a cut/insert point.
34
-
35
- Args:
36
- all_lines: All lines of the file (after modification).
37
- start_line: 1-indexed start line of the context focus area.
38
- end_line: 1-indexed end line of the context focus area.
39
- context_lines: Number of context lines before and after.
40
- marker: Text to show in the separator line.
41
-
42
- Returns:
43
- Formatted snippet with context and separator.
44
- """
45
- result: list[str] = []
46
-
47
- # Context before
48
- ctx_start = max(1, start_line - context_lines)
49
- for line_no in range(ctx_start, start_line):
50
- idx = line_no - 1
51
- if idx < len(all_lines):
52
- content = all_lines[idx].rstrip("\n")
53
- result.append(f"{line_no:>6}\t{content}")
54
-
55
- # Separator
56
- result.append(f" -------- {marker} --------")
57
-
58
- # Context after
59
- ctx_end = min(len(all_lines), end_line + context_lines)
60
- for line_no in range(end_line, ctx_end + 1):
61
- idx = line_no - 1
62
- if idx < len(all_lines):
63
- content = all_lines[idx].rstrip("\n")
64
- result.append(f"{line_no:>6}\t{content}")
65
-
66
- return "\n".join(result)
67
-
68
-
69
- def _build_insert_context_snippet(
70
- all_lines: list[str],
71
- insert_line: int,
72
- inserted_count: int,
73
- context_lines: int = 3,
74
- ) -> str:
75
- """Build a snippet showing context around inserted content.
76
-
77
- Args:
78
- all_lines: All lines of the file (after insertion).
79
- insert_line: 1-indexed line where content was inserted.
80
- inserted_count: Number of lines that were inserted.
81
- context_lines: Number of context lines before and after.
82
-
83
- Returns:
84
- Formatted snippet with context and inserted content highlighted.
85
- """
86
- result: list[str] = []
87
- insert_end = insert_line + inserted_count - 1
88
-
89
- # Context before
90
- ctx_start = max(1, insert_line - context_lines)
91
- for line_no in range(ctx_start, insert_line):
92
- idx = line_no - 1
93
- if idx < len(all_lines):
94
- content = all_lines[idx].rstrip("\n")
95
- result.append(f"{line_no:>6}\t{content}")
96
-
97
- # Start separator
98
- result.append(" -------- inserted --------")
99
-
100
- # Inserted content
101
- for line_no in range(insert_line, insert_end + 1):
102
- idx = line_no - 1
103
- if idx < len(all_lines):
104
- content = all_lines[idx].rstrip("\n")
105
- result.append(f"{line_no:>6}\t{content}")
106
-
107
- # End separator
108
- result.append(" -------- end --------")
109
-
110
- # Context after
111
- ctx_end = min(len(all_lines), insert_end + context_lines)
112
- for line_no in range(insert_end + 1, ctx_end + 1):
113
- idx = line_no - 1
114
- if idx < len(all_lines):
115
- content = all_lines[idx].rstrip("\n")
116
- result.append(f"{line_no:>6}\t{content}")
117
-
118
- return "\n".join(result)
119
-
120
-
121
- @register(tools.MOVE)
122
- class MoveTool(ToolABC):
123
- @classmethod
124
- def schema(cls) -> llm_param.ToolSchema:
125
- return llm_param.ToolSchema(
126
- name=tools.MOVE,
127
- type="function",
128
- description=load_desc(Path(__file__).parent / "move_tool.md"),
129
- parameters={
130
- "type": "object",
131
- "properties": {
132
- "source_file_path": {
133
- "type": "string",
134
- "description": "The absolute path to the source file to cut from",
135
- },
136
- "start_line": {
137
- "type": "integer",
138
- "description": "Start line number (1-indexed, inclusive)",
139
- },
140
- "end_line": {
141
- "type": "integer",
142
- "description": "End line number (1-indexed, inclusive)",
143
- },
144
- "target_file_path": {
145
- "type": "string",
146
- "description": "The absolute path to the target file to paste into",
147
- },
148
- "insert_line": {
149
- "type": "integer",
150
- "description": "Line number to insert before (1-indexed)",
151
- },
152
- },
153
- "required": ["source_file_path", "start_line", "end_line", "target_file_path", "insert_line"],
154
- "additionalProperties": False,
155
- },
156
- )
157
-
158
- @classmethod
159
- async def call(cls, arguments: str) -> message.ToolResultMessage:
160
- try:
161
- args = MoveArguments.model_validate_json(arguments)
162
- except ValueError as e:
163
- return message.ToolResultMessage(status="error", output_text=f"Invalid arguments: {e}")
164
-
165
- source_path = os.path.abspath(args.source_file_path)
166
- target_path = os.path.abspath(args.target_file_path)
167
- same_file = source_path == target_path
168
-
169
- # Validate paths
170
- if is_directory(source_path):
171
- return message.ToolResultMessage(
172
- status="error",
173
- output_text="<tool_use_error>Source path is a directory, not a file.</tool_use_error>",
174
- )
175
- if is_directory(target_path):
176
- return message.ToolResultMessage(
177
- status="error",
178
- output_text="<tool_use_error>Target path is a directory, not a file.</tool_use_error>",
179
- )
180
-
181
- # Validate line range
182
- if args.start_line > args.end_line:
183
- return message.ToolResultMessage(
184
- status="error",
185
- output_text="<tool_use_error>start_line must be <= end_line.</tool_use_error>",
186
- )
187
-
188
- # Check file tracker
189
- file_tracker = get_current_file_tracker()
190
- source_exists = file_exists(source_path)
191
- target_exists = file_exists(target_path)
192
-
193
- if not source_exists:
194
- return message.ToolResultMessage(
195
- status="error",
196
- output_text="<tool_use_error>Source file does not exist.</tool_use_error>",
197
- )
198
-
199
- source_status: model.FileStatus | None = None
200
- target_status: model.FileStatus | None = None
201
-
202
- if file_tracker is not None:
203
- source_status = file_tracker.get(source_path)
204
- if source_status is None:
205
- return message.ToolResultMessage(
206
- status="error",
207
- output_text="Source file has not been read yet. Read it first.",
208
- )
209
- if target_exists:
210
- target_status = file_tracker.get(target_path)
211
- if target_status is None:
212
- return message.ToolResultMessage(
213
- status="error",
214
- output_text="Target file has not been read yet. Read it first before writing to it.",
215
- )
216
-
217
- # Read source file
218
- try:
219
- source_content = await asyncio.to_thread(read_text, source_path)
220
- except OSError as e:
221
- return message.ToolResultMessage(
222
- status="error", output_text=f"<tool_use_error>Failed to read source: {e}</tool_use_error>"
223
- )
224
-
225
- # Verify source hasn't been modified externally
226
- if source_status is not None and source_status.content_sha256 is not None:
227
- current_sha256 = hash_text_sha256(source_content)
228
- if current_sha256 != source_status.content_sha256:
229
- return message.ToolResultMessage(
230
- status="error",
231
- output_text="Source file has been modified externally. Read it first before editing.",
232
- )
233
-
234
- source_lines = source_content.splitlines(keepends=True)
235
-
236
- # Validate line numbers against actual file
237
- if args.start_line > len(source_lines):
238
- return message.ToolResultMessage(
239
- status="error",
240
- output_text=f"<tool_use_error>start_line {args.start_line} exceeds file length {len(source_lines)}.</tool_use_error>",
241
- )
242
- if args.end_line > len(source_lines):
243
- return message.ToolResultMessage(
244
- status="error",
245
- output_text=f"<tool_use_error>end_line {args.end_line} exceeds file length {len(source_lines)}.</tool_use_error>",
246
- )
247
-
248
- # Extract the lines to move (convert to 0-indexed)
249
- cut_lines = source_lines[args.start_line - 1 : args.end_line]
250
-
251
- # Read target file content (if exists)
252
- target_before = ""
253
- if target_exists:
254
- try:
255
- target_before = await asyncio.to_thread(read_text, target_path)
256
- except OSError as e:
257
- return message.ToolResultMessage(
258
- status="error", output_text=f"<tool_use_error>Failed to read target: {e}</tool_use_error>"
259
- )
260
-
261
- # Verify target hasn't been modified externally
262
- if target_status is not None and target_status.content_sha256 is not None:
263
- current_sha256 = hash_text_sha256(target_before)
264
- if current_sha256 != target_status.content_sha256:
265
- return message.ToolResultMessage(
266
- status="error",
267
- output_text="Target file has been modified externally. Read it first before writing to it.",
268
- )
269
-
270
- # For new target file, only allow insert_line = 1
271
- if not target_exists and args.insert_line != 1:
272
- return message.ToolResultMessage(
273
- status="error",
274
- output_text="<tool_use_error>Target file does not exist. Use insert_line=1 to create new file.</tool_use_error>",
275
- )
276
-
277
- # Build new content for both files
278
- source_before = source_content
279
-
280
- if same_file:
281
- # Same file move: more complex logic
282
- # First remove the cut lines, then insert at adjusted position
283
- new_lines = source_lines[: args.start_line - 1] + source_lines[args.end_line :]
284
-
285
- # Adjust insert position if it was after the cut region
286
- adjusted_insert = args.insert_line
287
- if args.insert_line > args.end_line:
288
- adjusted_insert -= args.end_line - args.start_line + 1
289
- elif args.insert_line > args.start_line:
290
- # Insert position is within the cut region - error
291
- return message.ToolResultMessage(
292
- status="error",
293
- output_text="<tool_use_error>insert_line cannot be within the cut range for same-file move.</tool_use_error>",
294
- )
295
-
296
- # Validate adjusted insert line
297
- if adjusted_insert > len(new_lines) + 1:
298
- return message.ToolResultMessage(
299
- status="error",
300
- output_text=f"<tool_use_error>insert_line {args.insert_line} is out of bounds after cut.</tool_use_error>",
301
- )
302
-
303
- # Insert at adjusted position
304
- final_lines = new_lines[: adjusted_insert - 1] + cut_lines + new_lines[adjusted_insert - 1 :]
305
- source_after = "".join(final_lines)
306
- target_after = source_after # Same file
307
-
308
- # Write the file once
309
- try:
310
- await asyncio.to_thread(write_text, source_path, source_after)
311
- except OSError as e:
312
- return message.ToolResultMessage(
313
- status="error", output_text=f"<tool_use_error>Failed to write: {e}</tool_use_error>"
314
- )
315
-
316
- # Update tracker
317
- if file_tracker is not None:
318
- with contextlib.suppress(Exception):
319
- existing = file_tracker.get(source_path)
320
- is_mem = existing.is_memory if existing else False
321
- file_tracker[source_path] = model.FileStatus(
322
- mtime=Path(source_path).stat().st_mtime,
323
- content_sha256=hash_text_sha256(source_after),
324
- is_memory=is_mem,
325
- )
326
-
327
- ui_extra = build_structured_diff(source_before, source_after, file_path=source_path)
328
- cut_count = args.end_line - args.start_line + 1
329
-
330
- # Build context snippets for same-file move
331
- final_lines = source_after.splitlines(keepends=True)
332
- # Show context around cut location (now joined)
333
- cut_context = _build_context_snippet(final_lines, args.start_line, args.start_line, marker="cut here")
334
- # Show context around insert location
335
- insert_context = _build_insert_context_snippet(final_lines, adjusted_insert, cut_count)
336
-
337
- output = (
338
- f"Moved {cut_count} lines within {source_path} "
339
- f"(from lines {args.start_line}-{args.end_line} to line {args.insert_line}).\n\n"
340
- f"Source context (after cut):\n{cut_context}\n\n"
341
- f"Insert context:\n{insert_context}"
342
- )
343
- return message.ToolResultMessage(
344
- status="success",
345
- output_text=output,
346
- ui_extra=ui_extra,
347
- )
348
- else:
349
- # Different files
350
- # Remove lines from source
351
- new_source_lines = source_lines[: args.start_line - 1] + source_lines[args.end_line :]
352
- source_after = "".join(new_source_lines)
353
-
354
- # Insert into target
355
- target_lines = target_before.splitlines(keepends=True) if target_before else []
356
-
357
- # Validate insert_line for existing target
358
- if target_exists and args.insert_line > len(target_lines) + 1:
359
- return message.ToolResultMessage(
360
- status="error",
361
- output_text=f"<tool_use_error>insert_line {args.insert_line} exceeds target file length + 1.</tool_use_error>",
362
- )
363
-
364
- new_target_lines = target_lines[: args.insert_line - 1] + cut_lines + target_lines[args.insert_line - 1 :]
365
- target_after = "".join(new_target_lines)
366
-
367
- # Ensure target ends with newline if source content did
368
- if cut_lines and not target_after.endswith("\n"):
369
- target_after += "\n"
370
-
371
- # Write both files
372
- try:
373
- await asyncio.to_thread(write_text, source_path, source_after)
374
- await asyncio.to_thread(write_text, target_path, target_after)
375
- except OSError as e:
376
- return message.ToolResultMessage(
377
- status="error", output_text=f"<tool_use_error>Failed to write: {e}</tool_use_error>"
378
- )
379
-
380
- # Update tracker for both files
381
- if file_tracker is not None:
382
- with contextlib.suppress(Exception):
383
- existing = file_tracker.get(source_path)
384
- is_mem = existing.is_memory if existing else False
385
- file_tracker[source_path] = model.FileStatus(
386
- mtime=Path(source_path).stat().st_mtime,
387
- content_sha256=hash_text_sha256(source_after),
388
- is_memory=is_mem,
389
- )
390
- with contextlib.suppress(Exception):
391
- existing = file_tracker.get(target_path)
392
- is_mem = existing.is_memory if existing else False
393
- file_tracker[target_path] = model.FileStatus(
394
- mtime=Path(target_path).stat().st_mtime,
395
- content_sha256=hash_text_sha256(target_after),
396
- is_memory=is_mem,
397
- )
398
-
399
- # Build UI extra with diffs for both files
400
- source_diff = build_structured_diff(source_before, source_after, file_path=source_path)
401
- target_diff = build_structured_diff(target_before, target_after, file_path=target_path)
402
-
403
- ui_extra: model.ToolResultUIExtra | None = None
404
- if source_diff and target_diff:
405
- ui_extra = model.MultiUIExtra(items=[source_diff, target_diff])
406
- elif source_diff:
407
- ui_extra = source_diff
408
- elif target_diff:
409
- ui_extra = target_diff
410
-
411
- cut_count = args.end_line - args.start_line + 1
412
- action = "created" if not target_exists else "updated"
413
-
414
- # Build context snippets for different-file move
415
- source_after_lines = source_after.splitlines(keepends=True)
416
- target_after_lines = target_after.splitlines(keepends=True)
417
-
418
- # Show context around cut location in source file
419
- source_context = _build_context_snippet(
420
- source_after_lines, args.start_line, args.start_line, marker="cut here"
421
- )
422
- # Show context around insert location in target file
423
- target_context = _build_insert_context_snippet(target_after_lines, args.insert_line, cut_count)
424
-
425
- output = (
426
- f"Moved {cut_count} lines from {source_path} (lines {args.start_line}-{args.end_line}) "
427
- f"to {target_path} ({action}) at line {args.insert_line}.\n\n"
428
- f"Source file context (after move):\n{source_context}\n\n"
429
- f"Target file context (after insert):\n{target_context}"
430
- )
431
- return message.ToolResultMessage(
432
- status="success",
433
- output_text=output,
434
- ui_extra=ui_extra,
435
- )
@@ -1,148 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from collections.abc import Awaitable, Callable, Generator, MutableMapping
4
- from contextlib import contextmanager
5
- from contextvars import ContextVar, Token
6
- from dataclasses import dataclass
7
-
8
- from klaude_code.protocol import model
9
- from klaude_code.protocol.sub_agent import SubAgentResult
10
- from klaude_code.session.session import Session
11
-
12
- type FileTracker = MutableMapping[str, model.FileStatus]
13
-
14
-
15
- @dataclass
16
- class TodoContext:
17
- """Todo access interface exposed to tools.
18
-
19
- Tools can only read the current todo list and replace it with
20
- a new list; they cannot access the full Session object.
21
- """
22
-
23
- get_todos: Callable[[], list[model.TodoItem]]
24
- set_todos: Callable[[list[model.TodoItem]], None]
25
-
26
-
27
- @dataclass
28
- class SessionTodoStore:
29
- """Adapter exposing session todos through an explicit interface."""
30
-
31
- session: Session
32
-
33
- def get(self) -> list[model.TodoItem]:
34
- return self.session.todos
35
-
36
- def set(self, todos: list[model.TodoItem]) -> None:
37
- self.session.todos = todos
38
-
39
-
40
- @dataclass
41
- class ToolContextToken:
42
- """Tokens used to restore tool execution context.
43
-
44
- This captures the contextvar tokens for the current file tracker
45
- and todo context so callers can safely reset them after a tool
46
- finishes running.
47
- """
48
-
49
- file_tracker_token: Token[FileTracker | None] | None
50
- todo_token: Token[TodoContext | None] | None
51
-
52
-
53
- # Holds the current file tracker mapping for tool execution context.
54
- # Set by Agent/Reminder right before invoking a tool.
55
- current_file_tracker_var: ContextVar[FileTracker | None] = ContextVar("current_file_tracker", default=None)
56
-
57
-
58
- # Holds the todo access context for tools.
59
- current_todo_context_var: ContextVar[TodoContext | None] = ContextVar("current_todo_context", default=None)
60
-
61
-
62
- def set_tool_context_from_session(session: Session) -> ToolContextToken:
63
- """Bind the given session's file tracker and todos into tool context.
64
-
65
- This should be called by the Agent or reminder helpers immediately
66
- before invoking tools so that file and todo tools can operate on
67
- the correct per-session state without seeing the full Session.
68
- """
69
-
70
- file_tracker_token = current_file_tracker_var.set(session.file_tracker)
71
- todo_ctx = build_todo_context(session)
72
- todo_token = current_todo_context_var.set(todo_ctx)
73
- return ToolContextToken(file_tracker_token=file_tracker_token, todo_token=todo_token)
74
-
75
-
76
- def reset_tool_context(token: ToolContextToken) -> None:
77
- """Restore tool execution context from a previously captured token."""
78
-
79
- if token.file_tracker_token is not None:
80
- current_file_tracker_var.reset(token.file_tracker_token)
81
- if token.todo_token is not None:
82
- current_todo_context_var.reset(token.todo_token)
83
-
84
-
85
- @contextmanager
86
- def tool_context(file_tracker: FileTracker, todo_ctx: TodoContext) -> Generator[ToolContextToken]:
87
- """Context manager for setting and resetting tool execution context."""
88
-
89
- file_tracker_token = current_file_tracker_var.set(file_tracker)
90
- todo_token = current_todo_context_var.set(todo_ctx)
91
- token = ToolContextToken(file_tracker_token=file_tracker_token, todo_token=todo_token)
92
- try:
93
- yield token
94
- finally:
95
- reset_tool_context(token)
96
-
97
-
98
- def build_todo_context(session: Session) -> TodoContext:
99
- """Create a TodoContext backed by the given session."""
100
-
101
- store = SessionTodoStore(session)
102
- return TodoContext(get_todos=store.get, set_todos=store.set)
103
-
104
-
105
- def get_current_file_tracker() -> FileTracker | None:
106
- """Return the current file tracker mapping for this tool context."""
107
-
108
- return current_file_tracker_var.get()
109
-
110
-
111
- def get_current_todo_context() -> TodoContext | None:
112
- """Return the current todo access context for this tool context."""
113
-
114
- return current_todo_context_var.get()
115
-
116
-
117
- # Holds a handle to run a nested subtask (sub-agent) from within a tool call.
118
- # The callable takes a model.SubAgentState and returns a SubAgentResult.
119
- current_run_subtask_callback: ContextVar[Callable[[model.SubAgentState], Awaitable[SubAgentResult]] | None] = (
120
- ContextVar("current_run_subtask_callback", default=None)
121
- )
122
-
123
-
124
- # Allows sub-agent execution to record the created/used session id for the currently
125
- # executing tool call (used by ToolExecutor.cancel() to include session_id in UIExtra).
126
- current_sub_agent_session_id_recorder: ContextVar[Callable[[str], None] | None] = ContextVar(
127
- "current_sub_agent_session_id_recorder",
128
- default=None,
129
- )
130
-
131
-
132
- def record_sub_agent_session_id(session_id: str) -> None:
133
- """Record the sub-agent session id for the current tool call, if supported."""
134
-
135
- recorder = current_sub_agent_session_id_recorder.get()
136
- if recorder is None:
137
- return
138
- recorder(session_id)
139
-
140
-
141
- # Tracks sub-agent resume claims for the current turn.
142
- #
143
- # This is used to reject multiple sub-agent tool calls in the same LLM response
144
- # that attempt to resume the same agent ID.
145
- current_sub_agent_resume_claims: ContextVar[set[str] | None] = ContextVar(
146
- "current_sub_agent_resume_claims",
147
- default=None,
148
- )