klaude-code 2.9.0__py3-none-any.whl → 2.10.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- klaude_code/app/runtime.py +1 -1
- klaude_code/auth/antigravity/oauth.py +33 -29
- klaude_code/auth/claude/oauth.py +34 -49
- klaude_code/cli/cost_cmd.py +4 -4
- klaude_code/cli/list_model.py +1 -2
- klaude_code/config/assets/builtin_config.yaml +17 -0
- klaude_code/const.py +4 -3
- klaude_code/core/agent_profile.py +2 -5
- klaude_code/core/bash_mode.py +276 -0
- klaude_code/core/executor.py +40 -7
- klaude_code/core/manager/llm_clients.py +1 -0
- klaude_code/core/manager/llm_clients_builder.py +2 -2
- klaude_code/core/memory.py +140 -0
- klaude_code/core/reminders.py +17 -89
- klaude_code/core/task.py +1 -1
- klaude_code/core/tool/file/read_tool.py +13 -2
- klaude_code/core/tool/shell/bash_tool.py +1 -1
- klaude_code/core/turn.py +10 -4
- klaude_code/llm/bedrock_anthropic/__init__.py +3 -0
- klaude_code/llm/input_common.py +18 -0
- klaude_code/llm/{codex → openai_codex}/__init__.py +1 -1
- klaude_code/llm/{codex → openai_codex}/client.py +3 -3
- klaude_code/llm/openai_compatible/client.py +3 -1
- klaude_code/llm/openai_compatible/stream.py +19 -9
- klaude_code/llm/{responses → openai_responses}/client.py +1 -1
- klaude_code/llm/registry.py +3 -3
- klaude_code/llm/stream_parts.py +3 -1
- klaude_code/llm/usage.py +1 -1
- klaude_code/protocol/events.py +17 -1
- klaude_code/protocol/message.py +1 -0
- klaude_code/protocol/model.py +14 -1
- klaude_code/protocol/op.py +12 -0
- klaude_code/protocol/op_handler.py +5 -0
- klaude_code/session/session.py +22 -1
- klaude_code/tui/command/resume_cmd.py +1 -1
- klaude_code/tui/commands.py +15 -0
- klaude_code/tui/components/bash_syntax.py +4 -0
- klaude_code/tui/components/command_output.py +4 -5
- klaude_code/tui/components/developer.py +1 -3
- klaude_code/tui/components/diffs.py +3 -2
- klaude_code/tui/components/metadata.py +23 -26
- klaude_code/tui/components/rich/code_panel.py +31 -16
- klaude_code/tui/components/rich/markdown.py +44 -28
- klaude_code/tui/components/rich/status.py +2 -2
- klaude_code/tui/components/rich/theme.py +28 -16
- klaude_code/tui/components/tools.py +23 -0
- klaude_code/tui/components/user_input.py +49 -58
- klaude_code/tui/components/welcome.py +47 -2
- klaude_code/tui/display.py +15 -7
- klaude_code/tui/input/completers.py +8 -0
- klaude_code/tui/input/key_bindings.py +37 -1
- klaude_code/tui/input/prompt_toolkit.py +58 -31
- klaude_code/tui/machine.py +87 -49
- klaude_code/tui/renderer.py +148 -30
- klaude_code/tui/runner.py +22 -0
- klaude_code/tui/terminal/image.py +24 -3
- klaude_code/tui/terminal/notifier.py +11 -12
- klaude_code/tui/terminal/selector.py +1 -1
- klaude_code/ui/terminal/title.py +4 -2
- {klaude_code-2.9.0.dist-info → klaude_code-2.10.0.dist-info}/METADATA +1 -1
- {klaude_code-2.9.0.dist-info → klaude_code-2.10.0.dist-info}/RECORD +67 -66
- klaude_code/llm/bedrock/__init__.py +0 -3
- klaude_code/tui/components/assistant.py +0 -2
- /klaude_code/llm/{bedrock → bedrock_anthropic}/client.py +0 -0
- /klaude_code/llm/{codex → openai_codex}/prompt_sync.py +0 -0
- /klaude_code/llm/{responses → openai_responses}/__init__.py +0 -0
- /klaude_code/llm/{responses → openai_responses}/input.py +0 -0
- {klaude_code-2.9.0.dist-info → klaude_code-2.10.0.dist-info}/WHEEL +0 -0
- {klaude_code-2.9.0.dist-info → klaude_code-2.10.0.dist-info}/entry_points.txt +0 -0
|
@@ -219,6 +219,14 @@ class _ComboCompleter(Completer):
|
|
|
219
219
|
document: Document,
|
|
220
220
|
complete_event, # type: ignore[override]
|
|
221
221
|
) -> Iterable[Completion]:
|
|
222
|
+
# Bash mode: disable all completions.
|
|
223
|
+
# A command is considered bash mode only when the first character is `!` (or full-width `!`).
|
|
224
|
+
try:
|
|
225
|
+
if document.text.startswith(("!", "!")):
|
|
226
|
+
return
|
|
227
|
+
except Exception:
|
|
228
|
+
pass
|
|
229
|
+
|
|
222
230
|
# Try slash command completion first (only on first line)
|
|
223
231
|
if document.cursor_position_row == 0 and self._slash_completer.is_slash_command_context(document):
|
|
224
232
|
yield from self._slash_completer.get_completions(document, complete_event)
|
|
@@ -76,6 +76,9 @@ def create_key_bindings(
|
|
|
76
76
|
term_program = os.environ.get("TERM_PROGRAM", "").lower()
|
|
77
77
|
swallow_next_control_j = False
|
|
78
78
|
|
|
79
|
+
def _is_bash_mode_text(text: str) -> bool:
|
|
80
|
+
return text.startswith(("!", "!"))
|
|
81
|
+
|
|
79
82
|
def _data_requests_newline(data: str) -> bool:
|
|
80
83
|
"""Return True when incoming key data should insert a newline.
|
|
81
84
|
|
|
@@ -374,6 +377,33 @@ def create_key_bindings(
|
|
|
374
377
|
buf = event.current_buffer
|
|
375
378
|
doc = buf.document # type: ignore
|
|
376
379
|
|
|
380
|
+
# Normalize a leading full-width exclamation mark to ASCII so that:
|
|
381
|
+
# - UI echo shows `!cmd` consistently
|
|
382
|
+
# - history stores `!cmd` (not `!cmd`)
|
|
383
|
+
# - bash-mode detection is stable
|
|
384
|
+
try:
|
|
385
|
+
current_text = buf.text # type: ignore[reportUnknownMemberType]
|
|
386
|
+
cursor_pos = int(buf.cursor_position) # type: ignore[reportUnknownMemberType]
|
|
387
|
+
except Exception:
|
|
388
|
+
current_text = ""
|
|
389
|
+
cursor_pos = 0
|
|
390
|
+
|
|
391
|
+
if current_text.startswith("!"):
|
|
392
|
+
normalized = "!" + current_text[1:]
|
|
393
|
+
if normalized != current_text:
|
|
394
|
+
with contextlib.suppress(Exception):
|
|
395
|
+
buf.text = normalized # type: ignore[reportUnknownMemberType]
|
|
396
|
+
buf.cursor_position = min(cursor_pos, len(normalized)) # type: ignore[reportUnknownMemberType]
|
|
397
|
+
current_text = normalized
|
|
398
|
+
|
|
399
|
+
# Bash mode: if there is no command after `!` (ignoring only space/tab),
|
|
400
|
+
# ignore Enter but keep the input text as-is.
|
|
401
|
+
if _is_bash_mode_text(current_text):
|
|
402
|
+
after_bang = current_text[1:]
|
|
403
|
+
command = after_bang.lstrip(" \t")
|
|
404
|
+
if command == "":
|
|
405
|
+
return
|
|
406
|
+
|
|
377
407
|
data = getattr(event, "data", "")
|
|
378
408
|
if isinstance(data, str) and _data_requests_newline(data):
|
|
379
409
|
_insert_newline(event)
|
|
@@ -393,7 +423,13 @@ def create_key_bindings(
|
|
|
393
423
|
# When completions are visible, Enter accepts the current selection.
|
|
394
424
|
# This aligns with common TUI completion UX: navigation doesn't modify
|
|
395
425
|
# the buffer, and Enter/Tab inserts the selected option.
|
|
396
|
-
|
|
426
|
+
#
|
|
427
|
+
# Bash mode disables completions entirely, so always prefer submitting.
|
|
428
|
+
if (
|
|
429
|
+
not _is_bash_mode_text(current_text)
|
|
430
|
+
and not _should_submit_instead_of_accepting_completion(buf)
|
|
431
|
+
and _accept_current_completion(buf)
|
|
432
|
+
):
|
|
397
433
|
return
|
|
398
434
|
|
|
399
435
|
# Before submitting, expand any folded paste markers so that:
|
|
@@ -62,12 +62,13 @@ COMPLETION_SELECTED_LIGHT_BG = "ansigreen"
|
|
|
62
62
|
COMPLETION_SELECTED_UNKNOWN_BG = "ansigreen"
|
|
63
63
|
COMPLETION_MENU = "ansibrightblack"
|
|
64
64
|
INPUT_PROMPT_STYLE = "ansimagenta bold"
|
|
65
|
+
INPUT_PROMPT_BASH_STYLE = "ansigreen bold"
|
|
65
66
|
PLACEHOLDER_TEXT_STYLE_DARK_BG = "fg:#5a5a5a"
|
|
66
67
|
PLACEHOLDER_TEXT_STYLE_LIGHT_BG = "fg:#7a7a7a"
|
|
67
68
|
PLACEHOLDER_TEXT_STYLE_UNKNOWN_BG = "fg:#8a8a8a"
|
|
68
|
-
PLACEHOLDER_SYMBOL_STYLE_DARK_BG = "
|
|
69
|
-
PLACEHOLDER_SYMBOL_STYLE_LIGHT_BG = "
|
|
70
|
-
PLACEHOLDER_SYMBOL_STYLE_UNKNOWN_BG = "
|
|
69
|
+
PLACEHOLDER_SYMBOL_STYLE_DARK_BG = "fg:ansiblue"
|
|
70
|
+
PLACEHOLDER_SYMBOL_STYLE_LIGHT_BG = "fg:ansiblue"
|
|
71
|
+
PLACEHOLDER_SYMBOL_STYLE_UNKNOWN_BG = "fg:ansiblue"
|
|
71
72
|
|
|
72
73
|
|
|
73
74
|
# ---------------------------------------------------------------------------
|
|
@@ -244,6 +245,7 @@ class PromptToolkitInput(InputProviderABC):
|
|
|
244
245
|
get_current_llm_config: Callable[[], llm_param.LLMConfigParameter | None] | None = None,
|
|
245
246
|
command_info_provider: Callable[[], list[CommandInfo]] | None = None,
|
|
246
247
|
):
|
|
248
|
+
self._prompt_text = prompt
|
|
247
249
|
self._status_provider = status_provider
|
|
248
250
|
self._pre_prompt = pre_prompt
|
|
249
251
|
self._post_prompt = post_prompt
|
|
@@ -296,11 +298,11 @@ class PromptToolkitInput(InputProviderABC):
|
|
|
296
298
|
completion_selected = COMPLETION_SELECTED_UNKNOWN_BG
|
|
297
299
|
|
|
298
300
|
return PromptSession(
|
|
301
|
+
# Use a stable prompt string; we override the style dynamically in prompt_async.
|
|
299
302
|
[(INPUT_PROMPT_STYLE, prompt)],
|
|
300
303
|
history=FileHistory(str(history_path)),
|
|
301
304
|
multiline=True,
|
|
302
305
|
cursor=CursorShape.BLINKING_BEAM,
|
|
303
|
-
prompt_continuation=[(INPUT_PROMPT_STYLE, " ")],
|
|
304
306
|
key_bindings=kb,
|
|
305
307
|
completer=ThreadedCompleter(create_repl_completer(command_info_provider=self._command_info_provider)),
|
|
306
308
|
complete_while_typing=True,
|
|
@@ -340,6 +342,25 @@ class PromptToolkitInput(InputProviderABC):
|
|
|
340
342
|
),
|
|
341
343
|
)
|
|
342
344
|
|
|
345
|
+
def _is_bash_mode_active(self) -> bool:
|
|
346
|
+
try:
|
|
347
|
+
text = self._session.default_buffer.text
|
|
348
|
+
return text.startswith(("!", "!"))
|
|
349
|
+
except Exception:
|
|
350
|
+
return False
|
|
351
|
+
|
|
352
|
+
def _get_prompt_message(self) -> FormattedText:
|
|
353
|
+
style = INPUT_PROMPT_BASH_STYLE if self._is_bash_mode_active() else INPUT_PROMPT_STYLE
|
|
354
|
+
return FormattedText([(style, self._prompt_text)])
|
|
355
|
+
|
|
356
|
+
def _bash_mode_toolbar_fragments(self) -> StyleAndTextTuples:
|
|
357
|
+
if not self._is_bash_mode_active():
|
|
358
|
+
return []
|
|
359
|
+
return [
|
|
360
|
+
("fg:ansigreen", " bash mode"),
|
|
361
|
+
("fg:ansibrightblack", " (type ! at start; backspace first char to exit)"),
|
|
362
|
+
]
|
|
363
|
+
|
|
343
364
|
def _setup_model_picker(self) -> None:
|
|
344
365
|
"""Initialize the model picker overlay and attach it to the layout."""
|
|
345
366
|
model_picker = SelectOverlay[str](
|
|
@@ -600,18 +621,32 @@ class PromptToolkitInput(InputProviderABC):
|
|
|
600
621
|
display_text = f"Debug log: {debug_log_path}"
|
|
601
622
|
text_style = "fg:ansibrightblack"
|
|
602
623
|
|
|
624
|
+
bash_frags = self._bash_mode_toolbar_fragments()
|
|
625
|
+
bash_plain = "".join(frag[1] for frag in bash_frags)
|
|
626
|
+
|
|
603
627
|
if display_text:
|
|
604
628
|
left_text = " " + display_text
|
|
605
629
|
try:
|
|
606
630
|
terminal_width = shutil.get_terminal_size().columns
|
|
607
|
-
padding = " " * max(0, terminal_width - len(left_text))
|
|
608
631
|
except (OSError, ValueError):
|
|
632
|
+
terminal_width = 0
|
|
633
|
+
|
|
634
|
+
if terminal_width > 0 and bash_plain:
|
|
635
|
+
# Keep the right-side bash mode hint visible by truncating the left side if needed.
|
|
636
|
+
reserved = len(bash_plain)
|
|
637
|
+
max_left = max(0, terminal_width - reserved)
|
|
638
|
+
if len(left_text) > max_left:
|
|
639
|
+
left_text = left_text[: max_left - 1] + "…" if max_left >= 2 else ""
|
|
640
|
+
padding = " " * max(0, terminal_width - len(left_text) - reserved)
|
|
641
|
+
else:
|
|
609
642
|
padding = ""
|
|
610
643
|
|
|
611
|
-
|
|
612
|
-
return FormattedText([(text_style, toolbar_text)])
|
|
644
|
+
return FormattedText([(text_style, left_text + padding), *bash_frags])
|
|
613
645
|
|
|
614
|
-
# Show shortcut hints when nothing else to display
|
|
646
|
+
# Show shortcut hints when nothing else to display.
|
|
647
|
+
# In bash mode, prefer showing only the bash hint (no placeholder shortcuts).
|
|
648
|
+
if bash_frags:
|
|
649
|
+
return FormattedText([("fg:default", " "), *bash_frags])
|
|
615
650
|
return self._render_shortcut_hints()
|
|
616
651
|
|
|
617
652
|
# -------------------------------------------------------------------------
|
|
@@ -632,29 +667,20 @@ class PromptToolkitInput(InputProviderABC):
|
|
|
632
667
|
return FormattedText(
|
|
633
668
|
[
|
|
634
669
|
(text_style, " "),
|
|
635
|
-
(symbol_style, "
|
|
636
|
-
(text_style, " "),
|
|
637
|
-
(
|
|
638
|
-
(text_style, " • "),
|
|
639
|
-
(symbol_style, "
|
|
640
|
-
(text_style, " "),
|
|
641
|
-
(
|
|
642
|
-
(text_style, " • "),
|
|
643
|
-
(symbol_style, "
|
|
644
|
-
(text_style, " "),
|
|
645
|
-
(
|
|
646
|
-
(text_style, " • "),
|
|
647
|
-
(symbol_style, "
|
|
648
|
-
(text_style, " "),
|
|
649
|
-
(text_style, "models"),
|
|
650
|
-
(text_style, " • "),
|
|
651
|
-
(symbol_style, " ctrl-t "),
|
|
652
|
-
(text_style, " "),
|
|
653
|
-
(text_style, "think"),
|
|
654
|
-
(text_style, " • "),
|
|
655
|
-
(symbol_style, " ctrl-v "),
|
|
656
|
-
(text_style, " "),
|
|
657
|
-
(text_style, "paste image"),
|
|
670
|
+
(symbol_style, "@"),
|
|
671
|
+
(text_style, " files • "),
|
|
672
|
+
(symbol_style, "$"),
|
|
673
|
+
(text_style, " skills • "),
|
|
674
|
+
(symbol_style, "/"),
|
|
675
|
+
(text_style, " commands • "),
|
|
676
|
+
(symbol_style, "!"),
|
|
677
|
+
(text_style, " shell • "),
|
|
678
|
+
(symbol_style, "ctrl-l"),
|
|
679
|
+
(text_style, " models • "),
|
|
680
|
+
(symbol_style, "ctrl-t"),
|
|
681
|
+
(text_style, " think • "),
|
|
682
|
+
(symbol_style, "ctrl-v"),
|
|
683
|
+
(text_style, " paste image"),
|
|
658
684
|
]
|
|
659
685
|
)
|
|
660
686
|
|
|
@@ -680,6 +706,7 @@ class PromptToolkitInput(InputProviderABC):
|
|
|
680
706
|
# proper styling instead of showing raw escape codes.
|
|
681
707
|
with patch_stdout(raw=True):
|
|
682
708
|
line: str = await self._session.prompt_async(
|
|
709
|
+
message=self._get_prompt_message,
|
|
683
710
|
bottom_toolbar=self._get_bottom_toolbar,
|
|
684
711
|
)
|
|
685
712
|
if self._post_prompt is not None:
|
klaude_code/tui/machine.py
CHANGED
|
@@ -9,20 +9,23 @@ from klaude_code.const import (
|
|
|
9
9
|
STATUS_COMPACTING_TEXT,
|
|
10
10
|
STATUS_COMPOSING_TEXT,
|
|
11
11
|
STATUS_DEFAULT_TEXT,
|
|
12
|
+
STATUS_RUNNING_TEXT,
|
|
12
13
|
STATUS_SHOW_BUFFER_LENGTH,
|
|
13
14
|
STATUS_THINKING_TEXT,
|
|
14
15
|
)
|
|
15
16
|
from klaude_code.protocol import events, model, tools
|
|
16
17
|
from klaude_code.tui.commands import (
|
|
17
18
|
AppendAssistant,
|
|
19
|
+
AppendBashCommandOutput,
|
|
18
20
|
AppendThinking,
|
|
19
21
|
EmitOsc94Error,
|
|
20
22
|
EmitTmuxSignal,
|
|
21
23
|
EndAssistantStream,
|
|
22
24
|
EndThinkingStream,
|
|
23
25
|
PrintBlankLine,
|
|
24
|
-
PrintRuleLine,
|
|
25
26
|
RenderAssistantImage,
|
|
27
|
+
RenderBashCommandEnd,
|
|
28
|
+
RenderBashCommandStart,
|
|
26
29
|
RenderCommand,
|
|
27
30
|
RenderCommandOutput,
|
|
28
31
|
RenderCompactionSummary,
|
|
@@ -32,7 +35,6 @@ from klaude_code.tui.commands import (
|
|
|
32
35
|
RenderTaskFinish,
|
|
33
36
|
RenderTaskMetadata,
|
|
34
37
|
RenderTaskStart,
|
|
35
|
-
RenderThinkingHeader,
|
|
36
38
|
RenderToolCall,
|
|
37
39
|
RenderToolResult,
|
|
38
40
|
RenderTurnStart,
|
|
@@ -68,25 +70,6 @@ FAST_TOOLS: frozenset[str] = frozenset(
|
|
|
68
70
|
)
|
|
69
71
|
|
|
70
72
|
|
|
71
|
-
@dataclass
|
|
72
|
-
class SubAgentThinkingHeaderState:
|
|
73
|
-
buffer: str = ""
|
|
74
|
-
last_header: str | None = None
|
|
75
|
-
|
|
76
|
-
def append_and_extract_new_header(self, content: str) -> str | None:
|
|
77
|
-
self.buffer += content
|
|
78
|
-
|
|
79
|
-
max_chars = 8192
|
|
80
|
-
if len(self.buffer) > max_chars:
|
|
81
|
-
self.buffer = self.buffer[-max_chars:]
|
|
82
|
-
|
|
83
|
-
header = extract_last_bold_header(normalize_thinking_content(self.buffer))
|
|
84
|
-
if header and header != self.last_header:
|
|
85
|
-
self.last_header = header
|
|
86
|
-
return header
|
|
87
|
-
return None
|
|
88
|
-
|
|
89
|
-
|
|
90
73
|
class ActivityState:
|
|
91
74
|
"""Tracks composing/tool activity for spinner display."""
|
|
92
75
|
|
|
@@ -110,7 +93,10 @@ class ActivityState:
|
|
|
110
93
|
|
|
111
94
|
def add_sub_agent_tool_call(self, tool_call_id: str, tool_name: str) -> None:
|
|
112
95
|
if tool_call_id in self._sub_agent_tool_calls_by_id:
|
|
113
|
-
|
|
96
|
+
old_tool_name = self._sub_agent_tool_calls_by_id[tool_call_id]
|
|
97
|
+
self._sub_agent_tool_calls[old_tool_name] = self._sub_agent_tool_calls.get(old_tool_name, 0) - 1
|
|
98
|
+
if self._sub_agent_tool_calls[old_tool_name] <= 0:
|
|
99
|
+
self._sub_agent_tool_calls.pop(old_tool_name, None)
|
|
114
100
|
self._sub_agent_tool_calls_by_id[tool_call_id] = tool_name
|
|
115
101
|
self._sub_agent_tool_calls[tool_name] = self._sub_agent_tool_calls.get(tool_name, 0) + 1
|
|
116
102
|
|
|
@@ -262,7 +248,7 @@ class SpinnerStatusState:
|
|
|
262
248
|
|
|
263
249
|
if base_status:
|
|
264
250
|
# Default "Thinking ..." uses normal style; custom headers use bold italic
|
|
265
|
-
is_default_reasoning = base_status
|
|
251
|
+
is_default_reasoning = base_status in {STATUS_THINKING_TEXT, STATUS_RUNNING_TEXT}
|
|
266
252
|
status_style = ThemeKey.STATUS_TEXT if is_default_reasoning else ThemeKey.STATUS_TEXT_BOLD_ITALIC
|
|
267
253
|
if activity_text:
|
|
268
254
|
result = Text()
|
|
@@ -303,7 +289,6 @@ class SpinnerStatusState:
|
|
|
303
289
|
class _SessionState:
|
|
304
290
|
session_id: str
|
|
305
291
|
sub_agent_state: model.SubAgentState | None = None
|
|
306
|
-
sub_agent_thinking_header: SubAgentThinkingHeaderState | None = None
|
|
307
292
|
model_id: str | None = None
|
|
308
293
|
assistant_stream_active: bool = False
|
|
309
294
|
thinking_stream_active: bool = False
|
|
@@ -410,6 +395,37 @@ class DisplayStateMachine:
|
|
|
410
395
|
cmds.append(RenderUserMessage(e))
|
|
411
396
|
return cmds
|
|
412
397
|
|
|
398
|
+
case events.BashCommandStartEvent() as e:
|
|
399
|
+
if s.is_sub_agent:
|
|
400
|
+
return []
|
|
401
|
+
if not is_replay:
|
|
402
|
+
self._spinner.set_reasoning_status(STATUS_RUNNING_TEXT)
|
|
403
|
+
cmds.append(TaskClockStart())
|
|
404
|
+
cmds.append(SpinnerStart())
|
|
405
|
+
cmds.extend(self._spinner_update_commands())
|
|
406
|
+
|
|
407
|
+
cmds.append(RenderBashCommandStart(e))
|
|
408
|
+
return cmds
|
|
409
|
+
|
|
410
|
+
case events.BashCommandOutputDeltaEvent() as e:
|
|
411
|
+
if s.is_sub_agent:
|
|
412
|
+
return []
|
|
413
|
+
cmds.append(AppendBashCommandOutput(e))
|
|
414
|
+
return cmds
|
|
415
|
+
|
|
416
|
+
case events.BashCommandEndEvent() as e:
|
|
417
|
+
if s.is_sub_agent:
|
|
418
|
+
return []
|
|
419
|
+
cmds.append(RenderBashCommandEnd(e))
|
|
420
|
+
|
|
421
|
+
if not is_replay:
|
|
422
|
+
self._spinner.set_reasoning_status(None)
|
|
423
|
+
cmds.append(TaskClockClear())
|
|
424
|
+
cmds.append(SpinnerStop())
|
|
425
|
+
cmds.extend(self._spinner_update_commands())
|
|
426
|
+
|
|
427
|
+
return cmds
|
|
428
|
+
|
|
413
429
|
case events.TaskStartEvent() as e:
|
|
414
430
|
s.sub_agent_state = e.sub_agent_state
|
|
415
431
|
s.model_id = e.model_id
|
|
@@ -418,8 +434,6 @@ class DisplayStateMachine:
|
|
|
418
434
|
self._set_primary_if_needed(e.session_id)
|
|
419
435
|
if not is_replay:
|
|
420
436
|
cmds.append(TaskClockStart())
|
|
421
|
-
else:
|
|
422
|
-
s.sub_agent_thinking_header = SubAgentThinkingHeaderState()
|
|
423
437
|
|
|
424
438
|
if not is_replay:
|
|
425
439
|
cmds.append(SpinnerStart())
|
|
@@ -465,7 +479,11 @@ class DisplayStateMachine:
|
|
|
465
479
|
|
|
466
480
|
case events.ThinkingStartEvent() as e:
|
|
467
481
|
if s.is_sub_agent:
|
|
468
|
-
|
|
482
|
+
if not s.should_show_sub_agent_thinking_header:
|
|
483
|
+
return []
|
|
484
|
+
s.thinking_stream_active = True
|
|
485
|
+
cmds.append(StartThinkingStream(session_id=e.session_id))
|
|
486
|
+
return cmds
|
|
469
487
|
if not self._is_primary(e.session_id):
|
|
470
488
|
return []
|
|
471
489
|
s.thinking_stream_active = True
|
|
@@ -483,11 +501,7 @@ class DisplayStateMachine:
|
|
|
483
501
|
if s.is_sub_agent:
|
|
484
502
|
if not s.should_show_sub_agent_thinking_header:
|
|
485
503
|
return []
|
|
486
|
-
|
|
487
|
-
s.sub_agent_thinking_header = SubAgentThinkingHeaderState()
|
|
488
|
-
header = s.sub_agent_thinking_header.append_and_extract_new_header(e.content)
|
|
489
|
-
if header:
|
|
490
|
-
cmds.append(RenderThinkingHeader(session_id=e.session_id, header=header))
|
|
504
|
+
cmds.append(AppendThinking(session_id=e.session_id, content=e.content))
|
|
491
505
|
return cmds
|
|
492
506
|
|
|
493
507
|
if not self._is_primary(e.session_id):
|
|
@@ -507,7 +521,11 @@ class DisplayStateMachine:
|
|
|
507
521
|
|
|
508
522
|
case events.ThinkingEndEvent() as e:
|
|
509
523
|
if s.is_sub_agent:
|
|
510
|
-
|
|
524
|
+
if not s.should_show_sub_agent_thinking_header:
|
|
525
|
+
return []
|
|
526
|
+
s.thinking_stream_active = False
|
|
527
|
+
cmds.append(EndThinkingStream(session_id=e.session_id))
|
|
528
|
+
return cmds
|
|
511
529
|
if not self._is_primary(e.session_id):
|
|
512
530
|
return []
|
|
513
531
|
s.thinking_stream_active = False
|
|
@@ -579,6 +597,31 @@ class DisplayStateMachine:
|
|
|
579
597
|
return []
|
|
580
598
|
if not self._is_primary(e.session_id):
|
|
581
599
|
return []
|
|
600
|
+
|
|
601
|
+
# Some providers/models may not emit fine-grained AssistantText* deltas.
|
|
602
|
+
# In that case, ResponseCompleteEvent.content is the only assistant text we get.
|
|
603
|
+
# Render it as a single assistant stream to avoid dropping the entire message.
|
|
604
|
+
content = e.content
|
|
605
|
+
if content.strip():
|
|
606
|
+
# If we saw no streamed assistant text for this response, render from the final snapshot.
|
|
607
|
+
if s.assistant_char_count == 0:
|
|
608
|
+
if not s.assistant_stream_active:
|
|
609
|
+
s.assistant_stream_active = True
|
|
610
|
+
cmds.append(StartAssistantStream(session_id=e.session_id))
|
|
611
|
+
cmds.append(AppendAssistant(session_id=e.session_id, content=content))
|
|
612
|
+
s.assistant_char_count += len(content)
|
|
613
|
+
|
|
614
|
+
# Ensure any active assistant stream is finalized.
|
|
615
|
+
if s.assistant_stream_active:
|
|
616
|
+
s.assistant_stream_active = False
|
|
617
|
+
cmds.append(EndAssistantStream(session_id=e.session_id))
|
|
618
|
+
else:
|
|
619
|
+
# If there is an active stream but the final snapshot has no text,
|
|
620
|
+
# still finalize to flush any pending markdown rendering.
|
|
621
|
+
if s.assistant_stream_active:
|
|
622
|
+
s.assistant_stream_active = False
|
|
623
|
+
cmds.append(EndAssistantStream(session_id=e.session_id))
|
|
624
|
+
|
|
582
625
|
if not is_replay:
|
|
583
626
|
self._spinner.set_composing(False)
|
|
584
627
|
cmds.append(SpinnerStart())
|
|
@@ -603,14 +646,11 @@ class DisplayStateMachine:
|
|
|
603
646
|
# Skip activity state for fast tools on non-streaming models (e.g., Gemini)
|
|
604
647
|
# to avoid flash-and-disappear effect
|
|
605
648
|
if not is_replay and not s.should_skip_tool_activity(e.tool_name):
|
|
606
|
-
|
|
607
|
-
|
|
649
|
+
tool_active_form = get_tool_active_form(e.tool_name)
|
|
650
|
+
if is_sub_agent_tool(e.tool_name):
|
|
651
|
+
self._spinner.add_sub_agent_tool_call(e.tool_call_id, tool_active_form)
|
|
608
652
|
else:
|
|
609
|
-
tool_active_form
|
|
610
|
-
if is_sub_agent_tool(e.tool_name):
|
|
611
|
-
self._spinner.add_sub_agent_tool_call(e.tool_call_id, tool_active_form)
|
|
612
|
-
else:
|
|
613
|
-
self._spinner.add_tool_call(tool_active_form)
|
|
653
|
+
self._spinner.add_tool_call(tool_active_form)
|
|
614
654
|
|
|
615
655
|
if not is_replay:
|
|
616
656
|
cmds.extend(self._spinner_update_commands())
|
|
@@ -651,6 +691,8 @@ class DisplayStateMachine:
|
|
|
651
691
|
cmds.append(EndThinkingStream(e.session_id))
|
|
652
692
|
cmds.append(EndAssistantStream(e.session_id))
|
|
653
693
|
cmds.append(RenderTaskMetadata(e))
|
|
694
|
+
if is_replay:
|
|
695
|
+
cmds.append(PrintBlankLine())
|
|
654
696
|
return cmds
|
|
655
697
|
|
|
656
698
|
case events.TodoChangeEvent() as e:
|
|
@@ -679,15 +721,11 @@ class DisplayStateMachine:
|
|
|
679
721
|
case events.TaskFinishEvent() as e:
|
|
680
722
|
s.task_active = False
|
|
681
723
|
cmds.append(RenderTaskFinish(e))
|
|
682
|
-
if not s.is_sub_agent:
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
cmds.append(PrintRuleLine())
|
|
688
|
-
cmds.append(EmitTmuxSignal())
|
|
689
|
-
else:
|
|
690
|
-
s.sub_agent_thinking_header = None
|
|
724
|
+
if not s.is_sub_agent and not is_replay:
|
|
725
|
+
cmds.append(TaskClockClear())
|
|
726
|
+
self._spinner.reset()
|
|
727
|
+
cmds.append(SpinnerStop())
|
|
728
|
+
cmds.append(EmitTmuxSignal())
|
|
691
729
|
return cmds
|
|
692
730
|
|
|
693
731
|
case events.InterruptEvent() as e:
|