klaude-code 1.2.8__py3-none-any.whl → 1.2.9__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 (52) hide show
  1. klaude_code/auth/codex/__init__.py +1 -1
  2. klaude_code/command/__init__.py +2 -0
  3. klaude_code/command/prompt-deslop.md +14 -0
  4. klaude_code/command/release_notes_cmd.py +86 -0
  5. klaude_code/command/status_cmd.py +92 -54
  6. klaude_code/core/agent.py +13 -19
  7. klaude_code/core/manager/sub_agent_manager.py +5 -1
  8. klaude_code/core/prompt.py +38 -28
  9. klaude_code/core/reminders.py +4 -4
  10. klaude_code/core/task.py +59 -40
  11. klaude_code/core/tool/__init__.py +2 -0
  12. klaude_code/core/tool/file/apply_patch_tool.py +1 -1
  13. klaude_code/core/tool/file/edit_tool.py +1 -1
  14. klaude_code/core/tool/file/multi_edit_tool.py +1 -1
  15. klaude_code/core/tool/file/write_tool.py +1 -1
  16. klaude_code/core/tool/memory/memory_tool.py +2 -2
  17. klaude_code/core/tool/sub_agent_tool.py +2 -1
  18. klaude_code/core/tool/todo/todo_write_tool.py +1 -1
  19. klaude_code/core/tool/todo/update_plan_tool.py +1 -1
  20. klaude_code/core/tool/tool_context.py +21 -4
  21. klaude_code/core/tool/tool_runner.py +5 -8
  22. klaude_code/core/tool/web/mermaid_tool.py +1 -4
  23. klaude_code/core/turn.py +40 -37
  24. klaude_code/llm/anthropic/client.py +13 -44
  25. klaude_code/llm/client.py +1 -1
  26. klaude_code/llm/codex/client.py +4 -3
  27. klaude_code/llm/input_common.py +0 -6
  28. klaude_code/llm/openai_compatible/client.py +28 -72
  29. klaude_code/llm/openai_compatible/input.py +6 -4
  30. klaude_code/llm/openai_compatible/stream_processor.py +82 -0
  31. klaude_code/llm/openrouter/client.py +29 -59
  32. klaude_code/llm/openrouter/input.py +4 -27
  33. klaude_code/llm/responses/client.py +15 -48
  34. klaude_code/llm/usage.py +51 -10
  35. klaude_code/protocol/commands.py +1 -0
  36. klaude_code/protocol/events.py +11 -2
  37. klaude_code/protocol/model.py +142 -24
  38. klaude_code/protocol/sub_agent.py +5 -1
  39. klaude_code/session/export.py +51 -27
  40. klaude_code/session/session.py +28 -16
  41. klaude_code/session/templates/export_session.html +4 -1
  42. klaude_code/ui/modes/repl/__init__.py +1 -5
  43. klaude_code/ui/modes/repl/event_handler.py +153 -54
  44. klaude_code/ui/modes/repl/renderer.py +4 -4
  45. klaude_code/ui/renderers/developer.py +35 -25
  46. klaude_code/ui/renderers/metadata.py +68 -30
  47. klaude_code/ui/renderers/tools.py +53 -87
  48. klaude_code/ui/rich/markdown.py +5 -5
  49. {klaude_code-1.2.8.dist-info → klaude_code-1.2.9.dist-info}/METADATA +1 -1
  50. {klaude_code-1.2.8.dist-info → klaude_code-1.2.9.dist-info}/RECORD +52 -49
  51. {klaude_code-1.2.8.dist-info → klaude_code-1.2.9.dist-info}/WHEEL +0 -0
  52. {klaude_code-1.2.8.dist-info → klaude_code-1.2.9.dist-info}/entry_points.txt +0 -0
@@ -7,6 +7,7 @@ from klaude_code.protocol import commands, events, model
7
7
  from klaude_code.ui.renderers import diffs as r_diffs
8
8
  from klaude_code.ui.renderers.common import create_grid
9
9
  from klaude_code.ui.renderers.tools import render_path
10
+ from klaude_code.ui.rich.markdown import NoInsetMarkdown
10
11
  from klaude_code.ui.rich.theme import ThemeKey
11
12
  from klaude_code.ui.utils.common import truncate_display
12
13
 
@@ -100,6 +101,8 @@ def render_command_output(e: events.DeveloperMessageEvent) -> RenderableType:
100
101
  return Padding.indent(Text.from_markup(e.item.content or ""), level=2)
101
102
  case commands.CommandName.STATUS:
102
103
  return _render_status_output(e.item.command_output)
104
+ case commands.CommandName.RELEASE_NOTES:
105
+ return Padding.indent(NoInsetMarkdown(e.item.content or ""), level=2)
103
106
  case _:
104
107
  content = e.item.content or "(no content)"
105
108
  style = ThemeKey.TOOL_RESULT if not e.item.command_output.is_error else ThemeKey.ERROR
@@ -126,34 +129,41 @@ def _format_cost(cost: float | None, currency: str = "USD") -> str:
126
129
 
127
130
 
128
131
  def _render_status_output(command_output: model.CommandOutput) -> RenderableType:
129
- """Render session status as a two-column table with sections."""
130
- if not command_output.ui_extra or not command_output.ui_extra.session_status:
131
- return Text("(no status data)", style=ThemeKey.TOOL_RESULT)
132
+ """Render session status with total cost and per-model breakdown."""
133
+ if not isinstance(command_output.ui_extra, model.SessionStatusUIExtra):
134
+ return Text("(no status data)", style=ThemeKey.METADATA)
132
135
 
133
- status = command_output.ui_extra.session_status
136
+ status = command_output.ui_extra
134
137
  usage = status.usage
135
138
 
136
139
  table = Table.grid(padding=(0, 2))
137
- table.add_column(style=ThemeKey.TOOL_RESULT, no_wrap=True)
138
- table.add_column(style=ThemeKey.TOOL_RESULT, no_wrap=True)
139
- # Token Usage section
140
- table.add_row(Text("Token Usage", style="bold"), "")
141
- table.add_row("Input Tokens", _format_tokens(usage.input_tokens))
142
- if usage.cached_tokens > 0:
143
- table.add_row("Cached Tokens", _format_tokens(usage.cached_tokens))
144
- if usage.reasoning_tokens > 0:
145
- table.add_row("Reasoning Tokens", _format_tokens(usage.reasoning_tokens))
146
- table.add_row("Output Tokens", _format_tokens(usage.output_tokens))
147
- table.add_row("Total Tokens", _format_tokens(usage.total_tokens))
148
-
149
- # Cost section
150
- if usage.total_cost is not None:
151
- table.add_row("", "") # Empty line
152
- table.add_row(Text("Cost", style="bold"), "")
153
- table.add_row("Input Cost", _format_cost(usage.input_cost, usage.currency))
154
- if usage.cache_read_cost is not None and usage.cache_read_cost > 0:
155
- table.add_row("Cache Read Cost", _format_cost(usage.cache_read_cost, usage.currency))
156
- table.add_row("Output Cost", _format_cost(usage.output_cost, usage.currency))
157
- table.add_row("Total Cost", _format_cost(usage.total_cost, usage.currency))
140
+ table.add_column(style=ThemeKey.METADATA, overflow="fold")
141
+ table.add_column(style=ThemeKey.METADATA, overflow="fold")
142
+
143
+ # Total cost line
144
+ table.add_row(
145
+ Text("Total cost:", style=ThemeKey.METADATA_BOLD),
146
+ Text(_format_cost(usage.total_cost, usage.currency), style=ThemeKey.METADATA_BOLD),
147
+ )
148
+
149
+ # Per-model breakdown
150
+ if status.by_model:
151
+ table.add_row(Text("Usage by model:", style=ThemeKey.METADATA_BOLD), "")
152
+ for meta in status.by_model:
153
+ model_label = meta.model_name
154
+ if meta.provider:
155
+ model_label = f"{meta.model_name} ({meta.provider.lower().replace(' ', '-')})"
156
+
157
+ if meta.usage:
158
+ usage_detail = (
159
+ f"{_format_tokens(meta.usage.input_tokens)} input, "
160
+ f"{_format_tokens(meta.usage.output_tokens)} output, "
161
+ f"{_format_tokens(meta.usage.cached_tokens)} cache read, "
162
+ f"{_format_tokens(meta.usage.reasoning_tokens)} thinking, "
163
+ f"({_format_cost(meta.usage.total_cost, meta.usage.currency)})"
164
+ )
165
+ else:
166
+ usage_detail = "(no usage data)"
167
+ table.add_row(f"{model_label}:", usage_detail)
158
168
 
159
169
  return Padding.indent(table, level=2)
@@ -7,7 +7,7 @@ from rich.padding import Padding
7
7
  from rich.panel import Panel
8
8
  from rich.text import Text
9
9
 
10
- from klaude_code.protocol import events
10
+ from klaude_code.protocol import events, model
11
11
  from klaude_code.trace import is_debug_enabled
12
12
  from klaude_code.ui.rich.theme import ThemeKey
13
13
  from klaude_code.ui.utils.common import format_number
@@ -21,18 +21,34 @@ def _get_version() -> str:
21
21
  return "unknown"
22
22
 
23
23
 
24
- def render_response_metadata(e: events.ResponseMetadataEvent) -> RenderableType:
25
- metadata = e.metadata
24
+ def _render_task_metadata_block(
25
+ metadata: model.TaskMetadata,
26
+ *,
27
+ indent: int = 0,
28
+ show_context_and_time: bool = True,
29
+ ) -> list[RenderableType]:
30
+ """Render a single TaskMetadata block.
26
31
 
32
+ Args:
33
+ metadata: The TaskMetadata to render.
34
+ indent: Number of spaces to indent (0 for main, 2 for sub-agents).
35
+ show_context_and_time: Whether to show context usage percent and time.
36
+
37
+ Returns:
38
+ List of renderables for this metadata block.
39
+ """
27
40
  # Get currency symbol
28
41
  currency = metadata.usage.currency if metadata.usage else "USD"
29
42
  currency_symbol = "¥" if currency == "CNY" else "$"
30
43
 
31
44
  # Line 1: Model and Provider
32
- model_text = Text()
33
- model_text.append_text(Text("- ", style=ThemeKey.METADATA_BOLD)).append_text(
34
- Text(metadata.model_name, style=ThemeKey.METADATA_BOLD)
45
+ prefix = (
46
+ Text(" " * indent + "• ", style=ThemeKey.METADATA_BOLD)
47
+ if indent == 0
48
+ else Text(" " * indent + "└ ", style=ThemeKey.METADATA_DIM)
35
49
  )
50
+ model_text = Text()
51
+ model_text.append_text(prefix).append_text(Text(metadata.model_name, style=ThemeKey.METADATA_BOLD))
36
52
  if metadata.provider is not None:
37
53
  model_text.append_text(Text("@", style=ThemeKey.METADATA)).append_text(
38
54
  Text(metadata.provider.lower().replace(" ", "-"), style=ThemeKey.METADATA)
@@ -41,7 +57,7 @@ def render_response_metadata(e: events.ResponseMetadataEvent) -> RenderableType:
41
57
  renderables: list[RenderableType] = [model_text]
42
58
 
43
59
  # Line 2: Token consumption, Context, TPS, Cost
44
- parts: list[Text] = []
60
+ parts2: list[Text] = []
45
61
 
46
62
  if metadata.usage is not None:
47
63
  # Input
@@ -51,7 +67,7 @@ def render_response_metadata(e: events.ResponseMetadataEvent) -> RenderableType:
51
67
  ]
52
68
  if metadata.usage.input_cost is not None:
53
69
  input_parts.append((f"({currency_symbol}{metadata.usage.input_cost:.4f})", ThemeKey.METADATA_DIM))
54
- parts.append(Text.assemble(*input_parts))
70
+ parts2.append(Text.assemble(*input_parts))
55
71
 
56
72
  # Cached
57
73
  if metadata.usage.cached_tokens > 0:
@@ -61,7 +77,7 @@ def render_response_metadata(e: events.ResponseMetadataEvent) -> RenderableType:
61
77
  ]
62
78
  if metadata.usage.cache_read_cost is not None:
63
79
  cached_parts.append((f"({currency_symbol}{metadata.usage.cache_read_cost:.4f})", ThemeKey.METADATA_DIM))
64
- parts.append(Text.assemble(*cached_parts))
80
+ parts2.append(Text.assemble(*cached_parts))
65
81
 
66
82
  # Output
67
83
  output_parts: list[tuple[str, str]] = [
@@ -70,11 +86,11 @@ def render_response_metadata(e: events.ResponseMetadataEvent) -> RenderableType:
70
86
  ]
71
87
  if metadata.usage.output_cost is not None:
72
88
  output_parts.append((f"({currency_symbol}{metadata.usage.output_cost:.4f})", ThemeKey.METADATA_DIM))
73
- parts.append(Text.assemble(*output_parts))
89
+ parts2.append(Text.assemble(*output_parts))
74
90
 
75
91
  # Reasoning
76
92
  if metadata.usage.reasoning_tokens > 0:
77
- parts.append(
93
+ parts2.append(
78
94
  Text.assemble(
79
95
  ("thinking", ThemeKey.METADATA_DIM),
80
96
  (":", ThemeKey.METADATA_DIM),
@@ -85,14 +101,30 @@ def render_response_metadata(e: events.ResponseMetadataEvent) -> RenderableType:
85
101
  )
86
102
  )
87
103
 
88
- # Context
89
- if metadata.usage.context_usage_percent is not None:
90
- parts.append(
104
+ # Cost
105
+ if metadata.usage is not None and metadata.usage.total_cost is not None:
106
+ parts2.append(
107
+ Text.assemble(
108
+ ("cost", ThemeKey.METADATA_DIM),
109
+ (":", ThemeKey.METADATA_DIM),
110
+ (f"{currency_symbol}{metadata.usage.total_cost:.4f}", ThemeKey.METADATA_DIM),
111
+ )
112
+ )
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
+ if metadata.usage is not None:
119
+ # Context (only for main agent)
120
+ if show_context_and_time and metadata.usage.context_usage_percent is not None:
121
+ context_size = format_number(metadata.usage.context_window_size or 0)
122
+ parts3.append(
91
123
  Text.assemble(
92
124
  ("context", ThemeKey.METADATA_DIM),
93
125
  (":", ThemeKey.METADATA_DIM),
94
126
  (
95
- f"{metadata.usage.context_usage_percent:.1f}%",
127
+ f"{context_size}({metadata.usage.context_usage_percent:.1f}%)",
96
128
  ThemeKey.METADATA_DIM,
97
129
  ),
98
130
  )
@@ -100,7 +132,7 @@ def render_response_metadata(e: events.ResponseMetadataEvent) -> RenderableType:
100
132
 
101
133
  # TPS
102
134
  if metadata.usage.throughput_tps is not None:
103
- parts.append(
135
+ parts3.append(
104
136
  Text.assemble(
105
137
  ("tps", ThemeKey.METADATA_DIM),
106
138
  (":", ThemeKey.METADATA_DIM),
@@ -109,8 +141,8 @@ def render_response_metadata(e: events.ResponseMetadataEvent) -> RenderableType:
109
141
  )
110
142
 
111
143
  # Duration
112
- if metadata.task_duration_s is not None:
113
- parts.append(
144
+ if show_context_and_time and metadata.task_duration_s is not None:
145
+ parts3.append(
114
146
  Text.assemble(
115
147
  ("time", ThemeKey.METADATA_DIM),
116
148
  (":", ThemeKey.METADATA_DIM),
@@ -118,19 +150,25 @@ def render_response_metadata(e: events.ResponseMetadataEvent) -> RenderableType:
118
150
  )
119
151
  )
120
152
 
121
- # Cost
122
- if metadata.usage is not None and metadata.usage.total_cost is not None:
123
- parts.append(
124
- Text.assemble(
125
- ("cost", ThemeKey.METADATA_DIM),
126
- (":", ThemeKey.METADATA_DIM),
127
- (f"{currency_symbol}{metadata.usage.total_cost:.4f}", ThemeKey.METADATA_DIM),
128
- )
129
- )
153
+ if parts3:
154
+ line2 = Text(" / ", style=ThemeKey.METADATA_DIM).join(parts3)
155
+ renderables.append(Padding(line2, (0, 0, 0, indent + 2)))
156
+
157
+ return renderables
158
+
159
+
160
+ def render_task_metadata(e: events.TaskMetadataEvent) -> RenderableType:
161
+ """Render task metadata including main agent and sub-agents, aggregated by model+provider."""
162
+ renderables: list[RenderableType] = []
163
+
164
+ renderables.extend(_render_task_metadata_block(e.metadata.main, indent=0, show_context_and_time=True))
165
+
166
+ # Aggregate by (model_name, provider), sorted by total_cost descending
167
+ sorted_items = model.TaskMetadata.aggregate_by_model(e.metadata.sub_agent_task_metadata)
130
168
 
131
- if parts:
132
- line2 = Text("/", style=ThemeKey.METADATA_DIM).join(parts)
133
- renderables.append(Padding(line2, (0, 0, 0, 2)))
169
+ # Render each aggregated model block
170
+ for meta in sorted_items:
171
+ renderables.extend(_render_task_metadata_block(meta, indent=2, show_context_and_time=False))
134
172
 
135
173
  return Group(*renderables)
136
174
 
@@ -121,32 +121,24 @@ def render_read_tool_call(arguments: str) -> RenderableType:
121
121
  return grid
122
122
 
123
123
 
124
- def render_edit_tool_call(arguments: str) -> Text:
125
- render_result: Text = Text.assemble(("→ ", ThemeKey.TOOL_MARK))
124
+ def render_edit_tool_call(arguments: str) -> RenderableType:
125
+ grid = create_grid()
126
+ tool_name_column = Text.assemble(("→", ThemeKey.TOOL_MARK), " ", ("Edit", ThemeKey.TOOL_NAME))
126
127
  try:
127
128
  json_dict = json.loads(arguments)
128
129
  file_path = json_dict.get("file_path")
129
- render_result = (
130
- render_result.append_text(Text("Edit", ThemeKey.TOOL_NAME))
131
- .append_text(Text(" "))
132
- .append_text(render_path(file_path, ThemeKey.TOOL_PARAM_FILE_PATH))
133
- )
130
+ arguments_column = render_path(file_path, ThemeKey.TOOL_PARAM_FILE_PATH)
134
131
  except json.JSONDecodeError:
135
- render_result = (
136
- render_result.append_text(Text("Edit", ThemeKey.TOOL_NAME))
137
- .append_text(Text(" "))
138
- .append_text(
139
- Text(
140
- arguments.strip()[: const.INVALID_TOOL_CALL_MAX_LENGTH],
141
- style=ThemeKey.INVALID_TOOL_CALL_ARGS,
142
- )
143
- )
132
+ arguments_column = Text(
133
+ arguments.strip()[: const.INVALID_TOOL_CALL_MAX_LENGTH],
134
+ style=ThemeKey.INVALID_TOOL_CALL_ARGS,
144
135
  )
145
- return render_result
136
+ grid.add_row(tool_name_column, arguments_column)
137
+ return grid
146
138
 
147
139
 
148
- def render_write_tool_call(arguments: str) -> Text:
149
- render_result: Text = Text.assemble(("→ ", ThemeKey.TOOL_MARK))
140
+ def render_write_tool_call(arguments: str) -> RenderableType:
141
+ grid = create_grid()
150
142
  try:
151
143
  json_dict = json.loads(arguments)
152
144
  file_path = json_dict.get("file_path")
@@ -157,97 +149,77 @@ def render_write_tool_call(arguments: str) -> Text:
157
149
  abs_path = (Path().cwd() / abs_path).resolve()
158
150
  if abs_path.exists():
159
151
  op_label = "Overwrite"
160
- render_result = (
161
- render_result.append_text(Text(op_label, ThemeKey.TOOL_NAME))
162
- .append_text(Text(" "))
163
- .append_text(render_path(file_path, ThemeKey.TOOL_PARAM_FILE_PATH))
164
- )
152
+ tool_name_column = Text.assemble(("→", ThemeKey.TOOL_MARK), " ", (op_label, ThemeKey.TOOL_NAME))
153
+ arguments_column = render_path(file_path, ThemeKey.TOOL_PARAM_FILE_PATH)
165
154
  except json.JSONDecodeError:
166
- render_result = (
167
- render_result.append_text(Text("Write", ThemeKey.TOOL_NAME))
168
- .append_text(Text(" "))
169
- .append_text(
170
- Text(
171
- arguments.strip()[: const.INVALID_TOOL_CALL_MAX_LENGTH],
172
- style=ThemeKey.INVALID_TOOL_CALL_ARGS,
173
- )
174
- )
155
+ tool_name_column = Text.assemble(("→", ThemeKey.TOOL_MARK), " ", ("Write", ThemeKey.TOOL_NAME))
156
+ arguments_column = Text(
157
+ arguments.strip()[: const.INVALID_TOOL_CALL_MAX_LENGTH],
158
+ style=ThemeKey.INVALID_TOOL_CALL_ARGS,
175
159
  )
176
- return render_result
160
+ grid.add_row(tool_name_column, arguments_column)
161
+ return grid
177
162
 
178
163
 
179
- def render_multi_edit_tool_call(arguments: str) -> Text:
180
- render_result: Text = Text.assemble(("→ ", ThemeKey.TOOL_MARK), ("MultiEdit", ThemeKey.TOOL_NAME), " ")
164
+ def render_multi_edit_tool_call(arguments: str) -> RenderableType:
165
+ grid = create_grid()
166
+ tool_name_column = Text.assemble(("→", ThemeKey.TOOL_MARK), " ", ("MultiEdit", ThemeKey.TOOL_NAME))
181
167
  try:
182
168
  json_dict = json.loads(arguments)
183
169
  file_path = json_dict.get("file_path")
184
170
  edits = json_dict.get("edits", [])
185
- render_result = (
186
- render_result.append_text(render_path(file_path, ThemeKey.TOOL_PARAM_FILE_PATH))
187
- .append_text(Text(" - "))
188
- .append_text(Text(f"{len(edits)}", ThemeKey.TOOL_PARAM_BOLD))
189
- .append_text(Text(" updates", ThemeKey.TOOL_PARAM_FILE_PATH))
171
+ arguments_column = Text.assemble(
172
+ render_path(file_path, ThemeKey.TOOL_PARAM_FILE_PATH),
173
+ Text(" - "),
174
+ Text(f"{len(edits)}", ThemeKey.TOOL_PARAM_BOLD),
175
+ Text(" updates", ThemeKey.TOOL_PARAM_FILE_PATH),
190
176
  )
191
177
  except json.JSONDecodeError:
192
- render_result = render_result.append_text(
193
- Text(
194
- arguments.strip()[: const.INVALID_TOOL_CALL_MAX_LENGTH],
195
- style=ThemeKey.INVALID_TOOL_CALL_ARGS,
196
- )
178
+ arguments_column = Text(
179
+ arguments.strip()[: const.INVALID_TOOL_CALL_MAX_LENGTH],
180
+ style=ThemeKey.INVALID_TOOL_CALL_ARGS,
197
181
  )
198
- return render_result
182
+ grid.add_row(tool_name_column, arguments_column)
183
+ return grid
199
184
 
200
185
 
201
186
  def render_apply_patch_tool_call(arguments: str) -> RenderableType:
187
+ grid = create_grid()
188
+ tool_name_column = Text.assemble(("→", ThemeKey.TOOL_MARK), " ", ("Apply Patch", ThemeKey.TOOL_NAME))
189
+
202
190
  try:
203
191
  payload = json.loads(arguments)
204
192
  except json.JSONDecodeError:
205
- return Text.assemble(
206
- ("→ ", ThemeKey.TOOL_MARK),
207
- ("Apply Patch", ThemeKey.TOOL_NAME),
208
- " ",
209
- Text(
210
- arguments.strip()[: const.INVALID_TOOL_CALL_MAX_LENGTH],
211
- style=ThemeKey.INVALID_TOOL_CALL_ARGS,
212
- ),
193
+ arguments_column = Text(
194
+ arguments.strip()[: const.INVALID_TOOL_CALL_MAX_LENGTH],
195
+ style=ThemeKey.INVALID_TOOL_CALL_ARGS,
213
196
  )
197
+ grid.add_row(tool_name_column, arguments_column)
198
+ return grid
214
199
 
215
200
  patch_content = payload.get("patch", "")
216
-
217
- grid = create_grid()
218
- header = Text.assemble(("→ ", ThemeKey.TOOL_MARK), ("Apply Patch", ThemeKey.TOOL_NAME))
219
- summary = Text("", ThemeKey.TOOL_PARAM)
201
+ arguments_column = Text("", ThemeKey.TOOL_PARAM)
220
202
 
221
203
  if isinstance(patch_content, str):
222
204
  lines = [line for line in patch_content.splitlines() if line and not line.startswith("*** Begin Patch")]
223
205
  if lines:
224
- summary = Text(lines[0][: const.INVALID_TOOL_CALL_MAX_LENGTH], ThemeKey.TOOL_PARAM)
206
+ arguments_column = Text(lines[0][: const.INVALID_TOOL_CALL_MAX_LENGTH], ThemeKey.TOOL_PARAM)
225
207
  else:
226
- summary = Text(
208
+ arguments_column = Text(
227
209
  str(patch_content)[: const.INVALID_TOOL_CALL_MAX_LENGTH],
228
210
  ThemeKey.INVALID_TOOL_CALL_ARGS,
229
211
  )
230
212
 
231
- if summary.plain:
232
- grid.add_row(header, summary)
233
- else:
234
- grid.add_row(header, Text("", ThemeKey.TOOL_PARAM))
235
-
213
+ grid.add_row(tool_name_column, arguments_column)
236
214
  return grid
237
215
 
238
216
 
239
217
  def render_todo(tr: events.ToolResultEvent) -> RenderableType:
240
- if tr.ui_extra is None:
241
- return Text.assemble(
242
- (" ✘", ThemeKey.ERROR_BOLD),
243
- " ",
244
- Text("(no content)", style=ThemeKey.ERROR),
245
- )
246
- if tr.ui_extra.type != model.ToolResultUIExtraType.TODO_LIST or tr.ui_extra.todo_list is None:
218
+ if not isinstance(tr.ui_extra, model.TodoListUIExtra):
247
219
  return Text.assemble(
248
220
  (" ✘", ThemeKey.ERROR_BOLD),
249
221
  " ",
250
- Text("(invalid ui_extra)", style=ThemeKey.ERROR),
222
+ Text("(no content)" if tr.ui_extra is None else "(invalid ui_extra)", style=ThemeKey.ERROR),
251
223
  )
252
224
 
253
225
  ui_extra = tr.ui_extra.todo_list
@@ -283,11 +255,9 @@ def render_generic_tool_result(result: str, *, is_error: bool = False) -> Render
283
255
  def _extract_mermaid_link(
284
256
  ui_extra: model.ToolResultUIExtra | None,
285
257
  ) -> model.MermaidLinkUIExtra | None:
286
- if ui_extra is None:
287
- return None
288
- if ui_extra.type != model.ToolResultUIExtraType.MERMAID_LINK:
289
- return None
290
- return ui_extra.mermaid_link
258
+ if isinstance(ui_extra, model.MermaidLinkUIExtra):
259
+ return ui_extra
260
+ return None
291
261
 
292
262
 
293
263
  def render_memory_tool_call(arguments: str) -> RenderableType:
@@ -380,11 +350,9 @@ def render_mermaid_tool_result(tr: events.ToolResultEvent) -> RenderableType:
380
350
  def _extract_truncation(
381
351
  ui_extra: model.ToolResultUIExtra | None,
382
352
  ) -> model.TruncationUIExtra | None:
383
- if ui_extra is None:
384
- return None
385
- if ui_extra.type != model.ToolResultUIExtraType.TRUNCATION:
386
- return None
387
- return ui_extra.truncation
353
+ if isinstance(ui_extra, model.TruncationUIExtra):
354
+ return ui_extra
355
+ return None
388
356
 
389
357
 
390
358
  def render_truncation_info(ui_extra: model.TruncationUIExtra) -> RenderableType:
@@ -496,9 +464,7 @@ def render_tool_call(e: events.ToolCallEvent) -> RenderableType | None:
496
464
 
497
465
 
498
466
  def _extract_diff_text(ui_extra: model.ToolResultUIExtra | None) -> str | None:
499
- if ui_extra is None:
500
- return None
501
- if ui_extra.type == model.ToolResultUIExtraType.DIFF_TEXT:
467
+ if isinstance(ui_extra, model.DiffTextUIExtra):
502
468
  return ui_extra.diff_text
503
469
  return None
504
470
 
@@ -102,7 +102,6 @@ class MarkdownStream:
102
102
 
103
103
  # Defer Live creation until the first update.
104
104
  self.live: Live | None = None
105
- self._live_started: bool = False
106
105
 
107
106
  # Streaming control
108
107
  self.when: float = 0.0 # Timestamp of last update
@@ -119,9 +118,10 @@ class MarkdownStream:
119
118
  self.mark: str | None = mark
120
119
  self.indent: int = max(indent, 0)
121
120
 
122
- # Defer Live creation until the first update
123
- self.live: Live | None = None
124
- self._live_started: bool = False
121
+ @property
122
+ def _live_started(self) -> bool:
123
+ """Check if Live display has been started (derived from self.live)."""
124
+ return self.live is not None
125
125
 
126
126
  def _render_markdown_to_lines(self, text: str) -> list[str]:
127
127
  """Render markdown text to a list of lines.
@@ -214,7 +214,7 @@ class MarkdownStream:
214
214
  console=self.console,
215
215
  )
216
216
  self.live.start()
217
- self._live_started = True
217
+ # Note: self._live_started is now a property derived from self.live
218
218
 
219
219
  # If live rendering isn't available (e.g., after a final update), stop.
220
220
  if self.live is None:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: klaude-code
3
- Version: 1.2.8
3
+ Version: 1.2.9
4
4
  Summary: Add your description here
5
5
  Requires-Dist: anthropic>=0.66.0
6
6
  Requires-Dist: openai>=1.102.0