klaude-code 2.3.0__py3-none-any.whl → 2.4.1__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/cli/list_model.py +3 -3
- klaude_code/cli/main.py +2 -2
- klaude_code/config/assets/builtin_config.yaml +165 -307
- klaude_code/config/config.py +17 -17
- klaude_code/config/{select_model.py → model_matcher.py} +7 -7
- klaude_code/config/sub_agent_model_helper.py +1 -10
- klaude_code/config/thinking.py +2 -2
- klaude_code/core/agent_profile.py +9 -23
- klaude_code/core/executor.py +72 -70
- klaude_code/core/tool/file/diff_builder.py +25 -18
- klaude_code/llm/anthropic/client.py +5 -5
- klaude_code/llm/client.py +1 -1
- klaude_code/llm/codex/client.py +2 -2
- klaude_code/llm/google/client.py +6 -6
- klaude_code/llm/input_common.py +2 -2
- klaude_code/llm/openai_compatible/client.py +3 -3
- klaude_code/llm/openai_compatible/stream.py +1 -1
- klaude_code/llm/openrouter/client.py +4 -4
- klaude_code/llm/openrouter/input.py +1 -3
- klaude_code/llm/responses/client.py +5 -5
- klaude_code/protocol/events/__init__.py +7 -1
- klaude_code/protocol/events/chat.py +10 -0
- klaude_code/protocol/llm_param.py +1 -1
- klaude_code/protocol/model.py +0 -26
- klaude_code/protocol/op.py +0 -5
- klaude_code/session/session.py +4 -2
- 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 +5 -7
- klaude_code/tui/command/{model_select.py → model_picker.py} +3 -5
- 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 +11 -16
- 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/command_output.py +96 -0
- klaude_code/tui/components/developer.py +3 -110
- klaude_code/tui/components/welcome.py +2 -2
- klaude_code/tui/input/prompt_toolkit.py +6 -8
- klaude_code/tui/machine.py +5 -0
- klaude_code/tui/renderer.py +5 -5
- klaude_code/tui/runner.py +0 -6
- klaude_code/tui/terminal/selector.py +7 -8
- {klaude_code-2.3.0.dist-info → klaude_code-2.4.1.dist-info}/METADATA +21 -74
- {klaude_code-2.3.0.dist-info → klaude_code-2.4.1.dist-info}/RECORD +54 -53
- {klaude_code-2.3.0.dist-info → klaude_code-2.4.1.dist-info}/WHEEL +0 -0
- {klaude_code-2.3.0.dist-info → klaude_code-2.4.1.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])
|
|
@@ -8,7 +8,7 @@ from prompt_toolkit.styles import Style
|
|
|
8
8
|
|
|
9
9
|
from klaude_code.config.config import load_config
|
|
10
10
|
from klaude_code.config.sub_agent_model_helper import SubAgentModelHelper, SubAgentModelInfo
|
|
11
|
-
from klaude_code.protocol import commands, events, message,
|
|
11
|
+
from klaude_code.protocol import commands, events, message, op
|
|
12
12
|
from klaude_code.tui.terminal.selector import SelectItem, build_model_select_items, select_one
|
|
13
13
|
|
|
14
14
|
from .command_abc import Agent, CommandABC, CommandResult
|
|
@@ -136,12 +136,11 @@ class SubAgentModelCommand(CommandABC):
|
|
|
136
136
|
if not sub_agents:
|
|
137
137
|
return CommandResult(
|
|
138
138
|
events=[
|
|
139
|
-
events.
|
|
139
|
+
events.CommandOutputEvent(
|
|
140
140
|
session_id=agent.session.id,
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
),
|
|
141
|
+
command_name=self.name,
|
|
142
|
+
content="No sub-agents available",
|
|
143
|
+
is_error=True,
|
|
145
144
|
)
|
|
146
145
|
]
|
|
147
146
|
)
|
|
@@ -150,12 +149,10 @@ class SubAgentModelCommand(CommandABC):
|
|
|
150
149
|
if selected_sub_agent is None:
|
|
151
150
|
return CommandResult(
|
|
152
151
|
events=[
|
|
153
|
-
events.
|
|
152
|
+
events.CommandOutputEvent(
|
|
154
153
|
session_id=agent.session.id,
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
ui_extra=model.build_command_output_extra(self.name),
|
|
158
|
-
),
|
|
154
|
+
command_name=self.name,
|
|
155
|
+
content="(cancelled)",
|
|
159
156
|
)
|
|
160
157
|
]
|
|
161
158
|
)
|
|
@@ -166,12 +163,10 @@ class SubAgentModelCommand(CommandABC):
|
|
|
166
163
|
if selected_model is None:
|
|
167
164
|
return CommandResult(
|
|
168
165
|
events=[
|
|
169
|
-
events.
|
|
166
|
+
events.CommandOutputEvent(
|
|
170
167
|
session_id=agent.session.id,
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
ui_extra=model.build_command_output_extra(self.name),
|
|
174
|
-
),
|
|
168
|
+
command_name=self.name,
|
|
169
|
+
content="(cancelled)",
|
|
175
170
|
)
|
|
176
171
|
]
|
|
177
172
|
)
|
|
@@ -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
|
|
@@ -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,29 +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
|
|
4
|
+
from klaude_code.protocol import events, model
|
|
5
|
+
from klaude_code.tui.components.common import create_grid
|
|
8
6
|
from klaude_code.tui.components.rich.theme import ThemeKey
|
|
9
7
|
from klaude_code.tui.components.tools import render_path
|
|
10
8
|
|
|
11
9
|
REMINDER_BULLET = " ⧉"
|
|
12
10
|
|
|
13
11
|
|
|
14
|
-
def get_command_output(item: message.DeveloperMessage) -> model.CommandOutput | None:
|
|
15
|
-
if not item.ui_extra:
|
|
16
|
-
return None
|
|
17
|
-
for ui_item in item.ui_extra.items:
|
|
18
|
-
if isinstance(ui_item, model.CommandOutputUIItem):
|
|
19
|
-
return ui_item.output
|
|
20
|
-
return None
|
|
21
|
-
|
|
22
|
-
|
|
23
12
|
def need_render_developer_message(e: events.DeveloperMessageEvent) -> bool:
|
|
24
13
|
if not e.item.ui_extra:
|
|
25
14
|
return False
|
|
26
|
-
return
|
|
15
|
+
return len(e.item.ui_extra.items) > 0
|
|
27
16
|
|
|
28
17
|
|
|
29
18
|
def render_developer_message(e: events.DeveloperMessageEvent) -> RenderableType:
|
|
@@ -126,101 +115,5 @@ def render_developer_message(e: events.DeveloperMessageEvent) -> RenderableType:
|
|
|
126
115
|
),
|
|
127
116
|
)
|
|
128
117
|
parts.append(grid)
|
|
129
|
-
case model.CommandOutputUIItem():
|
|
130
|
-
# Rendered via render_command_output
|
|
131
|
-
pass
|
|
132
118
|
|
|
133
119
|
return Group(*parts) if parts else Text("")
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
def render_command_output(e: events.DeveloperMessageEvent) -> RenderableType:
|
|
137
|
-
"""Render developer command output content."""
|
|
138
|
-
command_output = get_command_output(e.item)
|
|
139
|
-
if not command_output:
|
|
140
|
-
return Text("")
|
|
141
|
-
|
|
142
|
-
content = message.join_text_parts(e.item.parts)
|
|
143
|
-
match command_output.command_name:
|
|
144
|
-
case commands.CommandName.STATUS:
|
|
145
|
-
return _render_status_output(command_output)
|
|
146
|
-
case commands.CommandName.FORK_SESSION:
|
|
147
|
-
return _render_fork_session_output(command_output)
|
|
148
|
-
case _:
|
|
149
|
-
content = content or "(no content)"
|
|
150
|
-
style = ThemeKey.TOOL_RESULT if not command_output.is_error else ThemeKey.ERROR
|
|
151
|
-
return Padding.indent(truncate_middle(content, base_style=style), level=2)
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
def _format_tokens(tokens: int) -> str:
|
|
155
|
-
"""Format token count with K/M suffix for readability."""
|
|
156
|
-
if tokens >= 1_000_000:
|
|
157
|
-
return f"{tokens / 1_000_000:.2f}M"
|
|
158
|
-
if tokens >= 1_000:
|
|
159
|
-
return f"{tokens / 1_000:.1f}K"
|
|
160
|
-
return str(tokens)
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
def _format_cost(cost: float | None, currency: str = "USD") -> str:
|
|
164
|
-
"""Format cost with currency symbol."""
|
|
165
|
-
if cost is None:
|
|
166
|
-
return "-"
|
|
167
|
-
symbol = "¥" if currency == "CNY" else "$"
|
|
168
|
-
if cost < 0.01:
|
|
169
|
-
return f"{symbol}{cost:.4f}"
|
|
170
|
-
return f"{symbol}{cost:.2f}"
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
def _render_fork_session_output(command_output: model.CommandOutput) -> RenderableType:
|
|
174
|
-
"""Render fork session output with usage instructions."""
|
|
175
|
-
if not isinstance(command_output.ui_extra, model.SessionIdUIExtra):
|
|
176
|
-
return Padding.indent(Text("(no session id)", style=ThemeKey.TOOL_RESULT), level=2)
|
|
177
|
-
|
|
178
|
-
grid = Table.grid(padding=(0, 1))
|
|
179
|
-
session_id = command_output.ui_extra.session_id
|
|
180
|
-
grid.add_column(style=ThemeKey.TOOL_RESULT, overflow="fold")
|
|
181
|
-
|
|
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))
|
|
184
|
-
|
|
185
|
-
return Padding.indent(grid, level=2)
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
def _render_status_output(command_output: model.CommandOutput) -> RenderableType:
|
|
189
|
-
"""Render session status with total cost and per-model breakdown."""
|
|
190
|
-
if not isinstance(command_output.ui_extra, model.SessionStatusUIExtra):
|
|
191
|
-
return Text("(no status data)", style=ThemeKey.TOOL_RESULT)
|
|
192
|
-
|
|
193
|
-
status = command_output.ui_extra
|
|
194
|
-
usage = status.usage
|
|
195
|
-
|
|
196
|
-
table = Table.grid(padding=(0, 2))
|
|
197
|
-
table.add_column(style=ThemeKey.TOOL_RESULT, overflow="fold")
|
|
198
|
-
table.add_column(style=ThemeKey.TOOL_RESULT, overflow="fold")
|
|
199
|
-
|
|
200
|
-
# Total cost line
|
|
201
|
-
table.add_row(
|
|
202
|
-
Text("Total cost:", style=ThemeKey.TOOL_RESULT_BOLD),
|
|
203
|
-
Text(_format_cost(usage.total_cost, usage.currency), style=ThemeKey.TOOL_RESULT_BOLD),
|
|
204
|
-
)
|
|
205
|
-
|
|
206
|
-
# Per-model breakdown
|
|
207
|
-
if status.by_model:
|
|
208
|
-
table.add_row(Text("Usage by model:", style=ThemeKey.TOOL_RESULT_BOLD), "")
|
|
209
|
-
for meta in status.by_model:
|
|
210
|
-
model_label = meta.model_name
|
|
211
|
-
if meta.provider:
|
|
212
|
-
model_label = f"{meta.model_name} ({meta.provider.lower().replace(' ', '-')})"
|
|
213
|
-
|
|
214
|
-
if meta.usage:
|
|
215
|
-
usage_detail = (
|
|
216
|
-
f"{_format_tokens(meta.usage.input_tokens)} input, "
|
|
217
|
-
f"{_format_tokens(meta.usage.output_tokens)} output, "
|
|
218
|
-
f"{_format_tokens(meta.usage.cached_tokens)} cache read, "
|
|
219
|
-
f"{_format_tokens(meta.usage.reasoning_tokens)} thinking, "
|
|
220
|
-
f"({_format_cost(meta.usage.total_cost, meta.usage.currency)})"
|
|
221
|
-
)
|
|
222
|
-
else:
|
|
223
|
-
usage_detail = "(no usage data)"
|
|
224
|
-
table.add_row(f"{model_label}:", usage_detail)
|
|
225
|
-
|
|
226
|
-
return Padding.indent(table, level=2)
|
|
@@ -38,7 +38,7 @@ def render_welcome(e: events.WelcomeEvent) -> RenderableType:
|
|
|
38
38
|
# Model line: model @ provider · params...
|
|
39
39
|
panel_content.append_text(
|
|
40
40
|
Text.assemble(
|
|
41
|
-
(str(e.llm_config.
|
|
41
|
+
(str(e.llm_config.model_id), ThemeKey.WELCOME_HIGHLIGHT),
|
|
42
42
|
(" @ ", ThemeKey.WELCOME_INFO),
|
|
43
43
|
(e.llm_config.provider_name, ThemeKey.WELCOME_INFO),
|
|
44
44
|
)
|
|
@@ -84,7 +84,7 @@ def render_welcome(e: events.WelcomeEvent) -> RenderableType:
|
|
|
84
84
|
(prefix, ThemeKey.LINES),
|
|
85
85
|
(sub_agent_type.lower().ljust(max_type_len), ThemeKey.WELCOME_INFO),
|
|
86
86
|
(": ", ThemeKey.LINES),
|
|
87
|
-
(str(sub_llm_config.
|
|
87
|
+
(str(sub_llm_config.model_id), ThemeKey.WELCOME_HIGHLIGHT),
|
|
88
88
|
(" @ ", ThemeKey.WELCOME_INFO),
|
|
89
89
|
(sub_llm_config.provider_name, ThemeKey.WELCOME_INFO),
|
|
90
90
|
)
|
|
@@ -26,7 +26,7 @@ from prompt_toolkit.styles import Style
|
|
|
26
26
|
from prompt_toolkit.utils import get_cwidth
|
|
27
27
|
|
|
28
28
|
from klaude_code.config import load_config
|
|
29
|
-
from klaude_code.config.
|
|
29
|
+
from klaude_code.config.model_matcher import match_model_from_config
|
|
30
30
|
from klaude_code.config.thinking import (
|
|
31
31
|
format_current_thinking,
|
|
32
32
|
get_thinking_picker_data,
|
|
@@ -452,23 +452,21 @@ class PromptToolkitInput(InputProviderABC):
|
|
|
452
452
|
# -------------------------------------------------------------------------
|
|
453
453
|
|
|
454
454
|
def _build_model_picker_items(self) -> tuple[list[SelectItem[str]], str | None]:
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
config.iter_model_entries(only_available=True),
|
|
458
|
-
key=lambda m: (m.model_name.lower(), m.provider.lower()),
|
|
459
|
-
)
|
|
460
|
-
if not models:
|
|
455
|
+
result = match_model_from_config()
|
|
456
|
+
if result.error_message or not result.filtered_models:
|
|
461
457
|
return [], None
|
|
462
458
|
|
|
463
|
-
items = build_model_select_items(
|
|
459
|
+
items = build_model_select_items(result.filtered_models)
|
|
464
460
|
|
|
465
461
|
initial = None
|
|
466
462
|
if self._get_current_model_config_name is not None:
|
|
467
463
|
with contextlib.suppress(Exception):
|
|
468
464
|
initial = self._get_current_model_config_name()
|
|
469
465
|
if initial is None:
|
|
466
|
+
config = load_config()
|
|
470
467
|
initial = config.main_model
|
|
471
468
|
if isinstance(initial, str) and initial and "@" not in initial:
|
|
469
|
+
config = load_config()
|
|
472
470
|
try:
|
|
473
471
|
resolved = config.resolve_model_location_prefer_available(initial) or config.resolve_model_location(
|
|
474
472
|
initial
|
klaude_code/tui/machine.py
CHANGED
|
@@ -21,6 +21,7 @@ from klaude_code.tui.commands import (
|
|
|
21
21
|
PrintRuleLine,
|
|
22
22
|
RenderAssistantImage,
|
|
23
23
|
RenderCommand,
|
|
24
|
+
RenderCommandOutput,
|
|
24
25
|
RenderDeveloperMessage,
|
|
25
26
|
RenderError,
|
|
26
27
|
RenderInterrupt,
|
|
@@ -365,6 +366,10 @@ class DisplayStateMachine:
|
|
|
365
366
|
cmds.append(RenderDeveloperMessage(e))
|
|
366
367
|
return cmds
|
|
367
368
|
|
|
369
|
+
case events.CommandOutputEvent() as e:
|
|
370
|
+
cmds.append(RenderCommandOutput(e))
|
|
371
|
+
return cmds
|
|
372
|
+
|
|
368
373
|
case events.TurnStartEvent() as e:
|
|
369
374
|
cmds.append(RenderTurnStart(e))
|
|
370
375
|
self._spinner.clear_for_new_turn()
|
klaude_code/tui/renderer.py
CHANGED
|
@@ -31,6 +31,7 @@ from klaude_code.tui.commands import (
|
|
|
31
31
|
PrintRuleLine,
|
|
32
32
|
RenderAssistantImage,
|
|
33
33
|
RenderCommand,
|
|
34
|
+
RenderCommandOutput,
|
|
34
35
|
RenderDeveloperMessage,
|
|
35
36
|
RenderError,
|
|
36
37
|
RenderInterrupt,
|
|
@@ -53,6 +54,7 @@ from klaude_code.tui.commands import (
|
|
|
53
54
|
TaskClockStart,
|
|
54
55
|
)
|
|
55
56
|
from klaude_code.tui.components import assistant as c_assistant
|
|
57
|
+
from klaude_code.tui.components import command_output as c_command_output
|
|
56
58
|
from klaude_code.tui.components import developer as c_developer
|
|
57
59
|
from klaude_code.tui.components import errors as c_errors
|
|
58
60
|
from klaude_code.tui.components import mermaid_viewer as c_mermaid_viewer
|
|
@@ -471,7 +473,6 @@ class TUICommandRenderer:
|
|
|
471
473
|
self.print()
|
|
472
474
|
case events.DeveloperMessageEvent() as e:
|
|
473
475
|
self.display_developer_message(e)
|
|
474
|
-
self.display_command_output(e)
|
|
475
476
|
case events.UserMessageEvent() as e:
|
|
476
477
|
if is_sub_agent:
|
|
477
478
|
continue
|
|
@@ -504,11 +505,9 @@ class TUICommandRenderer:
|
|
|
504
505
|
with self.session_print_context(e.session_id):
|
|
505
506
|
self.print(c_developer.render_developer_message(e))
|
|
506
507
|
|
|
507
|
-
def display_command_output(self, e: events.
|
|
508
|
-
if not c_developer.get_command_output(e.item):
|
|
509
|
-
return
|
|
508
|
+
def display_command_output(self, e: events.CommandOutputEvent) -> None:
|
|
510
509
|
with self.session_print_context(e.session_id):
|
|
511
|
-
self.print(
|
|
510
|
+
self.print(c_command_output.render_command_output(e))
|
|
512
511
|
self.print()
|
|
513
512
|
|
|
514
513
|
def display_welcome(self, event: events.WelcomeEvent) -> None:
|
|
@@ -627,6 +626,7 @@ class TUICommandRenderer:
|
|
|
627
626
|
self.display_task_start(event)
|
|
628
627
|
case RenderDeveloperMessage(event=event):
|
|
629
628
|
self.display_developer_message(event)
|
|
629
|
+
case RenderCommandOutput(event=event):
|
|
630
630
|
self.display_command_output(event)
|
|
631
631
|
case RenderTurnStart(event=event):
|
|
632
632
|
self.display_turn_start(event)
|
klaude_code/tui/runner.py
CHANGED
|
@@ -76,14 +76,8 @@ async def submit_user_input_payload(
|
|
|
76
76
|
if len(run_ops) > 1:
|
|
77
77
|
raise ValueError("Multiple RunAgentOperation results are not supported")
|
|
78
78
|
|
|
79
|
-
for run_op in run_ops:
|
|
80
|
-
run_op.persist_user_input = cmd_result.persist
|
|
81
|
-
run_op.emit_user_message_event = False
|
|
82
|
-
|
|
83
79
|
if cmd_result.events:
|
|
84
80
|
for evt in cmd_result.events:
|
|
85
|
-
if cmd_result.persist and isinstance(evt, events.DeveloperMessageEvent):
|
|
86
|
-
agent.session.append_history([evt.item])
|
|
87
81
|
await executor.context.emit_event(evt)
|
|
88
82
|
|
|
89
83
|
submitted_ids: list[str] = []
|