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