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