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.
- klaude_code/app/runtime.py +2 -15
- klaude_code/cli/list_model.py +30 -13
- klaude_code/cli/main.py +26 -10
- klaude_code/config/assets/builtin_config.yaml +177 -310
- klaude_code/config/config.py +158 -21
- klaude_code/config/{select_model.py → model_matcher.py} +41 -16
- klaude_code/config/sub_agent_model_helper.py +217 -0
- klaude_code/config/thinking.py +2 -2
- klaude_code/const.py +1 -1
- klaude_code/core/agent_profile.py +43 -5
- klaude_code/core/executor.py +129 -47
- klaude_code/core/manager/llm_clients_builder.py +17 -11
- klaude_code/core/prompts/prompt-nano-banana.md +1 -1
- klaude_code/core/tool/file/diff_builder.py +25 -18
- klaude_code/core/tool/sub_agent_tool.py +2 -1
- klaude_code/llm/anthropic/client.py +12 -9
- klaude_code/llm/anthropic/input.py +54 -29
- klaude_code/llm/client.py +1 -1
- klaude_code/llm/codex/client.py +2 -2
- klaude_code/llm/google/client.py +7 -7
- klaude_code/llm/google/input.py +23 -2
- klaude_code/llm/input_common.py +2 -2
- klaude_code/llm/openai_compatible/client.py +3 -3
- klaude_code/llm/openai_compatible/input.py +22 -13
- klaude_code/llm/openai_compatible/stream.py +1 -1
- klaude_code/llm/openrouter/client.py +4 -4
- klaude_code/llm/openrouter/input.py +35 -25
- klaude_code/llm/responses/client.py +5 -5
- klaude_code/llm/responses/input.py +96 -57
- klaude_code/protocol/commands.py +1 -2
- klaude_code/protocol/events/__init__.py +7 -1
- klaude_code/protocol/events/chat.py +10 -0
- klaude_code/protocol/events/system.py +4 -0
- klaude_code/protocol/llm_param.py +1 -1
- klaude_code/protocol/model.py +0 -26
- klaude_code/protocol/op.py +17 -5
- 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 +13 -3
- klaude_code/skill/assets/create-plan/SKILL.md +3 -5
- klaude_code/tui/command/__init__.py +3 -6
- klaude_code/tui/command/clear_cmd.py +0 -1
- klaude_code/tui/command/command_abc.py +6 -4
- klaude_code/tui/command/copy_cmd.py +10 -10
- klaude_code/tui/command/debug_cmd.py +11 -10
- klaude_code/tui/command/export_online_cmd.py +18 -23
- klaude_code/tui/command/fork_session_cmd.py +39 -43
- klaude_code/tui/command/model_cmd.py +10 -49
- klaude_code/tui/command/model_picker.py +142 -0
- klaude_code/tui/command/refresh_cmd.py +0 -1
- klaude_code/tui/command/registry.py +15 -21
- klaude_code/tui/command/resume_cmd.py +10 -16
- klaude_code/tui/command/status_cmd.py +8 -12
- klaude_code/tui/command/sub_agent_model_cmd.py +185 -0
- klaude_code/tui/command/terminal_setup_cmd.py +8 -11
- klaude_code/tui/command/thinking_cmd.py +4 -6
- klaude_code/tui/commands.py +5 -0
- klaude_code/tui/components/bash_syntax.py +1 -1
- klaude_code/tui/components/command_output.py +96 -0
- klaude_code/tui/components/common.py +1 -1
- klaude_code/tui/components/developer.py +3 -115
- 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 +19 -8
- klaude_code/tui/machine.py +5 -0
- klaude_code/tui/renderer.py +7 -8
- klaude_code/tui/runner.py +0 -6
- klaude_code/tui/terminal/selector.py +8 -6
- {klaude_code-2.2.0.dist-info → klaude_code-2.4.0.dist-info}/METADATA +21 -74
- {klaude_code-2.2.0.dist-info → klaude_code-2.4.0.dist-info}/RECORD +79 -76
- klaude_code/tui/command/help_cmd.py +0 -51
- klaude_code/tui/command/model_select.py +0 -84
- klaude_code/tui/command/release_notes_cmd.py +0 -85
- {klaude_code-2.2.0.dist-info → klaude_code-2.4.0.dist-info}/WHEEL +0 -0
- {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,
|
|
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
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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.
|
|
196
|
+
events.ErrorEvent(
|
|
199
197
|
session_id=agent.session.id,
|
|
200
|
-
|
|
201
|
-
|
|
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,
|
|
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.
|
|
90
|
+
event = events.CommandOutputEvent(
|
|
91
91
|
session_id=agent.session.id,
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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]
|
|
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.
|
|
100
|
+
event = events.CommandOutputEvent(
|
|
104
101
|
session_id=agent.session.id,
|
|
105
|
-
|
|
106
|
-
|
|
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]
|
|
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.
|
|
141
|
+
event = events.CommandOutputEvent(
|
|
142
142
|
session_id=session.id,
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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]
|
|
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
|
|
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.
|
|
229
|
+
events.CommandOutputEvent(
|
|
230
230
|
session_id=agent.session.id,
|
|
231
|
-
|
|
232
|
-
|
|
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.
|
|
241
|
+
events.CommandOutputEvent(
|
|
244
242
|
session_id=agent.session.id,
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
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,
|
|
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.
|
|
82
|
+
events.CommandOutputEvent(
|
|
83
83
|
session_id=agent.session.id,
|
|
84
|
-
|
|
85
|
-
|
|
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
|
)
|
klaude_code/tui/commands.py
CHANGED
|
@@ -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)
|
|
@@ -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
|
|
7
|
-
from klaude_code.tui.components.common import create_grid
|
|
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
|
|
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)
|