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.
Files changed (72) hide show
  1. klaude_code/app/__init__.py +1 -2
  2. klaude_code/app/runtime.py +13 -41
  3. klaude_code/cli/list_model.py +27 -10
  4. klaude_code/cli/main.py +42 -159
  5. klaude_code/config/assets/builtin_config.yaml +36 -14
  6. klaude_code/config/config.py +144 -7
  7. klaude_code/config/select_model.py +38 -13
  8. klaude_code/config/sub_agent_model_helper.py +217 -0
  9. klaude_code/const.py +2 -2
  10. klaude_code/core/agent_profile.py +71 -5
  11. klaude_code/core/executor.py +75 -0
  12. klaude_code/core/manager/llm_clients_builder.py +18 -12
  13. klaude_code/core/prompts/prompt-nano-banana.md +1 -0
  14. klaude_code/core/tool/shell/command_safety.py +4 -189
  15. klaude_code/core/tool/sub_agent_tool.py +2 -1
  16. klaude_code/core/turn.py +1 -1
  17. klaude_code/llm/anthropic/client.py +8 -5
  18. klaude_code/llm/anthropic/input.py +54 -29
  19. klaude_code/llm/google/client.py +2 -2
  20. klaude_code/llm/google/input.py +23 -2
  21. klaude_code/llm/openai_compatible/input.py +22 -13
  22. klaude_code/llm/openai_compatible/stream.py +1 -1
  23. klaude_code/llm/openrouter/input.py +37 -25
  24. klaude_code/llm/responses/client.py +1 -1
  25. klaude_code/llm/responses/input.py +96 -57
  26. klaude_code/protocol/commands.py +1 -2
  27. klaude_code/protocol/events/system.py +4 -0
  28. klaude_code/protocol/message.py +2 -2
  29. klaude_code/protocol/op.py +17 -0
  30. klaude_code/protocol/op_handler.py +5 -0
  31. klaude_code/protocol/sub_agent/AGENTS.md +28 -0
  32. klaude_code/protocol/sub_agent/__init__.py +10 -14
  33. klaude_code/protocol/sub_agent/image_gen.py +2 -1
  34. klaude_code/session/codec.py +2 -6
  35. klaude_code/session/session.py +9 -1
  36. klaude_code/skill/assets/create-plan/SKILL.md +3 -5
  37. klaude_code/tui/command/__init__.py +7 -10
  38. klaude_code/tui/command/clear_cmd.py +1 -1
  39. klaude_code/tui/command/command_abc.py +1 -2
  40. klaude_code/tui/command/copy_cmd.py +1 -2
  41. klaude_code/tui/command/fork_session_cmd.py +4 -4
  42. klaude_code/tui/command/model_cmd.py +6 -43
  43. klaude_code/tui/command/model_select.py +75 -15
  44. klaude_code/tui/command/refresh_cmd.py +1 -2
  45. klaude_code/tui/command/resume_cmd.py +3 -4
  46. klaude_code/tui/command/status_cmd.py +1 -1
  47. klaude_code/tui/command/sub_agent_model_cmd.py +190 -0
  48. klaude_code/tui/components/bash_syntax.py +1 -1
  49. klaude_code/tui/components/common.py +1 -1
  50. klaude_code/tui/components/developer.py +10 -15
  51. klaude_code/tui/components/metadata.py +2 -64
  52. klaude_code/tui/components/rich/cjk_wrap.py +3 -2
  53. klaude_code/tui/components/rich/status.py +49 -3
  54. klaude_code/tui/components/rich/theme.py +4 -2
  55. klaude_code/tui/components/sub_agent.py +25 -46
  56. klaude_code/tui/components/user_input.py +9 -21
  57. klaude_code/tui/components/welcome.py +99 -0
  58. klaude_code/tui/input/prompt_toolkit.py +14 -1
  59. klaude_code/tui/renderer.py +2 -3
  60. klaude_code/tui/runner.py +2 -2
  61. klaude_code/tui/terminal/selector.py +8 -18
  62. klaude_code/ui/__init__.py +0 -24
  63. klaude_code/ui/common.py +3 -2
  64. klaude_code/ui/core/display.py +2 -2
  65. {klaude_code-2.1.1.dist-info → klaude_code-2.3.0.dist-info}/METADATA +16 -81
  66. {klaude_code-2.1.1.dist-info → klaude_code-2.3.0.dist-info}/RECORD +68 -67
  67. klaude_code/tui/command/help_cmd.py +0 -51
  68. klaude_code/tui/command/prompt-commit.md +0 -82
  69. klaude_code/tui/command/release_notes_cmd.py +0 -85
  70. klaude_code/ui/exec_mode.py +0 -60
  71. {klaude_code-2.1.1.dist-info → klaude_code-2.3.0.dist-info}/WHEEL +0 -0
  72. {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
 
@@ -106,7 +106,7 @@ def truncate_head(
106
106
  text = text.expandtabs(TAB_EXPAND_WIDTH)
107
107
  lines = [line for line in text.split("\n") if line.strip()]
108
108
 
109
- out = Text()
109
+ out = Text(overflow="fold")
110
110
  if base_style is not None:
111
111
  out.style = base_style
112
112
 
@@ -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.METADATA), level=2)
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.METADATA, overflow="fold")
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.METADATA))
188
- grid.add_row(Text(f" klaude --resume-by-id {session_id}", style=ThemeKey.METADATA_BOLD))
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.METADATA)
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.METADATA, overflow="fold")
203
- table.add_column(style=ThemeKey.METADATA, overflow="fold")
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.METADATA_BOLD),
208
- Text(_format_cost(usage.total_cost, usage.currency), style=ThemeKey.METADATA_BOLD),
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.METADATA_BOLD), "")
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 format_model_params, format_number
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.METADATA_BOLD),
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.truncate(max(1, max_width), overflow="ellipsis", pad=False)
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.truncate(max(1, main_budget), overflow="ellipsis", pad=False)
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 reverse " + palette.blue,
254
- ThemeKey.USER_INPUT_SKILL.value: "bold reverse " + palette.green,
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, StyleType
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
- # Use rich JSON for structured output
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
- group_elements: list[RenderableType] = [
97
- Text(
98
- "use /export to view full output",
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
- if not stripped_result:
113
- return Text()
114
-
115
- lines = stripped_result.splitlines()
116
- if len(lines) > SUB_AGENT_RESULT_MAX_LINES:
117
- hidden_count = len(lines) - SUB_AGENT_RESULT_MAX_LINES
118
- tail_text = "\n".join(lines[-SUB_AGENT_RESULT_MAX_LINES:])
119
- truncated_elements: list[RenderableType] = []
120
- # Add description heading separately so it won't be truncated
121
- if description:
122
- truncated_elements.append(Text(f"---\n{description}", style=style or ""))
123
- truncated_elements.append(
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
- normal_elements.append(Text(agent_id_footer, style=ThemeKey.SUB_AGENT_FOOTER))
140
- return Group(*normal_elements)
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 if recognized
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
- if is_slash_command_name(splits[0][1:]):
71
- has_command = True
72
- command_style = ThemeKey.USER_INPUT_SLASH_COMMAND
73
- line_text = Text.assemble(
74
- (f"{splits[0]}", ThemeKey.USER_INPUT_SLASH_COMMAND),
75
- " ",
76
- render_at_pattern(splits[1]) if len(splits) > 1 else Text(""),
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