klaude-code 2.2.0__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/runtime.py +2 -15
- klaude_code/cli/list_model.py +27 -10
- klaude_code/cli/main.py +25 -9
- klaude_code/config/assets/builtin_config.yaml +25 -16
- 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 +1 -1
- klaude_code/core/agent_profile.py +43 -5
- klaude_code/core/executor.py +75 -0
- klaude_code/core/manager/llm_clients_builder.py +17 -11
- klaude_code/core/prompts/prompt-nano-banana.md +1 -1
- klaude_code/core/tool/sub_agent_tool.py +2 -1
- klaude_code/llm/anthropic/client.py +7 -4
- klaude_code/llm/anthropic/input.py +54 -29
- klaude_code/llm/google/client.py +1 -1
- klaude_code/llm/google/input.py +23 -2
- klaude_code/llm/openai_compatible/input.py +22 -13
- klaude_code/llm/openrouter/input.py +37 -25
- 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/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 +3 -6
- klaude_code/tui/command/model_cmd.py +6 -43
- klaude_code/tui/command/model_select.py +75 -15
- 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 +0 -5
- klaude_code/tui/components/metadata.py +1 -63
- 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 +2 -0
- klaude_code/tui/components/sub_agent.py +25 -46
- 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/terminal/selector.py +5 -3
- {klaude_code-2.2.0.dist-info → klaude_code-2.3.0.dist-info}/METADATA +1 -1
- {klaude_code-2.2.0.dist-info → klaude_code-2.3.0.dist-info}/RECORD +50 -48
- klaude_code/tui/command/help_cmd.py +0 -51
- klaude_code/tui/command/release_notes_cmd.py +0 -85
- {klaude_code-2.2.0.dist-info → klaude_code-2.3.0.dist-info}/WHEEL +0 -0
- {klaude_code-2.2.0.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 "", style=ThemeKey.TOOL_RESULT), 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 _:
|
|
@@ -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(
|
|
@@ -195,53 +183,3 @@ def render_task_metadata(e: events.TaskMetadataEvent) -> RenderableType:
|
|
|
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"
|
|
@@ -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:
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
2
|
+
|
|
3
|
+
from rich.console import Group, RenderableType
|
|
4
|
+
from rich.text import Text
|
|
5
|
+
|
|
6
|
+
from klaude_code.log import is_debug_enabled
|
|
7
|
+
from klaude_code.protocol import events
|
|
8
|
+
from klaude_code.tui.components.rich.quote import Quote
|
|
9
|
+
from klaude_code.tui.components.rich.theme import ThemeKey
|
|
10
|
+
from klaude_code.ui.common import format_model_params
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _get_version() -> str:
|
|
14
|
+
"""Get the current version of klaude-code."""
|
|
15
|
+
try:
|
|
16
|
+
return version("klaude-code")
|
|
17
|
+
except PackageNotFoundError:
|
|
18
|
+
return "unknown"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def render_welcome(e: events.WelcomeEvent) -> RenderableType:
|
|
22
|
+
"""Render the welcome panel with model info and settings.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
e: The welcome event.
|
|
26
|
+
"""
|
|
27
|
+
debug_mode = is_debug_enabled()
|
|
28
|
+
|
|
29
|
+
panel_content = Text()
|
|
30
|
+
|
|
31
|
+
if e.show_klaude_code_info:
|
|
32
|
+
# First line: Klaude Code version
|
|
33
|
+
klaude_code_style = ThemeKey.WELCOME_DEBUG_TITLE if debug_mode else ThemeKey.WELCOME_HIGHLIGHT_BOLD
|
|
34
|
+
panel_content.append_text(Text("Klaude Code", style=klaude_code_style))
|
|
35
|
+
panel_content.append_text(Text(f" v{_get_version()}", style=ThemeKey.WELCOME_INFO))
|
|
36
|
+
panel_content.append_text(Text("\n"))
|
|
37
|
+
|
|
38
|
+
# Model line: model @ provider · params...
|
|
39
|
+
panel_content.append_text(
|
|
40
|
+
Text.assemble(
|
|
41
|
+
(str(e.llm_config.model), ThemeKey.WELCOME_HIGHLIGHT),
|
|
42
|
+
(" @ ", ThemeKey.WELCOME_INFO),
|
|
43
|
+
(e.llm_config.provider_name, ThemeKey.WELCOME_INFO),
|
|
44
|
+
)
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
# Use format_model_params for consistent formatting
|
|
48
|
+
param_strings = format_model_params(e.llm_config)
|
|
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
|
+
# Render config items with tree-style prefixes
|
|
54
|
+
for i, param_str in enumerate(param_strings):
|
|
55
|
+
is_last = i == len(param_strings) - 1 and not has_sub_agents
|
|
56
|
+
prefix = "└─ " if is_last else "├─ "
|
|
57
|
+
panel_content.append_text(
|
|
58
|
+
Text.assemble(
|
|
59
|
+
("\n", ThemeKey.WELCOME_INFO),
|
|
60
|
+
(prefix, ThemeKey.LINES),
|
|
61
|
+
(param_str, ThemeKey.WELCOME_INFO),
|
|
62
|
+
)
|
|
63
|
+
)
|
|
64
|
+
|
|
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), ThemeKey.WELCOME_HIGHLIGHT),
|
|
88
|
+
(" @ ", ThemeKey.WELCOME_INFO),
|
|
89
|
+
(sub_llm_config.provider_name, ThemeKey.WELCOME_INFO),
|
|
90
|
+
)
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
border_style = ThemeKey.WELCOME_DEBUG_BORDER if debug_mode else ThemeKey.LINES
|
|
94
|
+
|
|
95
|
+
if e.show_klaude_code_info:
|
|
96
|
+
groups = ["", Quote(panel_content, style=border_style, prefix="▌ "), ""]
|
|
97
|
+
else:
|
|
98
|
+
groups = [Quote(panel_content, style=border_style, prefix="▌ "), ""]
|
|
99
|
+
return Group(*groups)
|
|
@@ -455,7 +455,7 @@ class PromptToolkitInput(InputProviderABC):
|
|
|
455
455
|
config = load_config()
|
|
456
456
|
models: list[ModelEntry] = sorted(
|
|
457
457
|
config.iter_model_entries(only_available=True),
|
|
458
|
-
key=lambda m: m.model_name.lower(),
|
|
458
|
+
key=lambda m: (m.model_name.lower(), m.provider.lower()),
|
|
459
459
|
)
|
|
460
460
|
if not models:
|
|
461
461
|
return [], None
|
|
@@ -468,6 +468,15 @@ class PromptToolkitInput(InputProviderABC):
|
|
|
468
468
|
initial = self._get_current_model_config_name()
|
|
469
469
|
if initial is None:
|
|
470
470
|
initial = config.main_model
|
|
471
|
+
if isinstance(initial, str) and initial and "@" not in initial:
|
|
472
|
+
try:
|
|
473
|
+
resolved = config.resolve_model_location_prefer_available(initial) or config.resolve_model_location(
|
|
474
|
+
initial
|
|
475
|
+
)
|
|
476
|
+
except ValueError:
|
|
477
|
+
resolved = None
|
|
478
|
+
if resolved is not None:
|
|
479
|
+
initial = f"{resolved[0]}@{resolved[1]}"
|
|
471
480
|
return items, initial
|
|
472
481
|
|
|
473
482
|
def _open_model_picker(self) -> None:
|
|
@@ -608,6 +617,10 @@ class PromptToolkitInput(InputProviderABC):
|
|
|
608
617
|
(symbol_style, " ctrl-t "),
|
|
609
618
|
(text_style, " "),
|
|
610
619
|
(text_style, "think"),
|
|
620
|
+
(text_style, " "),
|
|
621
|
+
(symbol_style, " ctrl-v "),
|
|
622
|
+
(text_style, " "),
|
|
623
|
+
(text_style, "paste image"),
|
|
611
624
|
]
|
|
612
625
|
)
|
|
613
626
|
|
klaude_code/tui/renderer.py
CHANGED
|
@@ -61,6 +61,7 @@ from klaude_code.tui.components import sub_agent as c_sub_agent
|
|
|
61
61
|
from klaude_code.tui.components import thinking as c_thinking
|
|
62
62
|
from klaude_code.tui.components import tools as c_tools
|
|
63
63
|
from klaude_code.tui.components import user_input as c_user_input
|
|
64
|
+
from klaude_code.tui.components import welcome as c_welcome
|
|
64
65
|
from klaude_code.tui.components.common import truncate_head, truncate_middle
|
|
65
66
|
from klaude_code.tui.components.rich import status as r_status
|
|
66
67
|
from klaude_code.tui.components.rich.live import CropAboveLive, SingleLine
|
|
@@ -511,7 +512,7 @@ class TUICommandRenderer:
|
|
|
511
512
|
self.print()
|
|
512
513
|
|
|
513
514
|
def display_welcome(self, event: events.WelcomeEvent) -> None:
|
|
514
|
-
self.print(
|
|
515
|
+
self.print(c_welcome.render_welcome(event))
|
|
515
516
|
|
|
516
517
|
def display_user_message(self, event: events.UserMessageEvent) -> None:
|
|
517
518
|
self.print(c_user_input.render_user_input(event.content))
|
|
@@ -567,10 +568,8 @@ class TUICommandRenderer:
|
|
|
567
568
|
self.print(
|
|
568
569
|
c_sub_agent.render_sub_agent_result(
|
|
569
570
|
event.task_result,
|
|
570
|
-
code_theme=self.themes.code_theme,
|
|
571
571
|
has_structured_output=event.has_structured_output,
|
|
572
572
|
description=description,
|
|
573
|
-
style=ThemeKey.TOOL_RESULT,
|
|
574
573
|
)
|
|
575
574
|
)
|
|
576
575
|
|