klaude-code 2.0.1__py3-none-any.whl → 2.1.0__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 (160) hide show
  1. klaude_code/app/__init__.py +12 -0
  2. klaude_code/app/runtime.py +215 -0
  3. klaude_code/cli/auth_cmd.py +2 -2
  4. klaude_code/cli/config_cmd.py +2 -2
  5. klaude_code/cli/cost_cmd.py +1 -1
  6. klaude_code/cli/debug.py +12 -36
  7. klaude_code/cli/list_model.py +3 -3
  8. klaude_code/cli/main.py +17 -60
  9. klaude_code/cli/self_update.py +2 -187
  10. klaude_code/cli/session_cmd.py +2 -2
  11. klaude_code/config/config.py +1 -1
  12. klaude_code/config/select_model.py +1 -1
  13. klaude_code/const.py +10 -1
  14. klaude_code/core/agent.py +9 -62
  15. klaude_code/core/agent_profile.py +284 -0
  16. klaude_code/core/executor.py +343 -230
  17. klaude_code/core/manager/llm_clients_builder.py +1 -1
  18. klaude_code/core/manager/sub_agent_manager.py +16 -29
  19. klaude_code/core/reminders.py +107 -155
  20. klaude_code/core/task.py +12 -20
  21. klaude_code/core/tool/__init__.py +5 -19
  22. klaude_code/core/tool/context.py +84 -0
  23. klaude_code/core/tool/file/apply_patch_tool.py +18 -21
  24. klaude_code/core/tool/file/edit_tool.py +42 -44
  25. klaude_code/core/tool/file/read_tool.py +14 -9
  26. klaude_code/core/tool/file/write_tool.py +12 -13
  27. klaude_code/core/tool/report_back_tool.py +4 -1
  28. klaude_code/core/tool/shell/bash_tool.py +6 -11
  29. klaude_code/core/tool/skill/skill_tool.py +3 -1
  30. klaude_code/core/tool/sub_agent_tool.py +8 -7
  31. klaude_code/core/tool/todo/todo_write_tool.py +3 -9
  32. klaude_code/core/tool/todo/update_plan_tool.py +3 -5
  33. klaude_code/core/tool/tool_abc.py +2 -1
  34. klaude_code/core/tool/tool_registry.py +2 -33
  35. klaude_code/core/tool/tool_runner.py +13 -10
  36. klaude_code/core/tool/web/mermaid_tool.py +3 -1
  37. klaude_code/core/tool/web/web_fetch_tool.py +5 -3
  38. klaude_code/core/tool/web/web_search_tool.py +5 -3
  39. klaude_code/core/turn.py +86 -26
  40. klaude_code/llm/anthropic/client.py +1 -1
  41. klaude_code/llm/bedrock/client.py +1 -1
  42. klaude_code/llm/claude/client.py +1 -1
  43. klaude_code/llm/codex/client.py +1 -1
  44. klaude_code/llm/google/client.py +1 -1
  45. klaude_code/llm/openai_compatible/client.py +1 -1
  46. klaude_code/llm/openai_compatible/tool_call_accumulator.py +1 -1
  47. klaude_code/llm/openrouter/client.py +1 -1
  48. klaude_code/llm/openrouter/reasoning.py +1 -1
  49. klaude_code/llm/responses/client.py +1 -1
  50. klaude_code/protocol/events/__init__.py +57 -0
  51. klaude_code/protocol/events/base.py +18 -0
  52. klaude_code/protocol/events/chat.py +20 -0
  53. klaude_code/protocol/events/lifecycle.py +22 -0
  54. klaude_code/protocol/events/metadata.py +15 -0
  55. klaude_code/protocol/events/streaming.py +43 -0
  56. klaude_code/protocol/events/system.py +53 -0
  57. klaude_code/protocol/events/tools.py +23 -0
  58. klaude_code/protocol/message.py +3 -11
  59. klaude_code/protocol/model.py +78 -9
  60. klaude_code/protocol/op.py +5 -0
  61. klaude_code/protocol/sub_agent/explore.py +0 -15
  62. klaude_code/protocol/sub_agent/task.py +1 -1
  63. klaude_code/protocol/sub_agent/web.py +1 -17
  64. klaude_code/protocol/tools.py +0 -1
  65. klaude_code/session/session.py +6 -5
  66. klaude_code/skill/assets/create-plan/SKILL.md +76 -0
  67. klaude_code/skill/loader.py +1 -1
  68. klaude_code/skill/system_skills.py +1 -1
  69. klaude_code/tui/__init__.py +8 -0
  70. klaude_code/{command → tui/command}/clear_cmd.py +2 -1
  71. klaude_code/{command → tui/command}/debug_cmd.py +4 -3
  72. klaude_code/{command → tui/command}/export_cmd.py +2 -1
  73. klaude_code/{command → tui/command}/export_online_cmd.py +6 -5
  74. klaude_code/{command → tui/command}/fork_session_cmd.py +10 -9
  75. klaude_code/{command → tui/command}/help_cmd.py +3 -2
  76. klaude_code/{command → tui/command}/model_cmd.py +5 -4
  77. klaude_code/{command → tui/command}/model_select.py +2 -2
  78. klaude_code/{command → tui/command}/prompt_command.py +4 -3
  79. klaude_code/{command → tui/command}/refresh_cmd.py +3 -1
  80. klaude_code/{command → tui/command}/registry.py +16 -6
  81. klaude_code/{command → tui/command}/release_notes_cmd.py +3 -2
  82. klaude_code/{command → tui/command}/resume_cmd.py +6 -5
  83. klaude_code/{command → tui/command}/status_cmd.py +4 -3
  84. klaude_code/{command → tui/command}/terminal_setup_cmd.py +4 -3
  85. klaude_code/{command → tui/command}/thinking_cmd.py +4 -3
  86. klaude_code/tui/commands.py +164 -0
  87. klaude_code/{ui/renderers → tui/components}/assistant.py +3 -3
  88. klaude_code/{ui/renderers → tui/components}/bash_syntax.py +2 -2
  89. klaude_code/{ui/renderers → tui/components}/common.py +1 -1
  90. klaude_code/tui/components/developer.py +231 -0
  91. klaude_code/{ui/renderers → tui/components}/diffs.py +2 -2
  92. klaude_code/{ui/renderers → tui/components}/errors.py +2 -2
  93. klaude_code/{ui/renderers → tui/components}/metadata.py +34 -21
  94. klaude_code/{ui → tui/components}/rich/markdown.py +78 -34
  95. klaude_code/{ui → tui/components}/rich/status.py +2 -2
  96. klaude_code/{ui → tui/components}/rich/theme.py +12 -5
  97. klaude_code/{ui/renderers → tui/components}/sub_agent.py +23 -43
  98. klaude_code/{ui/renderers → tui/components}/thinking.py +3 -3
  99. klaude_code/{ui/renderers → tui/components}/tools.py +11 -48
  100. klaude_code/{ui/renderers → tui/components}/user_input.py +3 -20
  101. klaude_code/tui/display.py +85 -0
  102. klaude_code/{ui/modes/repl → tui/input}/__init__.py +1 -1
  103. klaude_code/{ui/modes/repl → tui/input}/completers.py +1 -1
  104. klaude_code/{ui/modes/repl/input_prompt_toolkit.py → tui/input/prompt_toolkit.py} +11 -7
  105. klaude_code/tui/machine.py +606 -0
  106. klaude_code/tui/renderer.py +707 -0
  107. klaude_code/tui/runner.py +321 -0
  108. klaude_code/tui/terminal/__init__.py +56 -0
  109. klaude_code/{ui → tui}/terminal/color.py +1 -1
  110. klaude_code/{ui → tui}/terminal/control.py +1 -1
  111. klaude_code/{ui → tui}/terminal/notifier.py +1 -1
  112. klaude_code/{ui → tui}/terminal/selector.py +36 -17
  113. klaude_code/ui/__init__.py +6 -50
  114. klaude_code/ui/core/display.py +3 -3
  115. klaude_code/ui/core/input.py +2 -1
  116. klaude_code/ui/{modes/debug/display.py → debug_mode.py} +1 -1
  117. klaude_code/ui/{modes/exec/display.py → exec_mode.py} +1 -4
  118. klaude_code/ui/terminal/__init__.py +6 -54
  119. klaude_code/ui/terminal/title.py +31 -0
  120. klaude_code/update.py +163 -0
  121. {klaude_code-2.0.1.dist-info → klaude_code-2.1.0.dist-info}/METADATA +1 -1
  122. klaude_code-2.1.0.dist-info/RECORD +235 -0
  123. klaude_code/cli/runtime.py +0 -525
  124. klaude_code/core/prompt.py +0 -108
  125. klaude_code/core/tool/file/move_tool.md +0 -41
  126. klaude_code/core/tool/file/move_tool.py +0 -435
  127. klaude_code/core/tool/tool_context.py +0 -148
  128. klaude_code/protocol/events.py +0 -194
  129. klaude_code/skill/assets/dev-docs/SKILL.md +0 -108
  130. klaude_code/trace/__init__.py +0 -21
  131. klaude_code/ui/core/stage_manager.py +0 -48
  132. klaude_code/ui/modes/__init__.py +0 -1
  133. klaude_code/ui/modes/debug/__init__.py +0 -1
  134. klaude_code/ui/modes/exec/__init__.py +0 -1
  135. klaude_code/ui/modes/repl/display.py +0 -61
  136. klaude_code/ui/modes/repl/event_handler.py +0 -634
  137. klaude_code/ui/modes/repl/renderer.py +0 -463
  138. klaude_code/ui/renderers/developer.py +0 -215
  139. klaude_code/ui/utils/__init__.py +0 -1
  140. klaude_code-2.0.1.dist-info/RECORD +0 -229
  141. /klaude_code/{trace/log.py → log.py} +0 -0
  142. /klaude_code/{command → tui/command}/__init__.py +0 -0
  143. /klaude_code/{command → tui/command}/command_abc.py +0 -0
  144. /klaude_code/{command → tui/command}/prompt-commit.md +0 -0
  145. /klaude_code/{command → tui/command}/prompt-init.md +0 -0
  146. /klaude_code/{ui/renderers → tui/components}/__init__.py +0 -0
  147. /klaude_code/{ui/renderers → tui/components}/mermaid_viewer.py +0 -0
  148. /klaude_code/{ui → tui/components}/rich/__init__.py +0 -0
  149. /klaude_code/{ui → tui/components}/rich/cjk_wrap.py +0 -0
  150. /klaude_code/{ui → tui/components}/rich/code_panel.py +0 -0
  151. /klaude_code/{ui → tui/components}/rich/live.py +0 -0
  152. /klaude_code/{ui → tui/components}/rich/quote.py +0 -0
  153. /klaude_code/{ui → tui/components}/rich/searchable_text.py +0 -0
  154. /klaude_code/{ui/modes/repl → tui/input}/clipboard.py +0 -0
  155. /klaude_code/{ui/modes/repl → tui/input}/key_bindings.py +0 -0
  156. /klaude_code/{ui → tui}/terminal/image.py +0 -0
  157. /klaude_code/{ui → tui}/terminal/progress_bar.py +0 -0
  158. /klaude_code/ui/{utils/common.py → common.py} +0 -0
  159. {klaude_code-2.0.1.dist-info → klaude_code-2.1.0.dist-info}/WHEEL +0 -0
  160. {klaude_code-2.0.1.dist-info → klaude_code-2.1.0.dist-info}/entry_points.txt +0 -0
@@ -1,463 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import contextlib
4
- from collections.abc import Iterator
5
- from contextlib import contextmanager
6
- from dataclasses import dataclass
7
- from typing import Any
8
-
9
- from rich.console import Console, Group, RenderableType
10
- from rich.padding import Padding
11
- from rich.spinner import Spinner
12
- from rich.style import Style, StyleType
13
- from rich.text import Text
14
-
15
- from klaude_code.const import (
16
- MARKDOWN_STREAM_LIVE_REPAINT_ENABLED,
17
- STATUS_DEFAULT_TEXT,
18
- STREAM_MAX_HEIGHT_SHRINK_RESET_LINES,
19
- )
20
- from klaude_code.protocol import events, model, tools
21
- from klaude_code.ui.renderers import assistant as r_assistant
22
- from klaude_code.ui.renderers import developer as r_developer
23
- from klaude_code.ui.renderers import errors as r_errors
24
- from klaude_code.ui.renderers import mermaid_viewer as r_mermaid_viewer
25
- from klaude_code.ui.renderers import metadata as r_metadata
26
- from klaude_code.ui.renderers import sub_agent as r_sub_agent
27
- from klaude_code.ui.renderers import thinking as r_thinking
28
- from klaude_code.ui.renderers import tools as r_tools
29
- from klaude_code.ui.renderers import user_input as r_user_input
30
- from klaude_code.ui.renderers.common import truncate_head, truncate_middle
31
- from klaude_code.ui.rich import status as r_status
32
- from klaude_code.ui.rich.live import CropAboveLive, SingleLine
33
- from klaude_code.ui.rich.quote import Quote
34
- from klaude_code.ui.rich.status import BreathingSpinner, ShimmerStatusText
35
- from klaude_code.ui.rich.theme import ThemeKey, get_theme
36
-
37
-
38
- @dataclass
39
- class SessionStatus:
40
- color: Style | None = None
41
- color_index: int | None = None
42
- sub_agent_state: model.SubAgentState | None = None
43
-
44
-
45
- class REPLRenderer:
46
- """Render REPL content via a Rich console."""
47
-
48
- def __init__(self, theme: str | None = None):
49
- self.themes = get_theme(theme)
50
- self.console: Console = Console(theme=self.themes.app_theme)
51
- self.console.push_theme(self.themes.markdown_theme)
52
- self._bottom_live: CropAboveLive | None = None
53
- self._stream_renderable: RenderableType | None = None
54
- self._stream_max_height: int = 0
55
- self._stream_last_height: int = 0
56
- self._stream_last_width: int = 0
57
- self._spinner_visible: bool = False
58
-
59
- self._status_text: ShimmerStatusText = ShimmerStatusText(STATUS_DEFAULT_TEXT)
60
- self._status_spinner: Spinner = BreathingSpinner(
61
- r_status.spinner_name(),
62
- text=SingleLine(self._status_text),
63
- style=ThemeKey.STATUS_SPINNER,
64
- )
65
-
66
- self.session_map: dict[str, SessionStatus] = {}
67
- self.current_sub_agent_color: Style | None = None
68
- self.sub_agent_color_index = 0
69
-
70
- def register_session(self, session_id: str, sub_agent_state: model.SubAgentState | None = None) -> None:
71
- session_status = SessionStatus(
72
- sub_agent_state=sub_agent_state,
73
- )
74
- if sub_agent_state is not None:
75
- color, color_index = self.pick_sub_agent_color()
76
- session_status.color = color
77
- session_status.color_index = color_index
78
- self.session_map[session_id] = session_status
79
-
80
- def is_sub_agent_session(self, session_id: str) -> bool:
81
- return session_id in self.session_map and self.session_map[session_id].sub_agent_state is not None
82
-
83
- def should_display_sub_agent_thinking_header(self, session_id: str) -> bool:
84
- # Hardcoded: only show sub-agent thinking headers for ImageGen.
85
- status = self.session_map.get(session_id)
86
- if status is None or status.sub_agent_state is None:
87
- return False
88
- return status.sub_agent_state.sub_agent_type == "ImageGen"
89
-
90
- def _advance_sub_agent_color_index(self) -> None:
91
- palette_size = len(self.themes.sub_agent_colors)
92
- if palette_size == 0:
93
- self.sub_agent_color_index = 0
94
- return
95
- self.sub_agent_color_index = (self.sub_agent_color_index + 1) % palette_size
96
-
97
- def pick_sub_agent_color(self) -> tuple[Style, int]:
98
- self._advance_sub_agent_color_index()
99
- palette = self.themes.sub_agent_colors
100
- if not palette:
101
- return Style(), 0
102
- return palette[self.sub_agent_color_index], self.sub_agent_color_index
103
-
104
- def get_session_sub_agent_color(self, session_id: str) -> Style:
105
- status = self.session_map.get(session_id)
106
- if status and status.color:
107
- return status.color
108
- return Style()
109
-
110
- def get_session_sub_agent_background(self, session_id: str) -> Style:
111
- status = self.session_map.get(session_id)
112
- backgrounds = self.themes.sub_agent_backgrounds
113
- if status and status.color_index is not None and backgrounds:
114
- return backgrounds[status.color_index]
115
- return Style()
116
-
117
- @contextmanager
118
- def session_print_context(self, session_id: str) -> Iterator[None]:
119
- """Temporarily switch to sub-agent quote style."""
120
- if session_id in self.session_map and self.session_map[session_id].color:
121
- self.current_sub_agent_color = self.session_map[session_id].color
122
- try:
123
- yield
124
- finally:
125
- self.current_sub_agent_color = None
126
-
127
- def print(self, *objects: Any, style: StyleType | None = None, end: str = "\n") -> None:
128
- if self.current_sub_agent_color:
129
- if objects:
130
- content = objects[0] if len(objects) == 1 else objects
131
- self.console.print(Quote(content, style=self.current_sub_agent_color), overflow="ellipsis")
132
- return
133
- self.console.print(*objects, style=style, end=end, overflow="ellipsis")
134
-
135
- def display_tool_call(self, e: events.ToolCallEvent) -> None:
136
- if r_tools.is_sub_agent_tool(e.tool_name):
137
- return
138
- renderable = r_tools.render_tool_call(e)
139
- if renderable is not None:
140
- self.print(renderable)
141
-
142
- def display_tool_call_result(self, e: events.ToolResultEvent, *, is_sub_agent: bool = False) -> None:
143
- if r_tools.is_sub_agent_tool(e.tool_name):
144
- return
145
- # Sub-agent errors: show only first 2 lines
146
- if is_sub_agent and e.status == "error":
147
- error_msg = truncate_head(e.result)
148
- self.print(r_errors.render_tool_error(error_msg))
149
- return
150
- if not is_sub_agent and e.tool_name == tools.MERMAID and isinstance(e.ui_extra, model.MermaidLinkUIExtra):
151
- image_path = r_mermaid_viewer.download_mermaid_png(
152
- link=e.ui_extra.link,
153
- tool_call_id=e.tool_call_id,
154
- session_id=e.session_id,
155
- )
156
- if image_path is not None:
157
- self.display_image(str(image_path), height=None)
158
-
159
- renderable = r_tools.render_tool_result(e, code_theme=self.themes.code_theme, session_id=e.session_id)
160
- else:
161
- renderable = r_tools.render_tool_result(e, code_theme=self.themes.code_theme, session_id=e.session_id)
162
- if renderable is not None:
163
- self.print(renderable)
164
-
165
- def display_thinking(self, content: str) -> None:
166
- renderable = r_thinking.render_thinking(
167
- content,
168
- code_theme=self.themes.code_theme,
169
- style=ThemeKey.THINKING,
170
- )
171
- if renderable is not None:
172
- self.console.push_theme(theme=self.themes.thinking_markdown_theme)
173
- self.print(renderable)
174
- self.console.pop_theme()
175
- self.print()
176
-
177
- def display_thinking_header(self, header: str) -> None:
178
- """Display a single thinking header line.
179
-
180
- Used by sub-agent sessions to avoid verbose thinking streaming.
181
- """
182
-
183
- stripped = header.strip()
184
- if not stripped:
185
- return
186
- self.print(
187
- Text.assemble(
188
- (r_thinking.THINKING_MESSAGE_MARK, ThemeKey.THINKING),
189
- " ",
190
- (stripped, ThemeKey.THINKING_BOLD),
191
- )
192
- )
193
-
194
- async def replay_history(self, history_events: events.ReplayHistoryEvent) -> None:
195
- tool_call_dict: dict[str, events.ToolCallEvent] = {}
196
- self.print()
197
- for event in history_events.events:
198
- event_session_id = getattr(event, "session_id", history_events.session_id)
199
- is_sub_agent = self.is_sub_agent_session(event_session_id)
200
-
201
- with self.session_print_context(event_session_id):
202
- match event:
203
- case events.TaskStartEvent() as e:
204
- self.display_task_start(e)
205
- case events.TurnStartEvent():
206
- self.print()
207
- case events.AssistantImageDeltaEvent() as e:
208
- self.display_image(e.file_path)
209
- case events.AssistantMessageEvent() as e:
210
- if is_sub_agent:
211
- if self.should_display_sub_agent_thinking_header(event_session_id) and e.thinking_text:
212
- header = r_thinking.extract_last_bold_header(
213
- r_thinking.normalize_thinking_content(e.thinking_text)
214
- )
215
- if header:
216
- self.display_thinking_header(header)
217
- continue
218
- if e.thinking_text:
219
- self.display_thinking(e.thinking_text)
220
- renderable = r_assistant.render_assistant_message(e.content, code_theme=self.themes.code_theme)
221
- if renderable is not None:
222
- self.print(renderable)
223
- self.print()
224
- case events.DeveloperMessageEvent() as e:
225
- self.display_developer_message(e)
226
- self.display_command_output(e)
227
- case events.UserMessageEvent() as e:
228
- if is_sub_agent:
229
- continue
230
- self.print(r_user_input.render_user_input(e.content))
231
- case events.ToolCallEvent() as e:
232
- tool_call_dict[e.tool_call_id] = e
233
- case events.ToolResultEvent() as e:
234
- tool_call_event = tool_call_dict.get(e.tool_call_id)
235
- if tool_call_event is not None:
236
- self.display_tool_call(tool_call_event)
237
- tool_call_dict.pop(e.tool_call_id, None)
238
- if is_sub_agent:
239
- continue
240
- self.display_tool_call_result(e)
241
- case events.TaskMetadataEvent() as e:
242
- self.print()
243
- self.print(r_metadata.render_task_metadata(e))
244
- self.print()
245
- case events.InterruptEvent():
246
- self.print()
247
- self.print(r_user_input.render_interrupt())
248
- case events.ErrorEvent() as e:
249
- self.display_error(e)
250
- case events.TaskFinishEvent() as e:
251
- self.display_task_finish(e)
252
-
253
- def display_developer_message(self, e: events.DeveloperMessageEvent) -> None:
254
- if not r_developer.need_render_developer_message(e):
255
- return
256
- with self.session_print_context(e.session_id):
257
- self.print(r_developer.render_developer_message(e))
258
-
259
- def display_command_output(self, e: events.DeveloperMessageEvent) -> None:
260
- if not e.item.command_output:
261
- return
262
- with self.session_print_context(e.session_id):
263
- self.print(r_developer.render_command_output(e))
264
- self.print()
265
-
266
- def display_welcome(self, event: events.WelcomeEvent) -> None:
267
- self.print(r_metadata.render_welcome(event))
268
-
269
- def display_user_message(self, event: events.UserMessageEvent) -> None:
270
- self.print(r_user_input.render_user_input(event.content))
271
-
272
- def display_task_start(self, event: events.TaskStartEvent) -> None:
273
- self.register_session(event.session_id, event.sub_agent_state)
274
- if event.sub_agent_state is not None:
275
- with self.session_print_context(event.session_id):
276
- self.print(
277
- r_sub_agent.render_sub_agent_call(
278
- event.sub_agent_state,
279
- self.get_session_sub_agent_color(event.session_id),
280
- )
281
- )
282
-
283
- def display_turn_start(self, event: events.TurnStartEvent) -> None:
284
- if not self.is_sub_agent_session(event.session_id):
285
- self.print()
286
-
287
- def display_assistant_message(self, content: str) -> None:
288
- renderable = r_assistant.render_assistant_message(content, code_theme=self.themes.code_theme)
289
- if renderable is not None:
290
- self.print(renderable)
291
- self.print()
292
-
293
- def display_image(self, file_path: str, height: int | None = 40) -> None:
294
- """Display an image in the terminal.
295
-
296
- Args:
297
- file_path: Path to the image file.
298
- height: Height in terminal lines for displaying the image.
299
- """
300
- from klaude_code.ui.terminal.image import print_kitty_image
301
-
302
- # Suspend the Live status bar while emitting raw terminal output to avoid
303
- # interleaving refreshes with Kitty graphics escape sequences.
304
- had_live = self._bottom_live is not None
305
- was_spinner_visible = self._spinner_visible
306
- has_stream = MARKDOWN_STREAM_LIVE_REPAINT_ENABLED and self._stream_renderable is not None
307
- resume_live = had_live and (was_spinner_visible or has_stream)
308
-
309
- if self._bottom_live is not None:
310
- with contextlib.suppress(Exception):
311
- self._bottom_live.stop()
312
- self._bottom_live = None
313
-
314
- try:
315
- print_kitty_image(file_path, height=height, file=self.console.file)
316
- finally:
317
- if resume_live:
318
- if was_spinner_visible:
319
- self.spinner_start()
320
- else:
321
- self._ensure_bottom_live_started()
322
- self._refresh_bottom_live()
323
-
324
- def display_task_metadata(self, event: events.TaskMetadataEvent) -> None:
325
- with self.session_print_context(event.session_id):
326
- self.print(r_metadata.render_task_metadata(event))
327
- self.print()
328
-
329
- def display_task_finish(self, event: events.TaskFinishEvent) -> None:
330
- if self.is_sub_agent_session(event.session_id):
331
- session_status = self.session_map.get(event.session_id)
332
- description = (
333
- session_status.sub_agent_state.sub_agent_desc
334
- if session_status and session_status.sub_agent_state
335
- else None
336
- )
337
- panel_style = self.get_session_sub_agent_background(event.session_id)
338
- with self.session_print_context(event.session_id):
339
- self.print(
340
- r_sub_agent.render_sub_agent_result(
341
- event.task_result,
342
- code_theme=self.themes.code_theme,
343
- has_structured_output=event.has_structured_output,
344
- description=description,
345
- panel_style=panel_style,
346
- )
347
- )
348
-
349
- def display_interrupt(self) -> None:
350
- self.print(r_user_input.render_interrupt())
351
-
352
- def display_error(self, event: events.ErrorEvent) -> None:
353
- if event.session_id:
354
- with self.session_print_context(event.session_id):
355
- self.print(r_errors.render_error(truncate_middle(event.error_message)))
356
- else:
357
- self.print(r_errors.render_error(truncate_middle(event.error_message)))
358
-
359
- # -------------------------------------------------------------------------
360
- # Spinner control methods
361
- # -------------------------------------------------------------------------
362
-
363
- def spinner_start(self) -> None:
364
- """Start the spinner animation."""
365
- self._spinner_visible = True
366
- self._ensure_bottom_live_started()
367
- self._refresh_bottom_live()
368
-
369
- def spinner_stop(self) -> None:
370
- """Stop the spinner animation."""
371
- self._spinner_visible = False
372
- self._refresh_bottom_live()
373
-
374
- def spinner_update(self, status_text: str | Text, right_text: RenderableType | None = None) -> None:
375
- """Update the spinner status text with optional right-aligned text."""
376
- self._status_text = ShimmerStatusText(status_text, right_text)
377
- self._status_spinner.update(text=SingleLine(self._status_text), style=ThemeKey.STATUS_SPINNER)
378
- self._refresh_bottom_live()
379
-
380
- def spinner_renderable(self) -> Spinner:
381
- """Return the spinner's renderable for embedding in other components."""
382
- return self._status_spinner
383
-
384
- def set_stream_renderable(self, renderable: RenderableType | None) -> None:
385
- """Set the current streaming renderable displayed above the status line."""
386
-
387
- if renderable is None:
388
- self._stream_renderable = None
389
- self._stream_max_height = 0
390
- self._stream_last_height = 0
391
- self._stream_last_width = 0
392
- self._refresh_bottom_live()
393
- return
394
-
395
- self._ensure_bottom_live_started()
396
- self._stream_renderable = renderable
397
-
398
- height = len(self.console.render_lines(renderable, self.console.options, pad=False))
399
- self._stream_last_height = height
400
- self._stream_last_width = self.console.size.width
401
-
402
- if self._stream_max_height - height > STREAM_MAX_HEIGHT_SHRINK_RESET_LINES:
403
- self._stream_max_height = height
404
- else:
405
- self._stream_max_height = max(self._stream_max_height, height)
406
- self._refresh_bottom_live()
407
-
408
- def _ensure_bottom_live_started(self) -> None:
409
- if self._bottom_live is not None:
410
- return
411
- self._bottom_live = CropAboveLive(
412
- Text(""),
413
- console=self.console,
414
- refresh_per_second=30,
415
- transient=True,
416
- redirect_stdout=False,
417
- redirect_stderr=False,
418
- )
419
- self._bottom_live.start()
420
-
421
- def _bottom_renderable(self) -> RenderableType:
422
- stream_part: RenderableType = Group()
423
- gap_part: RenderableType = Group()
424
-
425
- if MARKDOWN_STREAM_LIVE_REPAINT_ENABLED:
426
- stream = self._stream_renderable
427
- if stream is not None:
428
- current_width = self.console.size.width
429
- if self._stream_last_width != current_width:
430
- height = len(self.console.render_lines(stream, self.console.options, pad=False))
431
- self._stream_last_height = height
432
- self._stream_last_width = current_width
433
-
434
- if self._stream_max_height - height > STREAM_MAX_HEIGHT_SHRINK_RESET_LINES:
435
- self._stream_max_height = height
436
- else:
437
- self._stream_max_height = max(self._stream_max_height, height)
438
- else:
439
- height = self._stream_last_height
440
-
441
- pad_lines = max(self._stream_max_height - height, 0)
442
- if pad_lines:
443
- stream = Padding(stream, (0, 0, pad_lines, 0))
444
- stream_part = stream
445
-
446
- gap_part = Text("") if self._spinner_visible else Group()
447
-
448
- status_part: RenderableType = SingleLine(self._status_spinner) if self._spinner_visible else Group()
449
- return Group(stream_part, gap_part, status_part)
450
-
451
- def _refresh_bottom_live(self) -> None:
452
- if self._bottom_live is None:
453
- return
454
- self._bottom_live.update(self._bottom_renderable(), refresh=True)
455
-
456
- def stop_bottom_live(self) -> None:
457
- if self._bottom_live is None:
458
- return
459
- with contextlib.suppress(Exception):
460
- # Avoid cursor restore when stopping right before prompt_toolkit.
461
- self._bottom_live.transient = False
462
- self._bottom_live.stop()
463
- self._bottom_live = None
@@ -1,215 +0,0 @@
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, message, model
7
- from klaude_code.ui.renderers.common import create_grid, truncate_middle
8
- from klaude_code.ui.renderers.tools import render_path
9
- from klaude_code.ui.rich.markdown import NoInsetMarkdown
10
- from klaude_code.ui.rich.theme import ThemeKey
11
-
12
- REMINDER_BULLET = " ⧉"
13
-
14
-
15
- def need_render_developer_message(e: events.DeveloperMessageEvent) -> bool:
16
- return bool(
17
- e.item.memory_paths
18
- or e.item.external_file_changes
19
- or e.item.todo_use
20
- or e.item.at_files
21
- or e.item.user_image_count
22
- or e.item.skill_name
23
- )
24
-
25
-
26
- def render_developer_message(e: events.DeveloperMessageEvent) -> RenderableType:
27
- """Render developer message details into a single group.
28
-
29
- Includes: memory paths, external file changes, todo reminder, @file operations.
30
- Command output is excluded; render it separately via `render_command_output`.
31
- """
32
- parts: list[RenderableType] = []
33
-
34
- if mp := e.item.memory_paths:
35
- grid = create_grid()
36
- grid.add_row(
37
- Text(REMINDER_BULLET, style=ThemeKey.REMINDER),
38
- Text.assemble(
39
- ("Load memory ", ThemeKey.REMINDER),
40
- Text(", ", ThemeKey.REMINDER).join(
41
- render_path(memory_path, ThemeKey.REMINDER_BOLD) for memory_path in mp
42
- ),
43
- ),
44
- )
45
- parts.append(grid)
46
-
47
- if fc := e.item.external_file_changes:
48
- grid = create_grid()
49
- for file_path in fc:
50
- grid.add_row(
51
- Text(REMINDER_BULLET, style=ThemeKey.REMINDER),
52
- Text.assemble(
53
- ("Read ", ThemeKey.REMINDER),
54
- render_path(file_path, ThemeKey.REMINDER_BOLD),
55
- (" after external changes", ThemeKey.REMINDER),
56
- ),
57
- )
58
- parts.append(grid)
59
-
60
- if e.item.todo_use:
61
- grid = create_grid()
62
- grid.add_row(
63
- Text(REMINDER_BULLET, style=ThemeKey.REMINDER),
64
- Text("Todo hasn't been updated recently", ThemeKey.REMINDER),
65
- )
66
- parts.append(grid)
67
-
68
- if e.item.at_files:
69
- grid = create_grid()
70
- # Group at_files by (operation, mentioned_in)
71
- grouped: dict[tuple[str, str | None], list[str]] = {}
72
- for at_file in e.item.at_files:
73
- key = (at_file.operation, at_file.mentioned_in)
74
- if key not in grouped:
75
- grouped[key] = []
76
- grouped[key].append(at_file.path)
77
-
78
- for (operation, mentioned_in), paths in grouped.items():
79
- path_texts = Text(", ", ThemeKey.REMINDER).join(render_path(p, ThemeKey.REMINDER_BOLD) for p in paths)
80
- if mentioned_in:
81
- grid.add_row(
82
- Text(REMINDER_BULLET, style=ThemeKey.REMINDER),
83
- Text.assemble(
84
- (f"{operation} ", ThemeKey.REMINDER),
85
- path_texts,
86
- (" mentioned in ", ThemeKey.REMINDER),
87
- render_path(mentioned_in, ThemeKey.REMINDER_BOLD),
88
- ),
89
- )
90
- else:
91
- grid.add_row(
92
- Text(REMINDER_BULLET, style=ThemeKey.REMINDER),
93
- Text.assemble(
94
- (f"{operation} ", ThemeKey.REMINDER),
95
- path_texts,
96
- ),
97
- )
98
- parts.append(grid)
99
-
100
- if uic := e.item.user_image_count:
101
- grid = create_grid()
102
- grid.add_row(
103
- Text(REMINDER_BULLET, style=ThemeKey.REMINDER),
104
- Text(f"Attached {uic} image{'s' if uic > 1 else ''}", style=ThemeKey.REMINDER),
105
- )
106
- parts.append(grid)
107
-
108
- if sn := e.item.skill_name:
109
- grid = create_grid()
110
- grid.add_row(
111
- Text(REMINDER_BULLET, style=ThemeKey.REMINDER),
112
- Text.assemble(
113
- ("Activated skill ", ThemeKey.REMINDER),
114
- (sn, ThemeKey.REMINDER_BOLD),
115
- ),
116
- )
117
- parts.append(grid)
118
-
119
- return Group(*parts) if parts else Text("")
120
-
121
-
122
- def render_command_output(e: events.DeveloperMessageEvent) -> RenderableType:
123
- """Render developer command output content."""
124
- if not e.item.command_output:
125
- return Text("")
126
-
127
- content = message.join_text_parts(e.item.parts)
128
- match e.item.command_output.command_name:
129
- case commands.CommandName.HELP:
130
- return Padding.indent(Text.from_markup(content or ""), level=2)
131
- case commands.CommandName.STATUS:
132
- return _render_status_output(e.item.command_output)
133
- case commands.CommandName.RELEASE_NOTES:
134
- return Padding.indent(NoInsetMarkdown(content or ""), level=2)
135
- case commands.CommandName.FORK_SESSION:
136
- return _render_fork_session_output(e.item.command_output)
137
- case _:
138
- content = content or "(no content)"
139
- style = ThemeKey.TOOL_RESULT if not e.item.command_output.is_error else ThemeKey.ERROR
140
- return Padding.indent(truncate_middle(content, base_style=style), level=2)
141
-
142
-
143
- def _format_tokens(tokens: int) -> str:
144
- """Format token count with K/M suffix for readability."""
145
- if tokens >= 1_000_000:
146
- return f"{tokens / 1_000_000:.2f}M"
147
- if tokens >= 1_000:
148
- return f"{tokens / 1_000:.1f}K"
149
- return str(tokens)
150
-
151
-
152
- def _format_cost(cost: float | None, currency: str = "USD") -> str:
153
- """Format cost with currency symbol."""
154
- if cost is None:
155
- return "-"
156
- symbol = "¥" if currency == "CNY" else "$"
157
- if cost < 0.01:
158
- return f"{symbol}{cost:.4f}"
159
- return f"{symbol}{cost:.2f}"
160
-
161
-
162
- def _render_fork_session_output(command_output: model.CommandOutput) -> RenderableType:
163
- """Render fork session output with usage instructions."""
164
- if not isinstance(command_output.ui_extra, model.SessionIdUIExtra):
165
- return Padding.indent(Text("(no session id)", style=ThemeKey.METADATA), level=2)
166
-
167
- grid = Table.grid(padding=(0, 1))
168
- session_id = command_output.ui_extra.session_id
169
- grid.add_column(style=ThemeKey.METADATA, overflow="fold")
170
-
171
- grid.add_row(Text("Session forked. Resume command copied to clipboard:", style=ThemeKey.METADATA))
172
- grid.add_row(Text(f" klaude --resume-by-id {session_id}", style=ThemeKey.METADATA_BOLD))
173
-
174
- return Padding.indent(grid, level=2)
175
-
176
-
177
- def _render_status_output(command_output: model.CommandOutput) -> RenderableType:
178
- """Render session status with total cost and per-model breakdown."""
179
- if not isinstance(command_output.ui_extra, model.SessionStatusUIExtra):
180
- return Text("(no status data)", style=ThemeKey.METADATA)
181
-
182
- status = command_output.ui_extra
183
- usage = status.usage
184
-
185
- table = Table.grid(padding=(0, 2))
186
- table.add_column(style=ThemeKey.METADATA, overflow="fold")
187
- table.add_column(style=ThemeKey.METADATA, overflow="fold")
188
-
189
- # Total cost line
190
- table.add_row(
191
- Text("Total cost:", style=ThemeKey.METADATA_BOLD),
192
- Text(_format_cost(usage.total_cost, usage.currency), style=ThemeKey.METADATA_BOLD),
193
- )
194
-
195
- # Per-model breakdown
196
- if status.by_model:
197
- table.add_row(Text("Usage by model:", style=ThemeKey.METADATA_BOLD), "")
198
- for meta in status.by_model:
199
- model_label = meta.model_name
200
- if meta.provider:
201
- model_label = f"{meta.model_name} ({meta.provider.lower().replace(' ', '-')})"
202
-
203
- if meta.usage:
204
- usage_detail = (
205
- f"{_format_tokens(meta.usage.input_tokens)} input, "
206
- f"{_format_tokens(meta.usage.output_tokens)} output, "
207
- f"{_format_tokens(meta.usage.cached_tokens)} cache read, "
208
- f"{_format_tokens(meta.usage.reasoning_tokens)} thinking, "
209
- f"({_format_cost(meta.usage.total_cost, meta.usage.currency)})"
210
- )
211
- else:
212
- usage_detail = "(no usage data)"
213
- table.add_row(f"{model_label}:", usage_detail)
214
-
215
- return Padding.indent(table, level=2)
@@ -1 +0,0 @@
1
- # UI utilities