klaude-code 2.5.3__py3-none-any.whl → 2.6.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 (36) hide show
  1. klaude_code/auth/__init__.py +10 -0
  2. klaude_code/auth/env.py +77 -0
  3. klaude_code/cli/auth_cmd.py +87 -8
  4. klaude_code/cli/config_cmd.py +5 -5
  5. klaude_code/cli/cost_cmd.py +159 -60
  6. klaude_code/cli/main.py +52 -61
  7. klaude_code/cli/self_update.py +7 -7
  8. klaude_code/config/builtin_config.py +23 -9
  9. klaude_code/config/config.py +19 -9
  10. klaude_code/core/turn.py +7 -8
  11. klaude_code/llm/google/client.py +12 -0
  12. klaude_code/llm/openai_compatible/stream.py +5 -1
  13. klaude_code/llm/openrouter/client.py +1 -0
  14. klaude_code/protocol/events.py +214 -0
  15. klaude_code/protocol/sub_agent/image_gen.py +0 -4
  16. klaude_code/session/session.py +51 -18
  17. klaude_code/tui/commands.py +0 -5
  18. klaude_code/tui/components/metadata.py +4 -5
  19. klaude_code/tui/components/sub_agent.py +6 -0
  20. klaude_code/tui/display.py +11 -1
  21. klaude_code/tui/input/completers.py +11 -7
  22. klaude_code/tui/machine.py +89 -55
  23. klaude_code/tui/renderer.py +1 -62
  24. {klaude_code-2.5.3.dist-info → klaude_code-2.6.0.dist-info}/METADATA +23 -31
  25. {klaude_code-2.5.3.dist-info → klaude_code-2.6.0.dist-info}/RECORD +27 -34
  26. klaude_code/cli/session_cmd.py +0 -87
  27. klaude_code/protocol/events/__init__.py +0 -63
  28. klaude_code/protocol/events/base.py +0 -18
  29. klaude_code/protocol/events/chat.py +0 -30
  30. klaude_code/protocol/events/lifecycle.py +0 -23
  31. klaude_code/protocol/events/metadata.py +0 -16
  32. klaude_code/protocol/events/streaming.py +0 -43
  33. klaude_code/protocol/events/system.py +0 -56
  34. klaude_code/protocol/events/tools.py +0 -27
  35. {klaude_code-2.5.3.dist-info → klaude_code-2.6.0.dist-info}/WHEEL +0 -0
  36. {klaude_code-2.5.3.dist-info → klaude_code-2.6.0.dist-info}/entry_points.txt +0 -0
@@ -304,24 +304,57 @@ class Session(BaseModel):
304
304
  yield events.TurnStartEvent(session_id=self.id)
305
305
  match it:
306
306
  case message.AssistantMessage() as am:
307
- content = message.join_text_parts(am.parts)
308
- images = [part for part in am.parts if isinstance(part, message.ImageFilePart)]
309
- last_assistant_content = message.format_saved_images(images, content)
310
- thinking_text = "".join(
311
- part.text for part in am.parts if isinstance(part, message.ThinkingTextPart)
312
- )
313
- for image in images:
314
- yield events.AssistantImageDeltaEvent(
315
- file_path=image.file_path,
316
- response_id=am.response_id,
317
- session_id=self.id,
318
- )
319
- yield events.ResponseCompleteEvent(
320
- thinking_text=thinking_text or None,
321
- content=content,
322
- response_id=am.response_id,
323
- session_id=self.id,
324
- )
307
+ all_images = [part for part in am.parts if isinstance(part, message.ImageFilePart)]
308
+ full_content = message.join_text_parts(am.parts)
309
+ last_assistant_content = message.format_saved_images(all_images, full_content)
310
+
311
+ # Reconstruct streaming boundaries from saved parts.
312
+ # This allows replay to reuse the same TUI state machine as live events.
313
+ thinking_open = False
314
+ assistant_open = False
315
+
316
+ for part in am.parts:
317
+ if isinstance(part, message.ThinkingTextPart):
318
+ if assistant_open:
319
+ assistant_open = False
320
+ yield events.AssistantTextEndEvent(response_id=am.response_id, session_id=self.id)
321
+ if not thinking_open:
322
+ thinking_open = True
323
+ yield events.ThinkingStartEvent(response_id=am.response_id, session_id=self.id)
324
+ if part.text:
325
+ yield events.ThinkingDeltaEvent(
326
+ content=part.text,
327
+ response_id=am.response_id,
328
+ session_id=self.id,
329
+ )
330
+ continue
331
+
332
+ if thinking_open:
333
+ thinking_open = False
334
+ yield events.ThinkingEndEvent(response_id=am.response_id, session_id=self.id)
335
+
336
+ if isinstance(part, message.TextPart):
337
+ if not assistant_open:
338
+ assistant_open = True
339
+ yield events.AssistantTextStartEvent(response_id=am.response_id, session_id=self.id)
340
+ if part.text:
341
+ yield events.AssistantTextDeltaEvent(
342
+ content=part.text,
343
+ response_id=am.response_id,
344
+ session_id=self.id,
345
+ )
346
+ elif isinstance(part, message.ImageFilePart):
347
+ yield events.AssistantImageDeltaEvent(
348
+ file_path=part.file_path,
349
+ response_id=am.response_id,
350
+ session_id=self.id,
351
+ )
352
+
353
+ if thinking_open:
354
+ yield events.ThinkingEndEvent(response_id=am.response_id, session_id=self.id)
355
+ if assistant_open:
356
+ yield events.AssistantTextEndEvent(response_id=am.response_id, session_id=self.id)
357
+
325
358
  for part in am.parts:
326
359
  if not isinstance(part, message.ToolCallPart):
327
360
  continue
@@ -13,11 +13,6 @@ class RenderCommand:
13
13
  pass
14
14
 
15
15
 
16
- @dataclass(frozen=True, slots=True)
17
- class RenderReplayHistory(RenderCommand):
18
- event: events.ReplayHistoryEvent
19
-
20
-
21
16
  @dataclass(frozen=True, slots=True)
22
17
  class RenderWelcome(RenderCommand):
23
18
  event: events.WelcomeEvent
@@ -30,13 +30,12 @@ def _render_task_metadata_block(
30
30
  currency = metadata.usage.currency if metadata.usage else "USD"
31
31
  currency_symbol = "¥" if currency == "CNY" else "$"
32
32
 
33
- # Second column: model@provider description / tokens / cost / …
33
+ # Second column: provider/model description / tokens / cost / …
34
34
  content = Text()
35
- content.append_text(Text(metadata.model_name, style=ThemeKey.METADATA_BOLD))
36
35
  if metadata.provider is not None:
37
- content.append_text(Text("@", style=ThemeKey.METADATA)).append_text(
38
- Text(metadata.provider.lower().replace(" ", "-"), style=ThemeKey.METADATA)
39
- )
36
+ content.append_text(Text(metadata.provider.lower().replace(" ", "-"), style=ThemeKey.METADATA))
37
+ content.append_text(Text("/", style=ThemeKey.METADATA_DIM))
38
+ content.append_text(Text(metadata.model_name, style=ThemeKey.METADATA_BOLD))
40
39
  if metadata.description:
41
40
  content.append_text(Text(" ", style=ThemeKey.METADATA)).append_text(
42
41
  Text(metadata.description, style=ThemeKey.METADATA_ITALIC)
@@ -135,6 +135,7 @@ def build_sub_agent_state_from_tool_call(e: events.ToolCallEvent) -> model.SubAg
135
135
  description = profile.name
136
136
  prompt = ""
137
137
  output_schema: dict[str, Any] | None = None
138
+ generation: dict[str, Any] | None = None
138
139
  resume: str | None = None
139
140
  if e.arguments:
140
141
  try:
@@ -155,10 +156,15 @@ def build_sub_agent_state_from_tool_call(e: events.ToolCallEvent) -> model.SubAg
155
156
  schema_value = payload.get(profile.output_schema_arg)
156
157
  if isinstance(schema_value, dict):
157
158
  output_schema = cast(dict[str, Any], schema_value)
159
+ # Extract generation config for ImageGen
160
+ generation_value = payload.get("generation")
161
+ if isinstance(generation_value, dict):
162
+ generation = cast(dict[str, Any], generation_value)
158
163
  return model.SubAgentState(
159
164
  sub_agent_type=profile.name,
160
165
  sub_agent_desc=description,
161
166
  sub_agent_prompt=prompt,
162
167
  resume=resume,
163
168
  output_schema=output_schema,
169
+ generation=generation,
164
170
  )
@@ -30,8 +30,18 @@ class TUIDisplay(DisplayABC):
30
30
 
31
31
  @override
32
32
  async def consume_event(self, event: events.Event) -> None:
33
+ if isinstance(event, events.ReplayHistoryEvent):
34
+ await self._renderer.execute(self._machine.begin_replay())
35
+ for item in event.events:
36
+ commands = self._machine.transition_replay(item)
37
+ if commands:
38
+ await self._renderer.execute(commands)
39
+ await self._renderer.execute(self._machine.end_replay())
40
+ return
41
+
33
42
  commands = self._machine.transition(event)
34
- await self._renderer.execute(commands)
43
+ if commands:
44
+ await self._renderer.execute(commands)
35
45
 
36
46
  @override
37
47
  async def start(self) -> None:
@@ -313,11 +313,13 @@ class _AtFilesCompleter(Completer):
313
313
  if not suggestions:
314
314
  return [] # type: ignore[reportUnknownVariableType]
315
315
  start_position = token_start_in_input - len(text_before)
316
- for s in suggestions[: self._max_results]:
316
+ suggestions_to_show = suggestions[: self._max_results]
317
+ align_width = self._display_align_width(suggestions_to_show)
318
+ for s in suggestions_to_show:
317
319
  yield Completion(
318
320
  text=self._format_completion_text(s, is_quoted=is_quoted),
319
321
  start_position=start_position,
320
- display=self._format_display_label(s, 0),
322
+ display=self._format_display_label(s, align_width),
321
323
  display_meta=s,
322
324
  )
323
325
  return [] # type: ignore[reportUnknownVariableType]
@@ -329,12 +331,14 @@ class _AtFilesCompleter(Completer):
329
331
 
330
332
  # Prepare Completion objects. Replace from the '@' character.
331
333
  start_position = token_start_in_input - len(text_before) # negative
332
- for s in suggestions[: self._max_results]:
334
+ suggestions_to_show = suggestions[: self._max_results]
335
+ align_width = self._display_align_width(suggestions_to_show)
336
+ for s in suggestions_to_show:
333
337
  # Insert formatted text (with quoting when needed) so that subsequent typing does not keep triggering
334
338
  yield Completion(
335
339
  text=self._format_completion_text(s, is_quoted=is_quoted),
336
340
  start_position=start_position,
337
- display=self._format_display_label(s, 0),
341
+ display=self._format_display_label(s, align_width),
338
342
  display_meta=s,
339
343
  )
340
344
 
@@ -543,9 +547,9 @@ class _AtFilesCompleter(Completer):
543
547
  Keep this unstyled so that the completion menu's selection style can
544
548
  fully override the selected row.
545
549
  """
546
-
547
- _ = align_width
548
- return self._display_name(suggestion)
550
+ name = self._display_name(suggestion)
551
+ # Pad to align_width + extra padding for visual separation from meta
552
+ return name.ljust(align_width + 6)
549
553
 
550
554
  def _display_align_width(self, suggestions: list[str]) -> int:
551
555
  """Calculate alignment width for display labels."""
@@ -19,6 +19,7 @@ from klaude_code.tui.commands import (
19
19
  EmitTmuxSignal,
20
20
  EndAssistantStream,
21
21
  EndThinkingStream,
22
+ PrintBlankLine,
22
23
  PrintRuleLine,
23
24
  RenderAssistantImage,
24
25
  RenderCommand,
@@ -26,7 +27,6 @@ from klaude_code.tui.commands import (
26
27
  RenderDeveloperMessage,
27
28
  RenderError,
28
29
  RenderInterrupt,
29
- RenderReplayHistory,
30
30
  RenderTaskFinish,
31
31
  RenderTaskMetadata,
32
32
  RenderTaskStart,
@@ -382,17 +382,25 @@ class DisplayStateMachine:
382
382
  self._spinner.set_toast_status(None)
383
383
  return self._spinner_update_commands()
384
384
 
385
+ def begin_replay(self) -> list[RenderCommand]:
386
+ self._spinner.reset()
387
+ return [SpinnerStop(), PrintBlankLine()]
388
+
389
+ def end_replay(self) -> list[RenderCommand]:
390
+ return [SpinnerStop()]
391
+
392
+ def transition_replay(self, event: events.Event) -> list[RenderCommand]:
393
+ return self._transition(event, is_replay=True)
394
+
385
395
  def transition(self, event: events.Event) -> list[RenderCommand]:
396
+ return self._transition(event, is_replay=False)
397
+
398
+ def _transition(self, event: events.Event, *, is_replay: bool) -> list[RenderCommand]:
386
399
  session_id = getattr(event, "session_id", "__app__")
387
400
  s = self._session(session_id)
388
401
  cmds: list[RenderCommand] = []
389
402
 
390
403
  match event:
391
- case events.ReplayHistoryEvent() as e:
392
- cmds.append(RenderReplayHistory(e))
393
- cmds.append(SpinnerStop())
394
- return cmds
395
-
396
404
  case events.WelcomeEvent() as e:
397
405
  cmds.append(RenderWelcome(e))
398
406
  return cmds
@@ -408,13 +416,16 @@ class DisplayStateMachine:
408
416
  s.model_id = e.model_id
409
417
  if not s.is_sub_agent:
410
418
  self._set_primary_if_needed(e.session_id)
411
- cmds.append(TaskClockStart())
419
+ if not is_replay:
420
+ cmds.append(TaskClockStart())
412
421
  else:
413
422
  s.sub_agent_thinking_header = SubAgentThinkingHeaderState()
414
423
 
415
- cmds.append(SpinnerStart())
424
+ if not is_replay:
425
+ cmds.append(SpinnerStart())
416
426
  cmds.append(RenderTaskStart(e))
417
- cmds.extend(self._spinner_update_commands())
427
+ if not is_replay:
428
+ cmds.extend(self._spinner_update_commands())
418
429
  return cmds
419
430
 
420
431
  case events.DeveloperMessageEvent() as e:
@@ -427,9 +438,10 @@ class DisplayStateMachine:
427
438
 
428
439
  case events.TurnStartEvent() as e:
429
440
  cmds.append(RenderTurnStart(e))
430
- self._spinner.clear_for_new_turn()
431
- self._spinner.set_reasoning_status(None)
432
- cmds.extend(self._spinner_update_commands())
441
+ if not is_replay:
442
+ self._spinner.clear_for_new_turn()
443
+ self._spinner.set_reasoning_status(None)
444
+ cmds.extend(self._spinner_update_commands())
433
445
  return cmds
434
446
 
435
447
  case events.ThinkingStartEvent() as e:
@@ -441,9 +453,11 @@ class DisplayStateMachine:
441
453
  s.thinking_tail = ""
442
454
  # Ensure the status reflects that reasoning has started even
443
455
  # before we receive any deltas (or a bold header).
444
- self._spinner.set_reasoning_status(STATUS_THINKING_TEXT)
456
+ if not is_replay:
457
+ self._spinner.set_reasoning_status(STATUS_THINKING_TEXT)
445
458
  cmds.append(StartThinkingStream(session_id=e.session_id))
446
- cmds.extend(self._spinner_update_commands())
459
+ if not is_replay:
460
+ cmds.extend(self._spinner_update_commands())
447
461
  return cmds
448
462
 
449
463
  case events.ThinkingDeltaEvent() as e:
@@ -463,7 +477,7 @@ class DisplayStateMachine:
463
477
 
464
478
  # Update reasoning status for spinner (based on bounded tail).
465
479
  # Only extract headers for models that use markdown bold headers in thinking.
466
- if s.should_extract_reasoning_header:
480
+ if not is_replay and s.should_extract_reasoning_header:
467
481
  s.thinking_tail = (s.thinking_tail + e.content)[-8192:]
468
482
  header = extract_last_bold_header(normalize_thinking_content(s.thinking_tail))
469
483
  if header:
@@ -478,26 +492,31 @@ class DisplayStateMachine:
478
492
  if not self._is_primary(e.session_id):
479
493
  return []
480
494
  s.thinking_stream_active = False
481
- self._spinner.clear_default_reasoning_status()
495
+ if not is_replay:
496
+ self._spinner.clear_default_reasoning_status()
482
497
  cmds.append(EndThinkingStream(session_id=e.session_id))
483
- cmds.append(SpinnerStart())
484
- cmds.extend(self._spinner_update_commands())
498
+ if not is_replay:
499
+ cmds.append(SpinnerStart())
500
+ cmds.extend(self._spinner_update_commands())
485
501
  return cmds
486
502
 
487
503
  case events.AssistantTextStartEvent() as e:
488
504
  if s.is_sub_agent:
489
- self._spinner.set_composing(True)
490
- cmds.extend(self._spinner_update_commands())
505
+ if not is_replay:
506
+ self._spinner.set_composing(True)
507
+ cmds.extend(self._spinner_update_commands())
491
508
  return cmds
492
509
  if not self._is_primary(e.session_id):
493
510
  return []
494
511
 
495
512
  s.assistant_stream_active = True
496
513
  s.assistant_char_count = 0
497
- self._spinner.set_composing(True)
498
- self._spinner.clear_tool_calls()
514
+ if not is_replay:
515
+ self._spinner.set_composing(True)
516
+ self._spinner.clear_tool_calls()
499
517
  cmds.append(StartAssistantStream(session_id=e.session_id))
500
- cmds.extend(self._spinner_update_commands())
518
+ if not is_replay:
519
+ cmds.extend(self._spinner_update_commands())
501
520
  return cmds
502
521
 
503
522
  case events.AssistantTextDeltaEvent() as e:
@@ -507,24 +526,29 @@ class DisplayStateMachine:
507
526
  return []
508
527
 
509
528
  s.assistant_char_count += len(e.content)
510
- self._spinner.set_buffer_length(s.assistant_char_count)
529
+ if not is_replay:
530
+ self._spinner.set_buffer_length(s.assistant_char_count)
511
531
  cmds.append(AppendAssistant(session_id=e.session_id, content=e.content))
512
- cmds.extend(self._spinner_update_commands())
532
+ if not is_replay:
533
+ cmds.extend(self._spinner_update_commands())
513
534
  return cmds
514
535
 
515
536
  case events.AssistantTextEndEvent() as e:
516
537
  if s.is_sub_agent:
517
- self._spinner.set_composing(False)
518
- cmds.extend(self._spinner_update_commands())
538
+ if not is_replay:
539
+ self._spinner.set_composing(False)
540
+ cmds.extend(self._spinner_update_commands())
519
541
  return cmds
520
542
  if not self._is_primary(e.session_id):
521
543
  return []
522
544
 
523
545
  s.assistant_stream_active = False
524
- self._spinner.set_composing(False)
546
+ if not is_replay:
547
+ self._spinner.set_composing(False)
525
548
  cmds.append(EndAssistantStream(session_id=e.session_id))
526
- cmds.append(SpinnerStart())
527
- cmds.extend(self._spinner_update_commands())
549
+ if not is_replay:
550
+ cmds.append(SpinnerStart())
551
+ cmds.extend(self._spinner_update_commands())
528
552
  return cmds
529
553
 
530
554
  case events.AssistantImageDeltaEvent() as e:
@@ -536,9 +560,10 @@ class DisplayStateMachine:
536
560
  return []
537
561
  if not self._is_primary(e.session_id):
538
562
  return []
539
- self._spinner.set_composing(False)
540
- cmds.append(SpinnerStart())
541
- cmds.extend(self._spinner_update_commands())
563
+ if not is_replay:
564
+ self._spinner.set_composing(False)
565
+ cmds.append(SpinnerStart())
566
+ cmds.extend(self._spinner_update_commands())
542
567
  return cmds
543
568
 
544
569
  case events.ToolCallStartEvent() as e:
@@ -553,18 +578,20 @@ class DisplayStateMachine:
553
578
  primary.thinking_stream_active = False
554
579
  cmds.append(EndThinkingStream(session_id=primary.session_id))
555
580
 
556
- self._spinner.set_composing(False)
581
+ if not is_replay:
582
+ self._spinner.set_composing(False)
557
583
 
558
584
  # Skip activity state for fast tools on non-streaming models (e.g., Gemini)
559
585
  # to avoid flash-and-disappear effect
560
- if not s.should_skip_tool_activity(e.tool_name):
586
+ if not is_replay and not s.should_skip_tool_activity(e.tool_name):
561
587
  tool_active_form = get_tool_active_form(e.tool_name)
562
588
  if is_sub_agent_tool(e.tool_name):
563
589
  self._spinner.add_sub_agent_tool_call(e.tool_call_id, tool_active_form)
564
590
  else:
565
591
  self._spinner.add_tool_call(tool_active_form)
566
592
 
567
- cmds.extend(self._spinner_update_commands())
593
+ if not is_replay:
594
+ cmds.extend(self._spinner_update_commands())
568
595
  return cmds
569
596
 
570
597
  case events.ToolCallEvent() as e:
@@ -583,7 +610,7 @@ class DisplayStateMachine:
583
610
  return cmds
584
611
 
585
612
  case events.ToolResultEvent() as e:
586
- if is_sub_agent_tool(e.tool_name):
613
+ if not is_replay and is_sub_agent_tool(e.tool_name):
587
614
  self._spinner.finish_sub_agent_tool_call(e.tool_call_id, get_tool_active_form(e.tool_name))
588
615
  cmds.extend(self._spinner_update_commands())
589
616
 
@@ -601,9 +628,10 @@ class DisplayStateMachine:
601
628
 
602
629
  case events.TodoChangeEvent() as e:
603
630
  todo_text = _extract_active_form_text(e)
604
- self._spinner.set_todo_status(todo_text)
605
- self._spinner.clear_for_new_turn()
606
- cmds.extend(self._spinner_update_commands())
631
+ if not is_replay:
632
+ self._spinner.set_todo_status(todo_text)
633
+ self._spinner.clear_for_new_turn()
634
+ cmds.extend(self._spinner_update_commands())
607
635
  return cmds
608
636
 
609
637
  case events.UsageEvent() as e:
@@ -613,7 +641,7 @@ class DisplayStateMachine:
613
641
  if not self._is_primary(e.session_id):
614
642
  return []
615
643
  context_percent = e.usage.context_usage_percent
616
- if context_percent is not None:
644
+ if not is_replay and context_percent is not None:
617
645
  self._spinner.set_context_percent(context_percent)
618
646
  cmds.extend(self._spinner_update_commands())
619
647
  return cmds
@@ -624,37 +652,43 @@ class DisplayStateMachine:
624
652
  case events.TaskFinishEvent() as e:
625
653
  cmds.append(RenderTaskFinish(e))
626
654
  if not s.is_sub_agent:
627
- cmds.append(TaskClockClear())
628
- self._spinner.reset()
629
- cmds.append(SpinnerStop())
630
- cmds.append(PrintRuleLine())
631
- cmds.append(EmitTmuxSignal())
655
+ if not is_replay:
656
+ cmds.append(TaskClockClear())
657
+ self._spinner.reset()
658
+ cmds.append(SpinnerStop())
659
+ cmds.append(PrintRuleLine())
660
+ cmds.append(EmitTmuxSignal())
632
661
  else:
633
662
  s.sub_agent_thinking_header = None
634
663
  return cmds
635
664
 
636
665
  case events.InterruptEvent() as e:
637
- self._spinner.reset()
638
- cmds.append(SpinnerStop())
666
+ if not is_replay:
667
+ self._spinner.reset()
668
+ cmds.append(SpinnerStop())
639
669
  cmds.append(EndThinkingStream(session_id=e.session_id))
640
670
  cmds.append(EndAssistantStream(session_id=e.session_id))
641
- cmds.append(TaskClockClear())
671
+ if not is_replay:
672
+ cmds.append(TaskClockClear())
642
673
  cmds.append(RenderInterrupt(session_id=e.session_id))
643
674
  return cmds
644
675
 
645
676
  case events.ErrorEvent() as e:
646
- cmds.append(EmitOsc94Error())
677
+ if not is_replay:
678
+ cmds.append(EmitOsc94Error())
647
679
  cmds.append(RenderError(e))
648
- if not e.can_retry:
680
+ if not is_replay and not e.can_retry:
649
681
  self._spinner.reset()
650
682
  cmds.append(SpinnerStop())
651
- cmds.extend(self._spinner_update_commands())
683
+ if not is_replay:
684
+ cmds.extend(self._spinner_update_commands())
652
685
  return cmds
653
686
 
654
687
  case events.EndEvent():
655
- self._spinner.reset()
656
- cmds.append(SpinnerStop())
657
- cmds.append(TaskClockClear())
688
+ if not is_replay:
689
+ self._spinner.reset()
690
+ cmds.append(SpinnerStop())
691
+ cmds.append(TaskClockClear())
658
692
  return cmds
659
693
 
660
694
  case _:
@@ -35,7 +35,6 @@ from klaude_code.tui.commands import (
35
35
  RenderDeveloperMessage,
36
36
  RenderError,
37
37
  RenderInterrupt,
38
- RenderReplayHistory,
39
38
  RenderTaskFinish,
40
39
  RenderTaskMetadata,
41
40
  RenderTaskStart,
@@ -437,67 +436,10 @@ class TUICommandRenderer:
437
436
  Text.assemble(
438
437
  (c_thinking.THINKING_MESSAGE_MARK, ThemeKey.THINKING),
439
438
  " ",
440
- (stripped, ThemeKey.THINKING_BOLD),
439
+ (stripped, ThemeKey.THINKING),
441
440
  )
442
441
  )
443
442
 
444
- async def replay_history(self, history_events: events.ReplayHistoryEvent) -> None:
445
- tool_call_dict: dict[str, events.ToolCallEvent] = {}
446
- self.print()
447
- for event in history_events.events:
448
- event_session_id = getattr(event, "session_id", history_events.session_id)
449
- is_sub_agent = self.is_sub_agent_session(event_session_id)
450
-
451
- with self.session_print_context(event_session_id):
452
- match event:
453
- case events.TaskStartEvent() as e:
454
- self.display_task_start(e)
455
- case events.TurnStartEvent():
456
- self.print()
457
- case events.AssistantImageDeltaEvent() as e:
458
- self.display_image(e.file_path)
459
- case events.ResponseCompleteEvent() as e:
460
- if is_sub_agent:
461
- if self._should_display_sub_agent_thinking_header(event_session_id) and e.thinking_text:
462
- header = c_thinking.extract_last_bold_header(
463
- c_thinking.normalize_thinking_content(e.thinking_text)
464
- )
465
- if header:
466
- self.display_thinking_header(header)
467
- continue
468
- if e.thinking_text:
469
- self.display_thinking(e.thinking_text)
470
- renderable = c_assistant.render_assistant_message(e.content, code_theme=self.themes.code_theme)
471
- if renderable is not None:
472
- self.print(renderable)
473
- self.print()
474
- case events.DeveloperMessageEvent() as e:
475
- self.display_developer_message(e)
476
- case events.UserMessageEvent() as e:
477
- if is_sub_agent:
478
- continue
479
- self.print(c_user_input.render_user_input(e.content))
480
- case events.ToolCallEvent() as e:
481
- tool_call_dict[e.tool_call_id] = e
482
- case events.ToolResultEvent() as e:
483
- tool_call_event = tool_call_dict.get(e.tool_call_id)
484
- if tool_call_event is not None:
485
- self.display_tool_call(tool_call_event)
486
- tool_call_dict.pop(e.tool_call_id, None)
487
- if is_sub_agent:
488
- continue
489
- self.display_tool_call_result(e)
490
- case events.TaskMetadataEvent() as e:
491
- self.print(c_metadata.render_task_metadata(e))
492
- self.print()
493
- case events.InterruptEvent():
494
- self.print()
495
- self.print(c_user_input.render_interrupt())
496
- case events.ErrorEvent() as e:
497
- self.display_error(e)
498
- case events.TaskFinishEvent() as e:
499
- self.display_task_finish(e)
500
-
501
443
  def display_developer_message(self, e: events.DeveloperMessageEvent) -> None:
502
444
  if not c_developer.need_render_developer_message(e):
503
445
  return
@@ -615,9 +557,6 @@ class TUICommandRenderer:
615
557
  async def execute(self, commands: list[RenderCommand]) -> None:
616
558
  for cmd in commands:
617
559
  match cmd:
618
- case RenderReplayHistory(event=event):
619
- await self.replay_history(event)
620
- self.spinner_stop()
621
560
  case RenderWelcome(event=event):
622
561
  self.display_welcome(event)
623
562
  case RenderUserMessage(event=event):