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.
Files changed (140) hide show
  1. klaude_code/cli/main.py +9 -4
  2. klaude_code/cli/runtime.py +42 -43
  3. klaude_code/command/__init__.py +7 -5
  4. klaude_code/command/clear_cmd.py +6 -29
  5. klaude_code/command/command_abc.py +44 -8
  6. klaude_code/command/diff_cmd.py +33 -27
  7. klaude_code/command/export_cmd.py +18 -26
  8. klaude_code/command/help_cmd.py +10 -8
  9. klaude_code/command/model_cmd.py +11 -40
  10. klaude_code/command/{prompt-update-dev-doc.md → prompt-dev-docs-update.md} +3 -2
  11. klaude_code/command/{prompt-dev-doc.md → prompt-dev-docs.md} +3 -2
  12. klaude_code/command/prompt-init.md +2 -5
  13. klaude_code/command/prompt_command.py +6 -6
  14. klaude_code/command/refresh_cmd.py +4 -5
  15. klaude_code/command/registry.py +16 -19
  16. klaude_code/command/terminal_setup_cmd.py +12 -11
  17. klaude_code/config/__init__.py +4 -0
  18. klaude_code/config/config.py +25 -26
  19. klaude_code/config/list_model.py +8 -3
  20. klaude_code/config/select_model.py +1 -1
  21. klaude_code/const/__init__.py +1 -1
  22. klaude_code/core/__init__.py +0 -3
  23. klaude_code/core/agent.py +25 -50
  24. klaude_code/core/executor.py +268 -101
  25. klaude_code/core/prompt.py +12 -12
  26. klaude_code/core/{prompt → prompts}/prompt-gemini.md +1 -1
  27. klaude_code/core/reminders.py +76 -95
  28. klaude_code/core/task.py +21 -14
  29. klaude_code/core/tool/__init__.py +45 -11
  30. klaude_code/core/tool/file/apply_patch.py +5 -1
  31. klaude_code/core/tool/file/apply_patch_tool.py +11 -13
  32. klaude_code/core/tool/file/edit_tool.py +27 -23
  33. klaude_code/core/tool/file/multi_edit_tool.py +15 -17
  34. klaude_code/core/tool/file/read_tool.py +41 -36
  35. klaude_code/core/tool/file/write_tool.py +13 -15
  36. klaude_code/core/tool/memory/memory_tool.py +85 -68
  37. klaude_code/core/tool/memory/skill_tool.py +10 -12
  38. klaude_code/core/tool/shell/bash_tool.py +24 -22
  39. klaude_code/core/tool/shell/command_safety.py +12 -1
  40. klaude_code/core/tool/sub_agent_tool.py +11 -12
  41. klaude_code/core/tool/todo/todo_write_tool.py +21 -28
  42. klaude_code/core/tool/todo/update_plan_tool.py +14 -24
  43. klaude_code/core/tool/tool_abc.py +3 -4
  44. klaude_code/core/tool/tool_context.py +7 -7
  45. klaude_code/core/tool/tool_registry.py +30 -47
  46. klaude_code/core/tool/tool_runner.py +35 -43
  47. klaude_code/core/tool/truncation.py +14 -20
  48. klaude_code/core/tool/web/mermaid_tool.py +12 -14
  49. klaude_code/core/tool/web/web_fetch_tool.py +15 -17
  50. klaude_code/core/turn.py +19 -7
  51. klaude_code/llm/__init__.py +3 -4
  52. klaude_code/llm/anthropic/client.py +30 -46
  53. klaude_code/llm/anthropic/input.py +4 -11
  54. klaude_code/llm/client.py +29 -8
  55. klaude_code/llm/input_common.py +66 -36
  56. klaude_code/llm/openai_compatible/client.py +42 -84
  57. klaude_code/llm/openai_compatible/input.py +11 -16
  58. klaude_code/llm/openai_compatible/tool_call_accumulator.py +2 -2
  59. klaude_code/llm/openrouter/client.py +40 -289
  60. klaude_code/llm/openrouter/input.py +13 -35
  61. klaude_code/llm/openrouter/reasoning_handler.py +209 -0
  62. klaude_code/llm/registry.py +5 -75
  63. klaude_code/llm/responses/client.py +34 -55
  64. klaude_code/llm/responses/input.py +24 -26
  65. klaude_code/llm/usage.py +109 -0
  66. klaude_code/protocol/__init__.py +4 -0
  67. klaude_code/protocol/events.py +3 -2
  68. klaude_code/protocol/{llm_parameter.py → llm_param.py} +12 -32
  69. klaude_code/protocol/model.py +49 -4
  70. klaude_code/protocol/op.py +18 -16
  71. klaude_code/protocol/op_handler.py +28 -0
  72. klaude_code/{core → protocol}/sub_agent.py +7 -0
  73. klaude_code/session/export.py +150 -70
  74. klaude_code/session/session.py +28 -14
  75. klaude_code/session/templates/export_session.html +180 -42
  76. klaude_code/trace/__init__.py +2 -2
  77. klaude_code/trace/log.py +11 -5
  78. klaude_code/ui/__init__.py +91 -8
  79. klaude_code/ui/core/__init__.py +1 -0
  80. klaude_code/ui/core/display.py +103 -0
  81. klaude_code/ui/core/input.py +71 -0
  82. klaude_code/ui/modes/__init__.py +1 -0
  83. klaude_code/ui/modes/debug/__init__.py +1 -0
  84. klaude_code/ui/{base/debug_event_display.py → modes/debug/display.py} +9 -5
  85. klaude_code/ui/modes/exec/__init__.py +1 -0
  86. klaude_code/ui/{base/exec_display.py → modes/exec/display.py} +28 -2
  87. klaude_code/ui/{repl → modes/repl}/__init__.py +5 -6
  88. klaude_code/ui/modes/repl/clipboard.py +152 -0
  89. klaude_code/ui/modes/repl/completers.py +429 -0
  90. klaude_code/ui/modes/repl/display.py +60 -0
  91. klaude_code/ui/modes/repl/event_handler.py +375 -0
  92. klaude_code/ui/modes/repl/input_prompt_toolkit.py +198 -0
  93. klaude_code/ui/modes/repl/key_bindings.py +170 -0
  94. klaude_code/ui/{repl → modes/repl}/renderer.py +109 -132
  95. klaude_code/ui/renderers/assistant.py +21 -0
  96. klaude_code/ui/renderers/common.py +0 -16
  97. klaude_code/ui/renderers/developer.py +18 -18
  98. klaude_code/ui/renderers/diffs.py +36 -14
  99. klaude_code/ui/renderers/errors.py +1 -1
  100. klaude_code/ui/renderers/metadata.py +50 -27
  101. klaude_code/ui/renderers/sub_agent.py +43 -9
  102. klaude_code/ui/renderers/thinking.py +33 -1
  103. klaude_code/ui/renderers/tools.py +212 -20
  104. klaude_code/ui/renderers/user_input.py +19 -23
  105. klaude_code/ui/rich/__init__.py +1 -0
  106. klaude_code/ui/{rich_ext → rich}/searchable_text.py +3 -1
  107. klaude_code/ui/{renderers → rich}/status.py +29 -18
  108. klaude_code/ui/{base → rich}/theme.py +8 -2
  109. klaude_code/ui/terminal/__init__.py +1 -0
  110. klaude_code/ui/{base/terminal_color.py → terminal/color.py} +4 -1
  111. klaude_code/ui/{base/terminal_control.py → terminal/control.py} +1 -0
  112. klaude_code/ui/{base/terminal_notifier.py → terminal/notifier.py} +5 -2
  113. klaude_code/ui/utils/__init__.py +1 -0
  114. klaude_code/ui/{base/utils.py → utils/common.py} +35 -3
  115. {klaude_code-1.2.1.dist-info → klaude_code-1.2.3.dist-info}/METADATA +1 -1
  116. klaude_code-1.2.3.dist-info/RECORD +161 -0
  117. klaude_code/core/clipboard_manifest.py +0 -124
  118. klaude_code/llm/openrouter/tool_call_accumulator.py +0 -80
  119. klaude_code/ui/base/__init__.py +0 -1
  120. klaude_code/ui/base/display_abc.py +0 -36
  121. klaude_code/ui/base/input_abc.py +0 -20
  122. klaude_code/ui/repl/display.py +0 -36
  123. klaude_code/ui/repl/event_handler.py +0 -247
  124. klaude_code/ui/repl/input.py +0 -773
  125. klaude_code/ui/rich_ext/__init__.py +0 -1
  126. klaude_code-1.2.1.dist-info/RECORD +0 -151
  127. /klaude_code/core/{prompt → prompts}/prompt-claude-code.md +0 -0
  128. /klaude_code/core/{prompt → prompts}/prompt-codex.md +0 -0
  129. /klaude_code/core/{prompt → prompts}/prompt-subagent-explore.md +0 -0
  130. /klaude_code/core/{prompt → prompts}/prompt-subagent-oracle.md +0 -0
  131. /klaude_code/core/{prompt → prompts}/prompt-subagent-webfetch.md +0 -0
  132. /klaude_code/core/{prompt → prompts}/prompt-subagent.md +0 -0
  133. /klaude_code/ui/{base → core}/stage_manager.py +0 -0
  134. /klaude_code/ui/{rich_ext → rich}/live.py +0 -0
  135. /klaude_code/ui/{rich_ext → rich}/markdown.py +0 -0
  136. /klaude_code/ui/{rich_ext → rich}/quote.py +0 -0
  137. /klaude_code/ui/{base → terminal}/progress_bar.py +0 -0
  138. /klaude_code/ui/{base → utils}/debouncer.py +0 -0
  139. {klaude_code-1.2.1.dist-info → klaude_code-1.2.3.dist-info}/WHEEL +0 -0
  140. {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.const import INVALID_TOOL_CALL_MAX_LENGTH
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.ui.base.theme import ThemeKey
12
- from klaude_code.ui.renderers.common import create_grid, truncate_display
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(", ".join([f"{k}: {v}" for k, v in json_dict.items()]), ThemeKey.TOOL_PARAM)
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(arguments.strip()[:INVALID_TOOL_CALL_MAX_LENGTH], style=ThemeKey.INVALID_TOOL_CALL_ARGS)
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], style=ThemeKey.INVALID_TOOL_CALL_ARGS
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(arguments.strip()[:INVALID_TOOL_CALL_MAX_LENGTH], style=ThemeKey.INVALID_TOOL_CALL_ARGS)
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(Text(arguments.strip()[:INVALID_TOOL_CALL_MAX_LENGTH], style=ThemeKey.INVALID_TOOL_CALL_ARGS))
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(Text(arguments.strip()[:INVALID_TOOL_CALL_MAX_LENGTH], style=ThemeKey.INVALID_TOOL_CALL_ARGS))
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(arguments.strip()[:INVALID_TOOL_CALL_MAX_LENGTH], style=ThemeKey.INVALID_TOOL_CALL_ARGS)
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(arguments.strip()[:INVALID_TOOL_CALL_MAX_LENGTH], style=ThemeKey.INVALID_TOOL_CALL_ARGS),
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(str(patch_content)[:INVALID_TOOL_CALL_MAX_LENGTH], ThemeKey.INVALID_TOOL_CALL_ARGS)
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((" ✘", ThemeKey.ERROR_BOLD), " ", Text("(no content)", style=ThemeKey.ERROR))
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((" ✘", ThemeKey.ERROR_BOLD), " ", Text("(invalid ui_extra)", style=ThemeKey.ERROR))
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(ui_extra: model.ToolResultUIExtra | None) -> model.MermaidLinkUIExtra | None:
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(arguments.strip()[:INVALID_TOOL_CALL_MAX_LENGTH], style=ThemeKey.INVALID_TOOL_CALL_ARGS)
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(arguments.strip()[:INVALID_TOOL_CALL_MAX_LENGTH], style=ThemeKey.INVALID_TOOL_CALL_ARGS)
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(ui_extra: model.ToolResultUIExtra | None) -> model.TruncationUIExtra | None:
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.protocol.commands import CommandName
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 _is_valid_slash_command(splits[0][1:]):
50
- if len(splits) <= 1:
51
- renderables.append(Text(f" {line} ", style=ThemeKey.USER_INPUT_SLASH_COMMAND))
52
- continue
53
- else:
54
- line_text = Text.assemble(
55
- (f" {splits[0]} ", ThemeKey.USER_INPUT_SLASH_COMMAND),
56
- " ",
57
- render_at_pattern(splits[1]),
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.add_row(Text("❯", style=ThemeKey.USER_INPUT_PROMPT), Group(*renderables))
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__(self) -> Iterable[Tuple[str, str]]: # pragma: no cover - passthrough
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.base.terminal_color import get_last_terminal_background_rgb
17
- from klaude_code.ui.base.theme import ThemeKey
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
- period = len(chars) + padding * 2
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 % sweep_seconds) / sweep_seconds * float(period)
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
- text = Text()
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 = f"{self._main_text}{self._hint_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
- base_style = main_style if index < split_index else hint_style
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
- text.append(ch, style=style)
180
+ result.append(ch, style=style)
165
181
 
166
- yield text
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.green,
181
- ThemeKey.SPINNER_STATUS_TEXT.value: palette.green,
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(f"Failed to open /dev/tty for OSC color query: {exc}", debug_type=DebugType.TERMINAL)
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:
@@ -37,6 +37,7 @@ def start_esc_interrupt_monitor(
37
37
 
38
38
  # Fallback for non-interactive or non-POSIX environments.
39
39
  if not sys.stdin.isatty() or os.name != "posix":
40
+
40
41
  async def _noop() -> None: # type: ignore[return-type]
41
42
  return None
42
43
 
@@ -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("Terminal notifier skipped: disabled via config", debug_type=DebugType.TERMINAL)
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(["git", "rev-parse", "--git-dir"], cwd=path, capture_output=True, text=True, timeout=2)
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"], cwd=path, capture_output=True, text=True, timeout=2
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"], cwd=path, capture_output=True, text=True, timeout=2
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)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: klaude-code
3
- Version: 1.2.1
3
+ Version: 1.2.3
4
4
  Summary: Add your description here
5
5
  Requires-Dist: anthropic>=0.66.0
6
6
  Requires-Dist: openai>=1.102.0