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