klaude-code 1.2.6__py3-none-any.whl → 1.8.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 (205) hide show
  1. klaude_code/auth/__init__.py +24 -0
  2. klaude_code/auth/codex/__init__.py +20 -0
  3. klaude_code/auth/codex/exceptions.py +17 -0
  4. klaude_code/auth/codex/jwt_utils.py +45 -0
  5. klaude_code/auth/codex/oauth.py +229 -0
  6. klaude_code/auth/codex/token_manager.py +84 -0
  7. klaude_code/cli/auth_cmd.py +73 -0
  8. klaude_code/cli/config_cmd.py +91 -0
  9. klaude_code/cli/cost_cmd.py +338 -0
  10. klaude_code/cli/debug.py +78 -0
  11. klaude_code/cli/list_model.py +307 -0
  12. klaude_code/cli/main.py +233 -134
  13. klaude_code/cli/runtime.py +309 -117
  14. klaude_code/{version.py → cli/self_update.py} +114 -5
  15. klaude_code/cli/session_cmd.py +37 -21
  16. klaude_code/command/__init__.py +88 -27
  17. klaude_code/command/clear_cmd.py +8 -7
  18. klaude_code/command/command_abc.py +31 -31
  19. klaude_code/command/debug_cmd.py +79 -0
  20. klaude_code/command/export_cmd.py +19 -53
  21. klaude_code/command/export_online_cmd.py +154 -0
  22. klaude_code/command/fork_session_cmd.py +267 -0
  23. klaude_code/command/help_cmd.py +7 -8
  24. klaude_code/command/model_cmd.py +60 -10
  25. klaude_code/command/model_select.py +84 -0
  26. klaude_code/command/prompt-jj-describe.md +32 -0
  27. klaude_code/command/prompt_command.py +19 -11
  28. klaude_code/command/refresh_cmd.py +8 -10
  29. klaude_code/command/registry.py +139 -40
  30. klaude_code/command/release_notes_cmd.py +84 -0
  31. klaude_code/command/resume_cmd.py +111 -0
  32. klaude_code/command/status_cmd.py +104 -60
  33. klaude_code/command/terminal_setup_cmd.py +7 -9
  34. klaude_code/command/thinking_cmd.py +98 -0
  35. klaude_code/config/__init__.py +14 -6
  36. klaude_code/config/assets/__init__.py +1 -0
  37. klaude_code/config/assets/builtin_config.yaml +303 -0
  38. klaude_code/config/builtin_config.py +38 -0
  39. klaude_code/config/config.py +378 -109
  40. klaude_code/config/select_model.py +117 -53
  41. klaude_code/config/thinking.py +269 -0
  42. klaude_code/{const/__init__.py → const.py} +50 -19
  43. klaude_code/core/agent.py +20 -28
  44. klaude_code/core/executor.py +327 -112
  45. klaude_code/core/manager/__init__.py +2 -4
  46. klaude_code/core/manager/llm_clients.py +1 -15
  47. klaude_code/core/manager/llm_clients_builder.py +10 -11
  48. klaude_code/core/manager/sub_agent_manager.py +37 -6
  49. klaude_code/core/prompt.py +63 -44
  50. klaude_code/core/prompts/prompt-claude-code.md +2 -13
  51. klaude_code/core/prompts/prompt-codex-gpt-5-1-codex-max.md +117 -0
  52. klaude_code/core/prompts/prompt-codex-gpt-5-2-codex.md +117 -0
  53. klaude_code/core/prompts/prompt-codex.md +9 -42
  54. klaude_code/core/prompts/prompt-minimal.md +12 -0
  55. klaude_code/core/prompts/{prompt-subagent-explore.md → prompt-sub-agent-explore.md} +16 -3
  56. klaude_code/core/prompts/{prompt-subagent-oracle.md → prompt-sub-agent-oracle.md} +1 -2
  57. klaude_code/core/prompts/prompt-sub-agent-web.md +51 -0
  58. klaude_code/core/reminders.py +283 -95
  59. klaude_code/core/task.py +113 -75
  60. klaude_code/core/tool/__init__.py +24 -31
  61. klaude_code/core/tool/file/_utils.py +36 -0
  62. klaude_code/core/tool/file/apply_patch.py +17 -25
  63. klaude_code/core/tool/file/apply_patch_tool.py +57 -77
  64. klaude_code/core/tool/file/diff_builder.py +151 -0
  65. klaude_code/core/tool/file/edit_tool.py +50 -63
  66. klaude_code/core/tool/file/move_tool.md +41 -0
  67. klaude_code/core/tool/file/move_tool.py +435 -0
  68. klaude_code/core/tool/file/read_tool.md +1 -1
  69. klaude_code/core/tool/file/read_tool.py +86 -86
  70. klaude_code/core/tool/file/write_tool.py +59 -69
  71. klaude_code/core/tool/report_back_tool.py +84 -0
  72. klaude_code/core/tool/shell/bash_tool.py +265 -22
  73. klaude_code/core/tool/shell/command_safety.py +3 -6
  74. klaude_code/core/tool/{memory → skill}/skill_tool.py +16 -26
  75. klaude_code/core/tool/sub_agent_tool.py +13 -2
  76. klaude_code/core/tool/todo/todo_write_tool.md +0 -157
  77. klaude_code/core/tool/todo/todo_write_tool.py +1 -1
  78. klaude_code/core/tool/todo/todo_write_tool_raw.md +182 -0
  79. klaude_code/core/tool/todo/update_plan_tool.py +1 -1
  80. klaude_code/core/tool/tool_abc.py +18 -0
  81. klaude_code/core/tool/tool_context.py +27 -12
  82. klaude_code/core/tool/tool_registry.py +7 -7
  83. klaude_code/core/tool/tool_runner.py +44 -36
  84. klaude_code/core/tool/truncation.py +29 -14
  85. klaude_code/core/tool/web/mermaid_tool.md +43 -0
  86. klaude_code/core/tool/web/mermaid_tool.py +2 -5
  87. klaude_code/core/tool/web/web_fetch_tool.md +1 -1
  88. klaude_code/core/tool/web/web_fetch_tool.py +112 -22
  89. klaude_code/core/tool/web/web_search_tool.md +23 -0
  90. klaude_code/core/tool/web/web_search_tool.py +130 -0
  91. klaude_code/core/turn.py +168 -66
  92. klaude_code/llm/__init__.py +2 -10
  93. klaude_code/llm/anthropic/client.py +190 -178
  94. klaude_code/llm/anthropic/input.py +39 -15
  95. klaude_code/llm/bedrock/__init__.py +3 -0
  96. klaude_code/llm/bedrock/client.py +60 -0
  97. klaude_code/llm/client.py +7 -21
  98. klaude_code/llm/codex/__init__.py +5 -0
  99. klaude_code/llm/codex/client.py +149 -0
  100. klaude_code/llm/google/__init__.py +3 -0
  101. klaude_code/llm/google/client.py +309 -0
  102. klaude_code/llm/google/input.py +215 -0
  103. klaude_code/llm/input_common.py +3 -9
  104. klaude_code/llm/openai_compatible/client.py +72 -164
  105. klaude_code/llm/openai_compatible/input.py +6 -4
  106. klaude_code/llm/openai_compatible/stream.py +273 -0
  107. klaude_code/llm/openai_compatible/tool_call_accumulator.py +17 -1
  108. klaude_code/llm/openrouter/client.py +89 -160
  109. klaude_code/llm/openrouter/input.py +18 -30
  110. klaude_code/llm/openrouter/reasoning.py +118 -0
  111. klaude_code/llm/registry.py +39 -7
  112. klaude_code/llm/responses/client.py +184 -171
  113. klaude_code/llm/responses/input.py +20 -1
  114. klaude_code/llm/usage.py +17 -12
  115. klaude_code/protocol/commands.py +17 -1
  116. klaude_code/protocol/events.py +31 -4
  117. klaude_code/protocol/llm_param.py +13 -10
  118. klaude_code/protocol/model.py +232 -29
  119. klaude_code/protocol/op.py +90 -1
  120. klaude_code/protocol/op_handler.py +35 -1
  121. klaude_code/protocol/sub_agent/__init__.py +117 -0
  122. klaude_code/protocol/sub_agent/explore.py +63 -0
  123. klaude_code/protocol/sub_agent/oracle.py +91 -0
  124. klaude_code/protocol/sub_agent/task.py +61 -0
  125. klaude_code/protocol/sub_agent/web.py +79 -0
  126. klaude_code/protocol/tools.py +4 -2
  127. klaude_code/session/__init__.py +2 -2
  128. klaude_code/session/codec.py +71 -0
  129. klaude_code/session/export.py +293 -86
  130. klaude_code/session/selector.py +89 -67
  131. klaude_code/session/session.py +320 -309
  132. klaude_code/session/store.py +220 -0
  133. klaude_code/session/templates/export_session.html +595 -83
  134. klaude_code/session/templates/mermaid_viewer.html +926 -0
  135. klaude_code/skill/__init__.py +27 -0
  136. klaude_code/skill/assets/deslop/SKILL.md +17 -0
  137. klaude_code/skill/assets/dev-docs/SKILL.md +108 -0
  138. klaude_code/skill/assets/handoff/SKILL.md +39 -0
  139. klaude_code/skill/assets/jj-workspace/SKILL.md +20 -0
  140. klaude_code/skill/assets/skill-creator/SKILL.md +139 -0
  141. klaude_code/{core/tool/memory/skill_loader.py → skill/loader.py} +55 -15
  142. klaude_code/skill/manager.py +70 -0
  143. klaude_code/skill/system_skills.py +192 -0
  144. klaude_code/trace/__init__.py +20 -2
  145. klaude_code/trace/log.py +150 -5
  146. klaude_code/ui/__init__.py +4 -9
  147. klaude_code/ui/core/input.py +1 -1
  148. klaude_code/ui/core/stage_manager.py +7 -7
  149. klaude_code/ui/modes/debug/display.py +2 -1
  150. klaude_code/ui/modes/repl/__init__.py +3 -48
  151. klaude_code/ui/modes/repl/clipboard.py +5 -5
  152. klaude_code/ui/modes/repl/completers.py +487 -123
  153. klaude_code/ui/modes/repl/display.py +5 -4
  154. klaude_code/ui/modes/repl/event_handler.py +370 -117
  155. klaude_code/ui/modes/repl/input_prompt_toolkit.py +552 -105
  156. klaude_code/ui/modes/repl/key_bindings.py +146 -23
  157. klaude_code/ui/modes/repl/renderer.py +189 -99
  158. klaude_code/ui/renderers/assistant.py +9 -2
  159. klaude_code/ui/renderers/bash_syntax.py +178 -0
  160. klaude_code/ui/renderers/common.py +78 -0
  161. klaude_code/ui/renderers/developer.py +104 -48
  162. klaude_code/ui/renderers/diffs.py +87 -6
  163. klaude_code/ui/renderers/errors.py +11 -6
  164. klaude_code/ui/renderers/mermaid_viewer.py +57 -0
  165. klaude_code/ui/renderers/metadata.py +112 -76
  166. klaude_code/ui/renderers/sub_agent.py +92 -7
  167. klaude_code/ui/renderers/thinking.py +40 -18
  168. klaude_code/ui/renderers/tools.py +405 -227
  169. klaude_code/ui/renderers/user_input.py +73 -13
  170. klaude_code/ui/rich/__init__.py +10 -1
  171. klaude_code/ui/rich/cjk_wrap.py +228 -0
  172. klaude_code/ui/rich/code_panel.py +131 -0
  173. klaude_code/ui/rich/live.py +17 -0
  174. klaude_code/ui/rich/markdown.py +305 -170
  175. klaude_code/ui/rich/searchable_text.py +10 -13
  176. klaude_code/ui/rich/status.py +190 -49
  177. klaude_code/ui/rich/theme.py +135 -39
  178. klaude_code/ui/terminal/__init__.py +55 -0
  179. klaude_code/ui/terminal/color.py +1 -1
  180. klaude_code/ui/terminal/control.py +13 -22
  181. klaude_code/ui/terminal/notifier.py +44 -4
  182. klaude_code/ui/terminal/selector.py +658 -0
  183. klaude_code/ui/utils/common.py +0 -18
  184. klaude_code-1.8.0.dist-info/METADATA +377 -0
  185. klaude_code-1.8.0.dist-info/RECORD +219 -0
  186. {klaude_code-1.2.6.dist-info → klaude_code-1.8.0.dist-info}/entry_points.txt +1 -0
  187. klaude_code/command/diff_cmd.py +0 -138
  188. klaude_code/command/prompt-dev-docs-update.md +0 -56
  189. klaude_code/command/prompt-dev-docs.md +0 -46
  190. klaude_code/config/list_model.py +0 -162
  191. klaude_code/core/manager/agent_manager.py +0 -127
  192. klaude_code/core/prompts/prompt-subagent-webfetch.md +0 -46
  193. klaude_code/core/tool/file/multi_edit_tool.md +0 -42
  194. klaude_code/core/tool/file/multi_edit_tool.py +0 -199
  195. klaude_code/core/tool/memory/memory_tool.md +0 -16
  196. klaude_code/core/tool/memory/memory_tool.py +0 -462
  197. klaude_code/llm/openrouter/reasoning_handler.py +0 -209
  198. klaude_code/protocol/sub_agent.py +0 -348
  199. klaude_code/ui/utils/debouncer.py +0 -42
  200. klaude_code-1.2.6.dist-info/METADATA +0 -178
  201. klaude_code-1.2.6.dist-info/RECORD +0 -167
  202. /klaude_code/core/prompts/{prompt-subagent.md → prompt-sub-agent.md} +0 -0
  203. /klaude_code/core/tool/{memory → skill}/__init__.py +0 -0
  204. /klaude_code/core/tool/{memory → skill}/skill_tool.md +0 -0
  205. {klaude_code-1.2.6.dist-info → klaude_code-1.8.0.dist-info}/WHEEL +0 -0
@@ -1,7 +1,14 @@
1
1
  from rich.console import RenderableType
2
+ from rich.padding import Padding
3
+ from rich.text import Text
2
4
 
5
+ from klaude_code import const
3
6
  from klaude_code.ui.renderers.common import create_grid
4
7
  from klaude_code.ui.rich.markdown import NoInsetMarkdown
8
+ from klaude_code.ui.rich.theme import ThemeKey
9
+
10
+ # UI markers
11
+ ASSISTANT_MESSAGE_MARK = "•"
5
12
 
6
13
 
7
14
  def render_assistant_message(content: str, *, code_theme: str) -> RenderableType | None:
@@ -15,7 +22,7 @@ def render_assistant_message(content: str, *, code_theme: str) -> RenderableType
15
22
 
16
23
  grid = create_grid()
17
24
  grid.add_row(
18
- "•",
19
- NoInsetMarkdown(stripped, code_theme=code_theme),
25
+ Text(ASSISTANT_MESSAGE_MARK, style=ThemeKey.ASSISTANT_MESSAGE_MARK),
26
+ Padding(NoInsetMarkdown(stripped, code_theme=code_theme), (0, const.MARKDOWN_RIGHT_MARGIN, 0, 0)),
20
27
  )
21
28
  return grid
@@ -0,0 +1,178 @@
1
+ """Bash command syntax highlighting for terminal display."""
2
+
3
+ import re
4
+ from typing import Any
5
+
6
+ from pygments.lexers import BashLexer # pyright: ignore[reportUnknownVariableType]
7
+ from pygments.token import Token
8
+ from rich.text import Text
9
+
10
+ from klaude_code.ui.rich.theme import ThemeKey
11
+
12
+ # Token types for bash syntax highlighting
13
+ _STRING_TOKENS = frozenset(
14
+ {
15
+ Token.Literal.String,
16
+ Token.Literal.String.Double,
17
+ Token.Literal.String.Single,
18
+ Token.Literal.String.Backtick,
19
+ Token.Literal.String.Escape,
20
+ Token.Literal.String.Heredoc,
21
+ Token.Comment,
22
+ Token.Comment.Single,
23
+ Token.Comment.Hashbang,
24
+ }
25
+ )
26
+
27
+ _OPERATOR_TOKENS = frozenset(
28
+ {
29
+ Token.Operator,
30
+ Token.Punctuation,
31
+ }
32
+ )
33
+
34
+ # Operators that start a new command context (next non-whitespace token is a command)
35
+ _COMMAND_STARTERS = frozenset({"&&", "||", "|", ";", "&"})
36
+
37
+ # Commands that have subcommands (e.g., git commit, docker run)
38
+ _SUBCOMMAND_COMMANDS = frozenset(
39
+ {
40
+ # Version control
41
+ "git",
42
+ "jj",
43
+ "hg",
44
+ "svn",
45
+ # Container & orchestration
46
+ "docker",
47
+ "docker-compose",
48
+ "podman",
49
+ "kubectl",
50
+ "helm",
51
+ # Package managers
52
+ "npm",
53
+ "yarn",
54
+ "pnpm",
55
+ "cargo",
56
+ "uv",
57
+ "pip",
58
+ "poetry",
59
+ "brew",
60
+ "apt",
61
+ "apt-get",
62
+ "dnf",
63
+ "yum",
64
+ "pacman",
65
+ # Cloud CLIs
66
+ "aws",
67
+ "gcloud",
68
+ "az",
69
+ # Language tools
70
+ "go",
71
+ "rustup",
72
+ "python",
73
+ "ruby",
74
+ # Other common tools
75
+ "gh",
76
+ "systemctl",
77
+ "launchctl",
78
+ "supervisorctl",
79
+ }
80
+ )
81
+
82
+ _LEXER: Any = BashLexer(ensurenl=False) # pyright: ignore[reportUnknownVariableType]
83
+
84
+ # Regex to match heredoc: << [-]? [space]? ['"]? DELIMITER ['"]? [extra] \n body \n DELIMITER
85
+ # Groups: (<<-?) (space) (quote) (delimiter) (quote) (extra on first line) (body) (end delimiter)
86
+ _HEREDOC_PATTERN = re.compile(
87
+ r"^(<<-?)(\s*)(['\"]?)(\w+)\3([^\n]*)(\n.*\n)(\4)$",
88
+ re.DOTALL,
89
+ )
90
+
91
+
92
+ def _append_heredoc(result: Text, token_value: str) -> None:
93
+ """Append heredoc token with delimiter highlighting."""
94
+ match = _HEREDOC_PATTERN.match(token_value)
95
+ if match:
96
+ operator, space, quote, delimiter, extra, body, end_delimiter = match.groups()
97
+ # << or <<-
98
+ result.append(operator, style=ThemeKey.BASH_OPERATOR)
99
+ # Optional space
100
+ if space:
101
+ result.append(space)
102
+ # Opening quote
103
+ if quote:
104
+ result.append(quote, style=ThemeKey.BASH_HEREDOC_DELIMITER)
105
+ # Delimiter name (e.g., EOF)
106
+ result.append(delimiter, style=ThemeKey.BASH_HEREDOC_DELIMITER)
107
+ # Closing quote
108
+ if quote:
109
+ result.append(quote, style=ThemeKey.BASH_HEREDOC_DELIMITER)
110
+ # Extra content on first line (e.g., "> file.py")
111
+ if extra:
112
+ result.append(extra, style=ThemeKey.BASH_ARGUMENT)
113
+ # Body content
114
+ result.append(body, style=ThemeKey.BASH_STRING)
115
+ # End delimiter
116
+ result.append(end_delimiter, style=ThemeKey.BASH_HEREDOC_DELIMITER)
117
+ else:
118
+ # Fallback: couldn't parse heredoc structure
119
+ result.append(token_value, style=ThemeKey.BASH_STRING)
120
+
121
+
122
+ def highlight_bash_command(command: str) -> Text:
123
+ """Apply bash syntax highlighting to a command string, returning Rich Text.
124
+
125
+ Styling:
126
+ - Command names (first token after line start or operators): bold green
127
+ - Subcommands (for commands like git, docker): bold green
128
+ - Arguments: green
129
+ - Operators (&&, ||, |, ;): dim green
130
+ - Strings and comments: green
131
+ """
132
+ result = Text()
133
+ token_type: Any
134
+ token_value: str
135
+
136
+ # Track whether next non-whitespace token is a command
137
+ expect_command = True
138
+ # Track whether next non-flag token is a subcommand
139
+ expect_subcommand = False
140
+
141
+ for token_type, token_value in _LEXER.get_tokens(command):
142
+ # Determine style based on token type and context
143
+ if token_type in _STRING_TOKENS:
144
+ # Check if this is a heredoc (starts with <<)
145
+ if token_value.startswith("<<"):
146
+ _append_heredoc(result, token_value)
147
+ else:
148
+ result.append(token_value, style=ThemeKey.BASH_STRING)
149
+ expect_subcommand = False
150
+ elif token_type in _OPERATOR_TOKENS:
151
+ result.append(token_value, style=ThemeKey.BASH_OPERATOR)
152
+ # After command-starting operators, next token is a command
153
+ if token_value in _COMMAND_STARTERS:
154
+ expect_command = True
155
+ expect_subcommand = False
156
+ elif token_type in (Token.Text.Whitespace,):
157
+ result.append(token_value)
158
+ elif token_type == Token.Name.Builtin:
159
+ # Built-in commands are always commands
160
+ result.append(token_value, style=ThemeKey.BASH_COMMAND)
161
+ expect_command = False
162
+ expect_subcommand = token_value in _SUBCOMMAND_COMMANDS
163
+ elif expect_command and token_value.strip():
164
+ # First non-whitespace token in command context
165
+ result.append(token_value, style=ThemeKey.BASH_COMMAND)
166
+ expect_command = False
167
+ expect_subcommand = token_value in _SUBCOMMAND_COMMANDS
168
+ elif expect_subcommand and token_value.strip() and not token_value.startswith("-"):
169
+ # Subcommand: non-flag token after a command that has subcommands
170
+ result.append(token_value, style=ThemeKey.BASH_COMMAND)
171
+ expect_subcommand = False
172
+ else:
173
+ # Regular arguments (including flags, which reset subcommand expectation)
174
+ result.append(token_value, style=ThemeKey.BASH_ARGUMENT)
175
+ if token_value.strip():
176
+ expect_subcommand = False
177
+
178
+ return result
@@ -1,4 +1,9 @@
1
+ from rich.style import Style
1
2
  from rich.table import Table
3
+ from rich.text import Text
4
+
5
+ from klaude_code import const
6
+ from klaude_code.ui.rich.theme import ThemeKey
2
7
 
3
8
 
4
9
  def create_grid() -> Table:
@@ -6,3 +11,76 @@ def create_grid() -> Table:
6
11
  grid.add_column(no_wrap=True)
7
12
  grid.add_column(overflow="fold")
8
13
  return grid
14
+
15
+
16
+ def truncate_display(
17
+ text: str,
18
+ max_lines: int = const.TRUNCATE_DISPLAY_MAX_LINES,
19
+ max_line_length: int = const.TRUNCATE_DISPLAY_MAX_LINE_LENGTH,
20
+ *,
21
+ base_style: str | Style | None = None,
22
+ ) -> Text:
23
+ """Truncate long text for terminal display.
24
+
25
+ Applies `ThemeKey.TOOL_RESULT_TRUNCATED` style to truncation indicators.
26
+ """
27
+ # Expand tabs to spaces to ensure correct alignment when Rich applies padding.
28
+ text = text.expandtabs(8)
29
+
30
+ if max_lines <= 0:
31
+ truncated_lines = text.split("\n")
32
+ remaining = max(0, len(truncated_lines))
33
+ return Text(f"… (more {remaining} lines)", style=ThemeKey.TOOL_RESULT_TRUNCATED)
34
+
35
+ lines = text.split("\n")
36
+ truncated_lines = 0
37
+ head_lines: list[str] = []
38
+ tail_lines: list[str] = []
39
+
40
+ if len(lines) > max_lines:
41
+ truncated_lines = len(lines) - max_lines
42
+
43
+ # If the hidden section is too small, show everything instead of inserting
44
+ # the "(more N lines)" indicator.
45
+ if truncated_lines < 5:
46
+ truncated_lines = 0
47
+ head_lines = lines
48
+ else:
49
+ head_count = max_lines // 2
50
+ tail_count = max_lines - head_count
51
+ head_lines = lines[:head_count]
52
+ tail_lines = lines[-tail_count:]
53
+ else:
54
+ head_lines = lines
55
+
56
+ def append_line(out: Text, line: str) -> None:
57
+ if len(line) > max_line_length:
58
+ extra_chars = len(line) - max_line_length
59
+ out.append(line[:max_line_length])
60
+ out.append_text(
61
+ Text(
62
+ f" … (more {extra_chars} characters in this line)",
63
+ style=ThemeKey.TOOL_RESULT_TRUNCATED,
64
+ )
65
+ )
66
+ else:
67
+ out.append(line)
68
+
69
+ out = Text()
70
+ if base_style is not None:
71
+ out.style = base_style
72
+
73
+ for idx, line in enumerate(head_lines):
74
+ append_line(out, line)
75
+ if idx < len(head_lines) - 1 or truncated_lines > 0 or tail_lines:
76
+ out.append("\n")
77
+
78
+ if truncated_lines > 0:
79
+ out.append_text(Text(f"⋮ (more {truncated_lines} lines)\n", style=ThemeKey.TOOL_RESULT_TRUNCATED))
80
+
81
+ for idx, line in enumerate(tail_lines):
82
+ append_line(out, line)
83
+ if idx < len(tail_lines) - 1:
84
+ out.append("\n")
85
+
86
+ return out
@@ -4,11 +4,12 @@ from rich.table import Table
4
4
  from rich.text import Text
5
5
 
6
6
  from klaude_code.protocol import commands, events, model
7
- from klaude_code.ui.renderers import diffs as r_diffs
8
- from klaude_code.ui.renderers.common import create_grid
7
+ from klaude_code.ui.renderers.common import create_grid, truncate_display
9
8
  from klaude_code.ui.renderers.tools import render_path
9
+ from klaude_code.ui.rich.markdown import NoInsetMarkdown
10
10
  from klaude_code.ui.rich.theme import ThemeKey
11
- from klaude_code.ui.utils.common import truncate_display
11
+
12
+ REMINDER_BULLET = " ⧉"
12
13
 
13
14
 
14
15
  def need_render_developer_message(e: events.DeveloperMessageEvent) -> bool:
@@ -18,6 +19,7 @@ def need_render_developer_message(e: events.DeveloperMessageEvent) -> bool:
18
19
  or e.item.todo_use
19
20
  or e.item.at_files
20
21
  or e.item.user_image_count
22
+ or e.item.skill_name
21
23
  )
22
24
 
23
25
 
@@ -32,7 +34,7 @@ def render_developer_message(e: events.DeveloperMessageEvent) -> RenderableType:
32
34
  if mp := e.item.memory_paths:
33
35
  grid = create_grid()
34
36
  grid.add_row(
35
- Text(" +", style=ThemeKey.REMINDER),
37
+ Text(REMINDER_BULLET, style=ThemeKey.REMINDER),
36
38
  Text.assemble(
37
39
  ("Load memory ", ThemeKey.REMINDER),
38
40
  Text(", ", ThemeKey.REMINDER).join(
@@ -46,7 +48,7 @@ def render_developer_message(e: events.DeveloperMessageEvent) -> RenderableType:
46
48
  grid = create_grid()
47
49
  for file_path in fc:
48
50
  grid.add_row(
49
- Text(" +", style=ThemeKey.REMINDER),
51
+ Text(REMINDER_BULLET, style=ThemeKey.REMINDER),
50
52
  Text.assemble(
51
53
  ("Read ", ThemeKey.REMINDER),
52
54
  render_path(file_path, ThemeKey.REMINDER_BOLD),
@@ -58,31 +60,62 @@ def render_developer_message(e: events.DeveloperMessageEvent) -> RenderableType:
58
60
  if e.item.todo_use:
59
61
  grid = create_grid()
60
62
  grid.add_row(
61
- Text(" +", style=ThemeKey.REMINDER),
63
+ Text(REMINDER_BULLET, style=ThemeKey.REMINDER),
62
64
  Text("Todo hasn't been updated recently", ThemeKey.REMINDER),
63
65
  )
64
66
  parts.append(grid)
65
67
 
66
68
  if e.item.at_files:
67
69
  grid = create_grid()
70
+ # Group at_files by (operation, mentioned_in)
71
+ grouped: dict[tuple[str, str | None], list[str]] = {}
68
72
  for at_file in e.item.at_files:
69
- grid.add_row(
70
- Text(" +", style=ThemeKey.REMINDER),
71
- Text.assemble(
72
- (f"{at_file.operation} ", ThemeKey.REMINDER),
73
- render_path(at_file.path, ThemeKey.REMINDER_BOLD),
74
- ),
75
- )
73
+ key = (at_file.operation, at_file.mentioned_in)
74
+ if key not in grouped:
75
+ grouped[key] = []
76
+ grouped[key].append(at_file.path)
77
+
78
+ for (operation, mentioned_in), paths in grouped.items():
79
+ path_texts = Text(", ", ThemeKey.REMINDER).join(render_path(p, ThemeKey.REMINDER_BOLD) for p in paths)
80
+ if mentioned_in:
81
+ grid.add_row(
82
+ Text(REMINDER_BULLET, style=ThemeKey.REMINDER),
83
+ Text.assemble(
84
+ (f"{operation} ", ThemeKey.REMINDER),
85
+ path_texts,
86
+ (" mentioned in ", ThemeKey.REMINDER),
87
+ render_path(mentioned_in, ThemeKey.REMINDER_BOLD),
88
+ ),
89
+ )
90
+ else:
91
+ grid.add_row(
92
+ Text(REMINDER_BULLET, style=ThemeKey.REMINDER),
93
+ Text.assemble(
94
+ (f"{operation} ", ThemeKey.REMINDER),
95
+ path_texts,
96
+ ),
97
+ )
76
98
  parts.append(grid)
77
99
 
78
100
  if uic := e.item.user_image_count:
79
101
  grid = create_grid()
80
102
  grid.add_row(
81
- Text(" +", style=ThemeKey.REMINDER),
103
+ Text(REMINDER_BULLET, style=ThemeKey.REMINDER),
82
104
  Text(f"Attached {uic} image{'s' if uic > 1 else ''}", style=ThemeKey.REMINDER),
83
105
  )
84
106
  parts.append(grid)
85
107
 
108
+ if sn := e.item.skill_name:
109
+ grid = create_grid()
110
+ grid.add_row(
111
+ Text(REMINDER_BULLET, style=ThemeKey.REMINDER),
112
+ Text.assemble(
113
+ ("Activated skill ", ThemeKey.REMINDER),
114
+ (sn, ThemeKey.REMINDER_BOLD),
115
+ ),
116
+ )
117
+ parts.append(grid)
118
+
86
119
  return Group(*parts) if parts else Text("")
87
120
 
88
121
 
@@ -92,18 +125,18 @@ def render_command_output(e: events.DeveloperMessageEvent) -> RenderableType:
92
125
  return Text("")
93
126
 
94
127
  match e.item.command_output.command_name:
95
- case commands.CommandName.DIFF:
96
- if e.item.content is None or len(e.item.content) == 0:
97
- return Padding.indent(Text("(no changes)", style=ThemeKey.TOOL_RESULT), level=2)
98
- return r_diffs.render_diff_panel(e.item.content, show_file_name=True)
99
128
  case commands.CommandName.HELP:
100
129
  return Padding.indent(Text.from_markup(e.item.content or ""), level=2)
101
130
  case commands.CommandName.STATUS:
102
131
  return _render_status_output(e.item.command_output)
132
+ case commands.CommandName.RELEASE_NOTES:
133
+ return Padding.indent(NoInsetMarkdown(e.item.content or ""), level=2)
134
+ case commands.CommandName.FORK_SESSION:
135
+ return _render_fork_session_output(e.item.command_output)
103
136
  case _:
104
137
  content = e.item.content or "(no content)"
105
138
  style = ThemeKey.TOOL_RESULT if not e.item.command_output.is_error else ThemeKey.ERROR
106
- return Padding.indent(Text(truncate_display(content), style=style), level=2)
139
+ return Padding.indent(truncate_display(content, base_style=style), level=2)
107
140
 
108
141
 
109
142
  def _format_tokens(tokens: int) -> str:
@@ -115,44 +148,67 @@ def _format_tokens(tokens: int) -> str:
115
148
  return str(tokens)
116
149
 
117
150
 
118
- def _format_cost(cost: float | None) -> str:
119
- """Format cost in USD."""
151
+ def _format_cost(cost: float | None, currency: str = "USD") -> str:
152
+ """Format cost with currency symbol."""
120
153
  if cost is None:
121
154
  return "-"
155
+ symbol = "¥" if currency == "CNY" else "$"
122
156
  if cost < 0.01:
123
- return f"${cost:.4f}"
124
- return f"${cost:.2f}"
157
+ return f"{symbol}{cost:.4f}"
158
+ return f"{symbol}{cost:.2f}"
159
+
160
+
161
+ def _render_fork_session_output(command_output: model.CommandOutput) -> RenderableType:
162
+ """Render fork session output with usage instructions."""
163
+ if not isinstance(command_output.ui_extra, model.SessionIdUIExtra):
164
+ return Padding.indent(Text("(no session id)", style=ThemeKey.METADATA), level=2)
165
+
166
+ grid = Table.grid(padding=(0, 1))
167
+ session_id = command_output.ui_extra.session_id
168
+ grid.add_column(style=ThemeKey.METADATA, overflow="fold")
169
+
170
+ grid.add_row(Text("Session forked. Resume command copied to clipboard:", style=ThemeKey.METADATA))
171
+ grid.add_row(Text(f" klaude --resume-by-id {session_id}", style=ThemeKey.METADATA_BOLD))
172
+
173
+ return Padding.indent(grid, level=2)
125
174
 
126
175
 
127
176
  def _render_status_output(command_output: model.CommandOutput) -> RenderableType:
128
- """Render session status as a two-column table with sections."""
129
- if not command_output.ui_extra or not command_output.ui_extra.session_status:
130
- return Text("(no status data)", style=ThemeKey.TOOL_RESULT)
177
+ """Render session status with total cost and per-model breakdown."""
178
+ if not isinstance(command_output.ui_extra, model.SessionStatusUIExtra):
179
+ return Text("(no status data)", style=ThemeKey.METADATA)
131
180
 
132
- status = command_output.ui_extra.session_status
181
+ status = command_output.ui_extra
133
182
  usage = status.usage
134
183
 
135
184
  table = Table.grid(padding=(0, 2))
136
- table.add_column(style=ThemeKey.TOOL_RESULT, no_wrap=True)
137
- table.add_column(style=ThemeKey.TOOL_RESULT, no_wrap=True)
138
- # Token Usage section
139
- table.add_row(Text("Token Usage", style="bold"), "")
140
- table.add_row("Input Tokens", _format_tokens(usage.input_tokens))
141
- if usage.cached_tokens > 0:
142
- table.add_row("Cached Tokens", _format_tokens(usage.cached_tokens))
143
- if usage.reasoning_tokens > 0:
144
- table.add_row("Reasoning Tokens", _format_tokens(usage.reasoning_tokens))
145
- table.add_row("Output Tokens", _format_tokens(usage.output_tokens))
146
- table.add_row("Total Tokens", _format_tokens(usage.total_tokens))
147
-
148
- # Cost section
149
- if usage.total_cost is not None:
150
- table.add_row("", "") # Empty line
151
- table.add_row(Text("Cost", style="bold"), "")
152
- table.add_row("Input Cost", _format_cost(usage.input_cost))
153
- if usage.cache_read_cost is not None and usage.cache_read_cost > 0:
154
- table.add_row("Cache Read Cost", _format_cost(usage.cache_read_cost))
155
- table.add_row("Output Cost", _format_cost(usage.output_cost))
156
- table.add_row("Total Cost", _format_cost(usage.total_cost))
185
+ table.add_column(style=ThemeKey.METADATA, overflow="fold")
186
+ table.add_column(style=ThemeKey.METADATA, overflow="fold")
187
+
188
+ # Total cost line
189
+ table.add_row(
190
+ Text("Total cost:", style=ThemeKey.METADATA_BOLD),
191
+ Text(_format_cost(usage.total_cost, usage.currency), style=ThemeKey.METADATA_BOLD),
192
+ )
193
+
194
+ # Per-model breakdown
195
+ if status.by_model:
196
+ table.add_row(Text("Usage by model:", style=ThemeKey.METADATA_BOLD), "")
197
+ for meta in status.by_model:
198
+ model_label = meta.model_name
199
+ if meta.provider:
200
+ model_label = f"{meta.model_name} ({meta.provider.lower().replace(' ', '-')})"
201
+
202
+ if meta.usage:
203
+ usage_detail = (
204
+ f"{_format_tokens(meta.usage.input_tokens)} input, "
205
+ f"{_format_tokens(meta.usage.output_tokens)} output, "
206
+ f"{_format_tokens(meta.usage.cached_tokens)} cache read, "
207
+ f"{_format_tokens(meta.usage.reasoning_tokens)} thinking, "
208
+ f"({_format_cost(meta.usage.total_cost, meta.usage.currency)})"
209
+ )
210
+ else:
211
+ usage_detail = "(no usage data)"
212
+ table.add_row(f"{model_label}:", usage_detail)
157
213
 
158
214
  return Padding.indent(table, level=2)
@@ -5,6 +5,7 @@ from rich.panel import Panel
5
5
  from rich.text import Text
6
6
 
7
7
  from klaude_code import const
8
+ from klaude_code.protocol import model
8
9
  from klaude_code.ui.renderers.common import create_grid
9
10
  from klaude_code.ui.rich.theme import ThemeKey
10
11
 
@@ -73,10 +74,7 @@ def render_diff(diff_text: str, show_file_name: bool = False) -> RenderableType:
73
74
  if line.startswith("--- "):
74
75
  raw = line[4:].strip()
75
76
  if raw != "/dev/null":
76
- if raw.startswith(("a/", "b/")):
77
- from_file_name = raw[2:]
78
- else:
79
- from_file_name = raw
77
+ from_file_name = raw[2:] if raw.startswith(("a/", "b/")) else raw
80
78
  continue
81
79
 
82
80
  # Parse file name from diff headers
@@ -150,7 +148,7 @@ def render_diff(diff_text: str, show_file_name: bool = False) -> RenderableType:
150
148
  plus = parts[2] # like '+12,4'
151
149
  new_start = int(plus[1:].split(",")[0])
152
150
  new_ln = new_start
153
- except Exception:
151
+ except (IndexError, ValueError):
154
152
  new_ln = None
155
153
  if has_rendered_diff_content:
156
154
  grid.add_row(Text(f"{'⋮':>{const.DIFF_PREFIX_WIDTH}}", style=ThemeKey.TOOL_RESULT), "")
@@ -182,11 +180,35 @@ def render_diff(diff_text: str, show_file_name: bool = False) -> RenderableType:
182
180
  return grid
183
181
 
184
182
 
183
+ def render_structured_diff(ui_extra: model.DiffUIExtra, show_file_name: bool = False) -> RenderableType:
184
+ files = ui_extra.files
185
+ if not files:
186
+ return Text("")
187
+
188
+ grid = create_grid()
189
+ grid.padding = (0, 0)
190
+ show_headers = show_file_name or len(files) > 1
191
+
192
+ for idx, file_diff in enumerate(files):
193
+ if idx > 0:
194
+ grid.add_row("", "")
195
+
196
+ if show_headers:
197
+ grid.add_row(*_render_file_header(file_diff))
198
+
199
+ for line in file_diff.lines:
200
+ prefix = _make_structured_prefix(line, const.DIFF_PREFIX_WIDTH)
201
+ text = _render_structured_line(line)
202
+ grid.add_row(Text(prefix, ThemeKey.TOOL_RESULT), text)
203
+
204
+ return grid
205
+
206
+
185
207
  def render_diff_panel(
186
208
  diff_text: str,
187
209
  *,
188
210
  show_file_name: bool = True,
189
- heading: str = "Git Diff",
211
+ heading: str = "DIFF",
190
212
  indent: int = 2,
191
213
  ) -> RenderableType:
192
214
  lines = diff_text.splitlines()
@@ -213,3 +235,62 @@ def render_diff_panel(
213
235
  if indent <= 0:
214
236
  return panel
215
237
  return Padding.indent(panel, level=indent)
238
+
239
+
240
+ def _render_file_header(file_diff: model.DiffFileDiff) -> tuple[Text, Text]:
241
+ file_text = Text(file_diff.file_path, style=ThemeKey.DIFF_FILE_NAME)
242
+ stats_text = Text()
243
+ if file_diff.stats_add > 0:
244
+ stats_text.append(f"+{file_diff.stats_add}", style=ThemeKey.DIFF_STATS_ADD)
245
+ if file_diff.stats_remove > 0:
246
+ if stats_text.plain:
247
+ stats_text.append(" ")
248
+ stats_text.append(f"-{file_diff.stats_remove}", style=ThemeKey.DIFF_STATS_REMOVE)
249
+
250
+ file_line = Text(style=ThemeKey.DIFF_FILE_NAME)
251
+ file_line.append_text(file_text)
252
+ if stats_text.plain:
253
+ file_line.append(" (")
254
+ file_line.append_text(stats_text)
255
+ file_line.append(")")
256
+
257
+ if file_diff.stats_add > 0 and file_diff.stats_remove == 0:
258
+ file_mark = "+"
259
+ elif file_diff.stats_remove > 0 and file_diff.stats_add == 0:
260
+ file_mark = "-"
261
+ else:
262
+ file_mark = "±"
263
+
264
+ prefix = Text(f"{file_mark:>{const.DIFF_PREFIX_WIDTH}} ", style=ThemeKey.DIFF_FILE_NAME)
265
+ return prefix, file_line
266
+
267
+
268
+ def _make_structured_prefix(line: model.DiffLine, width: int) -> str:
269
+ if line.kind == "gap":
270
+ return f"{'⋮':>{width}} "
271
+ number = " " * width
272
+ if line.kind in {"add", "ctx"} and line.new_line_no is not None:
273
+ number = f"{line.new_line_no:>{width}}"
274
+ marker = "+" if line.kind == "add" else "-" if line.kind == "remove" else " "
275
+ return f"{number} {marker}"
276
+
277
+
278
+ def _render_structured_line(line: model.DiffLine) -> Text:
279
+ if line.kind == "gap":
280
+ return Text("")
281
+ text = Text()
282
+ for span in line.spans:
283
+ text.append(span.text, style=_span_style(line.kind, span.op))
284
+ return text
285
+
286
+
287
+ def _span_style(line_kind: str, span_op: str) -> ThemeKey:
288
+ if line_kind == "add":
289
+ if span_op == "insert":
290
+ return ThemeKey.DIFF_ADD_CHAR
291
+ return ThemeKey.DIFF_ADD
292
+ if line_kind == "remove":
293
+ if span_op == "delete":
294
+ return ThemeKey.DIFF_REMOVE_CHAR
295
+ return ThemeKey.DIFF_REMOVE
296
+ return ThemeKey.TOOL_RESULT
@@ -5,12 +5,17 @@ from klaude_code.ui.renderers.common import create_grid
5
5
  from klaude_code.ui.rich.theme import ThemeKey
6
6
 
7
7
 
8
- def render_error(error_msg: Text, indent: int = 2) -> RenderableType:
9
- """Stateless error renderer.
8
+ def render_error(error_msg: Text) -> RenderableType:
9
+ """Render error with X mark for error events."""
10
+ grid = create_grid()
11
+ error_msg.style = ThemeKey.ERROR
12
+ grid.add_row(Text("✘", style=ThemeKey.ERROR_BOLD), error_msg)
13
+ return grid
14
+
10
15
 
11
- Shows a two-column grid with an error mark and truncated message.
12
- """
16
+ def render_tool_error(error_msg: Text) -> RenderableType:
17
+ """Render error with indent for tool results."""
13
18
  grid = create_grid()
14
- error_msg.stylize(ThemeKey.ERROR)
15
- grid.add_row(Text(" " * indent + "✘", style=ThemeKey.ERROR_BOLD), error_msg)
19
+ error_msg.style = ThemeKey.ERROR
20
+ grid.add_row(Text(" "), error_msg)
16
21
  return grid