klaude-code 1.2.13__py3-none-any.whl → 1.2.15__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 (36) hide show
  1. klaude_code/cli/auth_cmd.py +73 -0
  2. klaude_code/cli/config_cmd.py +88 -0
  3. klaude_code/cli/debug.py +72 -0
  4. klaude_code/cli/main.py +31 -142
  5. klaude_code/cli/runtime.py +0 -31
  6. klaude_code/cli/session_cmd.py +3 -1
  7. klaude_code/command/model_cmd.py +1 -1
  8. klaude_code/command/thinking_cmd.py +1 -1
  9. klaude_code/config/__init__.py +0 -4
  10. klaude_code/config/config.py +31 -4
  11. klaude_code/const/__init__.py +8 -3
  12. klaude_code/core/agent.py +1 -1
  13. klaude_code/core/prompt.py +10 -5
  14. klaude_code/llm/client.py +4 -0
  15. klaude_code/llm/input_common.py +1 -1
  16. klaude_code/llm/openrouter/client.py +5 -4
  17. klaude_code/protocol/events.py +2 -2
  18. klaude_code/protocol/sub_agent.py +0 -1
  19. klaude_code/session/export.py +58 -0
  20. klaude_code/session/session.py +35 -3
  21. klaude_code/session/templates/export_session.html +54 -12
  22. klaude_code/trace/__init__.py +2 -2
  23. klaude_code/trace/log.py +143 -4
  24. klaude_code/ui/modes/debug/display.py +2 -1
  25. klaude_code/ui/modes/repl/completers.py +3 -3
  26. klaude_code/ui/modes/repl/event_handler.py +2 -1
  27. klaude_code/ui/modes/repl/renderer.py +48 -61
  28. klaude_code/ui/renderers/metadata.py +49 -88
  29. klaude_code/ui/renderers/thinking.py +2 -2
  30. klaude_code/ui/renderers/tools.py +49 -1
  31. klaude_code/ui/rich/markdown.py +27 -11
  32. klaude_code/ui/rich/theme.py +13 -13
  33. {klaude_code-1.2.13.dist-info → klaude_code-1.2.15.dist-info}/METADATA +1 -1
  34. {klaude_code-1.2.13.dist-info → klaude_code-1.2.15.dist-info}/RECORD +36 -33
  35. {klaude_code-1.2.13.dist-info → klaude_code-1.2.15.dist-info}/WHEEL +0 -0
  36. {klaude_code-1.2.13.dist-info → klaude_code-1.2.15.dist-info}/entry_points.txt +0 -0
@@ -99,48 +99,21 @@ class REPLRenderer:
99
99
  def print(self, *objects: Any, style: StyleType | None = None, end: str = "\n") -> None:
100
100
  if self.current_sub_agent_color:
101
101
  if objects:
102
- self.console.print(Quote(*objects, style=self.current_sub_agent_color))
102
+ content = objects[0] if len(objects) == 1 else objects
103
+ self.console.print(Quote(content, style=self.current_sub_agent_color))
103
104
  return
104
105
  self.console.print(*objects, style=style, end=end)
105
106
 
106
107
  def display_tool_call(self, e: events.ToolCallEvent) -> None:
107
- # Handle sub-agent tool calls in replay mode
108
108
  if r_tools.is_sub_agent_tool(e.tool_name):
109
- if e.is_replay:
110
- state = r_sub_agent.build_sub_agent_state_from_tool_call(e)
111
- if state is not None:
112
- sub_agent_default_style = (
113
- self.themes.sub_agent_colors[0] if self.themes.sub_agent_colors else Style()
114
- )
115
- self.print(
116
- Quote(
117
- r_sub_agent.render_sub_agent_call(state, sub_agent_default_style),
118
- style=sub_agent_default_style,
119
- )
120
- )
121
109
  return
122
-
123
110
  renderable = r_tools.render_tool_call(e)
124
111
  if renderable is not None:
125
112
  self.print(renderable)
126
113
 
127
114
  def display_tool_call_result(self, e: events.ToolResultEvent) -> None:
128
- # Handle sub-agent tool results in replay mode
129
115
  if r_tools.is_sub_agent_tool(e.tool_name):
130
- if e.is_replay:
131
- sub_agent_default_style = self.themes.sub_agent_colors[0] if self.themes.sub_agent_colors else Style()
132
- self.print(
133
- Quote(
134
- r_sub_agent.render_sub_agent_result(
135
- e.result,
136
- code_theme=self.themes.code_theme,
137
- style=sub_agent_default_style,
138
- ),
139
- style=sub_agent_default_style,
140
- )
141
- )
142
116
  return
143
-
144
117
  renderable = r_tools.render_tool_result(e)
145
118
  if renderable is not None:
146
119
  self.print(renderable)
@@ -160,39 +133,53 @@ class REPLRenderer:
160
133
  async def replay_history(self, history_events: events.ReplayHistoryEvent) -> None:
161
134
  tool_call_dict: dict[str, events.ToolCallEvent] = {}
162
135
  for event in history_events.events:
163
- match event:
164
- case events.TurnStartEvent():
165
- self.print()
166
- case events.AssistantMessageEvent() as assistant_event:
167
- renderable = r_assistant.render_assistant_message(
168
- assistant_event.content, code_theme=self.themes.code_theme
169
- )
170
- if renderable is not None:
171
- self.print(renderable)
136
+ event_session_id = getattr(event, "session_id", history_events.session_id)
137
+ is_sub_agent = self.is_sub_agent_session(event_session_id)
138
+
139
+ with self.session_print_context(event_session_id):
140
+ match event:
141
+ case events.TaskStartEvent() as e:
142
+ self.display_task_start(e)
143
+ case events.TurnStartEvent():
144
+ self.print()
145
+ case events.AssistantMessageEvent() as e:
146
+ if is_sub_agent:
147
+ continue
148
+ renderable = r_assistant.render_assistant_message(e.content, code_theme=self.themes.code_theme)
149
+ if renderable is not None:
150
+ self.print(renderable)
151
+ self.print()
152
+ case events.ThinkingEvent() as e:
153
+ if is_sub_agent:
154
+ continue
155
+ self.display_thinking(e.content)
156
+ case events.DeveloperMessageEvent() as e:
157
+ self.display_developer_message(e)
158
+ self.display_command_output(e)
159
+ case events.UserMessageEvent() as e:
160
+ if is_sub_agent:
161
+ continue
162
+ self.print(r_user_input.render_user_input(e.content))
163
+ case events.ToolCallEvent() as e:
164
+ tool_call_dict[e.tool_call_id] = e
165
+ case events.ToolResultEvent() as e:
166
+ tool_call_event = tool_call_dict.get(e.tool_call_id)
167
+ if tool_call_event is not None:
168
+ self.display_tool_call(tool_call_event)
169
+ tool_call_dict.pop(e.tool_call_id, None)
170
+ if is_sub_agent:
171
+ continue
172
+ self.display_tool_call_result(e)
173
+ case events.TaskMetadataEvent() as e:
174
+ self.print(r_metadata.render_task_metadata(e))
175
+ self.print()
176
+ case events.InterruptEvent():
172
177
  self.print()
173
- case events.ThinkingEvent() as thinking_event:
174
- self.display_thinking(thinking_event.content)
175
- case events.DeveloperMessageEvent() as developer_event:
176
- self.display_developer_message(developer_event)
177
- self.display_command_output(developer_event)
178
- case events.UserMessageEvent() as user_event:
179
- self.print(r_user_input.render_user_input(user_event.content))
180
- case events.ToolCallEvent() as tool_call_event:
181
- tool_call_dict[tool_call_event.tool_call_id] = tool_call_event
182
- case events.ToolResultEvent() as tool_result_event:
183
- tool_call_event = tool_call_dict.get(tool_result_event.tool_call_id)
184
- if tool_call_event is not None:
185
- self.display_tool_call(tool_call_event)
186
- tool_call_dict.pop(tool_result_event.tool_call_id, None)
187
- self.display_tool_call_result(tool_result_event)
188
- case events.TaskMetadataEvent() as metadata_event:
189
- self.print(r_metadata.render_task_metadata(metadata_event))
190
- self.print()
191
- case events.InterruptEvent():
192
- self.print()
193
- self.print(r_user_input.render_interrupt())
194
- case events.ErrorEvent() as e:
195
- self.display_error(e)
178
+ self.print(r_user_input.render_interrupt())
179
+ case events.ErrorEvent() as e:
180
+ self.display_error(e)
181
+ case events.TaskFinishEvent() as e:
182
+ self.display_task_finish(e)
196
183
 
197
184
  def display_developer_message(self, e: events.DeveloperMessageEvent) -> None:
198
185
  if not r_developer.need_render_developer_message(e):
@@ -9,6 +9,7 @@ from rich.text import Text
9
9
 
10
10
  from klaude_code.protocol import events, model
11
11
  from klaude_code.trace import is_debug_enabled
12
+ from klaude_code.ui.renderers.common import create_grid
12
13
  from klaude_code.ui.rich.theme import ThemeKey
13
14
  from klaude_code.ui.utils.common import format_number
14
15
 
@@ -24,163 +25,123 @@ def _get_version() -> str:
24
25
  def _render_task_metadata_block(
25
26
  metadata: model.TaskMetadata,
26
27
  *,
27
- indent: int = 0,
28
+ is_sub_agent: bool = False,
28
29
  show_context_and_time: bool = True,
29
- ) -> list[RenderableType]:
30
+ ) -> RenderableType:
30
31
  """Render a single TaskMetadata block.
31
32
 
32
33
  Args:
33
34
  metadata: The TaskMetadata to render.
34
- indent: Number of spaces to indent (0 for main, 2 for sub-agents).
35
+ is_sub_agent: Whether this is a sub-agent block.
35
36
  show_context_and_time: Whether to show context usage percent and time.
36
37
 
37
38
  Returns:
38
- List of renderables for this metadata block.
39
+ A renderable for this metadata block.
39
40
  """
41
+ grid = create_grid()
42
+
40
43
  # Get currency symbol
41
44
  currency = metadata.usage.currency if metadata.usage else "USD"
42
45
  currency_symbol = "¥" if currency == "CNY" else "$"
43
46
 
44
- # Line 1: Model and Provider
45
- prefix = (
46
- Text(" " * indent + "• ", style=ThemeKey.METADATA_BOLD)
47
- if indent == 0
48
- else Text(" " * indent + "└ ", style=ThemeKey.METADATA_DIM)
49
- )
50
- model_text = Text()
51
- model_text.append_text(prefix).append_text(Text(metadata.model_name, style=ThemeKey.METADATA_BOLD))
47
+ # First column: mark only
48
+ mark = Text("└", style=ThemeKey.METADATA_DIM) if is_sub_agent else Text("•", style=ThemeKey.METADATA_BOLD)
49
+
50
+ # Second column: model@provider / tokens / cost / ...
51
+ content = Text()
52
+ content.append_text(Text(metadata.model_name, style=ThemeKey.METADATA_BOLD))
52
53
  if metadata.provider is not None:
53
- model_text.append_text(Text("@", style=ThemeKey.METADATA)).append_text(
54
+ content.append_text(Text("@", style=ThemeKey.METADATA)).append_text(
54
55
  Text(metadata.provider.lower().replace(" ", "-"), style=ThemeKey.METADATA)
55
56
  )
56
57
 
57
- renderables: list[RenderableType] = [model_text]
58
-
59
- # Line 2: Token consumption, Context, TPS, Cost
60
- parts2: list[Text] = []
58
+ # All info parts (tokens, cost, context, etc.)
59
+ parts: list[Text] = []
61
60
 
62
61
  if metadata.usage is not None:
63
- # Input
64
- input_parts: list[tuple[str, str]] = [
65
- ("input:", ThemeKey.METADATA_DIM),
66
- (format_number(metadata.usage.input_tokens), ThemeKey.METADATA_DIM),
62
+ # Tokens: ↑37k c5k ↓907 r45k
63
+ token_parts: list[tuple[str, str]] = [
64
+ ("", ThemeKey.METADATA_DIM),
65
+ (format_number(metadata.usage.input_tokens), ThemeKey.METADATA),
67
66
  ]
68
- if metadata.usage.input_cost is not None:
69
- input_parts.append((f"({currency_symbol}{metadata.usage.input_cost:.4f})", ThemeKey.METADATA_DIM))
70
- parts2.append(Text.assemble(*input_parts))
71
-
72
- # Cached
73
67
  if metadata.usage.cached_tokens > 0:
74
- cached_parts: list[tuple[str, str]] = [
75
- ("cached:", ThemeKey.METADATA_DIM),
76
- (format_number(metadata.usage.cached_tokens), ThemeKey.METADATA_DIM),
77
- ]
78
- if metadata.usage.cache_read_cost is not None:
79
- cached_parts.append((f"({currency_symbol}{metadata.usage.cache_read_cost:.4f})", ThemeKey.METADATA_DIM))
80
- parts2.append(Text.assemble(*cached_parts))
81
-
82
- # Output
83
- output_parts: list[tuple[str, str]] = [
84
- ("output:", ThemeKey.METADATA_DIM),
85
- (format_number(metadata.usage.output_tokens), ThemeKey.METADATA_DIM),
86
- ]
87
- if metadata.usage.output_cost is not None:
88
- output_parts.append((f"({currency_symbol}{metadata.usage.output_cost:.4f})", ThemeKey.METADATA_DIM))
89
- parts2.append(Text.assemble(*output_parts))
90
-
91
- # Reasoning
68
+ token_parts.append((" c", ThemeKey.METADATA_DIM))
69
+ token_parts.append((format_number(metadata.usage.cached_tokens), ThemeKey.METADATA))
70
+ token_parts.append((" ↓", ThemeKey.METADATA_DIM))
71
+ token_parts.append((format_number(metadata.usage.output_tokens), ThemeKey.METADATA))
92
72
  if metadata.usage.reasoning_tokens > 0:
93
- parts2.append(
94
- Text.assemble(
95
- ("thinking", ThemeKey.METADATA_DIM),
96
- (":", ThemeKey.METADATA_DIM),
97
- (
98
- format_number(metadata.usage.reasoning_tokens),
99
- ThemeKey.METADATA_DIM,
100
- ),
101
- )
102
- )
73
+ token_parts.append((" r", ThemeKey.METADATA_DIM))
74
+ token_parts.append((format_number(metadata.usage.reasoning_tokens), ThemeKey.METADATA))
75
+ parts.append(Text.assemble(*token_parts))
103
76
 
104
77
  # Cost
105
78
  if metadata.usage is not None and metadata.usage.total_cost is not None:
106
- parts2.append(
79
+ parts.append(
107
80
  Text.assemble(
108
- ("cost", ThemeKey.METADATA_DIM),
109
- (":", ThemeKey.METADATA_DIM),
110
- (f"{currency_symbol}{metadata.usage.total_cost:.4f}", ThemeKey.METADATA_DIM),
81
+ (currency_symbol, ThemeKey.METADATA_DIM),
82
+ (f"{metadata.usage.total_cost:.4f}", ThemeKey.METADATA),
111
83
  )
112
84
  )
113
- if parts2:
114
- line2 = Text(" / ", style=ThemeKey.METADATA_DIM).join(parts2)
115
- renderables.append(Padding(line2, (0, 0, 0, indent + 2)))
116
-
117
- parts3: list[Text] = []
118
85
  if metadata.usage is not None:
119
86
  # Context (only for main agent)
120
87
  if show_context_and_time and metadata.usage.context_usage_percent is not None:
121
88
  context_size = format_number(metadata.usage.context_size or 0)
122
- parts3.append(
89
+ parts.append(
123
90
  Text.assemble(
124
- ("context", ThemeKey.METADATA_DIM),
125
- (":", ThemeKey.METADATA_DIM),
126
- (
127
- f"{context_size}({metadata.usage.context_usage_percent:.1f}%)",
128
- ThemeKey.METADATA_DIM,
129
- ),
91
+ (context_size, ThemeKey.METADATA),
92
+ (f" ({metadata.usage.context_usage_percent:.1f}%)", ThemeKey.METADATA_DIM),
130
93
  )
131
94
  )
132
95
 
133
96
  # TPS
134
97
  if metadata.usage.throughput_tps is not None:
135
- parts3.append(
98
+ parts.append(
136
99
  Text.assemble(
100
+ (f"{metadata.usage.throughput_tps:.1f} ", ThemeKey.METADATA),
137
101
  ("tps", ThemeKey.METADATA_DIM),
138
- (":", ThemeKey.METADATA_DIM),
139
- (f"{metadata.usage.throughput_tps:.1f}", ThemeKey.METADATA_DIM),
140
102
  )
141
103
  )
142
104
 
143
105
  # Duration
144
106
  if show_context_and_time and metadata.task_duration_s is not None:
145
- parts3.append(
107
+ parts.append(
146
108
  Text.assemble(
147
- ("time", ThemeKey.METADATA_DIM),
148
- (":", ThemeKey.METADATA_DIM),
149
- (f"{metadata.task_duration_s:.1f}s", ThemeKey.METADATA_DIM),
109
+ (f"{metadata.task_duration_s:.1f}", ThemeKey.METADATA),
110
+ ("s", ThemeKey.METADATA_DIM),
150
111
  )
151
112
  )
152
113
 
153
114
  # Turn count
154
115
  if show_context_and_time and metadata.turn_count > 0:
155
- parts3.append(
116
+ parts.append(
156
117
  Text.assemble(
157
- ("turns", ThemeKey.METADATA_DIM),
158
- (":", ThemeKey.METADATA_DIM),
159
- (str(metadata.turn_count), ThemeKey.METADATA_DIM),
118
+ (str(metadata.turn_count), ThemeKey.METADATA),
119
+ (" turns", ThemeKey.METADATA_DIM),
160
120
  )
161
121
  )
162
122
 
163
- if parts3:
164
- line2 = Text(" / ", style=ThemeKey.METADATA_DIM).join(parts3)
165
- renderables.append(Padding(line2, (0, 0, 0, indent + 2)))
123
+ if parts:
124
+ content.append_text(Text(" · ", style=ThemeKey.METADATA_DIM))
125
+ content.append_text(Text(" · ", style=ThemeKey.METADATA_DIM).join(parts))
166
126
 
167
- return renderables
127
+ grid.add_row(mark, content)
128
+ return grid if not is_sub_agent else Padding(grid, (0, 0, 0, 2))
168
129
 
169
130
 
170
131
  def render_task_metadata(e: events.TaskMetadataEvent) -> RenderableType:
171
132
  """Render task metadata including main agent and sub-agents, aggregated by model+provider."""
172
133
  renderables: list[RenderableType] = []
173
134
 
174
- renderables.extend(_render_task_metadata_block(e.metadata.main, indent=0, show_context_and_time=True))
135
+ renderables.append(_render_task_metadata_block(e.metadata.main, is_sub_agent=False, show_context_and_time=True))
175
136
 
176
137
  # Aggregate by (model_name, provider), sorted by total_cost descending
177
138
  sorted_items = model.TaskMetadata.aggregate_by_model(e.metadata.sub_agent_task_metadata)
178
139
 
179
140
  # Render each aggregated model block
180
141
  for meta in sorted_items:
181
- renderables.extend(_render_task_metadata_block(meta, indent=2, show_context_and_time=False))
142
+ renderables.append(_render_task_metadata_block(meta, is_sub_agent=True, show_context_and_time=False))
182
143
 
183
- return Group(*renderables)
144
+ return Padding(Group(*renderables), (0, 0, 0, 1))
184
145
 
185
146
 
186
147
  def render_welcome(e: events.WelcomeEvent, *, box_style: Box | None = None) -> RenderableType:
@@ -2,7 +2,7 @@ from rich.console import RenderableType
2
2
  from rich.padding import Padding
3
3
  from rich.text import Text
4
4
 
5
- from klaude_code.ui.rich.markdown import NoInsetMarkdown
5
+ from klaude_code.ui.rich.markdown import ThinkingMarkdown
6
6
  from klaude_code.ui.rich.theme import ThemeKey
7
7
 
8
8
 
@@ -30,7 +30,7 @@ def render_thinking(content: str, *, code_theme: str, style: str) -> RenderableT
30
30
  return None
31
31
 
32
32
  return Padding.indent(
33
- NoInsetMarkdown(
33
+ ThinkingMarkdown(
34
34
  _normalize_thinking_content(content),
35
35
  code_theme=code_theme,
36
36
  style=style,
@@ -1,5 +1,6 @@
1
1
  import json
2
2
  from pathlib import Path
3
+ from typing import Any, cast
3
4
 
4
5
  from rich.console import RenderableType
5
6
  from rich.padding import Padding
@@ -58,6 +59,49 @@ def render_generic_tool_call(tool_name: str, arguments: str, markup: str = "•"
58
59
  return grid
59
60
 
60
61
 
62
+ def render_bash_tool_call(arguments: str) -> RenderableType:
63
+ grid = create_grid()
64
+ tool_name_column = Text.assemble((">", ThemeKey.TOOL_MARK), " ", ("Bash", ThemeKey.TOOL_NAME))
65
+
66
+ try:
67
+ payload_raw: Any = json.loads(arguments) if arguments else {}
68
+ except json.JSONDecodeError:
69
+ summary = Text(
70
+ arguments.strip()[: const.INVALID_TOOL_CALL_MAX_LENGTH],
71
+ style=ThemeKey.INVALID_TOOL_CALL_ARGS,
72
+ )
73
+ grid.add_row(tool_name_column, summary)
74
+ return grid
75
+
76
+ if not isinstance(payload_raw, dict):
77
+ summary = Text(
78
+ str(payload_raw)[: const.INVALID_TOOL_CALL_MAX_LENGTH],
79
+ style=ThemeKey.INVALID_TOOL_CALL_ARGS,
80
+ )
81
+ grid.add_row(tool_name_column, summary)
82
+ return grid
83
+
84
+ payload: dict[str, object] = cast(dict[str, object], payload_raw)
85
+
86
+ summary = Text("", ThemeKey.TOOL_PARAM)
87
+ command = payload.get("command")
88
+ timeout_ms = payload.get("timeout_ms")
89
+
90
+ if isinstance(command, str) and command.strip():
91
+ summary.append(command.strip(), style=ThemeKey.TOOL_PARAM)
92
+
93
+ if isinstance(timeout_ms, int):
94
+ if summary:
95
+ summary.append(" ")
96
+ if timeout_ms >= 1000 and timeout_ms % 1000 == 0:
97
+ summary.append(f"{timeout_ms // 1000}s", style=ThemeKey.TOOL_TIMEOUT)
98
+ else:
99
+ summary.append(f"{timeout_ms}ms", style=ThemeKey.TOOL_TIMEOUT)
100
+
101
+ grid.add_row(tool_name_column, summary)
102
+ return grid
103
+
104
+
61
105
  def render_update_plan_tool_call(arguments: str) -> RenderableType:
62
106
  grid = create_grid()
63
107
  tool_name_column = Text.assemble(("◎", ThemeKey.TOOL_MARK), " ", ("Update Plan", ThemeKey.TOOL_NAME))
@@ -239,6 +283,10 @@ def render_todo(tr: events.ToolResultEvent) -> RenderableType:
239
283
  mark = "✔"
240
284
  mark_style = ThemeKey.TODO_NEW_COMPLETED_MARK if is_new_completed else ThemeKey.TODO_COMPLETED_MARK
241
285
  text_style = ThemeKey.TODO_NEW_COMPLETED if is_new_completed else ThemeKey.TODO_COMPLETED
286
+ case _:
287
+ mark = "?"
288
+ mark_style = ThemeKey.TODO_PENDING_MARK
289
+ text_style = ThemeKey.TODO_PENDING
242
290
  text = Text(todo.content)
243
291
  text.stylize(text_style)
244
292
  todo_grid.add_row(Text(mark, style=mark_style), text)
@@ -457,7 +505,7 @@ def render_tool_call(e: events.ToolCallEvent) -> RenderableType | None:
457
505
  case tools.MULTI_EDIT:
458
506
  return render_multi_edit_tool_call(e.arguments)
459
507
  case tools.BASH:
460
- return render_generic_tool_call(e.tool_name, e.arguments, ">")
508
+ return render_bash_tool_call(e.arguments)
461
509
  case tools.APPLY_PATCH:
462
510
  return render_apply_patch_tool_call(e.arguments)
463
511
  case tools.TODO_WRITE:
@@ -4,8 +4,10 @@ from __future__ import annotations
4
4
  import contextlib
5
5
  import io
6
6
  import time
7
+ from collections.abc import Callable
7
8
  from typing import Any, ClassVar
8
9
 
10
+ from rich import box
9
11
  from rich.console import Console, ConsoleOptions, Group, RenderableType, RenderResult
10
12
  from rich.live import Live
11
13
  from rich.markdown import CodeBlock, Heading, Markdown
@@ -32,7 +34,16 @@ class NoInsetCodeBlock(CodeBlock):
32
34
  word_wrap=True,
33
35
  padding=(0, 1),
34
36
  )
35
- yield Panel.fit(syntax, padding=0, border_style="markdown.code.panel")
37
+ yield Panel.fit(syntax, padding=(0, 0), style="markdown.code.block", box=box.SIMPLE)
38
+
39
+
40
+ class ThinkingCodeBlock(CodeBlock):
41
+ """A code block for thinking content that uses grey styling instead of syntax highlighting."""
42
+
43
+ def __rich_console__(self, console: Console, options: ConsoleOptions) -> RenderResult:
44
+ code = str(self.text).rstrip()
45
+ text = Text("```\n" + code + "\n```")
46
+ yield text
36
47
 
37
48
 
38
49
  class LeftHeading(Heading):
@@ -41,15 +52,6 @@ class LeftHeading(Heading):
41
52
  def __rich_console__(self, console: Console, options: ConsoleOptions) -> RenderResult:
42
53
  text = self.text
43
54
  text.justify = "left" # Override justification
44
- # if self.tag == "h1":
45
- # from rich.panel import Panel
46
- # from rich import box
47
- # # Draw a border around h1s, but keep text left-aligned
48
- # yield Panel(
49
- # text,
50
- # box=box.SQUARE,
51
- # style="markdown.h1.border",
52
- # )
53
55
  if self.tag == "h2":
54
56
  text.stylize(Style(bold=True, underline=False))
55
57
  yield Rule(title=text, characters="-", style="markdown.h2.border", align="left")
@@ -68,6 +70,17 @@ class NoInsetMarkdown(Markdown):
68
70
  }
69
71
 
70
72
 
73
+ class ThinkingMarkdown(Markdown):
74
+ """Markdown for thinking content with grey-styled code blocks and left-justified headings."""
75
+
76
+ elements: ClassVar[dict[str, type[Any]]] = {
77
+ **Markdown.elements,
78
+ "fence": ThinkingCodeBlock,
79
+ "code_block": ThinkingCodeBlock,
80
+ "heading_open": LeftHeading,
81
+ }
82
+
83
+
71
84
  class MarkdownStream:
72
85
  """Streaming markdown renderer that progressively displays content with a live updating window.
73
86
 
@@ -84,6 +97,7 @@ class MarkdownStream:
84
97
  spinner: Spinner | None = None,
85
98
  mark: str | None = None,
86
99
  indent: int = 0,
100
+ markdown_class: Callable[..., Markdown] | None = None,
87
101
  ) -> None:
88
102
  """Initialize the markdown stream.
89
103
 
@@ -93,6 +107,7 @@ class MarkdownStream:
93
107
  console (Console, optional): External console to use for rendering
94
108
  mark (str | None, optional): Marker shown before the first non-empty line when indent >= 2
95
109
  indent (int, optional): Number of spaces to indent all rendered lines on the left
110
+ markdown_class: Markdown class to use for rendering (defaults to NoInsetMarkdown)
96
111
  """
97
112
  self.printed: list[str] = [] # Stores lines that have already been printed
98
113
 
@@ -118,6 +133,7 @@ class MarkdownStream:
118
133
  self.spinner: Spinner | None = spinner
119
134
  self.mark: str | None = mark
120
135
  self.indent: int = max(indent, 0)
136
+ self.markdown_class: Callable[..., Markdown] = markdown_class or NoInsetMarkdown
121
137
 
122
138
  @property
123
139
  def _live_started(self) -> bool:
@@ -154,7 +170,7 @@ class MarkdownStream:
154
170
  width=effective_width,
155
171
  )
156
172
 
157
- markdown = NoInsetMarkdown(text, **self.mdargs)
173
+ markdown = self.markdown_class(text, **self.mdargs)
158
174
  temp_console.print(markdown)
159
175
  output = string_io.getvalue()
160
176
 
@@ -14,12 +14,12 @@ class Palette:
14
14
  blue: str
15
15
  orange: str
16
16
  magenta: str
17
- grey_blue: str
18
17
  grey1: str
19
18
  grey2: str
20
19
  grey3: str
21
20
  grey_green: str
22
21
  purple: str
22
+ lavender: str
23
23
  diff_add: str
24
24
  diff_remove: str
25
25
  code_theme: str
@@ -31,19 +31,19 @@ LIGHT_PALETTE = Palette(
31
31
  yellow="yellow",
32
32
  green="spring_green4",
33
33
  cyan="cyan",
34
- blue="#3678b7",
34
+ blue="#3078C5",
35
35
  orange="#d77757",
36
36
  magenta="magenta",
37
- grey_blue="steel_blue",
38
37
  grey1="#667e90",
39
38
  grey2="#93a4b1",
40
39
  grey3="#c4ced4",
41
- grey_green="#96a696",
40
+ grey_green="#96a096",
42
41
  purple="slate_blue3",
42
+ lavender="steel_blue",
43
43
  diff_add="#2e5a32 on #e8f5e9",
44
44
  diff_remove="#5a2e32 on #ffebee",
45
45
  code_theme="ansi_light",
46
- text_background="#f0f0f0",
46
+ text_background="#e0e0e0",
47
47
  )
48
48
 
49
49
  DARK_PALETTE = Palette(
@@ -54,12 +54,12 @@ DARK_PALETTE = Palette(
54
54
  blue="deep_sky_blue1",
55
55
  orange="#e6704e",
56
56
  magenta="magenta",
57
- grey_blue="steel_blue",
58
57
  grey1="#99aabb",
59
58
  grey2="#778899",
60
59
  grey3="#646464",
61
60
  grey_green="#6d8672",
62
61
  purple="#afbafe",
62
+ lavender="#9898b8",
63
63
  diff_add="#c8e6c9 on #2e4a32",
64
64
  diff_remove="#ffcdd2 on #4a2e32",
65
65
  code_theme="ansi_dark",
@@ -107,6 +107,7 @@ class ThemeKey(str, Enum):
107
107
  TOOL_MARK = "tool.mark"
108
108
  TOOL_APPROVED = "tool.approved"
109
109
  TOOL_REJECTED = "tool.rejected"
110
+ TOOL_TIMEOUT = "tool.timeout"
110
111
  # THINKING
111
112
  THINKING = "thinking"
112
113
  THINKING_BOLD = "thinking.bold"
@@ -174,9 +175,9 @@ def get_theme(theme: str | None = None) -> Themes:
174
175
  ThemeKey.USER_INPUT_AT_PATTERN.value: palette.purple,
175
176
  ThemeKey.USER_INPUT_SLASH_COMMAND.value: "bold reverse " + palette.blue,
176
177
  # METADATA
177
- ThemeKey.METADATA.value: palette.grey_blue,
178
- ThemeKey.METADATA_DIM.value: "dim " + palette.grey_blue,
179
- ThemeKey.METADATA_BOLD.value: "bold " + palette.grey_blue,
178
+ ThemeKey.METADATA.value: palette.lavender,
179
+ ThemeKey.METADATA_DIM.value: "dim " + palette.lavender,
180
+ ThemeKey.METADATA_BOLD.value: "bold " + palette.lavender,
180
181
  # SPINNER_STATUS
181
182
  ThemeKey.SPINNER_STATUS.value: palette.blue,
182
183
  ThemeKey.SPINNER_STATUS_TEXT.value: palette.blue,
@@ -196,6 +197,7 @@ def get_theme(theme: str | None = None) -> Themes:
196
197
  ThemeKey.TOOL_MARK.value: "bold",
197
198
  ThemeKey.TOOL_APPROVED.value: palette.green + " bold reverse",
198
199
  ThemeKey.TOOL_REJECTED.value: palette.red + " bold reverse",
200
+ ThemeKey.TOOL_TIMEOUT.value: palette.yellow,
199
201
  # THINKING
200
202
  ThemeKey.THINKING.value: "italic " + palette.grey2,
201
203
  ThemeKey.THINKING_BOLD.value: "bold italic " + palette.grey1,
@@ -232,7 +234,7 @@ def get_theme(theme: str | None = None) -> Themes:
232
234
  markdown_theme=Theme(
233
235
  styles={
234
236
  "markdown.code": palette.purple,
235
- "markdown.code.panel": palette.grey3,
237
+ "markdown.code.block": "on " + palette.text_background,
236
238
  "markdown.h1": "bold reverse",
237
239
  "markdown.h1.border": palette.grey3,
238
240
  "markdown.h2.border": palette.grey3,
@@ -245,8 +247,7 @@ def get_theme(theme: str | None = None) -> Themes:
245
247
  ),
246
248
  thinking_markdown_theme=Theme(
247
249
  styles={
248
- "markdown.code": palette.grey1 + " on " + palette.text_background,
249
- "markdown.code.panel": palette.grey3,
250
+ "markdown.code": palette.grey1 + " italic on " + palette.text_background,
250
251
  "markdown.h1": "bold reverse",
251
252
  "markdown.h1.border": palette.grey3,
252
253
  "markdown.h2.border": palette.grey3,
@@ -265,7 +266,6 @@ def get_theme(theme: str | None = None) -> Themes:
265
266
  Style(color=palette.blue),
266
267
  Style(color=palette.purple),
267
268
  Style(color=palette.orange),
268
- Style(color=palette.grey_blue),
269
269
  Style(color=palette.red),
270
270
  Style(color=palette.grey1),
271
271
  Style(color=palette.yellow),
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: klaude-code
3
- Version: 1.2.13
3
+ Version: 1.2.15
4
4
  Summary: Add your description here
5
5
  Requires-Dist: anthropic>=0.66.0
6
6
  Requires-Dist: openai>=1.102.0