klaude-code 1.2.6__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (167) hide show
  1. klaude_code/__init__.py +0 -0
  2. klaude_code/cli/__init__.py +1 -0
  3. klaude_code/cli/main.py +298 -0
  4. klaude_code/cli/runtime.py +331 -0
  5. klaude_code/cli/session_cmd.py +80 -0
  6. klaude_code/command/__init__.py +43 -0
  7. klaude_code/command/clear_cmd.py +20 -0
  8. klaude_code/command/command_abc.py +92 -0
  9. klaude_code/command/diff_cmd.py +138 -0
  10. klaude_code/command/export_cmd.py +86 -0
  11. klaude_code/command/help_cmd.py +51 -0
  12. klaude_code/command/model_cmd.py +43 -0
  13. klaude_code/command/prompt-dev-docs-update.md +56 -0
  14. klaude_code/command/prompt-dev-docs.md +46 -0
  15. klaude_code/command/prompt-init.md +45 -0
  16. klaude_code/command/prompt_command.py +69 -0
  17. klaude_code/command/refresh_cmd.py +43 -0
  18. klaude_code/command/registry.py +110 -0
  19. klaude_code/command/status_cmd.py +111 -0
  20. klaude_code/command/terminal_setup_cmd.py +252 -0
  21. klaude_code/config/__init__.py +11 -0
  22. klaude_code/config/config.py +177 -0
  23. klaude_code/config/list_model.py +162 -0
  24. klaude_code/config/select_model.py +67 -0
  25. klaude_code/const/__init__.py +133 -0
  26. klaude_code/core/__init__.py +0 -0
  27. klaude_code/core/agent.py +165 -0
  28. klaude_code/core/executor.py +485 -0
  29. klaude_code/core/manager/__init__.py +19 -0
  30. klaude_code/core/manager/agent_manager.py +127 -0
  31. klaude_code/core/manager/llm_clients.py +42 -0
  32. klaude_code/core/manager/llm_clients_builder.py +49 -0
  33. klaude_code/core/manager/sub_agent_manager.py +86 -0
  34. klaude_code/core/prompt.py +89 -0
  35. klaude_code/core/prompts/prompt-claude-code.md +98 -0
  36. klaude_code/core/prompts/prompt-codex.md +331 -0
  37. klaude_code/core/prompts/prompt-gemini.md +43 -0
  38. klaude_code/core/prompts/prompt-subagent-explore.md +27 -0
  39. klaude_code/core/prompts/prompt-subagent-oracle.md +23 -0
  40. klaude_code/core/prompts/prompt-subagent-webfetch.md +46 -0
  41. klaude_code/core/prompts/prompt-subagent.md +8 -0
  42. klaude_code/core/reminders.py +445 -0
  43. klaude_code/core/task.py +237 -0
  44. klaude_code/core/tool/__init__.py +75 -0
  45. klaude_code/core/tool/file/__init__.py +0 -0
  46. klaude_code/core/tool/file/apply_patch.py +492 -0
  47. klaude_code/core/tool/file/apply_patch_tool.md +1 -0
  48. klaude_code/core/tool/file/apply_patch_tool.py +204 -0
  49. klaude_code/core/tool/file/edit_tool.md +9 -0
  50. klaude_code/core/tool/file/edit_tool.py +274 -0
  51. klaude_code/core/tool/file/multi_edit_tool.md +42 -0
  52. klaude_code/core/tool/file/multi_edit_tool.py +199 -0
  53. klaude_code/core/tool/file/read_tool.md +14 -0
  54. klaude_code/core/tool/file/read_tool.py +326 -0
  55. klaude_code/core/tool/file/write_tool.md +8 -0
  56. klaude_code/core/tool/file/write_tool.py +146 -0
  57. klaude_code/core/tool/memory/__init__.py +0 -0
  58. klaude_code/core/tool/memory/memory_tool.md +16 -0
  59. klaude_code/core/tool/memory/memory_tool.py +462 -0
  60. klaude_code/core/tool/memory/skill_loader.py +245 -0
  61. klaude_code/core/tool/memory/skill_tool.md +24 -0
  62. klaude_code/core/tool/memory/skill_tool.py +97 -0
  63. klaude_code/core/tool/shell/__init__.py +0 -0
  64. klaude_code/core/tool/shell/bash_tool.md +43 -0
  65. klaude_code/core/tool/shell/bash_tool.py +123 -0
  66. klaude_code/core/tool/shell/command_safety.py +363 -0
  67. klaude_code/core/tool/sub_agent_tool.py +83 -0
  68. klaude_code/core/tool/todo/__init__.py +0 -0
  69. klaude_code/core/tool/todo/todo_write_tool.md +182 -0
  70. klaude_code/core/tool/todo/todo_write_tool.py +121 -0
  71. klaude_code/core/tool/todo/update_plan_tool.md +3 -0
  72. klaude_code/core/tool/todo/update_plan_tool.py +104 -0
  73. klaude_code/core/tool/tool_abc.py +25 -0
  74. klaude_code/core/tool/tool_context.py +106 -0
  75. klaude_code/core/tool/tool_registry.py +78 -0
  76. klaude_code/core/tool/tool_runner.py +252 -0
  77. klaude_code/core/tool/truncation.py +170 -0
  78. klaude_code/core/tool/web/__init__.py +0 -0
  79. klaude_code/core/tool/web/mermaid_tool.md +21 -0
  80. klaude_code/core/tool/web/mermaid_tool.py +76 -0
  81. klaude_code/core/tool/web/web_fetch_tool.md +8 -0
  82. klaude_code/core/tool/web/web_fetch_tool.py +159 -0
  83. klaude_code/core/turn.py +220 -0
  84. klaude_code/llm/__init__.py +21 -0
  85. klaude_code/llm/anthropic/__init__.py +3 -0
  86. klaude_code/llm/anthropic/client.py +221 -0
  87. klaude_code/llm/anthropic/input.py +200 -0
  88. klaude_code/llm/client.py +49 -0
  89. klaude_code/llm/input_common.py +239 -0
  90. klaude_code/llm/openai_compatible/__init__.py +3 -0
  91. klaude_code/llm/openai_compatible/client.py +211 -0
  92. klaude_code/llm/openai_compatible/input.py +109 -0
  93. klaude_code/llm/openai_compatible/tool_call_accumulator.py +80 -0
  94. klaude_code/llm/openrouter/__init__.py +3 -0
  95. klaude_code/llm/openrouter/client.py +200 -0
  96. klaude_code/llm/openrouter/input.py +160 -0
  97. klaude_code/llm/openrouter/reasoning_handler.py +209 -0
  98. klaude_code/llm/registry.py +22 -0
  99. klaude_code/llm/responses/__init__.py +3 -0
  100. klaude_code/llm/responses/client.py +216 -0
  101. klaude_code/llm/responses/input.py +167 -0
  102. klaude_code/llm/usage.py +109 -0
  103. klaude_code/protocol/__init__.py +4 -0
  104. klaude_code/protocol/commands.py +21 -0
  105. klaude_code/protocol/events.py +163 -0
  106. klaude_code/protocol/llm_param.py +147 -0
  107. klaude_code/protocol/model.py +287 -0
  108. klaude_code/protocol/op.py +89 -0
  109. klaude_code/protocol/op_handler.py +28 -0
  110. klaude_code/protocol/sub_agent.py +348 -0
  111. klaude_code/protocol/tools.py +15 -0
  112. klaude_code/session/__init__.py +4 -0
  113. klaude_code/session/export.py +624 -0
  114. klaude_code/session/selector.py +76 -0
  115. klaude_code/session/session.py +474 -0
  116. klaude_code/session/templates/export_session.html +1434 -0
  117. klaude_code/trace/__init__.py +3 -0
  118. klaude_code/trace/log.py +168 -0
  119. klaude_code/ui/__init__.py +91 -0
  120. klaude_code/ui/core/__init__.py +1 -0
  121. klaude_code/ui/core/display.py +103 -0
  122. klaude_code/ui/core/input.py +71 -0
  123. klaude_code/ui/core/stage_manager.py +55 -0
  124. klaude_code/ui/modes/__init__.py +1 -0
  125. klaude_code/ui/modes/debug/__init__.py +1 -0
  126. klaude_code/ui/modes/debug/display.py +36 -0
  127. klaude_code/ui/modes/exec/__init__.py +1 -0
  128. klaude_code/ui/modes/exec/display.py +63 -0
  129. klaude_code/ui/modes/repl/__init__.py +51 -0
  130. klaude_code/ui/modes/repl/clipboard.py +152 -0
  131. klaude_code/ui/modes/repl/completers.py +429 -0
  132. klaude_code/ui/modes/repl/display.py +60 -0
  133. klaude_code/ui/modes/repl/event_handler.py +375 -0
  134. klaude_code/ui/modes/repl/input_prompt_toolkit.py +198 -0
  135. klaude_code/ui/modes/repl/key_bindings.py +170 -0
  136. klaude_code/ui/modes/repl/renderer.py +281 -0
  137. klaude_code/ui/renderers/__init__.py +0 -0
  138. klaude_code/ui/renderers/assistant.py +21 -0
  139. klaude_code/ui/renderers/common.py +8 -0
  140. klaude_code/ui/renderers/developer.py +158 -0
  141. klaude_code/ui/renderers/diffs.py +215 -0
  142. klaude_code/ui/renderers/errors.py +16 -0
  143. klaude_code/ui/renderers/metadata.py +190 -0
  144. klaude_code/ui/renderers/sub_agent.py +71 -0
  145. klaude_code/ui/renderers/thinking.py +39 -0
  146. klaude_code/ui/renderers/tools.py +551 -0
  147. klaude_code/ui/renderers/user_input.py +65 -0
  148. klaude_code/ui/rich/__init__.py +1 -0
  149. klaude_code/ui/rich/live.py +65 -0
  150. klaude_code/ui/rich/markdown.py +308 -0
  151. klaude_code/ui/rich/quote.py +34 -0
  152. klaude_code/ui/rich/searchable_text.py +71 -0
  153. klaude_code/ui/rich/status.py +240 -0
  154. klaude_code/ui/rich/theme.py +274 -0
  155. klaude_code/ui/terminal/__init__.py +1 -0
  156. klaude_code/ui/terminal/color.py +244 -0
  157. klaude_code/ui/terminal/control.py +147 -0
  158. klaude_code/ui/terminal/notifier.py +107 -0
  159. klaude_code/ui/terminal/progress_bar.py +87 -0
  160. klaude_code/ui/utils/__init__.py +1 -0
  161. klaude_code/ui/utils/common.py +108 -0
  162. klaude_code/ui/utils/debouncer.py +42 -0
  163. klaude_code/version.py +163 -0
  164. klaude_code-1.2.6.dist-info/METADATA +178 -0
  165. klaude_code-1.2.6.dist-info/RECORD +167 -0
  166. klaude_code-1.2.6.dist-info/WHEEL +4 -0
  167. klaude_code-1.2.6.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,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
@@ -0,0 +1,281 @@
1
+ from __future__ import annotations
2
+
3
+ from contextlib import contextmanager
4
+ from dataclasses import dataclass
5
+ from typing import Any, Iterator
6
+
7
+ from rich import box
8
+ from rich.box import Box
9
+ from rich.console import Console
10
+ from rich.spinner import Spinner
11
+ from rich.status import Status
12
+ from rich.style import Style, StyleType
13
+ from rich.text import Text
14
+
15
+ from klaude_code.protocol import events, model
16
+ from klaude_code.ui.renderers import assistant as r_assistant
17
+ from klaude_code.ui.renderers import developer as r_developer
18
+ from klaude_code.ui.renderers import errors as r_errors
19
+ from klaude_code.ui.renderers import metadata as r_metadata
20
+ from klaude_code.ui.renderers import sub_agent as r_sub_agent
21
+ from klaude_code.ui.renderers import thinking as r_thinking
22
+ from klaude_code.ui.renderers import tools as r_tools
23
+ from klaude_code.ui.renderers import user_input as r_user_input
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
29
+
30
+
31
+ @dataclass
32
+ class SessionStatus:
33
+ color: Style | None = None
34
+ sub_agent_state: model.SubAgentState | None = None
35
+
36
+
37
+ class REPLRenderer:
38
+ """Render REPL content via a Rich console."""
39
+
40
+ def __init__(self, theme: str | None = None):
41
+ self.themes = get_theme(theme)
42
+ self.console: Console = Console(theme=self.themes.app_theme)
43
+ self.console.push_theme(self.themes.markdown_theme)
44
+ self._spinner: Status = self.console.status(
45
+ ShimmerStatusText("Thinking …", ThemeKey.SPINNER_STATUS_TEXT),
46
+ spinner=r_status.spinner_name(),
47
+ spinner_style=ThemeKey.SPINNER_STATUS,
48
+ )
49
+
50
+ self.session_map: dict[str, SessionStatus] = {}
51
+ self.current_sub_agent_color: Style | None = None
52
+ self.subagent_color_index = 0
53
+
54
+ def register_session(self, session_id: str, sub_agent_state: model.SubAgentState | None = None) -> None:
55
+ session_status = SessionStatus(
56
+ sub_agent_state=sub_agent_state,
57
+ )
58
+ if sub_agent_state is not None:
59
+ session_status.color = self.pick_sub_agent_color()
60
+ self.session_map[session_id] = session_status
61
+
62
+ def is_sub_agent_session(self, session_id: str) -> bool:
63
+ return session_id in self.session_map and self.session_map[session_id].sub_agent_state is not None
64
+
65
+ def _advance_sub_agent_color_index(self) -> None:
66
+ palette_size = len(self.themes.sub_agent_colors)
67
+ if palette_size == 0:
68
+ self.subagent_color_index = 0
69
+ return
70
+ self.subagent_color_index = (self.subagent_color_index + 1) % palette_size
71
+
72
+ def pick_sub_agent_color(self) -> Style:
73
+ self._advance_sub_agent_color_index()
74
+ palette = self.themes.sub_agent_colors
75
+ if not palette:
76
+ return Style()
77
+ return palette[self.subagent_color_index]
78
+
79
+ def get_session_sub_agent_color(self, session_id: str) -> Style:
80
+ status = self.session_map.get(session_id)
81
+ if status and status.color:
82
+ return status.color
83
+ return Style()
84
+
85
+ def box_style(self) -> Box:
86
+ return box.ROUNDED
87
+
88
+ @contextmanager
89
+ def session_print_context(self, session_id: str) -> Iterator[None]:
90
+ """Temporarily switch to sub-agent quote style."""
91
+ if session_id in self.session_map and self.session_map[session_id].color:
92
+ self.current_sub_agent_color = self.session_map[session_id].color
93
+ try:
94
+ yield
95
+ finally:
96
+ self.current_sub_agent_color = None
97
+
98
+ def print(self, *objects: Any, style: StyleType | None = None, end: str = "\n") -> None:
99
+ if self.current_sub_agent_color:
100
+ if objects:
101
+ self.console.print(Quote(*objects, style=self.current_sub_agent_color))
102
+ return
103
+ self.console.print(*objects, style=style, end=end)
104
+
105
+ def display_tool_call(self, e: events.ToolCallEvent) -> None:
106
+ # Handle sub-agent tool calls in replay mode
107
+ if r_tools.is_sub_agent_tool(e.tool_name):
108
+ if e.is_replay:
109
+ state = r_sub_agent.build_sub_agent_state_from_tool_call(e)
110
+ if state is not None:
111
+ sub_agent_default_style = (
112
+ self.themes.sub_agent_colors[0] if self.themes.sub_agent_colors else Style()
113
+ )
114
+ self.print(
115
+ Quote(
116
+ r_sub_agent.render_sub_agent_call(state, sub_agent_default_style),
117
+ style=sub_agent_default_style,
118
+ )
119
+ )
120
+ return
121
+
122
+ renderable = r_tools.render_tool_call(e)
123
+ if renderable is not None:
124
+ self.print(renderable)
125
+
126
+ def display_tool_call_result(self, e: events.ToolResultEvent) -> None:
127
+ # Handle sub-agent tool results in replay mode
128
+ if r_tools.is_sub_agent_tool(e.tool_name):
129
+ if e.is_replay:
130
+ sub_agent_default_style = self.themes.sub_agent_colors[0] if self.themes.sub_agent_colors else Style()
131
+ self.print(
132
+ Quote(
133
+ r_sub_agent.render_sub_agent_result(
134
+ e.result,
135
+ code_theme=self.themes.code_theme,
136
+ style=sub_agent_default_style,
137
+ ),
138
+ style=sub_agent_default_style,
139
+ )
140
+ )
141
+ return
142
+
143
+ renderable = r_tools.render_tool_result(e)
144
+ if renderable is not None:
145
+ self.print(renderable)
146
+
147
+ def display_thinking(self, content: str) -> None:
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:
154
+ self.console.push_theme(theme=self.themes.thinking_markdown_theme)
155
+ self.print(renderable)
156
+ self.console.pop_theme()
157
+ self.print()
158
+
159
+ async def replay_history(self, history_events: events.ReplayHistoryEvent) -> None:
160
+ tool_call_dict: dict[str, events.ToolCallEvent] = {}
161
+ for event in history_events.events:
162
+ match event:
163
+ case events.TurnStartEvent():
164
+ self.print()
165
+ case events.AssistantMessageEvent() as assistant_event:
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)
171
+ self.print()
172
+ case events.ThinkingEvent() as thinking_event:
173
+ self.display_thinking(thinking_event.content)
174
+ case events.DeveloperMessageEvent() as developer_event:
175
+ self.display_developer_message(developer_event)
176
+ self.display_command_output(developer_event)
177
+ case events.UserMessageEvent() as user_event:
178
+ self.print(r_user_input.render_user_input(user_event.content))
179
+ case events.ToolCallEvent() as tool_call_event:
180
+ tool_call_dict[tool_call_event.tool_call_id] = tool_call_event
181
+ case events.ToolResultEvent() as tool_result_event:
182
+ tool_call_event = tool_call_dict.get(tool_result_event.tool_call_id)
183
+ if tool_call_event is not None:
184
+ self.display_tool_call(tool_call_event)
185
+ tool_call_dict.pop(tool_result_event.tool_call_id, None)
186
+ self.display_tool_call_result(tool_result_event)
187
+ case events.ResponseMetadataEvent() as metadata_event:
188
+ self.print(r_metadata.render_response_metadata(metadata_event))
189
+ self.print()
190
+ case events.InterruptEvent():
191
+ self.print()
192
+ self.print(r_user_input.render_interrupt())
193
+
194
+ def display_developer_message(self, e: events.DeveloperMessageEvent) -> None:
195
+ if not r_developer.need_render_developer_message(e):
196
+ return
197
+ with self.session_print_context(e.session_id):
198
+ self.print(r_developer.render_developer_message(e))
199
+
200
+ def display_command_output(self, e: events.DeveloperMessageEvent) -> None:
201
+ if not e.item.command_output:
202
+ return
203
+ with self.session_print_context(e.session_id):
204
+ self.print(r_developer.render_command_output(e))
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
File without changes
@@ -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
@@ -0,0 +1,8 @@
1
+ from rich.table import Table
2
+
3
+
4
+ def create_grid() -> Table:
5
+ grid = Table.grid(padding=(0, 1))
6
+ grid.add_column(no_wrap=True)
7
+ grid.add_column(overflow="fold")
8
+ return grid
@@ -0,0 +1,158 @@
1
+ from rich.console import Group, RenderableType
2
+ from rich.padding import Padding
3
+ from rich.table import Table
4
+ from rich.text import Text
5
+
6
+ from klaude_code.protocol import commands, events, model
7
+ from klaude_code.ui.renderers import diffs as r_diffs
8
+ from klaude_code.ui.renderers.common import create_grid
9
+ from klaude_code.ui.renderers.tools import render_path
10
+ from klaude_code.ui.rich.theme import ThemeKey
11
+ from klaude_code.ui.utils.common import truncate_display
12
+
13
+
14
+ def need_render_developer_message(e: events.DeveloperMessageEvent) -> bool:
15
+ return bool(
16
+ e.item.memory_paths
17
+ or e.item.external_file_changes
18
+ or e.item.todo_use
19
+ or e.item.at_files
20
+ or e.item.user_image_count
21
+ )
22
+
23
+
24
+ def render_developer_message(e: events.DeveloperMessageEvent) -> RenderableType:
25
+ """Render developer message details into a single group.
26
+
27
+ Includes: memory paths, external file changes, todo reminder, @file operations.
28
+ Command output is excluded; render it separately via `render_command_output`.
29
+ """
30
+ parts: list[RenderableType] = []
31
+
32
+ if mp := e.item.memory_paths:
33
+ grid = create_grid()
34
+ grid.add_row(
35
+ Text(" +", style=ThemeKey.REMINDER),
36
+ Text.assemble(
37
+ ("Load memory ", ThemeKey.REMINDER),
38
+ Text(", ", ThemeKey.REMINDER).join(
39
+ render_path(memory_path, ThemeKey.REMINDER_BOLD) for memory_path in mp
40
+ ),
41
+ ),
42
+ )
43
+ parts.append(grid)
44
+
45
+ if fc := e.item.external_file_changes:
46
+ grid = create_grid()
47
+ for file_path in fc:
48
+ grid.add_row(
49
+ Text(" +", style=ThemeKey.REMINDER),
50
+ Text.assemble(
51
+ ("Read ", ThemeKey.REMINDER),
52
+ render_path(file_path, ThemeKey.REMINDER_BOLD),
53
+ (" after external changes", ThemeKey.REMINDER),
54
+ ),
55
+ )
56
+ parts.append(grid)
57
+
58
+ if e.item.todo_use:
59
+ grid = create_grid()
60
+ grid.add_row(
61
+ Text(" +", style=ThemeKey.REMINDER),
62
+ Text("Todo hasn't been updated recently", ThemeKey.REMINDER),
63
+ )
64
+ parts.append(grid)
65
+
66
+ if e.item.at_files:
67
+ grid = create_grid()
68
+ for at_file in e.item.at_files:
69
+ grid.add_row(
70
+ Text(" +", style=ThemeKey.REMINDER),
71
+ Text.assemble(
72
+ (f"{at_file.operation} ", ThemeKey.REMINDER),
73
+ render_path(at_file.path, ThemeKey.REMINDER_BOLD),
74
+ ),
75
+ )
76
+ parts.append(grid)
77
+
78
+ if uic := e.item.user_image_count:
79
+ grid = create_grid()
80
+ grid.add_row(
81
+ Text(" +", style=ThemeKey.REMINDER),
82
+ Text(f"Attached {uic} image{'s' if uic > 1 else ''}", style=ThemeKey.REMINDER),
83
+ )
84
+ parts.append(grid)
85
+
86
+ return Group(*parts) if parts else Text("")
87
+
88
+
89
+ def render_command_output(e: events.DeveloperMessageEvent) -> RenderableType:
90
+ """Render developer command output content."""
91
+ if not e.item.command_output:
92
+ return Text("")
93
+
94
+ match e.item.command_output.command_name:
95
+ case commands.CommandName.DIFF:
96
+ if e.item.content is None or len(e.item.content) == 0:
97
+ return Padding.indent(Text("(no changes)", style=ThemeKey.TOOL_RESULT), level=2)
98
+ return r_diffs.render_diff_panel(e.item.content, show_file_name=True)
99
+ case commands.CommandName.HELP:
100
+ return Padding.indent(Text.from_markup(e.item.content or ""), level=2)
101
+ case commands.CommandName.STATUS:
102
+ return _render_status_output(e.item.command_output)
103
+ case _:
104
+ content = e.item.content or "(no content)"
105
+ style = ThemeKey.TOOL_RESULT if not e.item.command_output.is_error else ThemeKey.ERROR
106
+ return Padding.indent(Text(truncate_display(content), style=style), level=2)
107
+
108
+
109
+ def _format_tokens(tokens: int) -> str:
110
+ """Format token count with K/M suffix for readability."""
111
+ if tokens >= 1_000_000:
112
+ return f"{tokens / 1_000_000:.2f}M"
113
+ if tokens >= 1_000:
114
+ return f"{tokens / 1_000:.1f}K"
115
+ return str(tokens)
116
+
117
+
118
+ def _format_cost(cost: float | None) -> str:
119
+ """Format cost in USD."""
120
+ if cost is None:
121
+ return "-"
122
+ if cost < 0.01:
123
+ return f"${cost:.4f}"
124
+ return f"${cost:.2f}"
125
+
126
+
127
+ def _render_status_output(command_output: model.CommandOutput) -> RenderableType:
128
+ """Render session status as a two-column table with sections."""
129
+ if not command_output.ui_extra or not command_output.ui_extra.session_status:
130
+ return Text("(no status data)", style=ThemeKey.TOOL_RESULT)
131
+
132
+ status = command_output.ui_extra.session_status
133
+ usage = status.usage
134
+
135
+ table = Table.grid(padding=(0, 2))
136
+ table.add_column(style=ThemeKey.TOOL_RESULT, no_wrap=True)
137
+ table.add_column(style=ThemeKey.TOOL_RESULT, no_wrap=True)
138
+ # Token Usage section
139
+ table.add_row(Text("Token Usage", style="bold"), "")
140
+ table.add_row("Input Tokens", _format_tokens(usage.input_tokens))
141
+ if usage.cached_tokens > 0:
142
+ table.add_row("Cached Tokens", _format_tokens(usage.cached_tokens))
143
+ if usage.reasoning_tokens > 0:
144
+ table.add_row("Reasoning Tokens", _format_tokens(usage.reasoning_tokens))
145
+ table.add_row("Output Tokens", _format_tokens(usage.output_tokens))
146
+ table.add_row("Total Tokens", _format_tokens(usage.total_tokens))
147
+
148
+ # Cost section
149
+ if usage.total_cost is not None:
150
+ table.add_row("", "") # Empty line
151
+ table.add_row(Text("Cost", style="bold"), "")
152
+ table.add_row("Input Cost", _format_cost(usage.input_cost))
153
+ if usage.cache_read_cost is not None and usage.cache_read_cost > 0:
154
+ table.add_row("Cache Read Cost", _format_cost(usage.cache_read_cost))
155
+ table.add_row("Output Cost", _format_cost(usage.output_cost))
156
+ table.add_row("Total Cost", _format_cost(usage.total_cost))
157
+
158
+ return Padding.indent(table, level=2)