klaude-code 1.2.13__py3-none-any.whl → 1.2.15__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/auth_cmd.py +73 -0
- klaude_code/cli/config_cmd.py +88 -0
- klaude_code/cli/debug.py +72 -0
- klaude_code/cli/main.py +31 -142
- klaude_code/cli/runtime.py +0 -31
- klaude_code/cli/session_cmd.py +3 -1
- klaude_code/command/model_cmd.py +1 -1
- klaude_code/command/thinking_cmd.py +1 -1
- klaude_code/config/__init__.py +0 -4
- klaude_code/config/config.py +31 -4
- klaude_code/const/__init__.py +8 -3
- klaude_code/core/agent.py +1 -1
- klaude_code/core/prompt.py +10 -5
- klaude_code/llm/client.py +4 -0
- klaude_code/llm/input_common.py +1 -1
- klaude_code/llm/openrouter/client.py +5 -4
- klaude_code/protocol/events.py +2 -2
- klaude_code/protocol/sub_agent.py +0 -1
- klaude_code/session/export.py +58 -0
- klaude_code/session/session.py +35 -3
- klaude_code/session/templates/export_session.html +54 -12
- klaude_code/trace/__init__.py +2 -2
- klaude_code/trace/log.py +143 -4
- klaude_code/ui/modes/debug/display.py +2 -1
- klaude_code/ui/modes/repl/completers.py +3 -3
- klaude_code/ui/modes/repl/event_handler.py +2 -1
- klaude_code/ui/modes/repl/renderer.py +48 -61
- klaude_code/ui/renderers/metadata.py +49 -88
- klaude_code/ui/renderers/thinking.py +2 -2
- klaude_code/ui/renderers/tools.py +49 -1
- klaude_code/ui/rich/markdown.py +27 -11
- klaude_code/ui/rich/theme.py +13 -13
- {klaude_code-1.2.13.dist-info → klaude_code-1.2.15.dist-info}/METADATA +1 -1
- {klaude_code-1.2.13.dist-info → klaude_code-1.2.15.dist-info}/RECORD +36 -33
- {klaude_code-1.2.13.dist-info → klaude_code-1.2.15.dist-info}/WHEEL +0 -0
- {klaude_code-1.2.13.dist-info → klaude_code-1.2.15.dist-info}/entry_points.txt +0 -0
|
@@ -99,48 +99,21 @@ class REPLRenderer:
|
|
|
99
99
|
def print(self, *objects: Any, style: StyleType | None = None, end: str = "\n") -> None:
|
|
100
100
|
if self.current_sub_agent_color:
|
|
101
101
|
if objects:
|
|
102
|
-
|
|
102
|
+
content = objects[0] if len(objects) == 1 else objects
|
|
103
|
+
self.console.print(Quote(content, style=self.current_sub_agent_color))
|
|
103
104
|
return
|
|
104
105
|
self.console.print(*objects, style=style, end=end)
|
|
105
106
|
|
|
106
107
|
def display_tool_call(self, e: events.ToolCallEvent) -> None:
|
|
107
|
-
# Handle sub-agent tool calls in replay mode
|
|
108
108
|
if r_tools.is_sub_agent_tool(e.tool_name):
|
|
109
|
-
if e.is_replay:
|
|
110
|
-
state = r_sub_agent.build_sub_agent_state_from_tool_call(e)
|
|
111
|
-
if state is not None:
|
|
112
|
-
sub_agent_default_style = (
|
|
113
|
-
self.themes.sub_agent_colors[0] if self.themes.sub_agent_colors else Style()
|
|
114
|
-
)
|
|
115
|
-
self.print(
|
|
116
|
-
Quote(
|
|
117
|
-
r_sub_agent.render_sub_agent_call(state, sub_agent_default_style),
|
|
118
|
-
style=sub_agent_default_style,
|
|
119
|
-
)
|
|
120
|
-
)
|
|
121
109
|
return
|
|
122
|
-
|
|
123
110
|
renderable = r_tools.render_tool_call(e)
|
|
124
111
|
if renderable is not None:
|
|
125
112
|
self.print(renderable)
|
|
126
113
|
|
|
127
114
|
def display_tool_call_result(self, e: events.ToolResultEvent) -> None:
|
|
128
|
-
# Handle sub-agent tool results in replay mode
|
|
129
115
|
if r_tools.is_sub_agent_tool(e.tool_name):
|
|
130
|
-
if e.is_replay:
|
|
131
|
-
sub_agent_default_style = self.themes.sub_agent_colors[0] if self.themes.sub_agent_colors else Style()
|
|
132
|
-
self.print(
|
|
133
|
-
Quote(
|
|
134
|
-
r_sub_agent.render_sub_agent_result(
|
|
135
|
-
e.result,
|
|
136
|
-
code_theme=self.themes.code_theme,
|
|
137
|
-
style=sub_agent_default_style,
|
|
138
|
-
),
|
|
139
|
-
style=sub_agent_default_style,
|
|
140
|
-
)
|
|
141
|
-
)
|
|
142
116
|
return
|
|
143
|
-
|
|
144
117
|
renderable = r_tools.render_tool_result(e)
|
|
145
118
|
if renderable is not None:
|
|
146
119
|
self.print(renderable)
|
|
@@ -160,39 +133,53 @@ class REPLRenderer:
|
|
|
160
133
|
async def replay_history(self, history_events: events.ReplayHistoryEvent) -> None:
|
|
161
134
|
tool_call_dict: dict[str, events.ToolCallEvent] = {}
|
|
162
135
|
for event in history_events.events:
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
self.print(
|
|
136
|
+
event_session_id = getattr(event, "session_id", history_events.session_id)
|
|
137
|
+
is_sub_agent = self.is_sub_agent_session(event_session_id)
|
|
138
|
+
|
|
139
|
+
with self.session_print_context(event_session_id):
|
|
140
|
+
match event:
|
|
141
|
+
case events.TaskStartEvent() as e:
|
|
142
|
+
self.display_task_start(e)
|
|
143
|
+
case events.TurnStartEvent():
|
|
144
|
+
self.print()
|
|
145
|
+
case events.AssistantMessageEvent() as e:
|
|
146
|
+
if is_sub_agent:
|
|
147
|
+
continue
|
|
148
|
+
renderable = r_assistant.render_assistant_message(e.content, code_theme=self.themes.code_theme)
|
|
149
|
+
if renderable is not None:
|
|
150
|
+
self.print(renderable)
|
|
151
|
+
self.print()
|
|
152
|
+
case events.ThinkingEvent() as e:
|
|
153
|
+
if is_sub_agent:
|
|
154
|
+
continue
|
|
155
|
+
self.display_thinking(e.content)
|
|
156
|
+
case events.DeveloperMessageEvent() as e:
|
|
157
|
+
self.display_developer_message(e)
|
|
158
|
+
self.display_command_output(e)
|
|
159
|
+
case events.UserMessageEvent() as e:
|
|
160
|
+
if is_sub_agent:
|
|
161
|
+
continue
|
|
162
|
+
self.print(r_user_input.render_user_input(e.content))
|
|
163
|
+
case events.ToolCallEvent() as e:
|
|
164
|
+
tool_call_dict[e.tool_call_id] = e
|
|
165
|
+
case events.ToolResultEvent() as e:
|
|
166
|
+
tool_call_event = tool_call_dict.get(e.tool_call_id)
|
|
167
|
+
if tool_call_event is not None:
|
|
168
|
+
self.display_tool_call(tool_call_event)
|
|
169
|
+
tool_call_dict.pop(e.tool_call_id, None)
|
|
170
|
+
if is_sub_agent:
|
|
171
|
+
continue
|
|
172
|
+
self.display_tool_call_result(e)
|
|
173
|
+
case events.TaskMetadataEvent() as e:
|
|
174
|
+
self.print(r_metadata.render_task_metadata(e))
|
|
175
|
+
self.print()
|
|
176
|
+
case events.InterruptEvent():
|
|
172
177
|
self.print()
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
case events.UserMessageEvent() as user_event:
|
|
179
|
-
self.print(r_user_input.render_user_input(user_event.content))
|
|
180
|
-
case events.ToolCallEvent() as tool_call_event:
|
|
181
|
-
tool_call_dict[tool_call_event.tool_call_id] = tool_call_event
|
|
182
|
-
case events.ToolResultEvent() as tool_result_event:
|
|
183
|
-
tool_call_event = tool_call_dict.get(tool_result_event.tool_call_id)
|
|
184
|
-
if tool_call_event is not None:
|
|
185
|
-
self.display_tool_call(tool_call_event)
|
|
186
|
-
tool_call_dict.pop(tool_result_event.tool_call_id, None)
|
|
187
|
-
self.display_tool_call_result(tool_result_event)
|
|
188
|
-
case events.TaskMetadataEvent() as metadata_event:
|
|
189
|
-
self.print(r_metadata.render_task_metadata(metadata_event))
|
|
190
|
-
self.print()
|
|
191
|
-
case events.InterruptEvent():
|
|
192
|
-
self.print()
|
|
193
|
-
self.print(r_user_input.render_interrupt())
|
|
194
|
-
case events.ErrorEvent() as e:
|
|
195
|
-
self.display_error(e)
|
|
178
|
+
self.print(r_user_input.render_interrupt())
|
|
179
|
+
case events.ErrorEvent() as e:
|
|
180
|
+
self.display_error(e)
|
|
181
|
+
case events.TaskFinishEvent() as e:
|
|
182
|
+
self.display_task_finish(e)
|
|
196
183
|
|
|
197
184
|
def display_developer_message(self, e: events.DeveloperMessageEvent) -> None:
|
|
198
185
|
if not r_developer.need_render_developer_message(e):
|
|
@@ -9,6 +9,7 @@ from rich.text import Text
|
|
|
9
9
|
|
|
10
10
|
from klaude_code.protocol import events, model
|
|
11
11
|
from klaude_code.trace import is_debug_enabled
|
|
12
|
+
from klaude_code.ui.renderers.common import create_grid
|
|
12
13
|
from klaude_code.ui.rich.theme import ThemeKey
|
|
13
14
|
from klaude_code.ui.utils.common import format_number
|
|
14
15
|
|
|
@@ -24,163 +25,123 @@ def _get_version() -> str:
|
|
|
24
25
|
def _render_task_metadata_block(
|
|
25
26
|
metadata: model.TaskMetadata,
|
|
26
27
|
*,
|
|
27
|
-
|
|
28
|
+
is_sub_agent: bool = False,
|
|
28
29
|
show_context_and_time: bool = True,
|
|
29
|
-
) ->
|
|
30
|
+
) -> RenderableType:
|
|
30
31
|
"""Render a single TaskMetadata block.
|
|
31
32
|
|
|
32
33
|
Args:
|
|
33
34
|
metadata: The TaskMetadata to render.
|
|
34
|
-
|
|
35
|
+
is_sub_agent: Whether this is a sub-agent block.
|
|
35
36
|
show_context_and_time: Whether to show context usage percent and time.
|
|
36
37
|
|
|
37
38
|
Returns:
|
|
38
|
-
|
|
39
|
+
A renderable for this metadata block.
|
|
39
40
|
"""
|
|
41
|
+
grid = create_grid()
|
|
42
|
+
|
|
40
43
|
# Get currency symbol
|
|
41
44
|
currency = metadata.usage.currency if metadata.usage else "USD"
|
|
42
45
|
currency_symbol = "¥" if currency == "CNY" else "$"
|
|
43
46
|
|
|
44
|
-
#
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
)
|
|
50
|
-
model_text = Text()
|
|
51
|
-
model_text.append_text(prefix).append_text(Text(metadata.model_name, style=ThemeKey.METADATA_BOLD))
|
|
47
|
+
# First column: mark only
|
|
48
|
+
mark = Text("└", style=ThemeKey.METADATA_DIM) if is_sub_agent else Text("•", style=ThemeKey.METADATA_BOLD)
|
|
49
|
+
|
|
50
|
+
# Second column: model@provider / tokens / cost / ...
|
|
51
|
+
content = Text()
|
|
52
|
+
content.append_text(Text(metadata.model_name, style=ThemeKey.METADATA_BOLD))
|
|
52
53
|
if metadata.provider is not None:
|
|
53
|
-
|
|
54
|
+
content.append_text(Text("@", style=ThemeKey.METADATA)).append_text(
|
|
54
55
|
Text(metadata.provider.lower().replace(" ", "-"), style=ThemeKey.METADATA)
|
|
55
56
|
)
|
|
56
57
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
# Line 2: Token consumption, Context, TPS, Cost
|
|
60
|
-
parts2: list[Text] = []
|
|
58
|
+
# All info parts (tokens, cost, context, etc.)
|
|
59
|
+
parts: list[Text] = []
|
|
61
60
|
|
|
62
61
|
if metadata.usage is not None:
|
|
63
|
-
#
|
|
64
|
-
|
|
65
|
-
("
|
|
66
|
-
(format_number(metadata.usage.input_tokens), ThemeKey.
|
|
62
|
+
# Tokens: ↑37k c5k ↓907 r45k
|
|
63
|
+
token_parts: list[tuple[str, str]] = [
|
|
64
|
+
("↑", ThemeKey.METADATA_DIM),
|
|
65
|
+
(format_number(metadata.usage.input_tokens), ThemeKey.METADATA),
|
|
67
66
|
]
|
|
68
|
-
if metadata.usage.input_cost is not None:
|
|
69
|
-
input_parts.append((f"({currency_symbol}{metadata.usage.input_cost:.4f})", ThemeKey.METADATA_DIM))
|
|
70
|
-
parts2.append(Text.assemble(*input_parts))
|
|
71
|
-
|
|
72
|
-
# Cached
|
|
73
67
|
if metadata.usage.cached_tokens > 0:
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
if metadata.usage.cache_read_cost is not None:
|
|
79
|
-
cached_parts.append((f"({currency_symbol}{metadata.usage.cache_read_cost:.4f})", ThemeKey.METADATA_DIM))
|
|
80
|
-
parts2.append(Text.assemble(*cached_parts))
|
|
81
|
-
|
|
82
|
-
# Output
|
|
83
|
-
output_parts: list[tuple[str, str]] = [
|
|
84
|
-
("output:", ThemeKey.METADATA_DIM),
|
|
85
|
-
(format_number(metadata.usage.output_tokens), ThemeKey.METADATA_DIM),
|
|
86
|
-
]
|
|
87
|
-
if metadata.usage.output_cost is not None:
|
|
88
|
-
output_parts.append((f"({currency_symbol}{metadata.usage.output_cost:.4f})", ThemeKey.METADATA_DIM))
|
|
89
|
-
parts2.append(Text.assemble(*output_parts))
|
|
90
|
-
|
|
91
|
-
# Reasoning
|
|
68
|
+
token_parts.append((" c", ThemeKey.METADATA_DIM))
|
|
69
|
+
token_parts.append((format_number(metadata.usage.cached_tokens), ThemeKey.METADATA))
|
|
70
|
+
token_parts.append((" ↓", ThemeKey.METADATA_DIM))
|
|
71
|
+
token_parts.append((format_number(metadata.usage.output_tokens), ThemeKey.METADATA))
|
|
92
72
|
if metadata.usage.reasoning_tokens > 0:
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
(":", ThemeKey.METADATA_DIM),
|
|
97
|
-
(
|
|
98
|
-
format_number(metadata.usage.reasoning_tokens),
|
|
99
|
-
ThemeKey.METADATA_DIM,
|
|
100
|
-
),
|
|
101
|
-
)
|
|
102
|
-
)
|
|
73
|
+
token_parts.append((" r", ThemeKey.METADATA_DIM))
|
|
74
|
+
token_parts.append((format_number(metadata.usage.reasoning_tokens), ThemeKey.METADATA))
|
|
75
|
+
parts.append(Text.assemble(*token_parts))
|
|
103
76
|
|
|
104
77
|
# Cost
|
|
105
78
|
if metadata.usage is not None and metadata.usage.total_cost is not None:
|
|
106
|
-
|
|
79
|
+
parts.append(
|
|
107
80
|
Text.assemble(
|
|
108
|
-
(
|
|
109
|
-
("
|
|
110
|
-
(f"{currency_symbol}{metadata.usage.total_cost:.4f}", ThemeKey.METADATA_DIM),
|
|
81
|
+
(currency_symbol, ThemeKey.METADATA_DIM),
|
|
82
|
+
(f"{metadata.usage.total_cost:.4f}", ThemeKey.METADATA),
|
|
111
83
|
)
|
|
112
84
|
)
|
|
113
|
-
if parts2:
|
|
114
|
-
line2 = Text(" / ", style=ThemeKey.METADATA_DIM).join(parts2)
|
|
115
|
-
renderables.append(Padding(line2, (0, 0, 0, indent + 2)))
|
|
116
|
-
|
|
117
|
-
parts3: list[Text] = []
|
|
118
85
|
if metadata.usage is not None:
|
|
119
86
|
# Context (only for main agent)
|
|
120
87
|
if show_context_and_time and metadata.usage.context_usage_percent is not None:
|
|
121
88
|
context_size = format_number(metadata.usage.context_size or 0)
|
|
122
|
-
|
|
89
|
+
parts.append(
|
|
123
90
|
Text.assemble(
|
|
124
|
-
(
|
|
125
|
-
("
|
|
126
|
-
(
|
|
127
|
-
f"{context_size}({metadata.usage.context_usage_percent:.1f}%)",
|
|
128
|
-
ThemeKey.METADATA_DIM,
|
|
129
|
-
),
|
|
91
|
+
(context_size, ThemeKey.METADATA),
|
|
92
|
+
(f" ({metadata.usage.context_usage_percent:.1f}%)", ThemeKey.METADATA_DIM),
|
|
130
93
|
)
|
|
131
94
|
)
|
|
132
95
|
|
|
133
96
|
# TPS
|
|
134
97
|
if metadata.usage.throughput_tps is not None:
|
|
135
|
-
|
|
98
|
+
parts.append(
|
|
136
99
|
Text.assemble(
|
|
100
|
+
(f"{metadata.usage.throughput_tps:.1f} ", ThemeKey.METADATA),
|
|
137
101
|
("tps", ThemeKey.METADATA_DIM),
|
|
138
|
-
(":", ThemeKey.METADATA_DIM),
|
|
139
|
-
(f"{metadata.usage.throughput_tps:.1f}", ThemeKey.METADATA_DIM),
|
|
140
102
|
)
|
|
141
103
|
)
|
|
142
104
|
|
|
143
105
|
# Duration
|
|
144
106
|
if show_context_and_time and metadata.task_duration_s is not None:
|
|
145
|
-
|
|
107
|
+
parts.append(
|
|
146
108
|
Text.assemble(
|
|
147
|
-
("
|
|
148
|
-
("
|
|
149
|
-
(f"{metadata.task_duration_s:.1f}s", ThemeKey.METADATA_DIM),
|
|
109
|
+
(f"{metadata.task_duration_s:.1f}", ThemeKey.METADATA),
|
|
110
|
+
("s", ThemeKey.METADATA_DIM),
|
|
150
111
|
)
|
|
151
112
|
)
|
|
152
113
|
|
|
153
114
|
# Turn count
|
|
154
115
|
if show_context_and_time and metadata.turn_count > 0:
|
|
155
|
-
|
|
116
|
+
parts.append(
|
|
156
117
|
Text.assemble(
|
|
157
|
-
(
|
|
158
|
-
("
|
|
159
|
-
(str(metadata.turn_count), ThemeKey.METADATA_DIM),
|
|
118
|
+
(str(metadata.turn_count), ThemeKey.METADATA),
|
|
119
|
+
(" turns", ThemeKey.METADATA_DIM),
|
|
160
120
|
)
|
|
161
121
|
)
|
|
162
122
|
|
|
163
|
-
if
|
|
164
|
-
|
|
165
|
-
|
|
123
|
+
if parts:
|
|
124
|
+
content.append_text(Text(" · ", style=ThemeKey.METADATA_DIM))
|
|
125
|
+
content.append_text(Text(" · ", style=ThemeKey.METADATA_DIM).join(parts))
|
|
166
126
|
|
|
167
|
-
|
|
127
|
+
grid.add_row(mark, content)
|
|
128
|
+
return grid if not is_sub_agent else Padding(grid, (0, 0, 0, 2))
|
|
168
129
|
|
|
169
130
|
|
|
170
131
|
def render_task_metadata(e: events.TaskMetadataEvent) -> RenderableType:
|
|
171
132
|
"""Render task metadata including main agent and sub-agents, aggregated by model+provider."""
|
|
172
133
|
renderables: list[RenderableType] = []
|
|
173
134
|
|
|
174
|
-
renderables.
|
|
135
|
+
renderables.append(_render_task_metadata_block(e.metadata.main, is_sub_agent=False, show_context_and_time=True))
|
|
175
136
|
|
|
176
137
|
# Aggregate by (model_name, provider), sorted by total_cost descending
|
|
177
138
|
sorted_items = model.TaskMetadata.aggregate_by_model(e.metadata.sub_agent_task_metadata)
|
|
178
139
|
|
|
179
140
|
# Render each aggregated model block
|
|
180
141
|
for meta in sorted_items:
|
|
181
|
-
renderables.
|
|
142
|
+
renderables.append(_render_task_metadata_block(meta, is_sub_agent=True, show_context_and_time=False))
|
|
182
143
|
|
|
183
|
-
return Group(*renderables)
|
|
144
|
+
return Padding(Group(*renderables), (0, 0, 0, 1))
|
|
184
145
|
|
|
185
146
|
|
|
186
147
|
def render_welcome(e: events.WelcomeEvent, *, box_style: Box | None = None) -> RenderableType:
|
|
@@ -2,7 +2,7 @@ from rich.console import RenderableType
|
|
|
2
2
|
from rich.padding import Padding
|
|
3
3
|
from rich.text import Text
|
|
4
4
|
|
|
5
|
-
from klaude_code.ui.rich.markdown import
|
|
5
|
+
from klaude_code.ui.rich.markdown import ThinkingMarkdown
|
|
6
6
|
from klaude_code.ui.rich.theme import ThemeKey
|
|
7
7
|
|
|
8
8
|
|
|
@@ -30,7 +30,7 @@ def render_thinking(content: str, *, code_theme: str, style: str) -> RenderableT
|
|
|
30
30
|
return None
|
|
31
31
|
|
|
32
32
|
return Padding.indent(
|
|
33
|
-
|
|
33
|
+
ThinkingMarkdown(
|
|
34
34
|
_normalize_thinking_content(content),
|
|
35
35
|
code_theme=code_theme,
|
|
36
36
|
style=style,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import json
|
|
2
2
|
from pathlib import Path
|
|
3
|
+
from typing import Any, cast
|
|
3
4
|
|
|
4
5
|
from rich.console import RenderableType
|
|
5
6
|
from rich.padding import Padding
|
|
@@ -58,6 +59,49 @@ def render_generic_tool_call(tool_name: str, arguments: str, markup: str = "•"
|
|
|
58
59
|
return grid
|
|
59
60
|
|
|
60
61
|
|
|
62
|
+
def render_bash_tool_call(arguments: str) -> RenderableType:
|
|
63
|
+
grid = create_grid()
|
|
64
|
+
tool_name_column = Text.assemble((">", ThemeKey.TOOL_MARK), " ", ("Bash", ThemeKey.TOOL_NAME))
|
|
65
|
+
|
|
66
|
+
try:
|
|
67
|
+
payload_raw: Any = json.loads(arguments) if arguments else {}
|
|
68
|
+
except json.JSONDecodeError:
|
|
69
|
+
summary = Text(
|
|
70
|
+
arguments.strip()[: const.INVALID_TOOL_CALL_MAX_LENGTH],
|
|
71
|
+
style=ThemeKey.INVALID_TOOL_CALL_ARGS,
|
|
72
|
+
)
|
|
73
|
+
grid.add_row(tool_name_column, summary)
|
|
74
|
+
return grid
|
|
75
|
+
|
|
76
|
+
if not isinstance(payload_raw, dict):
|
|
77
|
+
summary = Text(
|
|
78
|
+
str(payload_raw)[: const.INVALID_TOOL_CALL_MAX_LENGTH],
|
|
79
|
+
style=ThemeKey.INVALID_TOOL_CALL_ARGS,
|
|
80
|
+
)
|
|
81
|
+
grid.add_row(tool_name_column, summary)
|
|
82
|
+
return grid
|
|
83
|
+
|
|
84
|
+
payload: dict[str, object] = cast(dict[str, object], payload_raw)
|
|
85
|
+
|
|
86
|
+
summary = Text("", ThemeKey.TOOL_PARAM)
|
|
87
|
+
command = payload.get("command")
|
|
88
|
+
timeout_ms = payload.get("timeout_ms")
|
|
89
|
+
|
|
90
|
+
if isinstance(command, str) and command.strip():
|
|
91
|
+
summary.append(command.strip(), style=ThemeKey.TOOL_PARAM)
|
|
92
|
+
|
|
93
|
+
if isinstance(timeout_ms, int):
|
|
94
|
+
if summary:
|
|
95
|
+
summary.append(" ")
|
|
96
|
+
if timeout_ms >= 1000 and timeout_ms % 1000 == 0:
|
|
97
|
+
summary.append(f"{timeout_ms // 1000}s", style=ThemeKey.TOOL_TIMEOUT)
|
|
98
|
+
else:
|
|
99
|
+
summary.append(f"{timeout_ms}ms", style=ThemeKey.TOOL_TIMEOUT)
|
|
100
|
+
|
|
101
|
+
grid.add_row(tool_name_column, summary)
|
|
102
|
+
return grid
|
|
103
|
+
|
|
104
|
+
|
|
61
105
|
def render_update_plan_tool_call(arguments: str) -> RenderableType:
|
|
62
106
|
grid = create_grid()
|
|
63
107
|
tool_name_column = Text.assemble(("◎", ThemeKey.TOOL_MARK), " ", ("Update Plan", ThemeKey.TOOL_NAME))
|
|
@@ -239,6 +283,10 @@ def render_todo(tr: events.ToolResultEvent) -> RenderableType:
|
|
|
239
283
|
mark = "✔"
|
|
240
284
|
mark_style = ThemeKey.TODO_NEW_COMPLETED_MARK if is_new_completed else ThemeKey.TODO_COMPLETED_MARK
|
|
241
285
|
text_style = ThemeKey.TODO_NEW_COMPLETED if is_new_completed else ThemeKey.TODO_COMPLETED
|
|
286
|
+
case _:
|
|
287
|
+
mark = "?"
|
|
288
|
+
mark_style = ThemeKey.TODO_PENDING_MARK
|
|
289
|
+
text_style = ThemeKey.TODO_PENDING
|
|
242
290
|
text = Text(todo.content)
|
|
243
291
|
text.stylize(text_style)
|
|
244
292
|
todo_grid.add_row(Text(mark, style=mark_style), text)
|
|
@@ -457,7 +505,7 @@ def render_tool_call(e: events.ToolCallEvent) -> RenderableType | None:
|
|
|
457
505
|
case tools.MULTI_EDIT:
|
|
458
506
|
return render_multi_edit_tool_call(e.arguments)
|
|
459
507
|
case tools.BASH:
|
|
460
|
-
return
|
|
508
|
+
return render_bash_tool_call(e.arguments)
|
|
461
509
|
case tools.APPLY_PATCH:
|
|
462
510
|
return render_apply_patch_tool_call(e.arguments)
|
|
463
511
|
case tools.TODO_WRITE:
|
klaude_code/ui/rich/markdown.py
CHANGED
|
@@ -4,8 +4,10 @@ from __future__ import annotations
|
|
|
4
4
|
import contextlib
|
|
5
5
|
import io
|
|
6
6
|
import time
|
|
7
|
+
from collections.abc import Callable
|
|
7
8
|
from typing import Any, ClassVar
|
|
8
9
|
|
|
10
|
+
from rich import box
|
|
9
11
|
from rich.console import Console, ConsoleOptions, Group, RenderableType, RenderResult
|
|
10
12
|
from rich.live import Live
|
|
11
13
|
from rich.markdown import CodeBlock, Heading, Markdown
|
|
@@ -32,7 +34,16 @@ class NoInsetCodeBlock(CodeBlock):
|
|
|
32
34
|
word_wrap=True,
|
|
33
35
|
padding=(0, 1),
|
|
34
36
|
)
|
|
35
|
-
yield Panel.fit(syntax, padding=0,
|
|
37
|
+
yield Panel.fit(syntax, padding=(0, 0), style="markdown.code.block", box=box.SIMPLE)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class ThinkingCodeBlock(CodeBlock):
|
|
41
|
+
"""A code block for thinking content that uses grey styling instead of syntax highlighting."""
|
|
42
|
+
|
|
43
|
+
def __rich_console__(self, console: Console, options: ConsoleOptions) -> RenderResult:
|
|
44
|
+
code = str(self.text).rstrip()
|
|
45
|
+
text = Text("```\n" + code + "\n```")
|
|
46
|
+
yield text
|
|
36
47
|
|
|
37
48
|
|
|
38
49
|
class LeftHeading(Heading):
|
|
@@ -41,15 +52,6 @@ class LeftHeading(Heading):
|
|
|
41
52
|
def __rich_console__(self, console: Console, options: ConsoleOptions) -> RenderResult:
|
|
42
53
|
text = self.text
|
|
43
54
|
text.justify = "left" # Override justification
|
|
44
|
-
# if self.tag == "h1":
|
|
45
|
-
# from rich.panel import Panel
|
|
46
|
-
# from rich import box
|
|
47
|
-
# # Draw a border around h1s, but keep text left-aligned
|
|
48
|
-
# yield Panel(
|
|
49
|
-
# text,
|
|
50
|
-
# box=box.SQUARE,
|
|
51
|
-
# style="markdown.h1.border",
|
|
52
|
-
# )
|
|
53
55
|
if self.tag == "h2":
|
|
54
56
|
text.stylize(Style(bold=True, underline=False))
|
|
55
57
|
yield Rule(title=text, characters="-", style="markdown.h2.border", align="left")
|
|
@@ -68,6 +70,17 @@ class NoInsetMarkdown(Markdown):
|
|
|
68
70
|
}
|
|
69
71
|
|
|
70
72
|
|
|
73
|
+
class ThinkingMarkdown(Markdown):
|
|
74
|
+
"""Markdown for thinking content with grey-styled code blocks and left-justified headings."""
|
|
75
|
+
|
|
76
|
+
elements: ClassVar[dict[str, type[Any]]] = {
|
|
77
|
+
**Markdown.elements,
|
|
78
|
+
"fence": ThinkingCodeBlock,
|
|
79
|
+
"code_block": ThinkingCodeBlock,
|
|
80
|
+
"heading_open": LeftHeading,
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
|
|
71
84
|
class MarkdownStream:
|
|
72
85
|
"""Streaming markdown renderer that progressively displays content with a live updating window.
|
|
73
86
|
|
|
@@ -84,6 +97,7 @@ class MarkdownStream:
|
|
|
84
97
|
spinner: Spinner | None = None,
|
|
85
98
|
mark: str | None = None,
|
|
86
99
|
indent: int = 0,
|
|
100
|
+
markdown_class: Callable[..., Markdown] | None = None,
|
|
87
101
|
) -> None:
|
|
88
102
|
"""Initialize the markdown stream.
|
|
89
103
|
|
|
@@ -93,6 +107,7 @@ class MarkdownStream:
|
|
|
93
107
|
console (Console, optional): External console to use for rendering
|
|
94
108
|
mark (str | None, optional): Marker shown before the first non-empty line when indent >= 2
|
|
95
109
|
indent (int, optional): Number of spaces to indent all rendered lines on the left
|
|
110
|
+
markdown_class: Markdown class to use for rendering (defaults to NoInsetMarkdown)
|
|
96
111
|
"""
|
|
97
112
|
self.printed: list[str] = [] # Stores lines that have already been printed
|
|
98
113
|
|
|
@@ -118,6 +133,7 @@ class MarkdownStream:
|
|
|
118
133
|
self.spinner: Spinner | None = spinner
|
|
119
134
|
self.mark: str | None = mark
|
|
120
135
|
self.indent: int = max(indent, 0)
|
|
136
|
+
self.markdown_class: Callable[..., Markdown] = markdown_class or NoInsetMarkdown
|
|
121
137
|
|
|
122
138
|
@property
|
|
123
139
|
def _live_started(self) -> bool:
|
|
@@ -154,7 +170,7 @@ class MarkdownStream:
|
|
|
154
170
|
width=effective_width,
|
|
155
171
|
)
|
|
156
172
|
|
|
157
|
-
markdown =
|
|
173
|
+
markdown = self.markdown_class(text, **self.mdargs)
|
|
158
174
|
temp_console.print(markdown)
|
|
159
175
|
output = string_io.getvalue()
|
|
160
176
|
|
klaude_code/ui/rich/theme.py
CHANGED
|
@@ -14,12 +14,12 @@ class Palette:
|
|
|
14
14
|
blue: str
|
|
15
15
|
orange: str
|
|
16
16
|
magenta: str
|
|
17
|
-
grey_blue: str
|
|
18
17
|
grey1: str
|
|
19
18
|
grey2: str
|
|
20
19
|
grey3: str
|
|
21
20
|
grey_green: str
|
|
22
21
|
purple: str
|
|
22
|
+
lavender: str
|
|
23
23
|
diff_add: str
|
|
24
24
|
diff_remove: str
|
|
25
25
|
code_theme: str
|
|
@@ -31,19 +31,19 @@ LIGHT_PALETTE = Palette(
|
|
|
31
31
|
yellow="yellow",
|
|
32
32
|
green="spring_green4",
|
|
33
33
|
cyan="cyan",
|
|
34
|
-
blue="#
|
|
34
|
+
blue="#3078C5",
|
|
35
35
|
orange="#d77757",
|
|
36
36
|
magenta="magenta",
|
|
37
|
-
grey_blue="steel_blue",
|
|
38
37
|
grey1="#667e90",
|
|
39
38
|
grey2="#93a4b1",
|
|
40
39
|
grey3="#c4ced4",
|
|
41
|
-
grey_green="#
|
|
40
|
+
grey_green="#96a096",
|
|
42
41
|
purple="slate_blue3",
|
|
42
|
+
lavender="steel_blue",
|
|
43
43
|
diff_add="#2e5a32 on #e8f5e9",
|
|
44
44
|
diff_remove="#5a2e32 on #ffebee",
|
|
45
45
|
code_theme="ansi_light",
|
|
46
|
-
text_background="#
|
|
46
|
+
text_background="#e0e0e0",
|
|
47
47
|
)
|
|
48
48
|
|
|
49
49
|
DARK_PALETTE = Palette(
|
|
@@ -54,12 +54,12 @@ DARK_PALETTE = Palette(
|
|
|
54
54
|
blue="deep_sky_blue1",
|
|
55
55
|
orange="#e6704e",
|
|
56
56
|
magenta="magenta",
|
|
57
|
-
grey_blue="steel_blue",
|
|
58
57
|
grey1="#99aabb",
|
|
59
58
|
grey2="#778899",
|
|
60
59
|
grey3="#646464",
|
|
61
60
|
grey_green="#6d8672",
|
|
62
61
|
purple="#afbafe",
|
|
62
|
+
lavender="#9898b8",
|
|
63
63
|
diff_add="#c8e6c9 on #2e4a32",
|
|
64
64
|
diff_remove="#ffcdd2 on #4a2e32",
|
|
65
65
|
code_theme="ansi_dark",
|
|
@@ -107,6 +107,7 @@ class ThemeKey(str, Enum):
|
|
|
107
107
|
TOOL_MARK = "tool.mark"
|
|
108
108
|
TOOL_APPROVED = "tool.approved"
|
|
109
109
|
TOOL_REJECTED = "tool.rejected"
|
|
110
|
+
TOOL_TIMEOUT = "tool.timeout"
|
|
110
111
|
# THINKING
|
|
111
112
|
THINKING = "thinking"
|
|
112
113
|
THINKING_BOLD = "thinking.bold"
|
|
@@ -174,9 +175,9 @@ def get_theme(theme: str | None = None) -> Themes:
|
|
|
174
175
|
ThemeKey.USER_INPUT_AT_PATTERN.value: palette.purple,
|
|
175
176
|
ThemeKey.USER_INPUT_SLASH_COMMAND.value: "bold reverse " + palette.blue,
|
|
176
177
|
# METADATA
|
|
177
|
-
ThemeKey.METADATA.value: palette.
|
|
178
|
-
ThemeKey.METADATA_DIM.value: "dim " + palette.
|
|
179
|
-
ThemeKey.METADATA_BOLD.value: "bold " + palette.
|
|
178
|
+
ThemeKey.METADATA.value: palette.lavender,
|
|
179
|
+
ThemeKey.METADATA_DIM.value: "dim " + palette.lavender,
|
|
180
|
+
ThemeKey.METADATA_BOLD.value: "bold " + palette.lavender,
|
|
180
181
|
# SPINNER_STATUS
|
|
181
182
|
ThemeKey.SPINNER_STATUS.value: palette.blue,
|
|
182
183
|
ThemeKey.SPINNER_STATUS_TEXT.value: palette.blue,
|
|
@@ -196,6 +197,7 @@ def get_theme(theme: str | None = None) -> Themes:
|
|
|
196
197
|
ThemeKey.TOOL_MARK.value: "bold",
|
|
197
198
|
ThemeKey.TOOL_APPROVED.value: palette.green + " bold reverse",
|
|
198
199
|
ThemeKey.TOOL_REJECTED.value: palette.red + " bold reverse",
|
|
200
|
+
ThemeKey.TOOL_TIMEOUT.value: palette.yellow,
|
|
199
201
|
# THINKING
|
|
200
202
|
ThemeKey.THINKING.value: "italic " + palette.grey2,
|
|
201
203
|
ThemeKey.THINKING_BOLD.value: "bold italic " + palette.grey1,
|
|
@@ -232,7 +234,7 @@ def get_theme(theme: str | None = None) -> Themes:
|
|
|
232
234
|
markdown_theme=Theme(
|
|
233
235
|
styles={
|
|
234
236
|
"markdown.code": palette.purple,
|
|
235
|
-
"markdown.code.
|
|
237
|
+
"markdown.code.block": "on " + palette.text_background,
|
|
236
238
|
"markdown.h1": "bold reverse",
|
|
237
239
|
"markdown.h1.border": palette.grey3,
|
|
238
240
|
"markdown.h2.border": palette.grey3,
|
|
@@ -245,8 +247,7 @@ def get_theme(theme: str | None = None) -> Themes:
|
|
|
245
247
|
),
|
|
246
248
|
thinking_markdown_theme=Theme(
|
|
247
249
|
styles={
|
|
248
|
-
"markdown.code": palette.grey1 + " on " + palette.text_background,
|
|
249
|
-
"markdown.code.panel": palette.grey3,
|
|
250
|
+
"markdown.code": palette.grey1 + " italic on " + palette.text_background,
|
|
250
251
|
"markdown.h1": "bold reverse",
|
|
251
252
|
"markdown.h1.border": palette.grey3,
|
|
252
253
|
"markdown.h2.border": palette.grey3,
|
|
@@ -265,7 +266,6 @@ def get_theme(theme: str | None = None) -> Themes:
|
|
|
265
266
|
Style(color=palette.blue),
|
|
266
267
|
Style(color=palette.purple),
|
|
267
268
|
Style(color=palette.orange),
|
|
268
|
-
Style(color=palette.grey_blue),
|
|
269
269
|
Style(color=palette.red),
|
|
270
270
|
Style(color=palette.grey1),
|
|
271
271
|
Style(color=palette.yellow),
|