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
@@ -0,0 +1,170 @@
1
+ """REPL keyboard bindings for prompt_toolkit.
2
+
3
+ This module provides the factory function to create key bindings for the REPL input,
4
+ with dependencies injected to avoid circular imports.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import re
10
+ from collections.abc import Callable
11
+ from typing import cast
12
+
13
+ from prompt_toolkit.key_binding import KeyBindings
14
+
15
+
16
+ def create_key_bindings(
17
+ capture_clipboard_tag: Callable[[], str | None],
18
+ copy_to_clipboard: Callable[[str], None],
19
+ at_token_pattern: re.Pattern[str],
20
+ ) -> KeyBindings:
21
+ """Create REPL key bindings with injected dependencies.
22
+
23
+ Args:
24
+ capture_clipboard_tag: Callable to capture clipboard image and return tag
25
+ copy_to_clipboard: Callable to copy text to system clipboard
26
+ at_token_pattern: Pattern to match @token for completion refresh
27
+
28
+ Returns:
29
+ KeyBindings instance with all REPL handlers configured
30
+ """
31
+ kb = KeyBindings()
32
+
33
+ @kb.add("c-v")
34
+ def _(event): # type: ignore
35
+ """Paste image from clipboard as [Image #N]."""
36
+ tag = capture_clipboard_tag()
37
+ if tag:
38
+ try:
39
+ event.current_buffer.insert_text(tag) # pyright: ignore[reportUnknownMemberType]
40
+ except Exception:
41
+ pass
42
+
43
+ @kb.add("enter")
44
+ def _(event): # type: ignore
45
+ buf = event.current_buffer # type: ignore
46
+ doc = buf.document # type: ignore
47
+
48
+ # If VS Code/Windsurf/Cursor sent a "\\" sentinel before Enter (Shift+Enter mapping),
49
+ # treat it as a request for a newline instead of submit.
50
+ # This allows Shift+Enter to insert a newline in our multiline prompt.
51
+ try:
52
+ if doc.text_before_cursor.endswith("\\"): # type: ignore[reportUnknownMemberType]
53
+ buf.delete_before_cursor() # remove the sentinel backslash # type: ignore[reportUnknownMemberType]
54
+ buf.insert_text("\n") # type: ignore[reportUnknownMemberType]
55
+ return
56
+ except Exception:
57
+ # Fall through to default behavior if anything goes wrong
58
+ pass
59
+
60
+ # If the entire buffer is whitespace-only, insert a newline rather than submitting.
61
+ if len(buf.text.strip()) == 0: # type: ignore
62
+ buf.insert_text("\n") # type: ignore
63
+ return
64
+
65
+ # No need to persist manifest anymore - iter_inputs will handle image extraction
66
+ buf.validate_and_handle() # type: ignore
67
+
68
+ @kb.add("c-j")
69
+ def _(event): # type: ignore
70
+ event.current_buffer.insert_text("\n") # type: ignore
71
+
72
+ @kb.add("c")
73
+ def _(event): # type: ignore
74
+ """Copy selected text to system clipboard, or insert 'c' if no selection."""
75
+ buf = event.current_buffer # type: ignore
76
+ if buf.selection_state: # type: ignore[reportUnknownMemberType]
77
+ doc = buf.document # type: ignore[reportUnknownMemberType]
78
+ start, end = doc.selection_range() # type: ignore[reportUnknownMemberType]
79
+ selected_text: str = doc.text[start:end] # type: ignore[reportUnknownMemberType]
80
+
81
+ if selected_text:
82
+ copy_to_clipboard(selected_text) # type: ignore[reportUnknownArgumentType]
83
+ buf.exit_selection() # type: ignore[reportUnknownMemberType]
84
+ else:
85
+ buf.insert_text("c") # type: ignore[reportUnknownMemberType]
86
+
87
+ @kb.add("backspace")
88
+ def _(event): # type: ignore
89
+ """Ensure completions refresh on backspace when editing an @token.
90
+
91
+ We delete the character before cursor (default behavior), then explicitly
92
+ trigger completion refresh if the caret is still within an @... token.
93
+ """
94
+ buf = event.current_buffer # type: ignore
95
+ # Handle selection: cut selection if present, otherwise delete one character
96
+ if buf.selection_state: # type: ignore[reportUnknownMemberType]
97
+ buf.cut_selection() # type: ignore[reportUnknownMemberType]
98
+ else:
99
+ buf.delete_before_cursor() # type: ignore[reportUnknownMemberType]
100
+ # If the token pattern still applies, refresh completion popup
101
+ try:
102
+ text_before = buf.document.text_before_cursor # type: ignore[reportUnknownMemberType, reportUnknownVariableType]
103
+ # Check for both @ tokens and / tokens (slash commands on first line only)
104
+ should_refresh = False
105
+ if at_token_pattern.search(text_before): # type: ignore[reportUnknownArgumentType]
106
+ should_refresh = True
107
+ elif buf.document.cursor_position_row == 0: # type: ignore[reportUnknownMemberType]
108
+ # Check for slash command pattern without accessing protected attribute
109
+ text_before_str = cast(str, text_before or "")
110
+ if text_before_str.strip().startswith("/") and " " not in text_before_str:
111
+ should_refresh = True
112
+
113
+ if should_refresh:
114
+ buf.start_completion(select_first=False) # type: ignore[reportUnknownMemberType]
115
+ except Exception:
116
+ pass
117
+
118
+ @kb.add("left")
119
+ def _(event): # type: ignore
120
+ """Support wrapping to previous line when pressing left at column 0."""
121
+ buf = event.current_buffer # type: ignore
122
+ try:
123
+ doc = buf.document # type: ignore[reportUnknownMemberType]
124
+ row = cast(int, doc.cursor_position_row) # type: ignore[reportUnknownMemberType]
125
+ col = cast(int, doc.cursor_position_col) # type: ignore[reportUnknownMemberType]
126
+
127
+ # At the beginning of a non-first line: jump to previous line end.
128
+ if col == 0 and row > 0:
129
+ lines = cast(list[str], doc.lines) # type: ignore[reportUnknownMemberType]
130
+ prev_row = row - 1
131
+ if 0 <= prev_row < len(lines):
132
+ prev_line = lines[prev_row]
133
+ new_index = doc.translate_row_col_to_index(prev_row, len(prev_line)) # type: ignore[reportUnknownMemberType]
134
+ buf.cursor_position = new_index # type: ignore[reportUnknownMemberType]
135
+ return
136
+
137
+ # Default behavior: move one character left when possible.
138
+ if doc.cursor_position > 0: # type: ignore[reportUnknownMemberType]
139
+ buf.cursor_left() # type: ignore[reportUnknownMemberType]
140
+ except Exception:
141
+ pass
142
+
143
+ @kb.add("right")
144
+ def _(event): # type: ignore
145
+ """Support wrapping to next line when pressing right at line end."""
146
+ buf = event.current_buffer # type: ignore
147
+ try:
148
+ doc = buf.document # type: ignore[reportUnknownMemberType]
149
+ row = cast(int, doc.cursor_position_row) # type: ignore[reportUnknownMemberType]
150
+ col = cast(int, doc.cursor_position_col) # type: ignore[reportUnknownMemberType]
151
+ lines = cast(list[str], doc.lines) # type: ignore[reportUnknownMemberType]
152
+
153
+ current_line = lines[row] if 0 <= row < len(lines) else ""
154
+ at_line_end = col >= len(current_line)
155
+ is_last_line = row >= len(lines) - 1 if lines else True
156
+
157
+ # At end of a non-last line: jump to next line start.
158
+ if at_line_end and not is_last_line:
159
+ next_row = row + 1
160
+ new_index = doc.translate_row_col_to_index(next_row, 0) # type: ignore[reportUnknownMemberType]
161
+ buf.cursor_position = new_index # type: ignore[reportUnknownMemberType]
162
+ return
163
+
164
+ # Default behavior: move one character right when possible.
165
+ if doc.cursor_position < len(doc.text): # type: ignore[reportUnknownMemberType]
166
+ buf.cursor_right() # type: ignore[reportUnknownMemberType]
167
+ except Exception:
168
+ pass
169
+
170
+ return kb
@@ -1,6 +1,5 @@
1
1
  from __future__ import annotations
2
2
 
3
- import json
4
3
  from contextlib import contextmanager
5
4
  from dataclasses import dataclass
6
5
  from typing import Any, Iterator
@@ -8,25 +7,25 @@ from typing import Any, Iterator
8
7
  from rich import box
9
8
  from rich.box import Box
10
9
  from rich.console import Console
11
- from rich.padding import Padding
10
+ from rich.spinner import Spinner
12
11
  from rich.status import Status
13
12
  from rich.style import Style, StyleType
14
13
  from rich.text import Text
15
14
 
16
- from klaude_code.core.sub_agent import get_sub_agent_profile_by_tool
17
- from klaude_code.protocol import events, model, tools
18
- from klaude_code.ui.base.theme import ThemeKey, get_theme
15
+ from klaude_code.protocol import events, model
16
+ from klaude_code.ui.renderers import assistant as r_assistant
19
17
  from klaude_code.ui.renderers import developer as r_developer
20
- from klaude_code.ui.renderers import diffs as r_diffs
21
18
  from klaude_code.ui.renderers import errors as r_errors
22
19
  from klaude_code.ui.renderers import metadata as r_metadata
23
- from klaude_code.ui.renderers import status as r_status
24
20
  from klaude_code.ui.renderers import sub_agent as r_sub_agent
21
+ from klaude_code.ui.renderers import thinking as r_thinking
25
22
  from klaude_code.ui.renderers import tools as r_tools
26
23
  from klaude_code.ui.renderers import user_input as r_user_input
27
- from klaude_code.ui.renderers.common import create_grid, truncate_display
28
- from klaude_code.ui.rich_ext.markdown import NoInsetMarkdown
29
- from klaude_code.ui.rich_ext.quote import Quote
24
+ from klaude_code.ui.rich import status as r_status
25
+ from klaude_code.ui.rich.quote import Quote
26
+ from klaude_code.ui.rich.status import ShimmerStatusText
27
+ from klaude_code.ui.rich.theme import ThemeKey, get_theme
28
+ from klaude_code.ui.utils.common import truncate_display
30
29
 
31
30
 
32
31
  @dataclass
@@ -42,8 +41,8 @@ class REPLRenderer:
42
41
  self.themes = get_theme(theme)
43
42
  self.console: Console = Console(theme=self.themes.app_theme)
44
43
  self.console.push_theme(self.themes.markdown_theme)
45
- self.spinner: Status = self.console.status(
46
- r_status.render_status_text("Thinking …", ThemeKey.SPINNER_STATUS_TEXT),
44
+ self._spinner: Status = self.console.status(
45
+ ShimmerStatusText("Thinking …", ThemeKey.SPINNER_STATUS_TEXT),
47
46
  spinner=r_status.spinner_name(),
48
47
  spinner_style=ThemeKey.SPINNER_STATUS,
49
48
  )
@@ -86,14 +85,6 @@ class REPLRenderer:
86
85
  def box_style(self) -> Box:
87
86
  return box.ROUNDED
88
87
 
89
- @staticmethod
90
- def _extract_diff_text(ui_extra: model.ToolResultUIExtra | None) -> str | None:
91
- if ui_extra is None:
92
- return None
93
- if ui_extra.type == model.ToolResultUIExtraType.DIFF_TEXT:
94
- return ui_extra.diff_text
95
- return None
96
-
97
88
  @contextmanager
98
89
  def session_print_context(self, session_id: str) -> Iterator[None]:
99
90
  """Temporarily switch to sub-agent quote style."""
@@ -112,11 +103,10 @@ class REPLRenderer:
112
103
  self.console.print(*objects, style=style, end=end)
113
104
 
114
105
  def display_tool_call(self, e: events.ToolCallEvent) -> None:
106
+ # Handle sub-agent tool calls in replay mode
115
107
  if r_tools.is_sub_agent_tool(e.tool_name):
116
- # In replay mode, render sub-agent call here
117
- # In normal execution, handled by TaskStartEvent
118
108
  if e.is_replay:
119
- state = self._build_sub_agent_state_from_tool_call(e)
109
+ state = r_sub_agent.build_sub_agent_state_from_tool_call(e)
120
110
  if state is not None:
121
111
  sub_agent_default_style = (
122
112
  self.themes.sub_agent_colors[0] if self.themes.sub_agent_colors else Style()
@@ -128,36 +118,14 @@ class REPLRenderer:
128
118
  )
129
119
  )
130
120
  return
131
- match e.tool_name:
132
- case tools.READ:
133
- self.print(r_tools.render_read_tool_call(e.arguments))
134
- case tools.EDIT:
135
- self.print(r_tools.render_edit_tool_call(e.arguments))
136
- case tools.WRITE:
137
- self.print(r_tools.render_write_tool_call(e.arguments))
138
- case tools.MULTI_EDIT:
139
- self.print(r_tools.render_multi_edit_tool_call(e.arguments))
140
- case tools.BASH:
141
- self.print(r_tools.render_generic_tool_call(e.tool_name, e.arguments, ">"))
142
- case tools.APPLY_PATCH:
143
- self.print(r_tools.render_apply_patch_tool_call(e.arguments))
144
- case tools.TODO_WRITE:
145
- self.print(r_tools.render_generic_tool_call("Update Todos", "", "◎"))
146
- case tools.UPDATE_PLAN:
147
- self.print(r_tools.render_update_plan_tool_call(e.arguments))
148
- case tools.MERMAID:
149
- self.print(r_tools.render_mermaid_tool_call(e.arguments))
150
- case tools.MEMORY:
151
- self.print(r_tools.render_memory_tool_call(e.arguments))
152
- case tools.SKILL:
153
- self.print(r_tools.render_generic_tool_call(e.tool_name, e.arguments, "◈"))
154
- case _:
155
- self.print(r_tools.render_generic_tool_call(e.tool_name, e.arguments))
121
+
122
+ renderable = r_tools.render_tool_call(e)
123
+ if renderable is not None:
124
+ self.print(renderable)
156
125
 
157
126
  def display_tool_call_result(self, e: events.ToolResultEvent) -> None:
127
+ # Handle sub-agent tool results in replay mode
158
128
  if r_tools.is_sub_agent_tool(e.tool_name):
159
- # In replay mode, render sub-agent result here
160
- # In normal execution, handled by TaskFinishEvent
161
129
  if e.is_replay:
162
130
  sub_agent_default_style = self.themes.sub_agent_colors[0] if self.themes.sub_agent_colors else Style()
163
131
  self.print(
@@ -171,82 +139,20 @@ class REPLRenderer:
171
139
  )
172
140
  )
173
141
  return
174
- if e.status == "error" and e.ui_extra is None:
175
- self.print(r_errors.render_error(Text(truncate_display(e.result))))
176
- return
177
-
178
- # Show truncation info if output was truncated and saved to file
179
- truncation_info = r_tools.get_truncation_info(e)
180
- if truncation_info:
181
- self.print(r_tools.render_truncation_info(truncation_info))
182
- return
183
142
 
184
- diff_text = self._extract_diff_text(e.ui_extra)
185
-
186
- match e.tool_name:
187
- case tools.READ:
188
- pass
189
- case tools.EDIT | tools.MULTI_EDIT | tools.WRITE:
190
- self.print(Padding.indent(r_diffs.render_diff(diff_text or ""), level=2))
191
- case tools.MEMORY:
192
- if diff_text:
193
- self.print(Padding.indent(r_diffs.render_diff(diff_text), level=2))
194
- elif len(e.result.strip()) > 0:
195
- self.print(r_tools.render_generic_tool_result(e.result))
196
- case tools.TODO_WRITE | tools.UPDATE_PLAN:
197
- self.print(r_tools.render_todo(e))
198
- case tools.MERMAID:
199
- self.print(r_tools.render_mermaid_tool_result(e))
200
- case _:
201
- if e.tool_name in (tools.BASH, tools.APPLY_PATCH) and e.result.startswith("diff --git"):
202
- self.print(r_diffs.render_diff_panel(e.result, show_file_name=True))
203
- return
204
- if e.tool_name == tools.APPLY_PATCH and diff_text:
205
- self.print(Padding.indent(r_diffs.render_diff(diff_text, show_file_name=True), level=2))
206
- return
207
- if len(e.result.strip()) == 0:
208
- e.result = "(no content)"
209
- self.print(r_tools.render_generic_tool_result(e.result))
210
-
211
- def _build_sub_agent_state_from_tool_call(self, e: events.ToolCallEvent) -> model.SubAgentState | None:
212
- profile = get_sub_agent_profile_by_tool(e.tool_name)
213
- if profile is None:
214
- return None
215
- description = profile.name
216
- prompt = ""
217
- if e.arguments:
218
- try:
219
- payload: dict[str, object] = json.loads(e.arguments)
220
- except json.JSONDecodeError:
221
- payload = {}
222
- desc_value = payload.get("description")
223
- if isinstance(desc_value, str) and desc_value.strip():
224
- description = desc_value.strip()
225
- prompt_value = payload.get("prompt") or payload.get("task")
226
- if isinstance(prompt_value, str):
227
- prompt = prompt_value.strip()
228
- return model.SubAgentState(
229
- sub_agent_type=profile.name,
230
- sub_agent_desc=description,
231
- sub_agent_prompt=prompt,
232
- )
143
+ renderable = r_tools.render_tool_result(e)
144
+ if renderable is not None:
145
+ self.print(renderable)
233
146
 
234
147
  def display_thinking(self, content: str) -> None:
235
- if len(content.strip()) > 0:
148
+ renderable = r_thinking.render_thinking(
149
+ content,
150
+ code_theme=self.themes.code_theme,
151
+ style=ThemeKey.THINKING,
152
+ )
153
+ if renderable is not None:
236
154
  self.console.push_theme(theme=self.themes.thinking_markdown_theme)
237
- self.print(
238
- Padding.indent(
239
- NoInsetMarkdown(
240
- content.rstrip()
241
- .replace("**\n\n", "** \n")
242
- .replace("\\n\\n\n\n", "") # Weird case of Gemini 3
243
- .replace("****", "**\n\n**"), # remove extra newlines after bold titles
244
- code_theme=self.themes.code_theme,
245
- style=self.console.get_style(ThemeKey.THINKING),
246
- ),
247
- level=2,
248
- )
249
- )
155
+ self.print(renderable)
250
156
  self.console.pop_theme()
251
157
  self.print()
252
158
 
@@ -257,16 +163,11 @@ class REPLRenderer:
257
163
  case events.TurnStartEvent():
258
164
  self.print()
259
165
  case events.AssistantMessageEvent() as assistant_event:
260
- if len(assistant_event.content.strip()) > 0:
261
- grid = create_grid()
262
- grid.add_row(
263
- "•",
264
- NoInsetMarkdown(
265
- assistant_event.content.strip(),
266
- code_theme=self.themes.code_theme,
267
- ),
268
- )
269
- self.print(grid)
166
+ renderable = r_assistant.render_assistant_message(
167
+ assistant_event.content, code_theme=self.themes.code_theme
168
+ )
169
+ if renderable is not None:
170
+ self.print(renderable)
270
171
  self.print()
271
172
  case events.ThinkingEvent() as thinking_event:
272
173
  self.display_thinking(thinking_event.content)
@@ -302,3 +203,79 @@ class REPLRenderer:
302
203
  with self.session_print_context(e.session_id):
303
204
  self.print(r_developer.render_command_output(e))
304
205
  self.print()
206
+
207
+ def display_welcome(self, event: events.WelcomeEvent) -> None:
208
+ self.print(r_metadata.render_welcome(event, box_style=self.box_style()))
209
+
210
+ def display_user_message(self, event: events.UserMessageEvent) -> None:
211
+ self.print(r_user_input.render_user_input(event.content))
212
+
213
+ def display_task_start(self, event: events.TaskStartEvent) -> None:
214
+ self.register_session(event.session_id, event.sub_agent_state)
215
+ if event.sub_agent_state is not None:
216
+ with self.session_print_context(event.session_id):
217
+ self.print(
218
+ r_sub_agent.render_sub_agent_call(
219
+ event.sub_agent_state,
220
+ self.get_session_sub_agent_color(event.session_id),
221
+ )
222
+ )
223
+
224
+ def display_turn_start(self, event: events.TurnStartEvent) -> None:
225
+ if not self.is_sub_agent_session(event.session_id):
226
+ self.print()
227
+
228
+ def display_assistant_message(self, content: str) -> None:
229
+ renderable = r_assistant.render_assistant_message(content, code_theme=self.themes.code_theme)
230
+ if renderable is not None:
231
+ self.print(renderable)
232
+ self.print()
233
+
234
+ def display_response_metadata(self, event: events.ResponseMetadataEvent) -> None:
235
+ with self.session_print_context(event.session_id):
236
+ self.print(r_metadata.render_response_metadata(event))
237
+ self.print()
238
+
239
+ def display_task_finish(self, event: events.TaskFinishEvent) -> None:
240
+ if self.is_sub_agent_session(event.session_id):
241
+ with self.session_print_context(event.session_id):
242
+ self.print(
243
+ r_sub_agent.render_sub_agent_result(
244
+ event.task_result,
245
+ code_theme=self.themes.code_theme,
246
+ )
247
+ )
248
+
249
+ def display_interrupt(self) -> None:
250
+ self.print(r_user_input.render_interrupt())
251
+
252
+ def display_error(self, event: events.ErrorEvent) -> None:
253
+ self.print(
254
+ r_errors.render_error(
255
+ self.console.render_str(truncate_display(event.error_message)),
256
+ indent=0,
257
+ )
258
+ )
259
+
260
+ def display_thinking_prefix(self) -> None:
261
+ self.print(r_thinking.thinking_prefix())
262
+
263
+ # -------------------------------------------------------------------------
264
+ # Spinner control methods
265
+ # -------------------------------------------------------------------------
266
+
267
+ def spinner_start(self) -> None:
268
+ """Start the spinner animation."""
269
+ self._spinner.start()
270
+
271
+ def spinner_stop(self) -> None:
272
+ """Stop the spinner animation."""
273
+ self._spinner.stop()
274
+
275
+ def spinner_update(self, status_text: str | Text) -> None:
276
+ """Update the spinner status text."""
277
+ self._spinner.update(ShimmerStatusText(status_text, ThemeKey.SPINNER_STATUS_TEXT))
278
+
279
+ def spinner_renderable(self) -> Spinner:
280
+ """Return the spinner's renderable for embedding in other components."""
281
+ return self._spinner.renderable
@@ -0,0 +1,21 @@
1
+ from rich.console import RenderableType
2
+
3
+ from klaude_code.ui.renderers.common import create_grid
4
+ from klaude_code.ui.rich.markdown import NoInsetMarkdown
5
+
6
+
7
+ def render_assistant_message(content: str, *, code_theme: str) -> RenderableType | None:
8
+ """Render assistant message for replay history display.
9
+
10
+ Returns None if content is empty.
11
+ """
12
+ stripped = content.strip()
13
+ if len(stripped) == 0:
14
+ return None
15
+
16
+ grid = create_grid()
17
+ grid.add_row(
18
+ "•",
19
+ NoInsetMarkdown(stripped, code_theme=code_theme),
20
+ )
21
+ return grid
@@ -1,24 +1,8 @@
1
1
  from rich.table import Table
2
2
 
3
- from klaude_code.const import TRUNCATE_DISPLAY_MAX_LINE_LENGTH, TRUNCATE_DISPLAY_MAX_LINES
4
-
5
3
 
6
4
  def create_grid() -> Table:
7
5
  grid = Table.grid(padding=(0, 1))
8
6
  grid.add_column(no_wrap=True)
9
7
  grid.add_column(overflow="fold")
10
8
  return grid
11
-
12
-
13
- def truncate_display(
14
- text: str, max_lines: int = TRUNCATE_DISPLAY_MAX_LINES, max_line_length: int = TRUNCATE_DISPLAY_MAX_LINE_LENGTH
15
- ) -> str:
16
- lines = text.split("\n")
17
- if len(lines) > max_lines:
18
- lines = lines[:max_lines] + ["… (more " + str(len(lines) - max_lines) + " lines)"]
19
- for i, line in enumerate(lines):
20
- if len(line) > max_line_length:
21
- lines[i] = (
22
- line[:max_line_length] + "… (more " + str(len(line) - max_line_length) + " characters in this line)"
23
- )
24
- return "\n".join(lines)
@@ -2,12 +2,12 @@ from rich.console import Group, RenderableType
2
2
  from rich.padding import Padding
3
3
  from rich.text import Text
4
4
 
5
- from klaude_code.protocol import events
6
- from klaude_code.protocol.commands import CommandName
7
- from klaude_code.ui.base.theme import ThemeKey
5
+ from klaude_code.protocol import commands, events
8
6
  from klaude_code.ui.renderers import diffs as r_diffs
9
- from klaude_code.ui.renderers.common import create_grid, truncate_display
7
+ from klaude_code.ui.renderers.common import create_grid
10
8
  from klaude_code.ui.renderers.tools import render_path
9
+ from klaude_code.ui.rich.theme import ThemeKey
10
+ from klaude_code.ui.utils.common import truncate_display
11
11
 
12
12
 
13
13
  def need_render_developer_message(e: events.DeveloperMessageEvent) -> bool:
@@ -16,7 +16,7 @@ def need_render_developer_message(e: events.DeveloperMessageEvent) -> bool:
16
16
  or e.item.external_file_changes
17
17
  or e.item.todo_use
18
18
  or e.item.at_files
19
- or e.item.clipboard_images
19
+ or e.item.user_image_count
20
20
  )
21
21
 
22
22
 
@@ -56,7 +56,10 @@ def render_developer_message(e: events.DeveloperMessageEvent) -> RenderableType:
56
56
 
57
57
  if e.item.todo_use:
58
58
  grid = create_grid()
59
- grid.add_row(Text(" +", style=ThemeKey.REMINDER), Text("Todo hasn't been updated recently", ThemeKey.REMINDER))
59
+ grid.add_row(
60
+ Text(" +", style=ThemeKey.REMINDER),
61
+ Text("Todo hasn't been updated recently", ThemeKey.REMINDER),
62
+ )
60
63
  parts.append(grid)
61
64
 
62
65
  if e.item.at_files:
@@ -65,21 +68,18 @@ def render_developer_message(e: events.DeveloperMessageEvent) -> RenderableType:
65
68
  grid.add_row(
66
69
  Text(" +", style=ThemeKey.REMINDER),
67
70
  Text.assemble(
68
- (f"{at_file.operation} ", ThemeKey.REMINDER), render_path(at_file.path, ThemeKey.REMINDER_BOLD)
71
+ (f"{at_file.operation} ", ThemeKey.REMINDER),
72
+ render_path(at_file.path, ThemeKey.REMINDER_BOLD),
69
73
  ),
70
74
  )
71
75
  parts.append(grid)
72
76
 
73
- if ci := e.item.clipboard_images:
77
+ if uic := e.item.user_image_count:
74
78
  grid = create_grid()
75
- for img_tag in ci:
76
- grid.add_row(
77
- Text(" +", style=ThemeKey.REMINDER),
78
- Text.assemble(
79
- ("Read ", ThemeKey.REMINDER),
80
- Text(f"{img_tag} Image", style=ThemeKey.REMINDER_BOLD),
81
- ),
82
- )
79
+ grid.add_row(
80
+ Text(" +", style=ThemeKey.REMINDER),
81
+ Text(f"Attached {uic} image{'s' if uic > 1 else ''}", style=ThemeKey.REMINDER),
82
+ )
83
83
  parts.append(grid)
84
84
 
85
85
  return Group(*parts) if parts else Text("")
@@ -91,11 +91,11 @@ def render_command_output(e: events.DeveloperMessageEvent) -> RenderableType:
91
91
  return Text("")
92
92
 
93
93
  match e.item.command_output.command_name:
94
- case CommandName.DIFF:
94
+ case commands.CommandName.DIFF:
95
95
  if e.item.content is None or len(e.item.content) == 0:
96
96
  return Padding.indent(Text("(no changes)", style=ThemeKey.TOOL_RESULT), level=2)
97
97
  return r_diffs.render_diff_panel(e.item.content, show_file_name=True)
98
- case CommandName.HELP:
98
+ case commands.CommandName.HELP:
99
99
  return Padding.indent(Text.from_markup(e.item.content or ""), level=2)
100
100
  case _:
101
101
  content = e.item.content or "(no content)"