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.
- klaude_code/app/runtime.py +2 -6
- klaude_code/cli/main.py +0 -1
- klaude_code/config/assets/builtin_config.yaml +7 -0
- klaude_code/const.py +7 -4
- klaude_code/core/agent.py +10 -1
- klaude_code/core/agent_profile.py +47 -35
- klaude_code/core/executor.py +6 -21
- klaude_code/core/manager/sub_agent_manager.py +17 -1
- klaude_code/core/prompts/prompt-sub-agent-web.md +4 -4
- klaude_code/core/task.py +66 -4
- klaude_code/core/tool/__init__.py +0 -5
- klaude_code/core/tool/context.py +12 -1
- klaude_code/core/tool/offload.py +311 -0
- klaude_code/core/tool/shell/bash_tool.md +1 -43
- klaude_code/core/tool/sub_agent_tool.py +1 -0
- klaude_code/core/tool/todo/todo_write_tool.md +0 -23
- klaude_code/core/tool/tool_runner.py +14 -9
- klaude_code/core/tool/web/web_fetch_tool.md +1 -1
- klaude_code/core/tool/web/web_fetch_tool.py +14 -39
- klaude_code/core/turn.py +127 -139
- klaude_code/llm/anthropic/client.py +176 -82
- klaude_code/llm/bedrock/client.py +8 -12
- klaude_code/llm/claude/client.py +11 -15
- klaude_code/llm/client.py +31 -4
- klaude_code/llm/codex/client.py +7 -11
- klaude_code/llm/google/client.py +150 -69
- klaude_code/llm/openai_compatible/client.py +10 -15
- klaude_code/llm/openai_compatible/stream.py +68 -6
- klaude_code/llm/openrouter/client.py +9 -15
- klaude_code/llm/partial_message.py +35 -0
- klaude_code/llm/responses/client.py +134 -68
- klaude_code/llm/usage.py +30 -0
- klaude_code/protocol/commands.py +0 -4
- klaude_code/protocol/events/lifecycle.py +1 -0
- klaude_code/protocol/events/metadata.py +1 -0
- klaude_code/protocol/events/streaming.py +0 -1
- klaude_code/protocol/events/system.py +0 -4
- klaude_code/protocol/model.py +2 -15
- klaude_code/protocol/sub_agent/explore.py +0 -10
- klaude_code/protocol/sub_agent/image_gen.py +0 -7
- klaude_code/protocol/sub_agent/task.py +0 -10
- klaude_code/protocol/sub_agent/web.py +4 -12
- klaude_code/session/templates/export_session.html +4 -4
- klaude_code/skill/manager.py +2 -1
- klaude_code/tui/components/metadata.py +41 -49
- klaude_code/tui/components/rich/markdown.py +1 -3
- klaude_code/tui/components/rich/theme.py +2 -2
- klaude_code/tui/components/tools.py +0 -31
- klaude_code/tui/components/welcome.py +1 -32
- klaude_code/tui/input/prompt_toolkit.py +25 -9
- klaude_code/tui/machine.py +31 -19
- {klaude_code-2.4.2.dist-info → klaude_code-2.5.1.dist-info}/METADATA +1 -1
- {klaude_code-2.4.2.dist-info → klaude_code-2.5.1.dist-info}/RECORD +55 -55
- klaude_code/core/prompts/prompt-nano-banana.md +0 -1
- klaude_code/core/tool/truncation.py +0 -203
- {klaude_code-2.4.2.dist-info → klaude_code-2.5.1.dist-info}/WHEEL +0 -0
- {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("
|
|
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: ↑
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
67
|
-
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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"
|
|
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}
|
|
111
|
-
("
|
|
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
|
-
(
|
|
120
|
-
("
|
|
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
|
-
(
|
|
128
|
+
(suffix, ThemeKey.METADATA_DIM),
|
|
139
129
|
)
|
|
140
130
|
)
|
|
141
131
|
|
|
142
132
|
if parts:
|
|
143
|
-
content.append_text(Text("
|
|
144
|
-
content.append_text(Text("
|
|
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.
|
|
172
|
+
(f"{total_cost:.4f}", ThemeKey.METADATA_DIM),
|
|
180
173
|
)
|
|
181
|
-
|
|
182
|
-
|
|
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.
|
|
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="#
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
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
|
-
|
|
421
|
-
|
|
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
|
-
|
|
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()
|
klaude_code/tui/machine.py
CHANGED
|
@@ -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 "
|
|
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
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
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
|
|
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)
|