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
|
@@ -0,0 +1,231 @@
|
|
|
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.tui.components.common import create_grid, truncate_middle
|
|
8
|
+
from klaude_code.tui.components.rich.markdown import NoInsetMarkdown
|
|
9
|
+
from klaude_code.tui.components.rich.theme import ThemeKey
|
|
10
|
+
from klaude_code.tui.components.tools import render_path
|
|
11
|
+
|
|
12
|
+
REMINDER_BULLET = " ⧉"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def get_command_output(item: message.DeveloperMessage) -> model.CommandOutput | None:
|
|
16
|
+
if not item.ui_extra:
|
|
17
|
+
return None
|
|
18
|
+
for ui_item in item.ui_extra.items:
|
|
19
|
+
if isinstance(ui_item, model.CommandOutputUIItem):
|
|
20
|
+
return ui_item.output
|
|
21
|
+
return None
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def need_render_developer_message(e: events.DeveloperMessageEvent) -> bool:
|
|
25
|
+
if not e.item.ui_extra:
|
|
26
|
+
return False
|
|
27
|
+
return any(not isinstance(ui_item, model.CommandOutputUIItem) for ui_item in e.item.ui_extra.items)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def render_developer_message(e: events.DeveloperMessageEvent) -> RenderableType:
|
|
31
|
+
"""Render developer message details into a single group.
|
|
32
|
+
|
|
33
|
+
Includes: memory paths, external file changes, todo reminder, @file operations.
|
|
34
|
+
Command output is excluded; render it separately via `render_command_output`.
|
|
35
|
+
"""
|
|
36
|
+
parts: list[RenderableType] = []
|
|
37
|
+
|
|
38
|
+
if e.item.ui_extra:
|
|
39
|
+
for ui_item in e.item.ui_extra.items:
|
|
40
|
+
match ui_item:
|
|
41
|
+
case model.MemoryLoadedUIItem() as item:
|
|
42
|
+
grid = create_grid()
|
|
43
|
+
grid.add_row(
|
|
44
|
+
Text(REMINDER_BULLET, style=ThemeKey.REMINDER),
|
|
45
|
+
Text.assemble(
|
|
46
|
+
("Load memory ", ThemeKey.REMINDER),
|
|
47
|
+
Text(", ", ThemeKey.REMINDER).join(
|
|
48
|
+
render_path(mem.path, ThemeKey.REMINDER_BOLD) for mem in item.files
|
|
49
|
+
),
|
|
50
|
+
),
|
|
51
|
+
)
|
|
52
|
+
parts.append(grid)
|
|
53
|
+
case model.ExternalFileChangesUIItem() as item:
|
|
54
|
+
grid = create_grid()
|
|
55
|
+
for file_path in item.paths:
|
|
56
|
+
grid.add_row(
|
|
57
|
+
Text(REMINDER_BULLET, style=ThemeKey.REMINDER),
|
|
58
|
+
Text.assemble(
|
|
59
|
+
("Read ", ThemeKey.REMINDER),
|
|
60
|
+
render_path(file_path, ThemeKey.REMINDER_BOLD),
|
|
61
|
+
(" after external changes", ThemeKey.REMINDER),
|
|
62
|
+
),
|
|
63
|
+
)
|
|
64
|
+
parts.append(grid)
|
|
65
|
+
case model.TodoReminderUIItem() as item:
|
|
66
|
+
match item.reason:
|
|
67
|
+
case "not_used_recently":
|
|
68
|
+
text = "Todo hasn't been updated recently"
|
|
69
|
+
case "empty":
|
|
70
|
+
text = "Todo list is empty"
|
|
71
|
+
case _:
|
|
72
|
+
text = "Todo reminder"
|
|
73
|
+
grid = create_grid()
|
|
74
|
+
grid.add_row(
|
|
75
|
+
Text(REMINDER_BULLET, style=ThemeKey.REMINDER),
|
|
76
|
+
Text(text, ThemeKey.REMINDER),
|
|
77
|
+
)
|
|
78
|
+
parts.append(grid)
|
|
79
|
+
case model.AtFileOpsUIItem() as item:
|
|
80
|
+
grid = create_grid()
|
|
81
|
+
grouped: dict[tuple[str, str | None], list[str]] = {}
|
|
82
|
+
for op in item.ops:
|
|
83
|
+
key = (op.operation, op.mentioned_in)
|
|
84
|
+
grouped.setdefault(key, []).append(op.path)
|
|
85
|
+
|
|
86
|
+
for (operation, mentioned_in), paths in grouped.items():
|
|
87
|
+
path_texts = Text(", ", ThemeKey.REMINDER).join(
|
|
88
|
+
render_path(p, ThemeKey.REMINDER_BOLD) for p in paths
|
|
89
|
+
)
|
|
90
|
+
if mentioned_in:
|
|
91
|
+
grid.add_row(
|
|
92
|
+
Text(REMINDER_BULLET, style=ThemeKey.REMINDER),
|
|
93
|
+
Text.assemble(
|
|
94
|
+
(f"{operation} ", ThemeKey.REMINDER),
|
|
95
|
+
path_texts,
|
|
96
|
+
(" mentioned in ", ThemeKey.REMINDER),
|
|
97
|
+
render_path(mentioned_in, ThemeKey.REMINDER_BOLD),
|
|
98
|
+
),
|
|
99
|
+
)
|
|
100
|
+
else:
|
|
101
|
+
grid.add_row(
|
|
102
|
+
Text(REMINDER_BULLET, style=ThemeKey.REMINDER),
|
|
103
|
+
Text.assemble(
|
|
104
|
+
(f"{operation} ", ThemeKey.REMINDER),
|
|
105
|
+
path_texts,
|
|
106
|
+
),
|
|
107
|
+
)
|
|
108
|
+
parts.append(grid)
|
|
109
|
+
case model.UserImagesUIItem() as item:
|
|
110
|
+
grid = create_grid()
|
|
111
|
+
count = item.count
|
|
112
|
+
grid.add_row(
|
|
113
|
+
Text(REMINDER_BULLET, style=ThemeKey.REMINDER),
|
|
114
|
+
Text(
|
|
115
|
+
f"Attached {count} image{'s' if count > 1 else ''}",
|
|
116
|
+
style=ThemeKey.REMINDER,
|
|
117
|
+
),
|
|
118
|
+
)
|
|
119
|
+
parts.append(grid)
|
|
120
|
+
case model.SkillActivatedUIItem() as item:
|
|
121
|
+
grid = create_grid()
|
|
122
|
+
grid.add_row(
|
|
123
|
+
Text(REMINDER_BULLET, style=ThemeKey.REMINDER),
|
|
124
|
+
Text.assemble(
|
|
125
|
+
("Activated skill ", ThemeKey.REMINDER),
|
|
126
|
+
(item.name, ThemeKey.REMINDER_BOLD),
|
|
127
|
+
),
|
|
128
|
+
)
|
|
129
|
+
parts.append(grid)
|
|
130
|
+
case model.CommandOutputUIItem():
|
|
131
|
+
# Rendered via render_command_output
|
|
132
|
+
pass
|
|
133
|
+
|
|
134
|
+
return Group(*parts) if parts else Text("")
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def render_command_output(e: events.DeveloperMessageEvent) -> RenderableType:
|
|
138
|
+
"""Render developer command output content."""
|
|
139
|
+
command_output = get_command_output(e.item)
|
|
140
|
+
if not command_output:
|
|
141
|
+
return Text("")
|
|
142
|
+
|
|
143
|
+
content = message.join_text_parts(e.item.parts)
|
|
144
|
+
match command_output.command_name:
|
|
145
|
+
case commands.CommandName.HELP:
|
|
146
|
+
return Padding.indent(Text.from_markup(content or ""), level=2)
|
|
147
|
+
case commands.CommandName.STATUS:
|
|
148
|
+
return _render_status_output(command_output)
|
|
149
|
+
case commands.CommandName.RELEASE_NOTES:
|
|
150
|
+
return Padding.indent(NoInsetMarkdown(content or ""), level=2)
|
|
151
|
+
case commands.CommandName.FORK_SESSION:
|
|
152
|
+
return _render_fork_session_output(command_output)
|
|
153
|
+
case _:
|
|
154
|
+
content = content or "(no content)"
|
|
155
|
+
style = ThemeKey.TOOL_RESULT if not command_output.is_error else ThemeKey.ERROR
|
|
156
|
+
return Padding.indent(truncate_middle(content, base_style=style), level=2)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _format_tokens(tokens: int) -> str:
|
|
160
|
+
"""Format token count with K/M suffix for readability."""
|
|
161
|
+
if tokens >= 1_000_000:
|
|
162
|
+
return f"{tokens / 1_000_000:.2f}M"
|
|
163
|
+
if tokens >= 1_000:
|
|
164
|
+
return f"{tokens / 1_000:.1f}K"
|
|
165
|
+
return str(tokens)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _format_cost(cost: float | None, currency: str = "USD") -> str:
|
|
169
|
+
"""Format cost with currency symbol."""
|
|
170
|
+
if cost is None:
|
|
171
|
+
return "-"
|
|
172
|
+
symbol = "¥" if currency == "CNY" else "$"
|
|
173
|
+
if cost < 0.01:
|
|
174
|
+
return f"{symbol}{cost:.4f}"
|
|
175
|
+
return f"{symbol}{cost:.2f}"
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _render_fork_session_output(command_output: model.CommandOutput) -> RenderableType:
|
|
179
|
+
"""Render fork session output with usage instructions."""
|
|
180
|
+
if not isinstance(command_output.ui_extra, model.SessionIdUIExtra):
|
|
181
|
+
return Padding.indent(Text("(no session id)", style=ThemeKey.METADATA), level=2)
|
|
182
|
+
|
|
183
|
+
grid = Table.grid(padding=(0, 1))
|
|
184
|
+
session_id = command_output.ui_extra.session_id
|
|
185
|
+
grid.add_column(style=ThemeKey.METADATA, overflow="fold")
|
|
186
|
+
|
|
187
|
+
grid.add_row(Text("Session forked. Resume command copied to clipboard:", style=ThemeKey.METADATA))
|
|
188
|
+
grid.add_row(Text(f" klaude --resume-by-id {session_id}", style=ThemeKey.METADATA_BOLD))
|
|
189
|
+
|
|
190
|
+
return Padding.indent(grid, level=2)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _render_status_output(command_output: model.CommandOutput) -> RenderableType:
|
|
194
|
+
"""Render session status with total cost and per-model breakdown."""
|
|
195
|
+
if not isinstance(command_output.ui_extra, model.SessionStatusUIExtra):
|
|
196
|
+
return Text("(no status data)", style=ThemeKey.METADATA)
|
|
197
|
+
|
|
198
|
+
status = command_output.ui_extra
|
|
199
|
+
usage = status.usage
|
|
200
|
+
|
|
201
|
+
table = Table.grid(padding=(0, 2))
|
|
202
|
+
table.add_column(style=ThemeKey.METADATA, overflow="fold")
|
|
203
|
+
table.add_column(style=ThemeKey.METADATA, overflow="fold")
|
|
204
|
+
|
|
205
|
+
# Total cost line
|
|
206
|
+
table.add_row(
|
|
207
|
+
Text("Total cost:", style=ThemeKey.METADATA_BOLD),
|
|
208
|
+
Text(_format_cost(usage.total_cost, usage.currency), style=ThemeKey.METADATA_BOLD),
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
# Per-model breakdown
|
|
212
|
+
if status.by_model:
|
|
213
|
+
table.add_row(Text("Usage by model:", style=ThemeKey.METADATA_BOLD), "")
|
|
214
|
+
for meta in status.by_model:
|
|
215
|
+
model_label = meta.model_name
|
|
216
|
+
if meta.provider:
|
|
217
|
+
model_label = f"{meta.model_name} ({meta.provider.lower().replace(' ', '-')})"
|
|
218
|
+
|
|
219
|
+
if meta.usage:
|
|
220
|
+
usage_detail = (
|
|
221
|
+
f"{_format_tokens(meta.usage.input_tokens)} input, "
|
|
222
|
+
f"{_format_tokens(meta.usage.output_tokens)} output, "
|
|
223
|
+
f"{_format_tokens(meta.usage.cached_tokens)} cache read, "
|
|
224
|
+
f"{_format_tokens(meta.usage.reasoning_tokens)} thinking, "
|
|
225
|
+
f"({_format_cost(meta.usage.total_cost, meta.usage.currency)})"
|
|
226
|
+
)
|
|
227
|
+
else:
|
|
228
|
+
usage_detail = "(no usage data)"
|
|
229
|
+
table.add_row(f"{model_label}:", usage_detail)
|
|
230
|
+
|
|
231
|
+
return Padding.indent(table, level=2)
|
|
@@ -6,8 +6,8 @@ from rich.text import Text
|
|
|
6
6
|
|
|
7
7
|
from klaude_code.const import DIFF_PREFIX_WIDTH, MAX_DIFF_LINES
|
|
8
8
|
from klaude_code.protocol import model
|
|
9
|
-
from klaude_code.
|
|
10
|
-
from klaude_code.
|
|
9
|
+
from klaude_code.tui.components.common import create_grid
|
|
10
|
+
from klaude_code.tui.components.rich.theme import ThemeKey
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
def _make_diff_prefix(line: str, new_ln: int | None, width: int) -> tuple[str, int | None]:
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
from rich.console import RenderableType
|
|
2
2
|
from rich.text import Text
|
|
3
3
|
|
|
4
|
-
from klaude_code.
|
|
5
|
-
from klaude_code.
|
|
4
|
+
from klaude_code.tui.components.common import create_grid
|
|
5
|
+
from klaude_code.tui.components.rich.theme import ThemeKey
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
def render_error(error_msg: Text) -> RenderableType:
|
|
@@ -1,17 +1,16 @@
|
|
|
1
1
|
from importlib.metadata import PackageNotFoundError, version
|
|
2
2
|
|
|
3
|
-
from rich import box
|
|
4
3
|
from rich.console import Group, RenderableType
|
|
5
4
|
from rich.padding import Padding
|
|
6
|
-
from rich.panel import Panel
|
|
7
5
|
from rich.text import Text
|
|
8
6
|
|
|
9
7
|
from klaude_code.const import DEFAULT_MAX_TOKENS
|
|
8
|
+
from klaude_code.log import is_debug_enabled
|
|
10
9
|
from klaude_code.protocol import events, model
|
|
11
|
-
from klaude_code.
|
|
12
|
-
from klaude_code.
|
|
13
|
-
from klaude_code.
|
|
14
|
-
from klaude_code.ui.
|
|
10
|
+
from klaude_code.tui.components.common import create_grid
|
|
11
|
+
from klaude_code.tui.components.rich.quote import Quote
|
|
12
|
+
from klaude_code.tui.components.rich.theme import ThemeKey
|
|
13
|
+
from klaude_code.ui.common import format_model_params, format_number
|
|
15
14
|
|
|
16
15
|
|
|
17
16
|
def _get_version() -> str:
|
|
@@ -100,7 +99,7 @@ def _render_task_metadata_block(
|
|
|
100
99
|
)
|
|
101
100
|
)
|
|
102
101
|
if metadata.usage is not None:
|
|
103
|
-
# Context
|
|
102
|
+
# Context usage
|
|
104
103
|
if show_context_and_time and metadata.usage.context_usage_percent is not None:
|
|
105
104
|
context_size = format_number(metadata.usage.context_size or 0)
|
|
106
105
|
# Calculate effective limit (same as Usage.context_usage_percent)
|
|
@@ -170,7 +169,7 @@ def render_task_metadata(e: events.TaskMetadataEvent) -> RenderableType:
|
|
|
170
169
|
|
|
171
170
|
# Render each sub-agent metadata block
|
|
172
171
|
for meta in e.metadata.sub_agent_task_metadata:
|
|
173
|
-
renderables.append(_render_task_metadata_block(meta, is_sub_agent=True, show_context_and_time=
|
|
172
|
+
renderables.append(_render_task_metadata_block(meta, is_sub_agent=True, show_context_and_time=True))
|
|
174
173
|
|
|
175
174
|
# Add total cost line when there are sub-agents
|
|
176
175
|
if e.metadata.sub_agent_task_metadata:
|
|
@@ -199,17 +198,29 @@ def render_task_metadata(e: events.TaskMetadataEvent) -> RenderableType:
|
|
|
199
198
|
|
|
200
199
|
|
|
201
200
|
def render_welcome(e: events.WelcomeEvent) -> RenderableType:
|
|
202
|
-
"""Render the welcome panel with model info and settings.
|
|
201
|
+
"""Render the welcome panel with model info and settings.
|
|
202
|
+
|
|
203
|
+
Args:
|
|
204
|
+
e: The welcome event.
|
|
205
|
+
"""
|
|
203
206
|
debug_mode = is_debug_enabled()
|
|
204
207
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
(
|
|
211
|
-
("
|
|
212
|
-
(
|
|
208
|
+
panel_content = Text()
|
|
209
|
+
|
|
210
|
+
if e.show_klaude_code_info:
|
|
211
|
+
# First line: Klaude Code version
|
|
212
|
+
klaude_code_style = ThemeKey.WELCOME_DEBUG_TITLE if debug_mode else ThemeKey.WELCOME_HIGHLIGHT_BOLD
|
|
213
|
+
panel_content.append_text(Text("Klaude Code", style=klaude_code_style))
|
|
214
|
+
panel_content.append_text(Text(f" v{_get_version()}", style=ThemeKey.WELCOME_INFO))
|
|
215
|
+
panel_content.append_text(Text("\n"))
|
|
216
|
+
|
|
217
|
+
# Model line: model @ provider · params...
|
|
218
|
+
panel_content.append_text(
|
|
219
|
+
Text.assemble(
|
|
220
|
+
(str(e.llm_config.model), ThemeKey.WELCOME_HIGHLIGHT),
|
|
221
|
+
(" @ ", ThemeKey.WELCOME_INFO),
|
|
222
|
+
(e.llm_config.provider_name, ThemeKey.WELCOME_INFO),
|
|
223
|
+
)
|
|
213
224
|
)
|
|
214
225
|
|
|
215
226
|
# Use format_model_params for consistent formatting
|
|
@@ -228,7 +239,9 @@ def render_welcome(e: events.WelcomeEvent) -> RenderableType:
|
|
|
228
239
|
)
|
|
229
240
|
|
|
230
241
|
border_style = ThemeKey.WELCOME_DEBUG_BORDER if debug_mode else ThemeKey.LINES
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
"",
|
|
234
|
-
|
|
242
|
+
|
|
243
|
+
if e.show_klaude_code_info:
|
|
244
|
+
groups = ["", Quote(panel_content, style=border_style, prefix="▌ "), ""]
|
|
245
|
+
else:
|
|
246
|
+
groups = [Quote(panel_content, style=border_style, prefix="▌ "), ""]
|
|
247
|
+
return Group(*groups)
|
|
@@ -18,8 +18,13 @@ from rich.table import Table
|
|
|
18
18
|
from rich.text import Text
|
|
19
19
|
from rich.theme import Theme
|
|
20
20
|
|
|
21
|
-
from klaude_code.const import
|
|
22
|
-
|
|
21
|
+
from klaude_code.const import (
|
|
22
|
+
MARKDOWN_RIGHT_MARGIN,
|
|
23
|
+
MARKDOWN_STREAM_LIVE_REPAINT_ENABLED,
|
|
24
|
+
MARKDOWN_STREAM_SYNCHRONIZED_OUTPUT_ENABLED,
|
|
25
|
+
UI_REFRESH_RATE_FPS,
|
|
26
|
+
)
|
|
27
|
+
from klaude_code.tui.components.rich.code_panel import CodePanel
|
|
23
28
|
|
|
24
29
|
|
|
25
30
|
class NoInsetCodeBlock(CodeBlock):
|
|
@@ -56,7 +61,13 @@ class Divider(MarkdownElement):
|
|
|
56
61
|
|
|
57
62
|
class MarkdownTable(TableElement):
|
|
58
63
|
def __rich_console__(self, console: Console, options: ConsoleOptions) -> RenderResult:
|
|
59
|
-
|
|
64
|
+
# rich.box.MARKDOWN intentionally includes a blank top/bottom edge row. Rather than
|
|
65
|
+
# post-processing rendered segments, disable outer edges to avoid emitting those rows.
|
|
66
|
+
table = Table(
|
|
67
|
+
box=box.MARKDOWN,
|
|
68
|
+
show_edge=False,
|
|
69
|
+
border_style=console.get_style("markdown.table.border"),
|
|
70
|
+
)
|
|
60
71
|
|
|
61
72
|
if self.header is not None and self.header.row is not None:
|
|
62
73
|
for column in self.header.row.cells:
|
|
@@ -67,27 +78,7 @@ class MarkdownTable(TableElement):
|
|
|
67
78
|
row_content = [element.content for element in row.cells]
|
|
68
79
|
table.add_row(*row_content)
|
|
69
80
|
|
|
70
|
-
|
|
71
|
-
segments = list(console.render(table, options))
|
|
72
|
-
|
|
73
|
-
# Skip leading blank line (before first newline)
|
|
74
|
-
first_newline_idx = next((i for i, s in enumerate(segments) if s.text == "\n"), None)
|
|
75
|
-
if first_newline_idx is not None:
|
|
76
|
-
first_line = "".join(s.text for s in segments[:first_newline_idx])
|
|
77
|
-
if not first_line.strip():
|
|
78
|
-
segments = segments[first_newline_idx + 1 :]
|
|
79
|
-
|
|
80
|
-
# Skip trailing blank line (after last newline)
|
|
81
|
-
while len(segments) >= 2 and segments[-1].text == "\n":
|
|
82
|
-
prev_newline = next((i for i in range(len(segments) - 2, -1, -1) if segments[i].text == "\n"), None)
|
|
83
|
-
if prev_newline is not None:
|
|
84
|
-
between = "".join(s.text for s in segments[prev_newline + 1 : -1])
|
|
85
|
-
if not between.strip():
|
|
86
|
-
segments = segments[: prev_newline + 1]
|
|
87
|
-
continue
|
|
88
|
-
break
|
|
89
|
-
|
|
90
|
-
yield from segments
|
|
81
|
+
yield table
|
|
91
82
|
|
|
92
83
|
|
|
93
84
|
class LeftHeading(Heading):
|
|
@@ -201,6 +192,52 @@ class MarkdownStream:
|
|
|
201
192
|
def _get_base_width(self) -> int:
|
|
202
193
|
return self.console.options.max_width
|
|
203
194
|
|
|
195
|
+
def _should_use_synchronized_output(self) -> bool:
|
|
196
|
+
if not MARKDOWN_STREAM_SYNCHRONIZED_OUTPUT_ENABLED:
|
|
197
|
+
return False
|
|
198
|
+
if self._live_sink is None:
|
|
199
|
+
return False
|
|
200
|
+
console_file = getattr(self.console, "file", None)
|
|
201
|
+
if console_file is None:
|
|
202
|
+
return False
|
|
203
|
+
isatty = getattr(console_file, "isatty", None)
|
|
204
|
+
if isatty is None:
|
|
205
|
+
return False
|
|
206
|
+
return bool(isatty())
|
|
207
|
+
|
|
208
|
+
@contextlib.contextmanager
|
|
209
|
+
def _synchronized_output(self) -> Any:
|
|
210
|
+
"""Batch terminal updates to reduce flicker.
|
|
211
|
+
|
|
212
|
+
Uses xterm's "Synchronized Output" mode (DECSET/DECRST 2026). Terminals that
|
|
213
|
+
don't support it will typically ignore the escape codes.
|
|
214
|
+
"""
|
|
215
|
+
|
|
216
|
+
if not self._should_use_synchronized_output():
|
|
217
|
+
yield
|
|
218
|
+
return
|
|
219
|
+
|
|
220
|
+
console_file = self.console.file
|
|
221
|
+
enabled = False
|
|
222
|
+
try:
|
|
223
|
+
console_file.write("\x1b[?2026h")
|
|
224
|
+
flush = getattr(console_file, "flush", None)
|
|
225
|
+
if flush is not None:
|
|
226
|
+
flush()
|
|
227
|
+
enabled = True
|
|
228
|
+
except Exception:
|
|
229
|
+
pass
|
|
230
|
+
|
|
231
|
+
try:
|
|
232
|
+
yield
|
|
233
|
+
finally:
|
|
234
|
+
if enabled:
|
|
235
|
+
with contextlib.suppress(Exception):
|
|
236
|
+
console_file.write("\x1b[?2026l")
|
|
237
|
+
flush = getattr(console_file, "flush", None)
|
|
238
|
+
if flush is not None:
|
|
239
|
+
flush()
|
|
240
|
+
|
|
204
241
|
def compute_candidate_stable_line(self, text: str) -> int:
|
|
205
242
|
"""Return the start line of the last top-level block, or 0.
|
|
206
243
|
|
|
@@ -427,26 +464,22 @@ class MarkdownStream:
|
|
|
427
464
|
|
|
428
465
|
start = time.time()
|
|
429
466
|
|
|
467
|
+
stable_chunk_to_print: str | None = None
|
|
430
468
|
stable_changed = final or stable_line > self._stable_source_line_count
|
|
431
469
|
if stable_changed and stable_source:
|
|
432
470
|
stable_ansi = self.render_stable_ansi(stable_source, has_live_suffix=bool(live_source), final=final)
|
|
433
471
|
stable_lines = stable_ansi.splitlines(keepends=True)
|
|
434
472
|
new_lines = stable_lines[len(self._stable_rendered_lines) :]
|
|
435
473
|
if new_lines:
|
|
436
|
-
|
|
437
|
-
self.console.print(Text.from_ansi(stable_chunk), end="\n")
|
|
474
|
+
stable_chunk_to_print = "".join(new_lines)
|
|
438
475
|
self._stable_rendered_lines = stable_lines
|
|
439
476
|
self._stable_source_line_count = stable_line
|
|
440
477
|
elif final and not stable_source:
|
|
441
478
|
self._stable_rendered_lines = []
|
|
442
479
|
self._stable_source_line_count = stable_line
|
|
443
480
|
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
self._live_sink(None)
|
|
447
|
-
return
|
|
448
|
-
|
|
449
|
-
if MARKDOWN_STREAM_LIVE_REPAINT_ENABLED and self._live_sink is not None:
|
|
481
|
+
live_text_to_set: Text | None = None
|
|
482
|
+
if not final and MARKDOWN_STREAM_LIVE_REPAINT_ENABLED and self._live_sink is not None:
|
|
450
483
|
apply_mark_live = self._stable_source_line_count == 0
|
|
451
484
|
live_lines = self._render_markdown_to_lines(live_source, apply_mark=apply_mark_live)
|
|
452
485
|
|
|
@@ -468,8 +501,19 @@ class MarkdownStream:
|
|
|
468
501
|
if drop > 0:
|
|
469
502
|
live_lines = live_lines[drop:]
|
|
470
503
|
|
|
471
|
-
|
|
472
|
-
|
|
504
|
+
live_text_to_set = Text.from_ansi("".join(live_lines))
|
|
505
|
+
|
|
506
|
+
with self._synchronized_output():
|
|
507
|
+
if stable_chunk_to_print:
|
|
508
|
+
self.console.print(Text.from_ansi(stable_chunk_to_print), end="\n")
|
|
509
|
+
|
|
510
|
+
if final:
|
|
511
|
+
if self._live_sink is not None:
|
|
512
|
+
self._live_sink(None)
|
|
513
|
+
return
|
|
514
|
+
|
|
515
|
+
if live_text_to_set is not None and self._live_sink is not None:
|
|
516
|
+
self._live_sink(live_text_to_set)
|
|
473
517
|
|
|
474
518
|
elapsed = time.time() - start
|
|
475
519
|
self.min_delay = min(max(elapsed * 6, 1.0 / 30), 0.5)
|
|
@@ -23,8 +23,8 @@ from klaude_code.const import (
|
|
|
23
23
|
STATUS_SHIMMER_BAND_HALF_WIDTH,
|
|
24
24
|
STATUS_SHIMMER_PADDING,
|
|
25
25
|
)
|
|
26
|
-
from klaude_code.
|
|
27
|
-
from klaude_code.
|
|
26
|
+
from klaude_code.tui.components.rich.theme import ThemeKey
|
|
27
|
+
from klaude_code.tui.terminal.color import get_last_terminal_background_rgb
|
|
28
28
|
|
|
29
29
|
# Use an existing Rich spinner name; BreathingSpinner overrides its rendering
|
|
30
30
|
BREATHING_SPINNER_NAME = "dots"
|
|
@@ -53,7 +53,7 @@ LIGHT_PALETTE = Palette(
|
|
|
53
53
|
grey2="#93a4b1",
|
|
54
54
|
grey3="#c4ced4",
|
|
55
55
|
grey_green="#96a096",
|
|
56
|
-
purple="#
|
|
56
|
+
purple="#5f5fb7",
|
|
57
57
|
lavender="#5f87af",
|
|
58
58
|
diff_add="#2e5a32 on #dafbe1",
|
|
59
59
|
diff_add_char="#2e5a32 on #aceebb",
|
|
@@ -93,7 +93,7 @@ DARK_PALETTE = Palette(
|
|
|
93
93
|
diff_remove="#ffcdd2 on #3d1f23",
|
|
94
94
|
diff_remove_char="#ffcdd2 on #7a3a42",
|
|
95
95
|
code_theme="ansi_dark",
|
|
96
|
-
code_background="#
|
|
96
|
+
code_background="#1a1f2a",
|
|
97
97
|
green_background="#23342c",
|
|
98
98
|
blue_grey_background="#313848",
|
|
99
99
|
cyan_background="#1a3333",
|
|
@@ -110,6 +110,9 @@ DARK_PALETTE = Palette(
|
|
|
110
110
|
class ThemeKey(str, Enum):
|
|
111
111
|
LINES = "lines"
|
|
112
112
|
|
|
113
|
+
# CODE
|
|
114
|
+
CODE_BACKGROUND = "code_background"
|
|
115
|
+
|
|
113
116
|
# PANEL
|
|
114
117
|
SUB_AGENT_RESULT_PANEL = "panel.sub_agent_result"
|
|
115
118
|
WRITE_MARKDOWN_PANEL = "panel.write_markdown"
|
|
@@ -135,6 +138,7 @@ class ThemeKey(str, Enum):
|
|
|
135
138
|
STATUS_TEXT = "spinner.status.text"
|
|
136
139
|
STATUS_TEXT_BOLD = "spinner.status.text.bold"
|
|
137
140
|
STATUS_TEXT_BOLD_ITALIC = "spinner.status.text.bold_italic"
|
|
141
|
+
STATUS_TOAST = "spinner.status.toast"
|
|
138
142
|
# STATUS
|
|
139
143
|
STATUS_HINT = "status.hint"
|
|
140
144
|
# USER_INPUT
|
|
@@ -224,6 +228,8 @@ def get_theme(theme: str | None = None) -> Themes:
|
|
|
224
228
|
app_theme=Theme(
|
|
225
229
|
styles={
|
|
226
230
|
ThemeKey.LINES.value: palette.grey3,
|
|
231
|
+
# CODE
|
|
232
|
+
ThemeKey.CODE_BACKGROUND.value: f"on {palette.code_background}",
|
|
227
233
|
# PANEL
|
|
228
234
|
ThemeKey.SUB_AGENT_RESULT_PANEL.value: f"on {palette.blue_grey_background}",
|
|
229
235
|
ThemeKey.WRITE_MARKDOWN_PANEL.value: f"on {palette.green_background}",
|
|
@@ -257,10 +263,11 @@ def get_theme(theme: str | None = None) -> Themes:
|
|
|
257
263
|
ThemeKey.STATUS_TEXT.value: palette.blue,
|
|
258
264
|
ThemeKey.STATUS_TEXT_BOLD.value: "bold " + palette.blue,
|
|
259
265
|
ThemeKey.STATUS_TEXT_BOLD_ITALIC.value: "bold italic " + palette.blue,
|
|
266
|
+
ThemeKey.STATUS_TOAST.value: "bold " + palette.yellow,
|
|
260
267
|
ThemeKey.STATUS_HINT.value: palette.grey2,
|
|
261
268
|
# REMINDER
|
|
262
269
|
ThemeKey.REMINDER.value: palette.grey1,
|
|
263
|
-
ThemeKey.REMINDER_BOLD.value:
|
|
270
|
+
ThemeKey.REMINDER_BOLD.value: palette.grey1,
|
|
264
271
|
# TOOL
|
|
265
272
|
ThemeKey.INVALID_TOOL_CALL_ARGS.value: palette.yellow,
|
|
266
273
|
ThemeKey.TOOL_NAME.value: "bold",
|
|
@@ -268,9 +275,9 @@ def get_theme(theme: str | None = None) -> Themes:
|
|
|
268
275
|
ThemeKey.TOOL_PARAM.value: palette.green,
|
|
269
276
|
ThemeKey.TOOL_PARAM_BOLD.value: "bold " + palette.green,
|
|
270
277
|
ThemeKey.TOOL_RESULT.value: palette.grey_green,
|
|
271
|
-
ThemeKey.TOOL_RESULT_TREE_PREFIX.value: palette.
|
|
278
|
+
ThemeKey.TOOL_RESULT_TREE_PREFIX.value: palette.grey3 + " dim",
|
|
272
279
|
ThemeKey.TOOL_RESULT_BOLD.value: "bold " + palette.grey_green,
|
|
273
|
-
ThemeKey.TOOL_RESULT_TRUNCATED.value: palette.grey1,
|
|
280
|
+
ThemeKey.TOOL_RESULT_TRUNCATED.value: palette.grey1 + " dim",
|
|
274
281
|
ThemeKey.TOOL_MARK.value: "bold",
|
|
275
282
|
ThemeKey.TOOL_APPROVED.value: palette.green + " bold reverse",
|
|
276
283
|
ThemeKey.TOOL_REJECTED.value: palette.red + " bold reverse",
|