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
@@ -0,0 +1,606 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+
5
+ from rich.text import Text
6
+
7
+ from klaude_code.const import (
8
+ SIGINT_DOUBLE_PRESS_EXIT_TEXT,
9
+ STATUS_COMPOSING_TEXT,
10
+ STATUS_DEFAULT_TEXT,
11
+ STATUS_THINKING_TEXT,
12
+ )
13
+ from klaude_code.protocol import events, model
14
+ from klaude_code.tui.commands import (
15
+ AppendAssistant,
16
+ AppendThinking,
17
+ EmitOsc94Error,
18
+ EmitTmuxSignal,
19
+ EndAssistantStream,
20
+ EndThinkingStream,
21
+ PrintRuleLine,
22
+ RenderAssistantImage,
23
+ RenderCommand,
24
+ RenderDeveloperMessage,
25
+ RenderError,
26
+ RenderInterrupt,
27
+ RenderReplayHistory,
28
+ RenderTaskFinish,
29
+ RenderTaskMetadata,
30
+ RenderTaskStart,
31
+ RenderThinkingHeader,
32
+ RenderToolCall,
33
+ RenderToolResult,
34
+ RenderTurnStart,
35
+ RenderUserMessage,
36
+ RenderWelcome,
37
+ SpinnerStart,
38
+ SpinnerStop,
39
+ SpinnerUpdate,
40
+ StartAssistantStream,
41
+ StartThinkingStream,
42
+ TaskClockClear,
43
+ TaskClockStart,
44
+ )
45
+ from klaude_code.tui.components.rich import status as r_status
46
+ from klaude_code.tui.components.rich.theme import ThemeKey
47
+ from klaude_code.tui.components.thinking import extract_last_bold_header, normalize_thinking_content
48
+ from klaude_code.tui.components.tools import get_tool_active_form, is_sub_agent_tool
49
+
50
+
51
+ @dataclass
52
+ class SubAgentThinkingHeaderState:
53
+ buffer: str = ""
54
+ last_header: str | None = None
55
+
56
+ def append_and_extract_new_header(self, content: str) -> str | None:
57
+ self.buffer += content
58
+
59
+ max_chars = 8192
60
+ if len(self.buffer) > max_chars:
61
+ self.buffer = self.buffer[-max_chars:]
62
+
63
+ header = extract_last_bold_header(normalize_thinking_content(self.buffer))
64
+ if header and header != self.last_header:
65
+ self.last_header = header
66
+ return header
67
+ return None
68
+
69
+
70
+ class ActivityState:
71
+ """Tracks composing/tool activity for spinner display."""
72
+
73
+ def __init__(self) -> None:
74
+ self._composing: bool = False
75
+ self._buffer_length: int = 0
76
+ self._tool_calls: dict[str, int] = {}
77
+ self._sub_agent_tool_calls: dict[str, int] = {}
78
+ self._sub_agent_tool_calls_by_id: dict[str, str] = {}
79
+
80
+ @property
81
+ def is_composing(self) -> bool:
82
+ return self._composing and not self._tool_calls and not self._sub_agent_tool_calls
83
+
84
+ def set_composing(self, composing: bool) -> None:
85
+ self._composing = composing
86
+ if not composing:
87
+ self._buffer_length = 0
88
+
89
+ def set_buffer_length(self, length: int) -> None:
90
+ self._buffer_length = length
91
+
92
+ def add_tool_call(self, tool_name: str) -> None:
93
+ self._tool_calls[tool_name] = self._tool_calls.get(tool_name, 0) + 1
94
+
95
+ def add_sub_agent_tool_call(self, tool_call_id: str, tool_name: str) -> None:
96
+ if tool_call_id in self._sub_agent_tool_calls_by_id:
97
+ return
98
+ self._sub_agent_tool_calls_by_id[tool_call_id] = tool_name
99
+ self._sub_agent_tool_calls[tool_name] = self._sub_agent_tool_calls.get(tool_name, 0) + 1
100
+
101
+ def finish_sub_agent_tool_call(self, tool_call_id: str, tool_name: str | None = None) -> None:
102
+ existing_tool_name = self._sub_agent_tool_calls_by_id.pop(tool_call_id, None)
103
+ decremented_name = existing_tool_name or tool_name
104
+ if decremented_name is None:
105
+ return
106
+
107
+ current = self._sub_agent_tool_calls.get(decremented_name, 0)
108
+ if current <= 1:
109
+ self._sub_agent_tool_calls.pop(decremented_name, None)
110
+ else:
111
+ self._sub_agent_tool_calls[decremented_name] = current - 1
112
+
113
+ def clear_tool_calls(self) -> None:
114
+ self._tool_calls = {}
115
+
116
+ def clear_for_new_turn(self) -> None:
117
+ self._composing = False
118
+ self._buffer_length = 0
119
+ self._tool_calls = {}
120
+
121
+ def reset(self) -> None:
122
+ self._composing = False
123
+ self._buffer_length = 0
124
+ self._tool_calls = {}
125
+ self._sub_agent_tool_calls = {}
126
+ self._sub_agent_tool_calls_by_id = {}
127
+
128
+ def get_activity_text(self) -> Text | None:
129
+ if self._tool_calls or self._sub_agent_tool_calls:
130
+ activity_text = Text()
131
+
132
+ def _append_counts(counts: dict[str, int]) -> None:
133
+ first = True
134
+ for name, count in counts.items():
135
+ if not first:
136
+ activity_text.append(", ")
137
+ activity_text.append(Text(name, style=ThemeKey.STATUS_TEXT_BOLD))
138
+ if count > 1:
139
+ activity_text.append(f" x {count}")
140
+ first = False
141
+
142
+ if self._sub_agent_tool_calls:
143
+ _append_counts(self._sub_agent_tool_calls)
144
+ activity_text.append(" | ")
145
+
146
+ if self._tool_calls:
147
+ _append_counts(self._tool_calls)
148
+
149
+ return activity_text
150
+
151
+ if self._composing:
152
+ text = Text()
153
+ text.append(STATUS_COMPOSING_TEXT, style=ThemeKey.STATUS_TEXT)
154
+ if self._buffer_length > 0:
155
+ text.append(f" ({self._buffer_length:,})", style=ThemeKey.STATUS_TEXT)
156
+ return text
157
+
158
+ return None
159
+
160
+
161
+ class SpinnerStatusState:
162
+ """Multi-layer spinner status state management."""
163
+
164
+ def __init__(self) -> None:
165
+ self._todo_status: str | None = None
166
+ self._reasoning_status: str | None = None
167
+ self._toast_status: str | None = None
168
+ self._activity = ActivityState()
169
+ self._context_percent: float | None = None
170
+
171
+ def reset(self) -> None:
172
+ self._todo_status = None
173
+ self._reasoning_status = None
174
+ self._toast_status = None
175
+ self._activity.reset()
176
+ self._context_percent = None
177
+
178
+ def set_toast_status(self, status: str | None) -> None:
179
+ self._toast_status = status
180
+
181
+ def set_todo_status(self, status: str | None) -> None:
182
+ self._todo_status = status
183
+
184
+ def set_reasoning_status(self, status: str | None) -> None:
185
+ self._reasoning_status = status
186
+
187
+ def clear_default_reasoning_status(self) -> None:
188
+ """Clear reasoning status only if it's the default 'Reasoning ...' text."""
189
+ if self._reasoning_status == STATUS_THINKING_TEXT:
190
+ self._reasoning_status = None
191
+
192
+ def set_composing(self, composing: bool) -> None:
193
+ if composing:
194
+ self._reasoning_status = None
195
+ self._activity.set_composing(composing)
196
+
197
+ def set_buffer_length(self, length: int) -> None:
198
+ self._activity.set_buffer_length(length)
199
+
200
+ def add_tool_call(self, tool_name: str) -> None:
201
+ self._activity.add_tool_call(tool_name)
202
+
203
+ def clear_tool_calls(self) -> None:
204
+ self._activity.clear_tool_calls()
205
+
206
+ def add_sub_agent_tool_call(self, tool_call_id: str, tool_name: str) -> None:
207
+ self._activity.add_sub_agent_tool_call(tool_call_id, tool_name)
208
+
209
+ def finish_sub_agent_tool_call(self, tool_call_id: str, tool_name: str | None = None) -> None:
210
+ self._activity.finish_sub_agent_tool_call(tool_call_id, tool_name)
211
+
212
+ def clear_for_new_turn(self) -> None:
213
+ self._activity.clear_for_new_turn()
214
+
215
+ def set_context_percent(self, percent: float) -> None:
216
+ self._context_percent = percent
217
+
218
+ def get_activity_text(self) -> Text | None:
219
+ """Expose current activity for tests and UI composition."""
220
+ return self._activity.get_activity_text()
221
+
222
+ def get_status(self) -> Text:
223
+ if self._toast_status:
224
+ return Text(self._toast_status, style=ThemeKey.STATUS_TOAST)
225
+
226
+ activity_text = self._activity.get_activity_text()
227
+ base_status = self._reasoning_status or self._todo_status
228
+
229
+ if base_status:
230
+ # Default "Reasoning ..." uses normal style; custom headers use bold italic
231
+ is_default_reasoning = base_status == STATUS_THINKING_TEXT
232
+ status_style = ThemeKey.STATUS_TEXT if is_default_reasoning else ThemeKey.STATUS_TEXT_BOLD_ITALIC
233
+ if activity_text:
234
+ result = Text()
235
+ result.append(base_status, style=status_style)
236
+ result.append(" | ")
237
+ result.append_text(activity_text)
238
+ else:
239
+ result = Text(base_status, style=status_style)
240
+ elif activity_text:
241
+ activity_text.append(" …")
242
+ result = activity_text
243
+ else:
244
+ result = Text(STATUS_DEFAULT_TEXT, style=ThemeKey.STATUS_TEXT)
245
+
246
+ return result
247
+
248
+ def get_right_text(self) -> r_status.DynamicText | None:
249
+ elapsed_text = r_status.current_elapsed_text()
250
+ has_context = self._context_percent is not None
251
+ if elapsed_text is None and not has_context:
252
+ return None
253
+
254
+ def _render() -> Text:
255
+ parts: list[str] = []
256
+ if self._context_percent is not None:
257
+ parts.append(f"{self._context_percent:.1f}%")
258
+ current_elapsed = r_status.current_elapsed_text()
259
+ if current_elapsed is not None:
260
+ if parts:
261
+ parts.append(" · ")
262
+ parts.append(current_elapsed)
263
+ return Text("".join(parts), style=ThemeKey.METADATA_DIM)
264
+
265
+ return r_status.DynamicText(_render)
266
+
267
+
268
+ @dataclass
269
+ class _SessionState:
270
+ session_id: str
271
+ sub_agent_state: model.SubAgentState | None = None
272
+ sub_agent_thinking_header: SubAgentThinkingHeaderState | None = None
273
+ assistant_stream_active: bool = False
274
+ thinking_stream_active: bool = False
275
+ assistant_char_count: int = 0
276
+ thinking_tail: str = ""
277
+
278
+ @property
279
+ def is_sub_agent(self) -> bool:
280
+ return self.sub_agent_state is not None
281
+
282
+ @property
283
+ def should_show_sub_agent_thinking_header(self) -> bool:
284
+ return bool(self.sub_agent_state and self.sub_agent_state.sub_agent_type == "ImageGen")
285
+
286
+
287
+ class DisplayStateMachine:
288
+ """Simplified, session-aware REPL UI state machine.
289
+
290
+ This machine is deterministic because protocol events have explicit streaming
291
+ boundaries (Start/Delta/End).
292
+ """
293
+
294
+ def __init__(self) -> None:
295
+ self._sessions: dict[str, _SessionState] = {}
296
+ self._primary_session_id: str | None = None
297
+ self._spinner = SpinnerStatusState()
298
+
299
+ def _session(self, session_id: str) -> _SessionState:
300
+ existing = self._sessions.get(session_id)
301
+ if existing is not None:
302
+ return existing
303
+ st = _SessionState(session_id=session_id)
304
+ self._sessions[session_id] = st
305
+ return st
306
+
307
+ def _is_primary(self, session_id: str) -> bool:
308
+ return self._primary_session_id == session_id
309
+
310
+ def _set_primary_if_needed(self, session_id: str) -> None:
311
+ if self._primary_session_id is None:
312
+ self._primary_session_id = session_id
313
+
314
+ def _spinner_update_commands(self) -> list[RenderCommand]:
315
+ return [
316
+ SpinnerUpdate(
317
+ status_text=self._spinner.get_status(),
318
+ right_text=self._spinner.get_right_text(),
319
+ )
320
+ ]
321
+
322
+ def show_sigint_exit_toast(self) -> list[RenderCommand]:
323
+ self._spinner.set_toast_status(SIGINT_DOUBLE_PRESS_EXIT_TEXT)
324
+ return self._spinner_update_commands()
325
+
326
+ def clear_sigint_exit_toast(self) -> list[RenderCommand]:
327
+ self._spinner.set_toast_status(None)
328
+ return self._spinner_update_commands()
329
+
330
+ def transition(self, event: events.Event) -> list[RenderCommand]:
331
+ session_id = getattr(event, "session_id", "__app__")
332
+ s = self._session(session_id)
333
+ cmds: list[RenderCommand] = []
334
+
335
+ match event:
336
+ case events.ReplayHistoryEvent() as e:
337
+ cmds.append(RenderReplayHistory(e))
338
+ cmds.append(SpinnerStop())
339
+ return cmds
340
+
341
+ case events.WelcomeEvent() as e:
342
+ cmds.append(RenderWelcome(e))
343
+ return cmds
344
+
345
+ case events.UserMessageEvent() as e:
346
+ if s.is_sub_agent:
347
+ return []
348
+ cmds.append(RenderUserMessage(e))
349
+ return cmds
350
+
351
+ case events.TaskStartEvent() as e:
352
+ s.sub_agent_state = e.sub_agent_state
353
+ if not s.is_sub_agent:
354
+ self._set_primary_if_needed(e.session_id)
355
+ cmds.append(TaskClockStart())
356
+ else:
357
+ s.sub_agent_thinking_header = SubAgentThinkingHeaderState()
358
+
359
+ cmds.append(SpinnerStart())
360
+ cmds.append(RenderTaskStart(e))
361
+ cmds.extend(self._spinner_update_commands())
362
+ return cmds
363
+
364
+ case events.DeveloperMessageEvent() as e:
365
+ cmds.append(RenderDeveloperMessage(e))
366
+ return cmds
367
+
368
+ case events.TurnStartEvent() as e:
369
+ cmds.append(RenderTurnStart(e))
370
+ self._spinner.clear_for_new_turn()
371
+ self._spinner.set_reasoning_status(None)
372
+ cmds.extend(self._spinner_update_commands())
373
+ return cmds
374
+
375
+ case events.ThinkingStartEvent() as e:
376
+ if s.is_sub_agent:
377
+ return []
378
+ if not self._is_primary(e.session_id):
379
+ return []
380
+ s.thinking_stream_active = True
381
+ # Ensure the status reflects that reasoning has started even
382
+ # before we receive any deltas (or a bold header).
383
+ self._spinner.set_reasoning_status(STATUS_THINKING_TEXT)
384
+ cmds.append(StartThinkingStream(session_id=e.session_id))
385
+ cmds.extend(self._spinner_update_commands())
386
+ return cmds
387
+
388
+ case events.ThinkingDeltaEvent() as e:
389
+ if s.is_sub_agent:
390
+ if not s.should_show_sub_agent_thinking_header:
391
+ return []
392
+ if s.sub_agent_thinking_header is None:
393
+ s.sub_agent_thinking_header = SubAgentThinkingHeaderState()
394
+ header = s.sub_agent_thinking_header.append_and_extract_new_header(e.content)
395
+ if header:
396
+ cmds.append(RenderThinkingHeader(session_id=e.session_id, header=header))
397
+ return cmds
398
+
399
+ if not self._is_primary(e.session_id):
400
+ return []
401
+ cmds.append(AppendThinking(session_id=e.session_id, content=e.content))
402
+
403
+ # Update reasoning status for spinner (based on bounded tail).
404
+ s.thinking_tail = (s.thinking_tail + e.content)[-8192:]
405
+ header = extract_last_bold_header(normalize_thinking_content(s.thinking_tail))
406
+ if header:
407
+ self._spinner.set_reasoning_status(header)
408
+ cmds.extend(self._spinner_update_commands())
409
+
410
+ return cmds
411
+
412
+ case events.ThinkingEndEvent() as e:
413
+ if s.is_sub_agent:
414
+ return []
415
+ if not self._is_primary(e.session_id):
416
+ return []
417
+ s.thinking_stream_active = False
418
+ self._spinner.clear_default_reasoning_status()
419
+ cmds.append(EndThinkingStream(session_id=e.session_id))
420
+ cmds.append(SpinnerStart())
421
+ cmds.extend(self._spinner_update_commands())
422
+ return cmds
423
+
424
+ case events.AssistantTextStartEvent() as e:
425
+ if s.is_sub_agent:
426
+ self._spinner.set_composing(True)
427
+ cmds.extend(self._spinner_update_commands())
428
+ return cmds
429
+ if not self._is_primary(e.session_id):
430
+ return []
431
+
432
+ s.assistant_stream_active = True
433
+ s.assistant_char_count = 0
434
+ self._spinner.set_composing(True)
435
+ self._spinner.clear_tool_calls()
436
+ cmds.append(StartAssistantStream(session_id=e.session_id))
437
+ cmds.extend(self._spinner_update_commands())
438
+ return cmds
439
+
440
+ case events.AssistantTextDeltaEvent() as e:
441
+ if s.is_sub_agent:
442
+ return []
443
+ if not self._is_primary(e.session_id):
444
+ return []
445
+
446
+ s.assistant_char_count += len(e.content)
447
+ self._spinner.set_buffer_length(s.assistant_char_count)
448
+ cmds.append(AppendAssistant(session_id=e.session_id, content=e.content))
449
+ cmds.extend(self._spinner_update_commands())
450
+ return cmds
451
+
452
+ case events.AssistantTextEndEvent() as e:
453
+ if s.is_sub_agent:
454
+ self._spinner.set_composing(False)
455
+ cmds.extend(self._spinner_update_commands())
456
+ return cmds
457
+ if not self._is_primary(e.session_id):
458
+ return []
459
+
460
+ s.assistant_stream_active = False
461
+ self._spinner.set_composing(False)
462
+ cmds.append(EndAssistantStream(session_id=e.session_id))
463
+ cmds.append(SpinnerStart())
464
+ cmds.extend(self._spinner_update_commands())
465
+ return cmds
466
+
467
+ case events.AssistantImageDeltaEvent() as e:
468
+ cmds.append(RenderAssistantImage(session_id=e.session_id, file_path=e.file_path))
469
+ return cmds
470
+
471
+ case events.ResponseCompleteEvent() as e:
472
+ if s.is_sub_agent:
473
+ return []
474
+ if not self._is_primary(e.session_id):
475
+ return []
476
+ self._spinner.set_composing(False)
477
+ cmds.append(SpinnerStart())
478
+ cmds.extend(self._spinner_update_commands())
479
+ return cmds
480
+
481
+ case events.ToolCallStartEvent() as e:
482
+ # Defensive: ensure any active main-session streams are finalized
483
+ # before tools start producing output.
484
+ if self._primary_session_id is not None:
485
+ primary = self._sessions.get(self._primary_session_id)
486
+ if primary is not None and primary.assistant_stream_active:
487
+ primary.assistant_stream_active = False
488
+ cmds.append(EndAssistantStream(session_id=primary.session_id))
489
+ if primary is not None and primary.thinking_stream_active:
490
+ primary.thinking_stream_active = False
491
+ cmds.append(EndThinkingStream(session_id=primary.session_id))
492
+
493
+ self._spinner.set_composing(False)
494
+
495
+ tool_active_form = get_tool_active_form(e.tool_name)
496
+ if is_sub_agent_tool(e.tool_name):
497
+ self._spinner.add_sub_agent_tool_call(e.tool_call_id, tool_active_form)
498
+ else:
499
+ self._spinner.add_tool_call(tool_active_form)
500
+
501
+ cmds.extend(self._spinner_update_commands())
502
+ return cmds
503
+
504
+ case events.ToolCallEvent() as e:
505
+ # Same defensive behavior for tool calls that arrive without a
506
+ # preceding ToolCallStartEvent.
507
+ if self._primary_session_id is not None:
508
+ primary = self._sessions.get(self._primary_session_id)
509
+ if primary is not None and primary.assistant_stream_active:
510
+ primary.assistant_stream_active = False
511
+ cmds.append(EndAssistantStream(session_id=primary.session_id))
512
+ if primary is not None and primary.thinking_stream_active:
513
+ primary.thinking_stream_active = False
514
+ cmds.append(EndThinkingStream(session_id=primary.session_id))
515
+
516
+ cmds.append(RenderToolCall(e))
517
+ return cmds
518
+
519
+ case events.ToolResultEvent() as e:
520
+ if is_sub_agent_tool(e.tool_name):
521
+ self._spinner.finish_sub_agent_tool_call(e.tool_call_id, get_tool_active_form(e.tool_name))
522
+ cmds.extend(self._spinner_update_commands())
523
+
524
+ if s.is_sub_agent and e.status == "success":
525
+ return cmds
526
+
527
+ cmds.append(RenderToolResult(event=e, is_sub_agent_session=s.is_sub_agent))
528
+ return cmds
529
+
530
+ case events.TaskMetadataEvent() as e:
531
+ cmds.append(RenderTaskMetadata(e))
532
+ return cmds
533
+
534
+ case events.TodoChangeEvent() as e:
535
+ todo_text = _extract_active_form_text(e)
536
+ self._spinner.set_todo_status(todo_text)
537
+ self._spinner.clear_for_new_turn()
538
+ cmds.extend(self._spinner_update_commands())
539
+ return cmds
540
+
541
+ case events.UsageEvent() as e:
542
+ # UsageEvent is not rendered, but it drives context % display.
543
+ if s.is_sub_agent:
544
+ return []
545
+ if not self._is_primary(e.session_id):
546
+ return []
547
+ context_percent = e.usage.context_usage_percent
548
+ if context_percent is not None:
549
+ self._spinner.set_context_percent(context_percent)
550
+ cmds.extend(self._spinner_update_commands())
551
+ return cmds
552
+
553
+ case events.TurnEndEvent():
554
+ return []
555
+
556
+ case events.TaskFinishEvent() as e:
557
+ cmds.append(RenderTaskFinish(e))
558
+ if not s.is_sub_agent:
559
+ cmds.append(TaskClockClear())
560
+ self._spinner.reset()
561
+ cmds.append(SpinnerStop())
562
+ cmds.append(PrintRuleLine())
563
+ cmds.append(EmitTmuxSignal())
564
+ else:
565
+ s.sub_agent_thinking_header = None
566
+ return cmds
567
+
568
+ case events.InterruptEvent() as e:
569
+ self._spinner.reset()
570
+ cmds.append(SpinnerStop())
571
+ cmds.append(EndThinkingStream(session_id=e.session_id))
572
+ cmds.append(EndAssistantStream(session_id=e.session_id))
573
+ cmds.append(TaskClockClear())
574
+ cmds.append(RenderInterrupt(session_id=e.session_id))
575
+ return cmds
576
+
577
+ case events.ErrorEvent() as e:
578
+ cmds.append(EmitOsc94Error())
579
+ cmds.append(RenderError(e))
580
+ if not e.can_retry:
581
+ self._spinner.reset()
582
+ cmds.append(SpinnerStop())
583
+ cmds.extend(self._spinner_update_commands())
584
+ return cmds
585
+
586
+ case events.EndEvent():
587
+ self._spinner.reset()
588
+ cmds.append(SpinnerStop())
589
+ cmds.append(TaskClockClear())
590
+ return cmds
591
+
592
+ case _:
593
+ return []
594
+
595
+
596
+ def _extract_active_form_text(todo_event: events.TodoChangeEvent) -> str | None:
597
+ status_text: str | None = None
598
+ for todo in todo_event.todos:
599
+ if todo.status == "in_progress" and todo.content:
600
+ status_text = todo.content
601
+
602
+ if status_text is None:
603
+ return None
604
+
605
+ normalized = status_text.replace("\n", " ").strip()
606
+ return normalized if normalized else None