klaude-code 2.4.2__py3-none-any.whl → 2.5.1__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 (57) hide show
  1. klaude_code/app/runtime.py +2 -6
  2. klaude_code/cli/main.py +0 -1
  3. klaude_code/config/assets/builtin_config.yaml +7 -0
  4. klaude_code/const.py +7 -4
  5. klaude_code/core/agent.py +10 -1
  6. klaude_code/core/agent_profile.py +47 -35
  7. klaude_code/core/executor.py +6 -21
  8. klaude_code/core/manager/sub_agent_manager.py +17 -1
  9. klaude_code/core/prompts/prompt-sub-agent-web.md +4 -4
  10. klaude_code/core/task.py +66 -4
  11. klaude_code/core/tool/__init__.py +0 -5
  12. klaude_code/core/tool/context.py +12 -1
  13. klaude_code/core/tool/offload.py +311 -0
  14. klaude_code/core/tool/shell/bash_tool.md +1 -43
  15. klaude_code/core/tool/sub_agent_tool.py +1 -0
  16. klaude_code/core/tool/todo/todo_write_tool.md +0 -23
  17. klaude_code/core/tool/tool_runner.py +14 -9
  18. klaude_code/core/tool/web/web_fetch_tool.md +1 -1
  19. klaude_code/core/tool/web/web_fetch_tool.py +14 -39
  20. klaude_code/core/turn.py +127 -139
  21. klaude_code/llm/anthropic/client.py +176 -82
  22. klaude_code/llm/bedrock/client.py +8 -12
  23. klaude_code/llm/claude/client.py +11 -15
  24. klaude_code/llm/client.py +31 -4
  25. klaude_code/llm/codex/client.py +7 -11
  26. klaude_code/llm/google/client.py +150 -69
  27. klaude_code/llm/openai_compatible/client.py +10 -15
  28. klaude_code/llm/openai_compatible/stream.py +68 -6
  29. klaude_code/llm/openrouter/client.py +9 -15
  30. klaude_code/llm/partial_message.py +35 -0
  31. klaude_code/llm/responses/client.py +134 -68
  32. klaude_code/llm/usage.py +30 -0
  33. klaude_code/protocol/commands.py +0 -4
  34. klaude_code/protocol/events/lifecycle.py +1 -0
  35. klaude_code/protocol/events/metadata.py +1 -0
  36. klaude_code/protocol/events/streaming.py +0 -1
  37. klaude_code/protocol/events/system.py +0 -4
  38. klaude_code/protocol/model.py +2 -15
  39. klaude_code/protocol/sub_agent/explore.py +0 -10
  40. klaude_code/protocol/sub_agent/image_gen.py +0 -7
  41. klaude_code/protocol/sub_agent/task.py +0 -10
  42. klaude_code/protocol/sub_agent/web.py +4 -12
  43. klaude_code/session/templates/export_session.html +4 -4
  44. klaude_code/skill/manager.py +2 -1
  45. klaude_code/tui/components/metadata.py +41 -49
  46. klaude_code/tui/components/rich/markdown.py +1 -3
  47. klaude_code/tui/components/rich/theme.py +2 -2
  48. klaude_code/tui/components/tools.py +0 -31
  49. klaude_code/tui/components/welcome.py +1 -32
  50. klaude_code/tui/input/prompt_toolkit.py +25 -9
  51. klaude_code/tui/machine.py +31 -19
  52. {klaude_code-2.4.2.dist-info → klaude_code-2.5.1.dist-info}/METADATA +1 -1
  53. {klaude_code-2.4.2.dist-info → klaude_code-2.5.1.dist-info}/RECORD +55 -55
  54. klaude_code/core/prompts/prompt-nano-banana.md +0 -1
  55. klaude_code/core/tool/truncation.py +0 -203
  56. {klaude_code-2.4.2.dist-info → klaude_code-2.5.1.dist-info}/WHEEL +0 -0
  57. {klaude_code-2.4.2.dist-info → klaude_code-2.5.1.dist-info}/entry_points.txt +0 -0
@@ -32,51 +32,40 @@ def _render_task_metadata_block(
32
32
  currency_symbol = "¥" if currency == "CNY" else "$"
33
33
 
34
34
  # First column: mark only
35
- mark = Text("└", style=ThemeKey.METADATA_DIM) if is_sub_agent else Text("", style=ThemeKey.METADATA)
35
+ mark = Text("└", style=ThemeKey.METADATA_DIM) if is_sub_agent else Text("", style=ThemeKey.METADATA)
36
36
 
37
- # Second column: model@provider / tokens / cost / …
37
+ # Second column: model@provider description / tokens / cost / …
38
38
  content = Text()
39
39
  content.append_text(Text(metadata.model_name, style=ThemeKey.METADATA_BOLD))
40
40
  if metadata.provider is not None:
41
41
  content.append_text(Text("@", style=ThemeKey.METADATA)).append_text(
42
42
  Text(metadata.provider.lower().replace(" ", "-"), style=ThemeKey.METADATA)
43
43
  )
44
+ if metadata.description:
45
+ content.append_text(Text(" ", style=ThemeKey.METADATA)).append_text(
46
+ Text(metadata.description, style=ThemeKey.METADATA_DIM)
47
+ )
44
48
 
45
49
  # All info parts (tokens, cost, context, etc.)
46
50
  parts: list[Text] = []
47
51
 
48
52
  if metadata.usage is not None:
49
- # Tokens: ↑ 37k cache 5k ↓ 907 think 45k
50
- token_parts: list[Text] = [
51
- Text.assemble(("↑", ThemeKey.METADATA_DIM), (format_number(metadata.usage.input_tokens), ThemeKey.METADATA))
52
- ]
53
+ # Tokens: ↑37k 5k ↓907 ∿45k ⌗ 100
54
+ token_text = Text()
55
+ token_text.append("↑", style=ThemeKey.METADATA_DIM)
56
+ token_text.append(format_number(metadata.usage.input_tokens), style=ThemeKey.METADATA)
53
57
  if metadata.usage.cached_tokens > 0:
54
- token_parts.append(
55
- Text.assemble(
56
- Text("cache ", style=ThemeKey.METADATA_DIM),
57
- Text(format_number(metadata.usage.cached_tokens), style=ThemeKey.METADATA),
58
- )
59
- )
60
- token_parts.append(
61
- Text.assemble(
62
- ("↓", ThemeKey.METADATA_DIM), (format_number(metadata.usage.output_tokens), ThemeKey.METADATA)
63
- )
64
- )
58
+ token_text.append(" ◎", style=ThemeKey.METADATA_DIM)
59
+ token_text.append(format_number(metadata.usage.cached_tokens), style=ThemeKey.METADATA)
60
+ token_text.append("", style=ThemeKey.METADATA_DIM)
61
+ token_text.append(format_number(metadata.usage.output_tokens), style=ThemeKey.METADATA)
65
62
  if metadata.usage.reasoning_tokens > 0:
66
- token_parts.append(
67
- Text.assemble(
68
- ("think ", ThemeKey.METADATA_DIM),
69
- (format_number(metadata.usage.reasoning_tokens), ThemeKey.METADATA),
70
- )
71
- )
63
+ token_text.append(" ∿", style=ThemeKey.METADATA_DIM)
64
+ token_text.append(format_number(metadata.usage.reasoning_tokens), style=ThemeKey.METADATA)
72
65
  if metadata.usage.image_tokens > 0:
73
- token_parts.append(
74
- Text.assemble(
75
- ("image ", ThemeKey.METADATA_DIM),
76
- (format_number(metadata.usage.image_tokens), ThemeKey.METADATA),
77
- )
78
- )
79
- parts.append(Text(" · ").join(token_parts))
66
+ token_text.append(" ⌗ ", style=ThemeKey.METADATA_DIM)
67
+ token_text.append(format_number(metadata.usage.image_tokens), style=ThemeKey.METADATA)
68
+ parts.append(token_text)
80
69
 
81
70
  # Cost
82
71
  if metadata.usage is not None and metadata.usage.total_cost is not None:
@@ -87,41 +76,41 @@ def _render_task_metadata_block(
87
76
  )
88
77
  )
89
78
  if metadata.usage is not None:
90
- # Context usage
79
+ # Context usage: 31k/168k(18.4%)
91
80
  if show_context_and_time and metadata.usage.context_usage_percent is not None:
92
81
  context_size = format_number(metadata.usage.context_size or 0)
93
- # Calculate effective limit (same as Usage.context_usage_percent)
94
82
  effective_limit = (metadata.usage.context_limit or 0) - (metadata.usage.max_tokens or DEFAULT_MAX_TOKENS)
95
83
  effective_limit_str = format_number(effective_limit) if effective_limit > 0 else "?"
96
84
  parts.append(
97
85
  Text.assemble(
98
- ("context ", ThemeKey.METADATA_DIM),
99
86
  (context_size, ThemeKey.METADATA),
100
87
  ("/", ThemeKey.METADATA_DIM),
101
88
  (effective_limit_str, ThemeKey.METADATA),
102
- (f" ({metadata.usage.context_usage_percent:.1f}%)", ThemeKey.METADATA_DIM),
89
+ (f"({metadata.usage.context_usage_percent:.1f}%)", ThemeKey.METADATA_DIM),
103
90
  )
104
91
  )
105
92
 
106
- # TPS
93
+ # TPS: 45.2tps
107
94
  if metadata.usage.throughput_tps is not None:
108
95
  parts.append(
109
96
  Text.assemble(
110
- (f"{metadata.usage.throughput_tps:.1f} ", ThemeKey.METADATA),
111
- ("avg-tps", ThemeKey.METADATA_DIM),
97
+ (f"{metadata.usage.throughput_tps:.1f}", ThemeKey.METADATA),
98
+ ("tps", ThemeKey.METADATA_DIM),
112
99
  )
113
100
  )
114
101
 
115
- # First token latency
102
+ # First token latency: 100ms-ftl / 2.1s-ftl
116
103
  if metadata.usage.first_token_latency_ms is not None:
104
+ ftl_ms = metadata.usage.first_token_latency_ms
105
+ ftl_str = f"{ftl_ms / 1000:.1f}s" if ftl_ms >= 1000 else f"{ftl_ms:.0f}ms"
117
106
  parts.append(
118
107
  Text.assemble(
119
- (f"{metadata.usage.first_token_latency_ms:.0f}", ThemeKey.METADATA),
120
- ("ms avg-ftl", ThemeKey.METADATA_DIM),
108
+ (ftl_str, ThemeKey.METADATA),
109
+ ("-ftl", ThemeKey.METADATA_DIM),
121
110
  )
122
111
  )
123
112
 
124
- # Duration
113
+ # Duration: 12.5s
125
114
  if show_context_and_time and metadata.task_duration_s is not None:
126
115
  parts.append(
127
116
  Text.assemble(
@@ -130,18 +119,19 @@ def _render_task_metadata_block(
130
119
  )
131
120
  )
132
121
 
133
- # Turn count
122
+ # Turn count: 1step / 3steps
134
123
  if show_context_and_time and metadata.turn_count > 0:
124
+ suffix = "step" if metadata.turn_count == 1 else "steps"
135
125
  parts.append(
136
126
  Text.assemble(
137
127
  (str(metadata.turn_count), ThemeKey.METADATA),
138
- (" turns", ThemeKey.METADATA_DIM),
128
+ (suffix, ThemeKey.METADATA_DIM),
139
129
  )
140
130
  )
141
131
 
142
132
  if parts:
143
- content.append_text(Text(" · ", style=ThemeKey.METADATA_DIM))
144
- content.append_text(Text(" · ", style=ThemeKey.METADATA_DIM).join(parts))
133
+ content.append_text(Text(" ", style=ThemeKey.METADATA_DIM))
134
+ content.append_text(Text(" ", style=ThemeKey.METADATA_DIM).join(parts))
145
135
 
146
136
  grid.add_row(mark, content)
147
137
  return grid if not is_sub_agent else Padding(grid, (0, 0, 0, 2))
@@ -151,6 +141,9 @@ def render_task_metadata(e: events.TaskMetadataEvent) -> RenderableType:
151
141
  """Render task metadata including main agent and sub-agents."""
152
142
  renderables: list[RenderableType] = []
153
143
 
144
+ if e.cancelled:
145
+ renderables.append(Text())
146
+
154
147
  renderables.append(
155
148
  _render_task_metadata_block(e.metadata.main_agent, is_sub_agent=False, show_context_and_time=True)
156
149
  )
@@ -176,10 +169,9 @@ def render_task_metadata(e: events.TaskMetadataEvent) -> RenderableType:
176
169
  ("Σ ", ThemeKey.METADATA_DIM),
177
170
  ("total ", ThemeKey.METADATA_DIM),
178
171
  (currency_symbol, ThemeKey.METADATA_DIM),
179
- (f"{total_cost:.4f}", ThemeKey.METADATA),
172
+ (f"{total_cost:.4f}", ThemeKey.METADATA_DIM),
180
173
  )
181
- grid = create_grid()
182
- grid.add_row(Text(" ", style=ThemeKey.METADATA_DIM), total_line)
183
- renderables.append(Padding(grid, (0, 0, 0, 2)))
174
+
175
+ renderables.append(Padding(total_line, (0, 0, 0, 2)))
184
176
 
185
177
  return Group(*renderables)
@@ -61,10 +61,8 @@ class Divider(MarkdownElement):
61
61
 
62
62
  class MarkdownTable(TableElement):
63
63
  def __rich_console__(self, console: Console, options: ConsoleOptions) -> RenderResult:
64
- # rich.box.MARKDOWN intentionally includes a blank top/bottom edge row. Rather than
65
- # post-processing rendered segments, disable outer edges to avoid emitting those rows.
66
64
  table = Table(
67
- box=box.MARKDOWN,
65
+ box=box.MINIMAL,
68
66
  show_edge=False,
69
67
  border_style=console.get_style("markdown.table.border"),
70
68
  )
@@ -54,7 +54,7 @@ LIGHT_PALETTE = Palette(
54
54
  grey3="#c4ced4",
55
55
  grey_green="#96a096",
56
56
  purple="#5f5fb7",
57
- lavender="#5f87af",
57
+ lavender="#7878b0",
58
58
  diff_add="#2e5a32 on #dafbe1",
59
59
  diff_add_char="#2e5a32 on #aceebb",
60
60
  diff_remove="#82071e on #ffecec",
@@ -276,7 +276,7 @@ def get_theme(theme: str | None = None) -> Themes:
276
276
  ThemeKey.TOOL_PARAM.value: palette.green,
277
277
  ThemeKey.TOOL_PARAM_BOLD.value: "bold " + palette.green,
278
278
  ThemeKey.TOOL_RESULT.value: palette.grey_green,
279
- ThemeKey.TOOL_RESULT_TREE_PREFIX.value: palette.grey3 + " dim",
279
+ ThemeKey.TOOL_RESULT_TREE_PREFIX.value: palette.grey3,
280
280
  ThemeKey.TOOL_RESULT_BOLD.value: "bold " + palette.grey_green,
281
281
  ThemeKey.TOOL_RESULT_TRUNCATED.value: palette.grey1 + " dim",
282
282
  ThemeKey.TOOL_MARK.value: "bold",
@@ -498,31 +498,6 @@ def render_mermaid_tool_result(
498
498
  return viewer
499
499
 
500
500
 
501
- def _extract_truncation(
502
- ui_extra: model.ToolResultUIExtra | None,
503
- ) -> model.TruncationUIExtra | None:
504
- return ui_extra if isinstance(ui_extra, model.TruncationUIExtra) else None
505
-
506
-
507
- def render_truncation_info(ui_extra: model.TruncationUIExtra) -> RenderableType:
508
- """Render truncation info for the user."""
509
- truncated_kb = ui_extra.truncated_length / 1024
510
-
511
- text = Text.assemble(
512
- ("Offload context to ", ThemeKey.TOOL_RESULT_TRUNCATED),
513
- (ui_extra.saved_file_path, ThemeKey.TOOL_RESULT_TRUNCATED),
514
- (f", {truncated_kb:.1f}KB truncated", ThemeKey.TOOL_RESULT_TRUNCATED),
515
- )
516
- text.no_wrap = True
517
- text.overflow = "ellipsis"
518
- return text
519
-
520
-
521
- def get_truncation_info(tr: events.ToolResultEvent) -> model.TruncationUIExtra | None:
522
- """Extract truncation info from a tool result event."""
523
- return _extract_truncation(tr.ui_extra)
524
-
525
-
526
501
  def render_report_back_tool_call() -> RenderableType:
527
502
  return _render_tool_call_tree(mark=MARK_DONE, tool_name="Report Back", details=None)
528
503
 
@@ -659,12 +634,6 @@ def render_tool_result(
659
634
  rendered.append(r_diffs.render_structured_diff(item, show_file_name=show_file_name))
660
635
  return wrap(Group(*rendered)) if rendered else None
661
636
 
662
- # Show truncation info if output was truncated and saved to file
663
- truncation_info = get_truncation_info(e)
664
- if truncation_info:
665
- result = render_generic_tool_result(e.result, is_error=e.is_error)
666
- return wrap(Group(render_truncation_info(truncation_info), result))
667
-
668
637
  diff_ui = _extract_diff(e.ui_extra)
669
638
  md_ui = _extract_markdown_doc(e.ui_extra)
670
639
 
@@ -47,12 +47,9 @@ def render_welcome(e: events.WelcomeEvent) -> RenderableType:
47
47
  # Use format_model_params for consistent formatting
48
48
  param_strings = format_model_params(e.llm_config)
49
49
 
50
- # Check if we have sub-agent models to show
51
- has_sub_agents = e.show_sub_agent_models and e.sub_agent_models
52
-
53
50
  # Render config items with tree-style prefixes
54
51
  for i, param_str in enumerate(param_strings):
55
- is_last = i == len(param_strings) - 1 and not has_sub_agents
52
+ is_last = i == len(param_strings) - 1
56
53
  prefix = "└─ " if is_last else "├─ "
57
54
  panel_content.append_text(
58
55
  Text.assemble(
@@ -62,34 +59,6 @@ def render_welcome(e: events.WelcomeEvent) -> RenderableType:
62
59
  )
63
60
  )
64
61
 
65
- # Render sub-agent models
66
- if has_sub_agents:
67
- # Add sub-agents header with tree prefix
68
- panel_content.append_text(
69
- Text.assemble(
70
- ("\n", ThemeKey.WELCOME_INFO),
71
- ("└─ ", ThemeKey.LINES),
72
- ("sub-agents:", ThemeKey.WELCOME_INFO),
73
- )
74
- )
75
- sub_agent_items = list(e.sub_agent_models.items())
76
- max_type_len = max(len(t) for t in e.sub_agent_models)
77
- for i, (sub_agent_type, sub_llm_config) in enumerate(sub_agent_items):
78
- is_last = i == len(sub_agent_items) - 1
79
- prefix = "└─ " if is_last else "├─ "
80
- panel_content.append_text(
81
- Text.assemble(
82
- ("\n", ThemeKey.WELCOME_INFO),
83
- (" ", ThemeKey.WELCOME_INFO), # Indentation for sub-items
84
- (prefix, ThemeKey.LINES),
85
- (sub_agent_type.lower().ljust(max_type_len), ThemeKey.WELCOME_INFO),
86
- (": ", ThemeKey.LINES),
87
- (str(sub_llm_config.model_id), ThemeKey.WELCOME_HIGHLIGHT),
88
- (" @ ", ThemeKey.WELCOME_INFO),
89
- (sub_llm_config.provider_name, ThemeKey.WELCOME_INFO),
90
- )
91
- )
92
-
93
62
  border_style = ThemeKey.WELCOME_DEBUG_BORDER if debug_mode else ThemeKey.LINES
94
63
 
95
64
  if e.show_klaude_code_info:
@@ -394,17 +394,14 @@ class PromptToolkitInput(InputProviderABC):
394
394
  with contextlib.suppress(Exception):
395
395
  _patch_completion_menu_controls(self._session.app.layout.container)
396
396
 
397
- # Reserve more vertical space while the model picker overlay is open.
397
+ # Reserve more vertical space while overlays (selector, completion menu) are open.
398
398
  # prompt_toolkit's default multiline prompt caps out at ~9 lines.
399
- self._patch_prompt_height_for_model_picker()
399
+ self._patch_prompt_height_for_overlays()
400
400
 
401
401
  # Ensure completion menu has default selection
402
402
  self._session.default_buffer.on_completions_changed += self._select_first_completion_on_open # pyright: ignore[reportUnknownMemberType]
403
403
 
404
- def _patch_prompt_height_for_model_picker(self) -> None:
405
- if self._model_picker is None and self._thinking_picker is None:
406
- return
407
-
404
+ def _patch_prompt_height_for_overlays(self) -> None:
408
405
  with contextlib.suppress(Exception):
409
406
  root = self._session.app.layout.container
410
407
  input_window = _find_window_for_buffer(root, self._session.default_buffer)
@@ -417,14 +414,33 @@ class PromptToolkitInput(InputProviderABC):
417
414
  picker_open = (self._model_picker is not None and self._model_picker.is_open) or (
418
415
  self._thinking_picker is not None and self._thinking_picker.is_open
419
416
  )
420
- if picker_open:
421
- # Target 20 rows, but cap to the current terminal size.
417
+
418
+ try:
419
+ complete_state = self._session.default_buffer.complete_state
420
+ completion_open = complete_state is not None and bool(complete_state.completions)
421
+ except Exception:
422
+ completion_open = False
423
+
424
+ try:
425
+ original_height_value = original_height() if callable(original_height) else original_height
426
+ except Exception:
427
+ original_height_value = None
428
+ original_height_int = original_height_value if isinstance(original_height_value, int) else None
429
+
430
+ if picker_open or completion_open:
431
+ target_rows = 20 if picker_open else 14
432
+
433
+ # Cap to the current terminal size.
422
434
  # Leave a small buffer to avoid triggering "Window too small".
423
435
  try:
424
436
  rows = get_app().output.get_size().rows
425
437
  except Exception:
426
438
  rows = 0
427
- return max(3, min(20, rows - 2))
439
+
440
+ expanded = max(3, min(target_rows, rows - 2))
441
+ if original_height_int is not None:
442
+ expanded = max(original_height_int, expanded)
443
+ return expanded
428
444
 
429
445
  if callable(original_height):
430
446
  return original_height()
@@ -8,6 +8,7 @@ from klaude_code.const import (
8
8
  SIGINT_DOUBLE_PRESS_EXIT_TEXT,
9
9
  STATUS_COMPOSING_TEXT,
10
10
  STATUS_DEFAULT_TEXT,
11
+ STATUS_SHOW_BUFFER_LENGTH,
11
12
  STATUS_THINKING_TEXT,
12
13
  )
13
14
  from klaude_code.protocol import events, model, tools
@@ -65,17 +66,6 @@ FAST_TOOLS: frozenset[str] = frozenset(
65
66
  )
66
67
 
67
68
 
68
- def _should_skip_tool_activity(tool_name: str, model_id: str | None) -> bool:
69
- """Check if tool activity should be skipped for non-streaming models."""
70
- if model_id is None:
71
- return False
72
- if tool_name not in FAST_TOOLS:
73
- return False
74
- # Gemini and Grok models don't stream tool JSON at fine granularity
75
- model_lower = model_id.lower()
76
- return "gemini" in model_lower or "grok" in model_lower
77
-
78
-
79
69
  @dataclass
80
70
  class SubAgentThinkingHeaderState:
81
71
  buffer: str = ""
@@ -180,7 +170,7 @@ class ActivityState:
180
170
  if self._composing:
181
171
  text = Text()
182
172
  text.append(STATUS_COMPOSING_TEXT, style=ThemeKey.STATUS_TEXT)
183
- if self._buffer_length > 0:
173
+ if STATUS_SHOW_BUFFER_LENGTH and self._buffer_length > 0:
184
174
  text.append(f" ({self._buffer_length:,})", style=ThemeKey.STATUS_TEXT)
185
175
  return text
186
176
 
@@ -256,7 +246,7 @@ class SpinnerStatusState:
256
246
  base_status = self._reasoning_status or self._todo_status
257
247
 
258
248
  if base_status:
259
- # Default "Reasoning ..." uses normal style; custom headers use bold italic
249
+ # Default "Thinking ..." uses normal style; custom headers use bold italic
260
250
  is_default_reasoning = base_status == STATUS_THINKING_TEXT
261
251
  status_style = ThemeKey.STATUS_TEXT if is_default_reasoning else ThemeKey.STATUS_TEXT_BOLD_ITALIC
262
252
  if activity_text:
@@ -299,6 +289,7 @@ class _SessionState:
299
289
  session_id: str
300
290
  sub_agent_state: model.SubAgentState | None = None
301
291
  sub_agent_thinking_header: SubAgentThinkingHeaderState | None = None
292
+ model_id: str | None = None
302
293
  assistant_stream_active: bool = False
303
294
  thinking_stream_active: bool = False
304
295
  assistant_char_count: int = 0
@@ -312,6 +303,23 @@ class _SessionState:
312
303
  def should_show_sub_agent_thinking_header(self) -> bool:
313
304
  return bool(self.sub_agent_state and self.sub_agent_state.sub_agent_type == "ImageGen")
314
305
 
306
+ @property
307
+ def should_extract_reasoning_header(self) -> bool:
308
+ """Gemini and GPT-5 models use markdown bold headers in thinking."""
309
+ if self.model_id is None:
310
+ return False
311
+ model_lower = self.model_id.lower()
312
+ return "gemini" in model_lower or "gpt-5" in model_lower
313
+
314
+ def should_skip_tool_activity(self, tool_name: str) -> bool:
315
+ """Check if tool activity should be skipped for non-streaming models."""
316
+ if self.model_id is None:
317
+ return False
318
+ if tool_name not in FAST_TOOLS:
319
+ return False
320
+ model_lower = self.model_id.lower()
321
+ return "gemini" in model_lower or "grok" in model_lower
322
+
315
323
 
316
324
  class DisplayStateMachine:
317
325
  """Simplified, session-aware REPL UI state machine.
@@ -379,6 +387,7 @@ class DisplayStateMachine:
379
387
 
380
388
  case events.TaskStartEvent() as e:
381
389
  s.sub_agent_state = e.sub_agent_state
390
+ s.model_id = e.model_id
382
391
  if not s.is_sub_agent:
383
392
  self._set_primary_if_needed(e.session_id)
384
393
  cmds.append(TaskClockStart())
@@ -411,6 +420,7 @@ class DisplayStateMachine:
411
420
  if not self._is_primary(e.session_id):
412
421
  return []
413
422
  s.thinking_stream_active = True
423
+ s.thinking_tail = ""
414
424
  # Ensure the status reflects that reasoning has started even
415
425
  # before we receive any deltas (or a bold header).
416
426
  self._spinner.set_reasoning_status(STATUS_THINKING_TEXT)
@@ -434,11 +444,13 @@ class DisplayStateMachine:
434
444
  cmds.append(AppendThinking(session_id=e.session_id, content=e.content))
435
445
 
436
446
  # Update reasoning status for spinner (based on bounded tail).
437
- s.thinking_tail = (s.thinking_tail + e.content)[-8192:]
438
- header = extract_last_bold_header(normalize_thinking_content(s.thinking_tail))
439
- if header:
440
- self._spinner.set_reasoning_status(header)
441
- cmds.extend(self._spinner_update_commands())
447
+ # Only extract headers for models that use markdown bold headers in thinking.
448
+ if s.should_extract_reasoning_header:
449
+ s.thinking_tail = (s.thinking_tail + e.content)[-8192:]
450
+ header = extract_last_bold_header(normalize_thinking_content(s.thinking_tail))
451
+ if header:
452
+ self._spinner.set_reasoning_status(header)
453
+ cmds.extend(self._spinner_update_commands())
442
454
 
443
455
  return cmds
444
456
 
@@ -527,7 +539,7 @@ class DisplayStateMachine:
527
539
 
528
540
  # Skip activity state for fast tools on non-streaming models (e.g., Gemini)
529
541
  # to avoid flash-and-disappear effect
530
- if not _should_skip_tool_activity(e.tool_name, e.model_id):
542
+ if not s.should_skip_tool_activity(e.tool_name):
531
543
  tool_active_form = get_tool_active_form(e.tool_name)
532
544
  if is_sub_agent_tool(e.tool_name):
533
545
  self._spinner.add_sub_agent_tool_call(e.tool_call_id, tool_active_form)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: klaude-code
3
- Version: 2.4.2
3
+ Version: 2.5.1
4
4
  Summary: Minimal code agent CLI
5
5
  Requires-Dist: anthropic>=0.66.0
6
6
  Requires-Dist: chardet>=5.2.0