klaude-code 1.2.22__py3-none-any.whl → 1.2.24__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 (56) hide show
  1. klaude_code/command/prompt-jj-describe.md +32 -0
  2. klaude_code/command/status_cmd.py +1 -1
  3. klaude_code/{const/__init__.py → const.py} +11 -2
  4. klaude_code/core/executor.py +1 -1
  5. klaude_code/core/manager/sub_agent_manager.py +1 -1
  6. klaude_code/core/reminders.py +51 -0
  7. klaude_code/core/task.py +37 -18
  8. klaude_code/core/tool/__init__.py +1 -4
  9. klaude_code/core/tool/file/read_tool.py +23 -1
  10. klaude_code/core/tool/file/write_tool.py +7 -3
  11. klaude_code/core/tool/skill/__init__.py +0 -0
  12. klaude_code/core/tool/{memory → skill}/skill_tool.py +16 -39
  13. klaude_code/llm/openai_compatible/client.py +29 -102
  14. klaude_code/llm/openai_compatible/stream.py +272 -0
  15. klaude_code/llm/openrouter/client.py +29 -109
  16. klaude_code/llm/openrouter/{reasoning_handler.py → reasoning.py} +24 -2
  17. klaude_code/protocol/model.py +15 -2
  18. klaude_code/session/export.py +1 -1
  19. klaude_code/session/store.py +4 -2
  20. klaude_code/skill/__init__.py +27 -0
  21. klaude_code/skill/assets/deslop/SKILL.md +17 -0
  22. klaude_code/skill/assets/dev-docs/SKILL.md +108 -0
  23. klaude_code/skill/assets/handoff/SKILL.md +39 -0
  24. klaude_code/skill/assets/jj-workspace/SKILL.md +20 -0
  25. klaude_code/skill/assets/skill-creator/SKILL.md +139 -0
  26. klaude_code/{core/tool/memory/skill_loader.py → skill/loader.py} +60 -24
  27. klaude_code/skill/manager.py +70 -0
  28. klaude_code/skill/system_skills.py +192 -0
  29. klaude_code/ui/core/stage_manager.py +0 -3
  30. klaude_code/ui/modes/repl/completers.py +103 -3
  31. klaude_code/ui/modes/repl/event_handler.py +101 -49
  32. klaude_code/ui/modes/repl/input_prompt_toolkit.py +55 -6
  33. klaude_code/ui/modes/repl/renderer.py +24 -17
  34. klaude_code/ui/renderers/assistant.py +7 -2
  35. klaude_code/ui/renderers/developer.py +12 -0
  36. klaude_code/ui/renderers/diffs.py +1 -1
  37. klaude_code/ui/renderers/metadata.py +6 -8
  38. klaude_code/ui/renderers/sub_agent.py +28 -5
  39. klaude_code/ui/renderers/thinking.py +16 -10
  40. klaude_code/ui/renderers/tools.py +83 -34
  41. klaude_code/ui/renderers/user_input.py +32 -2
  42. klaude_code/ui/rich/markdown.py +40 -20
  43. klaude_code/ui/rich/status.py +15 -19
  44. klaude_code/ui/rich/theme.py +70 -17
  45. {klaude_code-1.2.22.dist-info → klaude_code-1.2.24.dist-info}/METADATA +18 -13
  46. {klaude_code-1.2.22.dist-info → klaude_code-1.2.24.dist-info}/RECORD +49 -45
  47. klaude_code/command/prompt-deslop.md +0 -14
  48. klaude_code/command/prompt-dev-docs-update.md +0 -56
  49. klaude_code/command/prompt-dev-docs.md +0 -46
  50. klaude_code/command/prompt-handoff.md +0 -33
  51. klaude_code/command/prompt-jj-workspace.md +0 -18
  52. klaude_code/core/tool/memory/__init__.py +0 -5
  53. klaude_code/llm/openai_compatible/stream_processor.py +0 -83
  54. /klaude_code/core/tool/{memory → skill}/skill_tool.md +0 -0
  55. {klaude_code-1.2.22.dist-info → klaude_code-1.2.24.dist-info}/WHEEL +0 -0
  56. {klaude_code-1.2.22.dist-info → klaude_code-1.2.24.dist-info}/entry_points.txt +0 -0
@@ -7,6 +7,7 @@ from typing import NamedTuple, override
7
7
 
8
8
  from prompt_toolkit import PromptSession
9
9
  from prompt_toolkit.completion import ThreadedCompleter
10
+ from prompt_toolkit.cursor_shapes import CursorShape
10
11
  from prompt_toolkit.formatted_text import FormattedText
11
12
  from prompt_toolkit.history import FileHistory
12
13
  from prompt_toolkit.patch_stdout import patch_stdout
@@ -17,6 +18,8 @@ from klaude_code.ui.core.input import InputProviderABC
17
18
  from klaude_code.ui.modes.repl.clipboard import capture_clipboard_tag, copy_to_clipboard, extract_images_from_text
18
19
  from klaude_code.ui.modes.repl.completers import AT_TOKEN_PATTERN, create_repl_completer
19
20
  from klaude_code.ui.modes.repl.key_bindings import create_key_bindings
21
+ from klaude_code.ui.renderers.user_input import USER_MESSAGE_MARK
22
+ from klaude_code.ui.terminal.color import is_light_terminal_background
20
23
  from klaude_code.ui.utils.common import get_current_git_branch, show_path_with_tilde
21
24
 
22
25
 
@@ -30,18 +33,27 @@ class REPLStatusSnapshot(NamedTuple):
30
33
  update_message: str | None = None
31
34
 
32
35
 
33
- COMPLETION_SELECTED = "#5869f7"
36
+ COMPLETION_SELECTED_DARK_BG = "#8b9bff"
37
+ COMPLETION_SELECTED_LIGHT_BG = "#5869f7"
38
+ COMPLETION_SELECTED_UNKNOWN_BG = "#7080f0"
34
39
  COMPLETION_MENU = "ansibrightblack"
35
- INPUT_PROMPT_STYLE = "ansimagenta"
40
+ INPUT_PROMPT_STYLE = "ansimagenta bold"
41
+ PLACEHOLDER_TEXT_STYLE_DARK_BG = "fg:#5a5a5a italic"
42
+ PLACEHOLDER_TEXT_STYLE_LIGHT_BG = "fg:#7a7a7a italic"
43
+ PLACEHOLDER_TEXT_STYLE_UNKNOWN_BG = "fg:#8a8a8a italic"
44
+ PLACEHOLDER_SYMBOL_STYLE_DARK_BG = "bg:#2a2a2a fg:#5a5a5a"
45
+ PLACEHOLDER_SYMBOL_STYLE_LIGHT_BG = "bg:#e6e6e6 fg:#7a7a7a"
46
+ PLACEHOLDER_SYMBOL_STYLE_UNKNOWN_BG = "bg:#2a2a2a fg:#8a8a8a"
36
47
 
37
48
 
38
49
  class PromptToolkitInput(InputProviderABC):
39
50
  def __init__(
40
51
  self,
41
- prompt: str = "❯ ",
52
+ prompt: str = USER_MESSAGE_MARK,
42
53
  status_provider: Callable[[], REPLStatusSnapshot] | None = None,
43
54
  ): # ▌
44
55
  self._status_provider = status_provider
56
+ self._is_light_terminal_background = is_light_terminal_background(timeout=0.2)
45
57
 
46
58
  project = str(Path.cwd()).strip("/").replace("/", "-")
47
59
  history_path = Path.home() / ".klaude" / "projects" / project / "input" / "input_history.txt"
@@ -56,10 +68,19 @@ class PromptToolkitInput(InputProviderABC):
56
68
  at_token_pattern=AT_TOKEN_PATTERN,
57
69
  )
58
70
 
71
+ # Select completion selected color based on terminal background
72
+ if self._is_light_terminal_background is True:
73
+ completion_selected = COMPLETION_SELECTED_LIGHT_BG
74
+ elif self._is_light_terminal_background is False:
75
+ completion_selected = COMPLETION_SELECTED_DARK_BG
76
+ else:
77
+ completion_selected = COMPLETION_SELECTED_UNKNOWN_BG
78
+
59
79
  self._session: PromptSession[str] = PromptSession(
60
80
  [(INPUT_PROMPT_STYLE, prompt)],
61
81
  history=FileHistory(str(history_path)),
62
82
  multiline=True,
83
+ cursor=CursorShape.BEAM,
63
84
  prompt_continuation=[(INPUT_PROMPT_STYLE, " ")],
64
85
  key_bindings=kb,
65
86
  completer=ThreadedCompleter(create_repl_completer()),
@@ -75,8 +96,8 @@ class PromptToolkitInput(InputProviderABC):
75
96
  "scrollbar.button": "bg:default",
76
97
  "completion-menu.completion": f"bg:default fg:{COMPLETION_MENU}",
77
98
  "completion-menu.meta.completion": f"bg:default fg:{COMPLETION_MENU}",
78
- "completion-menu.completion.current": f"noreverse bg:default fg:{COMPLETION_SELECTED} bold",
79
- "completion-menu.meta.completion.current": f"bg:default fg:{COMPLETION_SELECTED} bold",
99
+ "completion-menu.completion.current": f"noreverse bg:default fg:{completion_selected} bold",
100
+ "completion-menu.meta.completion.current": f"bg:default fg:{completion_selected} bold",
80
101
  }
81
102
  ),
82
103
  )
@@ -144,6 +165,34 @@ class PromptToolkitInput(InputProviderABC):
144
165
  toolbar_text = left_text + padding + right_text
145
166
  return FormattedText([("#2c7eac", toolbar_text)])
146
167
 
168
+ def _render_input_placeholder(self) -> FormattedText:
169
+ if self._is_light_terminal_background is True:
170
+ text_style = PLACEHOLDER_TEXT_STYLE_LIGHT_BG
171
+ symbol_style = PLACEHOLDER_SYMBOL_STYLE_LIGHT_BG
172
+ elif self._is_light_terminal_background is False:
173
+ text_style = PLACEHOLDER_TEXT_STYLE_DARK_BG
174
+ symbol_style = PLACEHOLDER_SYMBOL_STYLE_DARK_BG
175
+ else:
176
+ text_style = PLACEHOLDER_TEXT_STYLE_UNKNOWN_BG
177
+ symbol_style = PLACEHOLDER_SYMBOL_STYLE_UNKNOWN_BG
178
+
179
+ return FormattedText(
180
+ [
181
+ (text_style, " " * 10),
182
+ (symbol_style, " @ "),
183
+ (text_style, " "),
184
+ (text_style, "files"),
185
+ (text_style, " "),
186
+ (symbol_style, " $ "),
187
+ (text_style, " "),
188
+ (text_style, "skills"),
189
+ (text_style, " "),
190
+ (symbol_style, " / "),
191
+ (text_style, " "),
192
+ (text_style, "commands"),
193
+ ]
194
+ )
195
+
147
196
  async def start(self) -> None:
148
197
  pass
149
198
 
@@ -154,7 +203,7 @@ class PromptToolkitInput(InputProviderABC):
154
203
  async def iter_inputs(self) -> AsyncIterator[UserInputPayload]:
155
204
  while True:
156
205
  with patch_stdout():
157
- line: str = await self._session.prompt_async()
206
+ line: str = await self._session.prompt_async(placeholder=self._render_input_placeholder())
158
207
 
159
208
  # Extract images referenced in the input text
160
209
  images = extract_images_from_text(line)
@@ -5,14 +5,13 @@ from contextlib import contextmanager
5
5
  from dataclasses import dataclass
6
6
  from typing import Any
7
7
 
8
- from rich import box
9
- from rich.box import Box
10
8
  from rich.console import Console
11
9
  from rich.spinner import Spinner
12
10
  from rich.status import Status
13
11
  from rich.style import Style, StyleType
14
12
  from rich.text import Text
15
13
 
14
+ from klaude_code import const
16
15
  from klaude_code.protocol import events, model
17
16
  from klaude_code.ui.renderers import assistant as r_assistant
18
17
  from klaude_code.ui.renderers import developer as r_developer
@@ -32,6 +31,7 @@ from klaude_code.ui.rich.theme import ThemeKey, get_theme
32
31
  @dataclass
33
32
  class SessionStatus:
34
33
  color: Style | None = None
34
+ color_index: int | None = None
35
35
  sub_agent_state: model.SubAgentState | None = None
36
36
 
37
37
 
@@ -43,9 +43,9 @@ class REPLRenderer:
43
43
  self.console: Console = Console(theme=self.themes.app_theme)
44
44
  self.console.push_theme(self.themes.markdown_theme)
45
45
  self._spinner: Status = self.console.status(
46
- ShimmerStatusText("Thinking …", ThemeKey.SPINNER_STATUS_TEXT),
46
+ ShimmerStatusText(const.STATUS_DEFAULT_TEXT),
47
47
  spinner=r_status.spinner_name(),
48
- spinner_style=ThemeKey.SPINNER_STATUS,
48
+ spinner_style=ThemeKey.STATUS_SPINNER,
49
49
  )
50
50
 
51
51
  self.session_map: dict[str, SessionStatus] = {}
@@ -57,7 +57,9 @@ class REPLRenderer:
57
57
  sub_agent_state=sub_agent_state,
58
58
  )
59
59
  if sub_agent_state is not None:
60
- session_status.color = self.pick_sub_agent_color()
60
+ color, color_index = self.pick_sub_agent_color()
61
+ session_status.color = color
62
+ session_status.color_index = color_index
61
63
  self.session_map[session_id] = session_status
62
64
 
63
65
  def is_sub_agent_session(self, session_id: str) -> bool:
@@ -70,12 +72,12 @@ class REPLRenderer:
70
72
  return
71
73
  self.sub_agent_color_index = (self.sub_agent_color_index + 1) % palette_size
72
74
 
73
- def pick_sub_agent_color(self) -> Style:
75
+ def pick_sub_agent_color(self) -> tuple[Style, int]:
74
76
  self._advance_sub_agent_color_index()
75
77
  palette = self.themes.sub_agent_colors
76
78
  if not palette:
77
- return Style()
78
- return palette[self.sub_agent_color_index]
79
+ return Style(), 0
80
+ return palette[self.sub_agent_color_index], self.sub_agent_color_index
79
81
 
80
82
  def get_session_sub_agent_color(self, session_id: str) -> Style:
81
83
  status = self.session_map.get(session_id)
@@ -83,8 +85,12 @@ class REPLRenderer:
83
85
  return status.color
84
86
  return Style()
85
87
 
86
- def box_style(self) -> Box:
87
- return box.ROUNDED
88
+ def get_session_sub_agent_background(self, session_id: str) -> Style:
89
+ status = self.session_map.get(session_id)
90
+ backgrounds = self.themes.sub_agent_backgrounds
91
+ if status and status.color_index is not None and backgrounds:
92
+ return backgrounds[status.color_index]
93
+ return Style()
88
94
 
89
95
  @contextmanager
90
96
  def session_print_context(self, session_id: str) -> Iterator[None]:
@@ -114,7 +120,7 @@ class REPLRenderer:
114
120
  def display_tool_call_result(self, e: events.ToolResultEvent) -> None:
115
121
  if r_tools.is_sub_agent_tool(e.tool_name):
116
122
  return
117
- renderable = r_tools.render_tool_result(e)
123
+ renderable = r_tools.render_tool_result(e, code_theme=self.themes.code_theme)
118
124
  if renderable is not None:
119
125
  self.print(renderable)
120
126
 
@@ -152,7 +158,6 @@ class REPLRenderer:
152
158
  case events.ThinkingEvent() as e:
153
159
  if is_sub_agent:
154
160
  continue
155
- self.display_thinking_prefix()
156
161
  self.display_thinking(e.content)
157
162
  case events.DeveloperMessageEvent() as e:
158
163
  self.display_developer_message(e)
@@ -196,7 +201,7 @@ class REPLRenderer:
196
201
  self.print()
197
202
 
198
203
  def display_welcome(self, event: events.WelcomeEvent) -> None:
199
- self.print(r_metadata.render_welcome(event, box_style=self.box_style()))
204
+ self.print(r_metadata.render_welcome(event))
200
205
 
201
206
  def display_user_message(self, event: events.UserMessageEvent) -> None:
202
207
  self.print(r_user_input.render_user_input(event.content))
@@ -229,12 +234,17 @@ class REPLRenderer:
229
234
 
230
235
  def display_task_finish(self, event: events.TaskFinishEvent) -> None:
231
236
  if self.is_sub_agent_session(event.session_id):
237
+ session_status = self.session_map.get(event.session_id)
238
+ description = session_status.sub_agent_state.sub_agent_desc if session_status and session_status.sub_agent_state else None
239
+ panel_style = self.get_session_sub_agent_background(event.session_id)
232
240
  with self.session_print_context(event.session_id):
233
241
  self.print(
234
242
  r_sub_agent.render_sub_agent_result(
235
243
  event.task_result,
236
244
  code_theme=self.themes.code_theme,
237
245
  has_structured_output=event.has_structured_output,
246
+ description=description,
247
+ panel_style=panel_style,
238
248
  )
239
249
  )
240
250
 
@@ -249,9 +259,6 @@ class REPLRenderer:
249
259
  )
250
260
  )
251
261
 
252
- def display_thinking_prefix(self) -> None:
253
- self.print(r_thinking.thinking_prefix())
254
-
255
262
  # -------------------------------------------------------------------------
256
263
  # Spinner control methods
257
264
  # -------------------------------------------------------------------------
@@ -266,7 +273,7 @@ class REPLRenderer:
266
273
 
267
274
  def spinner_update(self, status_text: str | Text, right_text: Text | None = None) -> None:
268
275
  """Update the spinner status text with optional right-aligned text."""
269
- self._spinner.update(ShimmerStatusText(status_text, ThemeKey.SPINNER_STATUS_TEXT, right_text))
276
+ self._spinner.update(ShimmerStatusText(status_text, right_text))
270
277
 
271
278
  def spinner_renderable(self) -> Spinner:
272
279
  """Return the spinner's renderable for embedding in other components."""
@@ -1,8 +1,13 @@
1
1
  from rich.console import RenderableType
2
+ from rich.padding import Padding
2
3
 
4
+ from klaude_code import const
3
5
  from klaude_code.ui.renderers.common import create_grid
4
6
  from klaude_code.ui.rich.markdown import NoInsetMarkdown
5
7
 
8
+ # UI markers
9
+ ASSISTANT_MESSAGE_MARK = "◆"
10
+
6
11
 
7
12
  def render_assistant_message(content: str, *, code_theme: str) -> RenderableType | None:
8
13
  """Render assistant message for replay history display.
@@ -15,7 +20,7 @@ def render_assistant_message(content: str, *, code_theme: str) -> RenderableType
15
20
 
16
21
  grid = create_grid()
17
22
  grid.add_row(
18
- "•",
19
- NoInsetMarkdown(stripped, code_theme=code_theme),
23
+ ASSISTANT_MESSAGE_MARK,
24
+ Padding(NoInsetMarkdown(stripped, code_theme=code_theme), (0, const.MARKDOWN_RIGHT_MARGIN, 0, 0)),
20
25
  )
21
26
  return grid
@@ -17,6 +17,7 @@ def need_render_developer_message(e: events.DeveloperMessageEvent) -> bool:
17
17
  or e.item.todo_use
18
18
  or e.item.at_files
19
19
  or e.item.user_image_count
20
+ or e.item.skill_name
20
21
  )
21
22
 
22
23
 
@@ -93,6 +94,17 @@ def render_developer_message(e: events.DeveloperMessageEvent) -> RenderableType:
93
94
  )
94
95
  parts.append(grid)
95
96
 
97
+ if sn := e.item.skill_name:
98
+ grid = create_grid()
99
+ grid.add_row(
100
+ Text(" +", style=ThemeKey.REMINDER),
101
+ Text.assemble(
102
+ ("Activated skill ", ThemeKey.REMINDER),
103
+ (sn, ThemeKey.REMINDER_BOLD),
104
+ ),
105
+ )
106
+ parts.append(grid)
107
+
96
108
  return Group(*parts) if parts else Text("")
97
109
 
98
110
 
@@ -208,7 +208,7 @@ def render_diff_panel(
208
208
  diff_text: str,
209
209
  *,
210
210
  show_file_name: bool = True,
211
- heading: str = "Git Diff",
211
+ heading: str = "DIFF",
212
212
  indent: int = 2,
213
213
  ) -> RenderableType:
214
214
  lines = diff_text.splitlines()
@@ -1,7 +1,6 @@
1
1
  from importlib.metadata import version
2
2
 
3
3
  from rich import box
4
- from rich.box import Box
5
4
  from rich.console import Group, RenderableType
6
5
  from rich.padding import Padding
7
6
  from rich.panel import Panel
@@ -45,7 +44,7 @@ def _render_task_metadata_block(
45
44
  currency_symbol = "¥" if currency == "CNY" else "$"
46
45
 
47
46
  # First column: mark only
48
- mark = Text("└", style=ThemeKey.METADATA_DIM) if is_sub_agent else Text("", style=ThemeKey.METADATA_BOLD)
47
+ mark = Text("└", style=ThemeKey.METADATA_DIM) if is_sub_agent else Text("", style=ThemeKey.METADATA)
49
48
 
50
49
  # Second column: model@provider / tokens / cost / ...
51
50
  content = Text()
@@ -151,7 +150,9 @@ def render_task_metadata(e: events.TaskMetadataEvent) -> RenderableType:
151
150
  """Render task metadata including main agent and sub-agents, aggregated by model+provider."""
152
151
  renderables: list[RenderableType] = []
153
152
 
154
- renderables.append(_render_task_metadata_block(e.metadata.main, is_sub_agent=False, show_context_and_time=True))
153
+ renderables.append(
154
+ _render_task_metadata_block(e.metadata.main_agent, is_sub_agent=False, show_context_and_time=True)
155
+ )
155
156
 
156
157
  # Aggregate by (model_name, provider), sorted by total_cost descending
157
158
  sorted_items = model.TaskMetadata.aggregate_by_model(e.metadata.sub_agent_task_metadata)
@@ -163,11 +164,8 @@ def render_task_metadata(e: events.TaskMetadataEvent) -> RenderableType:
163
164
  return Group(*renderables)
164
165
 
165
166
 
166
- def render_welcome(e: events.WelcomeEvent, *, box_style: Box | None = None) -> RenderableType:
167
+ def render_welcome(e: events.WelcomeEvent) -> RenderableType:
167
168
  """Render the welcome panel with model info and settings."""
168
- if box_style is None:
169
- box_style = box.ROUNDED
170
-
171
169
  debug_mode = is_debug_enabled()
172
170
 
173
171
  # First line: Klaude Code version
@@ -217,6 +215,6 @@ def render_welcome(e: events.WelcomeEvent, *, box_style: Box | None = None) -> R
217
215
 
218
216
  border_style = ThemeKey.WELCOME_DEBUG_BORDER if debug_mode else ThemeKey.LINES
219
217
  return Group(
220
- Panel.fit(panel_content, border_style=border_style, box=box_style),
218
+ Panel.fit(panel_content, border_style=border_style, box=box.ROUNDED),
221
219
  "", # empty line
222
220
  )
@@ -1,6 +1,7 @@
1
1
  import json
2
2
  from typing import Any, cast
3
3
 
4
+ from rich import box
4
5
  from rich.console import Group, RenderableType
5
6
  from rich.json import JSON
6
7
  from rich.panel import Panel
@@ -58,10 +59,22 @@ def render_sub_agent_call(e: model.SubAgentState, style: Style | None = None) ->
58
59
 
59
60
 
60
61
  def render_sub_agent_result(
61
- result: str, *, code_theme: str, style: Style | None = None, has_structured_output: bool = False
62
+ result: str,
63
+ *,
64
+ code_theme: str,
65
+ style: Style | None = None,
66
+ has_structured_output: bool = False,
67
+ description: str | None = None,
68
+ panel_style: Style | None = None,
62
69
  ) -> RenderableType:
63
70
  stripped_result = result.strip()
64
71
 
72
+ # Add markdown heading if description is provided
73
+ if description:
74
+ stripped_result = f"# {description}\n\n{stripped_result}"
75
+
76
+ result_panel_style = panel_style or ThemeKey.SUB_AGENT_RESULT_PANEL
77
+
65
78
  # Use rich JSON for structured output
66
79
  if has_structured_output:
67
80
  try:
@@ -73,7 +86,9 @@ def render_sub_agent_result(
73
86
  ),
74
87
  JSON(stripped_result),
75
88
  ),
89
+ box=box.SIMPLE,
76
90
  border_style=ThemeKey.LINES,
91
+ style=result_panel_style,
77
92
  )
78
93
  except json.JSONDecodeError:
79
94
  # Fall back to markdown if not valid JSON
@@ -82,20 +97,28 @@ def render_sub_agent_result(
82
97
  lines = stripped_result.splitlines()
83
98
  if len(lines) > const.SUB_AGENT_RESULT_MAX_LINES:
84
99
  hidden_count = len(lines) - const.SUB_AGENT_RESULT_MAX_LINES
85
- truncated_text = "\n".join(lines[-const.SUB_AGENT_RESULT_MAX_LINES :])
100
+ head_count = const.SUB_AGENT_RESULT_MAX_LINES // 2
101
+ tail_count = const.SUB_AGENT_RESULT_MAX_LINES - head_count
102
+ head_text = "\n".join(lines[:head_count])
103
+ tail_text = "\n".join(lines[-tail_count:])
86
104
  return Panel.fit(
87
105
  Group(
106
+ NoInsetMarkdown(head_text, code_theme=code_theme, style=style or ""),
88
107
  Text(
89
- f"… more {hidden_count} lines — use /export to view full output",
90
- style=ThemeKey.TOOL_RESULT,
108
+ f"\n… more {hidden_count} lines — use /export to view full output\n",
109
+ style=ThemeKey.TOOL_RESULT_TRUNCATED,
91
110
  ),
92
- NoInsetMarkdown(truncated_text, code_theme=code_theme, style=style or ""),
111
+ NoInsetMarkdown(tail_text, code_theme=code_theme, style=style or ""),
93
112
  ),
113
+ box=box.SIMPLE,
94
114
  border_style=ThemeKey.LINES,
115
+ style=result_panel_style,
95
116
  )
96
117
  return Panel.fit(
97
118
  NoInsetMarkdown(stripped_result, code_theme=code_theme),
119
+ box=box.SIMPLE,
98
120
  border_style=ThemeKey.LINES,
121
+ style=result_panel_style,
99
122
  )
100
123
 
101
124
 
@@ -4,12 +4,13 @@ from rich.console import RenderableType
4
4
  from rich.padding import Padding
5
5
  from rich.text import Text
6
6
 
7
+ from klaude_code import const
8
+ from klaude_code.ui.renderers.common import create_grid
7
9
  from klaude_code.ui.rich.markdown import ThinkingMarkdown
8
10
  from klaude_code.ui.rich.theme import ThemeKey
9
11
 
10
-
11
- def thinking_prefix() -> Text:
12
- return Text.from_markup("[not italic]⸫[/not italic] Thinking …", style=ThemeKey.THINKING)
12
+ # UI markers
13
+ THINKING_MESSAGE_MARK = "⠶"
13
14
 
14
15
 
15
16
  def normalize_thinking_content(content: str) -> str:
@@ -37,7 +38,7 @@ def normalize_thinking_content(content: str) -> str:
37
38
 
38
39
 
39
40
  def render_thinking(content: str, *, code_theme: str, style: str) -> RenderableType | None:
40
- """Render thinking content as indented markdown.
41
+ """Render thinking content as markdown with left mark.
41
42
 
42
43
  Returns None if content is empty.
43
44
  Note: Caller should push thinking_markdown_theme before printing.
@@ -45,11 +46,16 @@ def render_thinking(content: str, *, code_theme: str, style: str) -> RenderableT
45
46
  if len(content.strip()) == 0:
46
47
  return None
47
48
 
48
- return Padding.indent(
49
- ThinkingMarkdown(
50
- normalize_thinking_content(content),
51
- code_theme=code_theme,
52
- style=style,
49
+ grid = create_grid()
50
+ grid.add_row(
51
+ Text(THINKING_MESSAGE_MARK, style=ThemeKey.THINKING),
52
+ Padding(
53
+ ThinkingMarkdown(
54
+ normalize_thinking_content(content),
55
+ code_theme=code_theme,
56
+ style=style,
57
+ ),
58
+ (0, const.MARKDOWN_RIGHT_MARGIN, 0, 0),
53
59
  ),
54
- level=2,
55
60
  )
61
+ return grid