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.
Files changed (167) hide show
  1. klaude_code/__init__.py +0 -0
  2. klaude_code/cli/__init__.py +1 -0
  3. klaude_code/cli/main.py +298 -0
  4. klaude_code/cli/runtime.py +331 -0
  5. klaude_code/cli/session_cmd.py +80 -0
  6. klaude_code/command/__init__.py +43 -0
  7. klaude_code/command/clear_cmd.py +20 -0
  8. klaude_code/command/command_abc.py +92 -0
  9. klaude_code/command/diff_cmd.py +138 -0
  10. klaude_code/command/export_cmd.py +86 -0
  11. klaude_code/command/help_cmd.py +51 -0
  12. klaude_code/command/model_cmd.py +43 -0
  13. klaude_code/command/prompt-dev-docs-update.md +56 -0
  14. klaude_code/command/prompt-dev-docs.md +46 -0
  15. klaude_code/command/prompt-init.md +45 -0
  16. klaude_code/command/prompt_command.py +69 -0
  17. klaude_code/command/refresh_cmd.py +43 -0
  18. klaude_code/command/registry.py +110 -0
  19. klaude_code/command/status_cmd.py +111 -0
  20. klaude_code/command/terminal_setup_cmd.py +252 -0
  21. klaude_code/config/__init__.py +11 -0
  22. klaude_code/config/config.py +177 -0
  23. klaude_code/config/list_model.py +162 -0
  24. klaude_code/config/select_model.py +67 -0
  25. klaude_code/const/__init__.py +133 -0
  26. klaude_code/core/__init__.py +0 -0
  27. klaude_code/core/agent.py +165 -0
  28. klaude_code/core/executor.py +485 -0
  29. klaude_code/core/manager/__init__.py +19 -0
  30. klaude_code/core/manager/agent_manager.py +127 -0
  31. klaude_code/core/manager/llm_clients.py +42 -0
  32. klaude_code/core/manager/llm_clients_builder.py +49 -0
  33. klaude_code/core/manager/sub_agent_manager.py +86 -0
  34. klaude_code/core/prompt.py +89 -0
  35. klaude_code/core/prompts/prompt-claude-code.md +98 -0
  36. klaude_code/core/prompts/prompt-codex.md +331 -0
  37. klaude_code/core/prompts/prompt-gemini.md +43 -0
  38. klaude_code/core/prompts/prompt-subagent-explore.md +27 -0
  39. klaude_code/core/prompts/prompt-subagent-oracle.md +23 -0
  40. klaude_code/core/prompts/prompt-subagent-webfetch.md +46 -0
  41. klaude_code/core/prompts/prompt-subagent.md +8 -0
  42. klaude_code/core/reminders.py +445 -0
  43. klaude_code/core/task.py +237 -0
  44. klaude_code/core/tool/__init__.py +75 -0
  45. klaude_code/core/tool/file/__init__.py +0 -0
  46. klaude_code/core/tool/file/apply_patch.py +492 -0
  47. klaude_code/core/tool/file/apply_patch_tool.md +1 -0
  48. klaude_code/core/tool/file/apply_patch_tool.py +204 -0
  49. klaude_code/core/tool/file/edit_tool.md +9 -0
  50. klaude_code/core/tool/file/edit_tool.py +274 -0
  51. klaude_code/core/tool/file/multi_edit_tool.md +42 -0
  52. klaude_code/core/tool/file/multi_edit_tool.py +199 -0
  53. klaude_code/core/tool/file/read_tool.md +14 -0
  54. klaude_code/core/tool/file/read_tool.py +326 -0
  55. klaude_code/core/tool/file/write_tool.md +8 -0
  56. klaude_code/core/tool/file/write_tool.py +146 -0
  57. klaude_code/core/tool/memory/__init__.py +0 -0
  58. klaude_code/core/tool/memory/memory_tool.md +16 -0
  59. klaude_code/core/tool/memory/memory_tool.py +462 -0
  60. klaude_code/core/tool/memory/skill_loader.py +245 -0
  61. klaude_code/core/tool/memory/skill_tool.md +24 -0
  62. klaude_code/core/tool/memory/skill_tool.py +97 -0
  63. klaude_code/core/tool/shell/__init__.py +0 -0
  64. klaude_code/core/tool/shell/bash_tool.md +43 -0
  65. klaude_code/core/tool/shell/bash_tool.py +123 -0
  66. klaude_code/core/tool/shell/command_safety.py +363 -0
  67. klaude_code/core/tool/sub_agent_tool.py +83 -0
  68. klaude_code/core/tool/todo/__init__.py +0 -0
  69. klaude_code/core/tool/todo/todo_write_tool.md +182 -0
  70. klaude_code/core/tool/todo/todo_write_tool.py +121 -0
  71. klaude_code/core/tool/todo/update_plan_tool.md +3 -0
  72. klaude_code/core/tool/todo/update_plan_tool.py +104 -0
  73. klaude_code/core/tool/tool_abc.py +25 -0
  74. klaude_code/core/tool/tool_context.py +106 -0
  75. klaude_code/core/tool/tool_registry.py +78 -0
  76. klaude_code/core/tool/tool_runner.py +252 -0
  77. klaude_code/core/tool/truncation.py +170 -0
  78. klaude_code/core/tool/web/__init__.py +0 -0
  79. klaude_code/core/tool/web/mermaid_tool.md +21 -0
  80. klaude_code/core/tool/web/mermaid_tool.py +76 -0
  81. klaude_code/core/tool/web/web_fetch_tool.md +8 -0
  82. klaude_code/core/tool/web/web_fetch_tool.py +159 -0
  83. klaude_code/core/turn.py +220 -0
  84. klaude_code/llm/__init__.py +21 -0
  85. klaude_code/llm/anthropic/__init__.py +3 -0
  86. klaude_code/llm/anthropic/client.py +221 -0
  87. klaude_code/llm/anthropic/input.py +200 -0
  88. klaude_code/llm/client.py +49 -0
  89. klaude_code/llm/input_common.py +239 -0
  90. klaude_code/llm/openai_compatible/__init__.py +3 -0
  91. klaude_code/llm/openai_compatible/client.py +211 -0
  92. klaude_code/llm/openai_compatible/input.py +109 -0
  93. klaude_code/llm/openai_compatible/tool_call_accumulator.py +80 -0
  94. klaude_code/llm/openrouter/__init__.py +3 -0
  95. klaude_code/llm/openrouter/client.py +200 -0
  96. klaude_code/llm/openrouter/input.py +160 -0
  97. klaude_code/llm/openrouter/reasoning_handler.py +209 -0
  98. klaude_code/llm/registry.py +22 -0
  99. klaude_code/llm/responses/__init__.py +3 -0
  100. klaude_code/llm/responses/client.py +216 -0
  101. klaude_code/llm/responses/input.py +167 -0
  102. klaude_code/llm/usage.py +109 -0
  103. klaude_code/protocol/__init__.py +4 -0
  104. klaude_code/protocol/commands.py +21 -0
  105. klaude_code/protocol/events.py +163 -0
  106. klaude_code/protocol/llm_param.py +147 -0
  107. klaude_code/protocol/model.py +287 -0
  108. klaude_code/protocol/op.py +89 -0
  109. klaude_code/protocol/op_handler.py +28 -0
  110. klaude_code/protocol/sub_agent.py +348 -0
  111. klaude_code/protocol/tools.py +15 -0
  112. klaude_code/session/__init__.py +4 -0
  113. klaude_code/session/export.py +624 -0
  114. klaude_code/session/selector.py +76 -0
  115. klaude_code/session/session.py +474 -0
  116. klaude_code/session/templates/export_session.html +1434 -0
  117. klaude_code/trace/__init__.py +3 -0
  118. klaude_code/trace/log.py +168 -0
  119. klaude_code/ui/__init__.py +91 -0
  120. klaude_code/ui/core/__init__.py +1 -0
  121. klaude_code/ui/core/display.py +103 -0
  122. klaude_code/ui/core/input.py +71 -0
  123. klaude_code/ui/core/stage_manager.py +55 -0
  124. klaude_code/ui/modes/__init__.py +1 -0
  125. klaude_code/ui/modes/debug/__init__.py +1 -0
  126. klaude_code/ui/modes/debug/display.py +36 -0
  127. klaude_code/ui/modes/exec/__init__.py +1 -0
  128. klaude_code/ui/modes/exec/display.py +63 -0
  129. klaude_code/ui/modes/repl/__init__.py +51 -0
  130. klaude_code/ui/modes/repl/clipboard.py +152 -0
  131. klaude_code/ui/modes/repl/completers.py +429 -0
  132. klaude_code/ui/modes/repl/display.py +60 -0
  133. klaude_code/ui/modes/repl/event_handler.py +375 -0
  134. klaude_code/ui/modes/repl/input_prompt_toolkit.py +198 -0
  135. klaude_code/ui/modes/repl/key_bindings.py +170 -0
  136. klaude_code/ui/modes/repl/renderer.py +281 -0
  137. klaude_code/ui/renderers/__init__.py +0 -0
  138. klaude_code/ui/renderers/assistant.py +21 -0
  139. klaude_code/ui/renderers/common.py +8 -0
  140. klaude_code/ui/renderers/developer.py +158 -0
  141. klaude_code/ui/renderers/diffs.py +215 -0
  142. klaude_code/ui/renderers/errors.py +16 -0
  143. klaude_code/ui/renderers/metadata.py +190 -0
  144. klaude_code/ui/renderers/sub_agent.py +71 -0
  145. klaude_code/ui/renderers/thinking.py +39 -0
  146. klaude_code/ui/renderers/tools.py +551 -0
  147. klaude_code/ui/renderers/user_input.py +65 -0
  148. klaude_code/ui/rich/__init__.py +1 -0
  149. klaude_code/ui/rich/live.py +65 -0
  150. klaude_code/ui/rich/markdown.py +308 -0
  151. klaude_code/ui/rich/quote.py +34 -0
  152. klaude_code/ui/rich/searchable_text.py +71 -0
  153. klaude_code/ui/rich/status.py +240 -0
  154. klaude_code/ui/rich/theme.py +274 -0
  155. klaude_code/ui/terminal/__init__.py +1 -0
  156. klaude_code/ui/terminal/color.py +244 -0
  157. klaude_code/ui/terminal/control.py +147 -0
  158. klaude_code/ui/terminal/notifier.py +107 -0
  159. klaude_code/ui/terminal/progress_bar.py +87 -0
  160. klaude_code/ui/utils/__init__.py +1 -0
  161. klaude_code/ui/utils/common.py +108 -0
  162. klaude_code/ui/utils/debouncer.py +42 -0
  163. klaude_code/version.py +163 -0
  164. klaude_code-1.2.6.dist-info/METADATA +178 -0
  165. klaude_code-1.2.6.dist-info/RECORD +167 -0
  166. klaude_code-1.2.6.dist-info/WHEEL +4 -0
  167. 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
+ )