klaude-code 2.4.1__py3-none-any.whl → 2.5.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 (58) 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 +65 -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 +128 -138
  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/metadata.py +1 -0
  35. klaude_code/protocol/events/streaming.py +1 -0
  36. klaude_code/protocol/events/system.py +0 -4
  37. klaude_code/protocol/model.py +2 -15
  38. klaude_code/protocol/sub_agent/explore.py +0 -10
  39. klaude_code/protocol/sub_agent/image_gen.py +0 -7
  40. klaude_code/protocol/sub_agent/task.py +0 -10
  41. klaude_code/protocol/sub_agent/web.py +4 -12
  42. klaude_code/session/templates/export_session.html +4 -4
  43. klaude_code/skill/manager.py +2 -1
  44. klaude_code/tui/components/metadata.py +41 -49
  45. klaude_code/tui/components/rich/markdown.py +1 -3
  46. klaude_code/tui/components/rich/theme.py +2 -2
  47. klaude_code/tui/components/sub_agent.py +9 -1
  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 +40 -8
  52. klaude_code/tui/renderer.py +1 -0
  53. {klaude_code-2.4.1.dist-info → klaude_code-2.5.0.dist-info}/METADATA +2 -2
  54. {klaude_code-2.4.1.dist-info → klaude_code-2.5.0.dist-info}/RECORD +56 -56
  55. klaude_code/core/prompts/prompt-nano-banana.md +0 -1
  56. klaude_code/core/tool/truncation.py +0 -203
  57. {klaude_code-2.4.1.dist-info → klaude_code-2.5.0.dist-info}/WHEEL +0 -0
  58. {klaude_code-2.4.1.dist-info → klaude_code-2.5.0.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",
@@ -81,6 +81,7 @@ def render_sub_agent_result(
81
81
  *,
82
82
  has_structured_output: bool = False,
83
83
  description: str | None = None,
84
+ sub_agent_color: Style | None = None,
84
85
  ) -> RenderableType:
85
86
  stripped_result = result.strip()
86
87
  main_content, agent_id_footer = _extract_agent_id_footer(stripped_result)
@@ -88,7 +89,14 @@ def render_sub_agent_result(
88
89
 
89
90
  elements: list[RenderableType] = []
90
91
  if description:
91
- elements.append(Text(f"---\n{description}", style=ThemeKey.TOOL_RESULT))
92
+ elements.append(
93
+ Text(
94
+ f"---\n{description}",
95
+ style=Style(bold=True, color=sub_agent_color.color, frame=True)
96
+ if sub_agent_color
97
+ else ThemeKey.TOOL_RESULT_BOLD,
98
+ )
99
+ )
92
100
 
93
101
  # Try structured JSON output first
94
102
  use_text_rendering = True
@@ -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,9 +8,10 @@ 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
- from klaude_code.protocol import events, model
14
+ from klaude_code.protocol import events, model, tools
14
15
  from klaude_code.tui.commands import (
15
16
  AppendAssistant,
16
17
  AppendThinking,
@@ -48,6 +49,33 @@ from klaude_code.tui.components.rich.theme import ThemeKey
48
49
  from klaude_code.tui.components.thinking import extract_last_bold_header, normalize_thinking_content
49
50
  from klaude_code.tui.components.tools import get_tool_active_form, is_sub_agent_tool
50
51
 
52
+ # Tools that complete quickly and don't benefit from streaming activity display.
53
+ # For models without fine-grained tool JSON streaming (e.g., Gemini), showing these
54
+ # in the activity state causes a flash-and-disappear effect.
55
+ FAST_TOOLS: frozenset[str] = frozenset(
56
+ {
57
+ tools.READ,
58
+ tools.EDIT,
59
+ tools.WRITE,
60
+ tools.BASH,
61
+ tools.TODO_WRITE,
62
+ tools.UPDATE_PLAN,
63
+ tools.APPLY_PATCH,
64
+ tools.REPORT_BACK,
65
+ }
66
+ )
67
+
68
+
69
+ def _should_skip_tool_activity(tool_name: str, model_id: str | None) -> bool:
70
+ """Check if tool activity should be skipped for non-streaming models."""
71
+ if model_id is None:
72
+ return False
73
+ if tool_name not in FAST_TOOLS:
74
+ return False
75
+ # Gemini and Grok models don't stream tool JSON at fine granularity
76
+ model_lower = model_id.lower()
77
+ return "gemini" in model_lower or "grok" in model_lower
78
+
51
79
 
52
80
  @dataclass
53
81
  class SubAgentThinkingHeaderState:
@@ -142,7 +170,8 @@ class ActivityState:
142
170
 
143
171
  if self._sub_agent_tool_calls:
144
172
  _append_counts(self._sub_agent_tool_calls)
145
- activity_text.append(" | ")
173
+ if self._tool_calls:
174
+ activity_text.append(" , ")
146
175
 
147
176
  if self._tool_calls:
148
177
  _append_counts(self._tool_calls)
@@ -152,7 +181,7 @@ class ActivityState:
152
181
  if self._composing:
153
182
  text = Text()
154
183
  text.append(STATUS_COMPOSING_TEXT, style=ThemeKey.STATUS_TEXT)
155
- if self._buffer_length > 0:
184
+ if STATUS_SHOW_BUFFER_LENGTH and self._buffer_length > 0:
156
185
  text.append(f" ({self._buffer_length:,})", style=ThemeKey.STATUS_TEXT)
157
186
  return text
158
187
 
@@ -497,11 +526,14 @@ class DisplayStateMachine:
497
526
 
498
527
  self._spinner.set_composing(False)
499
528
 
500
- tool_active_form = get_tool_active_form(e.tool_name)
501
- if is_sub_agent_tool(e.tool_name):
502
- self._spinner.add_sub_agent_tool_call(e.tool_call_id, tool_active_form)
503
- else:
504
- self._spinner.add_tool_call(tool_active_form)
529
+ # Skip activity state for fast tools on non-streaming models (e.g., Gemini)
530
+ # to avoid flash-and-disappear effect
531
+ if not _should_skip_tool_activity(e.tool_name, e.model_id):
532
+ tool_active_form = get_tool_active_form(e.tool_name)
533
+ if is_sub_agent_tool(e.tool_name):
534
+ self._spinner.add_sub_agent_tool_call(e.tool_call_id, tool_active_form)
535
+ else:
536
+ self._spinner.add_tool_call(tool_active_form)
505
537
 
506
538
  cmds.extend(self._spinner_update_commands())
507
539
  return cmds
@@ -569,6 +569,7 @@ class TUICommandRenderer:
569
569
  event.task_result,
570
570
  has_structured_output=event.has_structured_output,
571
571
  description=description,
572
+ sub_agent_color=self._current_sub_agent_color,
572
573
  )
573
574
  )
574
575
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: klaude-code
3
- Version: 2.4.1
3
+ Version: 2.5.0
4
4
  Summary: Minimal code agent CLI
5
5
  Requires-Dist: anthropic>=0.66.0
6
6
  Requires-Dist: chardet>=5.2.0
@@ -17,7 +17,7 @@ Requires-Dist: rich>=14.1.0
17
17
  Requires-Dist: term-image>=0.7.2
18
18
  Requires-Dist: trafilatura>=2.0.0
19
19
  Requires-Dist: typer>=0.17.3
20
- Requires-Python: >=3.13
20
+ Requires-Python: >=3.13, <3.14
21
21
  Description-Content-Type: text/markdown
22
22
 
23
23
  # Klaude Code