klaude-code 1.9.0__py3-none-any.whl → 2.0.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/auth/base.py +2 -6
- klaude_code/cli/auth_cmd.py +4 -4
- klaude_code/cli/list_model.py +1 -1
- klaude_code/cli/main.py +1 -1
- klaude_code/cli/runtime.py +7 -5
- klaude_code/cli/self_update.py +1 -1
- klaude_code/cli/session_cmd.py +1 -1
- klaude_code/command/clear_cmd.py +6 -2
- klaude_code/command/command_abc.py +2 -2
- klaude_code/command/debug_cmd.py +4 -4
- klaude_code/command/export_cmd.py +2 -2
- klaude_code/command/export_online_cmd.py +12 -12
- klaude_code/command/fork_session_cmd.py +29 -23
- klaude_code/command/help_cmd.py +4 -4
- klaude_code/command/model_cmd.py +4 -4
- klaude_code/command/model_select.py +1 -1
- klaude_code/command/prompt-commit.md +11 -2
- klaude_code/command/prompt_command.py +3 -3
- klaude_code/command/refresh_cmd.py +2 -2
- klaude_code/command/registry.py +7 -5
- klaude_code/command/release_notes_cmd.py +4 -4
- klaude_code/command/resume_cmd.py +15 -11
- klaude_code/command/status_cmd.py +4 -4
- klaude_code/command/terminal_setup_cmd.py +8 -8
- klaude_code/command/thinking_cmd.py +4 -4
- klaude_code/config/assets/builtin_config.yaml +16 -0
- klaude_code/config/builtin_config.py +16 -5
- klaude_code/config/config.py +7 -2
- klaude_code/const.py +146 -91
- klaude_code/core/agent.py +3 -12
- klaude_code/core/executor.py +21 -13
- klaude_code/core/manager/sub_agent_manager.py +71 -7
- klaude_code/core/prompts/prompt-sub-agent-image-gen.md +1 -0
- klaude_code/core/prompts/prompt-sub-agent-web.md +27 -1
- klaude_code/core/reminders.py +88 -69
- klaude_code/core/task.py +44 -45
- klaude_code/core/tool/file/apply_patch_tool.py +9 -9
- klaude_code/core/tool/file/diff_builder.py +3 -5
- klaude_code/core/tool/file/edit_tool.py +23 -23
- klaude_code/core/tool/file/move_tool.py +43 -43
- klaude_code/core/tool/file/read_tool.py +44 -39
- klaude_code/core/tool/file/write_tool.py +14 -14
- klaude_code/core/tool/report_back_tool.py +4 -4
- klaude_code/core/tool/shell/bash_tool.py +23 -23
- klaude_code/core/tool/skill/skill_tool.py +7 -7
- klaude_code/core/tool/sub_agent_tool.py +38 -9
- klaude_code/core/tool/todo/todo_write_tool.py +8 -8
- klaude_code/core/tool/todo/update_plan_tool.py +6 -6
- klaude_code/core/tool/tool_abc.py +2 -2
- klaude_code/core/tool/tool_context.py +27 -0
- klaude_code/core/tool/tool_runner.py +88 -42
- klaude_code/core/tool/truncation.py +38 -20
- klaude_code/core/tool/web/mermaid_tool.py +6 -7
- klaude_code/core/tool/web/web_fetch_tool.py +68 -30
- klaude_code/core/tool/web/web_search_tool.py +15 -17
- klaude_code/core/turn.py +120 -73
- klaude_code/llm/anthropic/client.py +79 -44
- klaude_code/llm/anthropic/input.py +116 -108
- klaude_code/llm/bedrock/client.py +8 -5
- klaude_code/llm/claude/client.py +18 -8
- klaude_code/llm/client.py +4 -3
- klaude_code/llm/codex/client.py +15 -9
- klaude_code/llm/google/client.py +122 -60
- klaude_code/llm/google/input.py +94 -108
- klaude_code/llm/image.py +123 -0
- klaude_code/llm/input_common.py +136 -189
- klaude_code/llm/openai_compatible/client.py +17 -7
- klaude_code/llm/openai_compatible/input.py +36 -66
- klaude_code/llm/openai_compatible/stream.py +119 -67
- klaude_code/llm/openai_compatible/tool_call_accumulator.py +23 -11
- klaude_code/llm/openrouter/client.py +34 -9
- klaude_code/llm/openrouter/input.py +63 -64
- klaude_code/llm/openrouter/reasoning.py +22 -24
- klaude_code/llm/registry.py +20 -17
- klaude_code/llm/responses/client.py +107 -45
- klaude_code/llm/responses/input.py +115 -98
- klaude_code/llm/usage.py +52 -25
- klaude_code/protocol/__init__.py +1 -0
- klaude_code/protocol/events.py +16 -12
- klaude_code/protocol/llm_param.py +20 -2
- klaude_code/protocol/message.py +250 -0
- klaude_code/protocol/model.py +94 -281
- klaude_code/protocol/op.py +2 -2
- klaude_code/protocol/sub_agent/__init__.py +1 -0
- klaude_code/protocol/sub_agent/explore.py +10 -0
- klaude_code/protocol/sub_agent/image_gen.py +119 -0
- klaude_code/protocol/sub_agent/task.py +10 -0
- klaude_code/protocol/sub_agent/web.py +10 -0
- klaude_code/session/codec.py +6 -6
- klaude_code/session/export.py +261 -62
- klaude_code/session/selector.py +7 -24
- klaude_code/session/session.py +126 -54
- klaude_code/session/store.py +5 -32
- klaude_code/session/templates/export_session.html +1 -1
- klaude_code/session/templates/mermaid_viewer.html +1 -1
- klaude_code/trace/log.py +11 -6
- klaude_code/ui/core/input.py +1 -1
- klaude_code/ui/core/stage_manager.py +1 -8
- klaude_code/ui/modes/debug/display.py +2 -2
- klaude_code/ui/modes/repl/clipboard.py +2 -2
- klaude_code/ui/modes/repl/completers.py +18 -10
- klaude_code/ui/modes/repl/event_handler.py +136 -127
- klaude_code/ui/modes/repl/input_prompt_toolkit.py +1 -1
- klaude_code/ui/modes/repl/key_bindings.py +1 -1
- klaude_code/ui/modes/repl/renderer.py +107 -15
- klaude_code/ui/renderers/assistant.py +2 -2
- klaude_code/ui/renderers/common.py +65 -7
- klaude_code/ui/renderers/developer.py +7 -6
- klaude_code/ui/renderers/diffs.py +11 -11
- klaude_code/ui/renderers/mermaid_viewer.py +49 -2
- klaude_code/ui/renderers/metadata.py +33 -5
- klaude_code/ui/renderers/sub_agent.py +57 -16
- klaude_code/ui/renderers/thinking.py +37 -2
- klaude_code/ui/renderers/tools.py +180 -165
- klaude_code/ui/rich/live.py +3 -1
- klaude_code/ui/rich/markdown.py +39 -7
- klaude_code/ui/rich/quote.py +76 -1
- klaude_code/ui/rich/status.py +14 -8
- klaude_code/ui/rich/theme.py +8 -2
- klaude_code/ui/terminal/image.py +34 -0
- klaude_code/ui/terminal/notifier.py +2 -1
- klaude_code/ui/terminal/progress_bar.py +4 -4
- klaude_code/ui/terminal/selector.py +22 -4
- klaude_code/ui/utils/common.py +11 -2
- {klaude_code-1.9.0.dist-info → klaude_code-2.0.0.dist-info}/METADATA +4 -2
- klaude_code-2.0.0.dist-info/RECORD +229 -0
- klaude_code-1.9.0.dist-info/RECORD +0 -224
- {klaude_code-1.9.0.dist-info → klaude_code-2.0.0.dist-info}/WHEEL +0 -0
- {klaude_code-1.9.0.dist-info → klaude_code-2.0.0.dist-info}/entry_points.txt +0 -0
|
@@ -12,17 +12,22 @@ from rich.spinner import Spinner
|
|
|
12
12
|
from rich.style import Style, StyleType
|
|
13
13
|
from rich.text import Text
|
|
14
14
|
|
|
15
|
-
from klaude_code import
|
|
16
|
-
|
|
15
|
+
from klaude_code.const import (
|
|
16
|
+
MARKDOWN_STREAM_LIVE_REPAINT_ENABLED,
|
|
17
|
+
STATUS_DEFAULT_TEXT,
|
|
18
|
+
STREAM_MAX_HEIGHT_SHRINK_RESET_LINES,
|
|
19
|
+
)
|
|
20
|
+
from klaude_code.protocol import events, model, tools
|
|
17
21
|
from klaude_code.ui.renderers import assistant as r_assistant
|
|
18
22
|
from klaude_code.ui.renderers import developer as r_developer
|
|
19
23
|
from klaude_code.ui.renderers import errors as r_errors
|
|
24
|
+
from klaude_code.ui.renderers import mermaid_viewer as r_mermaid_viewer
|
|
20
25
|
from klaude_code.ui.renderers import metadata as r_metadata
|
|
21
26
|
from klaude_code.ui.renderers import sub_agent as r_sub_agent
|
|
22
27
|
from klaude_code.ui.renderers import thinking as r_thinking
|
|
23
28
|
from klaude_code.ui.renderers import tools as r_tools
|
|
24
29
|
from klaude_code.ui.renderers import user_input as r_user_input
|
|
25
|
-
from klaude_code.ui.renderers.common import
|
|
30
|
+
from klaude_code.ui.renderers.common import truncate_head, truncate_middle
|
|
26
31
|
from klaude_code.ui.rich import status as r_status
|
|
27
32
|
from klaude_code.ui.rich.live import CropAboveLive, SingleLine
|
|
28
33
|
from klaude_code.ui.rich.quote import Quote
|
|
@@ -51,7 +56,7 @@ class REPLRenderer:
|
|
|
51
56
|
self._stream_last_width: int = 0
|
|
52
57
|
self._spinner_visible: bool = False
|
|
53
58
|
|
|
54
|
-
self._status_text: ShimmerStatusText = ShimmerStatusText(
|
|
59
|
+
self._status_text: ShimmerStatusText = ShimmerStatusText(STATUS_DEFAULT_TEXT)
|
|
55
60
|
self._status_spinner: Spinner = BreathingSpinner(
|
|
56
61
|
r_status.spinner_name(),
|
|
57
62
|
text=SingleLine(self._status_text),
|
|
@@ -75,6 +80,13 @@ class REPLRenderer:
|
|
|
75
80
|
def is_sub_agent_session(self, session_id: str) -> bool:
|
|
76
81
|
return session_id in self.session_map and self.session_map[session_id].sub_agent_state is not None
|
|
77
82
|
|
|
83
|
+
def should_display_sub_agent_thinking_header(self, session_id: str) -> bool:
|
|
84
|
+
# Hardcoded: only show sub-agent thinking headers for ImageGen.
|
|
85
|
+
status = self.session_map.get(session_id)
|
|
86
|
+
if status is None or status.sub_agent_state is None:
|
|
87
|
+
return False
|
|
88
|
+
return status.sub_agent_state.sub_agent_type == "ImageGen"
|
|
89
|
+
|
|
78
90
|
def _advance_sub_agent_color_index(self) -> None:
|
|
79
91
|
palette_size = len(self.themes.sub_agent_colors)
|
|
80
92
|
if palette_size == 0:
|
|
@@ -127,10 +139,26 @@ class REPLRenderer:
|
|
|
127
139
|
if renderable is not None:
|
|
128
140
|
self.print(renderable)
|
|
129
141
|
|
|
130
|
-
def display_tool_call_result(self, e: events.ToolResultEvent) -> None:
|
|
142
|
+
def display_tool_call_result(self, e: events.ToolResultEvent, *, is_sub_agent: bool = False) -> None:
|
|
131
143
|
if r_tools.is_sub_agent_tool(e.tool_name):
|
|
132
144
|
return
|
|
133
|
-
|
|
145
|
+
# Sub-agent errors: show only first 2 lines
|
|
146
|
+
if is_sub_agent and e.status == "error":
|
|
147
|
+
error_msg = truncate_head(e.result)
|
|
148
|
+
self.print(r_errors.render_tool_error(error_msg))
|
|
149
|
+
return
|
|
150
|
+
if not is_sub_agent and e.tool_name == tools.MERMAID and isinstance(e.ui_extra, model.MermaidLinkUIExtra):
|
|
151
|
+
image_path = r_mermaid_viewer.download_mermaid_png(
|
|
152
|
+
link=e.ui_extra.link,
|
|
153
|
+
tool_call_id=e.tool_call_id,
|
|
154
|
+
session_id=e.session_id,
|
|
155
|
+
)
|
|
156
|
+
if image_path is not None:
|
|
157
|
+
self.display_image(str(image_path), height=None)
|
|
158
|
+
|
|
159
|
+
renderable = r_tools.render_tool_result(e, code_theme=self.themes.code_theme, session_id=e.session_id)
|
|
160
|
+
else:
|
|
161
|
+
renderable = r_tools.render_tool_result(e, code_theme=self.themes.code_theme, session_id=e.session_id)
|
|
134
162
|
if renderable is not None:
|
|
135
163
|
self.print(renderable)
|
|
136
164
|
|
|
@@ -146,8 +174,26 @@ class REPLRenderer:
|
|
|
146
174
|
self.console.pop_theme()
|
|
147
175
|
self.print()
|
|
148
176
|
|
|
177
|
+
def display_thinking_header(self, header: str) -> None:
|
|
178
|
+
"""Display a single thinking header line.
|
|
179
|
+
|
|
180
|
+
Used by sub-agent sessions to avoid verbose thinking streaming.
|
|
181
|
+
"""
|
|
182
|
+
|
|
183
|
+
stripped = header.strip()
|
|
184
|
+
if not stripped:
|
|
185
|
+
return
|
|
186
|
+
self.print(
|
|
187
|
+
Text.assemble(
|
|
188
|
+
(r_thinking.THINKING_MESSAGE_MARK, ThemeKey.THINKING),
|
|
189
|
+
" ",
|
|
190
|
+
(stripped, ThemeKey.THINKING_BOLD),
|
|
191
|
+
)
|
|
192
|
+
)
|
|
193
|
+
|
|
149
194
|
async def replay_history(self, history_events: events.ReplayHistoryEvent) -> None:
|
|
150
195
|
tool_call_dict: dict[str, events.ToolCallEvent] = {}
|
|
196
|
+
self.print()
|
|
151
197
|
for event in history_events.events:
|
|
152
198
|
event_session_id = getattr(event, "session_id", history_events.session_id)
|
|
153
199
|
is_sub_agent = self.is_sub_agent_session(event_session_id)
|
|
@@ -158,17 +204,23 @@ class REPLRenderer:
|
|
|
158
204
|
self.display_task_start(e)
|
|
159
205
|
case events.TurnStartEvent():
|
|
160
206
|
self.print()
|
|
207
|
+
case events.AssistantImageDeltaEvent() as e:
|
|
208
|
+
self.display_image(e.file_path)
|
|
161
209
|
case events.AssistantMessageEvent() as e:
|
|
162
210
|
if is_sub_agent:
|
|
211
|
+
if self.should_display_sub_agent_thinking_header(event_session_id) and e.thinking_text:
|
|
212
|
+
header = r_thinking.extract_last_bold_header(
|
|
213
|
+
r_thinking.normalize_thinking_content(e.thinking_text)
|
|
214
|
+
)
|
|
215
|
+
if header:
|
|
216
|
+
self.display_thinking_header(header)
|
|
163
217
|
continue
|
|
218
|
+
if e.thinking_text:
|
|
219
|
+
self.display_thinking(e.thinking_text)
|
|
164
220
|
renderable = r_assistant.render_assistant_message(e.content, code_theme=self.themes.code_theme)
|
|
165
221
|
if renderable is not None:
|
|
166
222
|
self.print(renderable)
|
|
167
223
|
self.print()
|
|
168
|
-
case events.ThinkingEvent() as e:
|
|
169
|
-
if is_sub_agent:
|
|
170
|
-
continue
|
|
171
|
-
self.display_thinking(e.content)
|
|
172
224
|
case events.DeveloperMessageEvent() as e:
|
|
173
225
|
self.display_developer_message(e)
|
|
174
226
|
self.display_command_output(e)
|
|
@@ -187,6 +239,7 @@ class REPLRenderer:
|
|
|
187
239
|
continue
|
|
188
240
|
self.display_tool_call_result(e)
|
|
189
241
|
case events.TaskMetadataEvent() as e:
|
|
242
|
+
self.print()
|
|
190
243
|
self.print(r_metadata.render_task_metadata(e))
|
|
191
244
|
self.print()
|
|
192
245
|
case events.InterruptEvent():
|
|
@@ -237,6 +290,37 @@ class REPLRenderer:
|
|
|
237
290
|
self.print(renderable)
|
|
238
291
|
self.print()
|
|
239
292
|
|
|
293
|
+
def display_image(self, file_path: str, height: int | None = 40) -> None:
|
|
294
|
+
"""Display an image in the terminal.
|
|
295
|
+
|
|
296
|
+
Args:
|
|
297
|
+
file_path: Path to the image file.
|
|
298
|
+
height: Height in terminal lines for displaying the image.
|
|
299
|
+
"""
|
|
300
|
+
from klaude_code.ui.terminal.image import print_kitty_image
|
|
301
|
+
|
|
302
|
+
# Suspend the Live status bar while emitting raw terminal output to avoid
|
|
303
|
+
# interleaving refreshes with Kitty graphics escape sequences.
|
|
304
|
+
had_live = self._bottom_live is not None
|
|
305
|
+
was_spinner_visible = self._spinner_visible
|
|
306
|
+
has_stream = MARKDOWN_STREAM_LIVE_REPAINT_ENABLED and self._stream_renderable is not None
|
|
307
|
+
resume_live = had_live and (was_spinner_visible or has_stream)
|
|
308
|
+
|
|
309
|
+
if self._bottom_live is not None:
|
|
310
|
+
with contextlib.suppress(Exception):
|
|
311
|
+
self._bottom_live.stop()
|
|
312
|
+
self._bottom_live = None
|
|
313
|
+
|
|
314
|
+
try:
|
|
315
|
+
print_kitty_image(file_path, height=height, file=self.console.file)
|
|
316
|
+
finally:
|
|
317
|
+
if resume_live:
|
|
318
|
+
if was_spinner_visible:
|
|
319
|
+
self.spinner_start()
|
|
320
|
+
else:
|
|
321
|
+
self._ensure_bottom_live_started()
|
|
322
|
+
self._refresh_bottom_live()
|
|
323
|
+
|
|
240
324
|
def display_task_metadata(self, event: events.TaskMetadataEvent) -> None:
|
|
241
325
|
with self.session_print_context(event.session_id):
|
|
242
326
|
self.print(r_metadata.render_task_metadata(event))
|
|
@@ -268,9 +352,9 @@ class REPLRenderer:
|
|
|
268
352
|
def display_error(self, event: events.ErrorEvent) -> None:
|
|
269
353
|
if event.session_id:
|
|
270
354
|
with self.session_print_context(event.session_id):
|
|
271
|
-
self.print(r_errors.render_error(
|
|
355
|
+
self.print(r_errors.render_error(truncate_middle(event.error_message)))
|
|
272
356
|
else:
|
|
273
|
-
self.print(r_errors.render_error(
|
|
357
|
+
self.print(r_errors.render_error(truncate_middle(event.error_message)))
|
|
274
358
|
|
|
275
359
|
# -------------------------------------------------------------------------
|
|
276
360
|
# Spinner control methods
|
|
@@ -314,7 +398,11 @@ class REPLRenderer:
|
|
|
314
398
|
height = len(self.console.render_lines(renderable, self.console.options, pad=False))
|
|
315
399
|
self._stream_last_height = height
|
|
316
400
|
self._stream_last_width = self.console.size.width
|
|
317
|
-
|
|
401
|
+
|
|
402
|
+
if self._stream_max_height - height > STREAM_MAX_HEIGHT_SHRINK_RESET_LINES:
|
|
403
|
+
self._stream_max_height = height
|
|
404
|
+
else:
|
|
405
|
+
self._stream_max_height = max(self._stream_max_height, height)
|
|
318
406
|
self._refresh_bottom_live()
|
|
319
407
|
|
|
320
408
|
def _ensure_bottom_live_started(self) -> None:
|
|
@@ -334,7 +422,7 @@ class REPLRenderer:
|
|
|
334
422
|
stream_part: RenderableType = Group()
|
|
335
423
|
gap_part: RenderableType = Group()
|
|
336
424
|
|
|
337
|
-
if
|
|
425
|
+
if MARKDOWN_STREAM_LIVE_REPAINT_ENABLED:
|
|
338
426
|
stream = self._stream_renderable
|
|
339
427
|
if stream is not None:
|
|
340
428
|
current_width = self.console.size.width
|
|
@@ -342,7 +430,11 @@ class REPLRenderer:
|
|
|
342
430
|
height = len(self.console.render_lines(stream, self.console.options, pad=False))
|
|
343
431
|
self._stream_last_height = height
|
|
344
432
|
self._stream_last_width = current_width
|
|
345
|
-
|
|
433
|
+
|
|
434
|
+
if self._stream_max_height - height > STREAM_MAX_HEIGHT_SHRINK_RESET_LINES:
|
|
435
|
+
self._stream_max_height = height
|
|
436
|
+
else:
|
|
437
|
+
self._stream_max_height = max(self._stream_max_height, height)
|
|
346
438
|
else:
|
|
347
439
|
height = self._stream_last_height
|
|
348
440
|
|
|
@@ -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 import
|
|
5
|
+
from klaude_code.const import MARKDOWN_RIGHT_MARGIN
|
|
6
6
|
from klaude_code.ui.renderers.common import create_grid
|
|
7
7
|
from klaude_code.ui.rich.markdown import NoInsetMarkdown
|
|
8
8
|
from klaude_code.ui.rich.theme import ThemeKey
|
|
@@ -23,6 +23,6 @@ def render_assistant_message(content: str, *, code_theme: str) -> RenderableType
|
|
|
23
23
|
grid = create_grid()
|
|
24
24
|
grid.add_row(
|
|
25
25
|
Text(ASSISTANT_MESSAGE_MARK, style=ThemeKey.ASSISTANT_MESSAGE_MARK),
|
|
26
|
-
Padding(NoInsetMarkdown(stripped, code_theme=code_theme), (0,
|
|
26
|
+
Padding(NoInsetMarkdown(stripped, code_theme=code_theme), (0, MARKDOWN_RIGHT_MARGIN, 0, 0)),
|
|
27
27
|
)
|
|
28
28
|
return grid
|
|
@@ -2,7 +2,13 @@ from rich.style import Style
|
|
|
2
2
|
from rich.table import Table
|
|
3
3
|
from rich.text import Text
|
|
4
4
|
|
|
5
|
-
from klaude_code import
|
|
5
|
+
from klaude_code.const import (
|
|
6
|
+
MIN_HIDDEN_LINES_FOR_INDICATOR,
|
|
7
|
+
TAB_EXPAND_WIDTH,
|
|
8
|
+
TRUNCATE_DISPLAY_MAX_LINE_LENGTH,
|
|
9
|
+
TRUNCATE_DISPLAY_MAX_LINES,
|
|
10
|
+
TRUNCATE_HEAD_MAX_LINES,
|
|
11
|
+
)
|
|
6
12
|
from klaude_code.ui.rich.theme import ThemeKey
|
|
7
13
|
|
|
8
14
|
|
|
@@ -13,10 +19,10 @@ def create_grid() -> Table:
|
|
|
13
19
|
return grid
|
|
14
20
|
|
|
15
21
|
|
|
16
|
-
def
|
|
22
|
+
def truncate_middle(
|
|
17
23
|
text: str,
|
|
18
|
-
max_lines: int =
|
|
19
|
-
max_line_length: int =
|
|
24
|
+
max_lines: int = TRUNCATE_DISPLAY_MAX_LINES,
|
|
25
|
+
max_line_length: int = TRUNCATE_DISPLAY_MAX_LINE_LENGTH,
|
|
20
26
|
*,
|
|
21
27
|
base_style: str | Style | None = None,
|
|
22
28
|
) -> Text:
|
|
@@ -25,7 +31,7 @@ def truncate_display(
|
|
|
25
31
|
Applies `ThemeKey.TOOL_RESULT_TRUNCATED` style to truncation indicators.
|
|
26
32
|
"""
|
|
27
33
|
# Expand tabs to spaces to ensure correct alignment when Rich applies padding.
|
|
28
|
-
text = text.expandtabs(
|
|
34
|
+
text = text.expandtabs(TAB_EXPAND_WIDTH)
|
|
29
35
|
|
|
30
36
|
if max_lines <= 0:
|
|
31
37
|
truncated_lines = text.split("\n")
|
|
@@ -42,7 +48,7 @@ def truncate_display(
|
|
|
42
48
|
|
|
43
49
|
# If the hidden section is too small, show everything instead of inserting
|
|
44
50
|
# the "(more N lines)" indicator.
|
|
45
|
-
if truncated_lines <
|
|
51
|
+
if truncated_lines < MIN_HIDDEN_LINES_FOR_INDICATOR:
|
|
46
52
|
truncated_lines = 0
|
|
47
53
|
head_lines = lines
|
|
48
54
|
else:
|
|
@@ -59,7 +65,7 @@ def truncate_display(
|
|
|
59
65
|
out.append(line[:max_line_length])
|
|
60
66
|
out.append_text(
|
|
61
67
|
Text(
|
|
62
|
-
f"
|
|
68
|
+
f"… (more {extra_chars} characters in this line)",
|
|
63
69
|
style=ThemeKey.TOOL_RESULT_TRUNCATED,
|
|
64
70
|
)
|
|
65
71
|
)
|
|
@@ -84,3 +90,55 @@ def truncate_display(
|
|
|
84
90
|
out.append("\n")
|
|
85
91
|
|
|
86
92
|
return out
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def truncate_head(
|
|
96
|
+
text: str,
|
|
97
|
+
max_lines: int = TRUNCATE_HEAD_MAX_LINES,
|
|
98
|
+
max_line_length: int = TRUNCATE_DISPLAY_MAX_LINE_LENGTH,
|
|
99
|
+
*,
|
|
100
|
+
base_style: str | Style | None = None,
|
|
101
|
+
truncated_style: str | Style | None = None,
|
|
102
|
+
) -> Text:
|
|
103
|
+
"""Truncate text to show only the first N lines."""
|
|
104
|
+
text = text.expandtabs(TAB_EXPAND_WIDTH)
|
|
105
|
+
lines = [line for line in text.split("\n") if line.strip()]
|
|
106
|
+
|
|
107
|
+
out = Text()
|
|
108
|
+
if base_style is not None:
|
|
109
|
+
out.style = base_style
|
|
110
|
+
|
|
111
|
+
if len(lines) <= max_lines:
|
|
112
|
+
for idx, line in enumerate(lines):
|
|
113
|
+
if len(line) > max_line_length:
|
|
114
|
+
out.append(line[:max_line_length])
|
|
115
|
+
out.append_text(
|
|
116
|
+
Text(
|
|
117
|
+
f" … (more {len(line) - max_line_length} characters)",
|
|
118
|
+
style=truncated_style or ThemeKey.TOOL_RESULT_TRUNCATED,
|
|
119
|
+
)
|
|
120
|
+
)
|
|
121
|
+
else:
|
|
122
|
+
out.append(line)
|
|
123
|
+
if idx < len(lines) - 1:
|
|
124
|
+
out.append("\n")
|
|
125
|
+
return out
|
|
126
|
+
|
|
127
|
+
for idx in range(max_lines):
|
|
128
|
+
line = lines[idx]
|
|
129
|
+
if len(line) > max_line_length:
|
|
130
|
+
out.append(line[:max_line_length])
|
|
131
|
+
out.append_text(
|
|
132
|
+
Text(
|
|
133
|
+
f" … (more {len(line) - max_line_length} characters)",
|
|
134
|
+
style=truncated_style or ThemeKey.TOOL_RESULT_TRUNCATED,
|
|
135
|
+
)
|
|
136
|
+
)
|
|
137
|
+
else:
|
|
138
|
+
out.append(line)
|
|
139
|
+
out.append("\n")
|
|
140
|
+
|
|
141
|
+
remaining = len(lines) - max_lines
|
|
142
|
+
out.append_text(Text(f"… more {remaining} lines", style=truncated_style or ThemeKey.TOOL_RESULT_TRUNCATED))
|
|
143
|
+
|
|
144
|
+
return out
|
|
@@ -3,8 +3,8 @@ from rich.padding import Padding
|
|
|
3
3
|
from rich.table import Table
|
|
4
4
|
from rich.text import Text
|
|
5
5
|
|
|
6
|
-
from klaude_code.protocol import commands, events, model
|
|
7
|
-
from klaude_code.ui.renderers.common import create_grid,
|
|
6
|
+
from klaude_code.protocol import commands, events, message, model
|
|
7
|
+
from klaude_code.ui.renderers.common import create_grid, truncate_middle
|
|
8
8
|
from klaude_code.ui.renderers.tools import render_path
|
|
9
9
|
from klaude_code.ui.rich.markdown import NoInsetMarkdown
|
|
10
10
|
from klaude_code.ui.rich.theme import ThemeKey
|
|
@@ -124,19 +124,20 @@ def render_command_output(e: events.DeveloperMessageEvent) -> RenderableType:
|
|
|
124
124
|
if not e.item.command_output:
|
|
125
125
|
return Text("")
|
|
126
126
|
|
|
127
|
+
content = message.join_text_parts(e.item.parts)
|
|
127
128
|
match e.item.command_output.command_name:
|
|
128
129
|
case commands.CommandName.HELP:
|
|
129
|
-
return Padding.indent(Text.from_markup(
|
|
130
|
+
return Padding.indent(Text.from_markup(content or ""), level=2)
|
|
130
131
|
case commands.CommandName.STATUS:
|
|
131
132
|
return _render_status_output(e.item.command_output)
|
|
132
133
|
case commands.CommandName.RELEASE_NOTES:
|
|
133
|
-
return Padding.indent(NoInsetMarkdown(
|
|
134
|
+
return Padding.indent(NoInsetMarkdown(content or ""), level=2)
|
|
134
135
|
case commands.CommandName.FORK_SESSION:
|
|
135
136
|
return _render_fork_session_output(e.item.command_output)
|
|
136
137
|
case _:
|
|
137
|
-
content =
|
|
138
|
+
content = content or "(no content)"
|
|
138
139
|
style = ThemeKey.TOOL_RESULT if not e.item.command_output.is_error else ThemeKey.ERROR
|
|
139
|
-
return Padding.indent(
|
|
140
|
+
return Padding.indent(truncate_middle(content, base_style=style), level=2)
|
|
140
141
|
|
|
141
142
|
|
|
142
143
|
def _format_tokens(tokens: int) -> str:
|
|
@@ -4,7 +4,7 @@ from rich.padding import Padding
|
|
|
4
4
|
from rich.panel import Panel
|
|
5
5
|
from rich.text import Text
|
|
6
6
|
|
|
7
|
-
from klaude_code import
|
|
7
|
+
from klaude_code.const import DIFF_PREFIX_WIDTH, MAX_DIFF_LINES
|
|
8
8
|
from klaude_code.protocol import model
|
|
9
9
|
from klaude_code.ui.renderers.common import create_grid
|
|
10
10
|
from klaude_code.ui.rich.theme import ThemeKey
|
|
@@ -65,7 +65,7 @@ def render_diff(diff_text: str, show_file_name: bool = False) -> RenderableType:
|
|
|
65
65
|
elif line.strip(): # Non-empty line in untracked section
|
|
66
66
|
file_text = Text(line.strip(), style=ThemeKey.TOOL_PARAM_BOLD)
|
|
67
67
|
grid.add_row(
|
|
68
|
-
Text(f"{'+':>{
|
|
68
|
+
Text(f"{'+':>{DIFF_PREFIX_WIDTH}}", style=ThemeKey.TOOL_PARAM_BOLD),
|
|
69
69
|
file_text,
|
|
70
70
|
)
|
|
71
71
|
continue
|
|
@@ -130,7 +130,7 @@ def render_diff(diff_text: str, show_file_name: bool = False) -> RenderableType:
|
|
|
130
130
|
file_mark = "±"
|
|
131
131
|
|
|
132
132
|
grid.add_row(
|
|
133
|
-
Text(f"{file_mark:>{
|
|
133
|
+
Text(f"{file_mark:>{DIFF_PREFIX_WIDTH}} ", style=ThemeKey.DIFF_FILE_NAME),
|
|
134
134
|
file_line,
|
|
135
135
|
)
|
|
136
136
|
has_rendered_file_header = True
|
|
@@ -151,7 +151,7 @@ def render_diff(diff_text: str, show_file_name: bool = False) -> RenderableType:
|
|
|
151
151
|
except (IndexError, ValueError):
|
|
152
152
|
new_ln = None
|
|
153
153
|
if has_rendered_diff_content:
|
|
154
|
-
grid.add_row(Text(f"{'⋮':>{
|
|
154
|
+
grid.add_row(Text(f"{'⋮':>{DIFF_PREFIX_WIDTH}}", style=ThemeKey.TOOL_RESULT), "")
|
|
155
155
|
continue
|
|
156
156
|
|
|
157
157
|
# Skip +++ lines (already handled above)
|
|
@@ -159,12 +159,12 @@ def render_diff(diff_text: str, show_file_name: bool = False) -> RenderableType:
|
|
|
159
159
|
continue
|
|
160
160
|
|
|
161
161
|
# Only handle unified diff hunk lines; ignore other metadata like
|
|
162
|
-
# "diff --git" or "index
|
|
162
|
+
# "diff --git" or "index …" which would otherwise skew counters.
|
|
163
163
|
if not line or line[:1] not in {" ", "+", "-"}:
|
|
164
164
|
continue
|
|
165
165
|
|
|
166
166
|
# Compute line number prefix and style diff content
|
|
167
|
-
prefix, new_ln = _make_diff_prefix(line, new_ln,
|
|
167
|
+
prefix, new_ln = _make_diff_prefix(line, new_ln, DIFF_PREFIX_WIDTH)
|
|
168
168
|
|
|
169
169
|
if line.startswith("-"):
|
|
170
170
|
text = Text(line[1:])
|
|
@@ -197,7 +197,7 @@ def render_structured_diff(ui_extra: model.DiffUIExtra, show_file_name: bool = F
|
|
|
197
197
|
grid.add_row(*_render_file_header(file_diff))
|
|
198
198
|
|
|
199
199
|
for line in file_diff.lines:
|
|
200
|
-
prefix = _make_structured_prefix(line,
|
|
200
|
+
prefix = _make_structured_prefix(line, DIFF_PREFIX_WIDTH)
|
|
201
201
|
text = _render_structured_line(line)
|
|
202
202
|
grid.add_row(Text(prefix, ThemeKey.TOOL_RESULT), text)
|
|
203
203
|
|
|
@@ -213,9 +213,9 @@ def render_diff_panel(
|
|
|
213
213
|
) -> RenderableType:
|
|
214
214
|
lines = diff_text.splitlines()
|
|
215
215
|
truncated_notice: Text | None = None
|
|
216
|
-
if len(lines) >
|
|
217
|
-
truncated_lines = len(lines) -
|
|
218
|
-
diff_text = "\n".join(lines[:
|
|
216
|
+
if len(lines) > MAX_DIFF_LINES:
|
|
217
|
+
truncated_lines = len(lines) - MAX_DIFF_LINES
|
|
218
|
+
diff_text = "\n".join(lines[:MAX_DIFF_LINES])
|
|
219
219
|
truncated_notice = Text(f"… truncated {truncated_lines} lines", style=ThemeKey.TOOL_MARK)
|
|
220
220
|
|
|
221
221
|
diff_body = render_diff(diff_text, show_file_name=show_file_name)
|
|
@@ -261,7 +261,7 @@ def _render_file_header(file_diff: model.DiffFileDiff) -> tuple[Text, Text]:
|
|
|
261
261
|
else:
|
|
262
262
|
file_mark = "±"
|
|
263
263
|
|
|
264
|
-
prefix = Text(f"{file_mark:>{
|
|
264
|
+
prefix = Text(f"{file_mark:>{DIFF_PREFIX_WIDTH}} ", style=ThemeKey.DIFF_FILE_NAME)
|
|
265
265
|
return prefix, file_line
|
|
266
266
|
|
|
267
267
|
|
|
@@ -5,11 +5,58 @@ import importlib.resources
|
|
|
5
5
|
from functools import lru_cache
|
|
6
6
|
from pathlib import Path
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
import httpx
|
|
9
|
+
|
|
10
|
+
from klaude_code.const import TOOL_OUTPUT_TRUNCATION_DIR
|
|
11
|
+
from klaude_code.llm.image import get_assistant_image_output_dir
|
|
12
|
+
|
|
13
|
+
_MERMAID_INK_PREFIX = "https://mermaid.ink/img/pako:"
|
|
14
|
+
_MERMAID_DEFAULT_PNG_WIDTH = 1600
|
|
15
|
+
_MERMAID_DEFAULT_PNG_SCALE = 2
|
|
9
16
|
|
|
10
17
|
|
|
11
18
|
def artifacts_dir() -> Path:
|
|
12
|
-
return Path(
|
|
19
|
+
return Path(TOOL_OUTPUT_TRUNCATION_DIR) / "mermaid"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _extract_pako_from_link(link: str) -> str | None:
|
|
23
|
+
"""Extract pako encoded string from mermaid.live link."""
|
|
24
|
+
# link format: https://mermaid.live/view#pako:xxxx
|
|
25
|
+
if "#pako:" not in link:
|
|
26
|
+
return None
|
|
27
|
+
return link.split("#pako:", 1)[1]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def download_mermaid_png(
|
|
31
|
+
*,
|
|
32
|
+
link: str,
|
|
33
|
+
tool_call_id: str,
|
|
34
|
+
session_id: str | None = None,
|
|
35
|
+
) -> Path | None:
|
|
36
|
+
"""Download PNG image from mermaid.ink and save locally."""
|
|
37
|
+
pako = _extract_pako_from_link(link)
|
|
38
|
+
if not pako:
|
|
39
|
+
return None
|
|
40
|
+
|
|
41
|
+
safe_id = tool_call_id.replace("/", "_")
|
|
42
|
+
output_dir = get_assistant_image_output_dir(session_id)
|
|
43
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
44
|
+
image_path = output_dir / f"mermaid-{safe_id}.png"
|
|
45
|
+
|
|
46
|
+
if image_path.exists():
|
|
47
|
+
return image_path
|
|
48
|
+
|
|
49
|
+
png_url = (
|
|
50
|
+
f"{_MERMAID_INK_PREFIX}{pako}?type=png&width={_MERMAID_DEFAULT_PNG_WIDTH}&scale={_MERMAID_DEFAULT_PNG_SCALE}"
|
|
51
|
+
)
|
|
52
|
+
try:
|
|
53
|
+
with httpx.Client(timeout=10.0) as client:
|
|
54
|
+
resp = client.get(png_url)
|
|
55
|
+
resp.raise_for_status()
|
|
56
|
+
image_path.write_bytes(resp.content)
|
|
57
|
+
return image_path
|
|
58
|
+
except Exception:
|
|
59
|
+
return None
|
|
13
60
|
|
|
14
61
|
|
|
15
62
|
@lru_cache(maxsize=1)
|
|
@@ -6,7 +6,7 @@ from rich.padding import Padding
|
|
|
6
6
|
from rich.panel import Panel
|
|
7
7
|
from rich.text import Text
|
|
8
8
|
|
|
9
|
-
from klaude_code import
|
|
9
|
+
from klaude_code.const import DEFAULT_MAX_TOKENS
|
|
10
10
|
from klaude_code.protocol import events, model
|
|
11
11
|
from klaude_code.trace import is_debug_enabled
|
|
12
12
|
from klaude_code.ui.renderers.common import create_grid
|
|
@@ -47,7 +47,7 @@ def _render_task_metadata_block(
|
|
|
47
47
|
# First column: mark only
|
|
48
48
|
mark = Text("└", style=ThemeKey.METADATA_DIM) if is_sub_agent else Text("⇅", style=ThemeKey.METADATA)
|
|
49
49
|
|
|
50
|
-
# Second column: model@provider / tokens / cost /
|
|
50
|
+
# Second column: model@provider / tokens / cost / …
|
|
51
51
|
content = Text()
|
|
52
52
|
content.append_text(Text(metadata.model_name, style=ThemeKey.METADATA_BOLD))
|
|
53
53
|
if metadata.provider is not None:
|
|
@@ -82,6 +82,13 @@ def _render_task_metadata_block(
|
|
|
82
82
|
(format_number(metadata.usage.reasoning_tokens), ThemeKey.METADATA),
|
|
83
83
|
)
|
|
84
84
|
)
|
|
85
|
+
if metadata.usage.image_tokens > 0:
|
|
86
|
+
token_parts.append(
|
|
87
|
+
Text.assemble(
|
|
88
|
+
("image ", ThemeKey.METADATA_DIM),
|
|
89
|
+
(format_number(metadata.usage.image_tokens), ThemeKey.METADATA),
|
|
90
|
+
)
|
|
91
|
+
)
|
|
85
92
|
parts.append(Text(" · ").join(token_parts))
|
|
86
93
|
|
|
87
94
|
# Cost
|
|
@@ -97,9 +104,7 @@ def _render_task_metadata_block(
|
|
|
97
104
|
if show_context_and_time and metadata.usage.context_usage_percent is not None:
|
|
98
105
|
context_size = format_number(metadata.usage.context_size or 0)
|
|
99
106
|
# Calculate effective limit (same as Usage.context_usage_percent)
|
|
100
|
-
effective_limit = (metadata.usage.context_limit or 0) - (
|
|
101
|
-
metadata.usage.max_tokens or const.DEFAULT_MAX_TOKENS
|
|
102
|
-
)
|
|
107
|
+
effective_limit = (metadata.usage.context_limit or 0) - (metadata.usage.max_tokens or DEFAULT_MAX_TOKENS)
|
|
103
108
|
effective_limit_str = format_number(effective_limit) if effective_limit > 0 else "?"
|
|
104
109
|
parts.append(
|
|
105
110
|
Text.assemble(
|
|
@@ -167,6 +172,29 @@ def render_task_metadata(e: events.TaskMetadataEvent) -> RenderableType:
|
|
|
167
172
|
for meta in e.metadata.sub_agent_task_metadata:
|
|
168
173
|
renderables.append(_render_task_metadata_block(meta, is_sub_agent=True, show_context_and_time=False))
|
|
169
174
|
|
|
175
|
+
# Add total cost line when there are sub-agents
|
|
176
|
+
if e.metadata.sub_agent_task_metadata:
|
|
177
|
+
total_cost = 0.0
|
|
178
|
+
currency = "USD"
|
|
179
|
+
# Sum up costs from main agent and all sub-agents
|
|
180
|
+
if e.metadata.main_agent.usage and e.metadata.main_agent.usage.total_cost:
|
|
181
|
+
total_cost += e.metadata.main_agent.usage.total_cost
|
|
182
|
+
currency = e.metadata.main_agent.usage.currency
|
|
183
|
+
for meta in e.metadata.sub_agent_task_metadata:
|
|
184
|
+
if meta.usage and meta.usage.total_cost:
|
|
185
|
+
total_cost += meta.usage.total_cost
|
|
186
|
+
|
|
187
|
+
currency_symbol = "¥" if currency == "CNY" else "$"
|
|
188
|
+
total_line = Text.assemble(
|
|
189
|
+
("Σ ", ThemeKey.METADATA_DIM),
|
|
190
|
+
("total ", ThemeKey.METADATA_DIM),
|
|
191
|
+
(currency_symbol, ThemeKey.METADATA_DIM),
|
|
192
|
+
(f"{total_cost:.4f}", ThemeKey.METADATA_BOLD),
|
|
193
|
+
)
|
|
194
|
+
grid = create_grid()
|
|
195
|
+
grid.add_row(Text(" ", style=ThemeKey.METADATA_DIM), total_line)
|
|
196
|
+
renderables.append(Padding(grid, (0, 0, 0, 2)))
|
|
197
|
+
|
|
170
198
|
return Group(*renderables)
|
|
171
199
|
|
|
172
200
|
|