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