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.
Files changed (160) hide show
  1. klaude_code/app/__init__.py +12 -0
  2. klaude_code/app/runtime.py +215 -0
  3. klaude_code/cli/auth_cmd.py +2 -2
  4. klaude_code/cli/config_cmd.py +2 -2
  5. klaude_code/cli/cost_cmd.py +1 -1
  6. klaude_code/cli/debug.py +12 -36
  7. klaude_code/cli/list_model.py +3 -3
  8. klaude_code/cli/main.py +17 -60
  9. klaude_code/cli/self_update.py +2 -187
  10. klaude_code/cli/session_cmd.py +2 -2
  11. klaude_code/config/config.py +1 -1
  12. klaude_code/config/select_model.py +1 -1
  13. klaude_code/const.py +10 -1
  14. klaude_code/core/agent.py +9 -62
  15. klaude_code/core/agent_profile.py +284 -0
  16. klaude_code/core/executor.py +343 -230
  17. klaude_code/core/manager/llm_clients_builder.py +1 -1
  18. klaude_code/core/manager/sub_agent_manager.py +16 -29
  19. klaude_code/core/reminders.py +107 -155
  20. klaude_code/core/task.py +12 -20
  21. klaude_code/core/tool/__init__.py +5 -19
  22. klaude_code/core/tool/context.py +84 -0
  23. klaude_code/core/tool/file/apply_patch_tool.py +18 -21
  24. klaude_code/core/tool/file/edit_tool.py +42 -44
  25. klaude_code/core/tool/file/read_tool.py +14 -9
  26. klaude_code/core/tool/file/write_tool.py +12 -13
  27. klaude_code/core/tool/report_back_tool.py +4 -1
  28. klaude_code/core/tool/shell/bash_tool.py +6 -11
  29. klaude_code/core/tool/skill/skill_tool.py +3 -1
  30. klaude_code/core/tool/sub_agent_tool.py +8 -7
  31. klaude_code/core/tool/todo/todo_write_tool.py +3 -9
  32. klaude_code/core/tool/todo/update_plan_tool.py +3 -5
  33. klaude_code/core/tool/tool_abc.py +2 -1
  34. klaude_code/core/tool/tool_registry.py +2 -33
  35. klaude_code/core/tool/tool_runner.py +13 -10
  36. klaude_code/core/tool/web/mermaid_tool.py +3 -1
  37. klaude_code/core/tool/web/web_fetch_tool.py +5 -3
  38. klaude_code/core/tool/web/web_search_tool.py +5 -3
  39. klaude_code/core/turn.py +86 -26
  40. klaude_code/llm/anthropic/client.py +1 -1
  41. klaude_code/llm/bedrock/client.py +1 -1
  42. klaude_code/llm/claude/client.py +1 -1
  43. klaude_code/llm/codex/client.py +1 -1
  44. klaude_code/llm/google/client.py +1 -1
  45. klaude_code/llm/openai_compatible/client.py +1 -1
  46. klaude_code/llm/openai_compatible/tool_call_accumulator.py +1 -1
  47. klaude_code/llm/openrouter/client.py +1 -1
  48. klaude_code/llm/openrouter/reasoning.py +1 -1
  49. klaude_code/llm/responses/client.py +1 -1
  50. klaude_code/protocol/events/__init__.py +57 -0
  51. klaude_code/protocol/events/base.py +18 -0
  52. klaude_code/protocol/events/chat.py +20 -0
  53. klaude_code/protocol/events/lifecycle.py +22 -0
  54. klaude_code/protocol/events/metadata.py +15 -0
  55. klaude_code/protocol/events/streaming.py +43 -0
  56. klaude_code/protocol/events/system.py +53 -0
  57. klaude_code/protocol/events/tools.py +23 -0
  58. klaude_code/protocol/message.py +3 -11
  59. klaude_code/protocol/model.py +78 -9
  60. klaude_code/protocol/op.py +5 -0
  61. klaude_code/protocol/sub_agent/explore.py +0 -15
  62. klaude_code/protocol/sub_agent/task.py +1 -1
  63. klaude_code/protocol/sub_agent/web.py +1 -17
  64. klaude_code/protocol/tools.py +0 -1
  65. klaude_code/session/session.py +6 -5
  66. klaude_code/skill/assets/create-plan/SKILL.md +76 -0
  67. klaude_code/skill/loader.py +1 -1
  68. klaude_code/skill/system_skills.py +1 -1
  69. klaude_code/tui/__init__.py +8 -0
  70. klaude_code/{command → tui/command}/clear_cmd.py +2 -1
  71. klaude_code/{command → tui/command}/debug_cmd.py +4 -3
  72. klaude_code/{command → tui/command}/export_cmd.py +2 -1
  73. klaude_code/{command → tui/command}/export_online_cmd.py +6 -5
  74. klaude_code/{command → tui/command}/fork_session_cmd.py +10 -9
  75. klaude_code/{command → tui/command}/help_cmd.py +3 -2
  76. klaude_code/{command → tui/command}/model_cmd.py +5 -4
  77. klaude_code/{command → tui/command}/model_select.py +2 -2
  78. klaude_code/{command → tui/command}/prompt_command.py +4 -3
  79. klaude_code/{command → tui/command}/refresh_cmd.py +3 -1
  80. klaude_code/{command → tui/command}/registry.py +16 -6
  81. klaude_code/{command → tui/command}/release_notes_cmd.py +3 -2
  82. klaude_code/{command → tui/command}/resume_cmd.py +6 -5
  83. klaude_code/{command → tui/command}/status_cmd.py +4 -3
  84. klaude_code/{command → tui/command}/terminal_setup_cmd.py +4 -3
  85. klaude_code/{command → tui/command}/thinking_cmd.py +4 -3
  86. klaude_code/tui/commands.py +164 -0
  87. klaude_code/{ui/renderers → tui/components}/assistant.py +3 -3
  88. klaude_code/{ui/renderers → tui/components}/bash_syntax.py +2 -2
  89. klaude_code/{ui/renderers → tui/components}/common.py +1 -1
  90. klaude_code/tui/components/developer.py +231 -0
  91. klaude_code/{ui/renderers → tui/components}/diffs.py +2 -2
  92. klaude_code/{ui/renderers → tui/components}/errors.py +2 -2
  93. klaude_code/{ui/renderers → tui/components}/metadata.py +34 -21
  94. klaude_code/{ui → tui/components}/rich/markdown.py +78 -34
  95. klaude_code/{ui → tui/components}/rich/status.py +2 -2
  96. klaude_code/{ui → tui/components}/rich/theme.py +12 -5
  97. klaude_code/{ui/renderers → tui/components}/sub_agent.py +23 -43
  98. klaude_code/{ui/renderers → tui/components}/thinking.py +3 -3
  99. klaude_code/{ui/renderers → tui/components}/tools.py +11 -48
  100. klaude_code/{ui/renderers → tui/components}/user_input.py +3 -20
  101. klaude_code/tui/display.py +85 -0
  102. klaude_code/{ui/modes/repl → tui/input}/__init__.py +1 -1
  103. klaude_code/{ui/modes/repl → tui/input}/completers.py +1 -1
  104. klaude_code/{ui/modes/repl/input_prompt_toolkit.py → tui/input/prompt_toolkit.py} +11 -7
  105. klaude_code/tui/machine.py +606 -0
  106. klaude_code/tui/renderer.py +707 -0
  107. klaude_code/tui/runner.py +321 -0
  108. klaude_code/tui/terminal/__init__.py +56 -0
  109. klaude_code/{ui → tui}/terminal/color.py +1 -1
  110. klaude_code/{ui → tui}/terminal/control.py +1 -1
  111. klaude_code/{ui → tui}/terminal/notifier.py +1 -1
  112. klaude_code/{ui → tui}/terminal/selector.py +36 -17
  113. klaude_code/ui/__init__.py +6 -50
  114. klaude_code/ui/core/display.py +3 -3
  115. klaude_code/ui/core/input.py +2 -1
  116. klaude_code/ui/{modes/debug/display.py → debug_mode.py} +1 -1
  117. klaude_code/ui/{modes/exec/display.py → exec_mode.py} +1 -4
  118. klaude_code/ui/terminal/__init__.py +6 -54
  119. klaude_code/ui/terminal/title.py +31 -0
  120. klaude_code/update.py +163 -0
  121. {klaude_code-2.0.1.dist-info → klaude_code-2.1.0.dist-info}/METADATA +1 -1
  122. klaude_code-2.1.0.dist-info/RECORD +235 -0
  123. klaude_code/cli/runtime.py +0 -525
  124. klaude_code/core/prompt.py +0 -108
  125. klaude_code/core/tool/file/move_tool.md +0 -41
  126. klaude_code/core/tool/file/move_tool.py +0 -435
  127. klaude_code/core/tool/tool_context.py +0 -148
  128. klaude_code/protocol/events.py +0 -194
  129. klaude_code/skill/assets/dev-docs/SKILL.md +0 -108
  130. klaude_code/trace/__init__.py +0 -21
  131. klaude_code/ui/core/stage_manager.py +0 -48
  132. klaude_code/ui/modes/__init__.py +0 -1
  133. klaude_code/ui/modes/debug/__init__.py +0 -1
  134. klaude_code/ui/modes/exec/__init__.py +0 -1
  135. klaude_code/ui/modes/repl/display.py +0 -61
  136. klaude_code/ui/modes/repl/event_handler.py +0 -634
  137. klaude_code/ui/modes/repl/renderer.py +0 -463
  138. klaude_code/ui/renderers/developer.py +0 -215
  139. klaude_code/ui/utils/__init__.py +0 -1
  140. klaude_code-2.0.1.dist-info/RECORD +0 -229
  141. /klaude_code/{trace/log.py → log.py} +0 -0
  142. /klaude_code/{command → tui/command}/__init__.py +0 -0
  143. /klaude_code/{command → tui/command}/command_abc.py +0 -0
  144. /klaude_code/{command → tui/command}/prompt-commit.md +0 -0
  145. /klaude_code/{command → tui/command}/prompt-init.md +0 -0
  146. /klaude_code/{ui/renderers → tui/components}/__init__.py +0 -0
  147. /klaude_code/{ui/renderers → tui/components}/mermaid_viewer.py +0 -0
  148. /klaude_code/{ui → tui/components}/rich/__init__.py +0 -0
  149. /klaude_code/{ui → tui/components}/rich/cjk_wrap.py +0 -0
  150. /klaude_code/{ui → tui/components}/rich/code_panel.py +0 -0
  151. /klaude_code/{ui → tui/components}/rich/live.py +0 -0
  152. /klaude_code/{ui → tui/components}/rich/quote.py +0 -0
  153. /klaude_code/{ui → tui/components}/rich/searchable_text.py +0 -0
  154. /klaude_code/{ui/modes/repl → tui/input}/clipboard.py +0 -0
  155. /klaude_code/{ui/modes/repl → tui/input}/key_bindings.py +0 -0
  156. /klaude_code/{ui → tui}/terminal/image.py +0 -0
  157. /klaude_code/{ui → tui}/terminal/progress_bar.py +0 -0
  158. /klaude_code/ui/{utils/common.py → common.py} +0 -0
  159. {klaude_code-2.0.1.dist-info → klaude_code-2.1.0.dist-info}/WHEEL +0 -0
  160. {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.ui.renderers.common import create_grid
10
- from klaude_code.ui.rich.theme import ThemeKey
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.ui.renderers.common import create_grid
5
- from klaude_code.ui.rich.theme import ThemeKey
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.trace import is_debug_enabled
12
- from klaude_code.ui.renderers.common import create_grid
13
- from klaude_code.ui.rich.theme import ThemeKey
14
- from klaude_code.ui.utils.common import format_model_params, format_number
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 (only for main agent)
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=False))
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
- # First line: Klaude Code version
206
- klaude_code_style = ThemeKey.WELCOME_DEBUG_TITLE if debug_mode else ThemeKey.WELCOME_HIGHLIGHT_BOLD
207
- panel_content = Text.assemble(
208
- ("Klaude Code", klaude_code_style),
209
- (f" v{_get_version()}\n", ThemeKey.WELCOME_INFO),
210
- (str(e.llm_config.model), ThemeKey.WELCOME_HIGHLIGHT),
211
- (" @ ", ThemeKey.WELCOME_INFO),
212
- (e.llm_config.provider_name, ThemeKey.WELCOME_INFO),
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
- return Group(
232
- Panel.fit(panel_content, border_style=border_style, box=box.ROUNDED),
233
- "", # empty line
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 MARKDOWN_RIGHT_MARGIN, MARKDOWN_STREAM_LIVE_REPAINT_ENABLED, UI_REFRESH_RATE_FPS
22
- from klaude_code.ui.rich.code_panel import CodePanel
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
- table = Table(box=box.MARKDOWN, border_style=console.get_style("markdown.table.border"))
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
- # Render table and strip top/bottom blank lines that MARKDOWN box adds
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
- stable_chunk = "".join(new_lines)
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
- if final:
445
- if self._live_sink is not None:
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
- live_text = Text.from_ansi("".join(live_lines))
472
- self._live_sink(live_text)
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.ui.rich.theme import ThemeKey
27
- from klaude_code.ui.terminal.color import get_last_terminal_background_rgb
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="#5f5fd7",
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="#2f3440",
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: "bold " + palette.grey1,
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.grey_green + " dim",
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",