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.
- klaude_code/app/__init__.py +12 -0
- klaude_code/app/runtime.py +215 -0
- klaude_code/cli/auth_cmd.py +2 -2
- klaude_code/cli/config_cmd.py +2 -2
- klaude_code/cli/cost_cmd.py +1 -1
- klaude_code/cli/debug.py +12 -36
- klaude_code/cli/list_model.py +3 -3
- klaude_code/cli/main.py +17 -60
- klaude_code/cli/self_update.py +2 -187
- klaude_code/cli/session_cmd.py +2 -2
- klaude_code/config/config.py +1 -1
- klaude_code/config/select_model.py +1 -1
- klaude_code/const.py +10 -1
- klaude_code/core/agent.py +9 -62
- klaude_code/core/agent_profile.py +284 -0
- klaude_code/core/executor.py +343 -230
- klaude_code/core/manager/llm_clients_builder.py +1 -1
- klaude_code/core/manager/sub_agent_manager.py +16 -29
- klaude_code/core/reminders.py +107 -155
- klaude_code/core/task.py +12 -20
- klaude_code/core/tool/__init__.py +5 -19
- klaude_code/core/tool/context.py +84 -0
- klaude_code/core/tool/file/apply_patch_tool.py +18 -21
- klaude_code/core/tool/file/edit_tool.py +42 -44
- klaude_code/core/tool/file/read_tool.py +14 -9
- klaude_code/core/tool/file/write_tool.py +12 -13
- klaude_code/core/tool/report_back_tool.py +4 -1
- klaude_code/core/tool/shell/bash_tool.py +6 -11
- klaude_code/core/tool/skill/skill_tool.py +3 -1
- klaude_code/core/tool/sub_agent_tool.py +8 -7
- klaude_code/core/tool/todo/todo_write_tool.py +3 -9
- klaude_code/core/tool/todo/update_plan_tool.py +3 -5
- klaude_code/core/tool/tool_abc.py +2 -1
- klaude_code/core/tool/tool_registry.py +2 -33
- klaude_code/core/tool/tool_runner.py +13 -10
- klaude_code/core/tool/web/mermaid_tool.py +3 -1
- klaude_code/core/tool/web/web_fetch_tool.py +5 -3
- klaude_code/core/tool/web/web_search_tool.py +5 -3
- klaude_code/core/turn.py +86 -26
- klaude_code/llm/anthropic/client.py +1 -1
- klaude_code/llm/bedrock/client.py +1 -1
- klaude_code/llm/claude/client.py +1 -1
- klaude_code/llm/codex/client.py +1 -1
- klaude_code/llm/google/client.py +1 -1
- klaude_code/llm/openai_compatible/client.py +1 -1
- klaude_code/llm/openai_compatible/tool_call_accumulator.py +1 -1
- klaude_code/llm/openrouter/client.py +1 -1
- klaude_code/llm/openrouter/reasoning.py +1 -1
- klaude_code/llm/responses/client.py +1 -1
- klaude_code/protocol/events/__init__.py +57 -0
- klaude_code/protocol/events/base.py +18 -0
- klaude_code/protocol/events/chat.py +20 -0
- klaude_code/protocol/events/lifecycle.py +22 -0
- klaude_code/protocol/events/metadata.py +15 -0
- klaude_code/protocol/events/streaming.py +43 -0
- klaude_code/protocol/events/system.py +53 -0
- klaude_code/protocol/events/tools.py +23 -0
- klaude_code/protocol/message.py +3 -11
- klaude_code/protocol/model.py +78 -9
- klaude_code/protocol/op.py +5 -0
- klaude_code/protocol/sub_agent/explore.py +0 -15
- klaude_code/protocol/sub_agent/task.py +1 -1
- klaude_code/protocol/sub_agent/web.py +1 -17
- klaude_code/protocol/tools.py +0 -1
- klaude_code/session/session.py +6 -5
- klaude_code/skill/assets/create-plan/SKILL.md +76 -0
- klaude_code/skill/loader.py +1 -1
- klaude_code/skill/system_skills.py +1 -1
- klaude_code/tui/__init__.py +8 -0
- klaude_code/{command → tui/command}/clear_cmd.py +2 -1
- klaude_code/{command → tui/command}/debug_cmd.py +4 -3
- klaude_code/{command → tui/command}/export_cmd.py +2 -1
- klaude_code/{command → tui/command}/export_online_cmd.py +6 -5
- klaude_code/{command → tui/command}/fork_session_cmd.py +10 -9
- klaude_code/{command → tui/command}/help_cmd.py +3 -2
- klaude_code/{command → tui/command}/model_cmd.py +5 -4
- klaude_code/{command → tui/command}/model_select.py +2 -2
- klaude_code/{command → tui/command}/prompt_command.py +4 -3
- klaude_code/{command → tui/command}/refresh_cmd.py +3 -1
- klaude_code/{command → tui/command}/registry.py +16 -6
- klaude_code/{command → tui/command}/release_notes_cmd.py +3 -2
- klaude_code/{command → tui/command}/resume_cmd.py +6 -5
- klaude_code/{command → tui/command}/status_cmd.py +4 -3
- klaude_code/{command → tui/command}/terminal_setup_cmd.py +4 -3
- klaude_code/{command → tui/command}/thinking_cmd.py +4 -3
- klaude_code/tui/commands.py +164 -0
- klaude_code/{ui/renderers → tui/components}/assistant.py +3 -3
- klaude_code/{ui/renderers → tui/components}/bash_syntax.py +2 -2
- klaude_code/{ui/renderers → tui/components}/common.py +1 -1
- klaude_code/tui/components/developer.py +231 -0
- klaude_code/{ui/renderers → tui/components}/diffs.py +2 -2
- klaude_code/{ui/renderers → tui/components}/errors.py +2 -2
- klaude_code/{ui/renderers → tui/components}/metadata.py +34 -21
- klaude_code/{ui → tui/components}/rich/markdown.py +78 -34
- klaude_code/{ui → tui/components}/rich/status.py +2 -2
- klaude_code/{ui → tui/components}/rich/theme.py +12 -5
- klaude_code/{ui/renderers → tui/components}/sub_agent.py +23 -43
- klaude_code/{ui/renderers → tui/components}/thinking.py +3 -3
- klaude_code/{ui/renderers → tui/components}/tools.py +11 -48
- klaude_code/{ui/renderers → tui/components}/user_input.py +3 -20
- klaude_code/tui/display.py +85 -0
- klaude_code/{ui/modes/repl → tui/input}/__init__.py +1 -1
- klaude_code/{ui/modes/repl → tui/input}/completers.py +1 -1
- klaude_code/{ui/modes/repl/input_prompt_toolkit.py → tui/input/prompt_toolkit.py} +11 -7
- klaude_code/tui/machine.py +606 -0
- klaude_code/tui/renderer.py +707 -0
- klaude_code/tui/runner.py +321 -0
- klaude_code/tui/terminal/__init__.py +56 -0
- klaude_code/{ui → tui}/terminal/color.py +1 -1
- klaude_code/{ui → tui}/terminal/control.py +1 -1
- klaude_code/{ui → tui}/terminal/notifier.py +1 -1
- klaude_code/{ui → tui}/terminal/selector.py +36 -17
- klaude_code/ui/__init__.py +6 -50
- klaude_code/ui/core/display.py +3 -3
- klaude_code/ui/core/input.py +2 -1
- klaude_code/ui/{modes/debug/display.py → debug_mode.py} +1 -1
- klaude_code/ui/{modes/exec/display.py → exec_mode.py} +1 -4
- klaude_code/ui/terminal/__init__.py +6 -54
- klaude_code/ui/terminal/title.py +31 -0
- klaude_code/update.py +163 -0
- {klaude_code-2.0.1.dist-info → klaude_code-2.1.0.dist-info}/METADATA +1 -1
- klaude_code-2.1.0.dist-info/RECORD +235 -0
- klaude_code/cli/runtime.py +0 -525
- klaude_code/core/prompt.py +0 -108
- klaude_code/core/tool/file/move_tool.md +0 -41
- klaude_code/core/tool/file/move_tool.py +0 -435
- klaude_code/core/tool/tool_context.py +0 -148
- klaude_code/protocol/events.py +0 -194
- klaude_code/skill/assets/dev-docs/SKILL.md +0 -108
- klaude_code/trace/__init__.py +0 -21
- klaude_code/ui/core/stage_manager.py +0 -48
- klaude_code/ui/modes/__init__.py +0 -1
- klaude_code/ui/modes/debug/__init__.py +0 -1
- klaude_code/ui/modes/exec/__init__.py +0 -1
- klaude_code/ui/modes/repl/display.py +0 -61
- klaude_code/ui/modes/repl/event_handler.py +0 -634
- klaude_code/ui/modes/repl/renderer.py +0 -463
- klaude_code/ui/renderers/developer.py +0 -215
- klaude_code/ui/utils/__init__.py +0 -1
- klaude_code-2.0.1.dist-info/RECORD +0 -229
- /klaude_code/{trace/log.py → log.py} +0 -0
- /klaude_code/{command → tui/command}/__init__.py +0 -0
- /klaude_code/{command → tui/command}/command_abc.py +0 -0
- /klaude_code/{command → tui/command}/prompt-commit.md +0 -0
- /klaude_code/{command → tui/command}/prompt-init.md +0 -0
- /klaude_code/{ui/renderers → tui/components}/__init__.py +0 -0
- /klaude_code/{ui/renderers → tui/components}/mermaid_viewer.py +0 -0
- /klaude_code/{ui → tui/components}/rich/__init__.py +0 -0
- /klaude_code/{ui → tui/components}/rich/cjk_wrap.py +0 -0
- /klaude_code/{ui → tui/components}/rich/code_panel.py +0 -0
- /klaude_code/{ui → tui/components}/rich/live.py +0 -0
- /klaude_code/{ui → tui/components}/rich/quote.py +0 -0
- /klaude_code/{ui → tui/components}/rich/searchable_text.py +0 -0
- /klaude_code/{ui/modes/repl → tui/input}/clipboard.py +0 -0
- /klaude_code/{ui/modes/repl → tui/input}/key_bindings.py +0 -0
- /klaude_code/{ui → tui}/terminal/image.py +0 -0
- /klaude_code/{ui → tui}/terminal/progress_bar.py +0 -0
- /klaude_code/ui/{utils/common.py → common.py} +0 -0
- {klaude_code-2.0.1.dist-info → klaude_code-2.1.0.dist-info}/WHEEL +0 -0
- {klaude_code-2.0.1.dist-info → klaude_code-2.1.0.dist-info}/entry_points.txt +0 -0
|
@@ -1,634 +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
|
-
emit_osc94(OSC94States.INDETERMINATE)
|
|
420
|
-
|
|
421
|
-
def _on_developer_message(self, event: events.DeveloperMessageEvent) -> None:
|
|
422
|
-
self.renderer.display_developer_message(event)
|
|
423
|
-
self.renderer.display_command_output(event)
|
|
424
|
-
|
|
425
|
-
def _on_turn_start(self, event: events.TurnStartEvent) -> None:
|
|
426
|
-
emit_osc94(OSC94States.INDETERMINATE)
|
|
427
|
-
self.renderer.display_turn_start(event)
|
|
428
|
-
self.spinner_status.clear_for_new_turn()
|
|
429
|
-
self.spinner_status.set_reasoning_status(None)
|
|
430
|
-
self._update_spinner()
|
|
431
|
-
|
|
432
|
-
async def _on_thinking_delta(self, event: events.ThinkingDeltaEvent) -> None:
|
|
433
|
-
if self.renderer.is_sub_agent_session(event.session_id):
|
|
434
|
-
if not self.renderer.should_display_sub_agent_thinking_header(event.session_id):
|
|
435
|
-
return
|
|
436
|
-
state = self._sub_agent_thinking_headers.setdefault(event.session_id, SubAgentThinkingHeaderState())
|
|
437
|
-
header = state.append_and_extract_new_header(event.content)
|
|
438
|
-
if header:
|
|
439
|
-
with self.renderer.session_print_context(event.session_id):
|
|
440
|
-
self.renderer.display_thinking_header(header)
|
|
441
|
-
return
|
|
442
|
-
|
|
443
|
-
first_delta = not self.thinking_stream.is_active
|
|
444
|
-
if first_delta:
|
|
445
|
-
self.thinking_stream.start(self._new_thinking_mdstream())
|
|
446
|
-
|
|
447
|
-
self.thinking_stream.append(event.content)
|
|
448
|
-
|
|
449
|
-
reasoning_status = extract_last_bold_header(normalize_thinking_content(self.thinking_stream.buffer))
|
|
450
|
-
if reasoning_status:
|
|
451
|
-
self.spinner_status.set_reasoning_status(reasoning_status)
|
|
452
|
-
self._update_spinner()
|
|
453
|
-
|
|
454
|
-
if first_delta:
|
|
455
|
-
self.thinking_stream.render(transform=normalize_thinking_content)
|
|
456
|
-
|
|
457
|
-
await self.stage_manager.enter_thinking_stage()
|
|
458
|
-
self._flush_thinking_buffer()
|
|
459
|
-
|
|
460
|
-
async def _on_assistant_delta(self, event: events.AssistantTextDeltaEvent) -> None:
|
|
461
|
-
if self.renderer.is_sub_agent_session(event.session_id):
|
|
462
|
-
self.spinner_status.set_composing(True)
|
|
463
|
-
self._update_spinner()
|
|
464
|
-
return
|
|
465
|
-
|
|
466
|
-
if len(event.content.strip()) == 0 and self.stage_manager.current_stage != Stage.ASSISTANT:
|
|
467
|
-
await self.stage_manager.transition_to(Stage.WAITING)
|
|
468
|
-
return
|
|
469
|
-
|
|
470
|
-
await self.stage_manager.transition_to(Stage.ASSISTANT)
|
|
471
|
-
first_delta = not self.assistant_stream.is_active
|
|
472
|
-
if first_delta:
|
|
473
|
-
self.spinner_status.set_composing(True)
|
|
474
|
-
self.spinner_status.clear_tool_calls()
|
|
475
|
-
self._update_spinner()
|
|
476
|
-
self.assistant_stream.start(self._new_assistant_mdstream())
|
|
477
|
-
self.assistant_stream.append(event.content)
|
|
478
|
-
self.spinner_status.set_buffer_length(len(self.assistant_stream.buffer))
|
|
479
|
-
if not first_delta:
|
|
480
|
-
self._update_spinner()
|
|
481
|
-
if first_delta:
|
|
482
|
-
self.assistant_stream.render()
|
|
483
|
-
self._flush_assistant_buffer()
|
|
484
|
-
|
|
485
|
-
async def _on_assistant_message(self, event: events.AssistantMessageEvent) -> None:
|
|
486
|
-
if self.renderer.is_sub_agent_session(event.session_id):
|
|
487
|
-
return
|
|
488
|
-
|
|
489
|
-
await self.stage_manager.transition_to(Stage.WAITING)
|
|
490
|
-
self.spinner_status.set_composing(False)
|
|
491
|
-
self._update_spinner()
|
|
492
|
-
self.renderer.spinner_start()
|
|
493
|
-
|
|
494
|
-
async def _on_assistant_image_delta(self, event: events.AssistantImageDeltaEvent) -> None:
|
|
495
|
-
await self.stage_manager.transition_to(Stage.ASSISTANT)
|
|
496
|
-
self.renderer.display_image(event.file_path)
|
|
497
|
-
|
|
498
|
-
async def _on_tool_call_start(self, event: events.TurnToolCallStartEvent) -> None:
|
|
499
|
-
self._flush_assistant_buffer()
|
|
500
|
-
self.spinner_status.set_composing(False)
|
|
501
|
-
self.spinner_status.add_tool_call(get_tool_active_form(event.tool_name))
|
|
502
|
-
self._update_spinner()
|
|
503
|
-
|
|
504
|
-
async def _on_tool_call(self, event: events.ToolCallEvent) -> None:
|
|
505
|
-
await self.stage_manager.transition_to(Stage.TOOL_CALL)
|
|
506
|
-
with self.renderer.session_print_context(event.session_id):
|
|
507
|
-
self.renderer.display_tool_call(event)
|
|
508
|
-
|
|
509
|
-
async def _on_tool_result(self, event: events.ToolResultEvent) -> None:
|
|
510
|
-
is_sub_agent = self.renderer.is_sub_agent_session(event.session_id)
|
|
511
|
-
if is_sub_agent and event.status == "success":
|
|
512
|
-
return
|
|
513
|
-
await self.stage_manager.transition_to(Stage.TOOL_RESULT)
|
|
514
|
-
with self.renderer.session_print_context(event.session_id):
|
|
515
|
-
self.renderer.display_tool_call_result(event, is_sub_agent=is_sub_agent)
|
|
516
|
-
|
|
517
|
-
def _on_task_metadata(self, event: events.TaskMetadataEvent) -> None:
|
|
518
|
-
self.renderer.display_task_metadata(event)
|
|
519
|
-
|
|
520
|
-
def _on_todo_change(self, event: events.TodoChangeEvent) -> None:
|
|
521
|
-
self.spinner_status.set_todo_status(self._extract_active_form_text(event))
|
|
522
|
-
# Clear tool calls when todo changes, as the tool execution has advanced
|
|
523
|
-
self.spinner_status.clear_for_new_turn()
|
|
524
|
-
self._update_spinner()
|
|
525
|
-
|
|
526
|
-
def _on_context_usage(self, event: events.ContextUsageEvent) -> None:
|
|
527
|
-
if self.renderer.is_sub_agent_session(event.session_id):
|
|
528
|
-
return
|
|
529
|
-
self.spinner_status.set_context_percent(event.context_percent)
|
|
530
|
-
self._update_spinner()
|
|
531
|
-
|
|
532
|
-
async def _on_task_finish(self, event: events.TaskFinishEvent) -> None:
|
|
533
|
-
self.renderer.display_task_finish(event)
|
|
534
|
-
if not self.renderer.is_sub_agent_session(event.session_id):
|
|
535
|
-
r_status.clear_task_start()
|
|
536
|
-
emit_osc94(OSC94States.HIDDEN)
|
|
537
|
-
self.spinner_status.reset()
|
|
538
|
-
self.renderer.spinner_stop()
|
|
539
|
-
self.renderer.console.print(Rule(characters="─", style=ThemeKey.LINES))
|
|
540
|
-
emit_tmux_signal() # Signal test harness if KLAUDE_TEST_SIGNAL is set
|
|
541
|
-
else:
|
|
542
|
-
self._sub_agent_thinking_headers.pop(event.session_id, None)
|
|
543
|
-
await self.stage_manager.transition_to(Stage.WAITING)
|
|
544
|
-
self._maybe_notify_task_finish(event)
|
|
545
|
-
|
|
546
|
-
async def _on_interrupt(self, event: events.InterruptEvent) -> None:
|
|
547
|
-
self.renderer.spinner_stop()
|
|
548
|
-
self.spinner_status.reset()
|
|
549
|
-
r_status.clear_task_start()
|
|
550
|
-
await self.stage_manager.transition_to(Stage.WAITING)
|
|
551
|
-
emit_osc94(OSC94States.HIDDEN)
|
|
552
|
-
self.renderer.display_interrupt()
|
|
553
|
-
|
|
554
|
-
async def _on_error(self, event: events.ErrorEvent) -> None:
|
|
555
|
-
emit_osc94(OSC94States.ERROR)
|
|
556
|
-
await self.stage_manager.transition_to(Stage.WAITING)
|
|
557
|
-
self.renderer.display_error(event)
|
|
558
|
-
if not event.can_retry:
|
|
559
|
-
self.renderer.spinner_stop()
|
|
560
|
-
self.spinner_status.reset()
|
|
561
|
-
|
|
562
|
-
async def _on_end(self, event: events.EndEvent) -> None:
|
|
563
|
-
emit_osc94(OSC94States.HIDDEN)
|
|
564
|
-
await self.stage_manager.transition_to(Stage.WAITING)
|
|
565
|
-
self.renderer.spinner_stop()
|
|
566
|
-
self.spinner_status.reset()
|
|
567
|
-
r_status.clear_task_start()
|
|
568
|
-
|
|
569
|
-
# ─────────────────────────────────────────────────────────────────────────────
|
|
570
|
-
# Private helper methods
|
|
571
|
-
# ─────────────────────────────────────────────────────────────────────────────
|
|
572
|
-
|
|
573
|
-
def _update_spinner(self) -> None:
|
|
574
|
-
"""Update spinner text from current status state."""
|
|
575
|
-
status_text = self.spinner_status.get_status()
|
|
576
|
-
right_text = self.spinner_status.get_right_text()
|
|
577
|
-
self.renderer.spinner_update(
|
|
578
|
-
status_text,
|
|
579
|
-
right_text,
|
|
580
|
-
)
|
|
581
|
-
|
|
582
|
-
def _flush_thinking_buffer(self) -> None:
|
|
583
|
-
self.thinking_stream.render(transform=normalize_thinking_content)
|
|
584
|
-
|
|
585
|
-
def _flush_assistant_buffer(self) -> None:
|
|
586
|
-
self.assistant_stream.render()
|
|
587
|
-
|
|
588
|
-
async def _finish_thinking_stream(self) -> None:
|
|
589
|
-
finalized = self.thinking_stream.finalize(transform=normalize_thinking_content)
|
|
590
|
-
if finalized:
|
|
591
|
-
self.renderer.print()
|
|
592
|
-
self.renderer.spinner_start()
|
|
593
|
-
|
|
594
|
-
async def _finish_assistant_stream(self) -> None:
|
|
595
|
-
finalized = self.assistant_stream.finalize()
|
|
596
|
-
if finalized:
|
|
597
|
-
self.renderer.print()
|
|
598
|
-
|
|
599
|
-
def _maybe_notify_task_finish(self, event: events.TaskFinishEvent) -> None:
|
|
600
|
-
if self.notifier is None:
|
|
601
|
-
return
|
|
602
|
-
if self.renderer.is_sub_agent_session(event.session_id):
|
|
603
|
-
return
|
|
604
|
-
notification = self._build_task_finish_notification(event)
|
|
605
|
-
self.notifier.notify(notification)
|
|
606
|
-
|
|
607
|
-
def _build_task_finish_notification(self, event: events.TaskFinishEvent) -> Notification:
|
|
608
|
-
body = self._compact_result_text(event.task_result)
|
|
609
|
-
return Notification(
|
|
610
|
-
type=NotificationType.AGENT_TASK_COMPLETE,
|
|
611
|
-
title="Task Completed",
|
|
612
|
-
body=body,
|
|
613
|
-
)
|
|
614
|
-
|
|
615
|
-
def _compact_result_text(self, text: str) -> str | None:
|
|
616
|
-
stripped = text.strip()
|
|
617
|
-
if len(stripped) == 0:
|
|
618
|
-
return None
|
|
619
|
-
squashed = " ".join(stripped.split())
|
|
620
|
-
if len(squashed) > 200:
|
|
621
|
-
return squashed[:197] + "…"
|
|
622
|
-
return squashed
|
|
623
|
-
|
|
624
|
-
def _extract_active_form_text(self, todo_event: events.TodoChangeEvent) -> str | None:
|
|
625
|
-
status_text: str | None = None
|
|
626
|
-
for todo in todo_event.todos:
|
|
627
|
-
if todo.status == "in_progress" and len(todo.content) > 0:
|
|
628
|
-
status_text = todo.content
|
|
629
|
-
|
|
630
|
-
if status_text is None:
|
|
631
|
-
return None
|
|
632
|
-
|
|
633
|
-
normalized = status_text.replace("\n", " ").strip()
|
|
634
|
-
return normalized if normalized else None
|