klaude-code 1.2.6__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/__init__.py +0 -0
- klaude_code/cli/__init__.py +1 -0
- klaude_code/cli/main.py +298 -0
- klaude_code/cli/runtime.py +331 -0
- klaude_code/cli/session_cmd.py +80 -0
- klaude_code/command/__init__.py +43 -0
- klaude_code/command/clear_cmd.py +20 -0
- klaude_code/command/command_abc.py +92 -0
- klaude_code/command/diff_cmd.py +138 -0
- klaude_code/command/export_cmd.py +86 -0
- klaude_code/command/help_cmd.py +51 -0
- klaude_code/command/model_cmd.py +43 -0
- klaude_code/command/prompt-dev-docs-update.md +56 -0
- klaude_code/command/prompt-dev-docs.md +46 -0
- klaude_code/command/prompt-init.md +45 -0
- klaude_code/command/prompt_command.py +69 -0
- klaude_code/command/refresh_cmd.py +43 -0
- klaude_code/command/registry.py +110 -0
- klaude_code/command/status_cmd.py +111 -0
- klaude_code/command/terminal_setup_cmd.py +252 -0
- klaude_code/config/__init__.py +11 -0
- klaude_code/config/config.py +177 -0
- klaude_code/config/list_model.py +162 -0
- klaude_code/config/select_model.py +67 -0
- klaude_code/const/__init__.py +133 -0
- klaude_code/core/__init__.py +0 -0
- klaude_code/core/agent.py +165 -0
- klaude_code/core/executor.py +485 -0
- klaude_code/core/manager/__init__.py +19 -0
- klaude_code/core/manager/agent_manager.py +127 -0
- klaude_code/core/manager/llm_clients.py +42 -0
- klaude_code/core/manager/llm_clients_builder.py +49 -0
- klaude_code/core/manager/sub_agent_manager.py +86 -0
- klaude_code/core/prompt.py +89 -0
- klaude_code/core/prompts/prompt-claude-code.md +98 -0
- klaude_code/core/prompts/prompt-codex.md +331 -0
- klaude_code/core/prompts/prompt-gemini.md +43 -0
- klaude_code/core/prompts/prompt-subagent-explore.md +27 -0
- klaude_code/core/prompts/prompt-subagent-oracle.md +23 -0
- klaude_code/core/prompts/prompt-subagent-webfetch.md +46 -0
- klaude_code/core/prompts/prompt-subagent.md +8 -0
- klaude_code/core/reminders.py +445 -0
- klaude_code/core/task.py +237 -0
- klaude_code/core/tool/__init__.py +75 -0
- klaude_code/core/tool/file/__init__.py +0 -0
- klaude_code/core/tool/file/apply_patch.py +492 -0
- klaude_code/core/tool/file/apply_patch_tool.md +1 -0
- klaude_code/core/tool/file/apply_patch_tool.py +204 -0
- klaude_code/core/tool/file/edit_tool.md +9 -0
- klaude_code/core/tool/file/edit_tool.py +274 -0
- klaude_code/core/tool/file/multi_edit_tool.md +42 -0
- klaude_code/core/tool/file/multi_edit_tool.py +199 -0
- klaude_code/core/tool/file/read_tool.md +14 -0
- klaude_code/core/tool/file/read_tool.py +326 -0
- klaude_code/core/tool/file/write_tool.md +8 -0
- klaude_code/core/tool/file/write_tool.py +146 -0
- klaude_code/core/tool/memory/__init__.py +0 -0
- klaude_code/core/tool/memory/memory_tool.md +16 -0
- klaude_code/core/tool/memory/memory_tool.py +462 -0
- klaude_code/core/tool/memory/skill_loader.py +245 -0
- klaude_code/core/tool/memory/skill_tool.md +24 -0
- klaude_code/core/tool/memory/skill_tool.py +97 -0
- klaude_code/core/tool/shell/__init__.py +0 -0
- klaude_code/core/tool/shell/bash_tool.md +43 -0
- klaude_code/core/tool/shell/bash_tool.py +123 -0
- klaude_code/core/tool/shell/command_safety.py +363 -0
- klaude_code/core/tool/sub_agent_tool.py +83 -0
- klaude_code/core/tool/todo/__init__.py +0 -0
- klaude_code/core/tool/todo/todo_write_tool.md +182 -0
- klaude_code/core/tool/todo/todo_write_tool.py +121 -0
- klaude_code/core/tool/todo/update_plan_tool.md +3 -0
- klaude_code/core/tool/todo/update_plan_tool.py +104 -0
- klaude_code/core/tool/tool_abc.py +25 -0
- klaude_code/core/tool/tool_context.py +106 -0
- klaude_code/core/tool/tool_registry.py +78 -0
- klaude_code/core/tool/tool_runner.py +252 -0
- klaude_code/core/tool/truncation.py +170 -0
- klaude_code/core/tool/web/__init__.py +0 -0
- klaude_code/core/tool/web/mermaid_tool.md +21 -0
- klaude_code/core/tool/web/mermaid_tool.py +76 -0
- klaude_code/core/tool/web/web_fetch_tool.md +8 -0
- klaude_code/core/tool/web/web_fetch_tool.py +159 -0
- klaude_code/core/turn.py +220 -0
- klaude_code/llm/__init__.py +21 -0
- klaude_code/llm/anthropic/__init__.py +3 -0
- klaude_code/llm/anthropic/client.py +221 -0
- klaude_code/llm/anthropic/input.py +200 -0
- klaude_code/llm/client.py +49 -0
- klaude_code/llm/input_common.py +239 -0
- klaude_code/llm/openai_compatible/__init__.py +3 -0
- klaude_code/llm/openai_compatible/client.py +211 -0
- klaude_code/llm/openai_compatible/input.py +109 -0
- klaude_code/llm/openai_compatible/tool_call_accumulator.py +80 -0
- klaude_code/llm/openrouter/__init__.py +3 -0
- klaude_code/llm/openrouter/client.py +200 -0
- klaude_code/llm/openrouter/input.py +160 -0
- klaude_code/llm/openrouter/reasoning_handler.py +209 -0
- klaude_code/llm/registry.py +22 -0
- klaude_code/llm/responses/__init__.py +3 -0
- klaude_code/llm/responses/client.py +216 -0
- klaude_code/llm/responses/input.py +167 -0
- klaude_code/llm/usage.py +109 -0
- klaude_code/protocol/__init__.py +4 -0
- klaude_code/protocol/commands.py +21 -0
- klaude_code/protocol/events.py +163 -0
- klaude_code/protocol/llm_param.py +147 -0
- klaude_code/protocol/model.py +287 -0
- klaude_code/protocol/op.py +89 -0
- klaude_code/protocol/op_handler.py +28 -0
- klaude_code/protocol/sub_agent.py +348 -0
- klaude_code/protocol/tools.py +15 -0
- klaude_code/session/__init__.py +4 -0
- klaude_code/session/export.py +624 -0
- klaude_code/session/selector.py +76 -0
- klaude_code/session/session.py +474 -0
- klaude_code/session/templates/export_session.html +1434 -0
- klaude_code/trace/__init__.py +3 -0
- klaude_code/trace/log.py +168 -0
- klaude_code/ui/__init__.py +91 -0
- klaude_code/ui/core/__init__.py +1 -0
- klaude_code/ui/core/display.py +103 -0
- klaude_code/ui/core/input.py +71 -0
- klaude_code/ui/core/stage_manager.py +55 -0
- klaude_code/ui/modes/__init__.py +1 -0
- klaude_code/ui/modes/debug/__init__.py +1 -0
- klaude_code/ui/modes/debug/display.py +36 -0
- klaude_code/ui/modes/exec/__init__.py +1 -0
- klaude_code/ui/modes/exec/display.py +63 -0
- klaude_code/ui/modes/repl/__init__.py +51 -0
- klaude_code/ui/modes/repl/clipboard.py +152 -0
- klaude_code/ui/modes/repl/completers.py +429 -0
- klaude_code/ui/modes/repl/display.py +60 -0
- klaude_code/ui/modes/repl/event_handler.py +375 -0
- klaude_code/ui/modes/repl/input_prompt_toolkit.py +198 -0
- klaude_code/ui/modes/repl/key_bindings.py +170 -0
- klaude_code/ui/modes/repl/renderer.py +281 -0
- klaude_code/ui/renderers/__init__.py +0 -0
- klaude_code/ui/renderers/assistant.py +21 -0
- klaude_code/ui/renderers/common.py +8 -0
- klaude_code/ui/renderers/developer.py +158 -0
- klaude_code/ui/renderers/diffs.py +215 -0
- klaude_code/ui/renderers/errors.py +16 -0
- klaude_code/ui/renderers/metadata.py +190 -0
- klaude_code/ui/renderers/sub_agent.py +71 -0
- klaude_code/ui/renderers/thinking.py +39 -0
- klaude_code/ui/renderers/tools.py +551 -0
- klaude_code/ui/renderers/user_input.py +65 -0
- klaude_code/ui/rich/__init__.py +1 -0
- klaude_code/ui/rich/live.py +65 -0
- klaude_code/ui/rich/markdown.py +308 -0
- klaude_code/ui/rich/quote.py +34 -0
- klaude_code/ui/rich/searchable_text.py +71 -0
- klaude_code/ui/rich/status.py +240 -0
- klaude_code/ui/rich/theme.py +274 -0
- klaude_code/ui/terminal/__init__.py +1 -0
- klaude_code/ui/terminal/color.py +244 -0
- klaude_code/ui/terminal/control.py +147 -0
- klaude_code/ui/terminal/notifier.py +107 -0
- klaude_code/ui/terminal/progress_bar.py +87 -0
- klaude_code/ui/utils/__init__.py +1 -0
- klaude_code/ui/utils/common.py +108 -0
- klaude_code/ui/utils/debouncer.py +42 -0
- klaude_code/version.py +163 -0
- klaude_code-1.2.6.dist-info/METADATA +178 -0
- klaude_code-1.2.6.dist-info/RECORD +167 -0
- klaude_code-1.2.6.dist-info/WHEEL +4 -0
- klaude_code-1.2.6.dist-info/entry_points.txt +3 -0
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
from rich import box
|
|
2
|
+
from rich.console import Group, RenderableType
|
|
3
|
+
from rich.padding import Padding
|
|
4
|
+
from rich.panel import Panel
|
|
5
|
+
from rich.text import Text
|
|
6
|
+
|
|
7
|
+
from klaude_code import const
|
|
8
|
+
from klaude_code.ui.renderers.common import create_grid
|
|
9
|
+
from klaude_code.ui.rich.theme import ThemeKey
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _make_diff_prefix(line: str, new_ln: int | None, width: int) -> tuple[str, int | None]:
|
|
13
|
+
kind = line[0]
|
|
14
|
+
|
|
15
|
+
number = " " * width
|
|
16
|
+
if kind in {"+", " "} and new_ln is not None:
|
|
17
|
+
number = f"{new_ln:>{width}}"
|
|
18
|
+
new_ln += 1
|
|
19
|
+
|
|
20
|
+
if kind == "-":
|
|
21
|
+
marker = "-"
|
|
22
|
+
elif kind == "+":
|
|
23
|
+
marker = "+"
|
|
24
|
+
else:
|
|
25
|
+
marker = " "
|
|
26
|
+
|
|
27
|
+
prefix = f"{number} {marker}"
|
|
28
|
+
return prefix, new_ln
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def render_diff(diff_text: str, show_file_name: bool = False) -> RenderableType:
|
|
32
|
+
if diff_text == "":
|
|
33
|
+
return Text("")
|
|
34
|
+
|
|
35
|
+
lines = diff_text.split("\n")
|
|
36
|
+
grid = create_grid()
|
|
37
|
+
grid.padding = (0, 0)
|
|
38
|
+
|
|
39
|
+
# Track line numbers based on hunk headers
|
|
40
|
+
new_ln: int | None = None
|
|
41
|
+
# Track if we're in untracked files section
|
|
42
|
+
in_untracked_section = False
|
|
43
|
+
# Track whether we've already rendered a file header
|
|
44
|
+
has_rendered_file_header = False
|
|
45
|
+
# Track whether we have rendered actual diff content for the current file
|
|
46
|
+
has_rendered_diff_content = False
|
|
47
|
+
# Track the "from" file name from --- line (used for deleted files)
|
|
48
|
+
from_file_name: str | None = None
|
|
49
|
+
|
|
50
|
+
for i, line in enumerate(lines):
|
|
51
|
+
# Check for untracked files section header
|
|
52
|
+
if line == "git ls-files --others --exclude-standard":
|
|
53
|
+
in_untracked_section = True
|
|
54
|
+
grid.add_row("", "")
|
|
55
|
+
grid.add_row("", Text("Untracked files:", style=ThemeKey.TOOL_MARK))
|
|
56
|
+
grid.add_row("", "")
|
|
57
|
+
continue
|
|
58
|
+
|
|
59
|
+
# Handle untracked files
|
|
60
|
+
if in_untracked_section:
|
|
61
|
+
# If we hit a new section or empty line, we're done with untracked files
|
|
62
|
+
if line.startswith("diff --git") or line.strip() == "":
|
|
63
|
+
in_untracked_section = False
|
|
64
|
+
elif line.strip(): # Non-empty line in untracked section
|
|
65
|
+
file_text = Text(line.strip(), style=ThemeKey.TOOL_PARAM_BOLD)
|
|
66
|
+
grid.add_row(
|
|
67
|
+
Text(f"{'+':>{const.DIFF_PREFIX_WIDTH}}", style=ThemeKey.TOOL_PARAM_BOLD),
|
|
68
|
+
file_text,
|
|
69
|
+
)
|
|
70
|
+
continue
|
|
71
|
+
|
|
72
|
+
# Capture "from" file name from --- line (needed for deleted files)
|
|
73
|
+
if line.startswith("--- "):
|
|
74
|
+
raw = line[4:].strip()
|
|
75
|
+
if raw != "/dev/null":
|
|
76
|
+
if raw.startswith(("a/", "b/")):
|
|
77
|
+
from_file_name = raw[2:]
|
|
78
|
+
else:
|
|
79
|
+
from_file_name = raw
|
|
80
|
+
continue
|
|
81
|
+
|
|
82
|
+
# Parse file name from diff headers
|
|
83
|
+
if show_file_name and line.startswith("+++ "):
|
|
84
|
+
# Extract file name from +++ header with proper handling of /dev/null
|
|
85
|
+
raw = line[4:].strip()
|
|
86
|
+
if raw == "/dev/null":
|
|
87
|
+
# File was deleted, use the "from" file name
|
|
88
|
+
file_name = from_file_name or raw
|
|
89
|
+
elif raw.startswith(("a/", "b/")):
|
|
90
|
+
file_name = raw[2:]
|
|
91
|
+
else:
|
|
92
|
+
file_name = raw
|
|
93
|
+
|
|
94
|
+
file_text = Text(file_name, style=ThemeKey.DIFF_FILE_NAME)
|
|
95
|
+
|
|
96
|
+
# Count actual +/- lines for this file from i+1 onwards
|
|
97
|
+
file_additions = 0
|
|
98
|
+
file_deletions = 0
|
|
99
|
+
for remaining_line in lines[i + 1 :]:
|
|
100
|
+
if remaining_line.startswith("diff --git"):
|
|
101
|
+
break
|
|
102
|
+
elif remaining_line.startswith("+") and not remaining_line.startswith("+++"):
|
|
103
|
+
file_additions += 1
|
|
104
|
+
elif remaining_line.startswith("-") and not remaining_line.startswith("---"):
|
|
105
|
+
file_deletions += 1
|
|
106
|
+
|
|
107
|
+
# Create stats text
|
|
108
|
+
stats_text = Text()
|
|
109
|
+
if file_additions > 0:
|
|
110
|
+
stats_text.append(f"+{file_additions}", style=ThemeKey.DIFF_STATS_ADD)
|
|
111
|
+
if file_deletions > 0:
|
|
112
|
+
if file_additions > 0:
|
|
113
|
+
stats_text.append(" ")
|
|
114
|
+
stats_text.append(f"-{file_deletions}", style=ThemeKey.DIFF_STATS_REMOVE)
|
|
115
|
+
|
|
116
|
+
# Combine file name and stats
|
|
117
|
+
file_line = Text(style=ThemeKey.DIFF_FILE_NAME)
|
|
118
|
+
file_line.append_text(file_text)
|
|
119
|
+
if stats_text.plain:
|
|
120
|
+
file_line.append(" (")
|
|
121
|
+
file_line.append_text(stats_text)
|
|
122
|
+
file_line.append(")")
|
|
123
|
+
|
|
124
|
+
if has_rendered_file_header:
|
|
125
|
+
grid.add_row("", "")
|
|
126
|
+
|
|
127
|
+
if file_additions > 0 and file_deletions == 0:
|
|
128
|
+
file_mark = "+"
|
|
129
|
+
elif file_deletions > 0 and file_additions == 0:
|
|
130
|
+
file_mark = "-"
|
|
131
|
+
else:
|
|
132
|
+
file_mark = "±"
|
|
133
|
+
|
|
134
|
+
grid.add_row(
|
|
135
|
+
Text(f"{file_mark:>{const.DIFF_PREFIX_WIDTH}} ", style=ThemeKey.DIFF_FILE_NAME),
|
|
136
|
+
file_line,
|
|
137
|
+
)
|
|
138
|
+
has_rendered_file_header = True
|
|
139
|
+
has_rendered_diff_content = False
|
|
140
|
+
continue
|
|
141
|
+
|
|
142
|
+
if line.startswith("diff --git"):
|
|
143
|
+
has_rendered_diff_content = False
|
|
144
|
+
continue
|
|
145
|
+
|
|
146
|
+
# Parse hunk headers to reset counters: @@ -l,s +l,s @@
|
|
147
|
+
if line.startswith("@@"):
|
|
148
|
+
try:
|
|
149
|
+
parts = line.split()
|
|
150
|
+
plus = parts[2] # like '+12,4'
|
|
151
|
+
new_start = int(plus[1:].split(",")[0])
|
|
152
|
+
new_ln = new_start
|
|
153
|
+
except Exception:
|
|
154
|
+
new_ln = None
|
|
155
|
+
if has_rendered_diff_content:
|
|
156
|
+
grid.add_row(Text(f"{'⋮':>{const.DIFF_PREFIX_WIDTH}}", style=ThemeKey.TOOL_RESULT), "")
|
|
157
|
+
continue
|
|
158
|
+
|
|
159
|
+
# Skip +++ lines (already handled above)
|
|
160
|
+
if line.startswith("+++ "):
|
|
161
|
+
continue
|
|
162
|
+
|
|
163
|
+
# Only handle unified diff hunk lines; ignore other metadata like
|
|
164
|
+
# "diff --git" or "index ..." which would otherwise skew counters.
|
|
165
|
+
if not line or line[:1] not in {" ", "+", "-"}:
|
|
166
|
+
continue
|
|
167
|
+
|
|
168
|
+
# Compute line number prefix and style diff content
|
|
169
|
+
prefix, new_ln = _make_diff_prefix(line, new_ln, const.DIFF_PREFIX_WIDTH)
|
|
170
|
+
|
|
171
|
+
if line.startswith("-"):
|
|
172
|
+
text = Text(line[1:])
|
|
173
|
+
text.stylize(ThemeKey.DIFF_REMOVE)
|
|
174
|
+
elif line.startswith("+"):
|
|
175
|
+
text = Text(line[1:])
|
|
176
|
+
text.stylize(ThemeKey.DIFF_ADD)
|
|
177
|
+
else:
|
|
178
|
+
text = Text(line, style=ThemeKey.TOOL_RESULT)
|
|
179
|
+
grid.add_row(Text(prefix, ThemeKey.TOOL_RESULT), text)
|
|
180
|
+
has_rendered_diff_content = True
|
|
181
|
+
|
|
182
|
+
return grid
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def render_diff_panel(
|
|
186
|
+
diff_text: str,
|
|
187
|
+
*,
|
|
188
|
+
show_file_name: bool = True,
|
|
189
|
+
heading: str = "Git Diff",
|
|
190
|
+
indent: int = 2,
|
|
191
|
+
) -> RenderableType:
|
|
192
|
+
lines = diff_text.splitlines()
|
|
193
|
+
truncated_notice: Text | None = None
|
|
194
|
+
if len(lines) > const.MAX_DIFF_LINES:
|
|
195
|
+
truncated_lines = len(lines) - const.MAX_DIFF_LINES
|
|
196
|
+
diff_text = "\n".join(lines[: const.MAX_DIFF_LINES])
|
|
197
|
+
truncated_notice = Text(f"… truncated {truncated_lines} lines", style=ThemeKey.TOOL_MARK)
|
|
198
|
+
|
|
199
|
+
diff_body = render_diff(diff_text, show_file_name=show_file_name)
|
|
200
|
+
renderables: list[RenderableType] = [
|
|
201
|
+
Text(f" {heading} ", style="bold reverse"),
|
|
202
|
+
diff_body,
|
|
203
|
+
]
|
|
204
|
+
if truncated_notice is not None:
|
|
205
|
+
renderables.extend([Text(""), truncated_notice])
|
|
206
|
+
|
|
207
|
+
panel = Panel.fit(
|
|
208
|
+
Group(*renderables),
|
|
209
|
+
border_style=ThemeKey.LINES,
|
|
210
|
+
title_align="center",
|
|
211
|
+
box=box.ROUNDED,
|
|
212
|
+
)
|
|
213
|
+
if indent <= 0:
|
|
214
|
+
return panel
|
|
215
|
+
return Padding.indent(panel, level=indent)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from rich.console import RenderableType
|
|
2
|
+
from rich.text import Text
|
|
3
|
+
|
|
4
|
+
from klaude_code.ui.renderers.common import create_grid
|
|
5
|
+
from klaude_code.ui.rich.theme import ThemeKey
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def render_error(error_msg: Text, indent: int = 2) -> RenderableType:
|
|
9
|
+
"""Stateless error renderer.
|
|
10
|
+
|
|
11
|
+
Shows a two-column grid with an error mark and truncated message.
|
|
12
|
+
"""
|
|
13
|
+
grid = create_grid()
|
|
14
|
+
error_msg.stylize(ThemeKey.ERROR)
|
|
15
|
+
grid.add_row(Text(" " * indent + "✘", style=ThemeKey.ERROR_BOLD), error_msg)
|
|
16
|
+
return grid
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
from importlib.metadata import version
|
|
2
|
+
|
|
3
|
+
from rich import box
|
|
4
|
+
from rich.box import Box
|
|
5
|
+
from rich.console import Group, RenderableType
|
|
6
|
+
from rich.padding import Padding
|
|
7
|
+
from rich.panel import Panel
|
|
8
|
+
from rich.text import Text
|
|
9
|
+
|
|
10
|
+
from klaude_code.protocol import events
|
|
11
|
+
from klaude_code.trace import is_debug_enabled
|
|
12
|
+
from klaude_code.ui.rich.theme import ThemeKey
|
|
13
|
+
from klaude_code.ui.utils.common import format_number
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _get_version() -> str:
|
|
17
|
+
"""Get the current version of klaude-code."""
|
|
18
|
+
try:
|
|
19
|
+
return version("klaude-code")
|
|
20
|
+
except Exception:
|
|
21
|
+
return "unknown"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def render_response_metadata(e: events.ResponseMetadataEvent) -> RenderableType:
|
|
25
|
+
metadata = e.metadata
|
|
26
|
+
|
|
27
|
+
# Line 1: Model and Provider
|
|
28
|
+
model_text = Text()
|
|
29
|
+
model_text.append_text(Text("- ", style=ThemeKey.METADATA_BOLD)).append_text(
|
|
30
|
+
Text(metadata.model_name, style=ThemeKey.METADATA_BOLD)
|
|
31
|
+
)
|
|
32
|
+
if metadata.provider is not None:
|
|
33
|
+
model_text.append_text(Text("@", style=ThemeKey.METADATA)).append_text(
|
|
34
|
+
Text(metadata.provider.lower().replace(" ", "-"), style=ThemeKey.METADATA)
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
renderables: list[RenderableType] = [model_text]
|
|
38
|
+
|
|
39
|
+
# Line 2: Token consumption, Context, TPS, Cost
|
|
40
|
+
parts: list[Text] = []
|
|
41
|
+
|
|
42
|
+
if metadata.usage is not None:
|
|
43
|
+
# Input
|
|
44
|
+
input_parts: list[tuple[str, str]] = [
|
|
45
|
+
("input:", ThemeKey.METADATA_DIM),
|
|
46
|
+
(format_number(metadata.usage.input_tokens), ThemeKey.METADATA_DIM),
|
|
47
|
+
]
|
|
48
|
+
if metadata.usage.input_cost is not None:
|
|
49
|
+
input_parts.append((f"(${metadata.usage.input_cost:.4f})", ThemeKey.METADATA_DIM))
|
|
50
|
+
parts.append(Text.assemble(*input_parts))
|
|
51
|
+
|
|
52
|
+
# Cached
|
|
53
|
+
if metadata.usage.cached_tokens > 0:
|
|
54
|
+
cached_parts: list[tuple[str, str]] = [
|
|
55
|
+
("cached:", ThemeKey.METADATA_DIM),
|
|
56
|
+
(format_number(metadata.usage.cached_tokens), ThemeKey.METADATA_DIM),
|
|
57
|
+
]
|
|
58
|
+
if metadata.usage.cache_read_cost is not None:
|
|
59
|
+
cached_parts.append((f"(${metadata.usage.cache_read_cost:.4f})", ThemeKey.METADATA_DIM))
|
|
60
|
+
parts.append(Text.assemble(*cached_parts))
|
|
61
|
+
|
|
62
|
+
# Output
|
|
63
|
+
output_parts: list[tuple[str, str]] = [
|
|
64
|
+
("output:", ThemeKey.METADATA_DIM),
|
|
65
|
+
(format_number(metadata.usage.output_tokens), ThemeKey.METADATA_DIM),
|
|
66
|
+
]
|
|
67
|
+
if metadata.usage.output_cost is not None:
|
|
68
|
+
output_parts.append((f"(${metadata.usage.output_cost:.4f})", ThemeKey.METADATA_DIM))
|
|
69
|
+
parts.append(Text.assemble(*output_parts))
|
|
70
|
+
|
|
71
|
+
# Reasoning
|
|
72
|
+
if metadata.usage.reasoning_tokens > 0:
|
|
73
|
+
parts.append(
|
|
74
|
+
Text.assemble(
|
|
75
|
+
("thinking", ThemeKey.METADATA_DIM),
|
|
76
|
+
(":", ThemeKey.METADATA_DIM),
|
|
77
|
+
(
|
|
78
|
+
format_number(metadata.usage.reasoning_tokens),
|
|
79
|
+
ThemeKey.METADATA_DIM,
|
|
80
|
+
),
|
|
81
|
+
)
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
# Context
|
|
85
|
+
if metadata.usage.context_usage_percent is not None:
|
|
86
|
+
parts.append(
|
|
87
|
+
Text.assemble(
|
|
88
|
+
("context", ThemeKey.METADATA_DIM),
|
|
89
|
+
(":", ThemeKey.METADATA_DIM),
|
|
90
|
+
(
|
|
91
|
+
f"{metadata.usage.context_usage_percent:.1f}%",
|
|
92
|
+
ThemeKey.METADATA_DIM,
|
|
93
|
+
),
|
|
94
|
+
)
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
# TPS
|
|
98
|
+
if metadata.usage.throughput_tps is not None:
|
|
99
|
+
parts.append(
|
|
100
|
+
Text.assemble(
|
|
101
|
+
("tps", ThemeKey.METADATA_DIM),
|
|
102
|
+
(":", ThemeKey.METADATA_DIM),
|
|
103
|
+
(f"{metadata.usage.throughput_tps:.1f}", ThemeKey.METADATA_DIM),
|
|
104
|
+
)
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
# Duration
|
|
108
|
+
if metadata.task_duration_s is not None:
|
|
109
|
+
parts.append(
|
|
110
|
+
Text.assemble(
|
|
111
|
+
("time", ThemeKey.METADATA_DIM),
|
|
112
|
+
(":", ThemeKey.METADATA_DIM),
|
|
113
|
+
(f"{metadata.task_duration_s:.1f}s", ThemeKey.METADATA_DIM),
|
|
114
|
+
)
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
# Cost (USD)
|
|
118
|
+
if metadata.usage is not None and metadata.usage.total_cost is not None:
|
|
119
|
+
parts.append(
|
|
120
|
+
Text.assemble(
|
|
121
|
+
("cost", ThemeKey.METADATA_DIM),
|
|
122
|
+
(":", ThemeKey.METADATA_DIM),
|
|
123
|
+
(f"${metadata.usage.total_cost:.4f}", ThemeKey.METADATA_DIM),
|
|
124
|
+
)
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
if parts:
|
|
128
|
+
line2 = Text("/", style=ThemeKey.METADATA_DIM).join(parts)
|
|
129
|
+
renderables.append(Padding(line2, (0, 0, 0, 2)))
|
|
130
|
+
|
|
131
|
+
return Group(*renderables)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def render_welcome(e: events.WelcomeEvent, *, box_style: Box | None = None) -> RenderableType:
|
|
135
|
+
"""Render the welcome panel with model info and settings."""
|
|
136
|
+
if box_style is None:
|
|
137
|
+
box_style = box.ROUNDED
|
|
138
|
+
|
|
139
|
+
debug_mode = is_debug_enabled()
|
|
140
|
+
|
|
141
|
+
# First line: Klaude Code version
|
|
142
|
+
klaude_code_style = ThemeKey.WELCOME_DEBUG_TITLE if debug_mode else ThemeKey.WELCOME_HIGHLIGHT_BOLD
|
|
143
|
+
panel_content = Text.assemble(
|
|
144
|
+
("Klaude Code", klaude_code_style),
|
|
145
|
+
(f" v{_get_version()}\n", ThemeKey.WELCOME_INFO),
|
|
146
|
+
(str(e.llm_config.model), ThemeKey.WELCOME_HIGHLIGHT),
|
|
147
|
+
(" @ ", ThemeKey.WELCOME_INFO),
|
|
148
|
+
(e.llm_config.provider_name, ThemeKey.WELCOME_INFO),
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
# Collect all config items to display
|
|
152
|
+
config_items: list[tuple[str, str]] = []
|
|
153
|
+
|
|
154
|
+
if e.llm_config.thinking is not None:
|
|
155
|
+
if e.llm_config.thinking.reasoning_effort:
|
|
156
|
+
config_items.append(("reasoning-effort", e.llm_config.thinking.reasoning_effort))
|
|
157
|
+
if e.llm_config.thinking.reasoning_summary:
|
|
158
|
+
config_items.append(("reasoning-summary", e.llm_config.thinking.reasoning_summary))
|
|
159
|
+
if e.llm_config.thinking.budget_tokens:
|
|
160
|
+
config_items.append(("thinking-budget", str(e.llm_config.thinking.budget_tokens)))
|
|
161
|
+
|
|
162
|
+
if e.llm_config.verbosity:
|
|
163
|
+
config_items.append(("verbosity", str(e.llm_config.verbosity)))
|
|
164
|
+
|
|
165
|
+
if pr := e.llm_config.provider_routing:
|
|
166
|
+
if pr.sort:
|
|
167
|
+
config_items.append(("provider-sort", str(pr.sort)))
|
|
168
|
+
if pr.only:
|
|
169
|
+
config_items.append(("provider-only", ">".join(pr.only)))
|
|
170
|
+
if pr.order:
|
|
171
|
+
config_items.append(("provider-order", ">".join(pr.order)))
|
|
172
|
+
|
|
173
|
+
# Render config items with tree-style prefixes
|
|
174
|
+
for i, (key, value) in enumerate(config_items):
|
|
175
|
+
is_last = i == len(config_items) - 1
|
|
176
|
+
prefix = "└─ " if is_last else "├─ "
|
|
177
|
+
panel_content.append_text(
|
|
178
|
+
Text.assemble(
|
|
179
|
+
("\n", ThemeKey.WELCOME_INFO),
|
|
180
|
+
(prefix, ThemeKey.LINES),
|
|
181
|
+
(f"{key}: ", ThemeKey.WELCOME_INFO),
|
|
182
|
+
(value, ThemeKey.WELCOME_INFO),
|
|
183
|
+
)
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
border_style = ThemeKey.WELCOME_DEBUG_BORDER if debug_mode else ThemeKey.LINES
|
|
187
|
+
return Group(
|
|
188
|
+
Panel.fit(panel_content, border_style=border_style, box=box_style),
|
|
189
|
+
"", # empty line
|
|
190
|
+
)
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import json
|
|
2
|
+
|
|
3
|
+
from rich.console import Group, RenderableType
|
|
4
|
+
from rich.panel import Panel
|
|
5
|
+
from rich.style import Style
|
|
6
|
+
from rich.text import Text
|
|
7
|
+
|
|
8
|
+
from klaude_code import const
|
|
9
|
+
from klaude_code.protocol import events, model
|
|
10
|
+
from klaude_code.protocol.sub_agent import get_sub_agent_profile_by_tool
|
|
11
|
+
from klaude_code.ui.rich.markdown import NoInsetMarkdown
|
|
12
|
+
from klaude_code.ui.rich.theme import ThemeKey
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def render_sub_agent_call(e: model.SubAgentState, style: Style | None = None) -> RenderableType:
|
|
16
|
+
"""Render sub-agent tool call header and prompt body."""
|
|
17
|
+
desc = Text(
|
|
18
|
+
f" {e.sub_agent_desc} ",
|
|
19
|
+
style=Style(color=style.color if style else None, bold=True, reverse=True),
|
|
20
|
+
)
|
|
21
|
+
return Group(
|
|
22
|
+
Text.assemble((e.sub_agent_type, ThemeKey.TOOL_NAME), " ", desc),
|
|
23
|
+
Text(e.sub_agent_prompt, style=style or ""),
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def render_sub_agent_result(result: str, *, code_theme: str, style: Style | None = None) -> RenderableType:
|
|
28
|
+
stripped_result = result.strip()
|
|
29
|
+
lines = stripped_result.splitlines()
|
|
30
|
+
if len(lines) > const.SUB_AGENT_RESULT_MAX_LINES:
|
|
31
|
+
hidden_count = len(lines) - const.SUB_AGENT_RESULT_MAX_LINES
|
|
32
|
+
truncated_text = "\n".join(lines[-const.SUB_AGENT_RESULT_MAX_LINES :])
|
|
33
|
+
return Panel.fit(
|
|
34
|
+
Group(
|
|
35
|
+
Text(
|
|
36
|
+
f"… more {hidden_count} lines — use /export to view full output",
|
|
37
|
+
style=ThemeKey.TOOL_RESULT,
|
|
38
|
+
),
|
|
39
|
+
NoInsetMarkdown(truncated_text, code_theme=code_theme, style=style or ""),
|
|
40
|
+
),
|
|
41
|
+
border_style=ThemeKey.LINES,
|
|
42
|
+
)
|
|
43
|
+
return Panel.fit(
|
|
44
|
+
NoInsetMarkdown(stripped_result, code_theme=code_theme),
|
|
45
|
+
border_style=ThemeKey.LINES,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def build_sub_agent_state_from_tool_call(e: events.ToolCallEvent) -> model.SubAgentState | None:
|
|
50
|
+
"""Build SubAgentState from a tool call event for replay rendering."""
|
|
51
|
+
profile = get_sub_agent_profile_by_tool(e.tool_name)
|
|
52
|
+
if profile is None:
|
|
53
|
+
return None
|
|
54
|
+
description = profile.name
|
|
55
|
+
prompt = ""
|
|
56
|
+
if e.arguments:
|
|
57
|
+
try:
|
|
58
|
+
payload: dict[str, object] = json.loads(e.arguments)
|
|
59
|
+
except json.JSONDecodeError:
|
|
60
|
+
payload = {}
|
|
61
|
+
desc_value = payload.get("description")
|
|
62
|
+
if isinstance(desc_value, str) and desc_value.strip():
|
|
63
|
+
description = desc_value.strip()
|
|
64
|
+
prompt_value = payload.get("prompt") or payload.get("task")
|
|
65
|
+
if isinstance(prompt_value, str):
|
|
66
|
+
prompt = prompt_value.strip()
|
|
67
|
+
return model.SubAgentState(
|
|
68
|
+
sub_agent_type=profile.name,
|
|
69
|
+
sub_agent_desc=description,
|
|
70
|
+
sub_agent_prompt=prompt,
|
|
71
|
+
)
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
from rich.console import RenderableType
|
|
2
|
+
from rich.padding import Padding
|
|
3
|
+
from rich.text import Text
|
|
4
|
+
|
|
5
|
+
from klaude_code.ui.rich.markdown import NoInsetMarkdown
|
|
6
|
+
from klaude_code.ui.rich.theme import ThemeKey
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def thinking_prefix() -> Text:
|
|
10
|
+
return Text.from_markup("[not italic]⸫[/not italic] Thinking …", style=ThemeKey.THINKING)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _normalize_thinking_content(content: str) -> str:
|
|
14
|
+
"""Normalize thinking content for display."""
|
|
15
|
+
return (
|
|
16
|
+
content.rstrip()
|
|
17
|
+
.replace("**\n\n", "** \n")
|
|
18
|
+
.replace("\\n\\n\n\n", "") # Weird case of Gemini 3
|
|
19
|
+
.replace("****", "**\n\n**") # remove extra newlines after bold titles
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def render_thinking(content: str, *, code_theme: str, style: str) -> RenderableType | None:
|
|
24
|
+
"""Render thinking content as indented markdown.
|
|
25
|
+
|
|
26
|
+
Returns None if content is empty.
|
|
27
|
+
Note: Caller should push thinking_markdown_theme before printing.
|
|
28
|
+
"""
|
|
29
|
+
if len(content.strip()) == 0:
|
|
30
|
+
return None
|
|
31
|
+
|
|
32
|
+
return Padding.indent(
|
|
33
|
+
NoInsetMarkdown(
|
|
34
|
+
_normalize_thinking_content(content),
|
|
35
|
+
code_theme=code_theme,
|
|
36
|
+
style=style,
|
|
37
|
+
),
|
|
38
|
+
level=2,
|
|
39
|
+
)
|