klaude-code 1.2.25__py3-none-any.whl → 1.2.27__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 (42) hide show
  1. klaude_code/cli/config_cmd.py +1 -5
  2. klaude_code/cli/list_model.py +170 -129
  3. klaude_code/cli/main.py +37 -5
  4. klaude_code/cli/runtime.py +4 -6
  5. klaude_code/cli/self_update.py +2 -1
  6. klaude_code/cli/session_cmd.py +1 -1
  7. klaude_code/config/__init__.py +3 -1
  8. klaude_code/config/assets/__init__.py +1 -0
  9. klaude_code/config/assets/builtin_config.yaml +233 -0
  10. klaude_code/config/builtin_config.py +37 -0
  11. klaude_code/config/config.py +332 -112
  12. klaude_code/config/select_model.py +45 -8
  13. klaude_code/const.py +5 -1
  14. klaude_code/core/executor.py +4 -2
  15. klaude_code/core/manager/llm_clients_builder.py +4 -1
  16. klaude_code/core/tool/file/apply_patch_tool.py +26 -3
  17. klaude_code/core/tool/file/edit_tool.py +4 -4
  18. klaude_code/core/tool/file/write_tool.py +4 -4
  19. klaude_code/core/tool/shell/bash_tool.py +2 -2
  20. klaude_code/llm/openai_compatible/stream.py +2 -1
  21. klaude_code/protocol/model.py +24 -1
  22. klaude_code/session/export.py +1 -1
  23. klaude_code/session/selector.py +2 -2
  24. klaude_code/session/session.py +4 -4
  25. klaude_code/ui/modes/repl/completers.py +4 -4
  26. klaude_code/ui/modes/repl/event_handler.py +23 -4
  27. klaude_code/ui/modes/repl/input_prompt_toolkit.py +4 -4
  28. klaude_code/ui/modes/repl/key_bindings.py +4 -4
  29. klaude_code/ui/modes/repl/renderer.py +22 -17
  30. klaude_code/ui/renderers/diffs.py +1 -1
  31. klaude_code/ui/renderers/metadata.py +2 -2
  32. klaude_code/ui/renderers/sub_agent.py +14 -12
  33. klaude_code/ui/renderers/thinking.py +1 -1
  34. klaude_code/ui/renderers/tools.py +27 -3
  35. klaude_code/ui/rich/markdown.py +35 -15
  36. klaude_code/ui/rich/theme.py +2 -5
  37. klaude_code/ui/terminal/color.py +1 -1
  38. klaude_code/ui/terminal/control.py +4 -4
  39. {klaude_code-1.2.25.dist-info → klaude_code-1.2.27.dist-info}/METADATA +121 -127
  40. {klaude_code-1.2.25.dist-info → klaude_code-1.2.27.dist-info}/RECORD +42 -39
  41. {klaude_code-1.2.25.dist-info → klaude_code-1.2.27.dist-info}/WHEEL +0 -0
  42. {klaude_code-1.2.25.dist-info → klaude_code-1.2.27.dist-info}/entry_points.txt +0 -0
@@ -20,7 +20,7 @@ class ApplyPatchHandler:
20
20
  @classmethod
21
21
  async def handle_apply_patch(cls, patch_text: str) -> model.ToolResultItem:
22
22
  try:
23
- output, diff_ui = await asyncio.to_thread(cls._apply_patch_in_thread, patch_text)
23
+ output, ui_extra = await asyncio.to_thread(cls._apply_patch_in_thread, patch_text)
24
24
  except apply_patch_module.DiffError as error:
25
25
  return model.ToolResultItem(status="error", output=str(error))
26
26
  except Exception as error: # pragma: no cover # unexpected errors bubbled to tool result
@@ -28,11 +28,11 @@ class ApplyPatchHandler:
28
28
  return model.ToolResultItem(
29
29
  status="success",
30
30
  output=output,
31
- ui_extra=diff_ui,
31
+ ui_extra=ui_extra,
32
32
  )
33
33
 
34
34
  @staticmethod
35
- def _apply_patch_in_thread(patch_text: str) -> tuple[str, model.DiffUIExtra]:
35
+ def _apply_patch_in_thread(patch_text: str) -> tuple[str, model.ToolResultUIExtra]:
36
36
  ap = apply_patch_module
37
37
  normalized_start = patch_text.lstrip()
38
38
  if not normalized_start.startswith("*** Begin Patch"):
@@ -69,6 +69,16 @@ class ApplyPatchHandler:
69
69
  commit = ap.patch_to_commit(patch, orig)
70
70
  diff_ui = ApplyPatchHandler._commit_to_structured_diff(commit)
71
71
 
72
+ md_items: list[model.MarkdownDocUIExtra] = []
73
+ for change_path, change in commit.changes.items():
74
+ if change.type == apply_patch_module.ActionType.ADD and change_path.endswith(".md"):
75
+ md_items.append(
76
+ model.MarkdownDocUIExtra(
77
+ file_path=resolve_path(change_path),
78
+ content=change.new_content or "",
79
+ )
80
+ )
81
+
72
82
  def write_fn(path: str, content: str) -> None:
73
83
  resolved = resolve_path(path)
74
84
  if os.path.isdir(resolved):
@@ -102,6 +112,16 @@ class ApplyPatchHandler:
102
112
  file_tracker.pop(resolved, None)
103
113
 
104
114
  ap.apply_commit(commit, write_fn, remove_fn)
115
+
116
+ # apply_patch can include multiple operations. If we added markdown files,
117
+ # return a MultiUIExtra so UI can render markdown previews (without showing a diff for those markdown adds).
118
+ if md_items:
119
+ items: list[model.MultiUIExtraItem] = []
120
+ items.extend(md_items)
121
+ if diff_ui.files:
122
+ items.append(diff_ui)
123
+ return "Done!", model.MultiUIExtra(items=items)
124
+
105
125
  return "Done!", diff_ui
106
126
 
107
127
  @staticmethod
@@ -110,6 +130,9 @@ class ApplyPatchHandler:
110
130
  for path in sorted(commit.changes):
111
131
  change = commit.changes[path]
112
132
  if change.type == apply_patch_module.ActionType.ADD:
133
+ # For markdown files created via Add File, we render content via MarkdownDocUIExtra instead of a diff.
134
+ if path.endswith(".md"):
135
+ continue
113
136
  files.append(build_structured_file_diff("", change.new_content or "", file_path=path))
114
137
  elif change.type == apply_patch_module.ActionType.DELETE:
115
138
  files.append(build_structured_file_diff(change.old_content or "", "", file_path=path))
@@ -88,7 +88,7 @@ class EditTool(ToolABC):
88
88
  async def call(cls, arguments: str) -> model.ToolResultItem:
89
89
  try:
90
90
  args = EditTool.EditArguments.model_validate_json(arguments)
91
- except Exception as e: # pragma: no cover - defensive
91
+ except ValueError as e: # pragma: no cover - defensive
92
92
  return model.ToolResultItem(status="error", output=f"Invalid arguments: {e}")
93
93
 
94
94
  file_path = os.path.abspath(args.file_path)
@@ -150,7 +150,7 @@ class EditTool(ToolABC):
150
150
  # Backward-compat: old sessions only stored mtime.
151
151
  try:
152
152
  current_mtime = Path(file_path).stat().st_mtime
153
- except Exception:
153
+ except OSError:
154
154
  current_mtime = tracked_status.mtime
155
155
  if current_mtime != tracked_status.mtime:
156
156
  return model.ToolResultItem(
@@ -188,7 +188,7 @@ class EditTool(ToolABC):
188
188
  # Write back
189
189
  try:
190
190
  await asyncio.to_thread(write_text, file_path, after)
191
- except Exception as e: # pragma: no cover
191
+ except (OSError, UnicodeError) as e: # pragma: no cover
192
192
  return model.ToolResultItem(status="error", output=f"<tool_use_error>{e}</tool_use_error>")
193
193
 
194
194
  # Prepare UI extra: unified diff with 3 context lines
@@ -233,7 +233,7 @@ class EditTool(ToolABC):
233
233
  plus_range = plus.split(" ")[0]
234
234
  start = int(plus_range.split(",")[0]) if "," in plus_range else int(plus_range)
235
235
  after_line_no = start - 1
236
- except Exception:
236
+ except (ValueError, IndexError):
237
237
  after_line_no = 0
238
238
  continue
239
239
  if line.startswith(" ") or (line.startswith("+") and not line.startswith("+++ ")):
@@ -49,7 +49,7 @@ class WriteTool(ToolABC):
49
49
  async def call(cls, arguments: str) -> model.ToolResultItem:
50
50
  try:
51
51
  args = WriteArguments.model_validate_json(arguments)
52
- except Exception as e: # pragma: no cover - defensive
52
+ except ValueError as e: # pragma: no cover - defensive
53
53
  return model.ToolResultItem(status="error", output=f"Invalid arguments: {e}")
54
54
 
55
55
  file_path = os.path.abspath(args.file_path)
@@ -79,7 +79,7 @@ class WriteTool(ToolABC):
79
79
  try:
80
80
  before = await asyncio.to_thread(read_text, file_path)
81
81
  before_read_ok = True
82
- except Exception:
82
+ except OSError:
83
83
  before = ""
84
84
  before_read_ok = False
85
85
 
@@ -98,7 +98,7 @@ class WriteTool(ToolABC):
98
98
  # Backward-compat: old sessions only stored mtime, or we couldn't hash.
99
99
  try:
100
100
  current_mtime = Path(file_path).stat().st_mtime
101
- except Exception:
101
+ except OSError:
102
102
  current_mtime = tracked_status.mtime
103
103
  if current_mtime != tracked_status.mtime:
104
104
  return model.ToolResultItem(
@@ -111,7 +111,7 @@ class WriteTool(ToolABC):
111
111
 
112
112
  try:
113
113
  await asyncio.to_thread(write_text, file_path, args.content)
114
- except Exception as e: # pragma: no cover
114
+ except (OSError, UnicodeError) as e: # pragma: no cover
115
115
  return model.ToolResultItem(status="error", output=f"<tool_use_error>{e}</tool_use_error>")
116
116
 
117
117
  if file_tracker is not None:
@@ -274,7 +274,7 @@ class BashTool(ToolABC):
274
274
  proc.terminate()
275
275
  except ProcessLookupError:
276
276
  return
277
- except Exception:
277
+ except OSError:
278
278
  # Fall back to kill below.
279
279
  pass
280
280
 
@@ -356,7 +356,7 @@ class BashTool(ToolABC):
356
356
  except asyncio.CancelledError:
357
357
  # Propagate cooperative cancellation so outer layers can handle interrupts correctly.
358
358
  raise
359
- except Exception as e: # safeguard against unexpected failures
359
+ except OSError as e: # safeguard: catch remaining OS-level errors (permissions, resources, etc.)
360
360
  return model.ToolResultItem(
361
361
  status="error",
362
362
  output=f"Execution error: {e}",
@@ -20,6 +20,7 @@ from typing import Any, Literal, cast
20
20
  import httpx
21
21
  import openai
22
22
  import openai.types
23
+ import pydantic
23
24
  from openai import AsyncStream
24
25
  from openai.types.chat.chat_completion_chunk import ChatCompletionChunk
25
26
 
@@ -204,7 +205,7 @@ async def parse_chat_completions_stream(
204
205
  try:
205
206
  usage = openai.types.CompletionUsage.model_validate(choice_usage)
206
207
  metadata_tracker.set_usage(convert_usage(usage, param.context_limit, param.max_tokens))
207
- except Exception:
208
+ except pydantic.ValidationError:
208
209
  pass
209
210
 
210
211
  delta = cast(Any, getattr(choice0, "delta", None))
@@ -151,6 +151,28 @@ class SessionStatusUIExtra(BaseModel):
151
151
  by_model: list["TaskMetadata"] = []
152
152
 
153
153
 
154
+ MultiUIExtraItem = (
155
+ DiffUIExtra
156
+ | TodoListUIExtra
157
+ | SessionIdUIExtra
158
+ | MermaidLinkUIExtra
159
+ | TruncationUIExtra
160
+ | MarkdownDocUIExtra
161
+ | SessionStatusUIExtra
162
+ )
163
+
164
+
165
+ class MultiUIExtra(BaseModel):
166
+ """A container UIExtra that can render multiple UI blocks for a single tool result.
167
+
168
+ This is primarily used by tools like apply_patch which can perform multiple
169
+ operations in one invocation.
170
+ """
171
+
172
+ type: Literal["multi"] = "multi"
173
+ items: list[MultiUIExtraItem]
174
+
175
+
154
176
  ToolResultUIExtra = Annotated[
155
177
  DiffUIExtra
156
178
  | TodoListUIExtra
@@ -158,7 +180,8 @@ ToolResultUIExtra = Annotated[
158
180
  | MermaidLinkUIExtra
159
181
  | TruncationUIExtra
160
182
  | MarkdownDocUIExtra
161
- | SessionStatusUIExtra,
183
+ | SessionStatusUIExtra
184
+ | MultiUIExtra,
162
185
  Field(discriminator="type"),
163
186
  ]
164
187
 
@@ -702,7 +702,7 @@ def _render_sub_agent_session(
702
702
 
703
703
  try:
704
704
  sub_session = Session.load(session_id)
705
- except Exception:
705
+ except (OSError, json.JSONDecodeError, ValueError):
706
706
  return None
707
707
 
708
708
  sub_history = sub_session.conversation_history
@@ -23,7 +23,7 @@ def resume_select_session() -> str | None:
23
23
  def _fmt(ts: float) -> str:
24
24
  try:
25
25
  return time.strftime("%m-%d %H:%M:%S", time.localtime(ts))
26
- except Exception:
26
+ except (ValueError, OSError):
27
27
  return str(ts)
28
28
 
29
29
  try:
@@ -76,6 +76,6 @@ def resume_select_session() -> str | None:
76
76
  idx = int(raw)
77
77
  if 1 <= idx <= len(sessions):
78
78
  return str(sessions[idx - 1].id)
79
- except Exception:
79
+ except (ValueError, EOFError):
80
80
  return None
81
81
  return None
@@ -7,7 +7,7 @@ from collections.abc import Iterable, Sequence
7
7
  from pathlib import Path
8
8
  from typing import Any, cast
9
9
 
10
- from pydantic import BaseModel, Field, PrivateAttr
10
+ from pydantic import BaseModel, Field, PrivateAttr, ValidationError
11
11
 
12
12
  from klaude_code.protocol import events, llm_param, model, tools
13
13
  from klaude_code.session.store import JsonlSessionStore, ProjectPaths, build_meta_snapshot
@@ -124,7 +124,7 @@ class Session(BaseModel):
124
124
  if isinstance(k, str) and isinstance(v, dict):
125
125
  try:
126
126
  file_tracker[k] = model.FileStatus.model_validate(v)
127
- except Exception:
127
+ except ValidationError:
128
128
  continue
129
129
 
130
130
  todos_raw = raw.get("todos")
@@ -135,7 +135,7 @@ class Session(BaseModel):
135
135
  continue
136
136
  try:
137
137
  todos.append(model.TodoItem.model_validate(todo_raw))
138
- except Exception:
138
+ except ValidationError:
139
139
  continue
140
140
 
141
141
  created_at = float(raw.get("created_at", time.time()))
@@ -306,7 +306,7 @@ class Session(BaseModel):
306
306
  seen_sub_agent_sessions.add(session_id)
307
307
  try:
308
308
  sub_session = Session.load(session_id)
309
- except Exception:
309
+ except (OSError, json.JSONDecodeError, ValueError):
310
310
  return
311
311
  yield from sub_session.get_history_item()
312
312
 
@@ -204,7 +204,7 @@ class _SkillCompleter(Completer):
204
204
  from klaude_code.skill import get_available_skills
205
205
 
206
206
  return get_available_skills()
207
- except Exception:
207
+ except (ImportError, RuntimeError):
208
208
  return []
209
209
 
210
210
  def is_skill_context(self, document: Document) -> bool:
@@ -497,7 +497,7 @@ class _AtFilesCompleter(Completer):
497
497
  try:
498
498
  if (cwd / s).is_dir():
499
499
  uniq[idx] = f"{s}/"
500
- except Exception:
500
+ except OSError:
501
501
  continue
502
502
  return uniq
503
503
 
@@ -530,7 +530,7 @@ class _AtFilesCompleter(Completer):
530
530
  if tag != "search":
531
531
  return root, None
532
532
  return root, kw
533
- except Exception:
533
+ except ValueError:
534
534
  return None, None
535
535
 
536
536
  # ---- Utilities ----
@@ -680,7 +680,7 @@ class _AtFilesCompleter(Completer):
680
680
  if p.is_dir() and not rel.endswith("/"):
681
681
  rel += "/"
682
682
  items.append(rel)
683
- except Exception:
683
+ except OSError:
684
684
  return []
685
685
  return items[: min(self._max_results, 100)]
686
686
 
@@ -122,6 +122,7 @@ class ActivityState:
122
122
 
123
123
  def __init__(self) -> None:
124
124
  self._composing: bool = False
125
+ self._buffer_length: int = 0
125
126
  self._tool_calls: dict[str, int] = {}
126
127
 
127
128
  @property
@@ -134,6 +135,11 @@ class ActivityState:
134
135
 
135
136
  def set_composing(self, composing: bool) -> None:
136
137
  self._composing = composing
138
+ if not composing:
139
+ self._buffer_length = 0
140
+
141
+ def set_buffer_length(self, length: int) -> None:
142
+ self._buffer_length = length
137
143
 
138
144
  def add_tool_call(self, tool_name: str) -> None:
139
145
  self._tool_calls[tool_name] = self._tool_calls.get(tool_name, 0) + 1
@@ -143,6 +149,7 @@ class ActivityState:
143
149
 
144
150
  def reset(self) -> None:
145
151
  self._composing = False
152
+ self._buffer_length = 0
146
153
  self._tool_calls = {}
147
154
 
148
155
  def get_activity_text(self) -> Text | None:
@@ -159,7 +166,12 @@ class ActivityState:
159
166
  first = False
160
167
  return activity_text
161
168
  if self._composing:
162
- return Text("Composing")
169
+ # Main status text with creative verb
170
+ text = Text.assemble(
171
+ ("Composing ", ThemeKey.STATUS_TEXT_BOLD),
172
+ (f"({self._buffer_length:,})", ThemeKey.STATUS_TEXT),
173
+ )
174
+ return text
163
175
  return None
164
176
 
165
177
 
@@ -206,6 +218,10 @@ class SpinnerStatusState:
206
218
  self._reasoning_status = None
207
219
  self._activity.set_composing(composing)
208
220
 
221
+ def set_buffer_length(self, length: int) -> None:
222
+ """Set buffer length for composing state display."""
223
+ self._activity.set_buffer_length(length)
224
+
209
225
  def add_tool_call(self, tool_name: str) -> None:
210
226
  """Add a tool call to the accumulator."""
211
227
  self._activity.add_tool_call(tool_name)
@@ -230,7 +246,7 @@ class SpinnerStatusState:
230
246
  """Get current spinner status as rich Text (without context)."""
231
247
  activity_text = self._activity.get_activity_text()
232
248
 
233
- base_status = self._todo_status or self._reasoning_status
249
+ base_status = self._reasoning_status or self._todo_status
234
250
 
235
251
  if base_status:
236
252
  result = Text(base_status, style=ThemeKey.STATUS_TEXT_BOLD)
@@ -375,7 +391,7 @@ class DisplayEventHandler:
375
391
  },
376
392
  theme=self.renderer.themes.thinking_markdown_theme,
377
393
  console=self.renderer.console,
378
- live_sink=self.renderer.set_stream_renderable,
394
+ live_sink=self.renderer.set_stream_renderable if const.MARKDOWN_STREAM_LIVE_REPAINT_ENABLED else None,
379
395
  mark=THINKING_MESSAGE_MARK,
380
396
  mark_style=ThemeKey.THINKING,
381
397
  left_margin=const.MARKDOWN_LEFT_MARGIN,
@@ -412,12 +428,15 @@ class DisplayEventHandler:
412
428
  mdargs={"code_theme": self.renderer.themes.code_theme},
413
429
  theme=self.renderer.themes.markdown_theme,
414
430
  console=self.renderer.console,
415
- live_sink=self.renderer.set_stream_renderable,
431
+ live_sink=self.renderer.set_stream_renderable if const.MARKDOWN_STREAM_LIVE_REPAINT_ENABLED else None,
416
432
  mark=ASSISTANT_MESSAGE_MARK,
417
433
  left_margin=const.MARKDOWN_LEFT_MARGIN,
418
434
  )
419
435
  self.assistant_stream.start(mdstream)
420
436
  self.assistant_stream.append(event.content)
437
+ self.spinner_status.set_buffer_length(len(self.assistant_stream.buffer))
438
+ if not first_delta:
439
+ self._update_spinner()
421
440
  if first_delta and self.assistant_stream.mdstream is not None:
422
441
  self.assistant_stream.mdstream.update(self.assistant_stream.buffer)
423
442
  await self.stage_manager.transition_to(Stage.ASSISTANT)
@@ -118,7 +118,7 @@ class PromptToolkitInput(InputProviderABC):
118
118
  try:
119
119
  status = self._status_provider()
120
120
  update_message = status.update_message
121
- except Exception:
121
+ except (AttributeError, RuntimeError):
122
122
  pass
123
123
 
124
124
  # If update available, show only the update message
@@ -127,7 +127,7 @@ class PromptToolkitInput(InputProviderABC):
127
127
  try:
128
128
  terminal_width = shutil.get_terminal_size().columns
129
129
  padding = " " * max(0, terminal_width - len(left_text))
130
- except Exception:
130
+ except (OSError, ValueError):
131
131
  padding = ""
132
132
  toolbar_text = left_text + padding
133
133
  return FormattedText([("#ansiyellow", toolbar_text)])
@@ -151,7 +151,7 @@ class PromptToolkitInput(InputProviderABC):
151
151
  # Add context if available
152
152
  if status.context_usage_percent is not None:
153
153
  right_parts.append(f"context {status.context_usage_percent:.1f}%")
154
- except Exception:
154
+ except (AttributeError, RuntimeError):
155
155
  pass
156
156
 
157
157
  # Build left and right text with borders
@@ -163,7 +163,7 @@ class PromptToolkitInput(InputProviderABC):
163
163
  terminal_width = shutil.get_terminal_size().columns
164
164
  used_width = len(left_text) + len(right_text)
165
165
  padding = " " * max(0, terminal_width - used_width)
166
- except Exception:
166
+ except (OSError, ValueError):
167
167
  padding = ""
168
168
 
169
169
  # Build result with style
@@ -52,7 +52,7 @@ def create_key_bindings(
52
52
  buf.delete_before_cursor() # remove the sentinel backslash # type: ignore[reportUnknownMemberType]
53
53
  buf.insert_text("\n") # type: ignore[reportUnknownMemberType]
54
54
  return
55
- except Exception:
55
+ except (AttributeError, TypeError):
56
56
  # Fall through to default behavior if anything goes wrong
57
57
  pass
58
58
 
@@ -111,7 +111,7 @@ def create_key_bindings(
111
111
 
112
112
  if should_refresh:
113
113
  buf.start_completion(select_first=False) # type: ignore[reportUnknownMemberType]
114
- except Exception:
114
+ except (AttributeError, TypeError):
115
115
  pass
116
116
 
117
117
  @kb.add("left")
@@ -136,7 +136,7 @@ def create_key_bindings(
136
136
  # Default behavior: move one character left when possible.
137
137
  if doc.cursor_position > 0: # type: ignore[reportUnknownMemberType]
138
138
  buf.cursor_left() # type: ignore[reportUnknownMemberType]
139
- except Exception:
139
+ except (AttributeError, IndexError, TypeError):
140
140
  pass
141
141
 
142
142
  @kb.add("right")
@@ -163,7 +163,7 @@ def create_key_bindings(
163
163
  # Default behavior: move one character right when possible.
164
164
  if doc.cursor_position < len(doc.text): # type: ignore[reportUnknownMemberType]
165
165
  buf.cursor_right() # type: ignore[reportUnknownMemberType]
166
- except Exception:
166
+ except (AttributeError, IndexError, TypeError):
167
167
  pass
168
168
 
169
169
  return kb
@@ -332,23 +332,28 @@ class REPLRenderer:
332
332
  self._bottom_live.start()
333
333
 
334
334
  def _bottom_renderable(self) -> RenderableType:
335
- stream = self._stream_renderable
336
- if stream is not None:
337
- current_width = self.console.size.width
338
- if self._stream_last_width != current_width:
339
- height = len(self.console.render_lines(stream, self.console.options, pad=False))
340
- self._stream_last_height = height
341
- self._stream_last_width = current_width
342
- self._stream_max_height = max(self._stream_max_height, height)
343
- else:
344
- height = self._stream_last_height
345
-
346
- pad_lines = max(self._stream_max_height - height, 0)
347
- if pad_lines:
348
- stream = Padding(stream, (0, 0, pad_lines, 0))
349
-
350
- stream_part: RenderableType = stream if stream is not None else Group()
351
- gap_part: RenderableType = Text("") if self._spinner_visible else Group()
335
+ stream_part: RenderableType = Group()
336
+ gap_part: RenderableType = Group()
337
+
338
+ if const.MARKDOWN_STREAM_LIVE_REPAINT_ENABLED:
339
+ stream = self._stream_renderable
340
+ if stream is not None:
341
+ current_width = self.console.size.width
342
+ if self._stream_last_width != current_width:
343
+ height = len(self.console.render_lines(stream, self.console.options, pad=False))
344
+ self._stream_last_height = height
345
+ self._stream_last_width = current_width
346
+ self._stream_max_height = max(self._stream_max_height, height)
347
+ else:
348
+ height = self._stream_last_height
349
+
350
+ pad_lines = max(self._stream_max_height - height, 0)
351
+ if pad_lines:
352
+ stream = Padding(stream, (0, 0, pad_lines, 0))
353
+ stream_part = stream
354
+
355
+ gap_part = Text("") if self._spinner_visible else Group()
356
+
352
357
  status_part: RenderableType = SingleLine(self._status_spinner) if self._spinner_visible else Group()
353
358
  return Group(stream_part, gap_part, status_part)
354
359
 
@@ -148,7 +148,7 @@ def render_diff(diff_text: str, show_file_name: bool = False) -> RenderableType:
148
148
  plus = parts[2] # like '+12,4'
149
149
  new_start = int(plus[1:].split(",")[0])
150
150
  new_ln = new_start
151
- except Exception:
151
+ except (IndexError, ValueError):
152
152
  new_ln = None
153
153
  if has_rendered_diff_content:
154
154
  grid.add_row(Text(f"{'⋮':>{const.DIFF_PREFIX_WIDTH}}", style=ThemeKey.TOOL_RESULT), "")
@@ -1,4 +1,4 @@
1
- from importlib.metadata import version
1
+ from importlib.metadata import PackageNotFoundError, version
2
2
 
3
3
  from rich import box
4
4
  from rich.console import Group, RenderableType
@@ -17,7 +17,7 @@ def _get_version() -> str:
17
17
  """Get the current version of klaude-code."""
18
18
  try:
19
19
  return version("klaude-code")
20
- except Exception:
20
+ except PackageNotFoundError:
21
21
  return "unknown"
22
22
 
23
23
 
@@ -68,24 +68,22 @@ def render_sub_agent_result(
68
68
  panel_style: Style | None = None,
69
69
  ) -> RenderableType:
70
70
  stripped_result = result.strip()
71
-
72
- # Add markdown heading if description is provided
73
- if description:
74
- stripped_result = f"# {description}\n\n{stripped_result}"
75
-
76
71
  result_panel_style = panel_style or ThemeKey.SUB_AGENT_RESULT_PANEL
77
72
 
78
73
  # Use rich JSON for structured output
79
74
  if has_structured_output:
80
75
  try:
81
- return Panel.fit(
82
- Group(
83
- Text(
84
- "use /export to view full output",
85
- style=ThemeKey.TOOL_RESULT,
86
- ),
87
- JSON(stripped_result),
76
+ group_elements: list[RenderableType] = [
77
+ Text(
78
+ "use /export to view full output",
79
+ style=ThemeKey.TOOL_RESULT,
88
80
  ),
81
+ JSON(stripped_result),
82
+ ]
83
+ if description:
84
+ group_elements.insert(0, NoInsetMarkdown(f"# {description}", code_theme=code_theme, style=style or ""))
85
+ return Panel.fit(
86
+ Group(*group_elements),
89
87
  box=box.SIMPLE,
90
88
  border_style=ThemeKey.LINES,
91
89
  style=result_panel_style,
@@ -94,6 +92,10 @@ def render_sub_agent_result(
94
92
  # Fall back to markdown if not valid JSON
95
93
  pass
96
94
 
95
+ # Add markdown heading if description is provided for non-structured output
96
+ if description:
97
+ stripped_result = f"# {description}\n\n{stripped_result}"
98
+
97
99
  lines = stripped_result.splitlines()
98
100
  if len(lines) > const.SUB_AGENT_RESULT_MAX_LINES:
99
101
  hidden_count = len(lines) - const.SUB_AGENT_RESULT_MAX_LINES
@@ -10,7 +10,7 @@ from klaude_code.ui.rich.markdown import ThinkingMarkdown
10
10
  from klaude_code.ui.rich.theme import ThemeKey
11
11
 
12
12
  # UI markers
13
- THINKING_MESSAGE_MARK = ""
13
+ THINKING_MESSAGE_MARK = ""
14
14
 
15
15
 
16
16
  def normalize_thinking_content(content: str) -> str: