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.
Files changed (69) hide show
  1. klaude_code/app/runtime.py +1 -1
  2. klaude_code/auth/antigravity/oauth.py +33 -29
  3. klaude_code/auth/claude/oauth.py +34 -49
  4. klaude_code/cli/cost_cmd.py +4 -4
  5. klaude_code/cli/list_model.py +1 -2
  6. klaude_code/config/assets/builtin_config.yaml +17 -0
  7. klaude_code/const.py +4 -3
  8. klaude_code/core/agent_profile.py +2 -5
  9. klaude_code/core/bash_mode.py +276 -0
  10. klaude_code/core/executor.py +40 -7
  11. klaude_code/core/manager/llm_clients.py +1 -0
  12. klaude_code/core/manager/llm_clients_builder.py +2 -2
  13. klaude_code/core/memory.py +140 -0
  14. klaude_code/core/reminders.py +17 -89
  15. klaude_code/core/task.py +1 -1
  16. klaude_code/core/tool/file/read_tool.py +13 -2
  17. klaude_code/core/tool/shell/bash_tool.py +1 -1
  18. klaude_code/core/turn.py +10 -4
  19. klaude_code/llm/bedrock_anthropic/__init__.py +3 -0
  20. klaude_code/llm/input_common.py +18 -0
  21. klaude_code/llm/{codex → openai_codex}/__init__.py +1 -1
  22. klaude_code/llm/{codex → openai_codex}/client.py +3 -3
  23. klaude_code/llm/openai_compatible/client.py +3 -1
  24. klaude_code/llm/openai_compatible/stream.py +19 -9
  25. klaude_code/llm/{responses → openai_responses}/client.py +1 -1
  26. klaude_code/llm/registry.py +3 -3
  27. klaude_code/llm/stream_parts.py +3 -1
  28. klaude_code/llm/usage.py +1 -1
  29. klaude_code/protocol/events.py +17 -1
  30. klaude_code/protocol/message.py +1 -0
  31. klaude_code/protocol/model.py +14 -1
  32. klaude_code/protocol/op.py +12 -0
  33. klaude_code/protocol/op_handler.py +5 -0
  34. klaude_code/session/session.py +22 -1
  35. klaude_code/tui/command/resume_cmd.py +1 -1
  36. klaude_code/tui/commands.py +15 -0
  37. klaude_code/tui/components/bash_syntax.py +4 -0
  38. klaude_code/tui/components/command_output.py +4 -5
  39. klaude_code/tui/components/developer.py +1 -3
  40. klaude_code/tui/components/diffs.py +3 -2
  41. klaude_code/tui/components/metadata.py +23 -26
  42. klaude_code/tui/components/rich/code_panel.py +31 -16
  43. klaude_code/tui/components/rich/markdown.py +44 -28
  44. klaude_code/tui/components/rich/status.py +2 -2
  45. klaude_code/tui/components/rich/theme.py +28 -16
  46. klaude_code/tui/components/tools.py +23 -0
  47. klaude_code/tui/components/user_input.py +49 -58
  48. klaude_code/tui/components/welcome.py +47 -2
  49. klaude_code/tui/display.py +15 -7
  50. klaude_code/tui/input/completers.py +8 -0
  51. klaude_code/tui/input/key_bindings.py +37 -1
  52. klaude_code/tui/input/prompt_toolkit.py +58 -31
  53. klaude_code/tui/machine.py +87 -49
  54. klaude_code/tui/renderer.py +148 -30
  55. klaude_code/tui/runner.py +22 -0
  56. klaude_code/tui/terminal/image.py +24 -3
  57. klaude_code/tui/terminal/notifier.py +11 -12
  58. klaude_code/tui/terminal/selector.py +1 -1
  59. klaude_code/ui/terminal/title.py +4 -2
  60. {klaude_code-2.9.0.dist-info → klaude_code-2.10.0.dist-info}/METADATA +1 -1
  61. {klaude_code-2.9.0.dist-info → klaude_code-2.10.0.dist-info}/RECORD +67 -66
  62. klaude_code/llm/bedrock/__init__.py +0 -3
  63. klaude_code/tui/components/assistant.py +0 -2
  64. /klaude_code/llm/{bedrock → bedrock_anthropic}/client.py +0 -0
  65. /klaude_code/llm/{codex → openai_codex}/prompt_sync.py +0 -0
  66. /klaude_code/llm/{responses → openai_responses}/__init__.py +0 -0
  67. /klaude_code/llm/{responses → openai_responses}/input.py +0 -0
  68. {klaude_code-2.9.0.dist-info → klaude_code-2.10.0.dist-info}/WHEEL +0 -0
  69. {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
- if not _should_submit_instead_of_accepting_completion(buf) and _accept_current_completion(buf):
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 = "bg:#2a2a2a fg:#5a5a5a"
69
- PLACEHOLDER_SYMBOL_STYLE_LIGHT_BG = "bg:#e6e6e6 fg:#7a7a7a"
70
- PLACEHOLDER_SYMBOL_STYLE_UNKNOWN_BG = "bg:#2a2a2a fg:#8a8a8a"
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
- toolbar_text = left_text + padding
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
- (text_style, "files"),
638
- (text_style, " • "),
639
- (symbol_style, " $ "),
640
- (text_style, " "),
641
- (text_style, "skills"),
642
- (text_style, " • "),
643
- (symbol_style, " / "),
644
- (text_style, " "),
645
- (text_style, "commands"),
646
- (text_style, " • "),
647
- (symbol_style, " ctrl-l "),
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:
@@ -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
- return
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 == STATUS_THINKING_TEXT
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
- return []
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
- if s.sub_agent_thinking_header is None:
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
- return []
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
- if e.tool_name == tools.TASK:
607
- pass
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 = get_tool_active_form(e.tool_name)
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
- if not is_replay:
684
- cmds.append(TaskClockClear())
685
- self._spinner.reset()
686
- cmds.append(SpinnerStop())
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: