klaude-code 1.2.26__py3-none-any.whl → 1.2.28__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/cli/config_cmd.py +1 -5
  2. klaude_code/cli/debug.py +9 -1
  3. klaude_code/cli/list_model.py +170 -129
  4. klaude_code/cli/main.py +76 -19
  5. klaude_code/cli/runtime.py +15 -11
  6. klaude_code/cli/self_update.py +2 -1
  7. klaude_code/cli/session_cmd.py +1 -1
  8. klaude_code/command/__init__.py +3 -0
  9. klaude_code/command/export_online_cmd.py +15 -12
  10. klaude_code/command/fork_session_cmd.py +42 -0
  11. klaude_code/config/__init__.py +3 -1
  12. klaude_code/config/assets/__init__.py +1 -0
  13. klaude_code/config/assets/builtin_config.yaml +233 -0
  14. klaude_code/config/builtin_config.py +37 -0
  15. klaude_code/config/config.py +332 -112
  16. klaude_code/config/select_model.py +46 -8
  17. klaude_code/core/executor.py +6 -3
  18. klaude_code/core/manager/llm_clients_builder.py +4 -1
  19. klaude_code/core/reminders.py +52 -16
  20. klaude_code/core/tool/file/edit_tool.py +4 -4
  21. klaude_code/core/tool/file/write_tool.py +4 -4
  22. klaude_code/core/tool/shell/bash_tool.py +2 -2
  23. klaude_code/core/tool/web/mermaid_tool.md +17 -0
  24. klaude_code/core/tool/web/mermaid_tool.py +2 -2
  25. klaude_code/llm/openai_compatible/stream.py +2 -1
  26. klaude_code/llm/openai_compatible/tool_call_accumulator.py +17 -1
  27. klaude_code/protocol/commands.py +1 -0
  28. klaude_code/protocol/model.py +1 -0
  29. klaude_code/session/export.py +52 -7
  30. klaude_code/session/selector.py +2 -2
  31. klaude_code/session/session.py +26 -4
  32. klaude_code/trace/log.py +7 -1
  33. klaude_code/ui/modes/repl/__init__.py +3 -44
  34. klaude_code/ui/modes/repl/completers.py +39 -7
  35. klaude_code/ui/modes/repl/event_handler.py +8 -6
  36. klaude_code/ui/modes/repl/input_prompt_toolkit.py +33 -66
  37. klaude_code/ui/modes/repl/key_bindings.py +4 -4
  38. klaude_code/ui/modes/repl/renderer.py +1 -6
  39. klaude_code/ui/renderers/common.py +11 -4
  40. klaude_code/ui/renderers/developer.py +17 -0
  41. klaude_code/ui/renderers/diffs.py +1 -1
  42. klaude_code/ui/renderers/errors.py +10 -5
  43. klaude_code/ui/renderers/metadata.py +2 -2
  44. klaude_code/ui/renderers/tools.py +8 -4
  45. klaude_code/ui/rich/markdown.py +5 -5
  46. klaude_code/ui/rich/theme.py +7 -3
  47. klaude_code/ui/terminal/color.py +1 -1
  48. klaude_code/ui/terminal/control.py +4 -4
  49. {klaude_code-1.2.26.dist-info → klaude_code-1.2.28.dist-info}/METADATA +121 -127
  50. {klaude_code-1.2.26.dist-info → klaude_code-1.2.28.dist-info}/RECORD +52 -48
  51. {klaude_code-1.2.26.dist-info → klaude_code-1.2.28.dist-info}/entry_points.txt +1 -0
  52. {klaude_code-1.2.26.dist-info → klaude_code-1.2.28.dist-info}/WHEEL +0 -0
@@ -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:
@@ -398,8 +398,14 @@ class _AtFilesCompleter(Completer):
398
398
 
399
399
  if not results:
400
400
  if self._has_cmd("fd"):
401
+ # First, get immediate children matching the keyword (depth=0).
402
+ # fd's traversal order is not depth-first, so --max-results may
403
+ # truncate shallow matches. We ensure depth=0 items are always included.
404
+ immediate = self._get_immediate_matches(cwd, key_norm)
401
405
  # Use fd to search anywhere in full path (files and directories), case-insensitive
402
- results, truncated = self._run_fd_search(cwd, key_norm, max_results=max_scan_results)
406
+ fd_results, truncated = self._run_fd_search(cwd, key_norm, max_results=max_scan_results)
407
+ # Merge: immediate matches first, then fd results (deduped in _filter_and_format)
408
+ results = immediate + fd_results
403
409
  elif self._has_cmd("rg"):
404
410
  # Use rg to search only in current directory
405
411
  rg_cache_ttl = max(self._cache_ttl, 30.0)
@@ -451,10 +457,11 @@ class _AtFilesCompleter(Completer):
451
457
  keyword_norm: str,
452
458
  ) -> list[str]:
453
459
  # Filter to keyword (case-insensitive) and rank by:
454
- # 1. Basename hit first, then path hit position, then length
460
+ # 1. Directory depth (shallower first)
461
+ # 2. Basename hit first, then path hit position, then length
455
462
  # Since both fd and rg now search from current directory, all paths are relative to cwd
456
463
  kn = keyword_norm
457
- out: list[tuple[str, tuple[int, int, int, int]]] = []
464
+ out: list[tuple[str, tuple[int, int, int, int, int]]] = []
458
465
  for p in paths_from_root:
459
466
  pl = p.lower()
460
467
  if kn not in pl:
@@ -469,7 +476,9 @@ class _AtFilesCompleter(Completer):
469
476
  base = os.path.basename(rel_to_cwd.rstrip("/")).lower()
470
477
  base_pos = base.find(kn)
471
478
  path_pos = pl.find(kn)
479
+ depth = rel_to_cwd.rstrip("/").count("/")
472
480
  score = (
481
+ depth,
473
482
  0 if base_pos != -1 else 1,
474
483
  base_pos if base_pos != -1 else 10_000,
475
484
  path_pos,
@@ -497,7 +506,7 @@ class _AtFilesCompleter(Completer):
497
506
  try:
498
507
  if (cwd / s).is_dir():
499
508
  uniq[idx] = f"{s}/"
500
- except Exception:
509
+ except OSError:
501
510
  continue
502
511
  return uniq
503
512
 
@@ -530,7 +539,7 @@ class _AtFilesCompleter(Completer):
530
539
  if tag != "search":
531
540
  return root, None
532
541
  return root, kw
533
- except Exception:
542
+ except ValueError:
534
543
  return None, None
535
544
 
536
545
  # ---- Utilities ----
@@ -680,10 +689,32 @@ class _AtFilesCompleter(Completer):
680
689
  if p.is_dir() and not rel.endswith("/"):
681
690
  rel += "/"
682
691
  items.append(rel)
683
- except Exception:
692
+ except OSError:
684
693
  return []
685
694
  return items[: min(self._max_results, 100)]
686
695
 
696
+ def _get_immediate_matches(self, cwd: Path, keyword_norm: str) -> list[str]:
697
+ """Get immediate children of cwd that match the keyword (case-insensitive).
698
+
699
+ This ensures depth=0 matches are always included, even when fd's
700
+ --max-results truncates before reaching them.
701
+ """
702
+ excluded = {".git", ".venv", "node_modules"}
703
+ items: list[str] = []
704
+ try:
705
+ for p in cwd.iterdir():
706
+ name = p.name
707
+ if name in excluded:
708
+ continue
709
+ if keyword_norm in name.lower():
710
+ rel = name
711
+ if p.is_dir():
712
+ rel += "/"
713
+ items.append(rel)
714
+ except OSError:
715
+ return []
716
+ return items
717
+
687
718
  def _run_cmd(self, cmd: list[str], cwd: Path | None = None, *, timeout_sec: float) -> _CmdResult:
688
719
  cmd_str = " ".join(cmd)
689
720
  start = time.monotonic()
@@ -691,6 +722,7 @@ class _AtFilesCompleter(Completer):
691
722
  p = subprocess.run(
692
723
  cmd,
693
724
  cwd=str(cwd) if cwd else None,
725
+ stdin=subprocess.DEVNULL,
694
726
  stdout=subprocess.PIPE,
695
727
  stderr=subprocess.DEVNULL,
696
728
  text=True,
@@ -167,10 +167,9 @@ class ActivityState:
167
167
  return activity_text
168
168
  if self._composing:
169
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
- )
170
+ text = Text("Composing", style=ThemeKey.STATUS_TEXT_BOLD)
171
+ if self._buffer_length > 0:
172
+ text.append(f" ({self._buffer_length:,})", style=ThemeKey.STATUS_TEXT)
174
173
  return text
175
174
  return None
176
175
 
@@ -246,13 +245,16 @@ class SpinnerStatusState:
246
245
  """Get current spinner status as rich Text (without context)."""
247
246
  activity_text = self._activity.get_activity_text()
248
247
 
249
- base_status = self._todo_status or self._reasoning_status
248
+ base_status = self._reasoning_status or self._todo_status
250
249
 
251
250
  if base_status:
252
- result = Text(base_status, style=ThemeKey.STATUS_TEXT_BOLD)
253
251
  if activity_text:
252
+ result = Text()
253
+ result.append(base_status, style=ThemeKey.STATUS_TEXT_BOLD_ITALIC)
254
254
  result.append(" | ")
255
255
  result.append_text(activity_text)
256
+ else:
257
+ result = Text(base_status, style=ThemeKey.STATUS_TEXT_BOLD_ITALIC)
256
258
  elif activity_text:
257
259
  activity_text.append(" …")
258
260
  result = activity_text
@@ -21,16 +21,11 @@ from klaude_code.ui.modes.repl.completers import AT_TOKEN_PATTERN, create_repl_c
21
21
  from klaude_code.ui.modes.repl.key_bindings import create_key_bindings
22
22
  from klaude_code.ui.renderers.user_input import USER_MESSAGE_MARK
23
23
  from klaude_code.ui.terminal.color import is_light_terminal_background
24
- from klaude_code.ui.utils.common import get_current_git_branch, show_path_with_tilde
25
24
 
26
25
 
27
26
  class REPLStatusSnapshot(NamedTuple):
28
27
  """Snapshot of REPL status for bottom toolbar display."""
29
28
 
30
- model_name: str
31
- context_usage_percent: float | None
32
- llm_calls: int
33
- tool_calls: int
34
29
  update_message: str | None = None
35
30
 
36
31
 
@@ -54,11 +49,16 @@ class PromptToolkitInput(InputProviderABC):
54
49
  status_provider: Callable[[], REPLStatusSnapshot] | None = None,
55
50
  pre_prompt: Callable[[], None] | None = None,
56
51
  post_prompt: Callable[[], None] | None = None,
52
+ is_light_background: bool | None = None,
57
53
  ): # ▌
58
54
  self._status_provider = status_provider
59
55
  self._pre_prompt = pre_prompt
60
56
  self._post_prompt = post_prompt
61
- self._is_light_terminal_background = is_light_terminal_background(timeout=0.2)
57
+ # Use provided value if available to avoid redundant TTY queries that may interfere
58
+ # with prompt_toolkit's terminal state after questionary has been used.
59
+ self._is_light_terminal_background = (
60
+ is_light_background if is_light_background is not None else is_light_terminal_background(timeout=0.2)
61
+ )
62
62
 
63
63
  project = str(Path.cwd()).strip("/").replace("/", "-")
64
64
  history_path = Path.home() / ".klaude" / "projects" / project / "input" / "input_history.txt"
@@ -91,7 +91,6 @@ class PromptToolkitInput(InputProviderABC):
91
91
  completer=ThreadedCompleter(create_repl_completer()),
92
92
  complete_while_typing=True,
93
93
  erase_when_done=True,
94
- bottom_toolbar=self._render_bottom_toolbar,
95
94
  mouse_support=False,
96
95
  style=Style.from_dict(
97
96
  {
@@ -107,68 +106,29 @@ class PromptToolkitInput(InputProviderABC):
107
106
  ),
108
107
  )
109
108
 
110
- def _render_bottom_toolbar(self) -> FormattedText:
111
- """Render bottom toolbar with working directory, git branch on left, model name and context usage on right.
112
-
113
- If an update is available, only show the update message on the left side.
114
- """
115
- # Check for update message first
116
- update_message: str | None = None
117
- if self._status_provider:
118
- try:
119
- status = self._status_provider()
120
- update_message = status.update_message
121
- except Exception:
122
- pass
123
-
124
- # If update available, show only the update message
125
- if update_message:
126
- left_text = " " + update_message
127
- try:
128
- terminal_width = shutil.get_terminal_size().columns
129
- padding = " " * max(0, terminal_width - len(left_text))
130
- except Exception:
131
- padding = ""
132
- toolbar_text = left_text + padding
133
- return FormattedText([("#ansiyellow", toolbar_text)])
134
-
135
- # Normal mode: Left side: path and git branch
136
- left_parts: list[str] = []
137
- left_parts.append(show_path_with_tilde())
138
-
139
- git_branch = get_current_git_branch()
140
- if git_branch:
141
- left_parts.append(git_branch)
142
-
143
- # Right side: status info
144
- right_parts: list[str] = []
145
- if self._status_provider:
146
- try:
147
- status = self._status_provider()
148
- model_name = status.model_name or "N/A"
149
- right_parts.append(model_name)
150
-
151
- # Add context if available
152
- if status.context_usage_percent is not None:
153
- right_parts.append(f"context {status.context_usage_percent:.1f}%")
154
- except Exception:
155
- pass
156
-
157
- # Build left and right text with borders
158
- left_text = " " + " · ".join(left_parts)
159
- right_text = (" · ".join(right_parts) + " ") if right_parts else " "
160
-
161
- # Calculate padding
109
+ def _get_bottom_toolbar(self) -> FormattedText | None:
110
+ """Return bottom toolbar content only when there's an update message available."""
111
+ if not self._status_provider:
112
+ return None
113
+
114
+ try:
115
+ status = self._status_provider()
116
+ update_message = status.update_message
117
+ except (AttributeError, RuntimeError):
118
+ return None
119
+
120
+ if not update_message:
121
+ return None
122
+
123
+ left_text = " " + update_message
162
124
  try:
163
125
  terminal_width = shutil.get_terminal_size().columns
164
- used_width = len(left_text) + len(right_text)
165
- padding = " " * max(0, terminal_width - used_width)
166
- except Exception:
126
+ padding = " " * max(0, terminal_width - len(left_text))
127
+ except (OSError, ValueError):
167
128
  padding = ""
168
129
 
169
- # Build result with style
170
- toolbar_text = left_text + padding + right_text
171
- return FormattedText([("#2c7eac", toolbar_text)])
130
+ toolbar_text = left_text + padding
131
+ return FormattedText([("#ansiyellow", toolbar_text)])
172
132
 
173
133
  def _render_input_placeholder(self) -> FormattedText:
174
134
  if self._is_light_terminal_background is True:
@@ -210,8 +170,15 @@ class PromptToolkitInput(InputProviderABC):
210
170
  if self._pre_prompt is not None:
211
171
  with contextlib.suppress(Exception):
212
172
  self._pre_prompt()
173
+
174
+ # Only show bottom toolbar if there's an update message
175
+ bottom_toolbar = self._get_bottom_toolbar()
176
+
213
177
  with patch_stdout():
214
- line: str = await self._session.prompt_async(placeholder=self._render_input_placeholder())
178
+ line: str = await self._session.prompt_async(
179
+ placeholder=self._render_input_placeholder(),
180
+ bottom_toolbar=bottom_toolbar,
181
+ )
215
182
  if self._post_prompt is not None:
216
183
  with contextlib.suppress(Exception):
217
184
  self._post_prompt()
@@ -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
@@ -266,12 +266,7 @@ class REPLRenderer:
266
266
  self.print(r_user_input.render_interrupt())
267
267
 
268
268
  def display_error(self, event: events.ErrorEvent) -> None:
269
- self.print(
270
- r_errors.render_error(
271
- truncate_display(event.error_message),
272
- indent=0,
273
- )
274
- )
269
+ self.print(r_errors.render_error(truncate_display(event.error_message)))
275
270
 
276
271
  # -------------------------------------------------------------------------
277
272
  # Spinner control methods
@@ -37,10 +37,17 @@ def truncate_display(
37
37
 
38
38
  if len(lines) > max_lines:
39
39
  truncated_lines = len(lines) - max_lines
40
- head_count = max_lines // 2
41
- tail_count = max_lines - head_count
42
- head_lines = lines[:head_count]
43
- tail_lines = lines[-tail_count:]
40
+
41
+ # If the hidden section is too small, show everything instead of inserting
42
+ # the "(more N lines)" indicator.
43
+ if truncated_lines < 5:
44
+ truncated_lines = 0
45
+ head_lines = lines
46
+ else:
47
+ head_count = max_lines // 2
48
+ tail_count = max_lines - head_count
49
+ head_lines = lines[:head_count]
50
+ tail_lines = lines[-tail_count:]
44
51
  else:
45
52
  head_lines = lines
46
53
 
@@ -120,6 +120,8 @@ def render_command_output(e: events.DeveloperMessageEvent) -> RenderableType:
120
120
  return _render_status_output(e.item.command_output)
121
121
  case commands.CommandName.RELEASE_NOTES:
122
122
  return Padding.indent(NoInsetMarkdown(e.item.content or ""), level=2)
123
+ case commands.CommandName.FORK_SESSION:
124
+ return _render_fork_session_output(e.item.command_output)
123
125
  case _:
124
126
  content = e.item.content or "(no content)"
125
127
  style = ThemeKey.TOOL_RESULT if not e.item.command_output.is_error else ThemeKey.ERROR
@@ -145,6 +147,21 @@ def _format_cost(cost: float | None, currency: str = "USD") -> str:
145
147
  return f"{symbol}{cost:.2f}"
146
148
 
147
149
 
150
+ def _render_fork_session_output(command_output: model.CommandOutput) -> RenderableType:
151
+ """Render fork session output with usage instructions."""
152
+ if not isinstance(command_output.ui_extra, model.SessionIdUIExtra):
153
+ return Text("(no session id)", style=ThemeKey.METADATA)
154
+
155
+ session_id = command_output.ui_extra.session_id
156
+ grid = Table.grid(padding=(0, 1))
157
+ grid.add_column(style=ThemeKey.METADATA, overflow="fold")
158
+
159
+ grid.add_row(Text("Session forked. To continue in a new conversation:", style=ThemeKey.METADATA))
160
+ grid.add_row(Text(f" klaude --resume-by-id {session_id}", style=ThemeKey.METADATA_BOLD))
161
+
162
+ return Padding.indent(grid, level=2)
163
+
164
+
148
165
  def _render_status_output(command_output: model.CommandOutput) -> RenderableType:
149
166
  """Render session status with total cost and per-model breakdown."""
150
167
  if not isinstance(command_output.ui_extra, model.SessionStatusUIExtra):
@@ -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), "")
@@ -5,12 +5,17 @@ from klaude_code.ui.renderers.common import create_grid
5
5
  from klaude_code.ui.rich.theme import ThemeKey
6
6
 
7
7
 
8
- def render_error(error_msg: Text, indent: int = 2) -> RenderableType:
9
- """Stateless error renderer.
8
+ def render_error(error_msg: Text) -> RenderableType:
9
+ """Render error with X mark for error events."""
10
+ grid = create_grid()
11
+ error_msg.style = ThemeKey.ERROR
12
+ grid.add_row(Text("✘", style=ThemeKey.ERROR_BOLD), error_msg)
13
+ return grid
14
+
10
15
 
11
- Shows a two-column grid with an error mark and truncated message.
12
- """
16
+ def render_tool_error(error_msg: Text) -> RenderableType:
17
+ """Render error with indent for tool results."""
13
18
  grid = create_grid()
14
19
  error_msg.style = ThemeKey.ERROR
15
- grid.add_row(Text(" " * indent + "✘", style=ThemeKey.ERROR_BOLD), error_msg)
20
+ grid.add_row(Text(" "), error_msg)
16
21
  return grid
@@ -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
 
@@ -18,7 +18,7 @@ from klaude_code.ui.rich.theme import ThemeKey
18
18
 
19
19
  # Tool markers (Unicode symbols for UI display)
20
20
  MARK_GENERIC = "⚒"
21
- MARK_BASH = ""
21
+ MARK_BASH = "$"
22
22
  MARK_PLAN = "Ξ"
23
23
  MARK_READ = "←"
24
24
  MARK_EDIT = "±"
@@ -205,9 +205,13 @@ def render_write_tool_call(arguments: str) -> RenderableType:
205
205
  grid = create_grid()
206
206
  try:
207
207
  json_dict = json.loads(arguments)
208
- file_path = json_dict.get("file_path")
208
+ file_path = json_dict.get("file_path", "")
209
209
  tool_name_column = Text.assemble((MARK_WRITE, ThemeKey.TOOL_MARK), " ", ("Write", ThemeKey.TOOL_NAME))
210
- arguments_column = render_path(file_path, ThemeKey.TOOL_PARAM_FILE_PATH)
210
+ # Markdown files show path in result panel, skip here to avoid duplication
211
+ if file_path.endswith(".md"):
212
+ arguments_column = Text("")
213
+ else:
214
+ arguments_column = render_path(file_path, ThemeKey.TOOL_PARAM_FILE_PATH)
211
215
  except json.JSONDecodeError:
212
216
  tool_name_column = Text.assemble((MARK_WRITE, ThemeKey.TOOL_MARK), " ", ("Write", ThemeKey.TOOL_NAME))
213
217
  arguments_column = Text(
@@ -569,7 +573,7 @@ def render_tool_result(e: events.ToolResultEvent, *, code_theme: str = "monokai"
569
573
  # Handle error case
570
574
  if e.status == "error" and e.ui_extra is None:
571
575
  error_msg = truncate_display(e.result)
572
- return r_errors.render_error(error_msg)
576
+ return r_errors.render_tool_error(error_msg)
573
577
 
574
578
  # Render multiple ui blocks if present
575
579
  if isinstance(e.ui_extra, model.MultiUIExtra) and e.ui_extra.items:
@@ -54,11 +54,11 @@ class Divider(MarkdownElement):
54
54
  yield Rule(style=style, characters="-")
55
55
 
56
56
 
57
- class MinimalHeavyHeadTable(TableElement):
57
+ class MarkdownTable(TableElement):
58
58
  """A table element with MINIMAL_HEAVY_HEAD box style."""
59
59
 
60
60
  def __rich_console__(self, console: Console, options: ConsoleOptions) -> RenderResult:
61
- table = Table(box=box.MARKDOWN)
61
+ table = Table(box=box.MARKDOWN, border_style=console.get_style("markdown.table.border"))
62
62
 
63
63
  if self.header is not None and self.header.row is not None:
64
64
  for column in self.header.row.cells:
@@ -97,7 +97,7 @@ class NoInsetMarkdown(Markdown):
97
97
  "code_block": NoInsetCodeBlock,
98
98
  "heading_open": LeftHeading,
99
99
  "hr": Divider,
100
- "table_open": MinimalHeavyHeadTable,
100
+ "table_open": MarkdownTable,
101
101
  }
102
102
 
103
103
 
@@ -110,7 +110,7 @@ class ThinkingMarkdown(Markdown):
110
110
  "code_block": ThinkingCodeBlock,
111
111
  "heading_open": LeftHeading,
112
112
  "hr": Divider,
113
- "table_open": MinimalHeavyHeadTable,
113
+ "table_open": MarkdownTable,
114
114
  }
115
115
 
116
116
 
@@ -192,7 +192,7 @@ class MarkdownStream:
192
192
 
193
193
  try:
194
194
  tokens = self._parser.parse(text)
195
- except Exception:
195
+ except Exception: # markdown-it-py may raise various internal errors during parsing
196
196
  return 0
197
197
 
198
198
  top_level: list[Token] = [token for token in tokens if token.level == 0 and token.map is not None]
@@ -130,6 +130,7 @@ class ThemeKey(str, Enum):
130
130
  STATUS_SPINNER = "spinner.status"
131
131
  STATUS_TEXT = "spinner.status.text"
132
132
  STATUS_TEXT_BOLD = "spinner.status.text.bold"
133
+ STATUS_TEXT_BOLD_ITALIC = "spinner.status.text.bold_italic"
133
134
  # STATUS
134
135
  STATUS_HINT = "status.hint"
135
136
  # USER_INPUT
@@ -222,7 +223,7 @@ def get_theme(theme: str | None = None) -> Themes:
222
223
  ThemeKey.ERROR_BOLD.value: "bold " + palette.red,
223
224
  ThemeKey.INTERRUPT.value: "reverse bold " + palette.red,
224
225
  # USER_INPUT
225
- ThemeKey.USER_INPUT.value: "bold " + palette.magenta,
226
+ ThemeKey.USER_INPUT.value: palette.magenta,
226
227
  ThemeKey.USER_INPUT_PROMPT.value: "bold " + palette.magenta,
227
228
  ThemeKey.USER_INPUT_AT_PATTERN.value: palette.purple,
228
229
  ThemeKey.USER_INPUT_SLASH_COMMAND.value: "bold reverse " + palette.blue,
@@ -234,7 +235,8 @@ def get_theme(theme: str | None = None) -> Themes:
234
235
  # STATUS
235
236
  ThemeKey.STATUS_SPINNER.value: palette.blue,
236
237
  ThemeKey.STATUS_TEXT.value: palette.blue,
237
- ThemeKey.STATUS_TEXT_BOLD.value: "bold italic " + palette.blue,
238
+ ThemeKey.STATUS_TEXT_BOLD.value: "bold " + palette.blue,
239
+ ThemeKey.STATUS_TEXT_BOLD_ITALIC.value: "bold italic " + palette.blue,
238
240
  ThemeKey.STATUS_HINT.value: palette.grey2,
239
241
  # REMINDER
240
242
  ThemeKey.REMINDER.value: palette.grey1,
@@ -276,7 +278,7 @@ def get_theme(theme: str | None = None) -> Themes:
276
278
  ThemeKey.RESUME_FLAG.value: "bold reverse " + palette.green,
277
279
  ThemeKey.RESUME_INFO.value: palette.green,
278
280
  # CONFIGURATION DISPLAY
279
- ThemeKey.CONFIG_TABLE_HEADER.value: palette.green,
281
+ ThemeKey.CONFIG_TABLE_HEADER.value: palette.grey1,
280
282
  ThemeKey.CONFIG_STATUS_OK.value: palette.green,
281
283
  ThemeKey.CONFIG_STATUS_PRIMARY.value: palette.yellow,
282
284
  ThemeKey.CONFIG_STATUS_ERROR.value: palette.red,
@@ -299,6 +301,7 @@ def get_theme(theme: str | None = None) -> Themes:
299
301
  "markdown.item.number": palette.grey2,
300
302
  "markdown.link": "underline " + palette.blue,
301
303
  "markdown.link_url": "underline " + palette.blue,
304
+ "markdown.table.border": palette.grey2,
302
305
  }
303
306
  ),
304
307
  thinking_markdown_theme=Theme(
@@ -319,6 +322,7 @@ def get_theme(theme: str | None = None) -> Themes:
319
322
  "markdown.link": "underline " + palette.blue,
320
323
  "markdown.link_url": "underline " + palette.blue,
321
324
  "markdown.strong": "bold italic " + palette.grey1,
325
+ "markdown.table.border": palette.grey2,
322
326
  }
323
327
  ),
324
328
  code_theme=palette.code_theme,
@@ -182,7 +182,7 @@ def _parse_osc_color_response(data: bytes) -> tuple[int, int, int] | None:
182
182
 
183
183
  try:
184
184
  text = data.decode("ascii", errors="ignore")
185
- except Exception:
185
+ except LookupError: # encoding lookup failure (should not happen with "ascii")
186
186
  return None
187
187
 
188
188
  match = _OSC_BG_REGEX.search(text)
@@ -48,7 +48,7 @@ def start_esc_interrupt_monitor(
48
48
  try:
49
49
  fd = sys.stdin.fileno()
50
50
  old = termios.tcgetattr(fd)
51
- except Exception as exc: # pragma: no cover - environment dependent
51
+ except OSError as exc: # pragma: no cover - environment dependent
52
52
  log((f"esc monitor init error: {exc}", "r red"))
53
53
  return
54
54
 
@@ -60,7 +60,7 @@ def start_esc_interrupt_monitor(
60
60
  continue
61
61
  try:
62
62
  ch = os.read(fd, 1).decode(errors="ignore")
63
- except Exception:
63
+ except OSError:
64
64
  continue
65
65
  if ch != "\x1b":
66
66
  continue
@@ -71,7 +71,7 @@ def start_esc_interrupt_monitor(
71
71
  while r2:
72
72
  try:
73
73
  seq += os.read(fd, 1).decode(errors="ignore")
74
- except Exception:
74
+ except OSError:
75
75
  break
76
76
  r2, _, _ = select.select([sys.stdin], [], [], 0.0)
77
77
 
@@ -127,7 +127,7 @@ def install_sigint_double_press_exit(
127
127
 
128
128
  try:
129
129
  signal.signal(signal.SIGINT, _handler)
130
- except Exception: # pragma: no cover - platform dependent
130
+ except (OSError, ValueError): # pragma: no cover - platform dependent
131
131
  # If installing the handler fails, restore() will be a no-op.
132
132
  return lambda: None
133
133