klaude-code 2.0.2__py3-none-any.whl → 2.1.1__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 (157) 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 +9 -1
  14. klaude_code/core/agent.py +9 -62
  15. klaude_code/core/agent_profile.py +291 -0
  16. klaude_code/core/executor.py +335 -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 +84 -103
  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 +39 -42
  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/sub_agent_tool.py +8 -7
  30. klaude_code/core/tool/todo/todo_write_tool.py +3 -9
  31. klaude_code/core/tool/todo/update_plan_tool.py +3 -5
  32. klaude_code/core/tool/tool_abc.py +2 -1
  33. klaude_code/core/tool/tool_registry.py +2 -33
  34. klaude_code/core/tool/tool_runner.py +13 -10
  35. klaude_code/core/tool/web/mermaid_tool.py +3 -1
  36. klaude_code/core/tool/web/web_fetch_tool.py +5 -3
  37. klaude_code/core/tool/web/web_search_tool.py +5 -3
  38. klaude_code/core/turn.py +87 -30
  39. klaude_code/llm/anthropic/client.py +1 -1
  40. klaude_code/llm/bedrock/client.py +1 -1
  41. klaude_code/llm/claude/client.py +1 -1
  42. klaude_code/llm/codex/client.py +1 -1
  43. klaude_code/llm/google/client.py +1 -1
  44. klaude_code/llm/openai_compatible/client.py +1 -1
  45. klaude_code/llm/openai_compatible/tool_call_accumulator.py +1 -1
  46. klaude_code/llm/openrouter/client.py +1 -1
  47. klaude_code/llm/openrouter/reasoning.py +1 -1
  48. klaude_code/llm/responses/client.py +1 -1
  49. klaude_code/protocol/commands.py +1 -0
  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 +27 -0
  58. klaude_code/protocol/op.py +5 -0
  59. klaude_code/protocol/tools.py +0 -1
  60. klaude_code/session/session.py +6 -7
  61. klaude_code/skill/assets/create-plan/SKILL.md +76 -0
  62. klaude_code/skill/loader.py +32 -88
  63. klaude_code/skill/manager.py +38 -0
  64. klaude_code/skill/system_skills.py +1 -1
  65. klaude_code/tui/__init__.py +8 -0
  66. klaude_code/{command → tui/command}/__init__.py +3 -0
  67. klaude_code/{command → tui/command}/clear_cmd.py +2 -1
  68. klaude_code/tui/command/copy_cmd.py +53 -0
  69. klaude_code/{command → tui/command}/debug_cmd.py +3 -2
  70. klaude_code/{command → tui/command}/export_cmd.py +2 -1
  71. klaude_code/{command → tui/command}/export_online_cmd.py +2 -1
  72. klaude_code/{command → tui/command}/fork_session_cmd.py +4 -3
  73. klaude_code/{command → tui/command}/help_cmd.py +2 -1
  74. klaude_code/{command → tui/command}/model_cmd.py +4 -3
  75. klaude_code/{command → tui/command}/model_select.py +2 -2
  76. klaude_code/{command → tui/command}/prompt_command.py +4 -3
  77. klaude_code/{command → tui/command}/refresh_cmd.py +3 -1
  78. klaude_code/{command → tui/command}/registry.py +6 -5
  79. klaude_code/{command → tui/command}/release_notes_cmd.py +2 -1
  80. klaude_code/{command → tui/command}/resume_cmd.py +4 -3
  81. klaude_code/{command → tui/command}/status_cmd.py +2 -1
  82. klaude_code/{command → tui/command}/terminal_setup_cmd.py +2 -1
  83. klaude_code/{command → tui/command}/thinking_cmd.py +3 -2
  84. klaude_code/tui/commands.py +164 -0
  85. klaude_code/{ui/renderers → tui/components}/assistant.py +3 -3
  86. klaude_code/{ui/renderers → tui/components}/bash_syntax.py +2 -2
  87. klaude_code/{ui/renderers → tui/components}/common.py +1 -1
  88. klaude_code/{ui/renderers → tui/components}/developer.py +4 -4
  89. klaude_code/{ui/renderers → tui/components}/diffs.py +2 -2
  90. klaude_code/{ui/renderers → tui/components}/errors.py +2 -2
  91. klaude_code/{ui/renderers → tui/components}/metadata.py +7 -7
  92. klaude_code/{ui → tui/components}/rich/markdown.py +9 -23
  93. klaude_code/{ui → tui/components}/rich/status.py +2 -2
  94. klaude_code/{ui → tui/components}/rich/theme.py +3 -1
  95. klaude_code/{ui/renderers → tui/components}/sub_agent.py +23 -43
  96. klaude_code/{ui/renderers → tui/components}/thinking.py +3 -3
  97. klaude_code/{ui/renderers → tui/components}/tools.py +13 -17
  98. klaude_code/{ui/renderers → tui/components}/user_input.py +3 -20
  99. klaude_code/tui/display.py +85 -0
  100. klaude_code/{ui/modes/repl → tui/input}/__init__.py +1 -1
  101. klaude_code/{ui/modes/repl → tui/input}/completers.py +1 -1
  102. klaude_code/{ui/modes/repl/input_prompt_toolkit.py → tui/input/prompt_toolkit.py} +6 -6
  103. klaude_code/tui/machine.py +608 -0
  104. klaude_code/tui/renderer.py +707 -0
  105. klaude_code/tui/runner.py +321 -0
  106. klaude_code/tui/terminal/__init__.py +56 -0
  107. klaude_code/{ui → tui}/terminal/color.py +1 -1
  108. klaude_code/{ui → tui}/terminal/control.py +1 -1
  109. klaude_code/{ui → tui}/terminal/notifier.py +1 -1
  110. klaude_code/ui/__init__.py +6 -50
  111. klaude_code/ui/core/display.py +3 -3
  112. klaude_code/ui/core/input.py +2 -1
  113. klaude_code/ui/{modes/debug/display.py → debug_mode.py} +1 -1
  114. klaude_code/ui/{modes/exec/display.py → exec_mode.py} +0 -2
  115. klaude_code/ui/terminal/__init__.py +6 -54
  116. klaude_code/ui/terminal/title.py +31 -0
  117. klaude_code/update.py +163 -0
  118. {klaude_code-2.0.2.dist-info → klaude_code-2.1.1.dist-info}/METADATA +1 -1
  119. klaude_code-2.1.1.dist-info/RECORD +233 -0
  120. klaude_code/cli/runtime.py +0 -518
  121. klaude_code/core/prompt.py +0 -108
  122. klaude_code/core/tool/skill/skill_tool.md +0 -24
  123. klaude_code/core/tool/skill/skill_tool.py +0 -87
  124. klaude_code/core/tool/tool_context.py +0 -148
  125. klaude_code/protocol/events.py +0 -195
  126. klaude_code/skill/assets/dev-docs/SKILL.md +0 -108
  127. klaude_code/trace/__init__.py +0 -21
  128. klaude_code/ui/core/stage_manager.py +0 -48
  129. klaude_code/ui/modes/__init__.py +0 -1
  130. klaude_code/ui/modes/debug/__init__.py +0 -1
  131. klaude_code/ui/modes/exec/__init__.py +0 -1
  132. klaude_code/ui/modes/repl/display.py +0 -61
  133. klaude_code/ui/modes/repl/event_handler.py +0 -629
  134. klaude_code/ui/modes/repl/renderer.py +0 -464
  135. klaude_code/ui/renderers/__init__.py +0 -0
  136. klaude_code/ui/utils/__init__.py +0 -1
  137. klaude_code-2.0.2.dist-info/RECORD +0 -227
  138. /klaude_code/{trace/log.py → log.py} +0 -0
  139. /klaude_code/{command → tui/command}/command_abc.py +0 -0
  140. /klaude_code/{command → tui/command}/prompt-commit.md +0 -0
  141. /klaude_code/{command → tui/command}/prompt-init.md +0 -0
  142. /klaude_code/{core/tool/skill → tui/components}/__init__.py +0 -0
  143. /klaude_code/{ui/renderers → tui/components}/mermaid_viewer.py +0 -0
  144. /klaude_code/{ui → tui/components}/rich/__init__.py +0 -0
  145. /klaude_code/{ui → tui/components}/rich/cjk_wrap.py +0 -0
  146. /klaude_code/{ui → tui/components}/rich/code_panel.py +0 -0
  147. /klaude_code/{ui → tui/components}/rich/live.py +0 -0
  148. /klaude_code/{ui → tui/components}/rich/quote.py +0 -0
  149. /klaude_code/{ui → tui/components}/rich/searchable_text.py +0 -0
  150. /klaude_code/{ui/modes/repl → tui/input}/clipboard.py +0 -0
  151. /klaude_code/{ui/modes/repl → tui/input}/key_bindings.py +0 -0
  152. /klaude_code/{ui → tui}/terminal/image.py +0 -0
  153. /klaude_code/{ui → tui}/terminal/progress_bar.py +0 -0
  154. /klaude_code/{ui → tui}/terminal/selector.py +0 -0
  155. /klaude_code/ui/{utils/common.py → common.py} +0 -0
  156. {klaude_code-2.0.2.dist-info → klaude_code-2.1.1.dist-info}/WHEEL +0 -0
  157. {klaude_code-2.0.2.dist-info → klaude_code-2.1.1.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,707 @@
1
+ from __future__ import annotations
2
+
3
+ import contextlib
4
+ from collections.abc import Callable, 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.rule import Rule
12
+ from rich.spinner import Spinner
13
+ from rich.style import Style, StyleType
14
+ from rich.text import Text
15
+
16
+ from klaude_code.const import (
17
+ MARKDOWN_LEFT_MARGIN,
18
+ MARKDOWN_STREAM_LIVE_REPAINT_ENABLED,
19
+ STATUS_DEFAULT_TEXT,
20
+ STREAM_MAX_HEIGHT_SHRINK_RESET_LINES,
21
+ )
22
+ from klaude_code.protocol import events, model, tools
23
+ from klaude_code.tui.commands import (
24
+ AppendAssistant,
25
+ AppendThinking,
26
+ EmitOsc94Error,
27
+ EmitTmuxSignal,
28
+ EndAssistantStream,
29
+ EndThinkingStream,
30
+ PrintBlankLine,
31
+ PrintRuleLine,
32
+ RenderAssistantImage,
33
+ RenderCommand,
34
+ RenderDeveloperMessage,
35
+ RenderError,
36
+ RenderInterrupt,
37
+ RenderReplayHistory,
38
+ RenderTaskFinish,
39
+ RenderTaskMetadata,
40
+ RenderTaskStart,
41
+ RenderThinkingHeader,
42
+ RenderToolCall,
43
+ RenderToolResult,
44
+ RenderTurnStart,
45
+ RenderUserMessage,
46
+ RenderWelcome,
47
+ SpinnerStart,
48
+ SpinnerStop,
49
+ SpinnerUpdate,
50
+ StartAssistantStream,
51
+ StartThinkingStream,
52
+ TaskClockClear,
53
+ TaskClockStart,
54
+ )
55
+ from klaude_code.tui.components import assistant as c_assistant
56
+ from klaude_code.tui.components import developer as c_developer
57
+ from klaude_code.tui.components import errors as c_errors
58
+ from klaude_code.tui.components import mermaid_viewer as c_mermaid_viewer
59
+ from klaude_code.tui.components import metadata as c_metadata
60
+ from klaude_code.tui.components import sub_agent as c_sub_agent
61
+ from klaude_code.tui.components import thinking as c_thinking
62
+ from klaude_code.tui.components import tools as c_tools
63
+ from klaude_code.tui.components import user_input as c_user_input
64
+ from klaude_code.tui.components.common import truncate_head, truncate_middle
65
+ from klaude_code.tui.components.rich import status as r_status
66
+ from klaude_code.tui.components.rich.live import CropAboveLive, SingleLine
67
+ from klaude_code.tui.components.rich.markdown import MarkdownStream, ThinkingMarkdown
68
+ from klaude_code.tui.components.rich.quote import Quote
69
+ from klaude_code.tui.components.rich.status import BreathingSpinner, ShimmerStatusText
70
+ from klaude_code.tui.components.rich.theme import ThemeKey, get_theme
71
+ from klaude_code.tui.terminal.image import print_kitty_image
72
+ from klaude_code.tui.terminal.notifier import (
73
+ Notification,
74
+ NotificationType,
75
+ TerminalNotifier,
76
+ emit_tmux_signal,
77
+ )
78
+ from klaude_code.tui.terminal.progress_bar import OSC94States, emit_osc94
79
+
80
+
81
+ @dataclass
82
+ class _ActiveStream:
83
+ buffer: str
84
+ mdstream: MarkdownStream
85
+
86
+ def append(self, content: str) -> None:
87
+ self.buffer += content
88
+
89
+
90
+ class _StreamState:
91
+ def __init__(self) -> None:
92
+ self._active: _ActiveStream | None = None
93
+
94
+ @property
95
+ def is_active(self) -> bool:
96
+ return self._active is not None
97
+
98
+ @property
99
+ def buffer(self) -> str:
100
+ return self._active.buffer if self._active else ""
101
+
102
+ def start(self, mdstream: MarkdownStream) -> None:
103
+ self._active = _ActiveStream(buffer="", mdstream=mdstream)
104
+
105
+ def append(self, content: str) -> None:
106
+ if self._active is None:
107
+ return
108
+ self._active.append(content)
109
+
110
+ def render(self, *, transform: Callable[[str], str] | None = None, final: bool = False) -> bool:
111
+ if self._active is None:
112
+ return False
113
+ text = self._active.buffer
114
+ if transform is not None:
115
+ text = transform(text)
116
+ self._active.mdstream.update(text, final=final)
117
+ if final:
118
+ self._active = None
119
+ return True
120
+
121
+ def finalize(self, *, transform: Callable[[str], str] | None = None) -> bool:
122
+ return self.render(transform=transform, final=True)
123
+
124
+
125
+ @dataclass
126
+ class _SessionStatus:
127
+ color: Style | None = None
128
+ color_index: int | None = None
129
+ sub_agent_state: model.SubAgentState | None = None
130
+
131
+
132
+ class TUICommandRenderer:
133
+ """Execute RenderCommand sequences and render them to the terminal.
134
+
135
+ This is the only component that performs actual terminal rendering.
136
+ """
137
+
138
+ def __init__(self, theme: str | None = None, notifier: TerminalNotifier | None = None) -> None:
139
+ self.themes = get_theme(theme)
140
+ self.console: Console = Console(theme=self.themes.app_theme)
141
+ self.console.push_theme(self.themes.markdown_theme)
142
+
143
+ self._bottom_live: CropAboveLive | None = None
144
+ self._stream_renderable: RenderableType | None = None
145
+ self._stream_max_height: int = 0
146
+ self._stream_last_height: int = 0
147
+ self._stream_last_width: int = 0
148
+ self._spinner_visible: bool = False
149
+ self._spinner_last_update_key: tuple[object, object] | None = None
150
+
151
+ self._status_text: ShimmerStatusText = ShimmerStatusText(STATUS_DEFAULT_TEXT)
152
+ self._status_spinner: Spinner = BreathingSpinner(
153
+ r_status.spinner_name(),
154
+ text=SingleLine(self._status_text),
155
+ style=ThemeKey.STATUS_SPINNER,
156
+ )
157
+
158
+ self._notifier = notifier
159
+ self._assistant_stream = _StreamState()
160
+ self._thinking_stream = _StreamState()
161
+
162
+ self._sessions: dict[str, _SessionStatus] = {}
163
+ self._current_sub_agent_color: Style | None = None
164
+ self._sub_agent_color_index = 0
165
+
166
+ # ---------------------------------------------------------------------
167
+ # Session helpers
168
+ # ---------------------------------------------------------------------
169
+
170
+ def register_session(self, session_id: str, sub_agent_state: model.SubAgentState | None = None) -> None:
171
+ st = _SessionStatus(sub_agent_state=sub_agent_state)
172
+ if sub_agent_state is not None:
173
+ color, color_index = self._pick_sub_agent_color()
174
+ st.color = color
175
+ st.color_index = color_index
176
+ self._sessions[session_id] = st
177
+
178
+ def is_sub_agent_session(self, session_id: str) -> bool:
179
+ return session_id in self._sessions and self._sessions[session_id].sub_agent_state is not None
180
+
181
+ def _should_display_sub_agent_thinking_header(self, session_id: str) -> bool:
182
+ # Hardcoded: only show sub-agent thinking headers for ImageGen.
183
+ st = self._sessions.get(session_id)
184
+ if st is None or st.sub_agent_state is None:
185
+ return False
186
+ return st.sub_agent_state.sub_agent_type == "ImageGen"
187
+
188
+ def _advance_sub_agent_color_index(self) -> None:
189
+ palette_size = len(self.themes.sub_agent_colors)
190
+ if palette_size == 0:
191
+ self._sub_agent_color_index = 0
192
+ return
193
+ self._sub_agent_color_index = (self._sub_agent_color_index + 1) % palette_size
194
+
195
+ def _pick_sub_agent_color(self) -> tuple[Style, int]:
196
+ self._advance_sub_agent_color_index()
197
+ palette = self.themes.sub_agent_colors
198
+ if not palette:
199
+ return Style(), 0
200
+ return palette[self._sub_agent_color_index], self._sub_agent_color_index
201
+
202
+ def _get_session_sub_agent_color(self, session_id: str) -> Style:
203
+ st = self._sessions.get(session_id)
204
+ if st and st.color:
205
+ return st.color
206
+ return Style()
207
+
208
+ @contextmanager
209
+ def session_print_context(self, session_id: str) -> Iterator[None]:
210
+ """Temporarily switch to sub-agent quote style."""
211
+
212
+ st = self._sessions.get(session_id)
213
+ if st is not None and st.color:
214
+ self._current_sub_agent_color = st.color
215
+ try:
216
+ yield
217
+ finally:
218
+ self._current_sub_agent_color = None
219
+
220
+ # ---------------------------------------------------------------------
221
+ # Low-level printing & bottom status
222
+ # ---------------------------------------------------------------------
223
+
224
+ def print(self, *objects: Any, style: StyleType | None = None, end: str = "\n") -> None:
225
+ if self._current_sub_agent_color:
226
+ if objects:
227
+ content = objects[0] if len(objects) == 1 else objects
228
+ self.console.print(Quote(content, style=self._current_sub_agent_color), overflow="ellipsis")
229
+ return
230
+ self.console.print(*objects, style=style, end=end, overflow="ellipsis")
231
+
232
+ def spinner_start(self) -> None:
233
+ self._spinner_visible = True
234
+ self._ensure_bottom_live_started()
235
+ self._refresh_bottom_live()
236
+
237
+ def spinner_stop(self) -> None:
238
+ self._spinner_visible = False
239
+ self._refresh_bottom_live()
240
+
241
+ def spinner_update(self, status_text: str | Text, right_text: RenderableType | None = None) -> None:
242
+ new_key = (self._spinner_text_key(status_text), self._spinner_right_text_key(right_text))
243
+ if self._spinner_last_update_key == new_key:
244
+ return
245
+ self._spinner_last_update_key = new_key
246
+
247
+ self._status_text = ShimmerStatusText(status_text, right_text)
248
+ self._status_spinner.update(text=SingleLine(self._status_text), style=ThemeKey.STATUS_SPINNER)
249
+ self._refresh_bottom_live()
250
+
251
+ @staticmethod
252
+ def _spinner_text_key(text: str | Text) -> object:
253
+ if isinstance(text, Text):
254
+ style = str(text.style) if text.style else ""
255
+ return ("Text", text.plain, style)
256
+ return ("str", text)
257
+
258
+ @staticmethod
259
+ def _spinner_right_text_key(text: RenderableType | None) -> object:
260
+ if text is None:
261
+ return ("none",)
262
+ if isinstance(text, Text):
263
+ style = str(text.style) if text.style else ""
264
+ return ("Text", text.plain, style)
265
+ if isinstance(text, str):
266
+ return ("str", text)
267
+ # Fall back to a unique key so we never skip updates for dynamic renderables.
268
+ return ("other", object())
269
+
270
+ def set_stream_renderable(self, renderable: RenderableType | None) -> None:
271
+ if renderable is None:
272
+ self._stream_renderable = None
273
+ self._stream_max_height = 0
274
+ self._stream_last_height = 0
275
+ self._stream_last_width = 0
276
+ self._refresh_bottom_live()
277
+ return
278
+
279
+ self._ensure_bottom_live_started()
280
+ self._stream_renderable = renderable
281
+
282
+ height = len(self.console.render_lines(renderable, self.console.options, pad=False))
283
+ self._stream_last_height = height
284
+ self._stream_last_width = self.console.size.width
285
+
286
+ if self._stream_max_height - height > STREAM_MAX_HEIGHT_SHRINK_RESET_LINES:
287
+ self._stream_max_height = height
288
+ else:
289
+ self._stream_max_height = max(self._stream_max_height, height)
290
+ self._refresh_bottom_live()
291
+
292
+ def _ensure_bottom_live_started(self) -> None:
293
+ if self._bottom_live is not None:
294
+ return
295
+ self._bottom_live = CropAboveLive(
296
+ Text(""),
297
+ console=self.console,
298
+ refresh_per_second=30,
299
+ transient=True,
300
+ redirect_stdout=False,
301
+ redirect_stderr=False,
302
+ )
303
+ self._bottom_live.start()
304
+
305
+ def _bottom_renderable(self) -> RenderableType:
306
+ stream_part: RenderableType = Group()
307
+ gap_part: RenderableType = Group()
308
+
309
+ if MARKDOWN_STREAM_LIVE_REPAINT_ENABLED:
310
+ stream = self._stream_renderable
311
+ if stream is not None:
312
+ current_width = self.console.size.width
313
+ if self._stream_last_width != current_width:
314
+ height = len(self.console.render_lines(stream, self.console.options, pad=False))
315
+ self._stream_last_height = height
316
+ self._stream_last_width = current_width
317
+
318
+ if self._stream_max_height - height > STREAM_MAX_HEIGHT_SHRINK_RESET_LINES:
319
+ self._stream_max_height = height
320
+ else:
321
+ self._stream_max_height = max(self._stream_max_height, height)
322
+ else:
323
+ height = self._stream_last_height
324
+
325
+ pad_lines = max(self._stream_max_height - height, 0)
326
+ if pad_lines:
327
+ stream = Padding(stream, (0, 0, pad_lines, 0))
328
+ stream_part = stream
329
+
330
+ gap_part = Text("") if self._spinner_visible else Group()
331
+
332
+ status_part: RenderableType = SingleLine(self._status_spinner) if self._spinner_visible else Group()
333
+ return Group(stream_part, gap_part, status_part)
334
+
335
+ def _refresh_bottom_live(self) -> None:
336
+ if self._bottom_live is None:
337
+ return
338
+ self._bottom_live.update(self._bottom_renderable(), refresh=True)
339
+
340
+ def stop_bottom_live(self) -> None:
341
+ if self._bottom_live is None:
342
+ return
343
+ with contextlib.suppress(Exception):
344
+ # Avoid cursor restore when stopping right before prompt_toolkit.
345
+ self._bottom_live.transient = False
346
+ self._bottom_live.stop()
347
+ self._bottom_live = None
348
+
349
+ # ---------------------------------------------------------------------
350
+ # Stream helpers (MarkdownStream)
351
+ # ---------------------------------------------------------------------
352
+
353
+ def _new_thinking_mdstream(self) -> MarkdownStream:
354
+ return MarkdownStream(
355
+ mdargs={
356
+ "code_theme": self.themes.code_theme,
357
+ "style": ThemeKey.THINKING,
358
+ },
359
+ theme=self.themes.thinking_markdown_theme,
360
+ console=self.console,
361
+ live_sink=None,
362
+ mark=c_thinking.THINKING_MESSAGE_MARK,
363
+ mark_style=ThemeKey.THINKING,
364
+ left_margin=MARKDOWN_LEFT_MARGIN,
365
+ markdown_class=ThinkingMarkdown,
366
+ )
367
+
368
+ def _new_assistant_mdstream(self) -> MarkdownStream:
369
+ return MarkdownStream(
370
+ mdargs={"code_theme": self.themes.code_theme},
371
+ theme=self.themes.markdown_theme,
372
+ console=self.console,
373
+ live_sink=self.set_stream_renderable,
374
+ mark=c_assistant.ASSISTANT_MESSAGE_MARK,
375
+ left_margin=MARKDOWN_LEFT_MARGIN,
376
+ )
377
+
378
+ def _flush_thinking(self) -> None:
379
+ self._thinking_stream.render(transform=c_thinking.normalize_thinking_content)
380
+
381
+ def _flush_assistant(self) -> None:
382
+ self._assistant_stream.render()
383
+
384
+ # ---------------------------------------------------------------------
385
+ # Event-specific rendering helpers
386
+ # ---------------------------------------------------------------------
387
+
388
+ def display_tool_call(self, e: events.ToolCallEvent) -> None:
389
+ if c_tools.is_sub_agent_tool(e.tool_name):
390
+ return
391
+ renderable = c_tools.render_tool_call(e)
392
+ if renderable is not None:
393
+ self.print(renderable)
394
+
395
+ def display_tool_call_result(self, e: events.ToolResultEvent, *, is_sub_agent: bool = False) -> None:
396
+ if c_tools.is_sub_agent_tool(e.tool_name):
397
+ return
398
+
399
+ if is_sub_agent and e.is_error:
400
+ error_msg = truncate_head(e.result)
401
+ self.print(c_errors.render_tool_error(error_msg))
402
+ return
403
+
404
+ if not is_sub_agent and e.tool_name == tools.MERMAID and isinstance(e.ui_extra, model.MermaidLinkUIExtra):
405
+ image_path = c_mermaid_viewer.download_mermaid_png(
406
+ link=e.ui_extra.link,
407
+ tool_call_id=e.tool_call_id,
408
+ session_id=e.session_id,
409
+ )
410
+ if image_path is not None:
411
+ self.display_image(str(image_path), height=None)
412
+
413
+ renderable = c_tools.render_tool_result(e, code_theme=self.themes.code_theme, session_id=e.session_id)
414
+ if renderable is not None:
415
+ self.print(renderable)
416
+
417
+ def display_thinking(self, content: str) -> None:
418
+ renderable = c_thinking.render_thinking(
419
+ content,
420
+ code_theme=self.themes.code_theme,
421
+ style=ThemeKey.THINKING,
422
+ )
423
+ if renderable is not None:
424
+ self.console.push_theme(theme=self.themes.thinking_markdown_theme)
425
+ self.print(renderable)
426
+ self.console.pop_theme()
427
+ self.print()
428
+
429
+ def display_thinking_header(self, header: str) -> None:
430
+ stripped = header.strip()
431
+ if not stripped:
432
+ return
433
+ self.print(
434
+ Text.assemble(
435
+ (c_thinking.THINKING_MESSAGE_MARK, ThemeKey.THINKING),
436
+ " ",
437
+ (stripped, ThemeKey.THINKING_BOLD),
438
+ )
439
+ )
440
+
441
+ async def replay_history(self, history_events: events.ReplayHistoryEvent) -> None:
442
+ tool_call_dict: dict[str, events.ToolCallEvent] = {}
443
+ self.print()
444
+ for event in history_events.events:
445
+ event_session_id = getattr(event, "session_id", history_events.session_id)
446
+ is_sub_agent = self.is_sub_agent_session(event_session_id)
447
+
448
+ with self.session_print_context(event_session_id):
449
+ match event:
450
+ case events.TaskStartEvent() as e:
451
+ self.display_task_start(e)
452
+ case events.TurnStartEvent():
453
+ self.print()
454
+ case events.AssistantImageDeltaEvent() as e:
455
+ self.display_image(e.file_path)
456
+ case events.ResponseCompleteEvent() as e:
457
+ if is_sub_agent:
458
+ if self._should_display_sub_agent_thinking_header(event_session_id) and e.thinking_text:
459
+ header = c_thinking.extract_last_bold_header(
460
+ c_thinking.normalize_thinking_content(e.thinking_text)
461
+ )
462
+ if header:
463
+ self.display_thinking_header(header)
464
+ continue
465
+ if e.thinking_text:
466
+ self.display_thinking(e.thinking_text)
467
+ renderable = c_assistant.render_assistant_message(e.content, code_theme=self.themes.code_theme)
468
+ if renderable is not None:
469
+ self.print(renderable)
470
+ self.print()
471
+ case events.DeveloperMessageEvent() as e:
472
+ self.display_developer_message(e)
473
+ self.display_command_output(e)
474
+ case events.UserMessageEvent() as e:
475
+ if is_sub_agent:
476
+ continue
477
+ self.print(c_user_input.render_user_input(e.content))
478
+ case events.ToolCallEvent() as e:
479
+ tool_call_dict[e.tool_call_id] = e
480
+ case events.ToolResultEvent() as e:
481
+ tool_call_event = tool_call_dict.get(e.tool_call_id)
482
+ if tool_call_event is not None:
483
+ self.display_tool_call(tool_call_event)
484
+ tool_call_dict.pop(e.tool_call_id, None)
485
+ if is_sub_agent:
486
+ continue
487
+ self.display_tool_call_result(e)
488
+ case events.TaskMetadataEvent() as e:
489
+ self.print()
490
+ self.print(c_metadata.render_task_metadata(e))
491
+ self.print()
492
+ case events.InterruptEvent():
493
+ self.print()
494
+ self.print(c_user_input.render_interrupt())
495
+ case events.ErrorEvent() as e:
496
+ self.display_error(e)
497
+ case events.TaskFinishEvent() as e:
498
+ self.display_task_finish(e)
499
+
500
+ def display_developer_message(self, e: events.DeveloperMessageEvent) -> None:
501
+ if not c_developer.need_render_developer_message(e):
502
+ return
503
+ with self.session_print_context(e.session_id):
504
+ self.print(c_developer.render_developer_message(e))
505
+
506
+ def display_command_output(self, e: events.DeveloperMessageEvent) -> None:
507
+ if not c_developer.get_command_output(e.item):
508
+ return
509
+ with self.session_print_context(e.session_id):
510
+ self.print(c_developer.render_command_output(e))
511
+ self.print()
512
+
513
+ def display_welcome(self, event: events.WelcomeEvent) -> None:
514
+ self.print(c_metadata.render_welcome(event))
515
+
516
+ def display_user_message(self, event: events.UserMessageEvent) -> None:
517
+ self.print(c_user_input.render_user_input(event.content))
518
+
519
+ def display_task_start(self, event: events.TaskStartEvent) -> None:
520
+ self.register_session(event.session_id, event.sub_agent_state)
521
+ if event.sub_agent_state is not None:
522
+ with self.session_print_context(event.session_id):
523
+ self.print(
524
+ c_sub_agent.render_sub_agent_call(
525
+ event.sub_agent_state,
526
+ self._get_session_sub_agent_color(event.session_id),
527
+ )
528
+ )
529
+
530
+ def display_turn_start(self, event: events.TurnStartEvent) -> None:
531
+ if not self.is_sub_agent_session(event.session_id):
532
+ self.print()
533
+
534
+ def display_image(self, file_path: str, height: int | None = 40) -> None:
535
+ # Suspend the Live status bar while emitting raw terminal output.
536
+ had_live = self._bottom_live is not None
537
+ was_spinner_visible = self._spinner_visible
538
+ has_stream = MARKDOWN_STREAM_LIVE_REPAINT_ENABLED and self._stream_renderable is not None
539
+ resume_live = had_live and (was_spinner_visible or has_stream)
540
+
541
+ if self._bottom_live is not None:
542
+ with contextlib.suppress(Exception):
543
+ self._bottom_live.stop()
544
+ self._bottom_live = None
545
+
546
+ try:
547
+ print_kitty_image(file_path, height=height, file=self.console.file)
548
+ finally:
549
+ if resume_live:
550
+ if was_spinner_visible:
551
+ self.spinner_start()
552
+ else:
553
+ self._ensure_bottom_live_started()
554
+ self._refresh_bottom_live()
555
+
556
+ def display_task_metadata(self, event: events.TaskMetadataEvent) -> None:
557
+ if self.is_sub_agent_session(event.session_id):
558
+ return
559
+ self.print(c_metadata.render_task_metadata(event))
560
+ self.print()
561
+
562
+ def display_task_finish(self, event: events.TaskFinishEvent) -> None:
563
+ if self.is_sub_agent_session(event.session_id):
564
+ st = self._sessions.get(event.session_id)
565
+ description = st.sub_agent_state.sub_agent_desc if st and st.sub_agent_state else None
566
+ with self.session_print_context(event.session_id):
567
+ self.print(
568
+ c_sub_agent.render_sub_agent_result(
569
+ event.task_result,
570
+ code_theme=self.themes.code_theme,
571
+ has_structured_output=event.has_structured_output,
572
+ description=description,
573
+ style=ThemeKey.TOOL_RESULT,
574
+ )
575
+ )
576
+
577
+ def display_interrupt(self) -> None:
578
+ self.print(c_user_input.render_interrupt())
579
+
580
+ def display_error(self, event: events.ErrorEvent) -> None:
581
+ if event.session_id:
582
+ with self.session_print_context(event.session_id):
583
+ self.print(c_errors.render_error(truncate_middle(event.error_message)))
584
+ else:
585
+ self.print(c_errors.render_error(truncate_middle(event.error_message)))
586
+
587
+ # ---------------------------------------------------------------------
588
+ # Notifications
589
+ # ---------------------------------------------------------------------
590
+
591
+ def _maybe_notify_task_finish(self, event: RenderTaskFinish) -> None:
592
+ if self._notifier is None:
593
+ return
594
+ if self.is_sub_agent_session(event.event.session_id):
595
+ return
596
+ body = self._compact_result_text(event.event.task_result)
597
+ notification = Notification(
598
+ type=NotificationType.AGENT_TASK_COMPLETE,
599
+ title="Task Completed",
600
+ body=body,
601
+ )
602
+ self._notifier.notify(notification)
603
+
604
+ def _compact_result_text(self, text: str) -> str | None:
605
+ stripped = text.strip()
606
+ if not stripped:
607
+ return None
608
+ squashed = " ".join(stripped.split())
609
+ if len(squashed) > 200:
610
+ return squashed[:197] + "…"
611
+ return squashed
612
+
613
+ # ---------------------------------------------------------------------
614
+ # RenderCommand executor
615
+ # ---------------------------------------------------------------------
616
+
617
+ async def execute(self, commands: list[RenderCommand]) -> None:
618
+ for cmd in commands:
619
+ match cmd:
620
+ case RenderReplayHistory(event=event):
621
+ await self.replay_history(event)
622
+ self.spinner_stop()
623
+ case RenderWelcome(event=event):
624
+ self.display_welcome(event)
625
+ case RenderUserMessage(event=event):
626
+ self.display_user_message(event)
627
+ case RenderTaskStart(event=event):
628
+ self.display_task_start(event)
629
+ case RenderDeveloperMessage(event=event):
630
+ self.display_developer_message(event)
631
+ self.display_command_output(event)
632
+ case RenderTurnStart(event=event):
633
+ self.display_turn_start(event)
634
+ case StartThinkingStream():
635
+ if not self._thinking_stream.is_active:
636
+ self._thinking_stream.start(self._new_thinking_mdstream())
637
+ case AppendThinking(content=content):
638
+ if self._thinking_stream.is_active:
639
+ first_delta = self._thinking_stream.buffer == ""
640
+ self._thinking_stream.append(content)
641
+ if first_delta:
642
+ self._thinking_stream.render(transform=c_thinking.normalize_thinking_content)
643
+ self._flush_thinking()
644
+ case EndThinkingStream():
645
+ finalized = self._thinking_stream.finalize(transform=c_thinking.normalize_thinking_content)
646
+ if finalized:
647
+ self.print()
648
+ case StartAssistantStream():
649
+ if not self._assistant_stream.is_active:
650
+ self._assistant_stream.start(self._new_assistant_mdstream())
651
+ case AppendAssistant(content=content):
652
+ if self._assistant_stream.is_active:
653
+ first_delta = self._assistant_stream.buffer == ""
654
+ self._assistant_stream.append(content)
655
+ if first_delta:
656
+ self._assistant_stream.render()
657
+ self._flush_assistant()
658
+ case EndAssistantStream():
659
+ finalized = self._assistant_stream.finalize()
660
+ if finalized:
661
+ self.print()
662
+ case RenderThinkingHeader(session_id=session_id, header=header):
663
+ with self.session_print_context(session_id):
664
+ self.display_thinking_header(header)
665
+ case RenderAssistantImage(file_path=file_path):
666
+ self.display_image(file_path)
667
+ case RenderToolCall(event=event):
668
+ with self.session_print_context(event.session_id):
669
+ self.display_tool_call(event)
670
+ case RenderToolResult(event=event, is_sub_agent_session=is_sub_agent_session):
671
+ with self.session_print_context(event.session_id):
672
+ self.display_tool_call_result(event, is_sub_agent=is_sub_agent_session)
673
+ case RenderTaskMetadata(event=event):
674
+ self.display_task_metadata(event)
675
+ case RenderTaskFinish() as cmd_finish:
676
+ self.display_task_finish(cmd_finish.event)
677
+ self._maybe_notify_task_finish(cmd_finish)
678
+ case RenderInterrupt():
679
+ self.display_interrupt()
680
+ case RenderError(event=event):
681
+ self.display_error(event)
682
+ case SpinnerStart():
683
+ self.spinner_start()
684
+ case SpinnerStop():
685
+ self.spinner_stop()
686
+ case SpinnerUpdate(status_text=status_text, right_text=right_text):
687
+ self.spinner_update(status_text, right_text)
688
+ case PrintBlankLine():
689
+ self.print()
690
+ case PrintRuleLine():
691
+ self.console.print(Rule(characters="─", style=ThemeKey.LINES))
692
+ case EmitOsc94Error():
693
+ emit_osc94(OSC94States.ERROR)
694
+ case EmitTmuxSignal():
695
+ emit_tmux_signal()
696
+ case TaskClockStart():
697
+ r_status.set_task_start()
698
+ case TaskClockClear():
699
+ r_status.clear_task_start()
700
+ case _:
701
+ continue
702
+
703
+ async def stop(self) -> None:
704
+ self._flush_assistant()
705
+ self._flush_thinking()
706
+ with contextlib.suppress(Exception):
707
+ self.spinner_stop()