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
@@ -1,629 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from collections.abc import Callable
4
- from dataclasses import dataclass
5
-
6
- from rich.rule import Rule
7
- from rich.text import Text
8
-
9
- from klaude_code.const import MARKDOWN_LEFT_MARGIN, MARKDOWN_STREAM_LIVE_REPAINT_ENABLED, STATUS_DEFAULT_TEXT
10
- from klaude_code.protocol import events
11
- from klaude_code.ui.core.stage_manager import Stage, StageManager
12
- from klaude_code.ui.modes.repl.renderer import REPLRenderer
13
- from klaude_code.ui.renderers.assistant import ASSISTANT_MESSAGE_MARK
14
- from klaude_code.ui.renderers.thinking import (
15
- THINKING_MESSAGE_MARK,
16
- extract_last_bold_header,
17
- normalize_thinking_content,
18
- )
19
- from klaude_code.ui.renderers.tools import get_tool_active_form
20
- from klaude_code.ui.rich import status as r_status
21
- from klaude_code.ui.rich.markdown import MarkdownStream, ThinkingMarkdown
22
- from klaude_code.ui.rich.theme import ThemeKey
23
- from klaude_code.ui.terminal.notifier import Notification, NotificationType, TerminalNotifier, emit_tmux_signal
24
- from klaude_code.ui.terminal.progress_bar import OSC94States, emit_osc94
25
-
26
-
27
- @dataclass
28
- class SubAgentThinkingHeaderState:
29
- buffer: str = ""
30
- last_header: str | None = None
31
-
32
- def append_and_extract_new_header(self, content: str) -> str | None:
33
- self.buffer += content
34
-
35
- # Sub-agent thinking does not need full streaming; keep a bounded tail.
36
- max_chars = 8192
37
- if len(self.buffer) > max_chars:
38
- self.buffer = self.buffer[-max_chars:]
39
-
40
- header = extract_last_bold_header(normalize_thinking_content(self.buffer))
41
- if header and header != self.last_header:
42
- self.last_header = header
43
- return header
44
- return None
45
-
46
-
47
- @dataclass
48
- class ActiveStream:
49
- """Active streaming state containing buffer and markdown renderer.
50
-
51
- This represents an active streaming session where content is being
52
- accumulated in a buffer and rendered via MarkdownStream.
53
- When streaming ends, this object is replaced with None.
54
- """
55
-
56
- buffer: str
57
- mdstream: MarkdownStream
58
-
59
- def append(self, content: str) -> None:
60
- self.buffer += content
61
-
62
-
63
- class StreamState:
64
- """Manages assistant message streaming state.
65
-
66
- The streaming state is either:
67
- - None: No active stream
68
- - ActiveStream: Active streaming with buffer and markdown renderer
69
-
70
- This design ensures buffer and mdstream are always in sync.
71
- """
72
-
73
- def __init__(self) -> None:
74
- self._active: ActiveStream | None = None
75
-
76
- @property
77
- def is_active(self) -> bool:
78
- return self._active is not None
79
-
80
- @property
81
- def buffer(self) -> str:
82
- return self._active.buffer if self._active else ""
83
-
84
- @property
85
- def mdstream(self) -> MarkdownStream | None:
86
- return self._active.mdstream if self._active else None
87
-
88
- def start(self, mdstream: MarkdownStream) -> None:
89
- """Start a new streaming session."""
90
- self._active = ActiveStream(buffer="", mdstream=mdstream)
91
-
92
- def append(self, content: str) -> None:
93
- """Append content to the buffer."""
94
- if self._active:
95
- self._active.append(content)
96
-
97
- def finish(self) -> None:
98
- """End the current streaming session."""
99
- self._active = None
100
-
101
- def render(self, *, transform: Callable[[str], str] | None = None, final: bool = False) -> bool:
102
- """Render the current buffer to the markdown stream.
103
-
104
- Returns:
105
- bool: True if an active stream was rendered.
106
- """
107
-
108
- if self._active is None:
109
- return False
110
-
111
- text = self._active.buffer
112
- if transform is not None:
113
- text = transform(text)
114
- self._active.mdstream.update(text, final=final)
115
-
116
- if final:
117
- self.finish()
118
-
119
- return True
120
-
121
- def finalize(self, *, transform: Callable[[str], str] | None = None) -> bool:
122
- """Finalize rendering and end the current streaming session."""
123
-
124
- return self.render(transform=transform, final=True)
125
-
126
-
127
- class ActivityState:
128
- """Represents the current activity state for spinner display.
129
-
130
- This is a discriminated union where the state is either:
131
- - None (thinking/idle)
132
- - Composing (assistant is streaming text)
133
- - ToolCalls (one or more tool calls in progress)
134
-
135
- Composing and ToolCalls are mutually exclusive - when tool calls start,
136
- composing state is automatically cleared.
137
- """
138
-
139
- def __init__(self) -> None:
140
- self._composing: bool = False
141
- self._buffer_length: int = 0
142
- self._tool_calls: dict[str, int] = {}
143
-
144
- @property
145
- def is_composing(self) -> bool:
146
- return self._composing and not self._tool_calls
147
-
148
- @property
149
- def has_tool_calls(self) -> bool:
150
- return bool(self._tool_calls)
151
-
152
- def set_composing(self, composing: bool) -> None:
153
- self._composing = composing
154
- if not composing:
155
- self._buffer_length = 0
156
-
157
- def set_buffer_length(self, length: int) -> None:
158
- self._buffer_length = length
159
-
160
- def add_tool_call(self, tool_name: str) -> None:
161
- self._tool_calls[tool_name] = self._tool_calls.get(tool_name, 0) + 1
162
-
163
- def clear_tool_calls(self) -> None:
164
- self._tool_calls = {}
165
-
166
- def reset(self) -> None:
167
- self._composing = False
168
- self._buffer_length = 0
169
- self._tool_calls = {}
170
-
171
- def get_activity_text(self) -> Text | None:
172
- """Get activity text for display. Returns None if idle/thinking."""
173
- if self._tool_calls:
174
- activity_text = Text()
175
- first = True
176
- for name, count in self._tool_calls.items():
177
- if not first:
178
- activity_text.append(", ")
179
- activity_text.append(Text(name, style=ThemeKey.STATUS_TEXT_BOLD))
180
- if count > 1:
181
- activity_text.append(f" x {count}")
182
- first = False
183
- return activity_text
184
- if self._composing:
185
- # Main status text with creative verb
186
- text = Text()
187
- text.append("Composing", style=ThemeKey.STATUS_TEXT_BOLD)
188
- if self._buffer_length > 0:
189
- text.append(f" ({self._buffer_length:,})", style=ThemeKey.STATUS_TEXT)
190
- return text
191
- return None
192
-
193
-
194
- class SpinnerStatusState:
195
- """Multi-layer spinner status state management.
196
-
197
- Layers:
198
- - todo_status: Set by TodoChange (preferred when present)
199
- - reasoning_status: Derived from Thinking/ThinkingDelta bold headers
200
- - activity: Current activity (composing or tool_calls), mutually exclusive
201
- - context_percent: Context usage percentage, updated during task execution
202
-
203
- Display logic:
204
- - If activity: show base + activity (if base exists) or activity + "…"
205
- - Elif base_status: show base_status
206
- - Else: show "Thinking …"
207
- - Context percent is appended at the end if available
208
- """
209
-
210
- def __init__(self) -> None:
211
- self._todo_status: str | None = None
212
- self._reasoning_status: str | None = None
213
- self._activity = ActivityState()
214
- self._context_percent: float | None = None
215
-
216
- def reset(self) -> None:
217
- """Reset all layers."""
218
- self._todo_status = None
219
- self._reasoning_status = None
220
- self._activity.reset()
221
- self._context_percent = None
222
-
223
- def set_todo_status(self, status: str | None) -> None:
224
- """Set base status from TodoChange."""
225
- self._todo_status = status
226
-
227
- def set_reasoning_status(self, status: str | None) -> None:
228
- """Set reasoning-derived base status from ThinkingDelta bold headers."""
229
- self._reasoning_status = status
230
-
231
- def set_composing(self, composing: bool) -> None:
232
- """Set composing state when assistant is streaming."""
233
- if composing:
234
- self._reasoning_status = None
235
- self._activity.set_composing(composing)
236
-
237
- def set_buffer_length(self, length: int) -> None:
238
- """Set buffer length for composing state display."""
239
- self._activity.set_buffer_length(length)
240
-
241
- def add_tool_call(self, tool_name: str) -> None:
242
- """Add a tool call to the accumulator."""
243
- self._activity.add_tool_call(tool_name)
244
-
245
- def clear_tool_calls(self) -> None:
246
- """Clear tool calls."""
247
- self._activity.clear_tool_calls()
248
-
249
- def clear_for_new_turn(self) -> None:
250
- """Clear activity state for a new turn."""
251
- self._activity.reset()
252
-
253
- def set_context_percent(self, percent: float) -> None:
254
- """Set context usage percentage."""
255
- self._context_percent = percent
256
-
257
- def get_activity_text(self) -> Text | None:
258
- """Get current activity text. Returns None if idle."""
259
- return self._activity.get_activity_text()
260
-
261
- def get_status(self) -> Text:
262
- """Get current spinner status as rich Text (without context)."""
263
- activity_text = self._activity.get_activity_text()
264
-
265
- base_status = self._reasoning_status or self._todo_status
266
-
267
- if base_status:
268
- if activity_text:
269
- result = Text()
270
- result.append(base_status, style=ThemeKey.STATUS_TEXT_BOLD_ITALIC)
271
- result.append(" | ")
272
- result.append_text(activity_text)
273
- else:
274
- result = Text(base_status, style=ThemeKey.STATUS_TEXT_BOLD_ITALIC)
275
- elif activity_text:
276
- activity_text.append(" …")
277
- result = activity_text
278
- else:
279
- result = Text(STATUS_DEFAULT_TEXT, style=ThemeKey.STATUS_TEXT)
280
-
281
- return result
282
-
283
- def get_right_text(self) -> r_status.DynamicText | None:
284
- """Get right-aligned status text (elapsed time and optional context %)."""
285
-
286
- elapsed_text = r_status.current_elapsed_text()
287
- has_context = self._context_percent is not None
288
-
289
- if elapsed_text is None and not has_context:
290
- return None
291
-
292
- def _render() -> Text:
293
- parts: list[str] = []
294
- if self._context_percent is not None:
295
- parts.append(f"{self._context_percent:.1f}%")
296
- current_elapsed = r_status.current_elapsed_text()
297
- if current_elapsed is not None:
298
- if parts:
299
- parts.append(" · ")
300
- parts.append(current_elapsed)
301
- return Text("".join(parts), style=ThemeKey.METADATA_DIM)
302
-
303
- return r_status.DynamicText(_render)
304
-
305
-
306
- class DisplayEventHandler:
307
- """Handle REPL events, buffering and delegating rendering work."""
308
-
309
- def __init__(self, renderer: REPLRenderer, notifier: TerminalNotifier | None = None):
310
- self.renderer = renderer
311
- self.notifier = notifier
312
- self.assistant_stream = StreamState()
313
- self.thinking_stream = StreamState()
314
- self._sub_agent_thinking_headers: dict[str, SubAgentThinkingHeaderState] = {}
315
- self.spinner_status = SpinnerStatusState()
316
-
317
- self.stage_manager = StageManager(
318
- finish_assistant=self._finish_assistant_stream,
319
- finish_thinking=self._finish_thinking_stream,
320
- )
321
-
322
- def _new_thinking_mdstream(self) -> MarkdownStream:
323
- return MarkdownStream(
324
- mdargs={
325
- "code_theme": self.renderer.themes.code_theme,
326
- "style": ThemeKey.THINKING,
327
- },
328
- theme=self.renderer.themes.thinking_markdown_theme,
329
- console=self.renderer.console,
330
- live_sink=self.renderer.set_stream_renderable if MARKDOWN_STREAM_LIVE_REPAINT_ENABLED else None,
331
- mark=THINKING_MESSAGE_MARK,
332
- mark_style=ThemeKey.THINKING,
333
- left_margin=MARKDOWN_LEFT_MARGIN,
334
- markdown_class=ThinkingMarkdown,
335
- )
336
-
337
- def _new_assistant_mdstream(self) -> MarkdownStream:
338
- return MarkdownStream(
339
- mdargs={"code_theme": self.renderer.themes.code_theme},
340
- theme=self.renderer.themes.markdown_theme,
341
- console=self.renderer.console,
342
- live_sink=self.renderer.set_stream_renderable if MARKDOWN_STREAM_LIVE_REPAINT_ENABLED else None,
343
- mark=ASSISTANT_MESSAGE_MARK,
344
- left_margin=MARKDOWN_LEFT_MARGIN,
345
- )
346
-
347
- async def consume_event(self, event: events.Event) -> None:
348
- match event:
349
- case events.ReplayHistoryEvent() as e:
350
- await self._on_replay_history(e)
351
- case events.WelcomeEvent() as e:
352
- self._on_welcome(e)
353
- case events.UserMessageEvent() as e:
354
- self._on_user_message(e)
355
- case events.TaskStartEvent() as e:
356
- self._on_task_start(e)
357
- case events.DeveloperMessageEvent() as e:
358
- self._on_developer_message(e)
359
- case events.TurnStartEvent() as e:
360
- self._on_turn_start(e)
361
- case events.ThinkingDeltaEvent() as e:
362
- await self._on_thinking_delta(e)
363
- case events.AssistantTextDeltaEvent() as e:
364
- await self._on_assistant_delta(e)
365
- case events.AssistantImageDeltaEvent() as e:
366
- await self._on_assistant_image_delta(e)
367
- case events.AssistantMessageEvent() as e:
368
- await self._on_assistant_message(e)
369
- case events.TurnToolCallStartEvent() as e:
370
- await self._on_tool_call_start(e)
371
- case events.ToolCallEvent() as e:
372
- await self._on_tool_call(e)
373
- case events.ToolResultEvent() as e:
374
- await self._on_tool_result(e)
375
- case events.TaskMetadataEvent() as e:
376
- self._on_task_metadata(e)
377
- case events.TodoChangeEvent() as e:
378
- self._on_todo_change(e)
379
- case events.ContextUsageEvent() as e:
380
- self._on_context_usage(e)
381
- case events.TurnEndEvent():
382
- pass
383
- case events.ResponseMetadataEvent():
384
- pass # Internal event, not displayed
385
- case events.TaskFinishEvent() as e:
386
- await self._on_task_finish(e)
387
- case events.InterruptEvent() as e:
388
- await self._on_interrupt(e)
389
- case events.ErrorEvent() as e:
390
- await self._on_error(e)
391
- case events.EndEvent() as e:
392
- await self._on_end(e)
393
-
394
- async def stop(self) -> None:
395
- self._flush_assistant_buffer()
396
- self._flush_thinking_buffer()
397
-
398
- # ─────────────────────────────────────────────────────────────────────────────
399
- # Private event handlers
400
- # ─────────────────────────────────────────────────────────────────────────────
401
-
402
- async def _on_replay_history(self, event: events.ReplayHistoryEvent) -> None:
403
- await self.renderer.replay_history(event)
404
- self.renderer.spinner_stop()
405
-
406
- def _on_welcome(self, event: events.WelcomeEvent) -> None:
407
- self.renderer.display_welcome(event)
408
-
409
- def _on_user_message(self, event: events.UserMessageEvent) -> None:
410
- self.renderer.display_user_message(event)
411
-
412
- def _on_task_start(self, event: events.TaskStartEvent) -> None:
413
- if event.sub_agent_state is None:
414
- r_status.set_task_start()
415
- else:
416
- self._sub_agent_thinking_headers[event.session_id] = SubAgentThinkingHeaderState()
417
- self.renderer.spinner_start()
418
- self.renderer.display_task_start(event)
419
-
420
- def _on_developer_message(self, event: events.DeveloperMessageEvent) -> None:
421
- self.renderer.display_developer_message(event)
422
- self.renderer.display_command_output(event)
423
-
424
- def _on_turn_start(self, event: events.TurnStartEvent) -> None:
425
- self.renderer.display_turn_start(event)
426
- self.spinner_status.clear_for_new_turn()
427
- self.spinner_status.set_reasoning_status(None)
428
- self._update_spinner()
429
-
430
- async def _on_thinking_delta(self, event: events.ThinkingDeltaEvent) -> None:
431
- if self.renderer.is_sub_agent_session(event.session_id):
432
- if not self.renderer.should_display_sub_agent_thinking_header(event.session_id):
433
- return
434
- state = self._sub_agent_thinking_headers.setdefault(event.session_id, SubAgentThinkingHeaderState())
435
- header = state.append_and_extract_new_header(event.content)
436
- if header:
437
- with self.renderer.session_print_context(event.session_id):
438
- self.renderer.display_thinking_header(header)
439
- return
440
-
441
- first_delta = not self.thinking_stream.is_active
442
- if first_delta:
443
- self.thinking_stream.start(self._new_thinking_mdstream())
444
-
445
- self.thinking_stream.append(event.content)
446
-
447
- reasoning_status = extract_last_bold_header(normalize_thinking_content(self.thinking_stream.buffer))
448
- if reasoning_status:
449
- self.spinner_status.set_reasoning_status(reasoning_status)
450
- self._update_spinner()
451
-
452
- if first_delta:
453
- self.thinking_stream.render(transform=normalize_thinking_content)
454
-
455
- await self.stage_manager.enter_thinking_stage()
456
- self._flush_thinking_buffer()
457
-
458
- async def _on_assistant_delta(self, event: events.AssistantTextDeltaEvent) -> None:
459
- if self.renderer.is_sub_agent_session(event.session_id):
460
- self.spinner_status.set_composing(True)
461
- self._update_spinner()
462
- return
463
-
464
- if len(event.content.strip()) == 0 and self.stage_manager.current_stage != Stage.ASSISTANT:
465
- await self.stage_manager.transition_to(Stage.WAITING)
466
- return
467
-
468
- await self.stage_manager.transition_to(Stage.ASSISTANT)
469
- first_delta = not self.assistant_stream.is_active
470
- if first_delta:
471
- self.spinner_status.set_composing(True)
472
- self.spinner_status.clear_tool_calls()
473
- self._update_spinner()
474
- self.assistant_stream.start(self._new_assistant_mdstream())
475
- self.assistant_stream.append(event.content)
476
- self.spinner_status.set_buffer_length(len(self.assistant_stream.buffer))
477
- if not first_delta:
478
- self._update_spinner()
479
- if first_delta:
480
- self.assistant_stream.render()
481
- self._flush_assistant_buffer()
482
-
483
- async def _on_assistant_message(self, event: events.AssistantMessageEvent) -> None:
484
- if self.renderer.is_sub_agent_session(event.session_id):
485
- return
486
-
487
- await self.stage_manager.transition_to(Stage.WAITING)
488
- self.spinner_status.set_composing(False)
489
- self._update_spinner()
490
- self.renderer.spinner_start()
491
-
492
- async def _on_assistant_image_delta(self, event: events.AssistantImageDeltaEvent) -> None:
493
- await self.stage_manager.transition_to(Stage.ASSISTANT)
494
- self.renderer.display_image(event.file_path)
495
-
496
- async def _on_tool_call_start(self, event: events.TurnToolCallStartEvent) -> None:
497
- self._flush_assistant_buffer()
498
- self.spinner_status.set_composing(False)
499
- self.spinner_status.add_tool_call(get_tool_active_form(event.tool_name))
500
- self._update_spinner()
501
-
502
- async def _on_tool_call(self, event: events.ToolCallEvent) -> None:
503
- await self.stage_manager.transition_to(Stage.TOOL_CALL)
504
- with self.renderer.session_print_context(event.session_id):
505
- self.renderer.display_tool_call(event)
506
-
507
- async def _on_tool_result(self, event: events.ToolResultEvent) -> None:
508
- is_sub_agent = self.renderer.is_sub_agent_session(event.session_id)
509
- if is_sub_agent and event.status == "success":
510
- return
511
- await self.stage_manager.transition_to(Stage.TOOL_RESULT)
512
- with self.renderer.session_print_context(event.session_id):
513
- self.renderer.display_tool_call_result(event, is_sub_agent=is_sub_agent)
514
-
515
- def _on_task_metadata(self, event: events.TaskMetadataEvent) -> None:
516
- self.renderer.display_task_metadata(event)
517
-
518
- def _on_todo_change(self, event: events.TodoChangeEvent) -> None:
519
- self.spinner_status.set_todo_status(self._extract_active_form_text(event))
520
- # Clear tool calls when todo changes, as the tool execution has advanced
521
- self.spinner_status.clear_for_new_turn()
522
- self._update_spinner()
523
-
524
- def _on_context_usage(self, event: events.ContextUsageEvent) -> None:
525
- if self.renderer.is_sub_agent_session(event.session_id):
526
- return
527
- self.spinner_status.set_context_percent(event.context_percent)
528
- self._update_spinner()
529
-
530
- async def _on_task_finish(self, event: events.TaskFinishEvent) -> None:
531
- self.renderer.display_task_finish(event)
532
- if not self.renderer.is_sub_agent_session(event.session_id):
533
- r_status.clear_task_start()
534
- self.spinner_status.reset()
535
- self.renderer.spinner_stop()
536
- self.renderer.console.print(Rule(characters="─", style=ThemeKey.LINES))
537
- emit_tmux_signal() # Signal test harness if KLAUDE_TEST_SIGNAL is set
538
- else:
539
- self._sub_agent_thinking_headers.pop(event.session_id, None)
540
- await self.stage_manager.transition_to(Stage.WAITING)
541
- self._maybe_notify_task_finish(event)
542
-
543
- async def _on_interrupt(self, event: events.InterruptEvent) -> None:
544
- self.renderer.spinner_stop()
545
- self.spinner_status.reset()
546
- r_status.clear_task_start()
547
- await self.stage_manager.transition_to(Stage.WAITING)
548
- self.renderer.display_interrupt()
549
-
550
- async def _on_error(self, event: events.ErrorEvent) -> None:
551
- emit_osc94(OSC94States.ERROR)
552
- await self.stage_manager.transition_to(Stage.WAITING)
553
- self.renderer.display_error(event)
554
- if not event.can_retry:
555
- self.renderer.spinner_stop()
556
- self.spinner_status.reset()
557
-
558
- async def _on_end(self, event: events.EndEvent) -> None:
559
- await self.stage_manager.transition_to(Stage.WAITING)
560
- self.renderer.spinner_stop()
561
- self.spinner_status.reset()
562
- r_status.clear_task_start()
563
-
564
- # ─────────────────────────────────────────────────────────────────────────────
565
- # Private helper methods
566
- # ─────────────────────────────────────────────────────────────────────────────
567
-
568
- def _update_spinner(self) -> None:
569
- """Update spinner text from current status state."""
570
- status_text = self.spinner_status.get_status()
571
- right_text = self.spinner_status.get_right_text()
572
- self.renderer.spinner_update(
573
- status_text,
574
- right_text,
575
- )
576
-
577
- def _flush_thinking_buffer(self) -> None:
578
- self.thinking_stream.render(transform=normalize_thinking_content)
579
-
580
- def _flush_assistant_buffer(self) -> None:
581
- self.assistant_stream.render()
582
-
583
- async def _finish_thinking_stream(self) -> None:
584
- finalized = self.thinking_stream.finalize(transform=normalize_thinking_content)
585
- if finalized:
586
- self.renderer.print()
587
- self.renderer.spinner_start()
588
-
589
- async def _finish_assistant_stream(self) -> None:
590
- finalized = self.assistant_stream.finalize()
591
- if finalized:
592
- self.renderer.print()
593
-
594
- def _maybe_notify_task_finish(self, event: events.TaskFinishEvent) -> None:
595
- if self.notifier is None:
596
- return
597
- if self.renderer.is_sub_agent_session(event.session_id):
598
- return
599
- notification = self._build_task_finish_notification(event)
600
- self.notifier.notify(notification)
601
-
602
- def _build_task_finish_notification(self, event: events.TaskFinishEvent) -> Notification:
603
- body = self._compact_result_text(event.task_result)
604
- return Notification(
605
- type=NotificationType.AGENT_TASK_COMPLETE,
606
- title="Task Completed",
607
- body=body,
608
- )
609
-
610
- def _compact_result_text(self, text: str) -> str | None:
611
- stripped = text.strip()
612
- if len(stripped) == 0:
613
- return None
614
- squashed = " ".join(stripped.split())
615
- if len(squashed) > 200:
616
- return squashed[:197] + "…"
617
- return squashed
618
-
619
- def _extract_active_form_text(self, todo_event: events.TodoChangeEvent) -> str | None:
620
- status_text: str | None = None
621
- for todo in todo_event.todos:
622
- if todo.status == "in_progress" and len(todo.content) > 0:
623
- status_text = todo.content
624
-
625
- if status_text is None:
626
- return None
627
-
628
- normalized = status_text.replace("\n", " ").strip()
629
- return normalized if normalized else None