klaude-code 2.1.1__py3-none-any.whl → 2.3.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.
- klaude_code/app/__init__.py +1 -2
- klaude_code/app/runtime.py +13 -41
- klaude_code/cli/list_model.py +27 -10
- klaude_code/cli/main.py +42 -159
- klaude_code/config/assets/builtin_config.yaml +36 -14
- klaude_code/config/config.py +144 -7
- klaude_code/config/select_model.py +38 -13
- klaude_code/config/sub_agent_model_helper.py +217 -0
- klaude_code/const.py +2 -2
- klaude_code/core/agent_profile.py +71 -5
- klaude_code/core/executor.py +75 -0
- klaude_code/core/manager/llm_clients_builder.py +18 -12
- klaude_code/core/prompts/prompt-nano-banana.md +1 -0
- klaude_code/core/tool/shell/command_safety.py +4 -189
- klaude_code/core/tool/sub_agent_tool.py +2 -1
- klaude_code/core/turn.py +1 -1
- klaude_code/llm/anthropic/client.py +8 -5
- klaude_code/llm/anthropic/input.py +54 -29
- klaude_code/llm/google/client.py +2 -2
- klaude_code/llm/google/input.py +23 -2
- klaude_code/llm/openai_compatible/input.py +22 -13
- klaude_code/llm/openai_compatible/stream.py +1 -1
- klaude_code/llm/openrouter/input.py +37 -25
- klaude_code/llm/responses/client.py +1 -1
- klaude_code/llm/responses/input.py +96 -57
- klaude_code/protocol/commands.py +1 -2
- klaude_code/protocol/events/system.py +4 -0
- klaude_code/protocol/message.py +2 -2
- klaude_code/protocol/op.py +17 -0
- klaude_code/protocol/op_handler.py +5 -0
- klaude_code/protocol/sub_agent/AGENTS.md +28 -0
- klaude_code/protocol/sub_agent/__init__.py +10 -14
- klaude_code/protocol/sub_agent/image_gen.py +2 -1
- klaude_code/session/codec.py +2 -6
- klaude_code/session/session.py +9 -1
- klaude_code/skill/assets/create-plan/SKILL.md +3 -5
- klaude_code/tui/command/__init__.py +7 -10
- klaude_code/tui/command/clear_cmd.py +1 -1
- klaude_code/tui/command/command_abc.py +1 -2
- klaude_code/tui/command/copy_cmd.py +1 -2
- klaude_code/tui/command/fork_session_cmd.py +4 -4
- klaude_code/tui/command/model_cmd.py +6 -43
- klaude_code/tui/command/model_select.py +75 -15
- klaude_code/tui/command/refresh_cmd.py +1 -2
- klaude_code/tui/command/resume_cmd.py +3 -4
- klaude_code/tui/command/status_cmd.py +1 -1
- klaude_code/tui/command/sub_agent_model_cmd.py +190 -0
- klaude_code/tui/components/bash_syntax.py +1 -1
- klaude_code/tui/components/common.py +1 -1
- klaude_code/tui/components/developer.py +10 -15
- klaude_code/tui/components/metadata.py +2 -64
- klaude_code/tui/components/rich/cjk_wrap.py +3 -2
- klaude_code/tui/components/rich/status.py +49 -3
- klaude_code/tui/components/rich/theme.py +4 -2
- klaude_code/tui/components/sub_agent.py +25 -46
- klaude_code/tui/components/user_input.py +9 -21
- klaude_code/tui/components/welcome.py +99 -0
- klaude_code/tui/input/prompt_toolkit.py +14 -1
- klaude_code/tui/renderer.py +2 -3
- klaude_code/tui/runner.py +2 -2
- klaude_code/tui/terminal/selector.py +8 -18
- klaude_code/ui/__init__.py +0 -24
- klaude_code/ui/common.py +3 -2
- klaude_code/ui/core/display.py +2 -2
- {klaude_code-2.1.1.dist-info → klaude_code-2.3.0.dist-info}/METADATA +16 -81
- {klaude_code-2.1.1.dist-info → klaude_code-2.3.0.dist-info}/RECORD +68 -67
- klaude_code/tui/command/help_cmd.py +0 -51
- klaude_code/tui/command/prompt-commit.md +0 -82
- klaude_code/tui/command/release_notes_cmd.py +0 -85
- klaude_code/ui/exec_mode.py +0 -60
- {klaude_code-2.1.1.dist-info → klaude_code-2.3.0.dist-info}/WHEEL +0 -0
- {klaude_code-2.1.1.dist-info → klaude_code-2.3.0.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
"""Command for changing sub-agent models."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
|
|
7
|
+
from prompt_toolkit.styles import Style
|
|
8
|
+
|
|
9
|
+
from klaude_code.config.config import load_config
|
|
10
|
+
from klaude_code.config.sub_agent_model_helper import SubAgentModelHelper, SubAgentModelInfo
|
|
11
|
+
from klaude_code.protocol import commands, events, message, model, op
|
|
12
|
+
from klaude_code.tui.terminal.selector import SelectItem, build_model_select_items, select_one
|
|
13
|
+
|
|
14
|
+
from .command_abc import Agent, CommandABC, CommandResult
|
|
15
|
+
|
|
16
|
+
SELECT_STYLE = Style(
|
|
17
|
+
[
|
|
18
|
+
("instruction", "ansibrightblack"),
|
|
19
|
+
("pointer", "ansigreen"),
|
|
20
|
+
("highlighted", "ansigreen"),
|
|
21
|
+
("text", "ansibrightblack"),
|
|
22
|
+
("question", "bold"),
|
|
23
|
+
("meta", "fg:ansibrightblack"),
|
|
24
|
+
("msg", ""),
|
|
25
|
+
]
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
USE_DEFAULT_BEHAVIOR = "__default__"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _build_sub_agent_select_items(
|
|
32
|
+
sub_agents: list[SubAgentModelInfo],
|
|
33
|
+
helper: SubAgentModelHelper,
|
|
34
|
+
main_model_name: str,
|
|
35
|
+
) -> list[SelectItem[str]]:
|
|
36
|
+
"""Build SelectItem list for sub-agent selection."""
|
|
37
|
+
items: list[SelectItem[str]] = []
|
|
38
|
+
max_name_len = max(len(sa.profile.name) for sa in sub_agents) if sub_agents else 0
|
|
39
|
+
|
|
40
|
+
for sa in sub_agents:
|
|
41
|
+
name = sa.profile.name
|
|
42
|
+
|
|
43
|
+
if sa.configured_model:
|
|
44
|
+
model_display = sa.configured_model
|
|
45
|
+
else:
|
|
46
|
+
behavior = helper.describe_empty_model_config_behavior(name, main_model_name=main_model_name)
|
|
47
|
+
model_display = f"({behavior.description})"
|
|
48
|
+
|
|
49
|
+
title = [
|
|
50
|
+
("class:msg", f"{name:<{max_name_len}}"),
|
|
51
|
+
("class:meta", f" current: {model_display}\n"),
|
|
52
|
+
]
|
|
53
|
+
items.append(SelectItem(title=title, value=name, search_text=name))
|
|
54
|
+
|
|
55
|
+
return items
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _select_sub_agent_sync(
|
|
59
|
+
sub_agents: list[SubAgentModelInfo],
|
|
60
|
+
helper: SubAgentModelHelper,
|
|
61
|
+
main_model_name: str,
|
|
62
|
+
) -> str | None:
|
|
63
|
+
"""Synchronous sub-agent type selection."""
|
|
64
|
+
items = _build_sub_agent_select_items(sub_agents, helper, main_model_name)
|
|
65
|
+
if not items:
|
|
66
|
+
return None
|
|
67
|
+
|
|
68
|
+
try:
|
|
69
|
+
result = select_one(
|
|
70
|
+
message="Select sub-agent to configure:",
|
|
71
|
+
items=items,
|
|
72
|
+
pointer="->",
|
|
73
|
+
style=SELECT_STYLE,
|
|
74
|
+
use_search_filter=False,
|
|
75
|
+
)
|
|
76
|
+
return result if isinstance(result, str) else None
|
|
77
|
+
except KeyboardInterrupt:
|
|
78
|
+
return None
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _select_model_for_sub_agent_sync(
|
|
82
|
+
helper: SubAgentModelHelper,
|
|
83
|
+
sub_agent_type: str,
|
|
84
|
+
main_model_name: str,
|
|
85
|
+
) -> str | None:
|
|
86
|
+
"""Synchronous model selection for a sub-agent."""
|
|
87
|
+
models = helper.get_selectable_models(sub_agent_type)
|
|
88
|
+
|
|
89
|
+
default_behavior = helper.describe_empty_model_config_behavior(sub_agent_type, main_model_name=main_model_name)
|
|
90
|
+
|
|
91
|
+
inherit_item = SelectItem[str](
|
|
92
|
+
title=[
|
|
93
|
+
("class:msg", "(Use default behavior)"),
|
|
94
|
+
("class:meta", f" -> {default_behavior.description}\n"),
|
|
95
|
+
],
|
|
96
|
+
value=USE_DEFAULT_BEHAVIOR,
|
|
97
|
+
search_text="default unset",
|
|
98
|
+
)
|
|
99
|
+
model_items = build_model_select_items(models)
|
|
100
|
+
all_items = [inherit_item, *model_items]
|
|
101
|
+
|
|
102
|
+
try:
|
|
103
|
+
result = select_one(
|
|
104
|
+
message=f"Select model for {sub_agent_type}:",
|
|
105
|
+
items=all_items,
|
|
106
|
+
pointer="->",
|
|
107
|
+
style=SELECT_STYLE,
|
|
108
|
+
use_search_filter=True,
|
|
109
|
+
)
|
|
110
|
+
return result if isinstance(result, str) else None
|
|
111
|
+
except KeyboardInterrupt:
|
|
112
|
+
return None
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class SubAgentModelCommand(CommandABC):
|
|
116
|
+
"""Configure models for sub-agents (Task, Explore, WebAgent, ImageGen)."""
|
|
117
|
+
|
|
118
|
+
@property
|
|
119
|
+
def name(self) -> commands.CommandName:
|
|
120
|
+
return commands.CommandName.SUB_AGENT_MODEL
|
|
121
|
+
|
|
122
|
+
@property
|
|
123
|
+
def summary(self) -> str:
|
|
124
|
+
return "Change sub-agent models"
|
|
125
|
+
|
|
126
|
+
@property
|
|
127
|
+
def is_interactive(self) -> bool:
|
|
128
|
+
return True
|
|
129
|
+
|
|
130
|
+
async def run(self, agent: Agent, user_input: message.UserInputPayload) -> CommandResult:
|
|
131
|
+
config = load_config()
|
|
132
|
+
helper = SubAgentModelHelper(config)
|
|
133
|
+
main_model_name = agent.get_llm_client().model_name
|
|
134
|
+
|
|
135
|
+
sub_agents = helper.get_available_sub_agents()
|
|
136
|
+
if not sub_agents:
|
|
137
|
+
return CommandResult(
|
|
138
|
+
events=[
|
|
139
|
+
events.DeveloperMessageEvent(
|
|
140
|
+
session_id=agent.session.id,
|
|
141
|
+
item=message.DeveloperMessage(
|
|
142
|
+
parts=message.text_parts_from_str("No sub-agents available"),
|
|
143
|
+
ui_extra=model.build_command_output_extra(self.name),
|
|
144
|
+
),
|
|
145
|
+
)
|
|
146
|
+
]
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
selected_sub_agent = await asyncio.to_thread(_select_sub_agent_sync, sub_agents, helper, main_model_name)
|
|
150
|
+
if selected_sub_agent is None:
|
|
151
|
+
return CommandResult(
|
|
152
|
+
events=[
|
|
153
|
+
events.DeveloperMessageEvent(
|
|
154
|
+
session_id=agent.session.id,
|
|
155
|
+
item=message.DeveloperMessage(
|
|
156
|
+
parts=message.text_parts_from_str("(cancelled)"),
|
|
157
|
+
ui_extra=model.build_command_output_extra(self.name),
|
|
158
|
+
),
|
|
159
|
+
)
|
|
160
|
+
]
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
selected_model = await asyncio.to_thread(
|
|
164
|
+
_select_model_for_sub_agent_sync, helper, selected_sub_agent, main_model_name
|
|
165
|
+
)
|
|
166
|
+
if selected_model is None:
|
|
167
|
+
return CommandResult(
|
|
168
|
+
events=[
|
|
169
|
+
events.DeveloperMessageEvent(
|
|
170
|
+
session_id=agent.session.id,
|
|
171
|
+
item=message.DeveloperMessage(
|
|
172
|
+
parts=message.text_parts_from_str("(cancelled)"),
|
|
173
|
+
ui_extra=model.build_command_output_extra(self.name),
|
|
174
|
+
),
|
|
175
|
+
)
|
|
176
|
+
]
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
model_name: str | None = None if selected_model == USE_DEFAULT_BEHAVIOR else selected_model
|
|
180
|
+
|
|
181
|
+
return CommandResult(
|
|
182
|
+
operations=[
|
|
183
|
+
op.ChangeSubAgentModelOperation(
|
|
184
|
+
session_id=agent.session.id,
|
|
185
|
+
sub_agent_type=selected_sub_agent,
|
|
186
|
+
model_name=model_name,
|
|
187
|
+
save_as_default=True,
|
|
188
|
+
)
|
|
189
|
+
]
|
|
190
|
+
)
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import re
|
|
4
4
|
from typing import Any
|
|
5
5
|
|
|
6
|
-
from pygments.lexers import BashLexer # pyright: ignore[reportUnknownVariableType]
|
|
6
|
+
from pygments.lexers.shell import BashLexer # pyright: ignore[reportMissingTypeStubs, reportUnknownVariableType]
|
|
7
7
|
from pygments.token import Token
|
|
8
8
|
from rich.text import Text
|
|
9
9
|
|
|
@@ -5,7 +5,6 @@ from rich.text import Text
|
|
|
5
5
|
|
|
6
6
|
from klaude_code.protocol import commands, events, message, model
|
|
7
7
|
from klaude_code.tui.components.common import create_grid, truncate_middle
|
|
8
|
-
from klaude_code.tui.components.rich.markdown import NoInsetMarkdown
|
|
9
8
|
from klaude_code.tui.components.rich.theme import ThemeKey
|
|
10
9
|
from klaude_code.tui.components.tools import render_path
|
|
11
10
|
|
|
@@ -142,12 +141,8 @@ def render_command_output(e: events.DeveloperMessageEvent) -> RenderableType:
|
|
|
142
141
|
|
|
143
142
|
content = message.join_text_parts(e.item.parts)
|
|
144
143
|
match command_output.command_name:
|
|
145
|
-
case commands.CommandName.HELP:
|
|
146
|
-
return Padding.indent(Text.from_markup(content or ""), level=2)
|
|
147
144
|
case commands.CommandName.STATUS:
|
|
148
145
|
return _render_status_output(command_output)
|
|
149
|
-
case commands.CommandName.RELEASE_NOTES:
|
|
150
|
-
return Padding.indent(NoInsetMarkdown(content or ""), level=2)
|
|
151
146
|
case commands.CommandName.FORK_SESSION:
|
|
152
147
|
return _render_fork_session_output(command_output)
|
|
153
148
|
case _:
|
|
@@ -178,14 +173,14 @@ def _format_cost(cost: float | None, currency: str = "USD") -> str:
|
|
|
178
173
|
def _render_fork_session_output(command_output: model.CommandOutput) -> RenderableType:
|
|
179
174
|
"""Render fork session output with usage instructions."""
|
|
180
175
|
if not isinstance(command_output.ui_extra, model.SessionIdUIExtra):
|
|
181
|
-
return Padding.indent(Text("(no session id)", style=ThemeKey.
|
|
176
|
+
return Padding.indent(Text("(no session id)", style=ThemeKey.TOOL_RESULT), level=2)
|
|
182
177
|
|
|
183
178
|
grid = Table.grid(padding=(0, 1))
|
|
184
179
|
session_id = command_output.ui_extra.session_id
|
|
185
|
-
grid.add_column(style=ThemeKey.
|
|
180
|
+
grid.add_column(style=ThemeKey.TOOL_RESULT, overflow="fold")
|
|
186
181
|
|
|
187
|
-
grid.add_row(Text("Session forked. Resume command copied to clipboard:", style=ThemeKey.
|
|
188
|
-
grid.add_row(Text(f" klaude --resume-by-id {session_id}", style=ThemeKey.
|
|
182
|
+
grid.add_row(Text("Session forked. Resume command copied to clipboard:", style=ThemeKey.TOOL_RESULT))
|
|
183
|
+
grid.add_row(Text(f" klaude --resume-by-id {session_id}", style=ThemeKey.TOOL_RESULT_BOLD))
|
|
189
184
|
|
|
190
185
|
return Padding.indent(grid, level=2)
|
|
191
186
|
|
|
@@ -193,24 +188,24 @@ def _render_fork_session_output(command_output: model.CommandOutput) -> Renderab
|
|
|
193
188
|
def _render_status_output(command_output: model.CommandOutput) -> RenderableType:
|
|
194
189
|
"""Render session status with total cost and per-model breakdown."""
|
|
195
190
|
if not isinstance(command_output.ui_extra, model.SessionStatusUIExtra):
|
|
196
|
-
return Text("(no status data)", style=ThemeKey.
|
|
191
|
+
return Text("(no status data)", style=ThemeKey.TOOL_RESULT)
|
|
197
192
|
|
|
198
193
|
status = command_output.ui_extra
|
|
199
194
|
usage = status.usage
|
|
200
195
|
|
|
201
196
|
table = Table.grid(padding=(0, 2))
|
|
202
|
-
table.add_column(style=ThemeKey.
|
|
203
|
-
table.add_column(style=ThemeKey.
|
|
197
|
+
table.add_column(style=ThemeKey.TOOL_RESULT, overflow="fold")
|
|
198
|
+
table.add_column(style=ThemeKey.TOOL_RESULT, overflow="fold")
|
|
204
199
|
|
|
205
200
|
# Total cost line
|
|
206
201
|
table.add_row(
|
|
207
|
-
Text("Total cost:", style=ThemeKey.
|
|
208
|
-
Text(_format_cost(usage.total_cost, usage.currency), style=ThemeKey.
|
|
202
|
+
Text("Total cost:", style=ThemeKey.TOOL_RESULT_BOLD),
|
|
203
|
+
Text(_format_cost(usage.total_cost, usage.currency), style=ThemeKey.TOOL_RESULT_BOLD),
|
|
209
204
|
)
|
|
210
205
|
|
|
211
206
|
# Per-model breakdown
|
|
212
207
|
if status.by_model:
|
|
213
|
-
table.add_row(Text("Usage by model:", style=ThemeKey.
|
|
208
|
+
table.add_row(Text("Usage by model:", style=ThemeKey.TOOL_RESULT_BOLD), "")
|
|
214
209
|
for meta in status.by_model:
|
|
215
210
|
model_label = meta.model_name
|
|
216
211
|
if meta.provider:
|
|
@@ -1,24 +1,12 @@
|
|
|
1
|
-
from importlib.metadata import PackageNotFoundError, version
|
|
2
|
-
|
|
3
1
|
from rich.console import Group, RenderableType
|
|
4
2
|
from rich.padding import Padding
|
|
5
3
|
from rich.text import Text
|
|
6
4
|
|
|
7
5
|
from klaude_code.const import DEFAULT_MAX_TOKENS
|
|
8
|
-
from klaude_code.log import is_debug_enabled
|
|
9
6
|
from klaude_code.protocol import events, model
|
|
10
7
|
from klaude_code.tui.components.common import create_grid
|
|
11
|
-
from klaude_code.tui.components.rich.quote import Quote
|
|
12
8
|
from klaude_code.tui.components.rich.theme import ThemeKey
|
|
13
|
-
from klaude_code.ui.common import
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
def _get_version() -> str:
|
|
17
|
-
"""Get the current version of klaude-code."""
|
|
18
|
-
try:
|
|
19
|
-
return version("klaude-code")
|
|
20
|
-
except PackageNotFoundError:
|
|
21
|
-
return "unknown"
|
|
9
|
+
from klaude_code.ui.common import format_number
|
|
22
10
|
|
|
23
11
|
|
|
24
12
|
def _render_task_metadata_block(
|
|
@@ -188,60 +176,10 @@ def render_task_metadata(e: events.TaskMetadataEvent) -> RenderableType:
|
|
|
188
176
|
("Σ ", ThemeKey.METADATA_DIM),
|
|
189
177
|
("total ", ThemeKey.METADATA_DIM),
|
|
190
178
|
(currency_symbol, ThemeKey.METADATA_DIM),
|
|
191
|
-
(f"{total_cost:.4f}", ThemeKey.
|
|
179
|
+
(f"{total_cost:.4f}", ThemeKey.METADATA),
|
|
192
180
|
)
|
|
193
181
|
grid = create_grid()
|
|
194
182
|
grid.add_row(Text(" ", style=ThemeKey.METADATA_DIM), total_line)
|
|
195
183
|
renderables.append(Padding(grid, (0, 0, 0, 2)))
|
|
196
184
|
|
|
197
185
|
return Group(*renderables)
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
def render_welcome(e: events.WelcomeEvent) -> RenderableType:
|
|
201
|
-
"""Render the welcome panel with model info and settings.
|
|
202
|
-
|
|
203
|
-
Args:
|
|
204
|
-
e: The welcome event.
|
|
205
|
-
"""
|
|
206
|
-
debug_mode = is_debug_enabled()
|
|
207
|
-
|
|
208
|
-
panel_content = Text()
|
|
209
|
-
|
|
210
|
-
if e.show_klaude_code_info:
|
|
211
|
-
# First line: Klaude Code version
|
|
212
|
-
klaude_code_style = ThemeKey.WELCOME_DEBUG_TITLE if debug_mode else ThemeKey.WELCOME_HIGHLIGHT_BOLD
|
|
213
|
-
panel_content.append_text(Text("Klaude Code", style=klaude_code_style))
|
|
214
|
-
panel_content.append_text(Text(f" v{_get_version()}", style=ThemeKey.WELCOME_INFO))
|
|
215
|
-
panel_content.append_text(Text("\n"))
|
|
216
|
-
|
|
217
|
-
# Model line: model @ provider · params...
|
|
218
|
-
panel_content.append_text(
|
|
219
|
-
Text.assemble(
|
|
220
|
-
(str(e.llm_config.model), ThemeKey.WELCOME_HIGHLIGHT),
|
|
221
|
-
(" @ ", ThemeKey.WELCOME_INFO),
|
|
222
|
-
(e.llm_config.provider_name, ThemeKey.WELCOME_INFO),
|
|
223
|
-
)
|
|
224
|
-
)
|
|
225
|
-
|
|
226
|
-
# Use format_model_params for consistent formatting
|
|
227
|
-
param_strings = format_model_params(e.llm_config)
|
|
228
|
-
|
|
229
|
-
# Render config items with tree-style prefixes
|
|
230
|
-
for i, param_str in enumerate(param_strings):
|
|
231
|
-
is_last = i == len(param_strings) - 1
|
|
232
|
-
prefix = "└─ " if is_last else "├─ "
|
|
233
|
-
panel_content.append_text(
|
|
234
|
-
Text.assemble(
|
|
235
|
-
("\n", ThemeKey.WELCOME_INFO),
|
|
236
|
-
(prefix, ThemeKey.LINES),
|
|
237
|
-
(param_str, ThemeKey.WELCOME_INFO),
|
|
238
|
-
)
|
|
239
|
-
)
|
|
240
|
-
|
|
241
|
-
border_style = ThemeKey.WELCOME_DEBUG_BORDER if debug_mode else ThemeKey.LINES
|
|
242
|
-
|
|
243
|
-
if e.show_klaude_code_info:
|
|
244
|
-
groups = ["", Quote(panel_content, style=border_style, prefix="▌ "), ""]
|
|
245
|
-
else:
|
|
246
|
-
groups = [Quote(panel_content, style=border_style, prefix="▌ "), ""]
|
|
247
|
-
return Group(*groups)
|
|
@@ -4,6 +4,7 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import unicodedata
|
|
6
6
|
from collections.abc import Callable
|
|
7
|
+
from typing import Any, cast
|
|
7
8
|
|
|
8
9
|
|
|
9
10
|
def _is_cjk_char(ch: str) -> bool:
|
|
@@ -222,7 +223,7 @@ def install_rich_cjk_wrap_patch() -> bool:
|
|
|
222
223
|
|
|
223
224
|
return break_positions
|
|
224
225
|
|
|
225
|
-
_wrap.divide_line = divide_line_patched # pyright: ignore[reportPrivateImportUsage]
|
|
226
|
-
_text.divide_line = divide_line_patched # pyright: ignore[reportPrivateImportUsage]
|
|
226
|
+
cast(Any, _wrap).divide_line = divide_line_patched # pyright: ignore[reportPrivateImportUsage]
|
|
227
|
+
cast(Any, _text).divide_line = divide_line_patched # pyright: ignore[reportPrivateImportUsage]
|
|
227
228
|
_rich_cjk_wrap_patch_installed = True
|
|
228
229
|
return True
|
|
@@ -263,6 +263,53 @@ def _breathing_style(console: Console, base_style: Style, intensity: float) -> S
|
|
|
263
263
|
return base_style + Style(color=breathing_color)
|
|
264
264
|
|
|
265
265
|
|
|
266
|
+
def truncate_left(text: Text, max_cells: int, *, console: Console, ellipsis: str = "…") -> Text:
|
|
267
|
+
"""Left-truncate Text to fit within max_cells.
|
|
268
|
+
|
|
269
|
+
Keeps the rightmost part of the text and prepends an ellipsis when truncation occurs.
|
|
270
|
+
Uses cell width so wide characters are handled reasonably.
|
|
271
|
+
"""
|
|
272
|
+
|
|
273
|
+
max_cells = max(0, int(max_cells))
|
|
274
|
+
if max_cells == 0:
|
|
275
|
+
return Text("")
|
|
276
|
+
|
|
277
|
+
if cell_len(text.plain) <= max_cells:
|
|
278
|
+
return text
|
|
279
|
+
|
|
280
|
+
ellipsis_cells = cell_len(ellipsis)
|
|
281
|
+
if max_cells <= ellipsis_cells:
|
|
282
|
+
# Not enough space to show any meaningful suffix.
|
|
283
|
+
clipped = Text(ellipsis, style=text.style)
|
|
284
|
+
clipped.truncate(max_cells, overflow="crop", pad=False)
|
|
285
|
+
return clipped
|
|
286
|
+
|
|
287
|
+
suffix_budget = max_cells - ellipsis_cells
|
|
288
|
+
plain = text.plain
|
|
289
|
+
|
|
290
|
+
suffix_cells = 0
|
|
291
|
+
start_index = len(plain)
|
|
292
|
+
for i in range(len(plain) - 1, -1, -1):
|
|
293
|
+
ch_cells = cell_len(plain[i])
|
|
294
|
+
if suffix_cells + ch_cells > suffix_budget:
|
|
295
|
+
break
|
|
296
|
+
suffix_cells += ch_cells
|
|
297
|
+
start_index = i
|
|
298
|
+
if suffix_cells == suffix_budget:
|
|
299
|
+
break
|
|
300
|
+
|
|
301
|
+
if start_index >= len(plain):
|
|
302
|
+
return Text(ellipsis, style=text.style)
|
|
303
|
+
|
|
304
|
+
suffix = text[start_index:]
|
|
305
|
+
try:
|
|
306
|
+
ellipsis_style = suffix.get_style_at_offset(console, 0)
|
|
307
|
+
except Exception:
|
|
308
|
+
ellipsis_style = suffix.style or text.style
|
|
309
|
+
|
|
310
|
+
return Text.assemble(Text(ellipsis, style=ellipsis_style), suffix)
|
|
311
|
+
|
|
312
|
+
|
|
266
313
|
class ShimmerStatusText:
|
|
267
314
|
"""Renderable status line with shimmer effect on the main text and hint.
|
|
268
315
|
|
|
@@ -322,12 +369,11 @@ class _StatusLeftText:
|
|
|
322
369
|
# If the hint itself can't fit, fall back to truncating the combined text.
|
|
323
370
|
if max_width <= hint_cells:
|
|
324
371
|
combined = Text.assemble(main_text, hint_text)
|
|
325
|
-
combined
|
|
326
|
-
yield combined
|
|
372
|
+
yield truncate_left(combined, max(1, max_width), console=console)
|
|
327
373
|
return
|
|
328
374
|
|
|
329
375
|
main_budget = max_width - hint_cells
|
|
330
|
-
main_text
|
|
376
|
+
main_text = truncate_left(main_text, max(1, main_budget), console=console)
|
|
331
377
|
yield Text.assemble(main_text, hint_text)
|
|
332
378
|
|
|
333
379
|
|
|
@@ -191,6 +191,7 @@ class ThemeKey(str, Enum):
|
|
|
191
191
|
WELCOME_HIGHLIGHT_BOLD = "welcome.highlight.bold"
|
|
192
192
|
WELCOME_HIGHLIGHT = "welcome.highlight"
|
|
193
193
|
WELCOME_INFO = "welcome.info"
|
|
194
|
+
WELCOME_INFO_BOLD = "welcome.info.bold"
|
|
194
195
|
# WELCOME DEBUG
|
|
195
196
|
WELCOME_DEBUG_TITLE = "welcome.debug.title"
|
|
196
197
|
WELCOME_DEBUG_BORDER = "welcome.debug.border"
|
|
@@ -250,8 +251,8 @@ def get_theme(theme: str | None = None) -> Themes:
|
|
|
250
251
|
ThemeKey.USER_INPUT.value: palette.magenta,
|
|
251
252
|
ThemeKey.USER_INPUT_PROMPT.value: "bold " + palette.magenta,
|
|
252
253
|
ThemeKey.USER_INPUT_AT_PATTERN.value: palette.purple,
|
|
253
|
-
ThemeKey.USER_INPUT_SLASH_COMMAND.value: "bold
|
|
254
|
-
ThemeKey.USER_INPUT_SKILL.value: "bold
|
|
254
|
+
ThemeKey.USER_INPUT_SLASH_COMMAND.value: "bold " + palette.blue,
|
|
255
|
+
ThemeKey.USER_INPUT_SKILL.value: "bold " + palette.green,
|
|
255
256
|
# ASSISTANT
|
|
256
257
|
ThemeKey.ASSISTANT_MESSAGE_MARK.value: "bold",
|
|
257
258
|
# METADATA
|
|
@@ -307,6 +308,7 @@ def get_theme(theme: str | None = None) -> Themes:
|
|
|
307
308
|
ThemeKey.WELCOME_HIGHLIGHT_BOLD.value: "bold",
|
|
308
309
|
ThemeKey.WELCOME_HIGHLIGHT.value: palette.blue,
|
|
309
310
|
ThemeKey.WELCOME_INFO.value: palette.grey1,
|
|
311
|
+
ThemeKey.WELCOME_INFO_BOLD.value: "bold " + palette.grey1,
|
|
310
312
|
# WELCOME DEBUG
|
|
311
313
|
ThemeKey.WELCOME_DEBUG_TITLE.value: "bold " + palette.red,
|
|
312
314
|
ThemeKey.WELCOME_DEBUG_BORDER.value: palette.red,
|
|
@@ -3,7 +3,7 @@ from typing import Any, cast
|
|
|
3
3
|
|
|
4
4
|
from rich.console import Group, RenderableType
|
|
5
5
|
from rich.json import JSON
|
|
6
|
-
from rich.style import Style
|
|
6
|
+
from rich.style import Style
|
|
7
7
|
from rich.text import Text
|
|
8
8
|
|
|
9
9
|
from klaude_code.const import SUB_AGENT_RESULT_MAX_LINES
|
|
@@ -79,65 +79,44 @@ def _extract_agent_id_footer(text: str) -> tuple[str, str | None]:
|
|
|
79
79
|
def render_sub_agent_result(
|
|
80
80
|
result: str,
|
|
81
81
|
*,
|
|
82
|
-
code_theme: str,
|
|
83
|
-
style: StyleType | None = None,
|
|
84
82
|
has_structured_output: bool = False,
|
|
85
83
|
description: str | None = None,
|
|
86
84
|
) -> RenderableType:
|
|
87
85
|
stripped_result = result.strip()
|
|
88
|
-
|
|
89
|
-
# Extract agentId footer for separate styling
|
|
90
86
|
main_content, agent_id_footer = _extract_agent_id_footer(stripped_result)
|
|
91
87
|
stripped_result = main_content.strip()
|
|
92
88
|
|
|
93
|
-
|
|
89
|
+
elements: list[RenderableType] = []
|
|
90
|
+
if description:
|
|
91
|
+
elements.append(Text(f"---\n{description}", style=ThemeKey.TOOL_RESULT))
|
|
92
|
+
|
|
93
|
+
# Try structured JSON output first
|
|
94
|
+
use_text_rendering = True
|
|
94
95
|
if has_structured_output:
|
|
95
96
|
try:
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
style=ThemeKey.TOOL_RESULT,
|
|
100
|
-
),
|
|
101
|
-
JSON(stripped_result),
|
|
102
|
-
]
|
|
103
|
-
if description:
|
|
104
|
-
group_elements.insert(0, Text(f"\n{description}", style=style or ""))
|
|
105
|
-
if agent_id_footer:
|
|
106
|
-
group_elements.append(Text(agent_id_footer, style=ThemeKey.SUB_AGENT_FOOTER))
|
|
107
|
-
return Group(*group_elements)
|
|
97
|
+
elements.append(Text("use /export to view full output", style=ThemeKey.TOOL_RESULT_TRUNCATED))
|
|
98
|
+
elements.append(JSON(stripped_result))
|
|
99
|
+
use_text_rendering = False
|
|
108
100
|
except json.JSONDecodeError:
|
|
109
|
-
# Fall back to markdown if not valid JSON
|
|
110
101
|
pass
|
|
111
102
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
Text(
|
|
125
|
-
f"( … more {hidden_count} lines)",
|
|
126
|
-
style=ThemeKey.TOOL_RESULT_TRUNCATED,
|
|
127
|
-
)
|
|
128
|
-
)
|
|
129
|
-
truncated_elements.append(Text(tail_text, style=style or ""))
|
|
130
|
-
if agent_id_footer:
|
|
131
|
-
truncated_elements.append(Text(agent_id_footer, style=ThemeKey.SUB_AGENT_FOOTER))
|
|
132
|
-
return Group(*truncated_elements)
|
|
103
|
+
# Text rendering (either fallback or non-structured)
|
|
104
|
+
if use_text_rendering:
|
|
105
|
+
if not stripped_result:
|
|
106
|
+
return Text()
|
|
107
|
+
|
|
108
|
+
lines = stripped_result.splitlines()
|
|
109
|
+
if len(lines) > SUB_AGENT_RESULT_MAX_LINES:
|
|
110
|
+
hidden_count = len(lines) - SUB_AGENT_RESULT_MAX_LINES
|
|
111
|
+
elements.append(Text(f"( ... more {hidden_count} lines)", style=ThemeKey.TOOL_RESULT_TRUNCATED))
|
|
112
|
+
elements.append(Text("\n".join(lines[-SUB_AGENT_RESULT_MAX_LINES:]), style=ThemeKey.TOOL_RESULT))
|
|
113
|
+
else:
|
|
114
|
+
elements.append(Text(stripped_result, style=ThemeKey.TOOL_RESULT))
|
|
133
115
|
|
|
134
|
-
# No truncation needed - add description heading if provided
|
|
135
|
-
if description:
|
|
136
|
-
stripped_result = f"\n# {description}\n\n{stripped_result}"
|
|
137
|
-
normal_elements: list[RenderableType] = [Text(stripped_result)]
|
|
138
116
|
if agent_id_footer:
|
|
139
|
-
|
|
140
|
-
|
|
117
|
+
elements.append(Text(agent_id_footer, style=ThemeKey.SUB_AGENT_FOOTER))
|
|
118
|
+
|
|
119
|
+
return Group(*elements)
|
|
141
120
|
|
|
142
121
|
|
|
143
122
|
def build_sub_agent_state_from_tool_call(e: events.ToolCallEvent) -> model.SubAgentState | None:
|
|
@@ -4,7 +4,6 @@ from rich.console import Group, RenderableType
|
|
|
4
4
|
from rich.text import Text
|
|
5
5
|
|
|
6
6
|
from klaude_code.skill import get_available_skills
|
|
7
|
-
from klaude_code.tui.command import is_slash_command_name
|
|
8
7
|
from klaude_code.tui.components.common import create_grid
|
|
9
8
|
from klaude_code.tui.components.rich.theme import ThemeKey
|
|
10
9
|
|
|
@@ -54,35 +53,28 @@ def _is_valid_skill_name(name: str) -> bool:
|
|
|
54
53
|
def render_user_input(content: str) -> RenderableType:
|
|
55
54
|
"""Render a user message as a group of quoted lines with styles.
|
|
56
55
|
|
|
57
|
-
- Highlights slash command on the first line
|
|
56
|
+
- Highlights slash command token on the first line
|
|
58
57
|
- Highlights $skill pattern on the first line if recognized
|
|
59
58
|
- Highlights @file patterns in all lines
|
|
60
59
|
"""
|
|
61
60
|
lines = content.strip().split("\n")
|
|
62
61
|
renderables: list[RenderableType] = []
|
|
63
|
-
has_command = False
|
|
64
|
-
command_style: str | None = None
|
|
65
62
|
for i, line in enumerate(lines):
|
|
66
63
|
line_text = render_at_pattern(line)
|
|
67
64
|
|
|
68
65
|
if i == 0 and line.startswith("/"):
|
|
69
66
|
splits = line.split(" ", maxsplit=1)
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
)
|
|
78
|
-
renderables.append(line_text)
|
|
79
|
-
continue
|
|
67
|
+
line_text = Text.assemble(
|
|
68
|
+
(splits[0], ThemeKey.USER_INPUT_SLASH_COMMAND),
|
|
69
|
+
" ",
|
|
70
|
+
render_at_pattern(splits[1]) if len(splits) > 1 else Text(""),
|
|
71
|
+
)
|
|
72
|
+
renderables.append(line_text)
|
|
73
|
+
continue
|
|
80
74
|
|
|
81
75
|
if i == 0 and (line.startswith("$") or line.startswith("¥")):
|
|
82
76
|
m = SKILL_RENDER_PATTERN.match(line)
|
|
83
77
|
if m and _is_valid_skill_name(m.group(1)):
|
|
84
|
-
has_command = True
|
|
85
|
-
command_style = ThemeKey.USER_INPUT_SKILL
|
|
86
78
|
skill_token = m.group(0) # e.g. "$skill-name"
|
|
87
79
|
rest = line[len(skill_token) :]
|
|
88
80
|
line_text = Text.assemble(
|
|
@@ -95,11 +87,7 @@ def render_user_input(content: str) -> RenderableType:
|
|
|
95
87
|
renderables.append(line_text)
|
|
96
88
|
grid = create_grid()
|
|
97
89
|
grid.padding = (0, 0)
|
|
98
|
-
mark = (
|
|
99
|
-
Text(USER_MESSAGE_MARK, style=ThemeKey.USER_INPUT_PROMPT)
|
|
100
|
-
if not has_command
|
|
101
|
-
else Text(" ", style=command_style or ThemeKey.USER_INPUT_SLASH_COMMAND)
|
|
102
|
-
)
|
|
90
|
+
mark = Text(USER_MESSAGE_MARK, style=ThemeKey.USER_INPUT_PROMPT)
|
|
103
91
|
grid.add_row(mark, Group(*renderables))
|
|
104
92
|
return grid
|
|
105
93
|
|