klaude-code 2.0.1__py3-none-any.whl → 2.1.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/__init__.py +12 -0
- klaude_code/app/runtime.py +215 -0
- klaude_code/cli/auth_cmd.py +2 -2
- klaude_code/cli/config_cmd.py +2 -2
- klaude_code/cli/cost_cmd.py +1 -1
- klaude_code/cli/debug.py +12 -36
- klaude_code/cli/list_model.py +3 -3
- klaude_code/cli/main.py +17 -60
- klaude_code/cli/self_update.py +2 -187
- klaude_code/cli/session_cmd.py +2 -2
- klaude_code/config/config.py +1 -1
- klaude_code/config/select_model.py +1 -1
- klaude_code/const.py +10 -1
- klaude_code/core/agent.py +9 -62
- klaude_code/core/agent_profile.py +284 -0
- klaude_code/core/executor.py +343 -230
- klaude_code/core/manager/llm_clients_builder.py +1 -1
- klaude_code/core/manager/sub_agent_manager.py +16 -29
- klaude_code/core/reminders.py +107 -155
- klaude_code/core/task.py +12 -20
- klaude_code/core/tool/__init__.py +5 -19
- klaude_code/core/tool/context.py +84 -0
- klaude_code/core/tool/file/apply_patch_tool.py +18 -21
- klaude_code/core/tool/file/edit_tool.py +42 -44
- klaude_code/core/tool/file/read_tool.py +14 -9
- klaude_code/core/tool/file/write_tool.py +12 -13
- klaude_code/core/tool/report_back_tool.py +4 -1
- klaude_code/core/tool/shell/bash_tool.py +6 -11
- klaude_code/core/tool/skill/skill_tool.py +3 -1
- klaude_code/core/tool/sub_agent_tool.py +8 -7
- klaude_code/core/tool/todo/todo_write_tool.py +3 -9
- klaude_code/core/tool/todo/update_plan_tool.py +3 -5
- klaude_code/core/tool/tool_abc.py +2 -1
- klaude_code/core/tool/tool_registry.py +2 -33
- klaude_code/core/tool/tool_runner.py +13 -10
- klaude_code/core/tool/web/mermaid_tool.py +3 -1
- klaude_code/core/tool/web/web_fetch_tool.py +5 -3
- klaude_code/core/tool/web/web_search_tool.py +5 -3
- klaude_code/core/turn.py +86 -26
- klaude_code/llm/anthropic/client.py +1 -1
- klaude_code/llm/bedrock/client.py +1 -1
- klaude_code/llm/claude/client.py +1 -1
- klaude_code/llm/codex/client.py +1 -1
- klaude_code/llm/google/client.py +1 -1
- klaude_code/llm/openai_compatible/client.py +1 -1
- klaude_code/llm/openai_compatible/tool_call_accumulator.py +1 -1
- klaude_code/llm/openrouter/client.py +1 -1
- klaude_code/llm/openrouter/reasoning.py +1 -1
- klaude_code/llm/responses/client.py +1 -1
- klaude_code/protocol/events/__init__.py +57 -0
- klaude_code/protocol/events/base.py +18 -0
- klaude_code/protocol/events/chat.py +20 -0
- klaude_code/protocol/events/lifecycle.py +22 -0
- klaude_code/protocol/events/metadata.py +15 -0
- klaude_code/protocol/events/streaming.py +43 -0
- klaude_code/protocol/events/system.py +53 -0
- klaude_code/protocol/events/tools.py +23 -0
- klaude_code/protocol/message.py +3 -11
- klaude_code/protocol/model.py +78 -9
- klaude_code/protocol/op.py +5 -0
- klaude_code/protocol/sub_agent/explore.py +0 -15
- klaude_code/protocol/sub_agent/task.py +1 -1
- klaude_code/protocol/sub_agent/web.py +1 -17
- klaude_code/protocol/tools.py +0 -1
- klaude_code/session/session.py +6 -5
- klaude_code/skill/assets/create-plan/SKILL.md +76 -0
- klaude_code/skill/loader.py +1 -1
- klaude_code/skill/system_skills.py +1 -1
- klaude_code/tui/__init__.py +8 -0
- klaude_code/{command → tui/command}/clear_cmd.py +2 -1
- klaude_code/{command → tui/command}/debug_cmd.py +4 -3
- klaude_code/{command → tui/command}/export_cmd.py +2 -1
- klaude_code/{command → tui/command}/export_online_cmd.py +6 -5
- klaude_code/{command → tui/command}/fork_session_cmd.py +10 -9
- klaude_code/{command → tui/command}/help_cmd.py +3 -2
- klaude_code/{command → tui/command}/model_cmd.py +5 -4
- klaude_code/{command → tui/command}/model_select.py +2 -2
- klaude_code/{command → tui/command}/prompt_command.py +4 -3
- klaude_code/{command → tui/command}/refresh_cmd.py +3 -1
- klaude_code/{command → tui/command}/registry.py +16 -6
- klaude_code/{command → tui/command}/release_notes_cmd.py +3 -2
- klaude_code/{command → tui/command}/resume_cmd.py +6 -5
- klaude_code/{command → tui/command}/status_cmd.py +4 -3
- klaude_code/{command → tui/command}/terminal_setup_cmd.py +4 -3
- klaude_code/{command → tui/command}/thinking_cmd.py +4 -3
- klaude_code/tui/commands.py +164 -0
- klaude_code/{ui/renderers → tui/components}/assistant.py +3 -3
- klaude_code/{ui/renderers → tui/components}/bash_syntax.py +2 -2
- klaude_code/{ui/renderers → tui/components}/common.py +1 -1
- klaude_code/tui/components/developer.py +231 -0
- klaude_code/{ui/renderers → tui/components}/diffs.py +2 -2
- klaude_code/{ui/renderers → tui/components}/errors.py +2 -2
- klaude_code/{ui/renderers → tui/components}/metadata.py +34 -21
- klaude_code/{ui → tui/components}/rich/markdown.py +78 -34
- klaude_code/{ui → tui/components}/rich/status.py +2 -2
- klaude_code/{ui → tui/components}/rich/theme.py +12 -5
- klaude_code/{ui/renderers → tui/components}/sub_agent.py +23 -43
- klaude_code/{ui/renderers → tui/components}/thinking.py +3 -3
- klaude_code/{ui/renderers → tui/components}/tools.py +11 -48
- klaude_code/{ui/renderers → tui/components}/user_input.py +3 -20
- klaude_code/tui/display.py +85 -0
- klaude_code/{ui/modes/repl → tui/input}/__init__.py +1 -1
- klaude_code/{ui/modes/repl → tui/input}/completers.py +1 -1
- klaude_code/{ui/modes/repl/input_prompt_toolkit.py → tui/input/prompt_toolkit.py} +11 -7
- klaude_code/tui/machine.py +606 -0
- klaude_code/tui/renderer.py +707 -0
- klaude_code/tui/runner.py +321 -0
- klaude_code/tui/terminal/__init__.py +56 -0
- klaude_code/{ui → tui}/terminal/color.py +1 -1
- klaude_code/{ui → tui}/terminal/control.py +1 -1
- klaude_code/{ui → tui}/terminal/notifier.py +1 -1
- klaude_code/{ui → tui}/terminal/selector.py +36 -17
- klaude_code/ui/__init__.py +6 -50
- klaude_code/ui/core/display.py +3 -3
- klaude_code/ui/core/input.py +2 -1
- klaude_code/ui/{modes/debug/display.py → debug_mode.py} +1 -1
- klaude_code/ui/{modes/exec/display.py → exec_mode.py} +1 -4
- klaude_code/ui/terminal/__init__.py +6 -54
- klaude_code/ui/terminal/title.py +31 -0
- klaude_code/update.py +163 -0
- {klaude_code-2.0.1.dist-info → klaude_code-2.1.0.dist-info}/METADATA +1 -1
- klaude_code-2.1.0.dist-info/RECORD +235 -0
- klaude_code/cli/runtime.py +0 -525
- klaude_code/core/prompt.py +0 -108
- klaude_code/core/tool/file/move_tool.md +0 -41
- klaude_code/core/tool/file/move_tool.py +0 -435
- klaude_code/core/tool/tool_context.py +0 -148
- klaude_code/protocol/events.py +0 -194
- klaude_code/skill/assets/dev-docs/SKILL.md +0 -108
- klaude_code/trace/__init__.py +0 -21
- klaude_code/ui/core/stage_manager.py +0 -48
- klaude_code/ui/modes/__init__.py +0 -1
- klaude_code/ui/modes/debug/__init__.py +0 -1
- klaude_code/ui/modes/exec/__init__.py +0 -1
- klaude_code/ui/modes/repl/display.py +0 -61
- klaude_code/ui/modes/repl/event_handler.py +0 -634
- klaude_code/ui/modes/repl/renderer.py +0 -463
- klaude_code/ui/renderers/developer.py +0 -215
- klaude_code/ui/utils/__init__.py +0 -1
- klaude_code-2.0.1.dist-info/RECORD +0 -229
- /klaude_code/{trace/log.py → log.py} +0 -0
- /klaude_code/{command → tui/command}/__init__.py +0 -0
- /klaude_code/{command → tui/command}/command_abc.py +0 -0
- /klaude_code/{command → tui/command}/prompt-commit.md +0 -0
- /klaude_code/{command → tui/command}/prompt-init.md +0 -0
- /klaude_code/{ui/renderers → tui/components}/__init__.py +0 -0
- /klaude_code/{ui/renderers → tui/components}/mermaid_viewer.py +0 -0
- /klaude_code/{ui → tui/components}/rich/__init__.py +0 -0
- /klaude_code/{ui → tui/components}/rich/cjk_wrap.py +0 -0
- /klaude_code/{ui → tui/components}/rich/code_panel.py +0 -0
- /klaude_code/{ui → tui/components}/rich/live.py +0 -0
- /klaude_code/{ui → tui/components}/rich/quote.py +0 -0
- /klaude_code/{ui → tui/components}/rich/searchable_text.py +0 -0
- /klaude_code/{ui/modes/repl → tui/input}/clipboard.py +0 -0
- /klaude_code/{ui/modes/repl → tui/input}/key_bindings.py +0 -0
- /klaude_code/{ui → tui}/terminal/image.py +0 -0
- /klaude_code/{ui → tui}/terminal/progress_bar.py +0 -0
- /klaude_code/ui/{utils/common.py → common.py} +0 -0
- {klaude_code-2.0.1.dist-info → klaude_code-2.1.0.dist-info}/WHEEL +0 -0
- {klaude_code-2.0.1.dist-info → klaude_code-2.1.0.dist-info}/entry_points.txt +0 -0
|
@@ -1,463 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import contextlib
|
|
4
|
-
from collections.abc import Iterator
|
|
5
|
-
from contextlib import contextmanager
|
|
6
|
-
from dataclasses import dataclass
|
|
7
|
-
from typing import Any
|
|
8
|
-
|
|
9
|
-
from rich.console import Console, Group, RenderableType
|
|
10
|
-
from rich.padding import Padding
|
|
11
|
-
from rich.spinner import Spinner
|
|
12
|
-
from rich.style import Style, StyleType
|
|
13
|
-
from rich.text import Text
|
|
14
|
-
|
|
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
|
|
21
|
-
from klaude_code.ui.renderers import assistant as r_assistant
|
|
22
|
-
from klaude_code.ui.renderers import developer as r_developer
|
|
23
|
-
from klaude_code.ui.renderers import errors as r_errors
|
|
24
|
-
from klaude_code.ui.renderers import mermaid_viewer as r_mermaid_viewer
|
|
25
|
-
from klaude_code.ui.renderers import metadata as r_metadata
|
|
26
|
-
from klaude_code.ui.renderers import sub_agent as r_sub_agent
|
|
27
|
-
from klaude_code.ui.renderers import thinking as r_thinking
|
|
28
|
-
from klaude_code.ui.renderers import tools as r_tools
|
|
29
|
-
from klaude_code.ui.renderers import user_input as r_user_input
|
|
30
|
-
from klaude_code.ui.renderers.common import truncate_head, truncate_middle
|
|
31
|
-
from klaude_code.ui.rich import status as r_status
|
|
32
|
-
from klaude_code.ui.rich.live import CropAboveLive, SingleLine
|
|
33
|
-
from klaude_code.ui.rich.quote import Quote
|
|
34
|
-
from klaude_code.ui.rich.status import BreathingSpinner, ShimmerStatusText
|
|
35
|
-
from klaude_code.ui.rich.theme import ThemeKey, get_theme
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
@dataclass
|
|
39
|
-
class SessionStatus:
|
|
40
|
-
color: Style | None = None
|
|
41
|
-
color_index: int | None = None
|
|
42
|
-
sub_agent_state: model.SubAgentState | None = None
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
class REPLRenderer:
|
|
46
|
-
"""Render REPL content via a Rich console."""
|
|
47
|
-
|
|
48
|
-
def __init__(self, theme: str | None = None):
|
|
49
|
-
self.themes = get_theme(theme)
|
|
50
|
-
self.console: Console = Console(theme=self.themes.app_theme)
|
|
51
|
-
self.console.push_theme(self.themes.markdown_theme)
|
|
52
|
-
self._bottom_live: CropAboveLive | None = None
|
|
53
|
-
self._stream_renderable: RenderableType | None = None
|
|
54
|
-
self._stream_max_height: int = 0
|
|
55
|
-
self._stream_last_height: int = 0
|
|
56
|
-
self._stream_last_width: int = 0
|
|
57
|
-
self._spinner_visible: bool = False
|
|
58
|
-
|
|
59
|
-
self._status_text: ShimmerStatusText = ShimmerStatusText(STATUS_DEFAULT_TEXT)
|
|
60
|
-
self._status_spinner: Spinner = BreathingSpinner(
|
|
61
|
-
r_status.spinner_name(),
|
|
62
|
-
text=SingleLine(self._status_text),
|
|
63
|
-
style=ThemeKey.STATUS_SPINNER,
|
|
64
|
-
)
|
|
65
|
-
|
|
66
|
-
self.session_map: dict[str, SessionStatus] = {}
|
|
67
|
-
self.current_sub_agent_color: Style | None = None
|
|
68
|
-
self.sub_agent_color_index = 0
|
|
69
|
-
|
|
70
|
-
def register_session(self, session_id: str, sub_agent_state: model.SubAgentState | None = None) -> None:
|
|
71
|
-
session_status = SessionStatus(
|
|
72
|
-
sub_agent_state=sub_agent_state,
|
|
73
|
-
)
|
|
74
|
-
if sub_agent_state is not None:
|
|
75
|
-
color, color_index = self.pick_sub_agent_color()
|
|
76
|
-
session_status.color = color
|
|
77
|
-
session_status.color_index = color_index
|
|
78
|
-
self.session_map[session_id] = session_status
|
|
79
|
-
|
|
80
|
-
def is_sub_agent_session(self, session_id: str) -> bool:
|
|
81
|
-
return session_id in self.session_map and self.session_map[session_id].sub_agent_state is not None
|
|
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
|
-
|
|
90
|
-
def _advance_sub_agent_color_index(self) -> None:
|
|
91
|
-
palette_size = len(self.themes.sub_agent_colors)
|
|
92
|
-
if palette_size == 0:
|
|
93
|
-
self.sub_agent_color_index = 0
|
|
94
|
-
return
|
|
95
|
-
self.sub_agent_color_index = (self.sub_agent_color_index + 1) % palette_size
|
|
96
|
-
|
|
97
|
-
def pick_sub_agent_color(self) -> tuple[Style, int]:
|
|
98
|
-
self._advance_sub_agent_color_index()
|
|
99
|
-
palette = self.themes.sub_agent_colors
|
|
100
|
-
if not palette:
|
|
101
|
-
return Style(), 0
|
|
102
|
-
return palette[self.sub_agent_color_index], self.sub_agent_color_index
|
|
103
|
-
|
|
104
|
-
def get_session_sub_agent_color(self, session_id: str) -> Style:
|
|
105
|
-
status = self.session_map.get(session_id)
|
|
106
|
-
if status and status.color:
|
|
107
|
-
return status.color
|
|
108
|
-
return Style()
|
|
109
|
-
|
|
110
|
-
def get_session_sub_agent_background(self, session_id: str) -> Style:
|
|
111
|
-
status = self.session_map.get(session_id)
|
|
112
|
-
backgrounds = self.themes.sub_agent_backgrounds
|
|
113
|
-
if status and status.color_index is not None and backgrounds:
|
|
114
|
-
return backgrounds[status.color_index]
|
|
115
|
-
return Style()
|
|
116
|
-
|
|
117
|
-
@contextmanager
|
|
118
|
-
def session_print_context(self, session_id: str) -> Iterator[None]:
|
|
119
|
-
"""Temporarily switch to sub-agent quote style."""
|
|
120
|
-
if session_id in self.session_map and self.session_map[session_id].color:
|
|
121
|
-
self.current_sub_agent_color = self.session_map[session_id].color
|
|
122
|
-
try:
|
|
123
|
-
yield
|
|
124
|
-
finally:
|
|
125
|
-
self.current_sub_agent_color = None
|
|
126
|
-
|
|
127
|
-
def print(self, *objects: Any, style: StyleType | None = None, end: str = "\n") -> None:
|
|
128
|
-
if self.current_sub_agent_color:
|
|
129
|
-
if objects:
|
|
130
|
-
content = objects[0] if len(objects) == 1 else objects
|
|
131
|
-
self.console.print(Quote(content, style=self.current_sub_agent_color), overflow="ellipsis")
|
|
132
|
-
return
|
|
133
|
-
self.console.print(*objects, style=style, end=end, overflow="ellipsis")
|
|
134
|
-
|
|
135
|
-
def display_tool_call(self, e: events.ToolCallEvent) -> None:
|
|
136
|
-
if r_tools.is_sub_agent_tool(e.tool_name):
|
|
137
|
-
return
|
|
138
|
-
renderable = r_tools.render_tool_call(e)
|
|
139
|
-
if renderable is not None:
|
|
140
|
-
self.print(renderable)
|
|
141
|
-
|
|
142
|
-
def display_tool_call_result(self, e: events.ToolResultEvent, *, is_sub_agent: bool = False) -> None:
|
|
143
|
-
if r_tools.is_sub_agent_tool(e.tool_name):
|
|
144
|
-
return
|
|
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)
|
|
162
|
-
if renderable is not None:
|
|
163
|
-
self.print(renderable)
|
|
164
|
-
|
|
165
|
-
def display_thinking(self, content: str) -> None:
|
|
166
|
-
renderable = r_thinking.render_thinking(
|
|
167
|
-
content,
|
|
168
|
-
code_theme=self.themes.code_theme,
|
|
169
|
-
style=ThemeKey.THINKING,
|
|
170
|
-
)
|
|
171
|
-
if renderable is not None:
|
|
172
|
-
self.console.push_theme(theme=self.themes.thinking_markdown_theme)
|
|
173
|
-
self.print(renderable)
|
|
174
|
-
self.console.pop_theme()
|
|
175
|
-
self.print()
|
|
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
|
-
|
|
194
|
-
async def replay_history(self, history_events: events.ReplayHistoryEvent) -> None:
|
|
195
|
-
tool_call_dict: dict[str, events.ToolCallEvent] = {}
|
|
196
|
-
self.print()
|
|
197
|
-
for event in history_events.events:
|
|
198
|
-
event_session_id = getattr(event, "session_id", history_events.session_id)
|
|
199
|
-
is_sub_agent = self.is_sub_agent_session(event_session_id)
|
|
200
|
-
|
|
201
|
-
with self.session_print_context(event_session_id):
|
|
202
|
-
match event:
|
|
203
|
-
case events.TaskStartEvent() as e:
|
|
204
|
-
self.display_task_start(e)
|
|
205
|
-
case events.TurnStartEvent():
|
|
206
|
-
self.print()
|
|
207
|
-
case events.AssistantImageDeltaEvent() as e:
|
|
208
|
-
self.display_image(e.file_path)
|
|
209
|
-
case events.AssistantMessageEvent() as e:
|
|
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)
|
|
217
|
-
continue
|
|
218
|
-
if e.thinking_text:
|
|
219
|
-
self.display_thinking(e.thinking_text)
|
|
220
|
-
renderable = r_assistant.render_assistant_message(e.content, code_theme=self.themes.code_theme)
|
|
221
|
-
if renderable is not None:
|
|
222
|
-
self.print(renderable)
|
|
223
|
-
self.print()
|
|
224
|
-
case events.DeveloperMessageEvent() as e:
|
|
225
|
-
self.display_developer_message(e)
|
|
226
|
-
self.display_command_output(e)
|
|
227
|
-
case events.UserMessageEvent() as e:
|
|
228
|
-
if is_sub_agent:
|
|
229
|
-
continue
|
|
230
|
-
self.print(r_user_input.render_user_input(e.content))
|
|
231
|
-
case events.ToolCallEvent() as e:
|
|
232
|
-
tool_call_dict[e.tool_call_id] = e
|
|
233
|
-
case events.ToolResultEvent() as e:
|
|
234
|
-
tool_call_event = tool_call_dict.get(e.tool_call_id)
|
|
235
|
-
if tool_call_event is not None:
|
|
236
|
-
self.display_tool_call(tool_call_event)
|
|
237
|
-
tool_call_dict.pop(e.tool_call_id, None)
|
|
238
|
-
if is_sub_agent:
|
|
239
|
-
continue
|
|
240
|
-
self.display_tool_call_result(e)
|
|
241
|
-
case events.TaskMetadataEvent() as e:
|
|
242
|
-
self.print()
|
|
243
|
-
self.print(r_metadata.render_task_metadata(e))
|
|
244
|
-
self.print()
|
|
245
|
-
case events.InterruptEvent():
|
|
246
|
-
self.print()
|
|
247
|
-
self.print(r_user_input.render_interrupt())
|
|
248
|
-
case events.ErrorEvent() as e:
|
|
249
|
-
self.display_error(e)
|
|
250
|
-
case events.TaskFinishEvent() as e:
|
|
251
|
-
self.display_task_finish(e)
|
|
252
|
-
|
|
253
|
-
def display_developer_message(self, e: events.DeveloperMessageEvent) -> None:
|
|
254
|
-
if not r_developer.need_render_developer_message(e):
|
|
255
|
-
return
|
|
256
|
-
with self.session_print_context(e.session_id):
|
|
257
|
-
self.print(r_developer.render_developer_message(e))
|
|
258
|
-
|
|
259
|
-
def display_command_output(self, e: events.DeveloperMessageEvent) -> None:
|
|
260
|
-
if not e.item.command_output:
|
|
261
|
-
return
|
|
262
|
-
with self.session_print_context(e.session_id):
|
|
263
|
-
self.print(r_developer.render_command_output(e))
|
|
264
|
-
self.print()
|
|
265
|
-
|
|
266
|
-
def display_welcome(self, event: events.WelcomeEvent) -> None:
|
|
267
|
-
self.print(r_metadata.render_welcome(event))
|
|
268
|
-
|
|
269
|
-
def display_user_message(self, event: events.UserMessageEvent) -> None:
|
|
270
|
-
self.print(r_user_input.render_user_input(event.content))
|
|
271
|
-
|
|
272
|
-
def display_task_start(self, event: events.TaskStartEvent) -> None:
|
|
273
|
-
self.register_session(event.session_id, event.sub_agent_state)
|
|
274
|
-
if event.sub_agent_state is not None:
|
|
275
|
-
with self.session_print_context(event.session_id):
|
|
276
|
-
self.print(
|
|
277
|
-
r_sub_agent.render_sub_agent_call(
|
|
278
|
-
event.sub_agent_state,
|
|
279
|
-
self.get_session_sub_agent_color(event.session_id),
|
|
280
|
-
)
|
|
281
|
-
)
|
|
282
|
-
|
|
283
|
-
def display_turn_start(self, event: events.TurnStartEvent) -> None:
|
|
284
|
-
if not self.is_sub_agent_session(event.session_id):
|
|
285
|
-
self.print()
|
|
286
|
-
|
|
287
|
-
def display_assistant_message(self, content: str) -> None:
|
|
288
|
-
renderable = r_assistant.render_assistant_message(content, code_theme=self.themes.code_theme)
|
|
289
|
-
if renderable is not None:
|
|
290
|
-
self.print(renderable)
|
|
291
|
-
self.print()
|
|
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
|
-
|
|
324
|
-
def display_task_metadata(self, event: events.TaskMetadataEvent) -> None:
|
|
325
|
-
with self.session_print_context(event.session_id):
|
|
326
|
-
self.print(r_metadata.render_task_metadata(event))
|
|
327
|
-
self.print()
|
|
328
|
-
|
|
329
|
-
def display_task_finish(self, event: events.TaskFinishEvent) -> None:
|
|
330
|
-
if self.is_sub_agent_session(event.session_id):
|
|
331
|
-
session_status = self.session_map.get(event.session_id)
|
|
332
|
-
description = (
|
|
333
|
-
session_status.sub_agent_state.sub_agent_desc
|
|
334
|
-
if session_status and session_status.sub_agent_state
|
|
335
|
-
else None
|
|
336
|
-
)
|
|
337
|
-
panel_style = self.get_session_sub_agent_background(event.session_id)
|
|
338
|
-
with self.session_print_context(event.session_id):
|
|
339
|
-
self.print(
|
|
340
|
-
r_sub_agent.render_sub_agent_result(
|
|
341
|
-
event.task_result,
|
|
342
|
-
code_theme=self.themes.code_theme,
|
|
343
|
-
has_structured_output=event.has_structured_output,
|
|
344
|
-
description=description,
|
|
345
|
-
panel_style=panel_style,
|
|
346
|
-
)
|
|
347
|
-
)
|
|
348
|
-
|
|
349
|
-
def display_interrupt(self) -> None:
|
|
350
|
-
self.print(r_user_input.render_interrupt())
|
|
351
|
-
|
|
352
|
-
def display_error(self, event: events.ErrorEvent) -> None:
|
|
353
|
-
if event.session_id:
|
|
354
|
-
with self.session_print_context(event.session_id):
|
|
355
|
-
self.print(r_errors.render_error(truncate_middle(event.error_message)))
|
|
356
|
-
else:
|
|
357
|
-
self.print(r_errors.render_error(truncate_middle(event.error_message)))
|
|
358
|
-
|
|
359
|
-
# -------------------------------------------------------------------------
|
|
360
|
-
# Spinner control methods
|
|
361
|
-
# -------------------------------------------------------------------------
|
|
362
|
-
|
|
363
|
-
def spinner_start(self) -> None:
|
|
364
|
-
"""Start the spinner animation."""
|
|
365
|
-
self._spinner_visible = True
|
|
366
|
-
self._ensure_bottom_live_started()
|
|
367
|
-
self._refresh_bottom_live()
|
|
368
|
-
|
|
369
|
-
def spinner_stop(self) -> None:
|
|
370
|
-
"""Stop the spinner animation."""
|
|
371
|
-
self._spinner_visible = False
|
|
372
|
-
self._refresh_bottom_live()
|
|
373
|
-
|
|
374
|
-
def spinner_update(self, status_text: str | Text, right_text: RenderableType | None = None) -> None:
|
|
375
|
-
"""Update the spinner status text with optional right-aligned text."""
|
|
376
|
-
self._status_text = ShimmerStatusText(status_text, right_text)
|
|
377
|
-
self._status_spinner.update(text=SingleLine(self._status_text), style=ThemeKey.STATUS_SPINNER)
|
|
378
|
-
self._refresh_bottom_live()
|
|
379
|
-
|
|
380
|
-
def spinner_renderable(self) -> Spinner:
|
|
381
|
-
"""Return the spinner's renderable for embedding in other components."""
|
|
382
|
-
return self._status_spinner
|
|
383
|
-
|
|
384
|
-
def set_stream_renderable(self, renderable: RenderableType | None) -> None:
|
|
385
|
-
"""Set the current streaming renderable displayed above the status line."""
|
|
386
|
-
|
|
387
|
-
if renderable is None:
|
|
388
|
-
self._stream_renderable = None
|
|
389
|
-
self._stream_max_height = 0
|
|
390
|
-
self._stream_last_height = 0
|
|
391
|
-
self._stream_last_width = 0
|
|
392
|
-
self._refresh_bottom_live()
|
|
393
|
-
return
|
|
394
|
-
|
|
395
|
-
self._ensure_bottom_live_started()
|
|
396
|
-
self._stream_renderable = renderable
|
|
397
|
-
|
|
398
|
-
height = len(self.console.render_lines(renderable, self.console.options, pad=False))
|
|
399
|
-
self._stream_last_height = height
|
|
400
|
-
self._stream_last_width = self.console.size.width
|
|
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)
|
|
406
|
-
self._refresh_bottom_live()
|
|
407
|
-
|
|
408
|
-
def _ensure_bottom_live_started(self) -> None:
|
|
409
|
-
if self._bottom_live is not None:
|
|
410
|
-
return
|
|
411
|
-
self._bottom_live = CropAboveLive(
|
|
412
|
-
Text(""),
|
|
413
|
-
console=self.console,
|
|
414
|
-
refresh_per_second=30,
|
|
415
|
-
transient=True,
|
|
416
|
-
redirect_stdout=False,
|
|
417
|
-
redirect_stderr=False,
|
|
418
|
-
)
|
|
419
|
-
self._bottom_live.start()
|
|
420
|
-
|
|
421
|
-
def _bottom_renderable(self) -> RenderableType:
|
|
422
|
-
stream_part: RenderableType = Group()
|
|
423
|
-
gap_part: RenderableType = Group()
|
|
424
|
-
|
|
425
|
-
if MARKDOWN_STREAM_LIVE_REPAINT_ENABLED:
|
|
426
|
-
stream = self._stream_renderable
|
|
427
|
-
if stream is not None:
|
|
428
|
-
current_width = self.console.size.width
|
|
429
|
-
if self._stream_last_width != current_width:
|
|
430
|
-
height = len(self.console.render_lines(stream, self.console.options, pad=False))
|
|
431
|
-
self._stream_last_height = height
|
|
432
|
-
self._stream_last_width = current_width
|
|
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)
|
|
438
|
-
else:
|
|
439
|
-
height = self._stream_last_height
|
|
440
|
-
|
|
441
|
-
pad_lines = max(self._stream_max_height - height, 0)
|
|
442
|
-
if pad_lines:
|
|
443
|
-
stream = Padding(stream, (0, 0, pad_lines, 0))
|
|
444
|
-
stream_part = stream
|
|
445
|
-
|
|
446
|
-
gap_part = Text("") if self._spinner_visible else Group()
|
|
447
|
-
|
|
448
|
-
status_part: RenderableType = SingleLine(self._status_spinner) if self._spinner_visible else Group()
|
|
449
|
-
return Group(stream_part, gap_part, status_part)
|
|
450
|
-
|
|
451
|
-
def _refresh_bottom_live(self) -> None:
|
|
452
|
-
if self._bottom_live is None:
|
|
453
|
-
return
|
|
454
|
-
self._bottom_live.update(self._bottom_renderable(), refresh=True)
|
|
455
|
-
|
|
456
|
-
def stop_bottom_live(self) -> None:
|
|
457
|
-
if self._bottom_live is None:
|
|
458
|
-
return
|
|
459
|
-
with contextlib.suppress(Exception):
|
|
460
|
-
# Avoid cursor restore when stopping right before prompt_toolkit.
|
|
461
|
-
self._bottom_live.transient = False
|
|
462
|
-
self._bottom_live.stop()
|
|
463
|
-
self._bottom_live = None
|
|
@@ -1,215 +0,0 @@
|
|
|
1
|
-
from rich.console import Group, 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 commands, events, message, model
|
|
7
|
-
from klaude_code.ui.renderers.common import create_grid, truncate_middle
|
|
8
|
-
from klaude_code.ui.renderers.tools import render_path
|
|
9
|
-
from klaude_code.ui.rich.markdown import NoInsetMarkdown
|
|
10
|
-
from klaude_code.ui.rich.theme import ThemeKey
|
|
11
|
-
|
|
12
|
-
REMINDER_BULLET = " ⧉"
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
def need_render_developer_message(e: events.DeveloperMessageEvent) -> bool:
|
|
16
|
-
return bool(
|
|
17
|
-
e.item.memory_paths
|
|
18
|
-
or e.item.external_file_changes
|
|
19
|
-
or e.item.todo_use
|
|
20
|
-
or e.item.at_files
|
|
21
|
-
or e.item.user_image_count
|
|
22
|
-
or e.item.skill_name
|
|
23
|
-
)
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
def render_developer_message(e: events.DeveloperMessageEvent) -> RenderableType:
|
|
27
|
-
"""Render developer message details into a single group.
|
|
28
|
-
|
|
29
|
-
Includes: memory paths, external file changes, todo reminder, @file operations.
|
|
30
|
-
Command output is excluded; render it separately via `render_command_output`.
|
|
31
|
-
"""
|
|
32
|
-
parts: list[RenderableType] = []
|
|
33
|
-
|
|
34
|
-
if mp := e.item.memory_paths:
|
|
35
|
-
grid = create_grid()
|
|
36
|
-
grid.add_row(
|
|
37
|
-
Text(REMINDER_BULLET, style=ThemeKey.REMINDER),
|
|
38
|
-
Text.assemble(
|
|
39
|
-
("Load memory ", ThemeKey.REMINDER),
|
|
40
|
-
Text(", ", ThemeKey.REMINDER).join(
|
|
41
|
-
render_path(memory_path, ThemeKey.REMINDER_BOLD) for memory_path in mp
|
|
42
|
-
),
|
|
43
|
-
),
|
|
44
|
-
)
|
|
45
|
-
parts.append(grid)
|
|
46
|
-
|
|
47
|
-
if fc := e.item.external_file_changes:
|
|
48
|
-
grid = create_grid()
|
|
49
|
-
for file_path in fc:
|
|
50
|
-
grid.add_row(
|
|
51
|
-
Text(REMINDER_BULLET, style=ThemeKey.REMINDER),
|
|
52
|
-
Text.assemble(
|
|
53
|
-
("Read ", ThemeKey.REMINDER),
|
|
54
|
-
render_path(file_path, ThemeKey.REMINDER_BOLD),
|
|
55
|
-
(" after external changes", ThemeKey.REMINDER),
|
|
56
|
-
),
|
|
57
|
-
)
|
|
58
|
-
parts.append(grid)
|
|
59
|
-
|
|
60
|
-
if e.item.todo_use:
|
|
61
|
-
grid = create_grid()
|
|
62
|
-
grid.add_row(
|
|
63
|
-
Text(REMINDER_BULLET, style=ThemeKey.REMINDER),
|
|
64
|
-
Text("Todo hasn't been updated recently", ThemeKey.REMINDER),
|
|
65
|
-
)
|
|
66
|
-
parts.append(grid)
|
|
67
|
-
|
|
68
|
-
if e.item.at_files:
|
|
69
|
-
grid = create_grid()
|
|
70
|
-
# Group at_files by (operation, mentioned_in)
|
|
71
|
-
grouped: dict[tuple[str, str | None], list[str]] = {}
|
|
72
|
-
for at_file in e.item.at_files:
|
|
73
|
-
key = (at_file.operation, at_file.mentioned_in)
|
|
74
|
-
if key not in grouped:
|
|
75
|
-
grouped[key] = []
|
|
76
|
-
grouped[key].append(at_file.path)
|
|
77
|
-
|
|
78
|
-
for (operation, mentioned_in), paths in grouped.items():
|
|
79
|
-
path_texts = Text(", ", ThemeKey.REMINDER).join(render_path(p, ThemeKey.REMINDER_BOLD) for p in paths)
|
|
80
|
-
if mentioned_in:
|
|
81
|
-
grid.add_row(
|
|
82
|
-
Text(REMINDER_BULLET, style=ThemeKey.REMINDER),
|
|
83
|
-
Text.assemble(
|
|
84
|
-
(f"{operation} ", ThemeKey.REMINDER),
|
|
85
|
-
path_texts,
|
|
86
|
-
(" mentioned in ", ThemeKey.REMINDER),
|
|
87
|
-
render_path(mentioned_in, ThemeKey.REMINDER_BOLD),
|
|
88
|
-
),
|
|
89
|
-
)
|
|
90
|
-
else:
|
|
91
|
-
grid.add_row(
|
|
92
|
-
Text(REMINDER_BULLET, style=ThemeKey.REMINDER),
|
|
93
|
-
Text.assemble(
|
|
94
|
-
(f"{operation} ", ThemeKey.REMINDER),
|
|
95
|
-
path_texts,
|
|
96
|
-
),
|
|
97
|
-
)
|
|
98
|
-
parts.append(grid)
|
|
99
|
-
|
|
100
|
-
if uic := e.item.user_image_count:
|
|
101
|
-
grid = create_grid()
|
|
102
|
-
grid.add_row(
|
|
103
|
-
Text(REMINDER_BULLET, style=ThemeKey.REMINDER),
|
|
104
|
-
Text(f"Attached {uic} image{'s' if uic > 1 else ''}", style=ThemeKey.REMINDER),
|
|
105
|
-
)
|
|
106
|
-
parts.append(grid)
|
|
107
|
-
|
|
108
|
-
if sn := e.item.skill_name:
|
|
109
|
-
grid = create_grid()
|
|
110
|
-
grid.add_row(
|
|
111
|
-
Text(REMINDER_BULLET, style=ThemeKey.REMINDER),
|
|
112
|
-
Text.assemble(
|
|
113
|
-
("Activated skill ", ThemeKey.REMINDER),
|
|
114
|
-
(sn, ThemeKey.REMINDER_BOLD),
|
|
115
|
-
),
|
|
116
|
-
)
|
|
117
|
-
parts.append(grid)
|
|
118
|
-
|
|
119
|
-
return Group(*parts) if parts else Text("")
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
def render_command_output(e: events.DeveloperMessageEvent) -> RenderableType:
|
|
123
|
-
"""Render developer command output content."""
|
|
124
|
-
if not e.item.command_output:
|
|
125
|
-
return Text("")
|
|
126
|
-
|
|
127
|
-
content = message.join_text_parts(e.item.parts)
|
|
128
|
-
match e.item.command_output.command_name:
|
|
129
|
-
case commands.CommandName.HELP:
|
|
130
|
-
return Padding.indent(Text.from_markup(content or ""), level=2)
|
|
131
|
-
case commands.CommandName.STATUS:
|
|
132
|
-
return _render_status_output(e.item.command_output)
|
|
133
|
-
case commands.CommandName.RELEASE_NOTES:
|
|
134
|
-
return Padding.indent(NoInsetMarkdown(content or ""), level=2)
|
|
135
|
-
case commands.CommandName.FORK_SESSION:
|
|
136
|
-
return _render_fork_session_output(e.item.command_output)
|
|
137
|
-
case _:
|
|
138
|
-
content = content or "(no content)"
|
|
139
|
-
style = ThemeKey.TOOL_RESULT if not e.item.command_output.is_error else ThemeKey.ERROR
|
|
140
|
-
return Padding.indent(truncate_middle(content, base_style=style), level=2)
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
def _format_tokens(tokens: int) -> str:
|
|
144
|
-
"""Format token count with K/M suffix for readability."""
|
|
145
|
-
if tokens >= 1_000_000:
|
|
146
|
-
return f"{tokens / 1_000_000:.2f}M"
|
|
147
|
-
if tokens >= 1_000:
|
|
148
|
-
return f"{tokens / 1_000:.1f}K"
|
|
149
|
-
return str(tokens)
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
def _format_cost(cost: float | None, currency: str = "USD") -> str:
|
|
153
|
-
"""Format cost with currency symbol."""
|
|
154
|
-
if cost is None:
|
|
155
|
-
return "-"
|
|
156
|
-
symbol = "¥" if currency == "CNY" else "$"
|
|
157
|
-
if cost < 0.01:
|
|
158
|
-
return f"{symbol}{cost:.4f}"
|
|
159
|
-
return f"{symbol}{cost:.2f}"
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
def _render_fork_session_output(command_output: model.CommandOutput) -> RenderableType:
|
|
163
|
-
"""Render fork session output with usage instructions."""
|
|
164
|
-
if not isinstance(command_output.ui_extra, model.SessionIdUIExtra):
|
|
165
|
-
return Padding.indent(Text("(no session id)", style=ThemeKey.METADATA), level=2)
|
|
166
|
-
|
|
167
|
-
grid = Table.grid(padding=(0, 1))
|
|
168
|
-
session_id = command_output.ui_extra.session_id
|
|
169
|
-
grid.add_column(style=ThemeKey.METADATA, overflow="fold")
|
|
170
|
-
|
|
171
|
-
grid.add_row(Text("Session forked. Resume command copied to clipboard:", style=ThemeKey.METADATA))
|
|
172
|
-
grid.add_row(Text(f" klaude --resume-by-id {session_id}", style=ThemeKey.METADATA_BOLD))
|
|
173
|
-
|
|
174
|
-
return Padding.indent(grid, level=2)
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
def _render_status_output(command_output: model.CommandOutput) -> RenderableType:
|
|
178
|
-
"""Render session status with total cost and per-model breakdown."""
|
|
179
|
-
if not isinstance(command_output.ui_extra, model.SessionStatusUIExtra):
|
|
180
|
-
return Text("(no status data)", style=ThemeKey.METADATA)
|
|
181
|
-
|
|
182
|
-
status = command_output.ui_extra
|
|
183
|
-
usage = status.usage
|
|
184
|
-
|
|
185
|
-
table = Table.grid(padding=(0, 2))
|
|
186
|
-
table.add_column(style=ThemeKey.METADATA, overflow="fold")
|
|
187
|
-
table.add_column(style=ThemeKey.METADATA, overflow="fold")
|
|
188
|
-
|
|
189
|
-
# Total cost line
|
|
190
|
-
table.add_row(
|
|
191
|
-
Text("Total cost:", style=ThemeKey.METADATA_BOLD),
|
|
192
|
-
Text(_format_cost(usage.total_cost, usage.currency), style=ThemeKey.METADATA_BOLD),
|
|
193
|
-
)
|
|
194
|
-
|
|
195
|
-
# Per-model breakdown
|
|
196
|
-
if status.by_model:
|
|
197
|
-
table.add_row(Text("Usage by model:", style=ThemeKey.METADATA_BOLD), "")
|
|
198
|
-
for meta in status.by_model:
|
|
199
|
-
model_label = meta.model_name
|
|
200
|
-
if meta.provider:
|
|
201
|
-
model_label = f"{meta.model_name} ({meta.provider.lower().replace(' ', '-')})"
|
|
202
|
-
|
|
203
|
-
if meta.usage:
|
|
204
|
-
usage_detail = (
|
|
205
|
-
f"{_format_tokens(meta.usage.input_tokens)} input, "
|
|
206
|
-
f"{_format_tokens(meta.usage.output_tokens)} output, "
|
|
207
|
-
f"{_format_tokens(meta.usage.cached_tokens)} cache read, "
|
|
208
|
-
f"{_format_tokens(meta.usage.reasoning_tokens)} thinking, "
|
|
209
|
-
f"({_format_cost(meta.usage.total_cost, meta.usage.currency)})"
|
|
210
|
-
)
|
|
211
|
-
else:
|
|
212
|
-
usage_detail = "(no usage data)"
|
|
213
|
-
table.add_row(f"{model_label}:", usage_detail)
|
|
214
|
-
|
|
215
|
-
return Padding.indent(table, level=2)
|
klaude_code/ui/utils/__init__.py
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
# UI utilities
|