klaude-code 2.2.0__py3-none-any.whl → 2.4.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 (82) hide show
  1. klaude_code/app/runtime.py +2 -15
  2. klaude_code/cli/list_model.py +30 -13
  3. klaude_code/cli/main.py +26 -10
  4. klaude_code/config/assets/builtin_config.yaml +177 -310
  5. klaude_code/config/config.py +158 -21
  6. klaude_code/config/{select_model.py → model_matcher.py} +41 -16
  7. klaude_code/config/sub_agent_model_helper.py +217 -0
  8. klaude_code/config/thinking.py +2 -2
  9. klaude_code/const.py +1 -1
  10. klaude_code/core/agent_profile.py +43 -5
  11. klaude_code/core/executor.py +129 -47
  12. klaude_code/core/manager/llm_clients_builder.py +17 -11
  13. klaude_code/core/prompts/prompt-nano-banana.md +1 -1
  14. klaude_code/core/tool/file/diff_builder.py +25 -18
  15. klaude_code/core/tool/sub_agent_tool.py +2 -1
  16. klaude_code/llm/anthropic/client.py +12 -9
  17. klaude_code/llm/anthropic/input.py +54 -29
  18. klaude_code/llm/client.py +1 -1
  19. klaude_code/llm/codex/client.py +2 -2
  20. klaude_code/llm/google/client.py +7 -7
  21. klaude_code/llm/google/input.py +23 -2
  22. klaude_code/llm/input_common.py +2 -2
  23. klaude_code/llm/openai_compatible/client.py +3 -3
  24. klaude_code/llm/openai_compatible/input.py +22 -13
  25. klaude_code/llm/openai_compatible/stream.py +1 -1
  26. klaude_code/llm/openrouter/client.py +4 -4
  27. klaude_code/llm/openrouter/input.py +35 -25
  28. klaude_code/llm/responses/client.py +5 -5
  29. klaude_code/llm/responses/input.py +96 -57
  30. klaude_code/protocol/commands.py +1 -2
  31. klaude_code/protocol/events/__init__.py +7 -1
  32. klaude_code/protocol/events/chat.py +10 -0
  33. klaude_code/protocol/events/system.py +4 -0
  34. klaude_code/protocol/llm_param.py +1 -1
  35. klaude_code/protocol/model.py +0 -26
  36. klaude_code/protocol/op.py +17 -5
  37. klaude_code/protocol/op_handler.py +5 -0
  38. klaude_code/protocol/sub_agent/AGENTS.md +28 -0
  39. klaude_code/protocol/sub_agent/__init__.py +10 -14
  40. klaude_code/protocol/sub_agent/image_gen.py +2 -1
  41. klaude_code/session/codec.py +2 -6
  42. klaude_code/session/session.py +13 -3
  43. klaude_code/skill/assets/create-plan/SKILL.md +3 -5
  44. klaude_code/tui/command/__init__.py +3 -6
  45. klaude_code/tui/command/clear_cmd.py +0 -1
  46. klaude_code/tui/command/command_abc.py +6 -4
  47. klaude_code/tui/command/copy_cmd.py +10 -10
  48. klaude_code/tui/command/debug_cmd.py +11 -10
  49. klaude_code/tui/command/export_online_cmd.py +18 -23
  50. klaude_code/tui/command/fork_session_cmd.py +39 -43
  51. klaude_code/tui/command/model_cmd.py +10 -49
  52. klaude_code/tui/command/model_picker.py +142 -0
  53. klaude_code/tui/command/refresh_cmd.py +0 -1
  54. klaude_code/tui/command/registry.py +15 -21
  55. klaude_code/tui/command/resume_cmd.py +10 -16
  56. klaude_code/tui/command/status_cmd.py +8 -12
  57. klaude_code/tui/command/sub_agent_model_cmd.py +185 -0
  58. klaude_code/tui/command/terminal_setup_cmd.py +8 -11
  59. klaude_code/tui/command/thinking_cmd.py +4 -6
  60. klaude_code/tui/commands.py +5 -0
  61. klaude_code/tui/components/bash_syntax.py +1 -1
  62. klaude_code/tui/components/command_output.py +96 -0
  63. klaude_code/tui/components/common.py +1 -1
  64. klaude_code/tui/components/developer.py +3 -115
  65. klaude_code/tui/components/metadata.py +1 -63
  66. klaude_code/tui/components/rich/cjk_wrap.py +3 -2
  67. klaude_code/tui/components/rich/status.py +49 -3
  68. klaude_code/tui/components/rich/theme.py +2 -0
  69. klaude_code/tui/components/sub_agent.py +25 -46
  70. klaude_code/tui/components/welcome.py +99 -0
  71. klaude_code/tui/input/prompt_toolkit.py +19 -8
  72. klaude_code/tui/machine.py +5 -0
  73. klaude_code/tui/renderer.py +7 -8
  74. klaude_code/tui/runner.py +0 -6
  75. klaude_code/tui/terminal/selector.py +8 -6
  76. {klaude_code-2.2.0.dist-info → klaude_code-2.4.0.dist-info}/METADATA +21 -74
  77. {klaude_code-2.2.0.dist-info → klaude_code-2.4.0.dist-info}/RECORD +79 -76
  78. klaude_code/tui/command/help_cmd.py +0 -51
  79. klaude_code/tui/command/model_select.py +0 -84
  80. klaude_code/tui/command/release_notes_cmd.py +0 -85
  81. {klaude_code-2.2.0.dist-info → klaude_code-2.4.0.dist-info}/WHEEL +0 -0
  82. {klaude_code-2.2.0.dist-info → klaude_code-2.4.0.dist-info}/entry_points.txt +0 -0
@@ -2,7 +2,7 @@ from importlib.resources import files
2
2
  from typing import TYPE_CHECKING
3
3
 
4
4
  from klaude_code.log import log_debug
5
- from klaude_code.protocol import commands, events, message, model, op
5
+ from klaude_code.protocol import commands, events, message, op
6
6
 
7
7
  from .command_abc import Agent, CommandResult
8
8
  from .prompt_command import PromptCommand
@@ -179,30 +179,24 @@ async def dispatch_command(user_input: message.UserInputPayload, agent: Agent, *
179
179
  result.operations = ops
180
180
  return result
181
181
  except Exception as e:
182
- command_output = (
183
- model.CommandOutput(command_name=command_identifier, is_error=True)
184
- if isinstance(command_identifier, commands.CommandName)
185
- else None
186
- )
187
- ui_extra = (
188
- model.build_command_output_extra(
189
- command_output.command_name,
190
- ui_extra=command_output.ui_extra,
191
- is_error=command_output.is_error,
182
+ error_content = f"Command {command_identifier} error: [{e.__class__.__name__}] {e!s}"
183
+ if isinstance(command_identifier, commands.CommandName):
184
+ return CommandResult(
185
+ events=[
186
+ events.CommandOutputEvent(
187
+ session_id=agent.session.id,
188
+ command_name=command_identifier,
189
+ content=error_content,
190
+ is_error=True,
191
+ )
192
+ ]
192
193
  )
193
- if command_output is not None
194
- else None
195
- )
196
194
  return CommandResult(
197
195
  events=[
198
- events.DeveloperMessageEvent(
196
+ events.ErrorEvent(
199
197
  session_id=agent.session.id,
200
- item=message.DeveloperMessage(
201
- parts=message.text_parts_from_str(
202
- f"Command {command_identifier} error: [{e.__class__.__name__}] {e!s}"
203
- ),
204
- ui_extra=ui_extra,
205
- ),
198
+ error_message=error_content,
199
+ can_retry=False,
206
200
  )
207
201
  ]
208
202
  )
@@ -3,7 +3,7 @@ import asyncio
3
3
  from prompt_toolkit.styles import Style
4
4
 
5
5
  from klaude_code.log import log
6
- from klaude_code.protocol import commands, events, message, model, op
6
+ from klaude_code.protocol import commands, events, message, op
7
7
  from klaude_code.session.selector import build_session_select_options, format_user_messages_display
8
8
  from klaude_code.tui.terminal.selector import SelectItem, select_one
9
9
 
@@ -87,29 +87,23 @@ class ResumeCommand(CommandABC):
87
87
  del user_input # unused
88
88
 
89
89
  if agent.session.messages_count > 0:
90
- event = events.DeveloperMessageEvent(
90
+ event = events.CommandOutputEvent(
91
91
  session_id=agent.session.id,
92
- item=message.DeveloperMessage(
93
- parts=message.text_parts_from_str(
94
- "Cannot resume: current session already has messages. Use `klaude -r` to start a new instance with session selection."
95
- ),
96
- ui_extra=model.build_command_output_extra(self.name, is_error=True),
97
- ),
92
+ command_name=self.name,
93
+ content="Cannot resume: current session already has messages. Use `klaude -r` to start a new instance with session selection.",
94
+ is_error=True,
98
95
  )
99
- return CommandResult(events=[event], persist=False)
96
+ return CommandResult(events=[event])
100
97
 
101
98
  selected_session_id = await asyncio.to_thread(select_session_sync)
102
99
  if selected_session_id is None:
103
- event = events.DeveloperMessageEvent(
100
+ event = events.CommandOutputEvent(
104
101
  session_id=agent.session.id,
105
- item=message.DeveloperMessage(
106
- parts=message.text_parts_from_str("(no session selected)"),
107
- ui_extra=model.build_command_output_extra(self.name),
108
- ),
102
+ command_name=self.name,
103
+ content="(no session selected)",
109
104
  )
110
- return CommandResult(events=[event], persist=False)
105
+ return CommandResult(events=[event])
111
106
 
112
107
  return CommandResult(
113
108
  operations=[op.ResumeSessionOperation(target_session_id=selected_session_id)],
114
- persist=False,
115
109
  )
@@ -138,19 +138,15 @@ class StatusCommand(CommandABC):
138
138
  session = agent.session
139
139
  aggregated = accumulate_session_usage(session)
140
140
 
141
- event = events.DeveloperMessageEvent(
141
+ event = events.CommandOutputEvent(
142
142
  session_id=session.id,
143
- item=message.DeveloperMessage(
144
- parts=message.text_parts_from_str(format_status_content(aggregated)),
145
- ui_extra=model.build_command_output_extra(
146
- self.name,
147
- ui_extra=model.SessionStatusUIExtra(
148
- usage=aggregated.total,
149
- task_count=aggregated.task_count,
150
- by_model=aggregated.by_model,
151
- ),
152
- ),
143
+ command_name=self.name,
144
+ content=format_status_content(aggregated),
145
+ ui_extra=model.SessionStatusUIExtra(
146
+ usage=aggregated.total,
147
+ task_count=aggregated.task_count,
148
+ by_model=aggregated.by_model,
153
149
  ),
154
150
  )
155
151
 
156
- return CommandResult(events=[event], persist=False)
152
+ return CommandResult(events=[event])
@@ -0,0 +1,185 @@
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, 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.CommandOutputEvent(
140
+ session_id=agent.session.id,
141
+ command_name=self.name,
142
+ content="No sub-agents available",
143
+ is_error=True,
144
+ )
145
+ ]
146
+ )
147
+
148
+ selected_sub_agent = await asyncio.to_thread(_select_sub_agent_sync, sub_agents, helper, main_model_name)
149
+ if selected_sub_agent is None:
150
+ return CommandResult(
151
+ events=[
152
+ events.CommandOutputEvent(
153
+ session_id=agent.session.id,
154
+ command_name=self.name,
155
+ content="(cancelled)",
156
+ )
157
+ ]
158
+ )
159
+
160
+ selected_model = await asyncio.to_thread(
161
+ _select_model_for_sub_agent_sync, helper, selected_sub_agent, main_model_name
162
+ )
163
+ if selected_model is None:
164
+ return CommandResult(
165
+ events=[
166
+ events.CommandOutputEvent(
167
+ session_id=agent.session.id,
168
+ command_name=self.name,
169
+ content="(cancelled)",
170
+ )
171
+ ]
172
+ )
173
+
174
+ model_name: str | None = None if selected_model == USE_DEFAULT_BEHAVIOR else selected_model
175
+
176
+ return CommandResult(
177
+ operations=[
178
+ op.ChangeSubAgentModelOperation(
179
+ session_id=agent.session.id,
180
+ sub_agent_type=selected_sub_agent,
181
+ model_name=model_name,
182
+ save_as_default=True,
183
+ )
184
+ ]
185
+ )
@@ -2,7 +2,7 @@ import os
2
2
  import subprocess
3
3
  from pathlib import Path
4
4
 
5
- from klaude_code.protocol import commands, events, message, model
5
+ from klaude_code.protocol import commands, events, message
6
6
 
7
7
  from .command_abc import Agent, CommandABC, CommandResult
8
8
 
@@ -226,12 +226,10 @@ class TerminalSetupCommand(CommandABC):
226
226
  """Create success result"""
227
227
  return CommandResult(
228
228
  events=[
229
- events.DeveloperMessageEvent(
229
+ events.CommandOutputEvent(
230
230
  session_id=agent.session.id,
231
- item=message.DeveloperMessage(
232
- parts=message.text_parts_from_str(msg),
233
- ui_extra=model.build_command_output_extra(self.name),
234
- ),
231
+ command_name=self.name,
232
+ content=msg,
235
233
  )
236
234
  ]
237
235
  )
@@ -240,12 +238,11 @@ class TerminalSetupCommand(CommandABC):
240
238
  """Create error result"""
241
239
  return CommandResult(
242
240
  events=[
243
- events.DeveloperMessageEvent(
241
+ events.CommandOutputEvent(
244
242
  session_id=agent.session.id,
245
- item=message.DeveloperMessage(
246
- parts=message.text_parts_from_str(msg),
247
- ui_extra=model.build_command_output_extra(self.name, is_error=True),
248
- ),
243
+ command_name=self.name,
244
+ content=msg,
245
+ is_error=True,
249
246
  )
250
247
  ]
251
248
  )
@@ -3,7 +3,7 @@ import asyncio
3
3
  from prompt_toolkit.styles import Style
4
4
 
5
5
  from klaude_code.config.thinking import get_thinking_picker_data, parse_thinking_value
6
- from klaude_code.protocol import commands, events, llm_param, message, model, op
6
+ from klaude_code.protocol import commands, events, llm_param, message, op
7
7
  from klaude_code.tui.terminal.selector import SelectItem, select_one
8
8
 
9
9
  from .command_abc import Agent, CommandABC, CommandResult
@@ -79,12 +79,10 @@ class ThinkingCommand(CommandABC):
79
79
  if new_thinking is None:
80
80
  return CommandResult(
81
81
  events=[
82
- events.DeveloperMessageEvent(
82
+ events.CommandOutputEvent(
83
83
  session_id=agent.session.id,
84
- item=message.DeveloperMessage(
85
- parts=message.text_parts_from_str("(no change)"),
86
- ui_extra=model.build_command_output_extra(self.name),
87
- ),
84
+ command_name=self.name,
85
+ content="(no change)",
88
86
  )
89
87
  ]
90
88
  )
@@ -38,6 +38,11 @@ class RenderDeveloperMessage(RenderCommand):
38
38
  event: events.DeveloperMessageEvent
39
39
 
40
40
 
41
+ @dataclass(frozen=True, slots=True)
42
+ class RenderCommandOutput(RenderCommand):
43
+ event: events.CommandOutputEvent
44
+
45
+
41
46
  @dataclass(frozen=True, slots=True)
42
47
  class RenderTurnStart(RenderCommand):
43
48
  event: events.TurnStartEvent
@@ -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
 
@@ -0,0 +1,96 @@
1
+ from rich.console import RenderableType
2
+ from rich.padding import Padding
3
+ from rich.table import Table
4
+ from rich.text import Text
5
+
6
+ from klaude_code.protocol import events, model
7
+ from klaude_code.tui.components.common import truncate_middle
8
+ from klaude_code.tui.components.rich.theme import ThemeKey
9
+
10
+
11
+ def render_command_output(e: events.CommandOutputEvent) -> RenderableType:
12
+ """Render command output content."""
13
+ match e.command_name:
14
+ case "status":
15
+ return _render_status_output(e)
16
+ case "fork-session":
17
+ return _render_fork_session_output(e)
18
+ case _:
19
+ content = e.content or "(no content)"
20
+ style = ThemeKey.TOOL_RESULT if not e.is_error else ThemeKey.ERROR
21
+ return Padding.indent(truncate_middle(content, base_style=style), level=2)
22
+
23
+
24
+ def _format_tokens(tokens: int) -> str:
25
+ """Format token count with K/M suffix for readability."""
26
+ if tokens >= 1_000_000:
27
+ return f"{tokens / 1_000_000:.2f}M"
28
+ if tokens >= 1_000:
29
+ return f"{tokens / 1_000:.1f}K"
30
+ return str(tokens)
31
+
32
+
33
+ def _format_cost(cost: float | None, currency: str = "USD") -> str:
34
+ """Format cost with currency symbol."""
35
+ if cost is None:
36
+ return "-"
37
+ symbol = "Y" if currency == "CNY" else "$"
38
+ if cost < 0.01:
39
+ return f"{symbol}{cost:.4f}"
40
+ return f"{symbol}{cost:.2f}"
41
+
42
+
43
+ def _render_fork_session_output(e: events.CommandOutputEvent) -> RenderableType:
44
+ """Render fork session output with usage instructions."""
45
+ if not isinstance(e.ui_extra, model.SessionIdUIExtra):
46
+ return Padding.indent(Text(e.content, style=ThemeKey.TOOL_RESULT), level=2)
47
+
48
+ grid = Table.grid(padding=(0, 1))
49
+ session_id = e.ui_extra.session_id
50
+ grid.add_column(style=ThemeKey.TOOL_RESULT, overflow="fold")
51
+
52
+ grid.add_row(Text("Session forked. Resume command copied to clipboard:", style=ThemeKey.TOOL_RESULT))
53
+ grid.add_row(Text(f" klaude --resume-by-id {session_id}", style=ThemeKey.TOOL_RESULT_BOLD))
54
+
55
+ return Padding.indent(grid, level=2)
56
+
57
+
58
+ def _render_status_output(e: events.CommandOutputEvent) -> RenderableType:
59
+ """Render session status with total cost and per-model breakdown."""
60
+ if not isinstance(e.ui_extra, model.SessionStatusUIExtra):
61
+ return Text("(no status data)", style=ThemeKey.TOOL_RESULT)
62
+
63
+ status = e.ui_extra
64
+ usage = status.usage
65
+
66
+ table = Table.grid(padding=(0, 2))
67
+ table.add_column(style=ThemeKey.TOOL_RESULT, overflow="fold")
68
+ table.add_column(style=ThemeKey.TOOL_RESULT, overflow="fold")
69
+
70
+ # Total cost line
71
+ table.add_row(
72
+ Text("Total cost:", style=ThemeKey.TOOL_RESULT_BOLD),
73
+ Text(_format_cost(usage.total_cost, usage.currency), style=ThemeKey.TOOL_RESULT_BOLD),
74
+ )
75
+
76
+ # Per-model breakdown
77
+ if status.by_model:
78
+ table.add_row(Text("Usage by model:", style=ThemeKey.TOOL_RESULT_BOLD), "")
79
+ for meta in status.by_model:
80
+ model_label = meta.model_name
81
+ if meta.provider:
82
+ model_label = f"{meta.model_name} ({meta.provider.lower().replace(' ', '-')})"
83
+
84
+ if meta.usage:
85
+ usage_detail = (
86
+ f"{_format_tokens(meta.usage.input_tokens)} input, "
87
+ f"{_format_tokens(meta.usage.output_tokens)} output, "
88
+ f"{_format_tokens(meta.usage.cached_tokens)} cache read, "
89
+ f"{_format_tokens(meta.usage.reasoning_tokens)} thinking, "
90
+ f"({_format_cost(meta.usage.total_cost, meta.usage.currency)})"
91
+ )
92
+ else:
93
+ usage_detail = "(no usage data)"
94
+ table.add_row(f"{model_label}:", usage_detail)
95
+
96
+ return Padding.indent(table, level=2)
@@ -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
 
@@ -1,30 +1,18 @@
1
1
  from rich.console import Group, RenderableType
2
- from rich.padding import Padding
3
- from rich.table import Table
4
2
  from rich.text import Text
5
3
 
6
- from klaude_code.protocol import commands, events, message, model
7
- from klaude_code.tui.components.common import create_grid, truncate_middle
8
- from klaude_code.tui.components.rich.markdown import NoInsetMarkdown
4
+ from klaude_code.protocol import events, model
5
+ from klaude_code.tui.components.common import create_grid
9
6
  from klaude_code.tui.components.rich.theme import ThemeKey
10
7
  from klaude_code.tui.components.tools import render_path
11
8
 
12
9
  REMINDER_BULLET = " ⧉"
13
10
 
14
11
 
15
- def get_command_output(item: message.DeveloperMessage) -> model.CommandOutput | None:
16
- if not item.ui_extra:
17
- return None
18
- for ui_item in item.ui_extra.items:
19
- if isinstance(ui_item, model.CommandOutputUIItem):
20
- return ui_item.output
21
- return None
22
-
23
-
24
12
  def need_render_developer_message(e: events.DeveloperMessageEvent) -> bool:
25
13
  if not e.item.ui_extra:
26
14
  return False
27
- return any(not isinstance(ui_item, model.CommandOutputUIItem) for ui_item in e.item.ui_extra.items)
15
+ return len(e.item.ui_extra.items) > 0
28
16
 
29
17
 
30
18
  def render_developer_message(e: events.DeveloperMessageEvent) -> RenderableType:
@@ -127,105 +115,5 @@ def render_developer_message(e: events.DeveloperMessageEvent) -> RenderableType:
127
115
  ),
128
116
  )
129
117
  parts.append(grid)
130
- case model.CommandOutputUIItem():
131
- # Rendered via render_command_output
132
- pass
133
118
 
134
119
  return Group(*parts) if parts else Text("")
135
-
136
-
137
- def render_command_output(e: events.DeveloperMessageEvent) -> RenderableType:
138
- """Render developer command output content."""
139
- command_output = get_command_output(e.item)
140
- if not command_output:
141
- return Text("")
142
-
143
- content = message.join_text_parts(e.item.parts)
144
- 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
- case commands.CommandName.STATUS:
148
- return _render_status_output(command_output)
149
- case commands.CommandName.RELEASE_NOTES:
150
- return Padding.indent(NoInsetMarkdown(content or ""), level=2)
151
- case commands.CommandName.FORK_SESSION:
152
- return _render_fork_session_output(command_output)
153
- case _:
154
- content = content or "(no content)"
155
- style = ThemeKey.TOOL_RESULT if not command_output.is_error else ThemeKey.ERROR
156
- return Padding.indent(truncate_middle(content, base_style=style), level=2)
157
-
158
-
159
- def _format_tokens(tokens: int) -> str:
160
- """Format token count with K/M suffix for readability."""
161
- if tokens >= 1_000_000:
162
- return f"{tokens / 1_000_000:.2f}M"
163
- if tokens >= 1_000:
164
- return f"{tokens / 1_000:.1f}K"
165
- return str(tokens)
166
-
167
-
168
- def _format_cost(cost: float | None, currency: str = "USD") -> str:
169
- """Format cost with currency symbol."""
170
- if cost is None:
171
- return "-"
172
- symbol = "¥" if currency == "CNY" else "$"
173
- if cost < 0.01:
174
- return f"{symbol}{cost:.4f}"
175
- return f"{symbol}{cost:.2f}"
176
-
177
-
178
- def _render_fork_session_output(command_output: model.CommandOutput) -> RenderableType:
179
- """Render fork session output with usage instructions."""
180
- if not isinstance(command_output.ui_extra, model.SessionIdUIExtra):
181
- return Padding.indent(Text("(no session id)", style=ThemeKey.TOOL_RESULT), level=2)
182
-
183
- grid = Table.grid(padding=(0, 1))
184
- session_id = command_output.ui_extra.session_id
185
- grid.add_column(style=ThemeKey.TOOL_RESULT, overflow="fold")
186
-
187
- grid.add_row(Text("Session forked. Resume command copied to clipboard:", style=ThemeKey.TOOL_RESULT))
188
- grid.add_row(Text(f" klaude --resume-by-id {session_id}", style=ThemeKey.TOOL_RESULT_BOLD))
189
-
190
- return Padding.indent(grid, level=2)
191
-
192
-
193
- def _render_status_output(command_output: model.CommandOutput) -> RenderableType:
194
- """Render session status with total cost and per-model breakdown."""
195
- if not isinstance(command_output.ui_extra, model.SessionStatusUIExtra):
196
- return Text("(no status data)", style=ThemeKey.TOOL_RESULT)
197
-
198
- status = command_output.ui_extra
199
- usage = status.usage
200
-
201
- table = Table.grid(padding=(0, 2))
202
- table.add_column(style=ThemeKey.TOOL_RESULT, overflow="fold")
203
- table.add_column(style=ThemeKey.TOOL_RESULT, overflow="fold")
204
-
205
- # Total cost line
206
- table.add_row(
207
- Text("Total cost:", style=ThemeKey.TOOL_RESULT_BOLD),
208
- Text(_format_cost(usage.total_cost, usage.currency), style=ThemeKey.TOOL_RESULT_BOLD),
209
- )
210
-
211
- # Per-model breakdown
212
- if status.by_model:
213
- table.add_row(Text("Usage by model:", style=ThemeKey.TOOL_RESULT_BOLD), "")
214
- for meta in status.by_model:
215
- model_label = meta.model_name
216
- if meta.provider:
217
- model_label = f"{meta.model_name} ({meta.provider.lower().replace(' ', '-')})"
218
-
219
- if meta.usage:
220
- usage_detail = (
221
- f"{_format_tokens(meta.usage.input_tokens)} input, "
222
- f"{_format_tokens(meta.usage.output_tokens)} output, "
223
- f"{_format_tokens(meta.usage.cached_tokens)} cache read, "
224
- f"{_format_tokens(meta.usage.reasoning_tokens)} thinking, "
225
- f"({_format_cost(meta.usage.total_cost, meta.usage.currency)})"
226
- )
227
- else:
228
- usage_detail = "(no usage data)"
229
- table.add_row(f"{model_label}:", usage_detail)
230
-
231
- return Padding.indent(table, level=2)