klaude-code 2.5.2__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 (61) 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 +89 -21
  4. klaude_code/cli/config_cmd.py +5 -5
  5. klaude_code/cli/cost_cmd.py +167 -68
  6. klaude_code/cli/main.py +51 -27
  7. klaude_code/cli/self_update.py +7 -7
  8. klaude_code/config/assets/builtin_config.yaml +45 -24
  9. klaude_code/config/builtin_config.py +23 -9
  10. klaude_code/config/config.py +19 -9
  11. klaude_code/config/model_matcher.py +1 -1
  12. klaude_code/const.py +2 -1
  13. klaude_code/core/tool/file/edit_tool.py +1 -1
  14. klaude_code/core/tool/file/read_tool.py +2 -2
  15. klaude_code/core/tool/file/write_tool.py +1 -1
  16. klaude_code/core/turn.py +21 -4
  17. klaude_code/llm/anthropic/client.py +75 -50
  18. klaude_code/llm/anthropic/input.py +20 -9
  19. klaude_code/llm/google/client.py +235 -148
  20. klaude_code/llm/google/input.py +44 -36
  21. klaude_code/llm/openai_compatible/stream.py +114 -100
  22. klaude_code/llm/openrouter/client.py +1 -0
  23. klaude_code/llm/openrouter/reasoning.py +4 -29
  24. klaude_code/llm/partial_message.py +2 -32
  25. klaude_code/llm/responses/client.py +99 -81
  26. klaude_code/llm/responses/input.py +11 -25
  27. klaude_code/llm/stream_parts.py +94 -0
  28. klaude_code/log.py +57 -0
  29. klaude_code/protocol/events.py +214 -0
  30. klaude_code/protocol/sub_agent/image_gen.py +0 -4
  31. klaude_code/session/session.py +51 -18
  32. klaude_code/tui/command/fork_session_cmd.py +14 -23
  33. klaude_code/tui/command/model_picker.py +2 -17
  34. klaude_code/tui/command/resume_cmd.py +2 -18
  35. klaude_code/tui/command/sub_agent_model_cmd.py +5 -19
  36. klaude_code/tui/command/thinking_cmd.py +2 -14
  37. klaude_code/tui/commands.py +0 -5
  38. klaude_code/tui/components/common.py +1 -1
  39. klaude_code/tui/components/metadata.py +21 -21
  40. klaude_code/tui/components/rich/quote.py +36 -8
  41. klaude_code/tui/components/rich/theme.py +2 -0
  42. klaude_code/tui/components/sub_agent.py +6 -0
  43. klaude_code/tui/display.py +11 -1
  44. klaude_code/tui/input/completers.py +11 -7
  45. klaude_code/tui/input/prompt_toolkit.py +3 -1
  46. klaude_code/tui/machine.py +108 -56
  47. klaude_code/tui/renderer.py +4 -65
  48. klaude_code/tui/terminal/selector.py +174 -31
  49. {klaude_code-2.5.2.dist-info → klaude_code-2.6.0.dist-info}/METADATA +23 -31
  50. {klaude_code-2.5.2.dist-info → klaude_code-2.6.0.dist-info}/RECORD +52 -58
  51. klaude_code/cli/session_cmd.py +0 -96
  52. klaude_code/protocol/events/__init__.py +0 -63
  53. klaude_code/protocol/events/base.py +0 -18
  54. klaude_code/protocol/events/chat.py +0 -30
  55. klaude_code/protocol/events/lifecycle.py +0 -23
  56. klaude_code/protocol/events/metadata.py +0 -16
  57. klaude_code/protocol/events/streaming.py +0 -43
  58. klaude_code/protocol/events/system.py +0 -56
  59. klaude_code/protocol/events/tools.py +0 -27
  60. {klaude_code-2.5.2.dist-info → klaude_code-2.6.0.dist-info}/WHEEL +0 -0
  61. {klaude_code-2.5.2.dist-info → klaude_code-2.6.0.dist-info}/entry_points.txt +0 -0
@@ -1,6 +1,8 @@
1
1
  from typing import TYPE_CHECKING, Any, Self
2
2
 
3
+ from rich.cells import cell_len
3
4
  from rich.console import Console, ConsoleOptions, RenderResult
5
+ from rich.measure import Measurement
4
6
  from rich.segment import Segment
5
7
  from rich.style import Style
6
8
 
@@ -16,10 +18,20 @@ class Quote:
16
18
  self.prefix = prefix
17
19
  self.style = style
18
20
 
21
+ def __rich_measure__(self, console: Console, options: ConsoleOptions) -> Measurement:
22
+ prefix_width = cell_len(self.prefix)
23
+ available_width = max(1, options.max_width - prefix_width)
24
+ content_measurement = Measurement.get(console, options.update(width=available_width), self.content)
25
+
26
+ minimum = min(options.max_width, content_measurement.minimum + prefix_width)
27
+ maximum = min(options.max_width, content_measurement.maximum + prefix_width)
28
+ return Measurement(minimum, maximum)
29
+
19
30
  def __rich_console__(self, console: Console, options: ConsoleOptions) -> RenderResult:
20
31
  # Reduce width to leave space for prefix
21
- prefix_width = len(self.prefix)
22
- render_options = options.update(width=options.max_width - prefix_width)
32
+ prefix_width = cell_len(self.prefix)
33
+ available_width = max(1, options.max_width - prefix_width)
34
+ render_options = options.update(width=available_width)
23
35
 
24
36
  # Get style
25
37
  quote_style = console.get_style(self.style) if isinstance(self.style, str) else self.style
@@ -29,7 +41,9 @@ class Quote:
29
41
  new_line = Segment("\n")
30
42
 
31
43
  # Render content as lines
32
- lines = console.render_lines(self.content, render_options)
44
+ # Avoid padding to full width.
45
+ # Trailing spaces can cause terminals to reflow wrapped lines on resize.
46
+ lines = console.render_lines(self.content, render_options, pad=False)
33
47
 
34
48
  for line in lines:
35
49
  yield prefix_segment
@@ -57,6 +71,19 @@ class TreeQuote:
57
71
  self.style = style
58
72
  self.style_first = style_first
59
73
 
74
+ def __rich_measure__(self, console: Console, options: ConsoleOptions) -> Measurement:
75
+ prefix_width = max(
76
+ cell_len(self.prefix_middle),
77
+ cell_len(self.prefix_last),
78
+ cell_len(self.prefix_first) if self.prefix_first is not None else 0,
79
+ )
80
+ available_width = max(1, options.max_width - prefix_width)
81
+ content_measurement = Measurement.get(console, options.update(width=available_width), self.content)
82
+
83
+ minimum = min(options.max_width, content_measurement.minimum + prefix_width)
84
+ maximum = min(options.max_width, content_measurement.maximum + prefix_width)
85
+ return Measurement(minimum, maximum)
86
+
60
87
  @classmethod
61
88
  def for_tool_call(cls, content: "RenderableType", *, mark: str, style: str, style_first: str) -> Self:
62
89
  """Create a tree quote for tool call display.
@@ -85,17 +112,18 @@ class TreeQuote:
85
112
  def __rich_console__(self, console: Console, options: ConsoleOptions) -> RenderResult:
86
113
  # Reduce width to leave space for prefix
87
114
  prefix_width = max(
88
- len(self.prefix_middle),
89
- len(self.prefix_last),
90
- len(self.prefix_first) if self.prefix_first is not None else 0,
115
+ cell_len(self.prefix_middle),
116
+ cell_len(self.prefix_last),
117
+ cell_len(self.prefix_first) if self.prefix_first is not None else 0,
91
118
  )
92
- render_options = options.update(width=options.max_width - prefix_width)
119
+ available_width = max(1, options.max_width - prefix_width)
120
+ render_options = options.update(width=available_width)
93
121
 
94
122
  quote_style = console.get_style(self.style) if isinstance(self.style, str) else self.style
95
123
  first_style = console.get_style(self.style_first) if isinstance(self.style_first, str) else self.style_first
96
124
 
97
125
  new_line = Segment("\n")
98
- lines = console.render_lines(self.content, render_options)
126
+ lines = console.render_lines(self.content, render_options, pad=False)
99
127
  line_count = len(lines)
100
128
 
101
129
  for idx, line in enumerate(lines):
@@ -133,6 +133,7 @@ class ThemeKey(str, Enum):
133
133
  METADATA = "metadata"
134
134
  METADATA_DIM = "metadata.dim"
135
135
  METADATA_BOLD = "metadata.bold"
136
+ METADATA_ITALIC = "metadata.italic"
136
137
  # SPINNER_STATUS
137
138
  STATUS_SPINNER = "spinner.status"
138
139
  STATUS_TEXT = "spinner.status.text"
@@ -259,6 +260,7 @@ def get_theme(theme: str | None = None) -> Themes:
259
260
  ThemeKey.METADATA.value: palette.lavender,
260
261
  ThemeKey.METADATA_DIM.value: "dim " + palette.lavender,
261
262
  ThemeKey.METADATA_BOLD.value: "bold " + palette.lavender,
263
+ ThemeKey.METADATA_ITALIC.value: "italic " + palette.lavender,
262
264
  # STATUS
263
265
  ThemeKey.STATUS_SPINNER.value: palette.blue,
264
266
  ThemeKey.STATUS_TEXT.value: palette.blue,
@@ -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."""
@@ -315,9 +315,11 @@ class PromptToolkitInput(InputProviderABC):
315
315
  "msg": "",
316
316
  "meta": "fg:ansibrightblack",
317
317
  "frame.border": "fg:ansibrightblack dim",
318
- "search_prefix": "fg:ansibrightblack",
318
+ "search_prefix": "ansibrightblack",
319
319
  "search_placeholder": "fg:ansibrightblack italic",
320
320
  "search_input": "",
321
+ "search_success": "noinherit fg:ansigreen",
322
+ "search_none": "noinherit fg:ansired",
321
323
  # Empty bottom-toolbar style
322
324
  "bottom-toolbar": "bg:default fg:default noreverse",
323
325
  "bottom-toolbar.text": "bg:default fg:default noreverse",
@@ -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,
@@ -243,7 +243,24 @@ class SpinnerStatusState:
243
243
  return Text(self._toast_status, style=ThemeKey.STATUS_TOAST)
244
244
 
245
245
  activity_text = self._activity.get_activity_text()
246
- base_status = self._reasoning_status or self._todo_status
246
+ todo_status = self._todo_status
247
+ reasoning_status = self._reasoning_status
248
+
249
+ if todo_status is not None:
250
+ base_status = todo_status
251
+ extra_reasoning = None if reasoning_status in (None, STATUS_THINKING_TEXT) else reasoning_status
252
+ else:
253
+ base_status = reasoning_status
254
+ extra_reasoning = None
255
+
256
+ if extra_reasoning is not None:
257
+ if activity_text is None:
258
+ activity_text = Text(extra_reasoning, style=ThemeKey.STATUS_TEXT_BOLD_ITALIC)
259
+ else:
260
+ prefixed = Text(extra_reasoning, style=ThemeKey.STATUS_TEXT_BOLD_ITALIC)
261
+ prefixed.append(" , ")
262
+ prefixed.append_text(activity_text)
263
+ activity_text = prefixed
247
264
 
248
265
  if base_status:
249
266
  # Default "Thinking ..." uses normal style; custom headers use bold italic
@@ -306,6 +323,7 @@ class _SessionState:
306
323
  @property
307
324
  def should_extract_reasoning_header(self) -> bool:
308
325
  """Gemini and GPT-5 models use markdown bold headers in thinking."""
326
+ return False # Temporarily disabled for all models
309
327
  if self.model_id is None:
310
328
  return False
311
329
  model_lower = self.model_id.lower()
@@ -364,17 +382,25 @@ class DisplayStateMachine:
364
382
  self._spinner.set_toast_status(None)
365
383
  return self._spinner_update_commands()
366
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
+
367
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]:
368
399
  session_id = getattr(event, "session_id", "__app__")
369
400
  s = self._session(session_id)
370
401
  cmds: list[RenderCommand] = []
371
402
 
372
403
  match event:
373
- case events.ReplayHistoryEvent() as e:
374
- cmds.append(RenderReplayHistory(e))
375
- cmds.append(SpinnerStop())
376
- return cmds
377
-
378
404
  case events.WelcomeEvent() as e:
379
405
  cmds.append(RenderWelcome(e))
380
406
  return cmds
@@ -390,13 +416,16 @@ class DisplayStateMachine:
390
416
  s.model_id = e.model_id
391
417
  if not s.is_sub_agent:
392
418
  self._set_primary_if_needed(e.session_id)
393
- cmds.append(TaskClockStart())
419
+ if not is_replay:
420
+ cmds.append(TaskClockStart())
394
421
  else:
395
422
  s.sub_agent_thinking_header = SubAgentThinkingHeaderState()
396
423
 
397
- cmds.append(SpinnerStart())
424
+ if not is_replay:
425
+ cmds.append(SpinnerStart())
398
426
  cmds.append(RenderTaskStart(e))
399
- cmds.extend(self._spinner_update_commands())
427
+ if not is_replay:
428
+ cmds.extend(self._spinner_update_commands())
400
429
  return cmds
401
430
 
402
431
  case events.DeveloperMessageEvent() as e:
@@ -409,9 +438,10 @@ class DisplayStateMachine:
409
438
 
410
439
  case events.TurnStartEvent() as e:
411
440
  cmds.append(RenderTurnStart(e))
412
- self._spinner.clear_for_new_turn()
413
- self._spinner.set_reasoning_status(None)
414
- 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())
415
445
  return cmds
416
446
 
417
447
  case events.ThinkingStartEvent() as e:
@@ -423,9 +453,11 @@ class DisplayStateMachine:
423
453
  s.thinking_tail = ""
424
454
  # Ensure the status reflects that reasoning has started even
425
455
  # before we receive any deltas (or a bold header).
426
- self._spinner.set_reasoning_status(STATUS_THINKING_TEXT)
456
+ if not is_replay:
457
+ self._spinner.set_reasoning_status(STATUS_THINKING_TEXT)
427
458
  cmds.append(StartThinkingStream(session_id=e.session_id))
428
- cmds.extend(self._spinner_update_commands())
459
+ if not is_replay:
460
+ cmds.extend(self._spinner_update_commands())
429
461
  return cmds
430
462
 
431
463
  case events.ThinkingDeltaEvent() as e:
@@ -445,7 +477,7 @@ class DisplayStateMachine:
445
477
 
446
478
  # Update reasoning status for spinner (based on bounded tail).
447
479
  # Only extract headers for models that use markdown bold headers in thinking.
448
- if s.should_extract_reasoning_header:
480
+ if not is_replay and s.should_extract_reasoning_header:
449
481
  s.thinking_tail = (s.thinking_tail + e.content)[-8192:]
450
482
  header = extract_last_bold_header(normalize_thinking_content(s.thinking_tail))
451
483
  if header:
@@ -460,26 +492,31 @@ class DisplayStateMachine:
460
492
  if not self._is_primary(e.session_id):
461
493
  return []
462
494
  s.thinking_stream_active = False
463
- self._spinner.clear_default_reasoning_status()
495
+ if not is_replay:
496
+ self._spinner.clear_default_reasoning_status()
464
497
  cmds.append(EndThinkingStream(session_id=e.session_id))
465
- cmds.append(SpinnerStart())
466
- cmds.extend(self._spinner_update_commands())
498
+ if not is_replay:
499
+ cmds.append(SpinnerStart())
500
+ cmds.extend(self._spinner_update_commands())
467
501
  return cmds
468
502
 
469
503
  case events.AssistantTextStartEvent() as e:
470
504
  if s.is_sub_agent:
471
- self._spinner.set_composing(True)
472
- cmds.extend(self._spinner_update_commands())
505
+ if not is_replay:
506
+ self._spinner.set_composing(True)
507
+ cmds.extend(self._spinner_update_commands())
473
508
  return cmds
474
509
  if not self._is_primary(e.session_id):
475
510
  return []
476
511
 
477
512
  s.assistant_stream_active = True
478
513
  s.assistant_char_count = 0
479
- self._spinner.set_composing(True)
480
- self._spinner.clear_tool_calls()
514
+ if not is_replay:
515
+ self._spinner.set_composing(True)
516
+ self._spinner.clear_tool_calls()
481
517
  cmds.append(StartAssistantStream(session_id=e.session_id))
482
- cmds.extend(self._spinner_update_commands())
518
+ if not is_replay:
519
+ cmds.extend(self._spinner_update_commands())
483
520
  return cmds
484
521
 
485
522
  case events.AssistantTextDeltaEvent() as e:
@@ -489,24 +526,29 @@ class DisplayStateMachine:
489
526
  return []
490
527
 
491
528
  s.assistant_char_count += len(e.content)
492
- self._spinner.set_buffer_length(s.assistant_char_count)
529
+ if not is_replay:
530
+ self._spinner.set_buffer_length(s.assistant_char_count)
493
531
  cmds.append(AppendAssistant(session_id=e.session_id, content=e.content))
494
- cmds.extend(self._spinner_update_commands())
532
+ if not is_replay:
533
+ cmds.extend(self._spinner_update_commands())
495
534
  return cmds
496
535
 
497
536
  case events.AssistantTextEndEvent() as e:
498
537
  if s.is_sub_agent:
499
- self._spinner.set_composing(False)
500
- cmds.extend(self._spinner_update_commands())
538
+ if not is_replay:
539
+ self._spinner.set_composing(False)
540
+ cmds.extend(self._spinner_update_commands())
501
541
  return cmds
502
542
  if not self._is_primary(e.session_id):
503
543
  return []
504
544
 
505
545
  s.assistant_stream_active = False
506
- self._spinner.set_composing(False)
546
+ if not is_replay:
547
+ self._spinner.set_composing(False)
507
548
  cmds.append(EndAssistantStream(session_id=e.session_id))
508
- cmds.append(SpinnerStart())
509
- cmds.extend(self._spinner_update_commands())
549
+ if not is_replay:
550
+ cmds.append(SpinnerStart())
551
+ cmds.extend(self._spinner_update_commands())
510
552
  return cmds
511
553
 
512
554
  case events.AssistantImageDeltaEvent() as e:
@@ -518,9 +560,10 @@ class DisplayStateMachine:
518
560
  return []
519
561
  if not self._is_primary(e.session_id):
520
562
  return []
521
- self._spinner.set_composing(False)
522
- cmds.append(SpinnerStart())
523
- 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())
524
567
  return cmds
525
568
 
526
569
  case events.ToolCallStartEvent() as e:
@@ -535,18 +578,20 @@ class DisplayStateMachine:
535
578
  primary.thinking_stream_active = False
536
579
  cmds.append(EndThinkingStream(session_id=primary.session_id))
537
580
 
538
- self._spinner.set_composing(False)
581
+ if not is_replay:
582
+ self._spinner.set_composing(False)
539
583
 
540
584
  # Skip activity state for fast tools on non-streaming models (e.g., Gemini)
541
585
  # to avoid flash-and-disappear effect
542
- 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):
543
587
  tool_active_form = get_tool_active_form(e.tool_name)
544
588
  if is_sub_agent_tool(e.tool_name):
545
589
  self._spinner.add_sub_agent_tool_call(e.tool_call_id, tool_active_form)
546
590
  else:
547
591
  self._spinner.add_tool_call(tool_active_form)
548
592
 
549
- cmds.extend(self._spinner_update_commands())
593
+ if not is_replay:
594
+ cmds.extend(self._spinner_update_commands())
550
595
  return cmds
551
596
 
552
597
  case events.ToolCallEvent() as e:
@@ -565,7 +610,7 @@ class DisplayStateMachine:
565
610
  return cmds
566
611
 
567
612
  case events.ToolResultEvent() as e:
568
- if is_sub_agent_tool(e.tool_name):
613
+ if not is_replay and is_sub_agent_tool(e.tool_name):
569
614
  self._spinner.finish_sub_agent_tool_call(e.tool_call_id, get_tool_active_form(e.tool_name))
570
615
  cmds.extend(self._spinner_update_commands())
571
616
 
@@ -583,9 +628,10 @@ class DisplayStateMachine:
583
628
 
584
629
  case events.TodoChangeEvent() as e:
585
630
  todo_text = _extract_active_form_text(e)
586
- self._spinner.set_todo_status(todo_text)
587
- self._spinner.clear_for_new_turn()
588
- 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())
589
635
  return cmds
590
636
 
591
637
  case events.UsageEvent() as e:
@@ -595,7 +641,7 @@ class DisplayStateMachine:
595
641
  if not self._is_primary(e.session_id):
596
642
  return []
597
643
  context_percent = e.usage.context_usage_percent
598
- if context_percent is not None:
644
+ if not is_replay and context_percent is not None:
599
645
  self._spinner.set_context_percent(context_percent)
600
646
  cmds.extend(self._spinner_update_commands())
601
647
  return cmds
@@ -606,37 +652,43 @@ class DisplayStateMachine:
606
652
  case events.TaskFinishEvent() as e:
607
653
  cmds.append(RenderTaskFinish(e))
608
654
  if not s.is_sub_agent:
609
- cmds.append(TaskClockClear())
610
- self._spinner.reset()
611
- cmds.append(SpinnerStop())
612
- cmds.append(PrintRuleLine())
613
- 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())
614
661
  else:
615
662
  s.sub_agent_thinking_header = None
616
663
  return cmds
617
664
 
618
665
  case events.InterruptEvent() as e:
619
- self._spinner.reset()
620
- cmds.append(SpinnerStop())
666
+ if not is_replay:
667
+ self._spinner.reset()
668
+ cmds.append(SpinnerStop())
621
669
  cmds.append(EndThinkingStream(session_id=e.session_id))
622
670
  cmds.append(EndAssistantStream(session_id=e.session_id))
623
- cmds.append(TaskClockClear())
671
+ if not is_replay:
672
+ cmds.append(TaskClockClear())
624
673
  cmds.append(RenderInterrupt(session_id=e.session_id))
625
674
  return cmds
626
675
 
627
676
  case events.ErrorEvent() as e:
628
- cmds.append(EmitOsc94Error())
677
+ if not is_replay:
678
+ cmds.append(EmitOsc94Error())
629
679
  cmds.append(RenderError(e))
630
- if not e.can_retry:
680
+ if not is_replay and not e.can_retry:
631
681
  self._spinner.reset()
632
682
  cmds.append(SpinnerStop())
633
- cmds.extend(self._spinner_update_commands())
683
+ if not is_replay:
684
+ cmds.extend(self._spinner_update_commands())
634
685
  return cmds
635
686
 
636
687
  case events.EndEvent():
637
- self._spinner.reset()
638
- cmds.append(SpinnerStop())
639
- cmds.append(TaskClockClear())
688
+ if not is_replay:
689
+ self._spinner.reset()
690
+ cmds.append(SpinnerStop())
691
+ cmds.append(TaskClockClear())
640
692
  return cmds
641
693
 
642
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,
@@ -64,7 +63,7 @@ from klaude_code.tui.components import thinking as c_thinking
64
63
  from klaude_code.tui.components import tools as c_tools
65
64
  from klaude_code.tui.components import user_input as c_user_input
66
65
  from klaude_code.tui.components import welcome as c_welcome
67
- from klaude_code.tui.components.common import truncate_head, truncate_middle
66
+ from klaude_code.tui.components.common import truncate_head
68
67
  from klaude_code.tui.components.rich import status as r_status
69
68
  from klaude_code.tui.components.rich.live import CropAboveLive, SingleLine
70
69
  from klaude_code.tui.components.rich.markdown import MarkdownStream, ThinkingMarkdown
@@ -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
@@ -578,9 +520,9 @@ class TUICommandRenderer:
578
520
  def display_error(self, event: events.ErrorEvent) -> None:
579
521
  if event.session_id:
580
522
  with self.session_print_context(event.session_id):
581
- self.print(c_errors.render_error(truncate_middle(event.error_message)))
523
+ self.print(c_errors.render_error(Text(event.error_message)))
582
524
  else:
583
- self.print(c_errors.render_error(truncate_middle(event.error_message)))
525
+ self.print(c_errors.render_error(Text(event.error_message)))
584
526
 
585
527
  # ---------------------------------------------------------------------
586
528
  # Notifications
@@ -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):