klaude-code 1.2.6__py3-none-any.whl → 1.8.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 (205) hide show
  1. klaude_code/auth/__init__.py +24 -0
  2. klaude_code/auth/codex/__init__.py +20 -0
  3. klaude_code/auth/codex/exceptions.py +17 -0
  4. klaude_code/auth/codex/jwt_utils.py +45 -0
  5. klaude_code/auth/codex/oauth.py +229 -0
  6. klaude_code/auth/codex/token_manager.py +84 -0
  7. klaude_code/cli/auth_cmd.py +73 -0
  8. klaude_code/cli/config_cmd.py +91 -0
  9. klaude_code/cli/cost_cmd.py +338 -0
  10. klaude_code/cli/debug.py +78 -0
  11. klaude_code/cli/list_model.py +307 -0
  12. klaude_code/cli/main.py +233 -134
  13. klaude_code/cli/runtime.py +309 -117
  14. klaude_code/{version.py → cli/self_update.py} +114 -5
  15. klaude_code/cli/session_cmd.py +37 -21
  16. klaude_code/command/__init__.py +88 -27
  17. klaude_code/command/clear_cmd.py +8 -7
  18. klaude_code/command/command_abc.py +31 -31
  19. klaude_code/command/debug_cmd.py +79 -0
  20. klaude_code/command/export_cmd.py +19 -53
  21. klaude_code/command/export_online_cmd.py +154 -0
  22. klaude_code/command/fork_session_cmd.py +267 -0
  23. klaude_code/command/help_cmd.py +7 -8
  24. klaude_code/command/model_cmd.py +60 -10
  25. klaude_code/command/model_select.py +84 -0
  26. klaude_code/command/prompt-jj-describe.md +32 -0
  27. klaude_code/command/prompt_command.py +19 -11
  28. klaude_code/command/refresh_cmd.py +8 -10
  29. klaude_code/command/registry.py +139 -40
  30. klaude_code/command/release_notes_cmd.py +84 -0
  31. klaude_code/command/resume_cmd.py +111 -0
  32. klaude_code/command/status_cmd.py +104 -60
  33. klaude_code/command/terminal_setup_cmd.py +7 -9
  34. klaude_code/command/thinking_cmd.py +98 -0
  35. klaude_code/config/__init__.py +14 -6
  36. klaude_code/config/assets/__init__.py +1 -0
  37. klaude_code/config/assets/builtin_config.yaml +303 -0
  38. klaude_code/config/builtin_config.py +38 -0
  39. klaude_code/config/config.py +378 -109
  40. klaude_code/config/select_model.py +117 -53
  41. klaude_code/config/thinking.py +269 -0
  42. klaude_code/{const/__init__.py → const.py} +50 -19
  43. klaude_code/core/agent.py +20 -28
  44. klaude_code/core/executor.py +327 -112
  45. klaude_code/core/manager/__init__.py +2 -4
  46. klaude_code/core/manager/llm_clients.py +1 -15
  47. klaude_code/core/manager/llm_clients_builder.py +10 -11
  48. klaude_code/core/manager/sub_agent_manager.py +37 -6
  49. klaude_code/core/prompt.py +63 -44
  50. klaude_code/core/prompts/prompt-claude-code.md +2 -13
  51. klaude_code/core/prompts/prompt-codex-gpt-5-1-codex-max.md +117 -0
  52. klaude_code/core/prompts/prompt-codex-gpt-5-2-codex.md +117 -0
  53. klaude_code/core/prompts/prompt-codex.md +9 -42
  54. klaude_code/core/prompts/prompt-minimal.md +12 -0
  55. klaude_code/core/prompts/{prompt-subagent-explore.md → prompt-sub-agent-explore.md} +16 -3
  56. klaude_code/core/prompts/{prompt-subagent-oracle.md → prompt-sub-agent-oracle.md} +1 -2
  57. klaude_code/core/prompts/prompt-sub-agent-web.md +51 -0
  58. klaude_code/core/reminders.py +283 -95
  59. klaude_code/core/task.py +113 -75
  60. klaude_code/core/tool/__init__.py +24 -31
  61. klaude_code/core/tool/file/_utils.py +36 -0
  62. klaude_code/core/tool/file/apply_patch.py +17 -25
  63. klaude_code/core/tool/file/apply_patch_tool.py +57 -77
  64. klaude_code/core/tool/file/diff_builder.py +151 -0
  65. klaude_code/core/tool/file/edit_tool.py +50 -63
  66. klaude_code/core/tool/file/move_tool.md +41 -0
  67. klaude_code/core/tool/file/move_tool.py +435 -0
  68. klaude_code/core/tool/file/read_tool.md +1 -1
  69. klaude_code/core/tool/file/read_tool.py +86 -86
  70. klaude_code/core/tool/file/write_tool.py +59 -69
  71. klaude_code/core/tool/report_back_tool.py +84 -0
  72. klaude_code/core/tool/shell/bash_tool.py +265 -22
  73. klaude_code/core/tool/shell/command_safety.py +3 -6
  74. klaude_code/core/tool/{memory → skill}/skill_tool.py +16 -26
  75. klaude_code/core/tool/sub_agent_tool.py +13 -2
  76. klaude_code/core/tool/todo/todo_write_tool.md +0 -157
  77. klaude_code/core/tool/todo/todo_write_tool.py +1 -1
  78. klaude_code/core/tool/todo/todo_write_tool_raw.md +182 -0
  79. klaude_code/core/tool/todo/update_plan_tool.py +1 -1
  80. klaude_code/core/tool/tool_abc.py +18 -0
  81. klaude_code/core/tool/tool_context.py +27 -12
  82. klaude_code/core/tool/tool_registry.py +7 -7
  83. klaude_code/core/tool/tool_runner.py +44 -36
  84. klaude_code/core/tool/truncation.py +29 -14
  85. klaude_code/core/tool/web/mermaid_tool.md +43 -0
  86. klaude_code/core/tool/web/mermaid_tool.py +2 -5
  87. klaude_code/core/tool/web/web_fetch_tool.md +1 -1
  88. klaude_code/core/tool/web/web_fetch_tool.py +112 -22
  89. klaude_code/core/tool/web/web_search_tool.md +23 -0
  90. klaude_code/core/tool/web/web_search_tool.py +130 -0
  91. klaude_code/core/turn.py +168 -66
  92. klaude_code/llm/__init__.py +2 -10
  93. klaude_code/llm/anthropic/client.py +190 -178
  94. klaude_code/llm/anthropic/input.py +39 -15
  95. klaude_code/llm/bedrock/__init__.py +3 -0
  96. klaude_code/llm/bedrock/client.py +60 -0
  97. klaude_code/llm/client.py +7 -21
  98. klaude_code/llm/codex/__init__.py +5 -0
  99. klaude_code/llm/codex/client.py +149 -0
  100. klaude_code/llm/google/__init__.py +3 -0
  101. klaude_code/llm/google/client.py +309 -0
  102. klaude_code/llm/google/input.py +215 -0
  103. klaude_code/llm/input_common.py +3 -9
  104. klaude_code/llm/openai_compatible/client.py +72 -164
  105. klaude_code/llm/openai_compatible/input.py +6 -4
  106. klaude_code/llm/openai_compatible/stream.py +273 -0
  107. klaude_code/llm/openai_compatible/tool_call_accumulator.py +17 -1
  108. klaude_code/llm/openrouter/client.py +89 -160
  109. klaude_code/llm/openrouter/input.py +18 -30
  110. klaude_code/llm/openrouter/reasoning.py +118 -0
  111. klaude_code/llm/registry.py +39 -7
  112. klaude_code/llm/responses/client.py +184 -171
  113. klaude_code/llm/responses/input.py +20 -1
  114. klaude_code/llm/usage.py +17 -12
  115. klaude_code/protocol/commands.py +17 -1
  116. klaude_code/protocol/events.py +31 -4
  117. klaude_code/protocol/llm_param.py +13 -10
  118. klaude_code/protocol/model.py +232 -29
  119. klaude_code/protocol/op.py +90 -1
  120. klaude_code/protocol/op_handler.py +35 -1
  121. klaude_code/protocol/sub_agent/__init__.py +117 -0
  122. klaude_code/protocol/sub_agent/explore.py +63 -0
  123. klaude_code/protocol/sub_agent/oracle.py +91 -0
  124. klaude_code/protocol/sub_agent/task.py +61 -0
  125. klaude_code/protocol/sub_agent/web.py +79 -0
  126. klaude_code/protocol/tools.py +4 -2
  127. klaude_code/session/__init__.py +2 -2
  128. klaude_code/session/codec.py +71 -0
  129. klaude_code/session/export.py +293 -86
  130. klaude_code/session/selector.py +89 -67
  131. klaude_code/session/session.py +320 -309
  132. klaude_code/session/store.py +220 -0
  133. klaude_code/session/templates/export_session.html +595 -83
  134. klaude_code/session/templates/mermaid_viewer.html +926 -0
  135. klaude_code/skill/__init__.py +27 -0
  136. klaude_code/skill/assets/deslop/SKILL.md +17 -0
  137. klaude_code/skill/assets/dev-docs/SKILL.md +108 -0
  138. klaude_code/skill/assets/handoff/SKILL.md +39 -0
  139. klaude_code/skill/assets/jj-workspace/SKILL.md +20 -0
  140. klaude_code/skill/assets/skill-creator/SKILL.md +139 -0
  141. klaude_code/{core/tool/memory/skill_loader.py → skill/loader.py} +55 -15
  142. klaude_code/skill/manager.py +70 -0
  143. klaude_code/skill/system_skills.py +192 -0
  144. klaude_code/trace/__init__.py +20 -2
  145. klaude_code/trace/log.py +150 -5
  146. klaude_code/ui/__init__.py +4 -9
  147. klaude_code/ui/core/input.py +1 -1
  148. klaude_code/ui/core/stage_manager.py +7 -7
  149. klaude_code/ui/modes/debug/display.py +2 -1
  150. klaude_code/ui/modes/repl/__init__.py +3 -48
  151. klaude_code/ui/modes/repl/clipboard.py +5 -5
  152. klaude_code/ui/modes/repl/completers.py +487 -123
  153. klaude_code/ui/modes/repl/display.py +5 -4
  154. klaude_code/ui/modes/repl/event_handler.py +370 -117
  155. klaude_code/ui/modes/repl/input_prompt_toolkit.py +552 -105
  156. klaude_code/ui/modes/repl/key_bindings.py +146 -23
  157. klaude_code/ui/modes/repl/renderer.py +189 -99
  158. klaude_code/ui/renderers/assistant.py +9 -2
  159. klaude_code/ui/renderers/bash_syntax.py +178 -0
  160. klaude_code/ui/renderers/common.py +78 -0
  161. klaude_code/ui/renderers/developer.py +104 -48
  162. klaude_code/ui/renderers/diffs.py +87 -6
  163. klaude_code/ui/renderers/errors.py +11 -6
  164. klaude_code/ui/renderers/mermaid_viewer.py +57 -0
  165. klaude_code/ui/renderers/metadata.py +112 -76
  166. klaude_code/ui/renderers/sub_agent.py +92 -7
  167. klaude_code/ui/renderers/thinking.py +40 -18
  168. klaude_code/ui/renderers/tools.py +405 -227
  169. klaude_code/ui/renderers/user_input.py +73 -13
  170. klaude_code/ui/rich/__init__.py +10 -1
  171. klaude_code/ui/rich/cjk_wrap.py +228 -0
  172. klaude_code/ui/rich/code_panel.py +131 -0
  173. klaude_code/ui/rich/live.py +17 -0
  174. klaude_code/ui/rich/markdown.py +305 -170
  175. klaude_code/ui/rich/searchable_text.py +10 -13
  176. klaude_code/ui/rich/status.py +190 -49
  177. klaude_code/ui/rich/theme.py +135 -39
  178. klaude_code/ui/terminal/__init__.py +55 -0
  179. klaude_code/ui/terminal/color.py +1 -1
  180. klaude_code/ui/terminal/control.py +13 -22
  181. klaude_code/ui/terminal/notifier.py +44 -4
  182. klaude_code/ui/terminal/selector.py +658 -0
  183. klaude_code/ui/utils/common.py +0 -18
  184. klaude_code-1.8.0.dist-info/METADATA +377 -0
  185. klaude_code-1.8.0.dist-info/RECORD +219 -0
  186. {klaude_code-1.2.6.dist-info → klaude_code-1.8.0.dist-info}/entry_points.txt +1 -0
  187. klaude_code/command/diff_cmd.py +0 -138
  188. klaude_code/command/prompt-dev-docs-update.md +0 -56
  189. klaude_code/command/prompt-dev-docs.md +0 -46
  190. klaude_code/config/list_model.py +0 -162
  191. klaude_code/core/manager/agent_manager.py +0 -127
  192. klaude_code/core/prompts/prompt-subagent-webfetch.md +0 -46
  193. klaude_code/core/tool/file/multi_edit_tool.md +0 -42
  194. klaude_code/core/tool/file/multi_edit_tool.py +0 -199
  195. klaude_code/core/tool/memory/memory_tool.md +0 -16
  196. klaude_code/core/tool/memory/memory_tool.py +0 -462
  197. klaude_code/llm/openrouter/reasoning_handler.py +0 -209
  198. klaude_code/protocol/sub_agent.py +0 -348
  199. klaude_code/ui/utils/debouncer.py +0 -42
  200. klaude_code-1.2.6.dist-info/METADATA +0 -178
  201. klaude_code-1.2.6.dist-info/RECORD +0 -167
  202. /klaude_code/core/prompts/{prompt-subagent.md → prompt-sub-agent.md} +0 -0
  203. /klaude_code/core/tool/{memory → skill}/__init__.py +0 -0
  204. /klaude_code/core/tool/{memory → skill}/skill_tool.md +0 -0
  205. {klaude_code-1.2.6.dist-info → klaude_code-1.8.0.dist-info}/WHEEL +0 -0
@@ -1,119 +1,290 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing import Awaitable, Callable
3
+ from dataclasses import dataclass
4
4
 
5
+ from rich.rule import Rule
5
6
  from rich.text import Text
6
7
 
7
8
  from klaude_code import const
8
9
  from klaude_code.protocol import events
9
10
  from klaude_code.ui.core.stage_manager import Stage, StageManager
10
11
  from klaude_code.ui.modes.repl.renderer import REPLRenderer
11
- from klaude_code.ui.rich.markdown import MarkdownStream
12
- from klaude_code.ui.terminal.notifier import Notification, NotificationType, TerminalNotifier
12
+ from klaude_code.ui.renderers.assistant import ASSISTANT_MESSAGE_MARK
13
+ from klaude_code.ui.renderers.thinking import THINKING_MESSAGE_MARK, normalize_thinking_content
14
+ from klaude_code.ui.rich import status as r_status
15
+ from klaude_code.ui.rich.markdown import MarkdownStream, ThinkingMarkdown
16
+ from klaude_code.ui.rich.theme import ThemeKey
17
+ from klaude_code.ui.terminal.notifier import Notification, NotificationType, TerminalNotifier, emit_tmux_signal
13
18
  from klaude_code.ui.terminal.progress_bar import OSC94States, emit_osc94
14
- from klaude_code.ui.utils.debouncer import Debouncer
15
19
 
16
20
 
17
- class StreamState:
18
- def __init__(self, interval: float, flush_handler: Callable[["StreamState"], Awaitable[None]]):
19
- self.buffer: str = ""
20
- self.mdstream: MarkdownStream | None = None
21
- self._flush_handler = flush_handler
22
- self.debouncer = Debouncer(interval=interval, callback=self._debounced_flush)
21
+ def extract_last_bold_header(text: str) -> str | None:
22
+ """Extract the latest complete bold header ("**...**") from text.
23
+
24
+ We treat a bold segment as a "header" only if it appears at the beginning
25
+ of a line (ignoring leading whitespace). This avoids picking up incidental
26
+ emphasis inside paragraphs.
27
+
28
+ Returns None if no complete bold segment is available yet.
29
+ """
30
+
31
+ last: str | None = None
32
+ i = 0
33
+ while True:
34
+ start = text.find("**", i)
35
+ if start < 0:
36
+ break
37
+
38
+ line_start = text.rfind("\n", 0, start) + 1
39
+ if text[line_start:start].strip():
40
+ i = start + 2
41
+ continue
42
+
43
+ end = text.find("**", start + 2)
44
+ if end < 0:
45
+ break
46
+
47
+ inner = " ".join(text[start + 2 : end].split())
48
+ if inner and "\n" not in inner:
49
+ last = inner
23
50
 
24
- async def _debounced_flush(self) -> None:
25
- await self._flush_handler(self)
51
+ i = end + 2
52
+
53
+ return last
54
+
55
+
56
+ @dataclass
57
+ class ActiveStream:
58
+ """Active streaming state containing buffer and markdown renderer.
59
+
60
+ This represents an active streaming session where content is being
61
+ accumulated in a buffer and rendered via MarkdownStream.
62
+ When streaming ends, this object is replaced with None.
63
+ """
64
+
65
+ buffer: str
66
+ mdstream: MarkdownStream
26
67
 
27
68
  def append(self, content: str) -> None:
28
69
  self.buffer += content
29
70
 
30
- def clear(self) -> None:
31
- self.buffer = ""
32
71
 
72
+ class StreamState:
73
+ """Manages assistant message streaming state.
33
74
 
34
- class SpinnerStatusState:
35
- """Multi-layer spinner status state management.
36
-
37
- Layers (from low to high priority):
38
- - base_status: Set by TodoChange, persistent within a turn
39
- - composing: True when assistant is streaming text
40
- - tool_calls: Accumulated from ToolCallStart
75
+ The streaming state is either:
76
+ - None: No active stream
77
+ - ActiveStream: Active streaming with buffer and markdown renderer
41
78
 
42
- Display logic:
43
- - If tool_calls: show base + tool_calls (composing is hidden)
44
- - Elif composing: show base + "Composing"
45
- - Elif base_status: show base_status
46
- - Else: show "Thinking …"
79
+ This design ensures buffer and mdstream are always in sync.
47
80
  """
48
81
 
49
- DEFAULT_STATUS = "Thinking …"
82
+ def __init__(self) -> None:
83
+ self._active: ActiveStream | None = None
84
+
85
+ @property
86
+ def is_active(self) -> bool:
87
+ return self._active is not None
88
+
89
+ @property
90
+ def buffer(self) -> str:
91
+ return self._active.buffer if self._active else ""
92
+
93
+ @property
94
+ def mdstream(self) -> MarkdownStream | None:
95
+ return self._active.mdstream if self._active else None
96
+
97
+ def start(self, mdstream: MarkdownStream) -> None:
98
+ """Start a new streaming session."""
99
+ self._active = ActiveStream(buffer="", mdstream=mdstream)
100
+
101
+ def append(self, content: str) -> None:
102
+ """Append content to the buffer."""
103
+ if self._active:
104
+ self._active.append(content)
105
+
106
+ def finish(self) -> None:
107
+ """End the current streaming session."""
108
+ self._active = None
109
+
110
+
111
+ class ActivityState:
112
+ """Represents the current activity state for spinner display.
113
+
114
+ This is a discriminated union where the state is either:
115
+ - None (thinking/idle)
116
+ - Composing (assistant is streaming text)
117
+ - ToolCalls (one or more tool calls in progress)
118
+
119
+ Composing and ToolCalls are mutually exclusive - when tool calls start,
120
+ composing state is automatically cleared.
121
+ """
50
122
 
51
123
  def __init__(self) -> None:
52
- self._base_status: str | None = None
53
124
  self._composing: bool = False
125
+ self._buffer_length: int = 0
54
126
  self._tool_calls: dict[str, int] = {}
55
- self._pending_clear: bool = False
56
127
 
57
- def reset(self) -> None:
58
- """Reset all layers."""
59
- self._base_status = None
60
- self._composing = False
61
- self._tool_calls = {}
62
- self._pending_clear = False
128
+ @property
129
+ def is_composing(self) -> bool:
130
+ return self._composing and not self._tool_calls
63
131
 
64
- def set_base_status(self, status: str | None) -> None:
65
- """Set base status from TodoChange."""
66
- self._base_status = status
132
+ @property
133
+ def has_tool_calls(self) -> bool:
134
+ return bool(self._tool_calls)
67
135
 
68
136
  def set_composing(self, composing: bool) -> None:
69
- """Set composing state when assistant is streaming."""
70
137
  self._composing = composing
138
+ if not composing:
139
+ self._buffer_length = 0
140
+
141
+ def set_buffer_length(self, length: int) -> None:
142
+ self._buffer_length = length
71
143
 
72
144
  def add_tool_call(self, tool_name: str) -> None:
73
- """Add a tool call to the accumulator."""
74
- if self._pending_clear:
75
- self._tool_calls = {}
76
- self._composing = False
77
- self._pending_clear = False
78
145
  self._tool_calls[tool_name] = self._tool_calls.get(tool_name, 0) + 1
79
146
 
80
147
  def clear_tool_calls(self) -> None:
81
- """Clear tool calls and composing state immediately."""
82
148
  self._tool_calls = {}
83
- self._composing = False
84
- self._pending_clear = False
85
149
 
86
- def mark_pending_clear(self) -> None:
87
- """Mark tool calls to be cleared on next add_tool_call or set_composing."""
88
- self._pending_clear = True
150
+ def reset(self) -> None:
151
+ self._composing = False
152
+ self._buffer_length = 0
153
+ self._tool_calls = {}
89
154
 
90
- def get_status(self) -> Text:
91
- """Get current spinner status as rich Text."""
92
- # Build activity text (tool_calls or composing)
93
- activity_text: Text | None = None
155
+ def get_activity_text(self) -> Text | None:
156
+ """Get activity text for display. Returns None if idle/thinking."""
94
157
  if self._tool_calls:
95
158
  activity_text = Text()
96
159
  first = True
97
160
  for name, count in self._tool_calls.items():
98
161
  if not first:
99
162
  activity_text.append(", ")
100
- activity_text.append(name, style="bold")
163
+ activity_text.append(Text(name, style=ThemeKey.STATUS_TEXT_BOLD))
101
164
  if count > 1:
102
- activity_text.append(f" × {count}")
165
+ activity_text.append(f" x {count}")
103
166
  first = False
104
- elif self._composing:
105
- activity_text = Text("Composing")
167
+ return activity_text
168
+ if self._composing:
169
+ # Main status text with creative verb
170
+ text = Text()
171
+ text.append("Composing", style=ThemeKey.STATUS_TEXT_BOLD)
172
+ if self._buffer_length > 0:
173
+ text.append(f" ({self._buffer_length:,})", style=ThemeKey.STATUS_TEXT)
174
+ return text
175
+ return None
176
+
177
+
178
+ class SpinnerStatusState:
179
+ """Multi-layer spinner status state management.
180
+
181
+ Layers:
182
+ - todo_status: Set by TodoChange (preferred when present)
183
+ - reasoning_status: Derived from Thinking/ThinkingDelta bold headers
184
+ - activity: Current activity (composing or tool_calls), mutually exclusive
185
+ - context_percent: Context usage percentage, updated during task execution
186
+
187
+ Display logic:
188
+ - If activity: show base + activity (if base exists) or activity + "..."
189
+ - Elif base_status: show base_status
190
+ - Else: show "Thinking …"
191
+ - Context percent is appended at the end if available
192
+ """
193
+
194
+ def __init__(self) -> None:
195
+ self._todo_status: str | None = None
196
+ self._reasoning_status: str | None = None
197
+ self._activity = ActivityState()
198
+ self._context_percent: float | None = None
199
+
200
+ def reset(self) -> None:
201
+ """Reset all layers."""
202
+ self._todo_status = None
203
+ self._reasoning_status = None
204
+ self._activity.reset()
205
+ self._context_percent = None
206
+
207
+ def set_todo_status(self, status: str | None) -> None:
208
+ """Set base status from TodoChange."""
209
+ self._todo_status = status
210
+
211
+ def set_reasoning_status(self, status: str | None) -> None:
212
+ """Set reasoning-derived base status from ThinkingDelta bold headers."""
213
+ self._reasoning_status = status
214
+
215
+ def set_composing(self, composing: bool) -> None:
216
+ """Set composing state when assistant is streaming."""
217
+ if composing:
218
+ self._reasoning_status = None
219
+ self._activity.set_composing(composing)
220
+
221
+ def set_buffer_length(self, length: int) -> None:
222
+ """Set buffer length for composing state display."""
223
+ self._activity.set_buffer_length(length)
224
+
225
+ def add_tool_call(self, tool_name: str) -> None:
226
+ """Add a tool call to the accumulator."""
227
+ self._activity.add_tool_call(tool_name)
228
+
229
+ def clear_tool_calls(self) -> None:
230
+ """Clear tool calls."""
231
+ self._activity.clear_tool_calls()
232
+
233
+ def clear_for_new_turn(self) -> None:
234
+ """Clear activity state for a new turn."""
235
+ self._activity.reset()
236
+
237
+ def set_context_percent(self, percent: float) -> None:
238
+ """Set context usage percentage."""
239
+ self._context_percent = percent
240
+
241
+ def get_activity_text(self) -> Text | None:
242
+ """Get current activity text. Returns None if idle."""
243
+ return self._activity.get_activity_text()
106
244
 
107
- if self._base_status:
108
- result = Text(self._base_status)
245
+ def get_status(self) -> Text:
246
+ """Get current spinner status as rich Text (without context)."""
247
+ activity_text = self._activity.get_activity_text()
248
+
249
+ base_status = self._reasoning_status or self._todo_status
250
+
251
+ if base_status:
109
252
  if activity_text:
253
+ result = Text()
254
+ result.append(base_status, style=ThemeKey.STATUS_TEXT_BOLD_ITALIC)
110
255
  result.append(" | ")
111
256
  result.append_text(activity_text)
112
- return result
113
- if activity_text:
257
+ else:
258
+ result = Text(base_status, style=ThemeKey.STATUS_TEXT_BOLD_ITALIC)
259
+ elif activity_text:
114
260
  activity_text.append(" …")
115
- return activity_text
116
- return Text(self.DEFAULT_STATUS)
261
+ result = activity_text
262
+ else:
263
+ result = Text(const.STATUS_DEFAULT_TEXT, style=ThemeKey.STATUS_TEXT)
264
+
265
+ return result
266
+
267
+ def get_right_text(self) -> r_status.DynamicText | None:
268
+ """Get right-aligned status text (elapsed time and optional context %)."""
269
+
270
+ elapsed_text = r_status.current_elapsed_text()
271
+ has_context = self._context_percent is not None
272
+
273
+ if elapsed_text is None and not has_context:
274
+ return None
275
+
276
+ def _render() -> Text:
277
+ parts: list[str] = []
278
+ if self._context_percent is not None:
279
+ parts.append(f"{self._context_percent:.1f}%")
280
+ current_elapsed = r_status.current_elapsed_text()
281
+ if current_elapsed is not None:
282
+ if parts:
283
+ parts.append(" · ")
284
+ parts.append(current_elapsed)
285
+ return Text("".join(parts), style=ThemeKey.METADATA_DIM)
286
+
287
+ return r_status.DynamicText(_render)
117
288
 
118
289
 
119
290
  class DisplayEventHandler:
@@ -122,14 +293,13 @@ class DisplayEventHandler:
122
293
  def __init__(self, renderer: REPLRenderer, notifier: TerminalNotifier | None = None):
123
294
  self.renderer = renderer
124
295
  self.notifier = notifier
125
- self.assistant_stream = StreamState(
126
- interval=1 / const.UI_REFRESH_RATE_FPS, flush_handler=self._flush_assistant_buffer
127
- )
296
+ self.assistant_stream = StreamState()
297
+ self.thinking_stream = StreamState()
128
298
  self.spinner_status = SpinnerStatusState()
129
299
 
130
300
  self.stage_manager = StageManager(
131
301
  finish_assistant=self._finish_assistant_stream,
132
- on_enter_thinking=self._print_thinking_prefix,
302
+ finish_thinking=self._finish_thinking_stream,
133
303
  )
134
304
 
135
305
  async def consume_event(self, event: events.Event) -> None:
@@ -148,6 +318,8 @@ class DisplayEventHandler:
148
318
  self._on_turn_start(e)
149
319
  case events.ThinkingEvent() as e:
150
320
  await self._on_thinking(e)
321
+ case events.ThinkingDeltaEvent() as e:
322
+ await self._on_thinking_delta(e)
151
323
  case events.AssistantMessageDeltaEvent() as e:
152
324
  await self._on_assistant_delta(e)
153
325
  case events.AssistantMessageEvent() as e:
@@ -158,12 +330,16 @@ class DisplayEventHandler:
158
330
  await self._on_tool_call(e)
159
331
  case events.ToolResultEvent() as e:
160
332
  await self._on_tool_result(e)
161
- case events.ResponseMetadataEvent() as e:
162
- self._on_response_metadata(e)
333
+ case events.TaskMetadataEvent() as e:
334
+ self._on_task_metadata(e)
163
335
  case events.TodoChangeEvent() as e:
164
336
  self._on_todo_change(e)
337
+ case events.ContextUsageEvent() as e:
338
+ self._on_context_usage(e)
165
339
  case events.TurnEndEvent():
166
340
  pass
341
+ case events.ResponseMetadataEvent():
342
+ pass # Internal event, not displayed
167
343
  case events.TaskFinishEvent() as e:
168
344
  await self._on_task_finish(e)
169
345
  case events.InterruptEvent() as e:
@@ -174,8 +350,8 @@ class DisplayEventHandler:
174
350
  await self._on_end(e)
175
351
 
176
352
  async def stop(self) -> None:
177
- await self.assistant_stream.debouncer.flush()
178
- self.assistant_stream.debouncer.cancel()
353
+ await self._flush_assistant_buffer(self.assistant_stream)
354
+ await self._flush_thinking_buffer(self.thinking_stream)
179
355
 
180
356
  # ─────────────────────────────────────────────────────────────────────────────
181
357
  # Private event handlers
@@ -192,6 +368,8 @@ class DisplayEventHandler:
192
368
  self.renderer.display_user_message(event)
193
369
 
194
370
  def _on_task_start(self, event: events.TaskStartEvent) -> None:
371
+ if event.sub_agent_state is None:
372
+ r_status.set_task_start()
195
373
  self.renderer.spinner_start()
196
374
  self.renderer.display_task_start(event)
197
375
  emit_osc94(OSC94States.INDETERMINATE)
@@ -203,56 +381,104 @@ class DisplayEventHandler:
203
381
  def _on_turn_start(self, event: events.TurnStartEvent) -> None:
204
382
  emit_osc94(OSC94States.INDETERMINATE)
205
383
  self.renderer.display_turn_start(event)
206
- self.spinner_status.mark_pending_clear()
384
+ self.spinner_status.clear_for_new_turn()
385
+ self.spinner_status.set_reasoning_status(None)
386
+ self._update_spinner()
207
387
 
208
388
  async def _on_thinking(self, event: events.ThinkingEvent) -> None:
209
389
  if self.renderer.is_sub_agent_session(event.session_id):
210
390
  return
211
- self._clear_and_update_spinner()
391
+ # If streaming was active, finalize it
392
+ if self.thinking_stream.is_active:
393
+ await self._finish_thinking_stream()
394
+ else:
395
+ # Non-streaming path (history replay or models without delta support)
396
+ reasoning_status = extract_last_bold_header(normalize_thinking_content(event.content))
397
+ if reasoning_status:
398
+ self.spinner_status.set_reasoning_status(reasoning_status)
399
+ self._update_spinner()
400
+ await self.stage_manager.enter_thinking_stage()
401
+ self.renderer.display_thinking(event.content)
402
+
403
+ async def _on_thinking_delta(self, event: events.ThinkingDeltaEvent) -> None:
404
+ if self.renderer.is_sub_agent_session(event.session_id):
405
+ return
406
+
407
+ first_delta = not self.thinking_stream.is_active
408
+ if first_delta:
409
+ mdstream = MarkdownStream(
410
+ mdargs={
411
+ "code_theme": self.renderer.themes.code_theme,
412
+ "style": ThemeKey.THINKING,
413
+ },
414
+ theme=self.renderer.themes.thinking_markdown_theme,
415
+ console=self.renderer.console,
416
+ live_sink=self.renderer.set_stream_renderable if const.MARKDOWN_STREAM_LIVE_REPAINT_ENABLED else None,
417
+ mark=THINKING_MESSAGE_MARK,
418
+ mark_style=ThemeKey.THINKING,
419
+ left_margin=const.MARKDOWN_LEFT_MARGIN,
420
+ markdown_class=ThinkingMarkdown,
421
+ )
422
+ self.thinking_stream.start(mdstream)
423
+
424
+ self.thinking_stream.append(event.content)
425
+
426
+ reasoning_status = extract_last_bold_header(normalize_thinking_content(self.thinking_stream.buffer))
427
+ if reasoning_status:
428
+ self.spinner_status.set_reasoning_status(reasoning_status)
429
+ self._update_spinner()
430
+
431
+ if first_delta and self.thinking_stream.mdstream is not None:
432
+ self.thinking_stream.mdstream.update(normalize_thinking_content(self.thinking_stream.buffer))
433
+
212
434
  await self.stage_manager.enter_thinking_stage()
213
- self.renderer.display_thinking(event.content)
435
+ await self._flush_thinking_buffer(self.thinking_stream)
214
436
 
215
437
  async def _on_assistant_delta(self, event: events.AssistantMessageDeltaEvent) -> None:
216
438
  if self.renderer.is_sub_agent_session(event.session_id):
439
+ self.spinner_status.set_composing(True)
440
+ self._update_spinner()
217
441
  return
218
442
  if len(event.content.strip()) == 0 and self.stage_manager.current_stage != Stage.ASSISTANT:
219
443
  return
220
- first_delta = self.assistant_stream.mdstream is None
444
+ first_delta = not self.assistant_stream.is_active
221
445
  if first_delta:
222
- self.spinner_status.clear_tool_calls()
223
446
  self.spinner_status.set_composing(True)
447
+ self.spinner_status.clear_tool_calls()
224
448
  self._update_spinner()
225
- self.assistant_stream.mdstream = MarkdownStream(
449
+ mdstream = MarkdownStream(
226
450
  mdargs={"code_theme": self.renderer.themes.code_theme},
227
451
  theme=self.renderer.themes.markdown_theme,
228
452
  console=self.renderer.console,
229
- spinner=self.renderer.spinner_renderable(),
230
- mark="➤",
231
- indent=2,
453
+ live_sink=self.renderer.set_stream_renderable if const.MARKDOWN_STREAM_LIVE_REPAINT_ENABLED else None,
454
+ mark=ASSISTANT_MESSAGE_MARK,
455
+ left_margin=const.MARKDOWN_LEFT_MARGIN,
232
456
  )
457
+ self.assistant_stream.start(mdstream)
233
458
  self.assistant_stream.append(event.content)
459
+ self.spinner_status.set_buffer_length(len(self.assistant_stream.buffer))
460
+ if not first_delta:
461
+ self._update_spinner()
234
462
  if first_delta and self.assistant_stream.mdstream is not None:
235
- # Stop spinner and immediately start MarkdownStream's Live
236
- # to avoid flicker. The update() call starts the Live with
237
- # the spinner embedded, providing seamless transition.
238
- self.renderer.spinner_stop()
239
463
  self.assistant_stream.mdstream.update(self.assistant_stream.buffer)
240
464
  await self.stage_manager.transition_to(Stage.ASSISTANT)
241
- self.assistant_stream.debouncer.schedule()
465
+ await self._flush_assistant_buffer(self.assistant_stream)
242
466
 
243
467
  async def _on_assistant_message(self, event: events.AssistantMessageEvent) -> None:
244
468
  if self.renderer.is_sub_agent_session(event.session_id):
245
469
  return
246
470
  await self.stage_manager.transition_to(Stage.ASSISTANT)
247
- if self.assistant_stream.mdstream is not None:
248
- self.assistant_stream.debouncer.cancel()
249
- self.assistant_stream.mdstream.update(event.content.strip(), final=True)
471
+ if self.assistant_stream.is_active:
472
+ mdstream = self.assistant_stream.mdstream
473
+ assert mdstream is not None
474
+ mdstream.update(event.content.strip(), final=True)
250
475
  else:
251
476
  self.renderer.display_assistant_message(event.content)
252
- self.assistant_stream.clear()
253
- self.assistant_stream.mdstream = None
477
+ self.assistant_stream.finish()
254
478
  self.spinner_status.set_composing(False)
479
+ self._update_spinner()
255
480
  await self.stage_manager.transition_to(Stage.WAITING)
481
+ self.renderer.print()
256
482
  self.renderer.spinner_start()
257
483
 
258
484
  def _on_tool_call_start(self, event: events.TurnToolCallStartEvent) -> None:
@@ -268,32 +494,44 @@ class DisplayEventHandler:
268
494
  self.renderer.display_tool_call(event)
269
495
 
270
496
  async def _on_tool_result(self, event: events.ToolResultEvent) -> None:
271
- if self.renderer.is_sub_agent_session(event.session_id):
497
+ if self.renderer.is_sub_agent_session(event.session_id) and event.status == "success":
272
498
  return
273
499
  await self.stage_manager.transition_to(Stage.TOOL_RESULT)
274
- self.renderer.display_tool_call_result(event)
500
+ with self.renderer.session_print_context(event.session_id):
501
+ self.renderer.display_tool_call_result(event)
275
502
 
276
- def _on_response_metadata(self, event: events.ResponseMetadataEvent) -> None:
277
- self.renderer.display_response_metadata(event)
503
+ def _on_task_metadata(self, event: events.TaskMetadataEvent) -> None:
504
+ self.renderer.display_task_metadata(event)
278
505
 
279
506
  def _on_todo_change(self, event: events.TodoChangeEvent) -> None:
280
507
  active_form_status_text = self._extract_active_form_text(event)
281
- self.spinner_status.set_base_status(active_form_status_text if active_form_status_text else None)
508
+ self.spinner_status.set_todo_status(active_form_status_text if active_form_status_text else None)
282
509
  # Clear tool calls when todo changes, as the tool execution has advanced
283
- self._clear_and_update_spinner()
510
+ self.spinner_status.clear_for_new_turn()
511
+ self._update_spinner()
512
+
513
+ def _on_context_usage(self, event: events.ContextUsageEvent) -> None:
514
+ if self.renderer.is_sub_agent_session(event.session_id):
515
+ return
516
+ self.spinner_status.set_context_percent(event.context_percent)
517
+ self._update_spinner()
284
518
 
285
519
  async def _on_task_finish(self, event: events.TaskFinishEvent) -> None:
286
520
  self.renderer.display_task_finish(event)
287
521
  if not self.renderer.is_sub_agent_session(event.session_id):
522
+ r_status.clear_task_start()
288
523
  emit_osc94(OSC94States.HIDDEN)
289
524
  self.spinner_status.reset()
290
- self.renderer.spinner_stop()
525
+ self.renderer.spinner_stop()
526
+ self.renderer.console.print(Rule(characters="─", style=ThemeKey.LINES))
527
+ emit_tmux_signal() # Signal test harness if KLAUDE_TEST_SIGNAL is set
291
528
  await self.stage_manager.transition_to(Stage.WAITING)
292
529
  self._maybe_notify_task_finish(event)
293
530
 
294
531
  async def _on_interrupt(self, event: events.InterruptEvent) -> None:
295
532
  self.renderer.spinner_stop()
296
533
  self.spinner_status.reset()
534
+ r_status.clear_task_start()
297
535
  await self.stage_manager.transition_to(Stage.WAITING)
298
536
  emit_osc94(OSC94States.HIDDEN)
299
537
  self.renderer.display_interrupt()
@@ -311,33 +549,48 @@ class DisplayEventHandler:
311
549
  await self.stage_manager.transition_to(Stage.WAITING)
312
550
  self.renderer.spinner_stop()
313
551
  self.spinner_status.reset()
552
+ r_status.clear_task_start()
314
553
 
315
554
  # ─────────────────────────────────────────────────────────────────────────────
316
555
  # Private helper methods
317
556
  # ─────────────────────────────────────────────────────────────────────────────
318
557
 
319
558
  async def _finish_assistant_stream(self) -> None:
320
- if self.assistant_stream.mdstream is not None:
321
- self.assistant_stream.debouncer.cancel()
322
- self.assistant_stream.mdstream.update(self.assistant_stream.buffer, final=True)
323
- self.assistant_stream.mdstream = None
324
- self.assistant_stream.clear()
325
-
326
- def _print_thinking_prefix(self) -> None:
327
- self.renderer.display_thinking_prefix()
559
+ if self.assistant_stream.is_active:
560
+ mdstream = self.assistant_stream.mdstream
561
+ assert mdstream is not None
562
+ mdstream.update(self.assistant_stream.buffer, final=True)
563
+ self.assistant_stream.finish()
328
564
 
329
565
  def _update_spinner(self) -> None:
330
566
  """Update spinner text from current status state."""
331
- self.renderer.spinner_update(self.spinner_status.get_status())
332
-
333
- def _clear_and_update_spinner(self) -> None:
334
- """Clear tool calls and update spinner."""
335
- self.spinner_status.clear_tool_calls()
336
- self._update_spinner()
567
+ status_text = self.spinner_status.get_status()
568
+ right_text = self.spinner_status.get_right_text()
569
+ self.renderer.spinner_update(
570
+ status_text,
571
+ right_text,
572
+ )
337
573
 
338
574
  async def _flush_assistant_buffer(self, state: StreamState) -> None:
339
- if state.mdstream is not None:
340
- state.mdstream.update(state.buffer)
575
+ if state.is_active:
576
+ mdstream = state.mdstream
577
+ assert mdstream is not None
578
+ mdstream.update(state.buffer)
579
+
580
+ async def _flush_thinking_buffer(self, state: StreamState) -> None:
581
+ if state.is_active:
582
+ mdstream = state.mdstream
583
+ assert mdstream is not None
584
+ mdstream.update(normalize_thinking_content(state.buffer))
585
+
586
+ async def _finish_thinking_stream(self) -> None:
587
+ if self.thinking_stream.is_active:
588
+ mdstream = self.thinking_stream.mdstream
589
+ assert mdstream is not None
590
+ mdstream.update(normalize_thinking_content(self.thinking_stream.buffer), final=True)
591
+ self.thinking_stream.finish()
592
+ self.renderer.print()
593
+ self.renderer.spinner_start()
341
594
 
342
595
  def _maybe_notify_task_finish(self, event: events.TaskFinishEvent) -> None:
343
596
  if self.notifier is None:
@@ -368,8 +621,8 @@ class DisplayEventHandler:
368
621
  status_text = ""
369
622
  for todo in todo_event.todos:
370
623
  if todo.status == "in_progress":
371
- if len(todo.activeForm) > 0:
372
- status_text = todo.activeForm
624
+ if len(todo.active_form) > 0:
625
+ status_text = todo.active_form
373
626
  if len(todo.content) > 0:
374
627
  status_text = todo.content
375
- return status_text.replace("\n", "")
628
+ return status_text.replace("\n", " ").strip()