klaude-code 1.2.17__py3-none-any.whl → 1.2.18__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 (54) hide show
  1. klaude_code/cli/config_cmd.py +1 -1
  2. klaude_code/cli/debug.py +1 -1
  3. klaude_code/cli/main.py +3 -9
  4. klaude_code/cli/runtime.py +10 -13
  5. klaude_code/command/__init__.py +4 -1
  6. klaude_code/command/clear_cmd.py +2 -7
  7. klaude_code/command/command_abc.py +33 -5
  8. klaude_code/command/debug_cmd.py +79 -0
  9. klaude_code/command/diff_cmd.py +2 -6
  10. klaude_code/command/export_cmd.py +7 -7
  11. klaude_code/command/export_online_cmd.py +1 -5
  12. klaude_code/command/help_cmd.py +4 -9
  13. klaude_code/command/model_cmd.py +10 -6
  14. klaude_code/command/prompt_command.py +2 -6
  15. klaude_code/command/refresh_cmd.py +2 -7
  16. klaude_code/command/registry.py +2 -4
  17. klaude_code/command/release_notes_cmd.py +2 -6
  18. klaude_code/command/status_cmd.py +2 -7
  19. klaude_code/command/terminal_setup_cmd.py +2 -6
  20. klaude_code/command/thinking_cmd.py +13 -8
  21. klaude_code/config/select_model.py +81 -5
  22. klaude_code/const/__init__.py +1 -1
  23. klaude_code/core/executor.py +236 -109
  24. klaude_code/core/manager/__init__.py +2 -4
  25. klaude_code/core/prompts/prompt-claude-code.md +1 -1
  26. klaude_code/core/prompts/prompt-sub-agent-web.md +8 -5
  27. klaude_code/core/reminders.py +9 -35
  28. klaude_code/core/tool/file/read_tool.py +38 -10
  29. klaude_code/core/tool/shell/bash_tool.py +22 -2
  30. klaude_code/core/tool/tool_runner.py +26 -23
  31. klaude_code/core/tool/truncation.py +23 -9
  32. klaude_code/core/tool/web/web_fetch_tool.md +1 -1
  33. klaude_code/core/tool/web/web_fetch_tool.py +36 -1
  34. klaude_code/core/turn.py +28 -0
  35. klaude_code/protocol/commands.py +1 -0
  36. klaude_code/protocol/sub_agent/web.py +3 -2
  37. klaude_code/session/session.py +2 -2
  38. klaude_code/session/templates/export_session.html +24 -13
  39. klaude_code/trace/__init__.py +20 -2
  40. klaude_code/ui/modes/repl/completers.py +19 -2
  41. klaude_code/ui/modes/repl/event_handler.py +8 -6
  42. klaude_code/ui/renderers/metadata.py +2 -4
  43. klaude_code/ui/renderers/thinking.py +24 -8
  44. klaude_code/ui/renderers/tools.py +79 -10
  45. klaude_code/ui/rich/code_panel.py +112 -0
  46. klaude_code/ui/rich/markdown.py +3 -4
  47. klaude_code/ui/rich/status.py +0 -2
  48. klaude_code/ui/rich/theme.py +10 -1
  49. {klaude_code-1.2.17.dist-info → klaude_code-1.2.18.dist-info}/METADATA +16 -6
  50. {klaude_code-1.2.17.dist-info → klaude_code-1.2.18.dist-info}/RECORD +53 -52
  51. klaude_code/core/manager/agent_manager.py +0 -132
  52. /klaude_code/{config → cli}/list_model.py +0 -0
  53. {klaude_code-1.2.17.dist-info → klaude_code-1.2.18.dist-info}/WHEEL +0 -0
  54. {klaude_code-1.2.17.dist-info → klaude_code-1.2.18.dist-info}/entry_points.txt +0 -0
@@ -1,3 +1,21 @@
1
- from .log import DebugType, is_debug_enabled, log, log_debug, logger, prepare_debug_log_file, set_debug_logging
1
+ from .log import (
2
+ DebugType,
3
+ get_current_log_file,
4
+ is_debug_enabled,
5
+ log,
6
+ log_debug,
7
+ logger,
8
+ prepare_debug_log_file,
9
+ set_debug_logging,
10
+ )
2
11
 
3
- __all__ = ["DebugType", "is_debug_enabled", "log", "log_debug", "logger", "prepare_debug_log_file", "set_debug_logging"]
12
+ __all__ = [
13
+ "DebugType",
14
+ "get_current_log_file",
15
+ "is_debug_enabled",
16
+ "log",
17
+ "log_debug",
18
+ "logger",
19
+ "prepare_debug_log_file",
20
+ "set_debug_logging",
21
+ ]
@@ -26,6 +26,7 @@ from prompt_toolkit.document import Document
26
26
  from prompt_toolkit.formatted_text import HTML
27
27
 
28
28
  from klaude_code.command import get_commands
29
+ from klaude_code.trace.log import DebugType, log_debug
29
30
 
30
31
  # Pattern to match @token for completion refresh (used by key bindings).
31
32
  # Supports both plain tokens like `@src/file.py` and quoted tokens like
@@ -85,7 +86,7 @@ class _SlashCommandCompleter(Completer):
85
86
  matched: list[tuple[str, object, str]] = []
86
87
  for cmd_name, cmd_obj in commands.items():
87
88
  if cmd_name.startswith(frag):
88
- hint = " [instructions]" if cmd_obj.support_addition_params else ""
89
+ hint = f" [{cmd_obj.placeholder}]" if cmd_obj.support_addition_params else ""
89
90
  matched.append((cmd_name, cmd_obj, hint))
90
91
 
91
92
  if not matched:
@@ -444,6 +445,8 @@ class _AtFilesCompleter(Completer):
444
445
  return items[: min(self._max_results, 100)]
445
446
 
446
447
  def _run_cmd(self, cmd: list[str], cwd: Path | None = None) -> _CmdResult:
448
+ cmd_str = " ".join(cmd)
449
+ start = time.monotonic()
447
450
  try:
448
451
  p = subprocess.run(
449
452
  cmd,
@@ -453,9 +456,23 @@ class _AtFilesCompleter(Completer):
453
456
  text=True,
454
457
  timeout=1.5,
455
458
  )
459
+ elapsed_ms = (time.monotonic() - start) * 1000
456
460
  if p.returncode == 0:
457
461
  lines = [ln.strip() for ln in p.stdout.splitlines() if ln.strip()]
462
+ log_debug(
463
+ f"[completer] cmd={cmd_str} elapsed={elapsed_ms:.1f}ms results={len(lines)}",
464
+ debug_type=DebugType.EXECUTION,
465
+ )
458
466
  return _CmdResult(True, lines)
467
+ log_debug(
468
+ f"[completer] cmd={cmd_str} elapsed={elapsed_ms:.1f}ms returncode={p.returncode}",
469
+ debug_type=DebugType.EXECUTION,
470
+ )
459
471
  return _CmdResult(False, [])
460
- except Exception:
472
+ except Exception as e:
473
+ elapsed_ms = (time.monotonic() - start) * 1000
474
+ log_debug(
475
+ f"[completer] cmd={cmd_str} elapsed={elapsed_ms:.1f}ms error={e!r}",
476
+ debug_type=DebugType.EXECUTION,
477
+ )
461
478
  return _CmdResult(False, [])
@@ -9,6 +9,7 @@ from klaude_code import const
9
9
  from klaude_code.protocol import events
10
10
  from klaude_code.ui.core.stage_manager import Stage, StageManager
11
11
  from klaude_code.ui.modes.repl.renderer import REPLRenderer
12
+ from klaude_code.ui.renderers.thinking import normalize_thinking_content
12
13
  from klaude_code.ui.rich.markdown import MarkdownStream, ThinkingMarkdown
13
14
  from klaude_code.ui.rich.theme import ThemeKey
14
15
  from klaude_code.ui.terminal.notifier import Notification, NotificationType, TerminalNotifier
@@ -121,7 +122,7 @@ class ActivityState:
121
122
  for name, count in self._tool_calls.items():
122
123
  if not first:
123
124
  activity_text.append(", ")
124
- activity_text.append(name)
125
+ activity_text.append(Text(name, style=ThemeKey.SPINNER_STATUS_TEXT_BOLD))
125
126
  if count > 1:
126
127
  activity_text.append(f" x {count}")
127
128
  first = False
@@ -348,7 +349,7 @@ class DisplayEventHandler:
348
349
  self.thinking_stream.append(event.content)
349
350
 
350
351
  if first_delta and self.thinking_stream.mdstream is not None:
351
- self.thinking_stream.mdstream.update(self.thinking_stream.buffer)
352
+ self.thinking_stream.mdstream.update(normalize_thinking_content(self.thinking_stream.buffer))
352
353
 
353
354
  await self.stage_manager.enter_thinking_stage()
354
355
  self.thinking_stream.debouncer.schedule()
@@ -415,10 +416,11 @@ class DisplayEventHandler:
415
416
  self.renderer.display_tool_call(event)
416
417
 
417
418
  async def _on_tool_result(self, event: events.ToolResultEvent) -> None:
418
- if self.renderer.is_sub_agent_session(event.session_id):
419
+ if self.renderer.is_sub_agent_session(event.session_id) and event.status == "success":
419
420
  return
420
421
  await self.stage_manager.transition_to(Stage.TOOL_RESULT)
421
- self.renderer.display_tool_call_result(event)
422
+ with self.renderer.session_print_context(event.session_id):
423
+ self.renderer.display_tool_call_result(event)
422
424
 
423
425
  def _on_task_metadata(self, event: events.TaskMetadataEvent) -> None:
424
426
  self.renderer.display_task_metadata(event)
@@ -498,14 +500,14 @@ class DisplayEventHandler:
498
500
  if state.is_active:
499
501
  mdstream = state.mdstream
500
502
  assert mdstream is not None
501
- mdstream.update(state.buffer)
503
+ mdstream.update(normalize_thinking_content(state.buffer))
502
504
 
503
505
  async def _finish_thinking_stream(self) -> None:
504
506
  if self.thinking_stream.is_active:
505
507
  self.thinking_stream.debouncer.cancel()
506
508
  mdstream = self.thinking_stream.mdstream
507
509
  assert mdstream is not None
508
- mdstream.update(self.thinking_stream.buffer, final=True)
510
+ mdstream.update(normalize_thinking_content(self.thinking_stream.buffer), final=True)
509
511
  self.thinking_stream.finish()
510
512
  self.renderer.console.pop_theme()
511
513
  self.renderer.print()
@@ -61,9 +61,7 @@ def _render_task_metadata_block(
61
61
  if metadata.usage is not None:
62
62
  # Tokens: ↑ 37k cache 5k ↓ 907 think 45k
63
63
  token_parts: list[Text] = [
64
- Text.assemble(
65
- ("↑ ", ThemeKey.METADATA_DIM), (format_number(metadata.usage.input_tokens), ThemeKey.METADATA)
66
- )
64
+ Text.assemble(("↑", ThemeKey.METADATA_DIM), (format_number(metadata.usage.input_tokens), ThemeKey.METADATA))
67
65
  ]
68
66
  if metadata.usage.cached_tokens > 0:
69
67
  token_parts.append(
@@ -74,7 +72,7 @@ def _render_task_metadata_block(
74
72
  )
75
73
  token_parts.append(
76
74
  Text.assemble(
77
- ("↓ ", ThemeKey.METADATA_DIM), (format_number(metadata.usage.output_tokens), ThemeKey.METADATA)
75
+ ("↓", ThemeKey.METADATA_DIM), (format_number(metadata.usage.output_tokens), ThemeKey.METADATA)
78
76
  )
79
77
  )
80
78
  if metadata.usage.reasoning_tokens > 0:
@@ -1,3 +1,5 @@
1
+ import re
2
+
1
3
  from rich.console import RenderableType
2
4
  from rich.padding import Padding
3
5
  from rich.text import Text
@@ -10,14 +12,28 @@ def thinking_prefix() -> Text:
10
12
  return Text.from_markup("[not italic]⸫[/not italic] Thinking …", style=ThemeKey.THINKING)
11
13
 
12
14
 
13
- def _normalize_thinking_content(content: str) -> str:
15
+ def normalize_thinking_content(content: str) -> str:
14
16
  """Normalize thinking content for display."""
15
- return (
16
- content.rstrip()
17
- .replace("**\n\n", "** \n")
18
- .replace("\\n\\n\n\n", "") # Weird case of Gemini 3
19
- .replace("****", "**\n\n**") # Remove extra newlines after bold titles
20
- )
17
+ text = content.rstrip()
18
+
19
+ # Weird case of Gemini 3
20
+ text = text.replace("\\n\\n\n\n", "")
21
+
22
+ # Fix OpenRouter OpenAI reasoning formatting where segments like
23
+ # "text**Title**\n\n" lose the blank line between segments.
24
+ # We want: "text\n**Title**\n" so that each bold title starts on
25
+ # its own line and uses a single trailing newline.
26
+ text = re.sub(r"([^\n])(\*\*[^*]+?\*\*)\n\n", r"\1 \n\n\2 \n", text)
27
+
28
+ # Remove extra newlines between back-to-back bold titles, eg
29
+ # "**Title1****Title2**" -> "**Title1**\n\n**Title2**".
30
+ text = text.replace("****", "**\n\n**")
31
+
32
+ # Compact double-newline after bold so the body text follows
33
+ # directly after the title line, using a markdown line break.
34
+ text = text.replace("**\n\n", "** \n")
35
+
36
+ return text
21
37
 
22
38
 
23
39
  def render_thinking(content: str, *, code_theme: str, style: str) -> RenderableType | None:
@@ -31,7 +47,7 @@ def render_thinking(content: str, *, code_theme: str, style: str) -> RenderableT
31
47
 
32
48
  return Padding.indent(
33
49
  ThinkingMarkdown(
34
- _normalize_thinking_content(content),
50
+ normalize_thinking_content(content),
35
51
  code_theme=code_theme,
36
52
  style=style,
37
53
  ),
@@ -2,7 +2,7 @@ import json
2
2
  from pathlib import Path
3
3
  from typing import Any, cast
4
4
 
5
- from rich.console import RenderableType
5
+ from rich.console import Group, RenderableType
6
6
  from rich.padding import Padding
7
7
  from rich.text import Text
8
8
 
@@ -379,6 +379,75 @@ def render_mermaid_tool_call(arguments: str) -> RenderableType:
379
379
  return grid
380
380
 
381
381
 
382
+ def _truncate_url(url: str, max_length: int = 400) -> str:
383
+ """Truncate URL for display, preserving domain and path structure."""
384
+ if len(url) <= max_length:
385
+ return url
386
+ # Remove protocol for display
387
+ display_url = url
388
+ for prefix in ("https://", "http://"):
389
+ if display_url.startswith(prefix):
390
+ display_url = display_url[len(prefix) :]
391
+ break
392
+ if len(display_url) <= max_length:
393
+ return display_url
394
+ # Truncate with ellipsis
395
+ return display_url[: max_length - 3] + "..."
396
+
397
+
398
+ def render_web_fetch_tool_call(arguments: str) -> RenderableType:
399
+ grid = create_grid()
400
+ tool_name_column = Text.assemble(("↓", ThemeKey.TOOL_MARK), " ", ("Fetch", ThemeKey.TOOL_NAME))
401
+
402
+ try:
403
+ payload: dict[str, str] = json.loads(arguments)
404
+ except json.JSONDecodeError:
405
+ summary = Text(
406
+ arguments.strip()[: const.INVALID_TOOL_CALL_MAX_LENGTH],
407
+ style=ThemeKey.INVALID_TOOL_CALL_ARGS,
408
+ )
409
+ grid.add_row(tool_name_column, summary)
410
+ return grid
411
+
412
+ url = payload.get("url", "")
413
+ summary = Text(_truncate_url(url), ThemeKey.TOOL_PARAM_FILE_PATH) if url else Text("(no url)", ThemeKey.TOOL_PARAM)
414
+
415
+ grid.add_row(tool_name_column, summary)
416
+ return grid
417
+
418
+
419
+ def render_web_search_tool_call(arguments: str) -> RenderableType:
420
+ grid = create_grid()
421
+ tool_name_column = Text.assemble(("◉", ThemeKey.TOOL_MARK), " ", ("Search", ThemeKey.TOOL_NAME))
422
+
423
+ try:
424
+ payload: dict[str, Any] = json.loads(arguments)
425
+ except json.JSONDecodeError:
426
+ summary = Text(
427
+ arguments.strip()[: const.INVALID_TOOL_CALL_MAX_LENGTH],
428
+ style=ThemeKey.INVALID_TOOL_CALL_ARGS,
429
+ )
430
+ grid.add_row(tool_name_column, summary)
431
+ return grid
432
+
433
+ query = payload.get("query", "")
434
+ max_results = payload.get("max_results")
435
+
436
+ summary = Text("", ThemeKey.TOOL_PARAM)
437
+ if query:
438
+ # Truncate long queries
439
+ display_query = query if len(query) <= 80 else query[:77] + "..."
440
+ summary.append(display_query, ThemeKey.TOOL_PARAM)
441
+ else:
442
+ summary.append("(no query)", ThemeKey.TOOL_PARAM)
443
+
444
+ if isinstance(max_results, int) and max_results != 10:
445
+ summary.append(f" (max {max_results})", ThemeKey.TOOL_TIMEOUT)
446
+
447
+ grid.add_row(tool_name_column, summary)
448
+ return grid
449
+
450
+
382
451
  def render_mermaid_tool_result(tr: events.ToolResultEvent) -> RenderableType:
383
452
  from klaude_code.ui.terminal import supports_osc8_hyperlinks
384
453
 
@@ -409,16 +478,12 @@ def _extract_truncation(
409
478
 
410
479
  def render_truncation_info(ui_extra: model.TruncationUIExtra) -> RenderableType:
411
480
  """Render truncation info for the user."""
412
- original_kb = ui_extra.original_length / 1024
413
481
  truncated_kb = ui_extra.truncated_length / 1024
482
+
414
483
  text = Text.assemble(
415
- ("Output truncated: ", ThemeKey.TOOL_RESULT),
416
- (f"{original_kb:.1f}KB", ThemeKey.TOOL_RESULT),
417
- (" total, ", ThemeKey.TOOL_RESULT),
418
- (f"{truncated_kb:.1f}KB", ThemeKey.TOOL_RESULT_BOLD),
419
- (" hidden\nFull output saved to ", ThemeKey.TOOL_RESULT),
420
- (ui_extra.saved_file_path, ThemeKey.TOOL_RESULT),
421
- ("\nUse Read with limit+offset or rg/grep to inspect", ThemeKey.TOOL_RESULT),
484
+ ("Offload context to ", ThemeKey.TOOL_RESULT_TRUNCATED),
485
+ (ui_extra.saved_file_path, ThemeKey.TOOL_RESULT_TRUNCATED),
486
+ (f", {truncated_kb:.1f}KB truncated", ThemeKey.TOOL_RESULT_TRUNCATED),
422
487
  )
423
488
  return Padding.indent(text, level=2)
424
489
 
@@ -506,6 +571,10 @@ def render_tool_call(e: events.ToolCallEvent) -> RenderableType | None:
506
571
  return render_generic_tool_call(e.tool_name, e.arguments, "◈")
507
572
  case tools.REPORT_BACK:
508
573
  return render_report_back_tool_call()
574
+ case tools.WEB_FETCH:
575
+ return render_web_fetch_tool_call(e.arguments)
576
+ case tools.WEB_SEARCH:
577
+ return render_web_search_tool_call(e.arguments)
509
578
  case _:
510
579
  return render_generic_tool_call(e.tool_name, e.arguments)
511
580
 
@@ -534,7 +603,7 @@ def render_tool_result(e: events.ToolResultEvent) -> RenderableType | None:
534
603
  # Show truncation info if output was truncated and saved to file
535
604
  truncation_info = get_truncation_info(e)
536
605
  if truncation_info:
537
- return render_truncation_info(truncation_info)
606
+ return Group(render_truncation_info(truncation_info), render_generic_tool_result(e.result))
538
607
 
539
608
  diff_text = _extract_diff_text(e.ui_extra)
540
609
 
@@ -0,0 +1,112 @@
1
+ """A panel that only has top and bottom borders, no left/right borders or padding."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING
6
+
7
+ from rich.console import ConsoleRenderable, RichCast
8
+ from rich.jupyter import JupyterMixin
9
+ from rich.measure import Measurement, measure_renderables
10
+ from rich.segment import Segment
11
+ from rich.style import StyleType
12
+
13
+ if TYPE_CHECKING:
14
+ from rich.console import Console, ConsoleOptions, RenderResult
15
+
16
+ # Box drawing characters
17
+ TOP_LEFT = "┌" # ┌
18
+ TOP_RIGHT = "┐" # ┐
19
+ BOTTOM_LEFT = "└" # └
20
+ BOTTOM_RIGHT = "┘" # ┘
21
+ HORIZONTAL = "─" # ─
22
+
23
+
24
+ class CodePanel(JupyterMixin):
25
+ """A panel with only top and bottom borders, no left/right borders.
26
+
27
+ This is designed for code blocks where you want easy copy-paste without
28
+ picking up border characters on the sides.
29
+
30
+ Example:
31
+ >>> console.print(CodePanel(Syntax(code, "python")))
32
+
33
+ Renders as:
34
+ ┌──────────────────────────┐
35
+ code line 1
36
+ code line 2
37
+ └──────────────────────────┘
38
+ """
39
+
40
+ def __init__(
41
+ self,
42
+ renderable: ConsoleRenderable | RichCast | str,
43
+ *,
44
+ border_style: StyleType = "none",
45
+ expand: bool = False,
46
+ padding: int = 1,
47
+ ) -> None:
48
+ """Initialize the CodePanel.
49
+
50
+ Args:
51
+ renderable: A console renderable object.
52
+ border_style: The style of the border. Defaults to "none".
53
+ expand: If True, expand to fill available width. Defaults to False.
54
+ padding: Left/right padding for content. Defaults to 1.
55
+ """
56
+ self.renderable = renderable
57
+ self.border_style = border_style
58
+ self.expand = expand
59
+ self.padding = padding
60
+
61
+ def __rich_console__(self, console: Console, options: ConsoleOptions) -> RenderResult:
62
+ border_style = console.get_style(self.border_style)
63
+ max_width = options.max_width
64
+ pad = self.padding
65
+
66
+ # Measure the content width (account for padding)
67
+ if self.expand:
68
+ content_width = max_width - pad * 2
69
+ else:
70
+ content_width = console.measure(self.renderable, options=options.update(width=max_width - pad * 2)).maximum
71
+ content_width = min(content_width, max_width - pad * 2)
72
+
73
+ # Render content lines
74
+ child_options = options.update(width=content_width)
75
+ lines = console.render_lines(self.renderable, child_options)
76
+
77
+ # Calculate border width based on content width + padding
78
+ border_width = content_width + pad * 2
79
+
80
+ new_line = Segment.line()
81
+ pad_segment = Segment(" " * pad) if pad > 0 else None
82
+
83
+ # Top border: ┌───...───┐
84
+ top_border = (
85
+ TOP_LEFT + (HORIZONTAL * (border_width - 2)) + TOP_RIGHT if border_width >= 2 else HORIZONTAL * border_width
86
+ )
87
+ yield Segment(top_border, border_style)
88
+ yield new_line
89
+
90
+ # Content lines with padding
91
+ for line in lines:
92
+ if pad_segment:
93
+ yield pad_segment
94
+ yield from line
95
+ if pad_segment:
96
+ yield pad_segment
97
+ yield new_line
98
+
99
+ # Bottom border: └───...───┘
100
+ bottom_border = (
101
+ BOTTOM_LEFT + (HORIZONTAL * (border_width - 2)) + BOTTOM_RIGHT
102
+ if border_width >= 2
103
+ else HORIZONTAL * border_width
104
+ )
105
+ yield Segment(bottom_border, border_style)
106
+ yield new_line
107
+
108
+ def __rich_measure__(self, console: Console, options: ConsoleOptions) -> Measurement:
109
+ if self.expand:
110
+ return Measurement(options.max_width, options.max_width)
111
+ width = measure_renderables(console, options, [self.renderable]).maximum + self.padding * 2
112
+ return Measurement(width, width)
@@ -7,11 +7,9 @@ import time
7
7
  from collections.abc import Callable
8
8
  from typing import Any, ClassVar
9
9
 
10
- from rich import box
11
10
  from rich.console import Console, ConsoleOptions, Group, RenderableType, RenderResult
12
11
  from rich.live import Live
13
12
  from rich.markdown import CodeBlock, Heading, Markdown
14
- from rich.panel import Panel
15
13
  from rich.rule import Rule
16
14
  from rich.spinner import Spinner
17
15
  from rich.style import Style
@@ -20,6 +18,7 @@ from rich.text import Text
20
18
  from rich.theme import Theme
21
19
 
22
20
  from klaude_code import const
21
+ from klaude_code.ui.rich.code_panel import CodePanel
23
22
 
24
23
 
25
24
  class NoInsetCodeBlock(CodeBlock):
@@ -34,7 +33,7 @@ class NoInsetCodeBlock(CodeBlock):
34
33
  word_wrap=True,
35
34
  padding=(0, 0),
36
35
  )
37
- yield Panel.fit(syntax, padding=(0, 0), box=box.HORIZONTALS, border_style="markdown.code.border")
36
+ yield CodePanel(syntax, border_style="markdown.code.border")
38
37
 
39
38
 
40
39
  class ThinkingCodeBlock(CodeBlock):
@@ -43,7 +42,7 @@ class ThinkingCodeBlock(CodeBlock):
43
42
  def __rich_console__(self, console: Console, options: ConsoleOptions) -> RenderResult:
44
43
  code = str(self.text).rstrip()
45
44
  text = Text(code, "markdown.code.block")
46
- yield text
45
+ yield CodePanel(text, border_style="markdown.code.border")
47
46
 
48
47
 
49
48
  class LeftHeading(Heading):
@@ -35,8 +35,6 @@ _BREATHING_SPINNER_GLYPHS_BASE = [
35
35
  "✴",
36
36
  "✷",
37
37
  "⟡",
38
- "⬡",
39
- "⬢",
40
38
  ]
41
39
 
42
40
  # Shuffle glyphs on module load for variety across sessions
@@ -86,6 +86,7 @@ class ThemeKey(str, Enum):
86
86
  # SPINNER_STATUS
87
87
  SPINNER_STATUS = "spinner.status"
88
88
  SPINNER_STATUS_TEXT = "spinner.status.text"
89
+ SPINNER_STATUS_TEXT_BOLD = "spinner.status.text.bold"
89
90
  # STATUS
90
91
  STATUS_HINT = "status.hint"
91
92
  # USER_INPUT
@@ -103,6 +104,7 @@ class ThemeKey(str, Enum):
103
104
  TOOL_PARAM = "tool.param"
104
105
  TOOL_PARAM_BOLD = "tool.param.bold"
105
106
  TOOL_RESULT = "tool.result"
107
+ TOOL_RESULT_TRUNCATED = "tool.result.truncated"
106
108
  TOOL_RESULT_BOLD = "tool.result.bold"
107
109
  TOOL_MARK = "tool.mark"
108
110
  TOOL_APPROVED = "tool.approved"
@@ -181,6 +183,7 @@ def get_theme(theme: str | None = None) -> Themes:
181
183
  # SPINNER_STATUS
182
184
  ThemeKey.SPINNER_STATUS.value: palette.blue,
183
185
  ThemeKey.SPINNER_STATUS_TEXT.value: palette.blue,
186
+ ThemeKey.SPINNER_STATUS_TEXT_BOLD.value: "bold " + palette.blue,
184
187
  # STATUS
185
188
  ThemeKey.STATUS_HINT.value: palette.grey2,
186
189
  # REMINDER
@@ -194,6 +197,7 @@ def get_theme(theme: str | None = None) -> Themes:
194
197
  ThemeKey.TOOL_PARAM_BOLD.value: "bold " + palette.green,
195
198
  ThemeKey.TOOL_RESULT.value: palette.grey_green,
196
199
  ThemeKey.TOOL_RESULT_BOLD.value: "bold " + palette.grey_green,
200
+ ThemeKey.TOOL_RESULT_TRUNCATED.value: palette.yellow,
197
201
  ThemeKey.TOOL_MARK.value: "bold",
198
202
  ThemeKey.TOOL_APPROVED.value: palette.green + " bold reverse",
199
203
  ThemeKey.TOOL_REJECTED.value: palette.red + " bold reverse",
@@ -243,11 +247,15 @@ def get_theme(theme: str | None = None) -> Themes:
243
247
  "markdown.hr": palette.grey3,
244
248
  "markdown.item.bullet": palette.grey2,
245
249
  "markdown.item.number": palette.grey2,
250
+ "markdown.link": "underline " + palette.blue,
251
+ "markdown.link_url": "underline " + palette.blue,
246
252
  }
247
253
  ),
248
254
  thinking_markdown_theme=Theme(
249
255
  styles={
250
256
  "markdown.code": palette.grey1 + " italic on " + palette.text_background,
257
+ "markdown.code.block": palette.grey1,
258
+ "markdown.code.border": palette.grey3,
251
259
  "markdown.h1": "bold reverse",
252
260
  "markdown.h1.border": palette.grey3,
253
261
  "markdown.h2.border": palette.grey3,
@@ -256,7 +264,8 @@ def get_theme(theme: str | None = None) -> Themes:
256
264
  "markdown.hr": palette.grey3,
257
265
  "markdown.item.bullet": palette.grey2,
258
266
  "markdown.item.number": palette.grey2,
259
- "markdown.code.block": palette.grey1,
267
+ "markdown.link": "underline " + palette.blue,
268
+ "markdown.link_url": "underline " + palette.blue,
260
269
  "markdown.strong": "bold italic " + palette.grey1,
261
270
  }
262
271
  ),
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: klaude-code
3
- Version: 1.2.17
3
+ Version: 1.2.18
4
4
  Summary: Add your description here
5
5
  Requires-Dist: anthropic>=0.66.0
6
6
  Requires-Dist: ddgs>=9.9.3
@@ -51,12 +51,17 @@ klaude [--model <name>] [--select-model]
51
51
 
52
52
  **Options:**
53
53
  - `--version`/`-V`: Show version and exit.
54
- - `--model`/`-m`: Select a model by logical name from config.
55
- - `--select-model`/`-s`: Interactively choose a model at startup.
54
+ - `--model`/`-m`: Preferred model name (exact match picks immediately; otherwise opens the interactive selector filtered by this value).
55
+ - `--select-model`/`-s`: Open the interactive model selector at startup (shows all models unless `--model` is also provided).
56
56
  - `--continue`/`-c`: Resume the most recent session.
57
57
  - `--resume`/`-r`: Select a session to resume for this project.
58
58
  - `--vanilla`: Minimal mode with only basic tools (Bash, Read, Edit) and no system prompts.
59
59
 
60
+ **Model selection behavior:**
61
+ - Default: uses `main_model` from config.
62
+ - `--select-model`: always prompts you to pick.
63
+ - `--model <value>`: tries to resolve `<value>` to a single model; if it can't, it prompts with a filtered list (and falls back to showing all models if there are no matches).
64
+
60
65
  **Debug Options:**
61
66
  - `--debug`/`-d`: Enable debug mode with verbose logging and LLM trace.
62
67
  - `--debug-filter`: Filter debug output by type (comma-separated).
@@ -64,7 +69,7 @@ klaude [--model <name>] [--select-model]
64
69
 
65
70
  ### Configuration
66
71
 
67
- An example config will be created in `~/.klaude/config.yaml` when first run.
72
+ An example config will be created in `~/.klaude/klaude-config.yaml` when first run.
68
73
 
69
74
  Open the configuration file in editor:
70
75
 
@@ -201,7 +206,7 @@ sub_agent_models:
201
206
  oracle: gpt-5.1
202
207
  explore: haiku
203
208
  task: opus
204
- webfetchagent: haiku
209
+ webagent: haiku
205
210
 
206
211
  ```
207
212
 
@@ -263,5 +268,10 @@ klaude exec "what is 2+2?"
263
268
  echo "hello world" | klaude exec
264
269
 
265
270
  # With model selection
271
+
272
+ # Exact model name (non-interactive)
266
273
  echo "generate quicksort in python" | klaude exec --model gpt-5.1
267
- ```
274
+
275
+ # Partial/ambiguous name opens the interactive selector (filtered)
276
+ echo "generate quicksort in python" | klaude exec --model gpt
277
+ ```