klaude-code 1.2.1__py3-none-any.whl → 1.2.3__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 (140) hide show
  1. klaude_code/cli/main.py +9 -4
  2. klaude_code/cli/runtime.py +42 -43
  3. klaude_code/command/__init__.py +7 -5
  4. klaude_code/command/clear_cmd.py +6 -29
  5. klaude_code/command/command_abc.py +44 -8
  6. klaude_code/command/diff_cmd.py +33 -27
  7. klaude_code/command/export_cmd.py +18 -26
  8. klaude_code/command/help_cmd.py +10 -8
  9. klaude_code/command/model_cmd.py +11 -40
  10. klaude_code/command/{prompt-update-dev-doc.md → prompt-dev-docs-update.md} +3 -2
  11. klaude_code/command/{prompt-dev-doc.md → prompt-dev-docs.md} +3 -2
  12. klaude_code/command/prompt-init.md +2 -5
  13. klaude_code/command/prompt_command.py +6 -6
  14. klaude_code/command/refresh_cmd.py +4 -5
  15. klaude_code/command/registry.py +16 -19
  16. klaude_code/command/terminal_setup_cmd.py +12 -11
  17. klaude_code/config/__init__.py +4 -0
  18. klaude_code/config/config.py +25 -26
  19. klaude_code/config/list_model.py +8 -3
  20. klaude_code/config/select_model.py +1 -1
  21. klaude_code/const/__init__.py +1 -1
  22. klaude_code/core/__init__.py +0 -3
  23. klaude_code/core/agent.py +25 -50
  24. klaude_code/core/executor.py +268 -101
  25. klaude_code/core/prompt.py +12 -12
  26. klaude_code/core/{prompt → prompts}/prompt-gemini.md +1 -1
  27. klaude_code/core/reminders.py +76 -95
  28. klaude_code/core/task.py +21 -14
  29. klaude_code/core/tool/__init__.py +45 -11
  30. klaude_code/core/tool/file/apply_patch.py +5 -1
  31. klaude_code/core/tool/file/apply_patch_tool.py +11 -13
  32. klaude_code/core/tool/file/edit_tool.py +27 -23
  33. klaude_code/core/tool/file/multi_edit_tool.py +15 -17
  34. klaude_code/core/tool/file/read_tool.py +41 -36
  35. klaude_code/core/tool/file/write_tool.py +13 -15
  36. klaude_code/core/tool/memory/memory_tool.py +85 -68
  37. klaude_code/core/tool/memory/skill_tool.py +10 -12
  38. klaude_code/core/tool/shell/bash_tool.py +24 -22
  39. klaude_code/core/tool/shell/command_safety.py +12 -1
  40. klaude_code/core/tool/sub_agent_tool.py +11 -12
  41. klaude_code/core/tool/todo/todo_write_tool.py +21 -28
  42. klaude_code/core/tool/todo/update_plan_tool.py +14 -24
  43. klaude_code/core/tool/tool_abc.py +3 -4
  44. klaude_code/core/tool/tool_context.py +7 -7
  45. klaude_code/core/tool/tool_registry.py +30 -47
  46. klaude_code/core/tool/tool_runner.py +35 -43
  47. klaude_code/core/tool/truncation.py +14 -20
  48. klaude_code/core/tool/web/mermaid_tool.py +12 -14
  49. klaude_code/core/tool/web/web_fetch_tool.py +15 -17
  50. klaude_code/core/turn.py +19 -7
  51. klaude_code/llm/__init__.py +3 -4
  52. klaude_code/llm/anthropic/client.py +30 -46
  53. klaude_code/llm/anthropic/input.py +4 -11
  54. klaude_code/llm/client.py +29 -8
  55. klaude_code/llm/input_common.py +66 -36
  56. klaude_code/llm/openai_compatible/client.py +42 -84
  57. klaude_code/llm/openai_compatible/input.py +11 -16
  58. klaude_code/llm/openai_compatible/tool_call_accumulator.py +2 -2
  59. klaude_code/llm/openrouter/client.py +40 -289
  60. klaude_code/llm/openrouter/input.py +13 -35
  61. klaude_code/llm/openrouter/reasoning_handler.py +209 -0
  62. klaude_code/llm/registry.py +5 -75
  63. klaude_code/llm/responses/client.py +34 -55
  64. klaude_code/llm/responses/input.py +24 -26
  65. klaude_code/llm/usage.py +109 -0
  66. klaude_code/protocol/__init__.py +4 -0
  67. klaude_code/protocol/events.py +3 -2
  68. klaude_code/protocol/{llm_parameter.py → llm_param.py} +12 -32
  69. klaude_code/protocol/model.py +49 -4
  70. klaude_code/protocol/op.py +18 -16
  71. klaude_code/protocol/op_handler.py +28 -0
  72. klaude_code/{core → protocol}/sub_agent.py +7 -0
  73. klaude_code/session/export.py +150 -70
  74. klaude_code/session/session.py +28 -14
  75. klaude_code/session/templates/export_session.html +180 -42
  76. klaude_code/trace/__init__.py +2 -2
  77. klaude_code/trace/log.py +11 -5
  78. klaude_code/ui/__init__.py +91 -8
  79. klaude_code/ui/core/__init__.py +1 -0
  80. klaude_code/ui/core/display.py +103 -0
  81. klaude_code/ui/core/input.py +71 -0
  82. klaude_code/ui/modes/__init__.py +1 -0
  83. klaude_code/ui/modes/debug/__init__.py +1 -0
  84. klaude_code/ui/{base/debug_event_display.py → modes/debug/display.py} +9 -5
  85. klaude_code/ui/modes/exec/__init__.py +1 -0
  86. klaude_code/ui/{base/exec_display.py → modes/exec/display.py} +28 -2
  87. klaude_code/ui/{repl → modes/repl}/__init__.py +5 -6
  88. klaude_code/ui/modes/repl/clipboard.py +152 -0
  89. klaude_code/ui/modes/repl/completers.py +429 -0
  90. klaude_code/ui/modes/repl/display.py +60 -0
  91. klaude_code/ui/modes/repl/event_handler.py +375 -0
  92. klaude_code/ui/modes/repl/input_prompt_toolkit.py +198 -0
  93. klaude_code/ui/modes/repl/key_bindings.py +170 -0
  94. klaude_code/ui/{repl → modes/repl}/renderer.py +109 -132
  95. klaude_code/ui/renderers/assistant.py +21 -0
  96. klaude_code/ui/renderers/common.py +0 -16
  97. klaude_code/ui/renderers/developer.py +18 -18
  98. klaude_code/ui/renderers/diffs.py +36 -14
  99. klaude_code/ui/renderers/errors.py +1 -1
  100. klaude_code/ui/renderers/metadata.py +50 -27
  101. klaude_code/ui/renderers/sub_agent.py +43 -9
  102. klaude_code/ui/renderers/thinking.py +33 -1
  103. klaude_code/ui/renderers/tools.py +212 -20
  104. klaude_code/ui/renderers/user_input.py +19 -23
  105. klaude_code/ui/rich/__init__.py +1 -0
  106. klaude_code/ui/{rich_ext → rich}/searchable_text.py +3 -1
  107. klaude_code/ui/{renderers → rich}/status.py +29 -18
  108. klaude_code/ui/{base → rich}/theme.py +8 -2
  109. klaude_code/ui/terminal/__init__.py +1 -0
  110. klaude_code/ui/{base/terminal_color.py → terminal/color.py} +4 -1
  111. klaude_code/ui/{base/terminal_control.py → terminal/control.py} +1 -0
  112. klaude_code/ui/{base/terminal_notifier.py → terminal/notifier.py} +5 -2
  113. klaude_code/ui/utils/__init__.py +1 -0
  114. klaude_code/ui/{base/utils.py → utils/common.py} +35 -3
  115. {klaude_code-1.2.1.dist-info → klaude_code-1.2.3.dist-info}/METADATA +1 -1
  116. klaude_code-1.2.3.dist-info/RECORD +161 -0
  117. klaude_code/core/clipboard_manifest.py +0 -124
  118. klaude_code/llm/openrouter/tool_call_accumulator.py +0 -80
  119. klaude_code/ui/base/__init__.py +0 -1
  120. klaude_code/ui/base/display_abc.py +0 -36
  121. klaude_code/ui/base/input_abc.py +0 -20
  122. klaude_code/ui/repl/display.py +0 -36
  123. klaude_code/ui/repl/event_handler.py +0 -247
  124. klaude_code/ui/repl/input.py +0 -773
  125. klaude_code/ui/rich_ext/__init__.py +0 -1
  126. klaude_code-1.2.1.dist-info/RECORD +0 -151
  127. /klaude_code/core/{prompt → prompts}/prompt-claude-code.md +0 -0
  128. /klaude_code/core/{prompt → prompts}/prompt-codex.md +0 -0
  129. /klaude_code/core/{prompt → prompts}/prompt-subagent-explore.md +0 -0
  130. /klaude_code/core/{prompt → prompts}/prompt-subagent-oracle.md +0 -0
  131. /klaude_code/core/{prompt → prompts}/prompt-subagent-webfetch.md +0 -0
  132. /klaude_code/core/{prompt → prompts}/prompt-subagent.md +0 -0
  133. /klaude_code/ui/{base → core}/stage_manager.py +0 -0
  134. /klaude_code/ui/{rich_ext → rich}/live.py +0 -0
  135. /klaude_code/ui/{rich_ext → rich}/markdown.py +0 -0
  136. /klaude_code/ui/{rich_ext → rich}/quote.py +0 -0
  137. /klaude_code/ui/{base → terminal}/progress_bar.py +0 -0
  138. /klaude_code/ui/{base → utils}/debouncer.py +0 -0
  139. {klaude_code-1.2.1.dist-info → klaude_code-1.2.3.dist-info}/WHEEL +0 -0
  140. {klaude_code-1.2.1.dist-info → klaude_code-1.2.3.dist-info}/entry_points.txt +0 -0
@@ -4,9 +4,9 @@ from rich.padding import Padding
4
4
  from rich.panel import Panel
5
5
  from rich.text import Text
6
6
 
7
- from klaude_code.const import DIFF_PREFIX_WIDTH, MAX_DIFF_LINES
8
- from klaude_code.ui.base.theme import ThemeKey
7
+ from klaude_code import const
9
8
  from klaude_code.ui.renderers.common import create_grid
9
+ from klaude_code.ui.rich.theme import ThemeKey
10
10
 
11
11
 
12
12
  def _make_diff_prefix(line: str, new_ln: int | None, width: int) -> tuple[str, int | None]:
@@ -44,6 +44,8 @@ def render_diff(diff_text: str, show_file_name: bool = False) -> RenderableType:
44
44
  has_rendered_file_header = False
45
45
  # Track whether we have rendered actual diff content for the current file
46
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
47
49
 
48
50
  for i, line in enumerate(lines):
49
51
  # Check for untracked files section header
@@ -61,15 +63,29 @@ def render_diff(diff_text: str, show_file_name: bool = False) -> RenderableType:
61
63
  in_untracked_section = False
62
64
  elif line.strip(): # Non-empty line in untracked section
63
65
  file_text = Text(line.strip(), style=ThemeKey.TOOL_PARAM_BOLD)
64
- grid.add_row(Text(f"{'+':>{DIFF_PREFIX_WIDTH}}", style=ThemeKey.TOOL_PARAM_BOLD), file_text)
66
+ grid.add_row(
67
+ Text(f"{'+':>{const.DIFF_PREFIX_WIDTH}}", style=ThemeKey.TOOL_PARAM_BOLD),
68
+ file_text,
69
+ )
65
70
  continue
66
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
+
67
82
  # Parse file name from diff headers
68
83
  if show_file_name and line.startswith("+++ "):
69
84
  # Extract file name from +++ header with proper handling of /dev/null
70
85
  raw = line[4:].strip()
71
86
  if raw == "/dev/null":
72
- file_name = raw
87
+ # File was deleted, use the "from" file name
88
+ file_name = from_file_name or raw
73
89
  elif raw.startswith(("a/", "b/")):
74
90
  file_name = raw[2:]
75
91
  else:
@@ -115,7 +131,10 @@ def render_diff(diff_text: str, show_file_name: bool = False) -> RenderableType:
115
131
  else:
116
132
  file_mark = "±"
117
133
 
118
- grid.add_row(Text(f"{file_mark:>{DIFF_PREFIX_WIDTH}} ", style=ThemeKey.DIFF_FILE_NAME), file_line)
134
+ grid.add_row(
135
+ Text(f"{file_mark:>{const.DIFF_PREFIX_WIDTH}} ", style=ThemeKey.DIFF_FILE_NAME),
136
+ file_line,
137
+ )
119
138
  has_rendered_file_header = True
120
139
  has_rendered_diff_content = False
121
140
  continue
@@ -134,11 +153,11 @@ def render_diff(diff_text: str, show_file_name: bool = False) -> RenderableType:
134
153
  except Exception:
135
154
  new_ln = None
136
155
  if has_rendered_diff_content:
137
- grid.add_row(Text(f"{'⋮':>{DIFF_PREFIX_WIDTH}}", style=ThemeKey.TOOL_RESULT), "")
156
+ grid.add_row(Text(f"{'⋮':>{const.DIFF_PREFIX_WIDTH}}", style=ThemeKey.TOOL_RESULT), "")
138
157
  continue
139
158
 
140
- # Skip file header lines entirely
141
- if line.startswith("--- ") or line.startswith("+++ "):
159
+ # Skip +++ lines (already handled above)
160
+ if line.startswith("+++ "):
142
161
  continue
143
162
 
144
163
  # Only handle unified diff hunk lines; ignore other metadata like
@@ -147,7 +166,7 @@ def render_diff(diff_text: str, show_file_name: bool = False) -> RenderableType:
147
166
  continue
148
167
 
149
168
  # Compute line number prefix and style diff content
150
- prefix, new_ln = _make_diff_prefix(line, new_ln, DIFF_PREFIX_WIDTH)
169
+ prefix, new_ln = _make_diff_prefix(line, new_ln, const.DIFF_PREFIX_WIDTH)
151
170
 
152
171
  if line.startswith("-"):
153
172
  text = Text(line[1:])
@@ -172,13 +191,16 @@ def render_diff_panel(
172
191
  ) -> RenderableType:
173
192
  lines = diff_text.splitlines()
174
193
  truncated_notice: Text | None = None
175
- if len(lines) > MAX_DIFF_LINES:
176
- truncated_lines = len(lines) - MAX_DIFF_LINES
177
- diff_text = "\n".join(lines[:MAX_DIFF_LINES])
178
- truncated_notice = Text(f"... truncated {truncated_lines} lines", style=ThemeKey.TOOL_MARK)
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)
179
198
 
180
199
  diff_body = render_diff(diff_text, show_file_name=show_file_name)
181
- renderables: list[RenderableType] = [Text(f" {heading} ", style="bold reverse"), diff_body]
200
+ renderables: list[RenderableType] = [
201
+ Text(f" {heading} ", style="bold reverse"),
202
+ diff_body,
203
+ ]
182
204
  if truncated_notice is not None:
183
205
  renderables.extend([Text(""), truncated_notice])
184
206
 
@@ -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.base.theme import ThemeKey
5
4
  from klaude_code.ui.renderers.common import create_grid
5
+ from klaude_code.ui.rich.theme import ThemeKey
6
6
 
7
7
 
8
8
  def render_error(error_msg: Text, indent: int = 2) -> RenderableType:
@@ -8,8 +8,9 @@ from rich.panel import Panel
8
8
  from rich.text import Text
9
9
 
10
10
  from klaude_code.protocol import events
11
- from klaude_code.ui.base.theme import ThemeKey
12
- from klaude_code.ui.base.utils import format_number
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
13
14
 
14
15
 
15
16
  def _get_version() -> str:
@@ -40,30 +41,32 @@ def render_response_metadata(e: events.ResponseMetadataEvent) -> RenderableType:
40
41
 
41
42
  if metadata.usage is not None:
42
43
  # Input
43
- parts.append(
44
- Text.assemble(
45
- ("input:", ThemeKey.METADATA_DIM),
46
- (format_number(metadata.usage.input_tokens), ThemeKey.METADATA_DIM),
47
- )
48
- )
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))
49
51
 
50
52
  # Cached
51
53
  if metadata.usage.cached_tokens > 0:
52
- parts.append(
53
- Text.assemble(
54
- ("cached", ThemeKey.METADATA_DIM),
55
- (":", ThemeKey.METADATA_DIM),
56
- (format_number(metadata.usage.cached_tokens), ThemeKey.METADATA_DIM),
57
- )
58
- )
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))
59
61
 
60
62
  # Output
61
- parts.append(
62
- Text.assemble(
63
- ("output:", ThemeKey.METADATA_DIM),
64
- (format_number(metadata.usage.output_tokens), ThemeKey.METADATA_DIM),
65
- )
66
- )
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))
67
70
 
68
71
  # Reasoning
69
72
  if metadata.usage.reasoning_tokens > 0:
@@ -71,7 +74,10 @@ def render_response_metadata(e: events.ResponseMetadataEvent) -> RenderableType:
71
74
  Text.assemble(
72
75
  ("thinking", ThemeKey.METADATA_DIM),
73
76
  (":", ThemeKey.METADATA_DIM),
74
- (format_number(metadata.usage.reasoning_tokens), ThemeKey.METADATA_DIM),
77
+ (
78
+ format_number(metadata.usage.reasoning_tokens),
79
+ ThemeKey.METADATA_DIM,
80
+ ),
75
81
  )
76
82
  )
77
83
 
@@ -81,7 +87,10 @@ def render_response_metadata(e: events.ResponseMetadataEvent) -> RenderableType:
81
87
  Text.assemble(
82
88
  ("context", ThemeKey.METADATA_DIM),
83
89
  (":", ThemeKey.METADATA_DIM),
84
- (f"{metadata.usage.context_usage_percent:.1f}%", ThemeKey.METADATA_DIM),
90
+ (
91
+ f"{metadata.usage.context_usage_percent:.1f}%",
92
+ ThemeKey.METADATA_DIM,
93
+ ),
85
94
  )
86
95
  )
87
96
 
@@ -95,16 +104,26 @@ def render_response_metadata(e: events.ResponseMetadataEvent) -> RenderableType:
95
104
  )
96
105
  )
97
106
 
98
- # Cost
107
+ # Duration
99
108
  if metadata.task_duration_s is not None:
100
109
  parts.append(
101
110
  Text.assemble(
102
- ("cost", ThemeKey.METADATA_DIM),
111
+ ("time", ThemeKey.METADATA_DIM),
103
112
  (":", ThemeKey.METADATA_DIM),
104
113
  (f"{metadata.task_duration_s:.1f}s", ThemeKey.METADATA_DIM),
105
114
  )
106
115
  )
107
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
+
108
127
  if parts:
109
128
  line2 = Text("/", style=ThemeKey.METADATA_DIM).join(parts)
110
129
  renderables.append(Padding(line2, (0, 0, 0, 2)))
@@ -117,9 +136,12 @@ def render_welcome(e: events.WelcomeEvent, *, box_style: Box | None = None) -> R
117
136
  if box_style is None:
118
137
  box_style = box.ROUNDED
119
138
 
139
+ debug_mode = is_debug_enabled()
140
+
120
141
  # First line: Klaude Code version
142
+ klaude_code_style = ThemeKey.WELCOME_DEBUG_TITLE if debug_mode else ThemeKey.WELCOME_HIGHLIGHT_BOLD
121
143
  panel_content = Text.assemble(
122
- ("Klaude Code", ThemeKey.WELCOME_HIGHLIGHT_BOLD),
144
+ ("Klaude Code", klaude_code_style),
123
145
  (f" v{_get_version()}\n", ThemeKey.WELCOME_INFO),
124
146
  (str(e.llm_config.model), ThemeKey.WELCOME_HIGHLIGHT),
125
147
  (" @ ", ThemeKey.WELCOME_INFO),
@@ -161,7 +183,8 @@ def render_welcome(e: events.WelcomeEvent, *, box_style: Box | None = None) -> R
161
183
  )
162
184
  )
163
185
 
186
+ border_style = ThemeKey.WELCOME_DEBUG_BORDER if debug_mode else ThemeKey.LINES
164
187
  return Group(
165
- Panel.fit(panel_content, border_style=ThemeKey.LINES, box=box_style),
188
+ Panel.fit(panel_content, border_style=border_style, box=box_style),
166
189
  "", # empty line
167
190
  )
@@ -1,12 +1,15 @@
1
+ import json
2
+
1
3
  from rich.console import Group, RenderableType
2
4
  from rich.panel import Panel
3
5
  from rich.style import Style
4
6
  from rich.text import Text
5
7
 
6
- from klaude_code.const import SUB_AGENT_RESULT_MAX_LINES
7
- from klaude_code.protocol import model
8
- from klaude_code.ui.base.theme import ThemeKey
9
- from klaude_code.ui.rich_ext.markdown import NoInsetMarkdown
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
10
13
 
11
14
 
12
15
  def render_sub_agent_call(e: model.SubAgentState, style: Style | None = None) -> RenderableType:
@@ -24,14 +27,45 @@ def render_sub_agent_call(e: model.SubAgentState, style: Style | None = None) ->
24
27
  def render_sub_agent_result(result: str, *, code_theme: str, style: Style | None = None) -> RenderableType:
25
28
  stripped_result = result.strip()
26
29
  lines = stripped_result.splitlines()
27
- if len(lines) > SUB_AGENT_RESULT_MAX_LINES:
28
- hidden_count = len(lines) - SUB_AGENT_RESULT_MAX_LINES
29
- truncated_text = "\n".join(lines[-SUB_AGENT_RESULT_MAX_LINES:])
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 :])
30
33
  return Panel.fit(
31
34
  Group(
32
- Text(f"... more {hidden_count} lines — use /export to view full output", style=ThemeKey.TOOL_RESULT),
35
+ Text(
36
+ f"… more {hidden_count} lines — use /export to view full output",
37
+ style=ThemeKey.TOOL_RESULT,
38
+ ),
33
39
  NoInsetMarkdown(truncated_text, code_theme=code_theme, style=style or ""),
34
40
  ),
35
41
  border_style=ThemeKey.LINES,
36
42
  )
37
- return Panel.fit(NoInsetMarkdown(stripped_result, code_theme=code_theme), border_style=ThemeKey.LINES)
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
+ )
@@ -1,7 +1,39 @@
1
+ from rich.console import RenderableType
2
+ from rich.padding import Padding
1
3
  from rich.text import Text
2
4
 
3
- from klaude_code.ui.base.theme import ThemeKey
5
+ from klaude_code.ui.rich.markdown import NoInsetMarkdown
6
+ from klaude_code.ui.rich.theme import ThemeKey
4
7
 
5
8
 
6
9
  def thinking_prefix() -> Text:
7
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
+ )