klaude-code 1.9.0__py3-none-any.whl → 2.0.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 (129) hide show
  1. klaude_code/auth/base.py +2 -6
  2. klaude_code/cli/auth_cmd.py +4 -4
  3. klaude_code/cli/list_model.py +1 -1
  4. klaude_code/cli/main.py +1 -1
  5. klaude_code/cli/runtime.py +7 -5
  6. klaude_code/cli/self_update.py +1 -1
  7. klaude_code/cli/session_cmd.py +1 -1
  8. klaude_code/command/clear_cmd.py +6 -2
  9. klaude_code/command/command_abc.py +2 -2
  10. klaude_code/command/debug_cmd.py +4 -4
  11. klaude_code/command/export_cmd.py +2 -2
  12. klaude_code/command/export_online_cmd.py +12 -12
  13. klaude_code/command/fork_session_cmd.py +29 -23
  14. klaude_code/command/help_cmd.py +4 -4
  15. klaude_code/command/model_cmd.py +4 -4
  16. klaude_code/command/model_select.py +1 -1
  17. klaude_code/command/prompt-commit.md +11 -2
  18. klaude_code/command/prompt_command.py +3 -3
  19. klaude_code/command/refresh_cmd.py +2 -2
  20. klaude_code/command/registry.py +7 -5
  21. klaude_code/command/release_notes_cmd.py +4 -4
  22. klaude_code/command/resume_cmd.py +15 -11
  23. klaude_code/command/status_cmd.py +4 -4
  24. klaude_code/command/terminal_setup_cmd.py +8 -8
  25. klaude_code/command/thinking_cmd.py +4 -4
  26. klaude_code/config/assets/builtin_config.yaml +16 -0
  27. klaude_code/config/builtin_config.py +16 -5
  28. klaude_code/config/config.py +7 -2
  29. klaude_code/const.py +146 -91
  30. klaude_code/core/agent.py +3 -12
  31. klaude_code/core/executor.py +21 -13
  32. klaude_code/core/manager/sub_agent_manager.py +71 -7
  33. klaude_code/core/prompts/prompt-sub-agent-image-gen.md +1 -0
  34. klaude_code/core/prompts/prompt-sub-agent-web.md +27 -1
  35. klaude_code/core/reminders.py +88 -69
  36. klaude_code/core/task.py +44 -45
  37. klaude_code/core/tool/file/apply_patch_tool.py +9 -9
  38. klaude_code/core/tool/file/diff_builder.py +3 -5
  39. klaude_code/core/tool/file/edit_tool.py +23 -23
  40. klaude_code/core/tool/file/move_tool.py +43 -43
  41. klaude_code/core/tool/file/read_tool.py +44 -39
  42. klaude_code/core/tool/file/write_tool.py +14 -14
  43. klaude_code/core/tool/report_back_tool.py +4 -4
  44. klaude_code/core/tool/shell/bash_tool.py +23 -23
  45. klaude_code/core/tool/skill/skill_tool.py +7 -7
  46. klaude_code/core/tool/sub_agent_tool.py +38 -9
  47. klaude_code/core/tool/todo/todo_write_tool.py +8 -8
  48. klaude_code/core/tool/todo/update_plan_tool.py +6 -6
  49. klaude_code/core/tool/tool_abc.py +2 -2
  50. klaude_code/core/tool/tool_context.py +27 -0
  51. klaude_code/core/tool/tool_runner.py +88 -42
  52. klaude_code/core/tool/truncation.py +38 -20
  53. klaude_code/core/tool/web/mermaid_tool.py +6 -7
  54. klaude_code/core/tool/web/web_fetch_tool.py +68 -30
  55. klaude_code/core/tool/web/web_search_tool.py +15 -17
  56. klaude_code/core/turn.py +120 -73
  57. klaude_code/llm/anthropic/client.py +79 -44
  58. klaude_code/llm/anthropic/input.py +116 -108
  59. klaude_code/llm/bedrock/client.py +8 -5
  60. klaude_code/llm/claude/client.py +18 -8
  61. klaude_code/llm/client.py +4 -3
  62. klaude_code/llm/codex/client.py +15 -9
  63. klaude_code/llm/google/client.py +122 -60
  64. klaude_code/llm/google/input.py +94 -108
  65. klaude_code/llm/image.py +123 -0
  66. klaude_code/llm/input_common.py +136 -189
  67. klaude_code/llm/openai_compatible/client.py +17 -7
  68. klaude_code/llm/openai_compatible/input.py +36 -66
  69. klaude_code/llm/openai_compatible/stream.py +119 -67
  70. klaude_code/llm/openai_compatible/tool_call_accumulator.py +23 -11
  71. klaude_code/llm/openrouter/client.py +34 -9
  72. klaude_code/llm/openrouter/input.py +63 -64
  73. klaude_code/llm/openrouter/reasoning.py +22 -24
  74. klaude_code/llm/registry.py +20 -17
  75. klaude_code/llm/responses/client.py +107 -45
  76. klaude_code/llm/responses/input.py +115 -98
  77. klaude_code/llm/usage.py +52 -25
  78. klaude_code/protocol/__init__.py +1 -0
  79. klaude_code/protocol/events.py +16 -12
  80. klaude_code/protocol/llm_param.py +20 -2
  81. klaude_code/protocol/message.py +250 -0
  82. klaude_code/protocol/model.py +94 -281
  83. klaude_code/protocol/op.py +2 -2
  84. klaude_code/protocol/sub_agent/__init__.py +1 -0
  85. klaude_code/protocol/sub_agent/explore.py +10 -0
  86. klaude_code/protocol/sub_agent/image_gen.py +119 -0
  87. klaude_code/protocol/sub_agent/task.py +10 -0
  88. klaude_code/protocol/sub_agent/web.py +10 -0
  89. klaude_code/session/codec.py +6 -6
  90. klaude_code/session/export.py +261 -62
  91. klaude_code/session/selector.py +7 -24
  92. klaude_code/session/session.py +126 -54
  93. klaude_code/session/store.py +5 -32
  94. klaude_code/session/templates/export_session.html +1 -1
  95. klaude_code/session/templates/mermaid_viewer.html +1 -1
  96. klaude_code/trace/log.py +11 -6
  97. klaude_code/ui/core/input.py +1 -1
  98. klaude_code/ui/core/stage_manager.py +1 -8
  99. klaude_code/ui/modes/debug/display.py +2 -2
  100. klaude_code/ui/modes/repl/clipboard.py +2 -2
  101. klaude_code/ui/modes/repl/completers.py +18 -10
  102. klaude_code/ui/modes/repl/event_handler.py +136 -127
  103. klaude_code/ui/modes/repl/input_prompt_toolkit.py +1 -1
  104. klaude_code/ui/modes/repl/key_bindings.py +1 -1
  105. klaude_code/ui/modes/repl/renderer.py +107 -15
  106. klaude_code/ui/renderers/assistant.py +2 -2
  107. klaude_code/ui/renderers/common.py +65 -7
  108. klaude_code/ui/renderers/developer.py +7 -6
  109. klaude_code/ui/renderers/diffs.py +11 -11
  110. klaude_code/ui/renderers/mermaid_viewer.py +49 -2
  111. klaude_code/ui/renderers/metadata.py +33 -5
  112. klaude_code/ui/renderers/sub_agent.py +57 -16
  113. klaude_code/ui/renderers/thinking.py +37 -2
  114. klaude_code/ui/renderers/tools.py +180 -165
  115. klaude_code/ui/rich/live.py +3 -1
  116. klaude_code/ui/rich/markdown.py +39 -7
  117. klaude_code/ui/rich/quote.py +76 -1
  118. klaude_code/ui/rich/status.py +14 -8
  119. klaude_code/ui/rich/theme.py +8 -2
  120. klaude_code/ui/terminal/image.py +34 -0
  121. klaude_code/ui/terminal/notifier.py +2 -1
  122. klaude_code/ui/terminal/progress_bar.py +4 -4
  123. klaude_code/ui/terminal/selector.py +22 -4
  124. klaude_code/ui/utils/common.py +11 -2
  125. {klaude_code-1.9.0.dist-info → klaude_code-2.0.0.dist-info}/METADATA +4 -2
  126. klaude_code-2.0.0.dist-info/RECORD +229 -0
  127. klaude_code-1.9.0.dist-info/RECORD +0 -224
  128. {klaude_code-1.9.0.dist-info → klaude_code-2.0.0.dist-info}/WHEEL +0 -0
  129. {klaude_code-1.9.0.dist-info → klaude_code-2.0.0.dist-info}/entry_points.txt +0 -0
@@ -4,20 +4,26 @@ from typing import Any, cast
4
4
 
5
5
  from rich import box
6
6
  from rich.console import Group, RenderableType
7
- from rich.padding import Padding
8
7
  from rich.panel import Panel
9
8
  from rich.style import Style
10
9
  from rich.text import Text
11
10
 
12
- from klaude_code import const
11
+ from klaude_code.const import (
12
+ BASH_OUTPUT_PANEL_THRESHOLD,
13
+ INVALID_TOOL_CALL_MAX_LENGTH,
14
+ QUERY_DISPLAY_TRUNCATE_LENGTH,
15
+ URL_TRUNCATE_MAX_LENGTH,
16
+ WEB_SEARCH_DEFAULT_MAX_RESULTS,
17
+ )
13
18
  from klaude_code.protocol import events, model, tools
14
19
  from klaude_code.protocol.sub_agent import is_sub_agent_tool as _is_sub_agent_tool
15
20
  from klaude_code.ui.renderers import diffs as r_diffs
16
21
  from klaude_code.ui.renderers import mermaid_viewer as r_mermaid_viewer
17
22
  from klaude_code.ui.renderers.bash_syntax import highlight_bash_command
18
- from klaude_code.ui.renderers.common import create_grid, truncate_display
23
+ from klaude_code.ui.renderers.common import create_grid, truncate_middle
19
24
  from klaude_code.ui.rich.code_panel import CodePanel
20
25
  from klaude_code.ui.rich.markdown import NoInsetMarkdown
26
+ from klaude_code.ui.rich.quote import TreeQuote
21
27
  from klaude_code.ui.rich.theme import ThemeKey
22
28
 
23
29
  # Tool markers (Unicode symbols for UI display)
@@ -56,55 +62,76 @@ def render_path(path: str, style: str, is_directory: bool = False) -> Text:
56
62
  return Text(path, style=style)
57
63
 
58
64
 
59
- def render_generic_tool_call(tool_name: str, arguments: str, markup: str = MARK_GENERIC) -> RenderableType:
65
+ def _render_tool_call_tree(
66
+ *,
67
+ mark: str,
68
+ tool_name: str,
69
+ details: RenderableType | None,
70
+ ) -> RenderableType:
71
+ # Keep the original 2-column layout (tool name on the left, details on the right),
72
+ # but move the tool mark into the tree prefix so it can connect to the tool result.
60
73
  grid = create_grid()
74
+ grid.add_row(
75
+ Text(tool_name, style=ThemeKey.TOOL_NAME),
76
+ details if details is not None else Text(""),
77
+ )
78
+
79
+ return TreeQuote.for_tool_call(
80
+ grid,
81
+ mark=mark,
82
+ style=ThemeKey.TOOL_RESULT_TREE_PREFIX,
83
+ style_first=ThemeKey.TOOL_MARK,
84
+ )
61
85
 
62
- tool_name_column = Text.assemble((markup, ThemeKey.TOOL_MARK), " ", (tool_name, ThemeKey.TOOL_NAME))
63
- arguments_column = Text("")
86
+
87
+ def render_generic_tool_call(tool_name: str, arguments: str, markup: str = MARK_GENERIC) -> RenderableType:
64
88
  if not arguments:
65
- grid.add_row(tool_name_column, arguments_column)
66
- return grid
89
+ return _render_tool_call_tree(mark=markup, tool_name=tool_name, details=None)
90
+
91
+ details: RenderableType
67
92
  try:
68
- json_dict = json.loads(arguments)
69
- if len(json_dict) == 0:
70
- arguments_column = Text("", ThemeKey.TOOL_PARAM)
71
- elif len(json_dict) == 1:
72
- arguments_column = Text(str(next(iter(json_dict.values()))), ThemeKey.TOOL_PARAM)
73
- else:
74
- arguments_column = Text(
75
- ", ".join([f"{k}: {v}" for k, v in json_dict.items()]),
76
- ThemeKey.TOOL_PARAM,
77
- )
93
+ payload = json.loads(arguments)
78
94
  except json.JSONDecodeError:
79
- arguments_column = Text(
80
- arguments.strip()[: const.INVALID_TOOL_CALL_MAX_LENGTH],
95
+ details = Text(
96
+ arguments.strip()[:INVALID_TOOL_CALL_MAX_LENGTH],
81
97
  style=ThemeKey.INVALID_TOOL_CALL_ARGS,
82
98
  )
83
- grid.add_row(tool_name_column, arguments_column)
84
- return grid
99
+ else:
100
+ if isinstance(payload, dict):
101
+ payload_dict = cast(dict[str, Any], payload)
102
+ if len(payload_dict) == 0:
103
+ details = Text("", ThemeKey.TOOL_PARAM)
104
+ elif len(payload_dict) == 1:
105
+ details = Text(str(next(iter(payload_dict.values()))), ThemeKey.TOOL_PARAM)
106
+ else:
107
+ details = Text(
108
+ ", ".join([f"{k}: {v}" for k, v in payload_dict.items()]),
109
+ ThemeKey.TOOL_PARAM,
110
+ )
111
+ else:
112
+ details = Text(str(payload)[:INVALID_TOOL_CALL_MAX_LENGTH], style=ThemeKey.INVALID_TOOL_CALL_ARGS)
113
+
114
+ return _render_tool_call_tree(mark=markup, tool_name=tool_name, details=details)
85
115
 
86
116
 
87
117
  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))
118
+ tool_name = "Bash"
90
119
 
91
120
  try:
92
121
  payload_raw: Any = json.loads(arguments) if arguments else {}
93
122
  except json.JSONDecodeError:
94
- summary = Text(
95
- arguments.strip()[: const.INVALID_TOOL_CALL_MAX_LENGTH],
123
+ details: RenderableType = Text(
124
+ arguments.strip()[:INVALID_TOOL_CALL_MAX_LENGTH],
96
125
  style=ThemeKey.INVALID_TOOL_CALL_ARGS,
97
126
  )
98
- grid.add_row(tool_name_column, summary)
99
- return grid
127
+ return _render_tool_call_tree(mark=MARK_BASH, tool_name=tool_name, details=details)
100
128
 
101
129
  if not isinstance(payload_raw, dict):
102
- summary = Text(
103
- str(payload_raw)[: const.INVALID_TOOL_CALL_MAX_LENGTH],
130
+ details = Text(
131
+ str(payload_raw)[:INVALID_TOOL_CALL_MAX_LENGTH],
104
132
  style=ThemeKey.INVALID_TOOL_CALL_ARGS,
105
133
  )
106
- grid.add_row(tool_name_column, summary)
107
- return grid
134
+ return _render_tool_call_tree(mark=MARK_BASH, tool_name=tool_name, details=details)
108
135
 
109
136
  payload: dict[str, object] = cast(dict[str, object], payload_raw)
110
137
 
@@ -118,24 +145,27 @@ def render_bash_tool_call(arguments: str) -> RenderableType:
118
145
 
119
146
  highlighted = highlight_bash_command(cmd_str)
120
147
 
121
- # For commands > 10 lines, use CodePanel for better display
122
- if line_count > 10:
148
+ # For commands > threshold lines, use CodePanel for better display
149
+ if line_count > BASH_OUTPUT_PANEL_THRESHOLD:
123
150
  code_panel = CodePanel(highlighted, border_style=ThemeKey.LINES)
124
151
  if isinstance(timeout_ms, int):
125
152
  if timeout_ms >= 1000 and timeout_ms % 1000 == 0:
126
153
  timeout_text = Text(f"{timeout_ms // 1000}s", style=ThemeKey.TOOL_TIMEOUT)
127
154
  else:
128
155
  timeout_text = Text(f"{timeout_ms}ms", style=ThemeKey.TOOL_TIMEOUT)
129
- grid.add_row(tool_name_column, Group(code_panel, timeout_text))
156
+ return _render_tool_call_tree(
157
+ mark=MARK_BASH,
158
+ tool_name=tool_name,
159
+ details=Group(code_panel, timeout_text),
160
+ )
130
161
  else:
131
- grid.add_row(tool_name_column, code_panel)
132
- return grid
162
+ return _render_tool_call_tree(mark=MARK_BASH, tool_name=tool_name, details=code_panel)
133
163
  if isinstance(timeout_ms, int):
134
164
  if timeout_ms >= 1000 and timeout_ms % 1000 == 0:
135
165
  highlighted.append(f" {timeout_ms // 1000}s", style=ThemeKey.TOOL_TIMEOUT)
136
166
  else:
137
167
  highlighted.append(f" {timeout_ms}ms", style=ThemeKey.TOOL_TIMEOUT)
138
- grid.add_row(tool_name_column, highlighted)
168
+ return _render_tool_call_tree(mark=MARK_BASH, tool_name=tool_name, details=highlighted)
139
169
  else:
140
170
  summary = Text("", ThemeKey.TOOL_PARAM)
141
171
  if isinstance(timeout_ms, int):
@@ -143,77 +173,73 @@ def render_bash_tool_call(arguments: str) -> RenderableType:
143
173
  summary.append(f"{timeout_ms // 1000}s", style=ThemeKey.TOOL_TIMEOUT)
144
174
  else:
145
175
  summary.append(f"{timeout_ms}ms", style=ThemeKey.TOOL_TIMEOUT)
146
- grid.add_row(tool_name_column, summary)
147
-
148
- return grid
176
+ bash_details: RenderableType | None = summary if summary.plain else None
177
+ return _render_tool_call_tree(mark=MARK_BASH, tool_name=tool_name, details=bash_details)
149
178
 
150
179
 
151
180
  def render_update_plan_tool_call(arguments: str) -> RenderableType:
152
- grid = create_grid()
153
- tool_name_column = Text.assemble((MARK_PLAN, ThemeKey.TOOL_MARK), " ", ("Update Plan", ThemeKey.TOOL_NAME))
154
- explanation_column = Text("")
181
+ tool_name = "Update Plan"
182
+ details: RenderableType | None = None
155
183
 
156
184
  if arguments:
157
185
  try:
158
186
  payload = json.loads(arguments)
159
187
  except json.JSONDecodeError:
160
- explanation_column = Text(
161
- arguments.strip()[: const.INVALID_TOOL_CALL_MAX_LENGTH],
188
+ details = Text(
189
+ arguments.strip()[:INVALID_TOOL_CALL_MAX_LENGTH],
162
190
  style=ThemeKey.INVALID_TOOL_CALL_ARGS,
163
191
  )
164
192
  else:
165
193
  explanation = payload.get("explanation")
166
194
  if isinstance(explanation, str) and explanation.strip():
167
- explanation_column = Text(explanation.strip(), style=ThemeKey.TODO_EXPLANATION)
195
+ details = Text(explanation.strip(), style=ThemeKey.TODO_EXPLANATION)
168
196
 
169
- grid.add_row(tool_name_column, explanation_column)
170
- return grid
197
+ return _render_tool_call_tree(mark=MARK_PLAN, tool_name=tool_name, details=details)
171
198
 
172
199
 
173
200
  def render_read_tool_call(arguments: str) -> RenderableType:
174
- grid = create_grid()
175
- render_result: Text = Text.assemble(("Read", ThemeKey.TOOL_NAME), " ")
201
+ tool_name = "Read"
202
+ details = Text("", ThemeKey.TOOL_PARAM)
176
203
  try:
177
204
  json_dict = json.loads(arguments)
178
205
  file_path = json_dict.get("file_path")
179
206
  limit = json_dict.get("limit", None)
180
207
  offset = json_dict.get("offset", None)
181
- render_result = render_result.append_text(render_path(file_path, ThemeKey.TOOL_PARAM_FILE_PATH))
208
+ if isinstance(file_path, str) and file_path:
209
+ details.append_text(render_path(file_path, ThemeKey.TOOL_PARAM_FILE_PATH))
210
+ else:
211
+ details.append("(no file_path)", style=ThemeKey.TOOL_PARAM)
182
212
  if limit is not None and offset is not None:
183
- render_result = (
184
- render_result.append_text(Text(" "))
213
+ details = (
214
+ details.append_text(Text(" "))
185
215
  .append_text(Text(str(offset), ThemeKey.TOOL_PARAM_BOLD))
186
216
  .append_text(Text(":", ThemeKey.TOOL_PARAM))
187
217
  .append_text(Text(str(offset + limit - 1), ThemeKey.TOOL_PARAM_BOLD))
188
218
  )
189
219
  elif limit is not None:
190
- render_result = (
191
- render_result.append_text(Text(" "))
220
+ details = (
221
+ details.append_text(Text(" "))
192
222
  .append_text(Text("1", ThemeKey.TOOL_PARAM_BOLD))
193
223
  .append_text(Text(":", ThemeKey.TOOL_PARAM))
194
224
  .append_text(Text(str(limit), ThemeKey.TOOL_PARAM_BOLD))
195
225
  )
196
226
  elif offset is not None:
197
- render_result = (
198
- render_result.append_text(Text(" "))
227
+ details = (
228
+ details.append_text(Text(" "))
199
229
  .append_text(Text(str(offset), ThemeKey.TOOL_PARAM_BOLD))
200
230
  .append_text(Text(":", ThemeKey.TOOL_PARAM))
201
231
  .append_text(Text("-", ThemeKey.TOOL_PARAM_BOLD))
202
232
  )
203
233
  except json.JSONDecodeError:
204
- render_result = render_result.append_text(
205
- Text(
206
- arguments.strip()[: const.INVALID_TOOL_CALL_MAX_LENGTH],
207
- style=ThemeKey.INVALID_TOOL_CALL_ARGS,
208
- )
234
+ details = Text(
235
+ arguments.strip()[:INVALID_TOOL_CALL_MAX_LENGTH],
236
+ style=ThemeKey.INVALID_TOOL_CALL_ARGS,
209
237
  )
210
- grid.add_row(Text(MARK_READ, ThemeKey.TOOL_MARK), render_result)
211
- return grid
238
+ return _render_tool_call_tree(mark=MARK_READ, tool_name=tool_name, details=details)
212
239
 
213
240
 
214
241
  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))
242
+ tool_name = "Edit"
217
243
  try:
218
244
  json_dict = json.loads(arguments)
219
245
  file_path = json_dict.get("file_path")
@@ -226,52 +252,46 @@ def render_edit_tool_call(arguments: str) -> RenderableType:
226
252
  replace_info.append(old_string, ThemeKey.BASH_STRING)
227
253
  replace_info.append(" → ", ThemeKey.BASH_OPERATOR)
228
254
  replace_info.append(new_string, ThemeKey.BASH_STRING)
229
- arguments_column: RenderableType = Group(path_text, replace_info)
255
+ details: RenderableType = Group(path_text, replace_info)
230
256
  else:
231
- arguments_column = path_text
257
+ details = path_text
232
258
  except json.JSONDecodeError:
233
- arguments_column = Text(
234
- arguments.strip()[: const.INVALID_TOOL_CALL_MAX_LENGTH],
259
+ details = Text(
260
+ arguments.strip()[:INVALID_TOOL_CALL_MAX_LENGTH],
235
261
  style=ThemeKey.INVALID_TOOL_CALL_ARGS,
236
262
  )
237
- grid.add_row(tool_name_column, arguments_column)
238
- return grid
263
+ return _render_tool_call_tree(mark=MARK_EDIT, tool_name=tool_name, details=details)
239
264
 
240
265
 
241
266
  def render_write_tool_call(arguments: str) -> RenderableType:
242
- grid = create_grid()
267
+ tool_name = "Write"
243
268
  try:
244
269
  json_dict = json.loads(arguments)
245
270
  file_path = json_dict.get("file_path", "")
246
- tool_name_column = Text.assemble((MARK_WRITE, ThemeKey.TOOL_MARK), " ", ("Write", ThemeKey.TOOL_NAME))
247
271
  # Markdown files show path in result panel, skip here to avoid duplication
248
272
  if file_path.endswith(".md"):
249
- arguments_column = Text("")
273
+ details: RenderableType | None = None
250
274
  else:
251
- arguments_column = render_path(file_path, ThemeKey.TOOL_PARAM_FILE_PATH)
275
+ details = render_path(file_path, ThemeKey.TOOL_PARAM_FILE_PATH)
252
276
  except json.JSONDecodeError:
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],
277
+ details = Text(
278
+ arguments.strip()[:INVALID_TOOL_CALL_MAX_LENGTH],
256
279
  style=ThemeKey.INVALID_TOOL_CALL_ARGS,
257
280
  )
258
- grid.add_row(tool_name_column, arguments_column)
259
- return grid
281
+ return _render_tool_call_tree(mark=MARK_WRITE, tool_name=tool_name, details=details)
260
282
 
261
283
 
262
284
  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))
285
+ tool_name = "Move"
265
286
 
266
287
  try:
267
288
  payload = json.loads(arguments)
268
289
  except json.JSONDecodeError:
269
- arguments_column = Text(
270
- arguments.strip()[: const.INVALID_TOOL_CALL_MAX_LENGTH],
290
+ details = Text(
291
+ arguments.strip()[:INVALID_TOOL_CALL_MAX_LENGTH],
271
292
  style=ThemeKey.INVALID_TOOL_CALL_ARGS,
272
293
  )
273
- grid.add_row(tool_name_column, arguments_column)
274
- return grid
294
+ return _render_tool_call_tree(mark=MARK_MOVE, tool_name=tool_name, details=details)
275
295
 
276
296
  source_path = payload.get("source_file_path", "")
277
297
  target_path = payload.get("target_file_path", "")
@@ -288,26 +308,23 @@ def render_move_tool_call(arguments: str) -> RenderableType:
288
308
  if target_path:
289
309
  parts.append_text(render_path(target_path, ThemeKey.TOOL_PARAM_FILE_PATH))
290
310
 
291
- grid.add_row(tool_name_column, parts)
292
- return grid
311
+ return _render_tool_call_tree(mark=MARK_MOVE, tool_name=tool_name, details=parts)
293
312
 
294
313
 
295
314
  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))
315
+ tool_name = "Apply Patch"
298
316
 
299
317
  try:
300
318
  payload = json.loads(arguments)
301
319
  except json.JSONDecodeError:
302
- arguments_column = Text(
303
- arguments.strip()[: const.INVALID_TOOL_CALL_MAX_LENGTH],
320
+ details = Text(
321
+ arguments.strip()[:INVALID_TOOL_CALL_MAX_LENGTH],
304
322
  style=ThemeKey.INVALID_TOOL_CALL_ARGS,
305
323
  )
306
- grid.add_row(tool_name_column, arguments_column)
307
- return grid
324
+ return _render_tool_call_tree(mark=MARK_EDIT, tool_name=tool_name, details=details)
308
325
 
309
326
  patch_content = payload.get("patch", "")
310
- arguments_column = Text("", ThemeKey.TOOL_PARAM)
327
+ details = Text("", ThemeKey.TOOL_PARAM)
311
328
 
312
329
  if isinstance(patch_content, str):
313
330
  update_count = 0
@@ -330,15 +347,14 @@ def render_apply_patch_tool_call(arguments: str) -> RenderableType:
330
347
  parts.append(f"Delete File × {delete_count}" if delete_count > 1 else "Delete File")
331
348
 
332
349
  if parts:
333
- arguments_column = Text(", ".join(parts), ThemeKey.TOOL_PARAM)
350
+ details = Text(", ".join(parts), ThemeKey.TOOL_PARAM)
334
351
  else:
335
- arguments_column = Text(
336
- str(patch_content)[: const.INVALID_TOOL_CALL_MAX_LENGTH],
352
+ details = Text(
353
+ str(patch_content)[:INVALID_TOOL_CALL_MAX_LENGTH],
337
354
  ThemeKey.INVALID_TOOL_CALL_ARGS,
338
355
  )
339
356
 
340
- grid.add_row(tool_name_column, arguments_column)
341
- return grid
357
+ return _render_tool_call_tree(mark=MARK_EDIT, tool_name=tool_name, details=details)
342
358
 
343
359
 
344
360
  def render_todo(tr: events.ToolResultEvent) -> RenderableType:
@@ -364,13 +380,13 @@ def render_todo(tr: events.ToolResultEvent) -> RenderableType:
364
380
  text.stylize(text_style)
365
381
  todo_grid.add_row(Text(mark, style=mark_style), text)
366
382
 
367
- return Padding.indent(todo_grid, level=2)
383
+ return todo_grid
368
384
 
369
385
 
370
386
  def render_generic_tool_result(result: str, *, is_error: bool = False) -> RenderableType:
371
- """Render a generic tool result as indented, truncated text."""
387
+ """Render a generic tool result as truncated text."""
372
388
  style = ThemeKey.ERROR if is_error else ThemeKey.TOOL_RESULT
373
- return Padding.indent(truncate_display(result, base_style=style), level=2)
389
+ return truncate_middle(result, base_style=style)
374
390
 
375
391
 
376
392
  def _extract_mermaid_link(
@@ -382,15 +398,14 @@ def _extract_mermaid_link(
382
398
 
383
399
 
384
400
  def render_mermaid_tool_call(arguments: str) -> RenderableType:
385
- grid = create_grid()
386
- tool_name_column = Text.assemble((MARK_MERMAID, ThemeKey.TOOL_MARK), " ", ("Mermaid", ThemeKey.TOOL_NAME))
401
+ tool_name = "Mermaid"
387
402
  summary = Text("", ThemeKey.TOOL_PARAM)
388
403
 
389
404
  try:
390
405
  payload: dict[str, str] = json.loads(arguments)
391
406
  except json.JSONDecodeError:
392
407
  summary = Text(
393
- arguments.strip()[: const.INVALID_TOOL_CALL_MAX_LENGTH],
408
+ arguments.strip()[:INVALID_TOOL_CALL_MAX_LENGTH],
394
409
  style=ThemeKey.INVALID_TOOL_CALL_ARGS,
395
410
  )
396
411
  else:
@@ -401,11 +416,10 @@ def render_mermaid_tool_call(arguments: str) -> RenderableType:
401
416
  else:
402
417
  summary = Text("0 lines", ThemeKey.TOOL_PARAM)
403
418
 
404
- grid.add_row(tool_name_column, summary)
405
- return grid
419
+ return _render_tool_call_tree(mark=MARK_MERMAID, tool_name=tool_name, details=summary)
406
420
 
407
421
 
408
- def _truncate_url(url: str, max_length: int = 400) -> str:
422
+ def _truncate_url(url: str, max_length: int = URL_TRUNCATE_MAX_LENGTH) -> str:
409
423
  """Truncate URL for display, preserving domain and path structure."""
410
424
  if len(url) <= max_length:
411
425
  return url
@@ -418,7 +432,7 @@ def _truncate_url(url: str, max_length: int = 400) -> str:
418
432
  if len(display_url) <= max_length:
419
433
  return display_url
420
434
  # Truncate with ellipsis
421
- return display_url[: max_length - 3] + "..."
435
+ return display_url[: max_length - 1] + ""
422
436
 
423
437
 
424
438
  def _render_mermaid_viewer_link(
@@ -440,7 +454,7 @@ def _render_mermaid_viewer_link(
440
454
  except ValueError:
441
455
  file_url = f"file://{viewer_path.as_posix()}"
442
456
 
443
- rendered = Text.assemble(("saved in:", ThemeKey.TOOL_RESULT), " ")
457
+ rendered = Text.assemble(("View diagram in ", ThemeKey.TOOL_RESULT), " ")
444
458
  start = len(rendered)
445
459
  rendered.append(display_path, ThemeKey.TOOL_RESULT_MERMAID)
446
460
  end = len(rendered)
@@ -452,39 +466,34 @@ def _render_mermaid_viewer_link(
452
466
 
453
467
 
454
468
  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))
469
+ tool_name = "Fetch"
457
470
 
458
471
  try:
459
472
  payload: dict[str, str] = json.loads(arguments)
460
473
  except json.JSONDecodeError:
461
474
  summary = Text(
462
- arguments.strip()[: const.INVALID_TOOL_CALL_MAX_LENGTH],
475
+ arguments.strip()[:INVALID_TOOL_CALL_MAX_LENGTH],
463
476
  style=ThemeKey.INVALID_TOOL_CALL_ARGS,
464
477
  )
465
- grid.add_row(tool_name_column, summary)
466
- return grid
478
+ return _render_tool_call_tree(mark=MARK_WEB_FETCH, tool_name=tool_name, details=summary)
467
479
 
468
480
  url = payload.get("url", "")
469
481
  summary = Text(_truncate_url(url), ThemeKey.TOOL_PARAM_FILE_PATH) if url else Text("(no url)", ThemeKey.TOOL_PARAM)
470
482
 
471
- grid.add_row(tool_name_column, summary)
472
- return grid
483
+ return _render_tool_call_tree(mark=MARK_WEB_FETCH, tool_name=tool_name, details=summary)
473
484
 
474
485
 
475
486
  def render_web_search_tool_call(arguments: str) -> RenderableType:
476
- grid = create_grid()
477
- tool_name_column = Text.assemble((MARK_WEB_SEARCH, ThemeKey.TOOL_MARK), " ", ("Web Search", ThemeKey.TOOL_NAME))
487
+ tool_name = "Web Search"
478
488
 
479
489
  try:
480
490
  payload: dict[str, Any] = json.loads(arguments)
481
491
  except json.JSONDecodeError:
482
492
  summary = Text(
483
- arguments.strip()[: const.INVALID_TOOL_CALL_MAX_LENGTH],
493
+ arguments.strip()[:INVALID_TOOL_CALL_MAX_LENGTH],
484
494
  style=ThemeKey.INVALID_TOOL_CALL_ARGS,
485
495
  )
486
- grid.add_row(tool_name_column, summary)
487
- return grid
496
+ return _render_tool_call_tree(mark=MARK_WEB_SEARCH, tool_name=tool_name, details=summary)
488
497
 
489
498
  query = payload.get("query", "")
490
499
  max_results = payload.get("max_results")
@@ -492,19 +501,24 @@ def render_web_search_tool_call(arguments: str) -> RenderableType:
492
501
  summary = Text("", ThemeKey.TOOL_PARAM)
493
502
  if query:
494
503
  # Truncate long queries
495
- display_query = query if len(query) <= 80 else query[:77] + "..."
504
+ display_query = (
505
+ query if len(query) <= QUERY_DISPLAY_TRUNCATE_LENGTH else query[: QUERY_DISPLAY_TRUNCATE_LENGTH - 1] + "…"
506
+ )
496
507
  summary.append(display_query, ThemeKey.TOOL_PARAM)
497
508
  else:
498
509
  summary.append("(no query)", ThemeKey.TOOL_PARAM)
499
510
 
500
- if isinstance(max_results, int) and max_results != 10:
511
+ if isinstance(max_results, int) and max_results != WEB_SEARCH_DEFAULT_MAX_RESULTS:
501
512
  summary.append(f" (max {max_results})", ThemeKey.TOOL_TIMEOUT)
502
513
 
503
- grid.add_row(tool_name_column, summary)
504
- return grid
514
+ return _render_tool_call_tree(mark=MARK_WEB_SEARCH, tool_name=tool_name, details=summary)
505
515
 
506
516
 
507
- def render_mermaid_tool_result(tr: events.ToolResultEvent) -> RenderableType:
517
+ def render_mermaid_tool_result(
518
+ tr: events.ToolResultEvent,
519
+ *,
520
+ session_id: str | None = None,
521
+ ) -> RenderableType:
508
522
  from klaude_code.ui.terminal import supports_osc8_hyperlinks
509
523
 
510
524
  link_info = _extract_mermaid_link(tr.ui_extra)
@@ -513,7 +527,8 @@ def render_mermaid_tool_result(tr: events.ToolResultEvent) -> RenderableType:
513
527
 
514
528
  use_osc8 = supports_osc8_hyperlinks()
515
529
  viewer = _render_mermaid_viewer_link(tr, link_info, use_osc8=use_osc8)
516
- return Padding.indent(viewer, level=2)
530
+
531
+ return viewer
517
532
 
518
533
 
519
534
  def _extract_truncation(
@@ -533,7 +548,7 @@ def render_truncation_info(ui_extra: model.TruncationUIExtra) -> RenderableType:
533
548
  (ui_extra.saved_file_path, ThemeKey.TOOL_RESULT_TRUNCATED),
534
549
  (f", {truncated_kb:.1f}KB truncated", ThemeKey.TOOL_RESULT_TRUNCATED),
535
550
  )
536
- return Padding.indent(text, level=2)
551
+ return text
537
552
 
538
553
 
539
554
  def get_truncation_info(tr: events.ToolResultEvent) -> model.TruncationUIExtra | None:
@@ -542,10 +557,7 @@ def get_truncation_info(tr: events.ToolResultEvent) -> model.TruncationUIExtra |
542
557
 
543
558
 
544
559
  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
560
+ return _render_tool_call_tree(mark=MARK_DONE, tool_name="Report Back", details=None)
549
561
 
550
562
 
551
563
  # Tool name to active form mapping (for spinner status)
@@ -655,75 +667,78 @@ def render_markdown_doc(md_ui: model.MarkdownDocUIExtra, *, code_theme: str) ->
655
667
  )
656
668
 
657
669
 
658
- def render_tool_result(e: events.ToolResultEvent, *, code_theme: str = "monokai") -> RenderableType | None:
670
+ def render_tool_result(
671
+ e: events.ToolResultEvent,
672
+ *,
673
+ code_theme: str = "monokai",
674
+ session_id: str | None = None,
675
+ ) -> RenderableType | None:
659
676
  """Unified entry point for rendering tool results.
660
677
 
661
678
  Returns a Rich Renderable or None if the tool result should not be rendered.
662
679
  """
663
- from klaude_code.ui.renderers import errors as r_errors
664
-
665
680
  if is_sub_agent_tool(e.tool_name):
666
681
  return None
667
682
 
683
+ def wrap(content: RenderableType) -> TreeQuote:
684
+ return TreeQuote.for_tool_result(content, is_last=e.is_last_in_turn)
685
+
668
686
  # Handle error case
669
687
  if e.status == "error" and e.ui_extra is None:
670
- error_msg = truncate_display(e.result)
671
- return r_errors.render_tool_error(error_msg)
688
+ return wrap(truncate_middle(e.result, base_style=ThemeKey.ERROR))
672
689
 
673
690
  # Render multiple ui blocks if present
674
691
  if isinstance(e.ui_extra, model.MultiUIExtra) and e.ui_extra.items:
675
692
  rendered: list[RenderableType] = []
676
693
  for item in e.ui_extra.items:
677
694
  if isinstance(item, model.MarkdownDocUIExtra):
678
- rendered.append(Padding.indent(render_markdown_doc(item, code_theme=code_theme), level=2))
695
+ rendered.append(render_markdown_doc(item, code_theme=code_theme))
679
696
  elif isinstance(item, model.DiffUIExtra):
680
697
  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
698
+ rendered.append(r_diffs.render_structured_diff(item, show_file_name=show_file_name))
699
+ return wrap(Group(*rendered)) if rendered else None
685
700
 
686
701
  # Show truncation info if output was truncated and saved to file
687
702
  truncation_info = get_truncation_info(e)
688
703
  if truncation_info:
689
- return Group(render_truncation_info(truncation_info), render_generic_tool_result(e.result))
704
+ result = render_generic_tool_result(e.result, is_error=e.status == "error")
705
+ return wrap(Group(render_truncation_info(truncation_info), result))
690
706
 
691
707
  diff_ui = _extract_diff(e.ui_extra)
692
708
  md_ui = _extract_markdown_doc(e.ui_extra)
693
709
 
710
+ def _render_fallback() -> TreeQuote:
711
+ if len(e.result.strip()) == 0:
712
+ return wrap(render_generic_tool_result("(no content)"))
713
+ return wrap(render_generic_tool_result(e.result, is_error=e.status == "error"))
714
+
694
715
  match e.tool_name:
695
716
  case tools.READ:
696
717
  return None
697
718
  case tools.EDIT:
698
- return Padding.indent(r_diffs.render_structured_diff(diff_ui) if diff_ui else Text(""), level=2)
719
+ return wrap(r_diffs.render_structured_diff(diff_ui) if diff_ui else Text(""))
699
720
  case tools.WRITE:
700
721
  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)
722
+ return wrap(render_markdown_doc(md_ui, code_theme=code_theme))
723
+ return wrap(r_diffs.render_structured_diff(diff_ui) if diff_ui else Text(""))
703
724
  case tools.MOVE:
704
725
  # Same-file move returns single DiffUIExtra, cross-file returns MultiUIExtra (handled above)
705
726
  if diff_ui:
706
- return Padding.indent(r_diffs.render_structured_diff(diff_ui, show_file_name=True), level=2)
727
+ return wrap(r_diffs.render_structured_diff(diff_ui, show_file_name=True))
707
728
  return None
708
729
  case tools.APPLY_PATCH:
709
730
  if md_ui:
710
- return Padding.indent(render_markdown_doc(md_ui, code_theme=code_theme), level=2)
731
+ return wrap(render_markdown_doc(md_ui, code_theme=code_theme))
711
732
  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)
733
+ return wrap(r_diffs.render_structured_diff(diff_ui, show_file_name=True))
734
+ return _render_fallback()
716
735
  case tools.TODO_WRITE | tools.UPDATE_PLAN:
717
- return render_todo(e)
736
+ return wrap(render_todo(e))
718
737
  case tools.MERMAID:
719
- return render_mermaid_tool_result(e)
738
+ return wrap(render_mermaid_tool_result(e, session_id=session_id))
720
739
  case tools.BASH:
721
740
  if e.result.startswith("diff --git"):
722
- return r_diffs.render_diff_panel(e.result, show_file_name=True)
723
- if len(e.result.strip()) == 0:
724
- return render_generic_tool_result("(no content)")
725
- return render_generic_tool_result(e.result)
741
+ return wrap(r_diffs.render_diff_panel(e.result, show_file_name=True))
742
+ return _render_fallback()
726
743
  case _:
727
- if len(e.result.strip()) == 0:
728
- return render_generic_tool_result("(no content)")
729
- return render_generic_tool_result(e.result)
744
+ return _render_fallback()