klaude-code 1.2.6__py3-none-any.whl → 1.8.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 (205) hide show
  1. klaude_code/auth/__init__.py +24 -0
  2. klaude_code/auth/codex/__init__.py +20 -0
  3. klaude_code/auth/codex/exceptions.py +17 -0
  4. klaude_code/auth/codex/jwt_utils.py +45 -0
  5. klaude_code/auth/codex/oauth.py +229 -0
  6. klaude_code/auth/codex/token_manager.py +84 -0
  7. klaude_code/cli/auth_cmd.py +73 -0
  8. klaude_code/cli/config_cmd.py +91 -0
  9. klaude_code/cli/cost_cmd.py +338 -0
  10. klaude_code/cli/debug.py +78 -0
  11. klaude_code/cli/list_model.py +307 -0
  12. klaude_code/cli/main.py +233 -134
  13. klaude_code/cli/runtime.py +309 -117
  14. klaude_code/{version.py → cli/self_update.py} +114 -5
  15. klaude_code/cli/session_cmd.py +37 -21
  16. klaude_code/command/__init__.py +88 -27
  17. klaude_code/command/clear_cmd.py +8 -7
  18. klaude_code/command/command_abc.py +31 -31
  19. klaude_code/command/debug_cmd.py +79 -0
  20. klaude_code/command/export_cmd.py +19 -53
  21. klaude_code/command/export_online_cmd.py +154 -0
  22. klaude_code/command/fork_session_cmd.py +267 -0
  23. klaude_code/command/help_cmd.py +7 -8
  24. klaude_code/command/model_cmd.py +60 -10
  25. klaude_code/command/model_select.py +84 -0
  26. klaude_code/command/prompt-jj-describe.md +32 -0
  27. klaude_code/command/prompt_command.py +19 -11
  28. klaude_code/command/refresh_cmd.py +8 -10
  29. klaude_code/command/registry.py +139 -40
  30. klaude_code/command/release_notes_cmd.py +84 -0
  31. klaude_code/command/resume_cmd.py +111 -0
  32. klaude_code/command/status_cmd.py +104 -60
  33. klaude_code/command/terminal_setup_cmd.py +7 -9
  34. klaude_code/command/thinking_cmd.py +98 -0
  35. klaude_code/config/__init__.py +14 -6
  36. klaude_code/config/assets/__init__.py +1 -0
  37. klaude_code/config/assets/builtin_config.yaml +303 -0
  38. klaude_code/config/builtin_config.py +38 -0
  39. klaude_code/config/config.py +378 -109
  40. klaude_code/config/select_model.py +117 -53
  41. klaude_code/config/thinking.py +269 -0
  42. klaude_code/{const/__init__.py → const.py} +50 -19
  43. klaude_code/core/agent.py +20 -28
  44. klaude_code/core/executor.py +327 -112
  45. klaude_code/core/manager/__init__.py +2 -4
  46. klaude_code/core/manager/llm_clients.py +1 -15
  47. klaude_code/core/manager/llm_clients_builder.py +10 -11
  48. klaude_code/core/manager/sub_agent_manager.py +37 -6
  49. klaude_code/core/prompt.py +63 -44
  50. klaude_code/core/prompts/prompt-claude-code.md +2 -13
  51. klaude_code/core/prompts/prompt-codex-gpt-5-1-codex-max.md +117 -0
  52. klaude_code/core/prompts/prompt-codex-gpt-5-2-codex.md +117 -0
  53. klaude_code/core/prompts/prompt-codex.md +9 -42
  54. klaude_code/core/prompts/prompt-minimal.md +12 -0
  55. klaude_code/core/prompts/{prompt-subagent-explore.md → prompt-sub-agent-explore.md} +16 -3
  56. klaude_code/core/prompts/{prompt-subagent-oracle.md → prompt-sub-agent-oracle.md} +1 -2
  57. klaude_code/core/prompts/prompt-sub-agent-web.md +51 -0
  58. klaude_code/core/reminders.py +283 -95
  59. klaude_code/core/task.py +113 -75
  60. klaude_code/core/tool/__init__.py +24 -31
  61. klaude_code/core/tool/file/_utils.py +36 -0
  62. klaude_code/core/tool/file/apply_patch.py +17 -25
  63. klaude_code/core/tool/file/apply_patch_tool.py +57 -77
  64. klaude_code/core/tool/file/diff_builder.py +151 -0
  65. klaude_code/core/tool/file/edit_tool.py +50 -63
  66. klaude_code/core/tool/file/move_tool.md +41 -0
  67. klaude_code/core/tool/file/move_tool.py +435 -0
  68. klaude_code/core/tool/file/read_tool.md +1 -1
  69. klaude_code/core/tool/file/read_tool.py +86 -86
  70. klaude_code/core/tool/file/write_tool.py +59 -69
  71. klaude_code/core/tool/report_back_tool.py +84 -0
  72. klaude_code/core/tool/shell/bash_tool.py +265 -22
  73. klaude_code/core/tool/shell/command_safety.py +3 -6
  74. klaude_code/core/tool/{memory → skill}/skill_tool.py +16 -26
  75. klaude_code/core/tool/sub_agent_tool.py +13 -2
  76. klaude_code/core/tool/todo/todo_write_tool.md +0 -157
  77. klaude_code/core/tool/todo/todo_write_tool.py +1 -1
  78. klaude_code/core/tool/todo/todo_write_tool_raw.md +182 -0
  79. klaude_code/core/tool/todo/update_plan_tool.py +1 -1
  80. klaude_code/core/tool/tool_abc.py +18 -0
  81. klaude_code/core/tool/tool_context.py +27 -12
  82. klaude_code/core/tool/tool_registry.py +7 -7
  83. klaude_code/core/tool/tool_runner.py +44 -36
  84. klaude_code/core/tool/truncation.py +29 -14
  85. klaude_code/core/tool/web/mermaid_tool.md +43 -0
  86. klaude_code/core/tool/web/mermaid_tool.py +2 -5
  87. klaude_code/core/tool/web/web_fetch_tool.md +1 -1
  88. klaude_code/core/tool/web/web_fetch_tool.py +112 -22
  89. klaude_code/core/tool/web/web_search_tool.md +23 -0
  90. klaude_code/core/tool/web/web_search_tool.py +130 -0
  91. klaude_code/core/turn.py +168 -66
  92. klaude_code/llm/__init__.py +2 -10
  93. klaude_code/llm/anthropic/client.py +190 -178
  94. klaude_code/llm/anthropic/input.py +39 -15
  95. klaude_code/llm/bedrock/__init__.py +3 -0
  96. klaude_code/llm/bedrock/client.py +60 -0
  97. klaude_code/llm/client.py +7 -21
  98. klaude_code/llm/codex/__init__.py +5 -0
  99. klaude_code/llm/codex/client.py +149 -0
  100. klaude_code/llm/google/__init__.py +3 -0
  101. klaude_code/llm/google/client.py +309 -0
  102. klaude_code/llm/google/input.py +215 -0
  103. klaude_code/llm/input_common.py +3 -9
  104. klaude_code/llm/openai_compatible/client.py +72 -164
  105. klaude_code/llm/openai_compatible/input.py +6 -4
  106. klaude_code/llm/openai_compatible/stream.py +273 -0
  107. klaude_code/llm/openai_compatible/tool_call_accumulator.py +17 -1
  108. klaude_code/llm/openrouter/client.py +89 -160
  109. klaude_code/llm/openrouter/input.py +18 -30
  110. klaude_code/llm/openrouter/reasoning.py +118 -0
  111. klaude_code/llm/registry.py +39 -7
  112. klaude_code/llm/responses/client.py +184 -171
  113. klaude_code/llm/responses/input.py +20 -1
  114. klaude_code/llm/usage.py +17 -12
  115. klaude_code/protocol/commands.py +17 -1
  116. klaude_code/protocol/events.py +31 -4
  117. klaude_code/protocol/llm_param.py +13 -10
  118. klaude_code/protocol/model.py +232 -29
  119. klaude_code/protocol/op.py +90 -1
  120. klaude_code/protocol/op_handler.py +35 -1
  121. klaude_code/protocol/sub_agent/__init__.py +117 -0
  122. klaude_code/protocol/sub_agent/explore.py +63 -0
  123. klaude_code/protocol/sub_agent/oracle.py +91 -0
  124. klaude_code/protocol/sub_agent/task.py +61 -0
  125. klaude_code/protocol/sub_agent/web.py +79 -0
  126. klaude_code/protocol/tools.py +4 -2
  127. klaude_code/session/__init__.py +2 -2
  128. klaude_code/session/codec.py +71 -0
  129. klaude_code/session/export.py +293 -86
  130. klaude_code/session/selector.py +89 -67
  131. klaude_code/session/session.py +320 -309
  132. klaude_code/session/store.py +220 -0
  133. klaude_code/session/templates/export_session.html +595 -83
  134. klaude_code/session/templates/mermaid_viewer.html +926 -0
  135. klaude_code/skill/__init__.py +27 -0
  136. klaude_code/skill/assets/deslop/SKILL.md +17 -0
  137. klaude_code/skill/assets/dev-docs/SKILL.md +108 -0
  138. klaude_code/skill/assets/handoff/SKILL.md +39 -0
  139. klaude_code/skill/assets/jj-workspace/SKILL.md +20 -0
  140. klaude_code/skill/assets/skill-creator/SKILL.md +139 -0
  141. klaude_code/{core/tool/memory/skill_loader.py → skill/loader.py} +55 -15
  142. klaude_code/skill/manager.py +70 -0
  143. klaude_code/skill/system_skills.py +192 -0
  144. klaude_code/trace/__init__.py +20 -2
  145. klaude_code/trace/log.py +150 -5
  146. klaude_code/ui/__init__.py +4 -9
  147. klaude_code/ui/core/input.py +1 -1
  148. klaude_code/ui/core/stage_manager.py +7 -7
  149. klaude_code/ui/modes/debug/display.py +2 -1
  150. klaude_code/ui/modes/repl/__init__.py +3 -48
  151. klaude_code/ui/modes/repl/clipboard.py +5 -5
  152. klaude_code/ui/modes/repl/completers.py +487 -123
  153. klaude_code/ui/modes/repl/display.py +5 -4
  154. klaude_code/ui/modes/repl/event_handler.py +370 -117
  155. klaude_code/ui/modes/repl/input_prompt_toolkit.py +552 -105
  156. klaude_code/ui/modes/repl/key_bindings.py +146 -23
  157. klaude_code/ui/modes/repl/renderer.py +189 -99
  158. klaude_code/ui/renderers/assistant.py +9 -2
  159. klaude_code/ui/renderers/bash_syntax.py +178 -0
  160. klaude_code/ui/renderers/common.py +78 -0
  161. klaude_code/ui/renderers/developer.py +104 -48
  162. klaude_code/ui/renderers/diffs.py +87 -6
  163. klaude_code/ui/renderers/errors.py +11 -6
  164. klaude_code/ui/renderers/mermaid_viewer.py +57 -0
  165. klaude_code/ui/renderers/metadata.py +112 -76
  166. klaude_code/ui/renderers/sub_agent.py +92 -7
  167. klaude_code/ui/renderers/thinking.py +40 -18
  168. klaude_code/ui/renderers/tools.py +405 -227
  169. klaude_code/ui/renderers/user_input.py +73 -13
  170. klaude_code/ui/rich/__init__.py +10 -1
  171. klaude_code/ui/rich/cjk_wrap.py +228 -0
  172. klaude_code/ui/rich/code_panel.py +131 -0
  173. klaude_code/ui/rich/live.py +17 -0
  174. klaude_code/ui/rich/markdown.py +305 -170
  175. klaude_code/ui/rich/searchable_text.py +10 -13
  176. klaude_code/ui/rich/status.py +190 -49
  177. klaude_code/ui/rich/theme.py +135 -39
  178. klaude_code/ui/terminal/__init__.py +55 -0
  179. klaude_code/ui/terminal/color.py +1 -1
  180. klaude_code/ui/terminal/control.py +13 -22
  181. klaude_code/ui/terminal/notifier.py +44 -4
  182. klaude_code/ui/terminal/selector.py +658 -0
  183. klaude_code/ui/utils/common.py +0 -18
  184. klaude_code-1.8.0.dist-info/METADATA +377 -0
  185. klaude_code-1.8.0.dist-info/RECORD +219 -0
  186. {klaude_code-1.2.6.dist-info → klaude_code-1.8.0.dist-info}/entry_points.txt +1 -0
  187. klaude_code/command/diff_cmd.py +0 -138
  188. klaude_code/command/prompt-dev-docs-update.md +0 -56
  189. klaude_code/command/prompt-dev-docs.md +0 -46
  190. klaude_code/config/list_model.py +0 -162
  191. klaude_code/core/manager/agent_manager.py +0 -127
  192. klaude_code/core/prompts/prompt-subagent-webfetch.md +0 -46
  193. klaude_code/core/tool/file/multi_edit_tool.md +0 -42
  194. klaude_code/core/tool/file/multi_edit_tool.py +0 -199
  195. klaude_code/core/tool/memory/memory_tool.md +0 -16
  196. klaude_code/core/tool/memory/memory_tool.py +0 -462
  197. klaude_code/llm/openrouter/reasoning_handler.py +0 -209
  198. klaude_code/protocol/sub_agent.py +0 -348
  199. klaude_code/ui/utils/debouncer.py +0 -42
  200. klaude_code-1.2.6.dist-info/METADATA +0 -178
  201. klaude_code-1.2.6.dist-info/RECORD +0 -167
  202. /klaude_code/core/prompts/{prompt-subagent.md → prompt-sub-agent.md} +0 -0
  203. /klaude_code/core/tool/{memory → skill}/__init__.py +0 -0
  204. /klaude_code/core/tool/{memory → skill}/skill_tool.md +0 -0
  205. {klaude_code-1.2.6.dist-info → klaude_code-1.8.0.dist-info}/WHEEL +0 -0
@@ -1,17 +1,43 @@
1
1
  import json
2
2
  from pathlib import Path
3
+ from typing import Any, cast
3
4
 
4
- from rich.console import RenderableType
5
+ from rich import box
6
+ from rich.console import Group, RenderableType
5
7
  from rich.padding import Padding
8
+ from rich.panel import Panel
9
+ from rich.style import Style
6
10
  from rich.text import Text
7
11
 
8
12
  from klaude_code import const
9
- from klaude_code.protocol import events, model
13
+ from klaude_code.protocol import events, model, tools
10
14
  from klaude_code.protocol.sub_agent import is_sub_agent_tool as _is_sub_agent_tool
11
15
  from klaude_code.ui.renderers import diffs as r_diffs
12
- from klaude_code.ui.renderers.common import create_grid
16
+ from klaude_code.ui.renderers import mermaid_viewer as r_mermaid_viewer
17
+ from klaude_code.ui.renderers.bash_syntax import highlight_bash_command
18
+ from klaude_code.ui.renderers.common import create_grid, truncate_display
19
+ from klaude_code.ui.rich.code_panel import CodePanel
20
+ from klaude_code.ui.rich.markdown import NoInsetMarkdown
13
21
  from klaude_code.ui.rich.theme import ThemeKey
14
- from klaude_code.ui.utils.common import truncate_display
22
+
23
+ # Tool markers (Unicode symbols for UI display)
24
+ MARK_GENERIC = "⚒"
25
+ MARK_BASH = "$"
26
+ MARK_PLAN = "◈"
27
+ MARK_READ = "→"
28
+ MARK_EDIT = "±"
29
+ MARK_WRITE = "+"
30
+ MARK_MOVE = "±"
31
+ MARK_MERMAID = "⧉"
32
+ MARK_WEB_FETCH = "→"
33
+ MARK_WEB_SEARCH = "✱"
34
+ MARK_DONE = "✔"
35
+ MARK_SKILL = "✪"
36
+
37
+ # Todo status markers
38
+ MARK_TODO_PENDING = "▢"
39
+ MARK_TODO_IN_PROGRESS = "◉"
40
+ MARK_TODO_COMPLETED = "✔"
15
41
 
16
42
 
17
43
  def is_sub_agent_tool(tool_name: str) -> bool:
@@ -30,7 +56,7 @@ def render_path(path: str, style: str, is_directory: bool = False) -> Text:
30
56
  return Text(path, style=style)
31
57
 
32
58
 
33
- def render_generic_tool_call(tool_name: str, arguments: str, markup: str = "•") -> RenderableType:
59
+ def render_generic_tool_call(tool_name: str, arguments: str, markup: str = MARK_GENERIC) -> RenderableType:
34
60
  grid = create_grid()
35
61
 
36
62
  tool_name_column = Text.assemble((markup, ThemeKey.TOOL_MARK), " ", (tool_name, ThemeKey.TOOL_NAME))
@@ -58,9 +84,73 @@ def render_generic_tool_call(tool_name: str, arguments: str, markup: str = "•"
58
84
  return grid
59
85
 
60
86
 
87
+ def render_bash_tool_call(arguments: str) -> RenderableType:
88
+ grid = create_grid()
89
+ tool_name_column = Text.assemble((MARK_BASH, ThemeKey.TOOL_MARK), " ", ("Bash", ThemeKey.TOOL_NAME))
90
+
91
+ try:
92
+ payload_raw: Any = json.loads(arguments) if arguments else {}
93
+ except json.JSONDecodeError:
94
+ summary = Text(
95
+ arguments.strip()[: const.INVALID_TOOL_CALL_MAX_LENGTH],
96
+ style=ThemeKey.INVALID_TOOL_CALL_ARGS,
97
+ )
98
+ grid.add_row(tool_name_column, summary)
99
+ return grid
100
+
101
+ if not isinstance(payload_raw, dict):
102
+ summary = Text(
103
+ str(payload_raw)[: const.INVALID_TOOL_CALL_MAX_LENGTH],
104
+ style=ThemeKey.INVALID_TOOL_CALL_ARGS,
105
+ )
106
+ grid.add_row(tool_name_column, summary)
107
+ return grid
108
+
109
+ payload: dict[str, object] = cast(dict[str, object], payload_raw)
110
+
111
+ command = payload.get("command")
112
+ timeout_ms = payload.get("timeout_ms")
113
+
114
+ # Build the command display with optional timeout suffix
115
+ if isinstance(command, str) and command.strip():
116
+ cmd_str = command.strip()
117
+ line_count = len(cmd_str.splitlines())
118
+
119
+ highlighted = highlight_bash_command(cmd_str)
120
+
121
+ # For commands > 10 lines, use CodePanel for better display
122
+ if line_count > 10:
123
+ code_panel = CodePanel(highlighted, border_style=ThemeKey.LINES)
124
+ if isinstance(timeout_ms, int):
125
+ if timeout_ms >= 1000 and timeout_ms % 1000 == 0:
126
+ timeout_text = Text(f"{timeout_ms // 1000}s", style=ThemeKey.TOOL_TIMEOUT)
127
+ else:
128
+ timeout_text = Text(f"{timeout_ms}ms", style=ThemeKey.TOOL_TIMEOUT)
129
+ grid.add_row(tool_name_column, Group(code_panel, timeout_text))
130
+ else:
131
+ grid.add_row(tool_name_column, code_panel)
132
+ return grid
133
+ if isinstance(timeout_ms, int):
134
+ if timeout_ms >= 1000 and timeout_ms % 1000 == 0:
135
+ highlighted.append(f" {timeout_ms // 1000}s", style=ThemeKey.TOOL_TIMEOUT)
136
+ else:
137
+ highlighted.append(f" {timeout_ms}ms", style=ThemeKey.TOOL_TIMEOUT)
138
+ grid.add_row(tool_name_column, highlighted)
139
+ else:
140
+ summary = Text("", ThemeKey.TOOL_PARAM)
141
+ if isinstance(timeout_ms, int):
142
+ if timeout_ms >= 1000 and timeout_ms % 1000 == 0:
143
+ summary.append(f"{timeout_ms // 1000}s", style=ThemeKey.TOOL_TIMEOUT)
144
+ else:
145
+ summary.append(f"{timeout_ms}ms", style=ThemeKey.TOOL_TIMEOUT)
146
+ grid.add_row(tool_name_column, summary)
147
+
148
+ return grid
149
+
150
+
61
151
  def render_update_plan_tool_call(arguments: str) -> RenderableType:
62
152
  grid = create_grid()
63
- tool_name_column = Text.assemble(("◎", ThemeKey.TOOL_MARK), " ", ("Update Plan", ThemeKey.TOOL_NAME))
153
+ tool_name_column = Text.assemble((MARK_PLAN, ThemeKey.TOOL_MARK), " ", ("Update Plan", ThemeKey.TOOL_NAME))
64
154
  explanation_column = Text("")
65
155
 
66
156
  if arguments:
@@ -117,154 +207,157 @@ def render_read_tool_call(arguments: str) -> RenderableType:
117
207
  style=ThemeKey.INVALID_TOOL_CALL_ARGS,
118
208
  )
119
209
  )
120
- grid.add_row(Text("←", ThemeKey.TOOL_MARK), render_result)
210
+ grid.add_row(Text(MARK_READ, ThemeKey.TOOL_MARK), render_result)
121
211
  return grid
122
212
 
123
213
 
124
- def render_edit_tool_call(arguments: str) -> Text:
125
- render_result: Text = Text.assemble(("→ ", ThemeKey.TOOL_MARK))
214
+ def render_edit_tool_call(arguments: str) -> RenderableType:
215
+ grid = create_grid()
216
+ tool_name_column = Text.assemble((MARK_EDIT, ThemeKey.TOOL_MARK), " ", ("Edit", ThemeKey.TOOL_NAME))
126
217
  try:
127
218
  json_dict = json.loads(arguments)
128
219
  file_path = json_dict.get("file_path")
129
- render_result = (
130
- render_result.append_text(Text("Edit", ThemeKey.TOOL_NAME))
131
- .append_text(Text(" "))
132
- .append_text(render_path(file_path, ThemeKey.TOOL_PARAM_FILE_PATH))
133
- )
220
+ replace_all = json_dict.get("replace_all", False)
221
+ path_text = render_path(file_path, ThemeKey.TOOL_PARAM_FILE_PATH)
222
+ if replace_all:
223
+ old_string = json_dict.get("old_string", "")
224
+ new_string = json_dict.get("new_string", "")
225
+ replace_info = Text("Replacing all ", ThemeKey.TOOL_RESULT_TRUNCATED)
226
+ replace_info.append(old_string, ThemeKey.BASH_STRING)
227
+ replace_info.append(" → ", ThemeKey.BASH_OPERATOR)
228
+ replace_info.append(new_string, ThemeKey.BASH_STRING)
229
+ arguments_column: RenderableType = Group(path_text, replace_info)
230
+ else:
231
+ arguments_column = path_text
134
232
  except json.JSONDecodeError:
135
- render_result = (
136
- render_result.append_text(Text("Edit", ThemeKey.TOOL_NAME))
137
- .append_text(Text(" "))
138
- .append_text(
139
- Text(
140
- arguments.strip()[: const.INVALID_TOOL_CALL_MAX_LENGTH],
141
- style=ThemeKey.INVALID_TOOL_CALL_ARGS,
142
- )
143
- )
233
+ arguments_column = Text(
234
+ arguments.strip()[: const.INVALID_TOOL_CALL_MAX_LENGTH],
235
+ style=ThemeKey.INVALID_TOOL_CALL_ARGS,
144
236
  )
145
- return render_result
237
+ grid.add_row(tool_name_column, arguments_column)
238
+ return grid
146
239
 
147
240
 
148
- def render_write_tool_call(arguments: str) -> Text:
149
- render_result: Text = Text.assemble(("→ ", ThemeKey.TOOL_MARK))
241
+ def render_write_tool_call(arguments: str) -> RenderableType:
242
+ grid = create_grid()
150
243
  try:
151
244
  json_dict = json.loads(arguments)
152
- file_path = json_dict.get("file_path")
153
- op_label = "Create"
154
- if isinstance(file_path, str):
155
- abs_path = Path(file_path)
156
- if not abs_path.is_absolute():
157
- abs_path = (Path().cwd() / abs_path).resolve()
158
- if abs_path.exists():
159
- op_label = "Overwrite"
160
- render_result = (
161
- render_result.append_text(Text(op_label, ThemeKey.TOOL_NAME))
162
- .append_text(Text(" "))
163
- .append_text(render_path(file_path, ThemeKey.TOOL_PARAM_FILE_PATH))
164
- )
245
+ file_path = json_dict.get("file_path", "")
246
+ tool_name_column = Text.assemble((MARK_WRITE, ThemeKey.TOOL_MARK), " ", ("Write", ThemeKey.TOOL_NAME))
247
+ # Markdown files show path in result panel, skip here to avoid duplication
248
+ if file_path.endswith(".md"):
249
+ arguments_column = Text("")
250
+ else:
251
+ arguments_column = render_path(file_path, ThemeKey.TOOL_PARAM_FILE_PATH)
165
252
  except json.JSONDecodeError:
166
- render_result = (
167
- render_result.append_text(Text("Write", ThemeKey.TOOL_NAME))
168
- .append_text(Text(" "))
169
- .append_text(
170
- Text(
171
- arguments.strip()[: const.INVALID_TOOL_CALL_MAX_LENGTH],
172
- style=ThemeKey.INVALID_TOOL_CALL_ARGS,
173
- )
174
- )
253
+ tool_name_column = Text.assemble((MARK_WRITE, ThemeKey.TOOL_MARK), " ", ("Write", ThemeKey.TOOL_NAME))
254
+ arguments_column = Text(
255
+ arguments.strip()[: const.INVALID_TOOL_CALL_MAX_LENGTH],
256
+ style=ThemeKey.INVALID_TOOL_CALL_ARGS,
175
257
  )
176
- return render_result
258
+ grid.add_row(tool_name_column, arguments_column)
259
+ return grid
260
+
177
261
 
262
+ def render_move_tool_call(arguments: str) -> RenderableType:
263
+ grid = create_grid()
264
+ tool_name_column = Text.assemble((MARK_MOVE, ThemeKey.TOOL_MARK), " ", ("Move", ThemeKey.TOOL_NAME))
178
265
 
179
- def render_multi_edit_tool_call(arguments: str) -> Text:
180
- render_result: Text = Text.assemble(("→ ", ThemeKey.TOOL_MARK), ("MultiEdit", ThemeKey.TOOL_NAME), " ")
181
266
  try:
182
- json_dict = json.loads(arguments)
183
- file_path = json_dict.get("file_path")
184
- edits = json_dict.get("edits", [])
185
- render_result = (
186
- render_result.append_text(render_path(file_path, ThemeKey.TOOL_PARAM_FILE_PATH))
187
- .append_text(Text(" - "))
188
- .append_text(Text(f"{len(edits)}", ThemeKey.TOOL_PARAM_BOLD))
189
- .append_text(Text(" updates", ThemeKey.TOOL_PARAM_FILE_PATH))
190
- )
267
+ payload = json.loads(arguments)
191
268
  except json.JSONDecodeError:
192
- render_result = render_result.append_text(
193
- Text(
194
- arguments.strip()[: const.INVALID_TOOL_CALL_MAX_LENGTH],
195
- style=ThemeKey.INVALID_TOOL_CALL_ARGS,
196
- )
269
+ arguments_column = Text(
270
+ arguments.strip()[: const.INVALID_TOOL_CALL_MAX_LENGTH],
271
+ style=ThemeKey.INVALID_TOOL_CALL_ARGS,
197
272
  )
198
- return render_result
273
+ grid.add_row(tool_name_column, arguments_column)
274
+ return grid
275
+
276
+ source_path = payload.get("source_file_path", "")
277
+ target_path = payload.get("target_file_path", "")
278
+ start_line = payload.get("start_line", "")
279
+ end_line = payload.get("end_line", "")
280
+
281
+ # Build display: source:start-end -> target
282
+ parts = Text()
283
+ if source_path:
284
+ parts.append_text(render_path(source_path, ThemeKey.TOOL_PARAM_FILE_PATH))
285
+ if start_line and end_line:
286
+ parts.append(f":{start_line}-{end_line}", style=ThemeKey.TOOL_PARAM)
287
+ parts.append(" -> ", style=ThemeKey.TOOL_PARAM)
288
+ if target_path:
289
+ parts.append_text(render_path(target_path, ThemeKey.TOOL_PARAM_FILE_PATH))
290
+
291
+ grid.add_row(tool_name_column, parts)
292
+ return grid
199
293
 
200
294
 
201
295
  def render_apply_patch_tool_call(arguments: str) -> RenderableType:
296
+ grid = create_grid()
297
+ tool_name_column = Text.assemble((MARK_EDIT, ThemeKey.TOOL_MARK), " ", ("Apply Patch", ThemeKey.TOOL_NAME))
298
+
202
299
  try:
203
300
  payload = json.loads(arguments)
204
301
  except json.JSONDecodeError:
205
- return Text.assemble(
206
- ("→ ", ThemeKey.TOOL_MARK),
207
- ("Apply Patch", ThemeKey.TOOL_NAME),
208
- " ",
209
- Text(
210
- arguments.strip()[: const.INVALID_TOOL_CALL_MAX_LENGTH],
211
- style=ThemeKey.INVALID_TOOL_CALL_ARGS,
212
- ),
302
+ arguments_column = Text(
303
+ arguments.strip()[: const.INVALID_TOOL_CALL_MAX_LENGTH],
304
+ style=ThemeKey.INVALID_TOOL_CALL_ARGS,
213
305
  )
306
+ grid.add_row(tool_name_column, arguments_column)
307
+ return grid
214
308
 
215
309
  patch_content = payload.get("patch", "")
216
-
217
- grid = create_grid()
218
- header = Text.assemble(("→ ", ThemeKey.TOOL_MARK), ("Apply Patch", ThemeKey.TOOL_NAME))
219
- summary = Text("", ThemeKey.TOOL_PARAM)
310
+ arguments_column = Text("", ThemeKey.TOOL_PARAM)
220
311
 
221
312
  if isinstance(patch_content, str):
222
- lines = [line for line in patch_content.splitlines() if line and not line.startswith("*** Begin Patch")]
223
- if lines:
224
- summary = Text(lines[0][: const.INVALID_TOOL_CALL_MAX_LENGTH], ThemeKey.TOOL_PARAM)
313
+ update_count = 0
314
+ add_count = 0
315
+ delete_count = 0
316
+ for line in patch_content.splitlines():
317
+ if line.startswith("*** Update File:"):
318
+ update_count += 1
319
+ elif line.startswith("*** Add File:"):
320
+ add_count += 1
321
+ elif line.startswith("*** Delete File:"):
322
+ delete_count += 1
323
+
324
+ parts: list[str] = []
325
+ if update_count > 0:
326
+ parts.append(f"Update File × {update_count}" if update_count > 1 else "Update File")
327
+ if add_count > 0:
328
+ parts.append(f"Add File × {add_count}" if add_count > 1 else "Add File")
329
+ if delete_count > 0:
330
+ parts.append(f"Delete File × {delete_count}" if delete_count > 1 else "Delete File")
331
+
332
+ if parts:
333
+ arguments_column = Text(", ".join(parts), ThemeKey.TOOL_PARAM)
225
334
  else:
226
- summary = Text(
335
+ arguments_column = Text(
227
336
  str(patch_content)[: const.INVALID_TOOL_CALL_MAX_LENGTH],
228
337
  ThemeKey.INVALID_TOOL_CALL_ARGS,
229
338
  )
230
339
 
231
- if summary.plain:
232
- grid.add_row(header, summary)
233
- else:
234
- grid.add_row(header, Text("", ThemeKey.TOOL_PARAM))
235
-
340
+ grid.add_row(tool_name_column, arguments_column)
236
341
  return grid
237
342
 
238
343
 
239
344
  def render_todo(tr: events.ToolResultEvent) -> RenderableType:
240
- if tr.ui_extra is None:
241
- return Text.assemble(
242
- (" ✘", ThemeKey.ERROR_BOLD),
243
- " ",
244
- Text("(no content)", style=ThemeKey.ERROR),
245
- )
246
- if tr.ui_extra.type != model.ToolResultUIExtraType.TODO_LIST or tr.ui_extra.todo_list is None:
247
- return Text.assemble(
248
- (" ✘", ThemeKey.ERROR_BOLD),
249
- " ",
250
- Text("(invalid ui_extra)", style=ThemeKey.ERROR),
251
- )
252
-
345
+ assert isinstance(tr.ui_extra, model.TodoListUIExtra)
253
346
  ui_extra = tr.ui_extra.todo_list
254
347
  todo_grid = create_grid()
255
348
  for todo in ui_extra.todos:
256
349
  is_new_completed = todo.content in ui_extra.new_completed
257
350
  match todo.status:
258
351
  case "pending":
259
- mark = "▢"
352
+ mark = MARK_TODO_PENDING
260
353
  mark_style = ThemeKey.TODO_PENDING_MARK
261
354
  text_style = ThemeKey.TODO_PENDING
262
355
  case "in_progress":
263
- mark = "◉"
356
+ mark = MARK_TODO_IN_PROGRESS
264
357
  mark_style = ThemeKey.TODO_IN_PROGRESS_MARK
265
358
  text_style = ThemeKey.TODO_IN_PROGRESS
266
359
  case "completed":
267
- mark = "✔"
360
+ mark = MARK_TODO_COMPLETED
268
361
  mark_style = ThemeKey.TODO_NEW_COMPLETED_MARK if is_new_completed else ThemeKey.TODO_COMPLETED_MARK
269
362
  text_style = ThemeKey.TODO_NEW_COMPLETED if is_new_completed else ThemeKey.TODO_COMPLETED
270
363
  text = Text(todo.content)
@@ -277,128 +370,168 @@ def render_todo(tr: events.ToolResultEvent) -> RenderableType:
277
370
  def render_generic_tool_result(result: str, *, is_error: bool = False) -> RenderableType:
278
371
  """Render a generic tool result as indented, truncated text."""
279
372
  style = ThemeKey.ERROR if is_error else ThemeKey.TOOL_RESULT
280
- return Padding.indent(Text(truncate_display(result), style=style), level=2)
373
+ return Padding.indent(truncate_display(result, base_style=style), level=2)
281
374
 
282
375
 
283
376
  def _extract_mermaid_link(
284
377
  ui_extra: model.ToolResultUIExtra | None,
285
378
  ) -> model.MermaidLinkUIExtra | None:
286
- if ui_extra is None:
287
- return None
288
- if ui_extra.type != model.ToolResultUIExtraType.MERMAID_LINK:
289
- return None
290
- return ui_extra.mermaid_link
379
+ if isinstance(ui_extra, model.MermaidLinkUIExtra):
380
+ return ui_extra
381
+ return None
291
382
 
292
383
 
293
- def render_memory_tool_call(arguments: str) -> RenderableType:
384
+ def render_mermaid_tool_call(arguments: str) -> RenderableType:
294
385
  grid = create_grid()
295
- command_display_names: dict[str, str] = {
296
- "view": "View",
297
- "create": "Create",
298
- "str_replace": "Replace",
299
- "insert": "Insert",
300
- "delete": "Delete",
301
- "rename": "Rename",
302
- }
386
+ tool_name_column = Text.assemble((MARK_MERMAID, ThemeKey.TOOL_MARK), " ", ("Mermaid", ThemeKey.TOOL_NAME))
387
+ summary = Text("", ThemeKey.TOOL_PARAM)
303
388
 
304
389
  try:
305
390
  payload: dict[str, str] = json.loads(arguments)
306
391
  except json.JSONDecodeError:
307
- tool_name_column = Text.assemble(("★", ThemeKey.TOOL_MARK), " ", ("Memory", ThemeKey.TOOL_NAME))
308
392
  summary = Text(
309
393
  arguments.strip()[: const.INVALID_TOOL_CALL_MAX_LENGTH],
310
394
  style=ThemeKey.INVALID_TOOL_CALL_ARGS,
311
395
  )
312
- grid.add_row(tool_name_column, summary)
313
- return grid
396
+ else:
397
+ code = payload.get("code", "")
398
+ if code:
399
+ line_count = len(code.splitlines())
400
+ summary = Text(f"{line_count} lines", ThemeKey.TOOL_PARAM)
401
+ else:
402
+ summary = Text("0 lines", ThemeKey.TOOL_PARAM)
314
403
 
315
- command = payload.get("command", "")
316
- display_name = command_display_names.get(command, command.title())
317
- tool_name_column = Text.assemble(("★", ThemeKey.TOOL_MARK), " ", (f"{display_name} Memory", ThemeKey.TOOL_NAME))
404
+ grid.add_row(tool_name_column, summary)
405
+ return grid
318
406
 
319
- summary = Text("", ThemeKey.TOOL_PARAM)
320
- path = payload.get("path")
321
- old_path = payload.get("old_path")
322
- new_path = payload.get("new_path")
323
-
324
- if command == "rename" and old_path and new_path:
325
- summary = Text.assemble(
326
- Text(old_path, ThemeKey.TOOL_PARAM_FILE_PATH),
327
- Text(" -> ", ThemeKey.TOOL_PARAM),
328
- Text(new_path, ThemeKey.TOOL_PARAM_FILE_PATH),
407
+
408
+ def _truncate_url(url: str, max_length: int = 400) -> str:
409
+ """Truncate URL for display, preserving domain and path structure."""
410
+ if len(url) <= max_length:
411
+ return url
412
+ # Remove protocol for display
413
+ display_url = url
414
+ for prefix in ("https://", "http://"):
415
+ if display_url.startswith(prefix):
416
+ display_url = display_url[len(prefix) :]
417
+ break
418
+ if len(display_url) <= max_length:
419
+ return display_url
420
+ # Truncate with ellipsis
421
+ return display_url[: max_length - 3] + "..."
422
+
423
+
424
+ def _render_mermaid_viewer_link(
425
+ tr: events.ToolResultEvent,
426
+ link_info: model.MermaidLinkUIExtra,
427
+ *,
428
+ use_osc8: bool,
429
+ ) -> RenderableType:
430
+ viewer_path = r_mermaid_viewer.build_viewer(code=link_info.code, link=link_info.link, tool_call_id=tr.tool_call_id)
431
+ if viewer_path is None:
432
+ return Text(link_info.link, style=ThemeKey.TOOL_RESULT_MERMAID, overflow="fold")
433
+
434
+ display_path = str(viewer_path)
435
+
436
+ file_url = ""
437
+ if use_osc8:
438
+ try:
439
+ file_url = viewer_path.resolve().as_uri()
440
+ except ValueError:
441
+ file_url = f"file://{viewer_path.as_posix()}"
442
+
443
+ rendered = Text.assemble(("saved in:", ThemeKey.TOOL_RESULT), " ")
444
+ start = len(rendered)
445
+ rendered.append(display_path, ThemeKey.TOOL_RESULT_MERMAID)
446
+ end = len(rendered)
447
+
448
+ if use_osc8 and file_url:
449
+ rendered.stylize(Style(link=file_url), start, end)
450
+
451
+ return rendered
452
+
453
+
454
+ def render_web_fetch_tool_call(arguments: str) -> RenderableType:
455
+ grid = create_grid()
456
+ tool_name_column = Text.assemble((MARK_WEB_FETCH, ThemeKey.TOOL_MARK), " ", ("Fetch", ThemeKey.TOOL_NAME))
457
+
458
+ try:
459
+ payload: dict[str, str] = json.loads(arguments)
460
+ except json.JSONDecodeError:
461
+ summary = Text(
462
+ arguments.strip()[: const.INVALID_TOOL_CALL_MAX_LENGTH],
463
+ style=ThemeKey.INVALID_TOOL_CALL_ARGS,
329
464
  )
330
- elif command == "insert" and path:
331
- insert_line = payload.get("insert_line")
332
- summary = Text(path, ThemeKey.TOOL_PARAM_FILE_PATH)
333
- if insert_line is not None:
334
- summary.append(f" line {insert_line}", ThemeKey.TOOL_PARAM)
335
- elif command == "view" and path:
336
- view_range = payload.get("view_range")
337
- summary = Text(path, ThemeKey.TOOL_PARAM_FILE_PATH)
338
- if view_range and isinstance(view_range, list) and len(view_range) >= 2:
339
- summary.append(f" {view_range[0]}:{view_range[1]}", ThemeKey.TOOL_PARAM)
340
- elif path:
341
- summary = Text(path, ThemeKey.TOOL_PARAM_FILE_PATH)
465
+ grid.add_row(tool_name_column, summary)
466
+ return grid
467
+
468
+ url = payload.get("url", "")
469
+ summary = Text(_truncate_url(url), ThemeKey.TOOL_PARAM_FILE_PATH) if url else Text("(no url)", ThemeKey.TOOL_PARAM)
342
470
 
343
471
  grid.add_row(tool_name_column, summary)
344
472
  return grid
345
473
 
346
474
 
347
- def render_mermaid_tool_call(arguments: str) -> RenderableType:
475
+ def render_web_search_tool_call(arguments: str) -> RenderableType:
348
476
  grid = create_grid()
349
- tool_name_column = Text.assemble(("⧉", ThemeKey.TOOL_MARK), " ", ("Mermaid", ThemeKey.TOOL_NAME))
350
- summary = Text("", ThemeKey.TOOL_PARAM)
477
+ tool_name_column = Text.assemble((MARK_WEB_SEARCH, ThemeKey.TOOL_MARK), " ", ("Web Search", ThemeKey.TOOL_NAME))
351
478
 
352
479
  try:
353
- payload: dict[str, str] = json.loads(arguments)
480
+ payload: dict[str, Any] = json.loads(arguments)
354
481
  except json.JSONDecodeError:
355
482
  summary = Text(
356
483
  arguments.strip()[: const.INVALID_TOOL_CALL_MAX_LENGTH],
357
484
  style=ThemeKey.INVALID_TOOL_CALL_ARGS,
358
485
  )
486
+ grid.add_row(tool_name_column, summary)
487
+ return grid
488
+
489
+ query = payload.get("query", "")
490
+ max_results = payload.get("max_results")
491
+
492
+ summary = Text("", ThemeKey.TOOL_PARAM)
493
+ if query:
494
+ # Truncate long queries
495
+ display_query = query if len(query) <= 80 else query[:77] + "..."
496
+ summary.append(display_query, ThemeKey.TOOL_PARAM)
359
497
  else:
360
- code = payload.get("code", "")
361
- if code:
362
- line_count = len(code.splitlines())
363
- summary = Text(f"{line_count} lines", ThemeKey.TOOL_PARAM)
364
- else:
365
- summary = Text("0 lines", ThemeKey.TOOL_PARAM)
498
+ summary.append("(no query)", ThemeKey.TOOL_PARAM)
499
+
500
+ if isinstance(max_results, int) and max_results != 10:
501
+ summary.append(f" (max {max_results})", ThemeKey.TOOL_TIMEOUT)
366
502
 
367
503
  grid.add_row(tool_name_column, summary)
368
504
  return grid
369
505
 
370
506
 
371
507
  def render_mermaid_tool_result(tr: events.ToolResultEvent) -> RenderableType:
508
+ from klaude_code.ui.terminal import supports_osc8_hyperlinks
509
+
372
510
  link_info = _extract_mermaid_link(tr.ui_extra)
373
511
  if link_info is None:
374
512
  return render_generic_tool_result(tr.result, is_error=tr.status == "error")
375
513
 
376
- link_text = Text.from_markup(f"[blue u][link={link_info.link}]Command+click to view[/link][/blue u]")
377
- return Padding.indent(link_text, level=2)
514
+ use_osc8 = supports_osc8_hyperlinks()
515
+ viewer = _render_mermaid_viewer_link(tr, link_info, use_osc8=use_osc8)
516
+ return Padding.indent(viewer, level=2)
378
517
 
379
518
 
380
519
  def _extract_truncation(
381
520
  ui_extra: model.ToolResultUIExtra | None,
382
521
  ) -> model.TruncationUIExtra | None:
383
- if ui_extra is None:
384
- return None
385
- if ui_extra.type != model.ToolResultUIExtraType.TRUNCATION:
386
- return None
387
- return ui_extra.truncation
522
+ if isinstance(ui_extra, model.TruncationUIExtra):
523
+ return ui_extra
524
+ return None
388
525
 
389
526
 
390
527
  def render_truncation_info(ui_extra: model.TruncationUIExtra) -> RenderableType:
391
528
  """Render truncation info for the user."""
392
- original_kb = ui_extra.original_length / 1024
393
529
  truncated_kb = ui_extra.truncated_length / 1024
530
+
394
531
  text = Text.assemble(
395
- ("Output truncated: ", ThemeKey.TOOL_RESULT),
396
- (f"{original_kb:.1f}KB", ThemeKey.TOOL_RESULT),
397
- (" total, ", ThemeKey.TOOL_RESULT),
398
- (f"{truncated_kb:.1f}KB", ThemeKey.TOOL_RESULT_BOLD),
399
- (" hidden\nFull output saved to ", ThemeKey.TOOL_RESULT),
400
- (ui_extra.saved_file_path, ThemeKey.TOOL_RESULT),
401
- ("\nUse Read with limit+offset or rg/grep to inspect", ThemeKey.TOOL_RESULT),
532
+ ("Offload context to ", ThemeKey.TOOL_RESULT_TRUNCATED),
533
+ (ui_extra.saved_file_path, ThemeKey.TOOL_RESULT_TRUNCATED),
534
+ (f", {truncated_kb:.1f}KB truncated", ThemeKey.TOOL_RESULT_TRUNCATED),
402
535
  )
403
536
  return Padding.indent(text, level=2)
404
537
 
@@ -408,35 +541,28 @@ def get_truncation_info(tr: events.ToolResultEvent) -> model.TruncationUIExtra |
408
541
  return _extract_truncation(tr.ui_extra)
409
542
 
410
543
 
411
- # Tool name to mark mapping
412
- _TOOL_MARKS: dict[str, str] = {
413
- "Read": "",
414
- "Edit": "",
415
- "Write": "→",
416
- "MultiEdit": "→",
417
- "Bash": ">",
418
- "apply_patch": "→",
419
- "TodoWrite": "◎",
420
- "update_plan": "◎",
421
- "Mermaid": "⧉",
422
- "Memory": "★",
423
- "Skill": "◈",
424
- }
544
+ def render_report_back_tool_call() -> RenderableType:
545
+ grid = create_grid()
546
+ tool_name_column = Text.assemble((MARK_DONE, ThemeKey.TOOL_MARK), " ", ("Report Back", ThemeKey.TOOL_NAME))
547
+ grid.add_row(tool_name_column, "")
548
+ return grid
549
+
425
550
 
426
551
  # Tool name to active form mapping (for spinner status)
427
552
  _TOOL_ACTIVE_FORM: dict[str, str] = {
428
- "Bash": "Bashing",
429
- "apply_patch": "Patching",
430
- "Edit": "Editing",
431
- "MultiEdit": "Editing",
432
- "Read": "Reading",
433
- "Write": "Writing",
434
- "TodoWrite": "Planning",
435
- "update_plan": "Planning",
436
- "Skill": "Skilling",
437
- "Mermaid": "Diagramming",
438
- "Memory": "Memorizing",
439
- "WebFetch": "Fetching",
553
+ tools.BASH: "Bashing",
554
+ tools.APPLY_PATCH: "Patching",
555
+ tools.MOVE: "Moving",
556
+ tools.EDIT: "Editing",
557
+ tools.READ: "Reading",
558
+ tools.WRITE: "Writing",
559
+ tools.TODO_WRITE: "Planning",
560
+ tools.UPDATE_PLAN: "Planning",
561
+ tools.SKILL: "Skilling",
562
+ tools.MERMAID: "Diagramming",
563
+ tools.WEB_FETCH: "Fetching Web",
564
+ tools.WEB_SEARCH: "Searching Web",
565
+ tools.REPORT_BACK: "Reporting",
440
566
  }
441
567
 
442
568
 
@@ -463,7 +589,6 @@ def render_tool_call(e: events.ToolCallEvent) -> RenderableType | None:
463
589
 
464
590
  Returns a Rich Renderable or None if the tool call should not be rendered.
465
591
  """
466
- from klaude_code.protocol import tools
467
592
 
468
593
  if is_sub_agent_tool(e.tool_name):
469
594
  return None
@@ -475,40 +600,66 @@ def render_tool_call(e: events.ToolCallEvent) -> RenderableType | None:
475
600
  return render_edit_tool_call(e.arguments)
476
601
  case tools.WRITE:
477
602
  return render_write_tool_call(e.arguments)
478
- case tools.MULTI_EDIT:
479
- return render_multi_edit_tool_call(e.arguments)
603
+ case tools.MOVE:
604
+ return render_move_tool_call(e.arguments)
480
605
  case tools.BASH:
481
- return render_generic_tool_call(e.tool_name, e.arguments, ">")
606
+ return render_bash_tool_call(e.arguments)
482
607
  case tools.APPLY_PATCH:
483
608
  return render_apply_patch_tool_call(e.arguments)
484
609
  case tools.TODO_WRITE:
485
- return render_generic_tool_call("Update Todos", "", "◎")
610
+ return render_generic_tool_call("Update Todos", "", MARK_PLAN)
486
611
  case tools.UPDATE_PLAN:
487
612
  return render_update_plan_tool_call(e.arguments)
488
613
  case tools.MERMAID:
489
614
  return render_mermaid_tool_call(e.arguments)
490
- case tools.MEMORY:
491
- return render_memory_tool_call(e.arguments)
492
615
  case tools.SKILL:
493
- return render_generic_tool_call(e.tool_name, e.arguments, "◈")
616
+ return render_generic_tool_call(e.tool_name, e.arguments, MARK_SKILL)
617
+ case tools.REPORT_BACK:
618
+ return render_report_back_tool_call()
619
+ case tools.WEB_FETCH:
620
+ return render_web_fetch_tool_call(e.arguments)
621
+ case tools.WEB_SEARCH:
622
+ return render_web_search_tool_call(e.arguments)
494
623
  case _:
495
624
  return render_generic_tool_call(e.tool_name, e.arguments)
496
625
 
497
626
 
498
- def _extract_diff_text(ui_extra: model.ToolResultUIExtra | None) -> str | None:
499
- if ui_extra is None:
500
- return None
501
- if ui_extra.type == model.ToolResultUIExtraType.DIFF_TEXT:
502
- return ui_extra.diff_text
627
+ def _extract_diff(ui_extra: model.ToolResultUIExtra | None) -> model.DiffUIExtra | None:
628
+ if isinstance(ui_extra, model.DiffUIExtra):
629
+ return ui_extra
630
+ if isinstance(ui_extra, model.MultiUIExtra):
631
+ for item in ui_extra.items:
632
+ if isinstance(item, model.DiffUIExtra):
633
+ return item
503
634
  return None
504
635
 
505
636
 
506
- def render_tool_result(e: events.ToolResultEvent) -> RenderableType | None:
637
+ def _extract_markdown_doc(ui_extra: model.ToolResultUIExtra | None) -> model.MarkdownDocUIExtra | None:
638
+ if isinstance(ui_extra, model.MarkdownDocUIExtra):
639
+ return ui_extra
640
+ if isinstance(ui_extra, model.MultiUIExtra):
641
+ for item in ui_extra.items:
642
+ if isinstance(item, model.MarkdownDocUIExtra):
643
+ return item
644
+ return None
645
+
646
+
647
+ def render_markdown_doc(md_ui: model.MarkdownDocUIExtra, *, code_theme: str) -> RenderableType:
648
+ """Render markdown document content in a panel."""
649
+ header = render_path(md_ui.file_path, ThemeKey.TOOL_PARAM_FILE_PATH)
650
+ return Panel.fit(
651
+ Group(header, Text(""), NoInsetMarkdown(md_ui.content, code_theme=code_theme)),
652
+ box=box.SIMPLE,
653
+ border_style=ThemeKey.LINES,
654
+ style=ThemeKey.WRITE_MARKDOWN_PANEL,
655
+ )
656
+
657
+
658
+ def render_tool_result(e: events.ToolResultEvent, *, code_theme: str = "monokai") -> RenderableType | None:
507
659
  """Unified entry point for rendering tool results.
508
660
 
509
661
  Returns a Rich Renderable or None if the tool result should not be rendered.
510
662
  """
511
- from klaude_code.protocol import tools
512
663
  from klaude_code.ui.renderers import errors as r_errors
513
664
 
514
665
  if is_sub_agent_tool(e.tool_name):
@@ -516,36 +667,63 @@ def render_tool_result(e: events.ToolResultEvent) -> RenderableType | None:
516
667
 
517
668
  # Handle error case
518
669
  if e.status == "error" and e.ui_extra is None:
519
- error_msg = Text(truncate_display(e.result))
520
- return r_errors.render_error(error_msg)
670
+ error_msg = truncate_display(e.result)
671
+ return r_errors.render_tool_error(error_msg)
672
+
673
+ # Render multiple ui blocks if present
674
+ if isinstance(e.ui_extra, model.MultiUIExtra) and e.ui_extra.items:
675
+ rendered: list[RenderableType] = []
676
+ for item in e.ui_extra.items:
677
+ if isinstance(item, model.MarkdownDocUIExtra):
678
+ rendered.append(Padding.indent(render_markdown_doc(item, code_theme=code_theme), level=2))
679
+ elif isinstance(item, model.DiffUIExtra):
680
+ show_file_name = e.tool_name in (tools.APPLY_PATCH, tools.MOVE)
681
+ rendered.append(
682
+ Padding.indent(r_diffs.render_structured_diff(item, show_file_name=show_file_name), level=2)
683
+ )
684
+ return Group(*rendered) if rendered else None
521
685
 
522
686
  # Show truncation info if output was truncated and saved to file
523
687
  truncation_info = get_truncation_info(e)
524
688
  if truncation_info:
525
- return render_truncation_info(truncation_info)
689
+ return Group(render_truncation_info(truncation_info), render_generic_tool_result(e.result))
526
690
 
527
- diff_text = _extract_diff_text(e.ui_extra)
691
+ diff_ui = _extract_diff(e.ui_extra)
692
+ md_ui = _extract_markdown_doc(e.ui_extra)
528
693
 
529
694
  match e.tool_name:
530
695
  case tools.READ:
531
696
  return None
532
- case tools.EDIT | tools.MULTI_EDIT | tools.WRITE:
533
- return Padding.indent(r_diffs.render_diff(diff_text or ""), level=2)
534
- case tools.MEMORY:
535
- if diff_text:
536
- return Padding.indent(r_diffs.render_diff(diff_text), level=2)
537
- elif len(e.result.strip()) > 0:
538
- return render_generic_tool_result(e.result)
697
+ case tools.EDIT:
698
+ return Padding.indent(r_diffs.render_structured_diff(diff_ui) if diff_ui else Text(""), level=2)
699
+ case tools.WRITE:
700
+ if md_ui:
701
+ return Padding.indent(render_markdown_doc(md_ui, code_theme=code_theme), level=2)
702
+ return Padding.indent(r_diffs.render_structured_diff(diff_ui) if diff_ui else Text(""), level=2)
703
+ case tools.MOVE:
704
+ # Same-file move returns single DiffUIExtra, cross-file returns MultiUIExtra (handled above)
705
+ if diff_ui:
706
+ return Padding.indent(r_diffs.render_structured_diff(diff_ui, show_file_name=True), level=2)
539
707
  return None
708
+ case tools.APPLY_PATCH:
709
+ if md_ui:
710
+ return Padding.indent(render_markdown_doc(md_ui, code_theme=code_theme), level=2)
711
+ if diff_ui:
712
+ return Padding.indent(r_diffs.render_structured_diff(diff_ui, show_file_name=True), level=2)
713
+ if len(e.result.strip()) == 0:
714
+ return render_generic_tool_result("(no content)")
715
+ return render_generic_tool_result(e.result)
540
716
  case tools.TODO_WRITE | tools.UPDATE_PLAN:
541
717
  return render_todo(e)
542
718
  case tools.MERMAID:
543
719
  return render_mermaid_tool_result(e)
544
- case _:
545
- if e.tool_name in (tools.BASH, tools.APPLY_PATCH) and e.result.startswith("diff --git"):
720
+ case tools.BASH:
721
+ if e.result.startswith("diff --git"):
546
722
  return r_diffs.render_diff_panel(e.result, show_file_name=True)
547
- if e.tool_name == tools.APPLY_PATCH and diff_text:
548
- return Padding.indent(r_diffs.render_diff(diff_text, show_file_name=True), level=2)
723
+ if len(e.result.strip()) == 0:
724
+ return render_generic_tool_result("(no content)")
725
+ return render_generic_tool_result(e.result)
726
+ case _:
549
727
  if len(e.result.strip()) == 0:
550
728
  return render_generic_tool_result("(no content)")
551
729
  return render_generic_tool_result(e.result)