klaude-code 1.2.6__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 (167) hide show
  1. klaude_code/__init__.py +0 -0
  2. klaude_code/cli/__init__.py +1 -0
  3. klaude_code/cli/main.py +298 -0
  4. klaude_code/cli/runtime.py +331 -0
  5. klaude_code/cli/session_cmd.py +80 -0
  6. klaude_code/command/__init__.py +43 -0
  7. klaude_code/command/clear_cmd.py +20 -0
  8. klaude_code/command/command_abc.py +92 -0
  9. klaude_code/command/diff_cmd.py +138 -0
  10. klaude_code/command/export_cmd.py +86 -0
  11. klaude_code/command/help_cmd.py +51 -0
  12. klaude_code/command/model_cmd.py +43 -0
  13. klaude_code/command/prompt-dev-docs-update.md +56 -0
  14. klaude_code/command/prompt-dev-docs.md +46 -0
  15. klaude_code/command/prompt-init.md +45 -0
  16. klaude_code/command/prompt_command.py +69 -0
  17. klaude_code/command/refresh_cmd.py +43 -0
  18. klaude_code/command/registry.py +110 -0
  19. klaude_code/command/status_cmd.py +111 -0
  20. klaude_code/command/terminal_setup_cmd.py +252 -0
  21. klaude_code/config/__init__.py +11 -0
  22. klaude_code/config/config.py +177 -0
  23. klaude_code/config/list_model.py +162 -0
  24. klaude_code/config/select_model.py +67 -0
  25. klaude_code/const/__init__.py +133 -0
  26. klaude_code/core/__init__.py +0 -0
  27. klaude_code/core/agent.py +165 -0
  28. klaude_code/core/executor.py +485 -0
  29. klaude_code/core/manager/__init__.py +19 -0
  30. klaude_code/core/manager/agent_manager.py +127 -0
  31. klaude_code/core/manager/llm_clients.py +42 -0
  32. klaude_code/core/manager/llm_clients_builder.py +49 -0
  33. klaude_code/core/manager/sub_agent_manager.py +86 -0
  34. klaude_code/core/prompt.py +89 -0
  35. klaude_code/core/prompts/prompt-claude-code.md +98 -0
  36. klaude_code/core/prompts/prompt-codex.md +331 -0
  37. klaude_code/core/prompts/prompt-gemini.md +43 -0
  38. klaude_code/core/prompts/prompt-subagent-explore.md +27 -0
  39. klaude_code/core/prompts/prompt-subagent-oracle.md +23 -0
  40. klaude_code/core/prompts/prompt-subagent-webfetch.md +46 -0
  41. klaude_code/core/prompts/prompt-subagent.md +8 -0
  42. klaude_code/core/reminders.py +445 -0
  43. klaude_code/core/task.py +237 -0
  44. klaude_code/core/tool/__init__.py +75 -0
  45. klaude_code/core/tool/file/__init__.py +0 -0
  46. klaude_code/core/tool/file/apply_patch.py +492 -0
  47. klaude_code/core/tool/file/apply_patch_tool.md +1 -0
  48. klaude_code/core/tool/file/apply_patch_tool.py +204 -0
  49. klaude_code/core/tool/file/edit_tool.md +9 -0
  50. klaude_code/core/tool/file/edit_tool.py +274 -0
  51. klaude_code/core/tool/file/multi_edit_tool.md +42 -0
  52. klaude_code/core/tool/file/multi_edit_tool.py +199 -0
  53. klaude_code/core/tool/file/read_tool.md +14 -0
  54. klaude_code/core/tool/file/read_tool.py +326 -0
  55. klaude_code/core/tool/file/write_tool.md +8 -0
  56. klaude_code/core/tool/file/write_tool.py +146 -0
  57. klaude_code/core/tool/memory/__init__.py +0 -0
  58. klaude_code/core/tool/memory/memory_tool.md +16 -0
  59. klaude_code/core/tool/memory/memory_tool.py +462 -0
  60. klaude_code/core/tool/memory/skill_loader.py +245 -0
  61. klaude_code/core/tool/memory/skill_tool.md +24 -0
  62. klaude_code/core/tool/memory/skill_tool.py +97 -0
  63. klaude_code/core/tool/shell/__init__.py +0 -0
  64. klaude_code/core/tool/shell/bash_tool.md +43 -0
  65. klaude_code/core/tool/shell/bash_tool.py +123 -0
  66. klaude_code/core/tool/shell/command_safety.py +363 -0
  67. klaude_code/core/tool/sub_agent_tool.py +83 -0
  68. klaude_code/core/tool/todo/__init__.py +0 -0
  69. klaude_code/core/tool/todo/todo_write_tool.md +182 -0
  70. klaude_code/core/tool/todo/todo_write_tool.py +121 -0
  71. klaude_code/core/tool/todo/update_plan_tool.md +3 -0
  72. klaude_code/core/tool/todo/update_plan_tool.py +104 -0
  73. klaude_code/core/tool/tool_abc.py +25 -0
  74. klaude_code/core/tool/tool_context.py +106 -0
  75. klaude_code/core/tool/tool_registry.py +78 -0
  76. klaude_code/core/tool/tool_runner.py +252 -0
  77. klaude_code/core/tool/truncation.py +170 -0
  78. klaude_code/core/tool/web/__init__.py +0 -0
  79. klaude_code/core/tool/web/mermaid_tool.md +21 -0
  80. klaude_code/core/tool/web/mermaid_tool.py +76 -0
  81. klaude_code/core/tool/web/web_fetch_tool.md +8 -0
  82. klaude_code/core/tool/web/web_fetch_tool.py +159 -0
  83. klaude_code/core/turn.py +220 -0
  84. klaude_code/llm/__init__.py +21 -0
  85. klaude_code/llm/anthropic/__init__.py +3 -0
  86. klaude_code/llm/anthropic/client.py +221 -0
  87. klaude_code/llm/anthropic/input.py +200 -0
  88. klaude_code/llm/client.py +49 -0
  89. klaude_code/llm/input_common.py +239 -0
  90. klaude_code/llm/openai_compatible/__init__.py +3 -0
  91. klaude_code/llm/openai_compatible/client.py +211 -0
  92. klaude_code/llm/openai_compatible/input.py +109 -0
  93. klaude_code/llm/openai_compatible/tool_call_accumulator.py +80 -0
  94. klaude_code/llm/openrouter/__init__.py +3 -0
  95. klaude_code/llm/openrouter/client.py +200 -0
  96. klaude_code/llm/openrouter/input.py +160 -0
  97. klaude_code/llm/openrouter/reasoning_handler.py +209 -0
  98. klaude_code/llm/registry.py +22 -0
  99. klaude_code/llm/responses/__init__.py +3 -0
  100. klaude_code/llm/responses/client.py +216 -0
  101. klaude_code/llm/responses/input.py +167 -0
  102. klaude_code/llm/usage.py +109 -0
  103. klaude_code/protocol/__init__.py +4 -0
  104. klaude_code/protocol/commands.py +21 -0
  105. klaude_code/protocol/events.py +163 -0
  106. klaude_code/protocol/llm_param.py +147 -0
  107. klaude_code/protocol/model.py +287 -0
  108. klaude_code/protocol/op.py +89 -0
  109. klaude_code/protocol/op_handler.py +28 -0
  110. klaude_code/protocol/sub_agent.py +348 -0
  111. klaude_code/protocol/tools.py +15 -0
  112. klaude_code/session/__init__.py +4 -0
  113. klaude_code/session/export.py +624 -0
  114. klaude_code/session/selector.py +76 -0
  115. klaude_code/session/session.py +474 -0
  116. klaude_code/session/templates/export_session.html +1434 -0
  117. klaude_code/trace/__init__.py +3 -0
  118. klaude_code/trace/log.py +168 -0
  119. klaude_code/ui/__init__.py +91 -0
  120. klaude_code/ui/core/__init__.py +1 -0
  121. klaude_code/ui/core/display.py +103 -0
  122. klaude_code/ui/core/input.py +71 -0
  123. klaude_code/ui/core/stage_manager.py +55 -0
  124. klaude_code/ui/modes/__init__.py +1 -0
  125. klaude_code/ui/modes/debug/__init__.py +1 -0
  126. klaude_code/ui/modes/debug/display.py +36 -0
  127. klaude_code/ui/modes/exec/__init__.py +1 -0
  128. klaude_code/ui/modes/exec/display.py +63 -0
  129. klaude_code/ui/modes/repl/__init__.py +51 -0
  130. klaude_code/ui/modes/repl/clipboard.py +152 -0
  131. klaude_code/ui/modes/repl/completers.py +429 -0
  132. klaude_code/ui/modes/repl/display.py +60 -0
  133. klaude_code/ui/modes/repl/event_handler.py +375 -0
  134. klaude_code/ui/modes/repl/input_prompt_toolkit.py +198 -0
  135. klaude_code/ui/modes/repl/key_bindings.py +170 -0
  136. klaude_code/ui/modes/repl/renderer.py +281 -0
  137. klaude_code/ui/renderers/__init__.py +0 -0
  138. klaude_code/ui/renderers/assistant.py +21 -0
  139. klaude_code/ui/renderers/common.py +8 -0
  140. klaude_code/ui/renderers/developer.py +158 -0
  141. klaude_code/ui/renderers/diffs.py +215 -0
  142. klaude_code/ui/renderers/errors.py +16 -0
  143. klaude_code/ui/renderers/metadata.py +190 -0
  144. klaude_code/ui/renderers/sub_agent.py +71 -0
  145. klaude_code/ui/renderers/thinking.py +39 -0
  146. klaude_code/ui/renderers/tools.py +551 -0
  147. klaude_code/ui/renderers/user_input.py +65 -0
  148. klaude_code/ui/rich/__init__.py +1 -0
  149. klaude_code/ui/rich/live.py +65 -0
  150. klaude_code/ui/rich/markdown.py +308 -0
  151. klaude_code/ui/rich/quote.py +34 -0
  152. klaude_code/ui/rich/searchable_text.py +71 -0
  153. klaude_code/ui/rich/status.py +240 -0
  154. klaude_code/ui/rich/theme.py +274 -0
  155. klaude_code/ui/terminal/__init__.py +1 -0
  156. klaude_code/ui/terminal/color.py +244 -0
  157. klaude_code/ui/terminal/control.py +147 -0
  158. klaude_code/ui/terminal/notifier.py +107 -0
  159. klaude_code/ui/terminal/progress_bar.py +87 -0
  160. klaude_code/ui/utils/__init__.py +1 -0
  161. klaude_code/ui/utils/common.py +108 -0
  162. klaude_code/ui/utils/debouncer.py +42 -0
  163. klaude_code/version.py +163 -0
  164. klaude_code-1.2.6.dist-info/METADATA +178 -0
  165. klaude_code-1.2.6.dist-info/RECORD +167 -0
  166. klaude_code-1.2.6.dist-info/WHEEL +4 -0
  167. klaude_code-1.2.6.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,551 @@
1
+ import json
2
+ from pathlib import Path
3
+
4
+ from rich.console import RenderableType
5
+ from rich.padding import Padding
6
+ from rich.text import Text
7
+
8
+ from klaude_code import const
9
+ from klaude_code.protocol import events, model
10
+ from klaude_code.protocol.sub_agent import is_sub_agent_tool as _is_sub_agent_tool
11
+ from klaude_code.ui.renderers import diffs as r_diffs
12
+ from klaude_code.ui.renderers.common import create_grid
13
+ from klaude_code.ui.rich.theme import ThemeKey
14
+ from klaude_code.ui.utils.common import truncate_display
15
+
16
+
17
+ def is_sub_agent_tool(tool_name: str) -> bool:
18
+ return _is_sub_agent_tool(tool_name)
19
+
20
+
21
+ def render_path(path: str, style: str, is_directory: bool = False) -> Text:
22
+ if path.startswith(str(Path().cwd())):
23
+ path = path.replace(str(Path().cwd()), "").lstrip("/")
24
+ elif path.startswith(str(Path().home())):
25
+ path = path.replace(str(Path().home()), "~")
26
+ elif not path.startswith("/") and not path.startswith("."):
27
+ path = "./" + path
28
+ if is_directory:
29
+ path = path.rstrip("/") + "/"
30
+ return Text(path, style=style)
31
+
32
+
33
+ def render_generic_tool_call(tool_name: str, arguments: str, markup: str = "•") -> RenderableType:
34
+ grid = create_grid()
35
+
36
+ tool_name_column = Text.assemble((markup, ThemeKey.TOOL_MARK), " ", (tool_name, ThemeKey.TOOL_NAME))
37
+ arguments_column = Text("")
38
+ if not arguments:
39
+ grid.add_row(tool_name_column, arguments_column)
40
+ return grid
41
+ try:
42
+ json_dict = json.loads(arguments)
43
+ if len(json_dict) == 0:
44
+ arguments_column = Text("", ThemeKey.TOOL_PARAM)
45
+ elif len(json_dict) == 1:
46
+ arguments_column = Text(str(next(iter(json_dict.values()))), ThemeKey.TOOL_PARAM)
47
+ else:
48
+ arguments_column = Text(
49
+ ", ".join([f"{k}: {v}" for k, v in json_dict.items()]),
50
+ ThemeKey.TOOL_PARAM,
51
+ )
52
+ except json.JSONDecodeError:
53
+ arguments_column = Text(
54
+ arguments.strip()[: const.INVALID_TOOL_CALL_MAX_LENGTH],
55
+ style=ThemeKey.INVALID_TOOL_CALL_ARGS,
56
+ )
57
+ grid.add_row(tool_name_column, arguments_column)
58
+ return grid
59
+
60
+
61
+ def render_update_plan_tool_call(arguments: str) -> RenderableType:
62
+ grid = create_grid()
63
+ tool_name_column = Text.assemble(("◎", ThemeKey.TOOL_MARK), " ", ("Update Plan", ThemeKey.TOOL_NAME))
64
+ explanation_column = Text("")
65
+
66
+ if arguments:
67
+ try:
68
+ payload = json.loads(arguments)
69
+ except json.JSONDecodeError:
70
+ explanation_column = Text(
71
+ arguments.strip()[: const.INVALID_TOOL_CALL_MAX_LENGTH],
72
+ style=ThemeKey.INVALID_TOOL_CALL_ARGS,
73
+ )
74
+ else:
75
+ explanation = payload.get("explanation")
76
+ if isinstance(explanation, str) and explanation.strip():
77
+ explanation_column = Text(explanation.strip(), style=ThemeKey.TODO_EXPLANATION)
78
+
79
+ grid.add_row(tool_name_column, explanation_column)
80
+ return grid
81
+
82
+
83
+ def render_read_tool_call(arguments: str) -> RenderableType:
84
+ grid = create_grid()
85
+ render_result: Text = Text.assemble(("Read", ThemeKey.TOOL_NAME), " ")
86
+ try:
87
+ json_dict = json.loads(arguments)
88
+ file_path = json_dict.get("file_path")
89
+ limit = json_dict.get("limit", None)
90
+ offset = json_dict.get("offset", None)
91
+ render_result = render_result.append_text(render_path(file_path, ThemeKey.TOOL_PARAM_FILE_PATH))
92
+ if limit is not None and offset is not None:
93
+ render_result = (
94
+ render_result.append_text(Text(" "))
95
+ .append_text(Text(str(offset), ThemeKey.TOOL_PARAM_BOLD))
96
+ .append_text(Text(":", ThemeKey.TOOL_PARAM))
97
+ .append_text(Text(str(offset + limit - 1), ThemeKey.TOOL_PARAM_BOLD))
98
+ )
99
+ elif limit is not None:
100
+ render_result = (
101
+ render_result.append_text(Text(" "))
102
+ .append_text(Text("1", ThemeKey.TOOL_PARAM_BOLD))
103
+ .append_text(Text(":", ThemeKey.TOOL_PARAM))
104
+ .append_text(Text(str(limit), ThemeKey.TOOL_PARAM_BOLD))
105
+ )
106
+ elif offset is not None:
107
+ render_result = (
108
+ render_result.append_text(Text(" "))
109
+ .append_text(Text(str(offset), ThemeKey.TOOL_PARAM_BOLD))
110
+ .append_text(Text(":", ThemeKey.TOOL_PARAM))
111
+ .append_text(Text("-", ThemeKey.TOOL_PARAM_BOLD))
112
+ )
113
+ except json.JSONDecodeError:
114
+ render_result = render_result.append_text(
115
+ Text(
116
+ arguments.strip()[: const.INVALID_TOOL_CALL_MAX_LENGTH],
117
+ style=ThemeKey.INVALID_TOOL_CALL_ARGS,
118
+ )
119
+ )
120
+ grid.add_row(Text("←", ThemeKey.TOOL_MARK), render_result)
121
+ return grid
122
+
123
+
124
+ def render_edit_tool_call(arguments: str) -> Text:
125
+ render_result: Text = Text.assemble(("→ ", ThemeKey.TOOL_MARK))
126
+ try:
127
+ json_dict = json.loads(arguments)
128
+ 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
+ )
134
+ 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
+ )
144
+ )
145
+ return render_result
146
+
147
+
148
+ def render_write_tool_call(arguments: str) -> Text:
149
+ render_result: Text = Text.assemble(("→ ", ThemeKey.TOOL_MARK))
150
+ try:
151
+ 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
+ )
165
+ 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
+ )
175
+ )
176
+ return render_result
177
+
178
+
179
+ def render_multi_edit_tool_call(arguments: str) -> Text:
180
+ render_result: Text = Text.assemble(("→ ", ThemeKey.TOOL_MARK), ("MultiEdit", ThemeKey.TOOL_NAME), " ")
181
+ 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
+ )
191
+ 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
+ )
197
+ )
198
+ return render_result
199
+
200
+
201
+ def render_apply_patch_tool_call(arguments: str) -> RenderableType:
202
+ try:
203
+ payload = json.loads(arguments)
204
+ 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
+ ),
213
+ )
214
+
215
+ 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)
220
+
221
+ 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)
225
+ else:
226
+ summary = Text(
227
+ str(patch_content)[: const.INVALID_TOOL_CALL_MAX_LENGTH],
228
+ ThemeKey.INVALID_TOOL_CALL_ARGS,
229
+ )
230
+
231
+ if summary.plain:
232
+ grid.add_row(header, summary)
233
+ else:
234
+ grid.add_row(header, Text("", ThemeKey.TOOL_PARAM))
235
+
236
+ return grid
237
+
238
+
239
+ 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
+
253
+ ui_extra = tr.ui_extra.todo_list
254
+ todo_grid = create_grid()
255
+ for todo in ui_extra.todos:
256
+ is_new_completed = todo.content in ui_extra.new_completed
257
+ match todo.status:
258
+ case "pending":
259
+ mark = "▢"
260
+ mark_style = ThemeKey.TODO_PENDING_MARK
261
+ text_style = ThemeKey.TODO_PENDING
262
+ case "in_progress":
263
+ mark = "◉"
264
+ mark_style = ThemeKey.TODO_IN_PROGRESS_MARK
265
+ text_style = ThemeKey.TODO_IN_PROGRESS
266
+ case "completed":
267
+ mark = "✔"
268
+ mark_style = ThemeKey.TODO_NEW_COMPLETED_MARK if is_new_completed else ThemeKey.TODO_COMPLETED_MARK
269
+ text_style = ThemeKey.TODO_NEW_COMPLETED if is_new_completed else ThemeKey.TODO_COMPLETED
270
+ text = Text(todo.content)
271
+ text.stylize(text_style)
272
+ todo_grid.add_row(Text(mark, style=mark_style), text)
273
+
274
+ return Padding.indent(todo_grid, level=2)
275
+
276
+
277
+ def render_generic_tool_result(result: str, *, is_error: bool = False) -> RenderableType:
278
+ """Render a generic tool result as indented, truncated text."""
279
+ style = ThemeKey.ERROR if is_error else ThemeKey.TOOL_RESULT
280
+ return Padding.indent(Text(truncate_display(result), style=style), level=2)
281
+
282
+
283
+ def _extract_mermaid_link(
284
+ ui_extra: model.ToolResultUIExtra | None,
285
+ ) -> 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
291
+
292
+
293
+ def render_memory_tool_call(arguments: str) -> RenderableType:
294
+ 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
+ }
303
+
304
+ try:
305
+ payload: dict[str, str] = json.loads(arguments)
306
+ except json.JSONDecodeError:
307
+ tool_name_column = Text.assemble(("★", ThemeKey.TOOL_MARK), " ", ("Memory", ThemeKey.TOOL_NAME))
308
+ summary = Text(
309
+ arguments.strip()[: const.INVALID_TOOL_CALL_MAX_LENGTH],
310
+ style=ThemeKey.INVALID_TOOL_CALL_ARGS,
311
+ )
312
+ grid.add_row(tool_name_column, summary)
313
+ return grid
314
+
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))
318
+
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),
329
+ )
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)
342
+
343
+ grid.add_row(tool_name_column, summary)
344
+ return grid
345
+
346
+
347
+ def render_mermaid_tool_call(arguments: str) -> RenderableType:
348
+ grid = create_grid()
349
+ tool_name_column = Text.assemble(("⧉", ThemeKey.TOOL_MARK), " ", ("Mermaid", ThemeKey.TOOL_NAME))
350
+ summary = Text("", ThemeKey.TOOL_PARAM)
351
+
352
+ try:
353
+ payload: dict[str, str] = json.loads(arguments)
354
+ except json.JSONDecodeError:
355
+ summary = Text(
356
+ arguments.strip()[: const.INVALID_TOOL_CALL_MAX_LENGTH],
357
+ style=ThemeKey.INVALID_TOOL_CALL_ARGS,
358
+ )
359
+ 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)
366
+
367
+ grid.add_row(tool_name_column, summary)
368
+ return grid
369
+
370
+
371
+ def render_mermaid_tool_result(tr: events.ToolResultEvent) -> RenderableType:
372
+ link_info = _extract_mermaid_link(tr.ui_extra)
373
+ if link_info is None:
374
+ return render_generic_tool_result(tr.result, is_error=tr.status == "error")
375
+
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)
378
+
379
+
380
+ def _extract_truncation(
381
+ ui_extra: model.ToolResultUIExtra | None,
382
+ ) -> 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
388
+
389
+
390
+ def render_truncation_info(ui_extra: model.TruncationUIExtra) -> RenderableType:
391
+ """Render truncation info for the user."""
392
+ original_kb = ui_extra.original_length / 1024
393
+ truncated_kb = ui_extra.truncated_length / 1024
394
+ 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),
402
+ )
403
+ return Padding.indent(text, level=2)
404
+
405
+
406
+ def get_truncation_info(tr: events.ToolResultEvent) -> model.TruncationUIExtra | None:
407
+ """Extract truncation info from a tool result event."""
408
+ return _extract_truncation(tr.ui_extra)
409
+
410
+
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
+ }
425
+
426
+ # Tool name to active form mapping (for spinner status)
427
+ _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",
440
+ }
441
+
442
+
443
+ def get_tool_active_form(tool_name: str) -> str:
444
+ """Get the active form of a tool name for spinner status.
445
+
446
+ Checks both the static mapping and sub agent profiles.
447
+ """
448
+ if tool_name in _TOOL_ACTIVE_FORM:
449
+ return _TOOL_ACTIVE_FORM[tool_name]
450
+
451
+ # Check sub agent profiles
452
+ from klaude_code.protocol.sub_agent import get_sub_agent_profile_by_tool
453
+
454
+ profile = get_sub_agent_profile_by_tool(tool_name)
455
+ if profile and profile.active_form:
456
+ return profile.active_form
457
+
458
+ return f"Calling {tool_name}"
459
+
460
+
461
+ def render_tool_call(e: events.ToolCallEvent) -> RenderableType | None:
462
+ """Unified entry point for rendering tool calls.
463
+
464
+ Returns a Rich Renderable or None if the tool call should not be rendered.
465
+ """
466
+ from klaude_code.protocol import tools
467
+
468
+ if is_sub_agent_tool(e.tool_name):
469
+ return None
470
+
471
+ match e.tool_name:
472
+ case tools.READ:
473
+ return render_read_tool_call(e.arguments)
474
+ case tools.EDIT:
475
+ return render_edit_tool_call(e.arguments)
476
+ case tools.WRITE:
477
+ return render_write_tool_call(e.arguments)
478
+ case tools.MULTI_EDIT:
479
+ return render_multi_edit_tool_call(e.arguments)
480
+ case tools.BASH:
481
+ return render_generic_tool_call(e.tool_name, e.arguments, ">")
482
+ case tools.APPLY_PATCH:
483
+ return render_apply_patch_tool_call(e.arguments)
484
+ case tools.TODO_WRITE:
485
+ return render_generic_tool_call("Update Todos", "", "◎")
486
+ case tools.UPDATE_PLAN:
487
+ return render_update_plan_tool_call(e.arguments)
488
+ case tools.MERMAID:
489
+ return render_mermaid_tool_call(e.arguments)
490
+ case tools.MEMORY:
491
+ return render_memory_tool_call(e.arguments)
492
+ case tools.SKILL:
493
+ return render_generic_tool_call(e.tool_name, e.arguments, "◈")
494
+ case _:
495
+ return render_generic_tool_call(e.tool_name, e.arguments)
496
+
497
+
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
503
+ return None
504
+
505
+
506
+ def render_tool_result(e: events.ToolResultEvent) -> RenderableType | None:
507
+ """Unified entry point for rendering tool results.
508
+
509
+ Returns a Rich Renderable or None if the tool result should not be rendered.
510
+ """
511
+ from klaude_code.protocol import tools
512
+ from klaude_code.ui.renderers import errors as r_errors
513
+
514
+ if is_sub_agent_tool(e.tool_name):
515
+ return None
516
+
517
+ # Handle error case
518
+ 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)
521
+
522
+ # Show truncation info if output was truncated and saved to file
523
+ truncation_info = get_truncation_info(e)
524
+ if truncation_info:
525
+ return render_truncation_info(truncation_info)
526
+
527
+ diff_text = _extract_diff_text(e.ui_extra)
528
+
529
+ match e.tool_name:
530
+ case tools.READ:
531
+ 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)
539
+ return None
540
+ case tools.TODO_WRITE | tools.UPDATE_PLAN:
541
+ return render_todo(e)
542
+ case tools.MERMAID:
543
+ 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"):
546
+ 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)
549
+ if len(e.result.strip()) == 0:
550
+ return render_generic_tool_result("(no content)")
551
+ return render_generic_tool_result(e.result)
@@ -0,0 +1,65 @@
1
+ import re
2
+
3
+ from rich.console import Group, RenderableType
4
+ from rich.text import Text
5
+
6
+ from klaude_code.command import is_slash_command_name
7
+ from klaude_code.ui.renderers.common import create_grid
8
+ from klaude_code.ui.rich.theme import ThemeKey
9
+
10
+
11
+ def render_at_pattern(
12
+ text: str,
13
+ at_style: str = ThemeKey.USER_INPUT_AT_PATTERN,
14
+ other_style: str = ThemeKey.USER_INPUT,
15
+ ) -> Text:
16
+ if "@" in text:
17
+ parts = re.split(r"(\s+)", text)
18
+ result = Text("")
19
+ for s in parts:
20
+ if s.startswith("@"):
21
+ result.append_text(Text(s, at_style))
22
+ else:
23
+ result.append_text(Text(s, other_style))
24
+ return result
25
+ return Text(text, style=other_style)
26
+
27
+
28
+ def render_user_input(content: str) -> RenderableType:
29
+ """Render a user message as a group of quoted lines with styles.
30
+
31
+ - Highlights slash command on the first line if recognized
32
+ - Highlights @file patterns in all lines
33
+ """
34
+ lines = content.strip().split("\n")
35
+ renderables: list[RenderableType] = []
36
+ has_command = False
37
+ for i, line in enumerate(lines):
38
+ line_text = render_at_pattern(line)
39
+
40
+ if i == 0 and line.startswith("/"):
41
+ splits = line.split(" ", maxsplit=1)
42
+ if is_slash_command_name(splits[0][1:]):
43
+ has_command = True
44
+ line_text = Text.assemble(
45
+ (f"{splits[0]}", ThemeKey.USER_INPUT_SLASH_COMMAND),
46
+ " ",
47
+ render_at_pattern(splits[1]) if len(splits) > 1 else Text(""),
48
+ )
49
+ renderables.append(line_text)
50
+ continue
51
+
52
+ renderables.append(line_text)
53
+ grid = create_grid()
54
+ grid.padding = (0, 0)
55
+ mark = (
56
+ Text("❯ ", style=ThemeKey.USER_INPUT_PROMPT)
57
+ if not has_command
58
+ else Text(" ", style=ThemeKey.USER_INPUT_SLASH_COMMAND)
59
+ )
60
+ grid.add_row(mark, Group(*renderables))
61
+ return grid
62
+
63
+
64
+ def render_interrupt() -> RenderableType:
65
+ return Text(" INTERRUPTED \n", style=ThemeKey.INTERRUPT)
@@ -0,0 +1 @@
1
+ # Rich rendering utilities
@@ -0,0 +1,65 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from rich._loop import loop_last
6
+ from rich.console import Console, ConsoleOptions, RenderableType, RenderResult
7
+ from rich.live import Live
8
+ from rich.segment import Segment
9
+
10
+
11
+ class CropAbove:
12
+ def __init__(self, renderable: RenderableType, style: str = "") -> None:
13
+ self.renderable = renderable
14
+ self.style = style
15
+
16
+ def __rich_console__(self, console: Console, options: ConsoleOptions) -> RenderResult:
17
+ style = console.get_style(self.style) if self.style else None
18
+ lines = console.render_lines(self.renderable, options, style=style, pad=False)
19
+ max_height = options.size.height
20
+ if len(lines) > max_height:
21
+ lines = lines[-max_height:]
22
+
23
+ new_line = Segment.line()
24
+ for last, line in loop_last(lines):
25
+ yield from line
26
+ if not last:
27
+ yield new_line
28
+
29
+
30
+ class CropAboveLive(Live):
31
+ def __init__(
32
+ self,
33
+ renderable: RenderableType | None = None,
34
+ *,
35
+ console: Console | None = None,
36
+ refresh_per_second: float = 4,
37
+ transient: bool = False,
38
+ get_renderable: Any | None = None,
39
+ style: str = "",
40
+ **kwargs: Any,
41
+ ) -> None:
42
+ self._crop_style: str = style
43
+
44
+ if get_renderable is not None:
45
+
46
+ def _wrapped_get() -> RenderableType:
47
+ assert get_renderable is not None
48
+ return CropAbove(get_renderable(), style=self._crop_style)
49
+
50
+ get_renderable = _wrapped_get
51
+
52
+ if renderable is not None:
53
+ renderable = CropAbove(renderable, style=self._crop_style)
54
+
55
+ super().__init__(
56
+ renderable,
57
+ console=console,
58
+ refresh_per_second=refresh_per_second,
59
+ transient=transient,
60
+ get_renderable=get_renderable,
61
+ **kwargs,
62
+ )
63
+
64
+ def update(self, renderable: RenderableType, refresh: bool = True) -> None: # type: ignore[override]
65
+ super().update(CropAbove(renderable, style=self._crop_style), refresh=refresh)