klaude-code 1.2.1__py3-none-any.whl → 1.2.3__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.
- klaude_code/cli/main.py +9 -4
- klaude_code/cli/runtime.py +42 -43
- klaude_code/command/__init__.py +7 -5
- klaude_code/command/clear_cmd.py +6 -29
- klaude_code/command/command_abc.py +44 -8
- klaude_code/command/diff_cmd.py +33 -27
- klaude_code/command/export_cmd.py +18 -26
- klaude_code/command/help_cmd.py +10 -8
- klaude_code/command/model_cmd.py +11 -40
- klaude_code/command/{prompt-update-dev-doc.md → prompt-dev-docs-update.md} +3 -2
- klaude_code/command/{prompt-dev-doc.md → prompt-dev-docs.md} +3 -2
- klaude_code/command/prompt-init.md +2 -5
- klaude_code/command/prompt_command.py +6 -6
- klaude_code/command/refresh_cmd.py +4 -5
- klaude_code/command/registry.py +16 -19
- klaude_code/command/terminal_setup_cmd.py +12 -11
- klaude_code/config/__init__.py +4 -0
- klaude_code/config/config.py +25 -26
- klaude_code/config/list_model.py +8 -3
- klaude_code/config/select_model.py +1 -1
- klaude_code/const/__init__.py +1 -1
- klaude_code/core/__init__.py +0 -3
- klaude_code/core/agent.py +25 -50
- klaude_code/core/executor.py +268 -101
- klaude_code/core/prompt.py +12 -12
- klaude_code/core/{prompt → prompts}/prompt-gemini.md +1 -1
- klaude_code/core/reminders.py +76 -95
- klaude_code/core/task.py +21 -14
- klaude_code/core/tool/__init__.py +45 -11
- klaude_code/core/tool/file/apply_patch.py +5 -1
- klaude_code/core/tool/file/apply_patch_tool.py +11 -13
- klaude_code/core/tool/file/edit_tool.py +27 -23
- klaude_code/core/tool/file/multi_edit_tool.py +15 -17
- klaude_code/core/tool/file/read_tool.py +41 -36
- klaude_code/core/tool/file/write_tool.py +13 -15
- klaude_code/core/tool/memory/memory_tool.py +85 -68
- klaude_code/core/tool/memory/skill_tool.py +10 -12
- klaude_code/core/tool/shell/bash_tool.py +24 -22
- klaude_code/core/tool/shell/command_safety.py +12 -1
- klaude_code/core/tool/sub_agent_tool.py +11 -12
- klaude_code/core/tool/todo/todo_write_tool.py +21 -28
- klaude_code/core/tool/todo/update_plan_tool.py +14 -24
- klaude_code/core/tool/tool_abc.py +3 -4
- klaude_code/core/tool/tool_context.py +7 -7
- klaude_code/core/tool/tool_registry.py +30 -47
- klaude_code/core/tool/tool_runner.py +35 -43
- klaude_code/core/tool/truncation.py +14 -20
- klaude_code/core/tool/web/mermaid_tool.py +12 -14
- klaude_code/core/tool/web/web_fetch_tool.py +15 -17
- klaude_code/core/turn.py +19 -7
- klaude_code/llm/__init__.py +3 -4
- klaude_code/llm/anthropic/client.py +30 -46
- klaude_code/llm/anthropic/input.py +4 -11
- klaude_code/llm/client.py +29 -8
- klaude_code/llm/input_common.py +66 -36
- klaude_code/llm/openai_compatible/client.py +42 -84
- klaude_code/llm/openai_compatible/input.py +11 -16
- klaude_code/llm/openai_compatible/tool_call_accumulator.py +2 -2
- klaude_code/llm/openrouter/client.py +40 -289
- klaude_code/llm/openrouter/input.py +13 -35
- klaude_code/llm/openrouter/reasoning_handler.py +209 -0
- klaude_code/llm/registry.py +5 -75
- klaude_code/llm/responses/client.py +34 -55
- klaude_code/llm/responses/input.py +24 -26
- klaude_code/llm/usage.py +109 -0
- klaude_code/protocol/__init__.py +4 -0
- klaude_code/protocol/events.py +3 -2
- klaude_code/protocol/{llm_parameter.py → llm_param.py} +12 -32
- klaude_code/protocol/model.py +49 -4
- klaude_code/protocol/op.py +18 -16
- klaude_code/protocol/op_handler.py +28 -0
- klaude_code/{core → protocol}/sub_agent.py +7 -0
- klaude_code/session/export.py +150 -70
- klaude_code/session/session.py +28 -14
- klaude_code/session/templates/export_session.html +180 -42
- klaude_code/trace/__init__.py +2 -2
- klaude_code/trace/log.py +11 -5
- klaude_code/ui/__init__.py +91 -8
- klaude_code/ui/core/__init__.py +1 -0
- klaude_code/ui/core/display.py +103 -0
- klaude_code/ui/core/input.py +71 -0
- klaude_code/ui/modes/__init__.py +1 -0
- klaude_code/ui/modes/debug/__init__.py +1 -0
- klaude_code/ui/{base/debug_event_display.py → modes/debug/display.py} +9 -5
- klaude_code/ui/modes/exec/__init__.py +1 -0
- klaude_code/ui/{base/exec_display.py → modes/exec/display.py} +28 -2
- klaude_code/ui/{repl → modes/repl}/__init__.py +5 -6
- klaude_code/ui/modes/repl/clipboard.py +152 -0
- klaude_code/ui/modes/repl/completers.py +429 -0
- klaude_code/ui/modes/repl/display.py +60 -0
- klaude_code/ui/modes/repl/event_handler.py +375 -0
- klaude_code/ui/modes/repl/input_prompt_toolkit.py +198 -0
- klaude_code/ui/modes/repl/key_bindings.py +170 -0
- klaude_code/ui/{repl → modes/repl}/renderer.py +109 -132
- klaude_code/ui/renderers/assistant.py +21 -0
- klaude_code/ui/renderers/common.py +0 -16
- klaude_code/ui/renderers/developer.py +18 -18
- klaude_code/ui/renderers/diffs.py +36 -14
- klaude_code/ui/renderers/errors.py +1 -1
- klaude_code/ui/renderers/metadata.py +50 -27
- klaude_code/ui/renderers/sub_agent.py +43 -9
- klaude_code/ui/renderers/thinking.py +33 -1
- klaude_code/ui/renderers/tools.py +212 -20
- klaude_code/ui/renderers/user_input.py +19 -23
- klaude_code/ui/rich/__init__.py +1 -0
- klaude_code/ui/{rich_ext → rich}/searchable_text.py +3 -1
- klaude_code/ui/{renderers → rich}/status.py +29 -18
- klaude_code/ui/{base → rich}/theme.py +8 -2
- klaude_code/ui/terminal/__init__.py +1 -0
- klaude_code/ui/{base/terminal_color.py → terminal/color.py} +4 -1
- klaude_code/ui/{base/terminal_control.py → terminal/control.py} +1 -0
- klaude_code/ui/{base/terminal_notifier.py → terminal/notifier.py} +5 -2
- klaude_code/ui/utils/__init__.py +1 -0
- klaude_code/ui/{base/utils.py → utils/common.py} +35 -3
- {klaude_code-1.2.1.dist-info → klaude_code-1.2.3.dist-info}/METADATA +1 -1
- klaude_code-1.2.3.dist-info/RECORD +161 -0
- klaude_code/core/clipboard_manifest.py +0 -124
- klaude_code/llm/openrouter/tool_call_accumulator.py +0 -80
- klaude_code/ui/base/__init__.py +0 -1
- klaude_code/ui/base/display_abc.py +0 -36
- klaude_code/ui/base/input_abc.py +0 -20
- klaude_code/ui/repl/display.py +0 -36
- klaude_code/ui/repl/event_handler.py +0 -247
- klaude_code/ui/repl/input.py +0 -773
- klaude_code/ui/rich_ext/__init__.py +0 -1
- klaude_code-1.2.1.dist-info/RECORD +0 -151
- /klaude_code/core/{prompt → prompts}/prompt-claude-code.md +0 -0
- /klaude_code/core/{prompt → prompts}/prompt-codex.md +0 -0
- /klaude_code/core/{prompt → prompts}/prompt-subagent-explore.md +0 -0
- /klaude_code/core/{prompt → prompts}/prompt-subagent-oracle.md +0 -0
- /klaude_code/core/{prompt → prompts}/prompt-subagent-webfetch.md +0 -0
- /klaude_code/core/{prompt → prompts}/prompt-subagent.md +0 -0
- /klaude_code/ui/{base → core}/stage_manager.py +0 -0
- /klaude_code/ui/{rich_ext → rich}/live.py +0 -0
- /klaude_code/ui/{rich_ext → rich}/markdown.py +0 -0
- /klaude_code/ui/{rich_ext → rich}/quote.py +0 -0
- /klaude_code/ui/{base → terminal}/progress_bar.py +0 -0
- /klaude_code/ui/{base → utils}/debouncer.py +0 -0
- {klaude_code-1.2.1.dist-info → klaude_code-1.2.3.dist-info}/WHEEL +0 -0
- {klaude_code-1.2.1.dist-info → klaude_code-1.2.3.dist-info}/entry_points.txt +0 -0
|
@@ -5,11 +5,13 @@ from rich.console import RenderableType
|
|
|
5
5
|
from rich.padding import Padding
|
|
6
6
|
from rich.text import Text
|
|
7
7
|
|
|
8
|
-
from klaude_code
|
|
9
|
-
from klaude_code.core.sub_agent import is_sub_agent_tool as _is_sub_agent_tool
|
|
8
|
+
from klaude_code import const
|
|
10
9
|
from klaude_code.protocol import events, model
|
|
11
|
-
from klaude_code.
|
|
12
|
-
from klaude_code.ui.renderers
|
|
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
|
|
13
15
|
|
|
14
16
|
|
|
15
17
|
def is_sub_agent_tool(tool_name: str) -> bool:
|
|
@@ -43,9 +45,15 @@ def render_generic_tool_call(tool_name: str, arguments: str, markup: str = "•"
|
|
|
43
45
|
elif len(json_dict) == 1:
|
|
44
46
|
arguments_column = Text(str(next(iter(json_dict.values()))), ThemeKey.TOOL_PARAM)
|
|
45
47
|
else:
|
|
46
|
-
arguments_column = Text(
|
|
48
|
+
arguments_column = Text(
|
|
49
|
+
", ".join([f"{k}: {v}" for k, v in json_dict.items()]),
|
|
50
|
+
ThemeKey.TOOL_PARAM,
|
|
51
|
+
)
|
|
47
52
|
except json.JSONDecodeError:
|
|
48
|
-
arguments_column = Text(
|
|
53
|
+
arguments_column = Text(
|
|
54
|
+
arguments.strip()[: const.INVALID_TOOL_CALL_MAX_LENGTH],
|
|
55
|
+
style=ThemeKey.INVALID_TOOL_CALL_ARGS,
|
|
56
|
+
)
|
|
49
57
|
grid.add_row(tool_name_column, arguments_column)
|
|
50
58
|
return grid
|
|
51
59
|
|
|
@@ -60,7 +68,8 @@ def render_update_plan_tool_call(arguments: str) -> RenderableType:
|
|
|
60
68
|
payload = json.loads(arguments)
|
|
61
69
|
except json.JSONDecodeError:
|
|
62
70
|
explanation_column = Text(
|
|
63
|
-
arguments.strip()[:INVALID_TOOL_CALL_MAX_LENGTH],
|
|
71
|
+
arguments.strip()[: const.INVALID_TOOL_CALL_MAX_LENGTH],
|
|
72
|
+
style=ThemeKey.INVALID_TOOL_CALL_ARGS,
|
|
64
73
|
)
|
|
65
74
|
else:
|
|
66
75
|
explanation = payload.get("explanation")
|
|
@@ -103,7 +112,10 @@ def render_read_tool_call(arguments: str) -> RenderableType:
|
|
|
103
112
|
)
|
|
104
113
|
except json.JSONDecodeError:
|
|
105
114
|
render_result = render_result.append_text(
|
|
106
|
-
Text(
|
|
115
|
+
Text(
|
|
116
|
+
arguments.strip()[: const.INVALID_TOOL_CALL_MAX_LENGTH],
|
|
117
|
+
style=ThemeKey.INVALID_TOOL_CALL_ARGS,
|
|
118
|
+
)
|
|
107
119
|
)
|
|
108
120
|
grid.add_row(Text("←", ThemeKey.TOOL_MARK), render_result)
|
|
109
121
|
return grid
|
|
@@ -123,7 +135,12 @@ def render_edit_tool_call(arguments: str) -> Text:
|
|
|
123
135
|
render_result = (
|
|
124
136
|
render_result.append_text(Text("Edit", ThemeKey.TOOL_NAME))
|
|
125
137
|
.append_text(Text(" "))
|
|
126
|
-
.append_text(
|
|
138
|
+
.append_text(
|
|
139
|
+
Text(
|
|
140
|
+
arguments.strip()[: const.INVALID_TOOL_CALL_MAX_LENGTH],
|
|
141
|
+
style=ThemeKey.INVALID_TOOL_CALL_ARGS,
|
|
142
|
+
)
|
|
143
|
+
)
|
|
127
144
|
)
|
|
128
145
|
return render_result
|
|
129
146
|
|
|
@@ -149,7 +166,12 @@ def render_write_tool_call(arguments: str) -> Text:
|
|
|
149
166
|
render_result = (
|
|
150
167
|
render_result.append_text(Text("Write", ThemeKey.TOOL_NAME))
|
|
151
168
|
.append_text(Text(" "))
|
|
152
|
-
.append_text(
|
|
169
|
+
.append_text(
|
|
170
|
+
Text(
|
|
171
|
+
arguments.strip()[: const.INVALID_TOOL_CALL_MAX_LENGTH],
|
|
172
|
+
style=ThemeKey.INVALID_TOOL_CALL_ARGS,
|
|
173
|
+
)
|
|
174
|
+
)
|
|
153
175
|
)
|
|
154
176
|
return render_result
|
|
155
177
|
|
|
@@ -168,7 +190,10 @@ def render_multi_edit_tool_call(arguments: str) -> Text:
|
|
|
168
190
|
)
|
|
169
191
|
except json.JSONDecodeError:
|
|
170
192
|
render_result = render_result.append_text(
|
|
171
|
-
Text(
|
|
193
|
+
Text(
|
|
194
|
+
arguments.strip()[: const.INVALID_TOOL_CALL_MAX_LENGTH],
|
|
195
|
+
style=ThemeKey.INVALID_TOOL_CALL_ARGS,
|
|
196
|
+
)
|
|
172
197
|
)
|
|
173
198
|
return render_result
|
|
174
199
|
|
|
@@ -181,7 +206,10 @@ def render_apply_patch_tool_call(arguments: str) -> RenderableType:
|
|
|
181
206
|
("→ ", ThemeKey.TOOL_MARK),
|
|
182
207
|
("Apply Patch", ThemeKey.TOOL_NAME),
|
|
183
208
|
" ",
|
|
184
|
-
Text(
|
|
209
|
+
Text(
|
|
210
|
+
arguments.strip()[: const.INVALID_TOOL_CALL_MAX_LENGTH],
|
|
211
|
+
style=ThemeKey.INVALID_TOOL_CALL_ARGS,
|
|
212
|
+
),
|
|
185
213
|
)
|
|
186
214
|
|
|
187
215
|
patch_content = payload.get("patch", "")
|
|
@@ -193,9 +221,12 @@ def render_apply_patch_tool_call(arguments: str) -> RenderableType:
|
|
|
193
221
|
if isinstance(patch_content, str):
|
|
194
222
|
lines = [line for line in patch_content.splitlines() if line and not line.startswith("*** Begin Patch")]
|
|
195
223
|
if lines:
|
|
196
|
-
summary = Text(lines[0][:INVALID_TOOL_CALL_MAX_LENGTH], ThemeKey.TOOL_PARAM)
|
|
224
|
+
summary = Text(lines[0][: const.INVALID_TOOL_CALL_MAX_LENGTH], ThemeKey.TOOL_PARAM)
|
|
197
225
|
else:
|
|
198
|
-
summary = Text(
|
|
226
|
+
summary = Text(
|
|
227
|
+
str(patch_content)[: const.INVALID_TOOL_CALL_MAX_LENGTH],
|
|
228
|
+
ThemeKey.INVALID_TOOL_CALL_ARGS,
|
|
229
|
+
)
|
|
199
230
|
|
|
200
231
|
if summary.plain:
|
|
201
232
|
grid.add_row(header, summary)
|
|
@@ -207,9 +238,17 @@ def render_apply_patch_tool_call(arguments: str) -> RenderableType:
|
|
|
207
238
|
|
|
208
239
|
def render_todo(tr: events.ToolResultEvent) -> RenderableType:
|
|
209
240
|
if tr.ui_extra is None:
|
|
210
|
-
return Text.assemble(
|
|
241
|
+
return Text.assemble(
|
|
242
|
+
(" ✘", ThemeKey.ERROR_BOLD),
|
|
243
|
+
" ",
|
|
244
|
+
Text("(no content)", style=ThemeKey.ERROR),
|
|
245
|
+
)
|
|
211
246
|
if tr.ui_extra.type != model.ToolResultUIExtraType.TODO_LIST or tr.ui_extra.todo_list is None:
|
|
212
|
-
return Text.assemble(
|
|
247
|
+
return Text.assemble(
|
|
248
|
+
(" ✘", ThemeKey.ERROR_BOLD),
|
|
249
|
+
" ",
|
|
250
|
+
Text("(invalid ui_extra)", style=ThemeKey.ERROR),
|
|
251
|
+
)
|
|
213
252
|
|
|
214
253
|
ui_extra = tr.ui_extra.todo_list
|
|
215
254
|
todo_grid = create_grid()
|
|
@@ -241,7 +280,9 @@ def render_generic_tool_result(result: str, *, is_error: bool = False) -> Render
|
|
|
241
280
|
return Padding.indent(Text(truncate_display(result), style=style), level=2)
|
|
242
281
|
|
|
243
282
|
|
|
244
|
-
def _extract_mermaid_link(
|
|
283
|
+
def _extract_mermaid_link(
|
|
284
|
+
ui_extra: model.ToolResultUIExtra | None,
|
|
285
|
+
) -> model.MermaidLinkUIExtra | None:
|
|
245
286
|
if ui_extra is None:
|
|
246
287
|
return None
|
|
247
288
|
if ui_extra.type != model.ToolResultUIExtraType.MERMAID_LINK:
|
|
@@ -264,7 +305,10 @@ def render_memory_tool_call(arguments: str) -> RenderableType:
|
|
|
264
305
|
payload: dict[str, str] = json.loads(arguments)
|
|
265
306
|
except json.JSONDecodeError:
|
|
266
307
|
tool_name_column = Text.assemble(("★", ThemeKey.TOOL_MARK), " ", ("Memory", ThemeKey.TOOL_NAME))
|
|
267
|
-
summary = Text(
|
|
308
|
+
summary = Text(
|
|
309
|
+
arguments.strip()[: const.INVALID_TOOL_CALL_MAX_LENGTH],
|
|
310
|
+
style=ThemeKey.INVALID_TOOL_CALL_ARGS,
|
|
311
|
+
)
|
|
268
312
|
grid.add_row(tool_name_column, summary)
|
|
269
313
|
return grid
|
|
270
314
|
|
|
@@ -308,7 +352,10 @@ def render_mermaid_tool_call(arguments: str) -> RenderableType:
|
|
|
308
352
|
try:
|
|
309
353
|
payload: dict[str, str] = json.loads(arguments)
|
|
310
354
|
except json.JSONDecodeError:
|
|
311
|
-
summary = Text(
|
|
355
|
+
summary = Text(
|
|
356
|
+
arguments.strip()[: const.INVALID_TOOL_CALL_MAX_LENGTH],
|
|
357
|
+
style=ThemeKey.INVALID_TOOL_CALL_ARGS,
|
|
358
|
+
)
|
|
312
359
|
else:
|
|
313
360
|
code = payload.get("code", "")
|
|
314
361
|
if code:
|
|
@@ -330,7 +377,9 @@ def render_mermaid_tool_result(tr: events.ToolResultEvent) -> RenderableType:
|
|
|
330
377
|
return Padding.indent(link_text, level=2)
|
|
331
378
|
|
|
332
379
|
|
|
333
|
-
def _extract_truncation(
|
|
380
|
+
def _extract_truncation(
|
|
381
|
+
ui_extra: model.ToolResultUIExtra | None,
|
|
382
|
+
) -> model.TruncationUIExtra | None:
|
|
334
383
|
if ui_extra is None:
|
|
335
384
|
return None
|
|
336
385
|
if ui_extra.type != model.ToolResultUIExtraType.TRUNCATION:
|
|
@@ -357,3 +406,146 @@ def render_truncation_info(ui_extra: model.TruncationUIExtra) -> RenderableType:
|
|
|
357
406
|
def get_truncation_info(tr: events.ToolResultEvent) -> model.TruncationUIExtra | None:
|
|
358
407
|
"""Extract truncation info from a tool result event."""
|
|
359
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)
|
|
@@ -3,9 +3,9 @@ import re
|
|
|
3
3
|
from rich.console import Group, RenderableType
|
|
4
4
|
from rich.text import Text
|
|
5
5
|
|
|
6
|
-
from klaude_code.
|
|
7
|
-
from klaude_code.ui.base.theme import ThemeKey
|
|
6
|
+
from klaude_code.command import is_slash_command_name
|
|
8
7
|
from klaude_code.ui.renderers.common import create_grid
|
|
8
|
+
from klaude_code.ui.rich.theme import ThemeKey
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
def render_at_pattern(
|
|
@@ -25,14 +25,6 @@ def render_at_pattern(
|
|
|
25
25
|
return Text(text, style=other_style)
|
|
26
26
|
|
|
27
27
|
|
|
28
|
-
def _is_valid_slash_command(command: str) -> bool:
|
|
29
|
-
try:
|
|
30
|
-
CommandName(command)
|
|
31
|
-
return True
|
|
32
|
-
except ValueError:
|
|
33
|
-
return False
|
|
34
|
-
|
|
35
|
-
|
|
36
28
|
def render_user_input(content: str) -> RenderableType:
|
|
37
29
|
"""Render a user message as a group of quoted lines with styles.
|
|
38
30
|
|
|
@@ -41,27 +33,31 @@ def render_user_input(content: str) -> RenderableType:
|
|
|
41
33
|
"""
|
|
42
34
|
lines = content.strip().split("\n")
|
|
43
35
|
renderables: list[RenderableType] = []
|
|
36
|
+
has_command = False
|
|
44
37
|
for i, line in enumerate(lines):
|
|
45
38
|
line_text = render_at_pattern(line)
|
|
46
39
|
|
|
47
40
|
if i == 0 and line.startswith("/"):
|
|
48
41
|
splits = line.split(" ", maxsplit=1)
|
|
49
|
-
if
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
)
|
|
59
|
-
renderables.append(line_text)
|
|
60
|
-
continue
|
|
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
|
|
61
51
|
|
|
62
52
|
renderables.append(line_text)
|
|
63
53
|
grid = create_grid()
|
|
64
|
-
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))
|
|
65
61
|
return grid
|
|
66
62
|
|
|
67
63
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Rich rendering utilities
|
|
@@ -23,7 +23,9 @@ class SearchableFormattedText:
|
|
|
23
23
|
self._plain = plain
|
|
24
24
|
|
|
25
25
|
# Recognized by prompt_toolkit's to_formatted_text(value)
|
|
26
|
-
def __pt_formatted_text__(
|
|
26
|
+
def __pt_formatted_text__(
|
|
27
|
+
self,
|
|
28
|
+
) -> Iterable[Tuple[str, str]]: # pragma: no cover - passthrough
|
|
27
29
|
return self._fragments
|
|
28
30
|
|
|
29
31
|
# Provide a human-readable representation.
|
|
@@ -13,8 +13,8 @@ from rich.table import Table
|
|
|
13
13
|
from rich.text import Text
|
|
14
14
|
|
|
15
15
|
from klaude_code import const
|
|
16
|
-
from klaude_code.ui.
|
|
17
|
-
from klaude_code.ui.
|
|
16
|
+
from klaude_code.ui.rich.theme import ThemeKey
|
|
17
|
+
from klaude_code.ui.terminal.color import get_last_terminal_background_rgb
|
|
18
18
|
|
|
19
19
|
BREATHING_SPINNER_NAME = "dot"
|
|
20
20
|
|
|
@@ -52,10 +52,21 @@ def _shimmer_profile(main_text: str) -> list[tuple[str, float]]:
|
|
|
52
52
|
return []
|
|
53
53
|
|
|
54
54
|
padding = const.STATUS_SHIMMER_PADDING
|
|
55
|
-
|
|
55
|
+
char_count = len(chars)
|
|
56
|
+
period = char_count + padding * 2
|
|
57
|
+
|
|
58
|
+
# Keep a roughly constant shimmer speed (characters per second)
|
|
59
|
+
# regardless of text length by deriving a character velocity from a
|
|
60
|
+
# baseline text length and the configured sweep duration.
|
|
61
|
+
# The baseline is chosen to be close to the default
|
|
62
|
+
# "Thinking … (esc to interrupt)" status line.
|
|
63
|
+
baseline_chars = 30
|
|
64
|
+
base_period = baseline_chars + padding * 2
|
|
56
65
|
sweep_seconds = const.STATUS_SHIMMER_SWEEP_SECONDS
|
|
66
|
+
char_speed = base_period / sweep_seconds if sweep_seconds > 0 else base_period
|
|
67
|
+
|
|
57
68
|
elapsed = _elapsed_since_start()
|
|
58
|
-
pos_f = (elapsed
|
|
69
|
+
pos_f = (elapsed * char_speed) % float(period)
|
|
59
70
|
pos = int(pos_f)
|
|
60
71
|
band_half_width = const.STATUS_SHIMMER_BAND_HALF_WIDTH
|
|
61
72
|
|
|
@@ -144,37 +155,37 @@ def _breathing_style(console: Console, base_style: Style, intensity: float) -> S
|
|
|
144
155
|
class ShimmerStatusText:
|
|
145
156
|
"""Renderable status line with shimmer effect on the main text and hint."""
|
|
146
157
|
|
|
147
|
-
def __init__(self, main_text: str, main_style: ThemeKey) -> None:
|
|
148
|
-
self._main_text = main_text
|
|
158
|
+
def __init__(self, main_text: str | Text, main_style: ThemeKey) -> None:
|
|
159
|
+
self._main_text = main_text if isinstance(main_text, Text) else Text(main_text)
|
|
149
160
|
self._main_style = main_style
|
|
150
|
-
self._hint_text = " (esc to interrupt)"
|
|
161
|
+
self._hint_text = Text(" (esc to interrupt)")
|
|
151
162
|
self._hint_style = ThemeKey.STATUS_HINT
|
|
152
163
|
|
|
153
164
|
def __rich_console__(self, console: Console, options: ConsoleOptions) -> RenderResult:
|
|
154
|
-
|
|
165
|
+
result = Text()
|
|
155
166
|
main_style = console.get_style(str(self._main_style))
|
|
156
167
|
hint_style = console.get_style(str(self._hint_style))
|
|
157
168
|
|
|
158
|
-
combined_text =
|
|
159
|
-
split_index = len(self._main_text)
|
|
169
|
+
combined_text = self._main_text.plain + self._hint_text.plain
|
|
170
|
+
split_index = len(self._main_text.plain)
|
|
160
171
|
|
|
161
172
|
for index, (ch, intensity) in enumerate(_shimmer_profile(combined_text)):
|
|
162
|
-
|
|
173
|
+
if index < split_index:
|
|
174
|
+
# Get style from main_text, merge with main_style
|
|
175
|
+
char_style = self._main_text.get_style_at_offset(console, index)
|
|
176
|
+
base_style = main_style + char_style
|
|
177
|
+
else:
|
|
178
|
+
base_style = hint_style
|
|
163
179
|
style = _shimmer_style(console, base_style, intensity)
|
|
164
|
-
|
|
180
|
+
result.append(ch, style=style)
|
|
165
181
|
|
|
166
|
-
yield
|
|
182
|
+
yield result
|
|
167
183
|
|
|
168
184
|
|
|
169
185
|
def spinner_name() -> str:
|
|
170
186
|
return BREATHING_SPINNER_NAME
|
|
171
187
|
|
|
172
188
|
|
|
173
|
-
def render_status_text(main_text: str, main_style: ThemeKey) -> RenderableType:
|
|
174
|
-
"""Create animated status text with shimmer main text and hint suffix."""
|
|
175
|
-
return ShimmerStatusText(main_text, main_style)
|
|
176
|
-
|
|
177
|
-
|
|
178
189
|
class BreathingSpinner(RichSpinner):
|
|
179
190
|
"""Custom spinner that animates color instead of glyphs.
|
|
180
191
|
|
|
@@ -124,6 +124,9 @@ class ThemeKey(str, Enum):
|
|
|
124
124
|
WELCOME_HIGHLIGHT_BOLD = "welcome.highlight.bold"
|
|
125
125
|
WELCOME_HIGHLIGHT = "welcome.highlight"
|
|
126
126
|
WELCOME_INFO = "welcome.info"
|
|
127
|
+
# WELCOME DEBUG
|
|
128
|
+
WELCOME_DEBUG_TITLE = "welcome.debug.title"
|
|
129
|
+
WELCOME_DEBUG_BORDER = "welcome.debug.border"
|
|
127
130
|
# RESUME
|
|
128
131
|
RESUME_FLAG = "resume.flag"
|
|
129
132
|
RESUME_INFO = "resume.info"
|
|
@@ -177,8 +180,8 @@ def get_theme(theme: str | None = None) -> Themes:
|
|
|
177
180
|
ThemeKey.METADATA_DIM.value: "dim " + palette.grey_blue,
|
|
178
181
|
ThemeKey.METADATA_BOLD.value: "bold " + palette.grey_blue,
|
|
179
182
|
# SPINNER_STATUS
|
|
180
|
-
ThemeKey.SPINNER_STATUS.value: palette.
|
|
181
|
-
ThemeKey.SPINNER_STATUS_TEXT.value: palette.
|
|
183
|
+
ThemeKey.SPINNER_STATUS.value: palette.blue,
|
|
184
|
+
ThemeKey.SPINNER_STATUS_TEXT.value: palette.blue,
|
|
182
185
|
# STATUS
|
|
183
186
|
ThemeKey.STATUS_HINT.value: palette.grey2,
|
|
184
187
|
# REMINDER
|
|
@@ -212,6 +215,9 @@ def get_theme(theme: str | None = None) -> Themes:
|
|
|
212
215
|
ThemeKey.WELCOME_HIGHLIGHT_BOLD.value: "bold",
|
|
213
216
|
ThemeKey.WELCOME_HIGHLIGHT.value: palette.blue,
|
|
214
217
|
ThemeKey.WELCOME_INFO.value: palette.grey1,
|
|
218
|
+
# WELCOME DEBUG
|
|
219
|
+
ThemeKey.WELCOME_DEBUG_TITLE.value: "bold " + palette.red,
|
|
220
|
+
ThemeKey.WELCOME_DEBUG_BORDER.value: palette.red,
|
|
215
221
|
# RESUME
|
|
216
222
|
ThemeKey.RESUME_FLAG.value: "bold reverse " + palette.green,
|
|
217
223
|
ThemeKey.RESUME_INFO.value: palette.green,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Terminal utilities
|
|
@@ -99,7 +99,10 @@ def _query_color_slot(slot: int, timeout: float) -> tuple[int, int, int] | None:
|
|
|
99
99
|
)
|
|
100
100
|
|
|
101
101
|
except OSError as exc:
|
|
102
|
-
log_debug(
|
|
102
|
+
log_debug(
|
|
103
|
+
f"Failed to open /dev/tty for OSC color query: {exc}",
|
|
104
|
+
debug_type=DebugType.TERMINAL,
|
|
105
|
+
)
|
|
103
106
|
return None
|
|
104
107
|
|
|
105
108
|
if raw is None or not raw:
|
|
@@ -52,7 +52,10 @@ class TerminalNotifier:
|
|
|
52
52
|
|
|
53
53
|
def notify(self, notification: Notification) -> bool:
|
|
54
54
|
if not self.config.enabled:
|
|
55
|
-
log_debug(
|
|
55
|
+
log_debug(
|
|
56
|
+
"Terminal notifier skipped: disabled via config",
|
|
57
|
+
debug_type=DebugType.TERMINAL,
|
|
58
|
+
)
|
|
56
59
|
return False
|
|
57
60
|
|
|
58
61
|
output = resolve_stream(self.config.stream)
|
|
@@ -100,5 +103,5 @@ class TerminalNotifier:
|
|
|
100
103
|
def _compact(text: str, limit: int = 160) -> str:
|
|
101
104
|
squashed = " ".join(text.split())
|
|
102
105
|
if len(squashed) > limit:
|
|
103
|
-
return squashed[: limit - 3] + "
|
|
106
|
+
return squashed[: limit - 3] + "…"
|
|
104
107
|
return squashed
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# UI utilities
|
|
@@ -2,6 +2,8 @@ import re
|
|
|
2
2
|
import subprocess
|
|
3
3
|
from pathlib import Path
|
|
4
4
|
|
|
5
|
+
from klaude_code import const
|
|
6
|
+
|
|
5
7
|
LEADING_NEWLINES_REGEX = re.compile(r"^\n{2,}")
|
|
6
8
|
|
|
7
9
|
|
|
@@ -36,14 +38,24 @@ def get_current_git_branch(path: Path | None = None) -> str | None:
|
|
|
36
38
|
|
|
37
39
|
try:
|
|
38
40
|
# Check if in git repository
|
|
39
|
-
git_dir = subprocess.run(
|
|
41
|
+
git_dir = subprocess.run(
|
|
42
|
+
["git", "rev-parse", "--git-dir"],
|
|
43
|
+
cwd=path,
|
|
44
|
+
capture_output=True,
|
|
45
|
+
text=True,
|
|
46
|
+
timeout=2,
|
|
47
|
+
)
|
|
40
48
|
|
|
41
49
|
if git_dir.returncode != 0:
|
|
42
50
|
return None
|
|
43
51
|
|
|
44
52
|
# Get current branch name
|
|
45
53
|
result = subprocess.run(
|
|
46
|
-
["git", "branch", "--show-current"],
|
|
54
|
+
["git", "branch", "--show-current"],
|
|
55
|
+
cwd=path,
|
|
56
|
+
capture_output=True,
|
|
57
|
+
text=True,
|
|
58
|
+
timeout=2,
|
|
47
59
|
)
|
|
48
60
|
|
|
49
61
|
if result.returncode == 0:
|
|
@@ -52,7 +64,11 @@ def get_current_git_branch(path: Path | None = None) -> str | None:
|
|
|
52
64
|
|
|
53
65
|
# Fallback: get HEAD reference
|
|
54
66
|
head_file = subprocess.run(
|
|
55
|
-
["git", "rev-parse", "--abbrev-ref", "HEAD"],
|
|
67
|
+
["git", "rev-parse", "--abbrev-ref", "HEAD"],
|
|
68
|
+
cwd=path,
|
|
69
|
+
capture_output=True,
|
|
70
|
+
text=True,
|
|
71
|
+
timeout=2,
|
|
56
72
|
)
|
|
57
73
|
|
|
58
74
|
if head_file.returncode == 0:
|
|
@@ -74,3 +90,19 @@ def show_path_with_tilde(path: Path | None = None):
|
|
|
74
90
|
return f"~/{relative_path}"
|
|
75
91
|
except ValueError:
|
|
76
92
|
return str(path)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def truncate_display(
|
|
96
|
+
text: str,
|
|
97
|
+
max_lines: int = const.TRUNCATE_DISPLAY_MAX_LINES,
|
|
98
|
+
max_line_length: int = const.TRUNCATE_DISPLAY_MAX_LINE_LENGTH,
|
|
99
|
+
) -> str:
|
|
100
|
+
lines = text.split("\n")
|
|
101
|
+
if len(lines) > max_lines:
|
|
102
|
+
lines = lines[:max_lines] + ["… (more " + str(len(lines) - max_lines) + " lines)"]
|
|
103
|
+
for i, line in enumerate(lines):
|
|
104
|
+
if len(line) > max_line_length:
|
|
105
|
+
lines[i] = (
|
|
106
|
+
line[:max_line_length] + "… (more " + str(len(line) - max_line_length) + " characters in this line)"
|
|
107
|
+
)
|
|
108
|
+
return "\n".join(lines)
|