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.
- klaude_code/cli/config_cmd.py +1 -1
- klaude_code/cli/debug.py +1 -1
- klaude_code/cli/main.py +3 -9
- klaude_code/cli/runtime.py +10 -13
- klaude_code/command/__init__.py +4 -1
- klaude_code/command/clear_cmd.py +2 -7
- klaude_code/command/command_abc.py +33 -5
- klaude_code/command/debug_cmd.py +79 -0
- klaude_code/command/diff_cmd.py +2 -6
- klaude_code/command/export_cmd.py +7 -7
- klaude_code/command/export_online_cmd.py +1 -5
- klaude_code/command/help_cmd.py +4 -9
- klaude_code/command/model_cmd.py +10 -6
- klaude_code/command/prompt_command.py +2 -6
- klaude_code/command/refresh_cmd.py +2 -7
- klaude_code/command/registry.py +2 -4
- klaude_code/command/release_notes_cmd.py +2 -6
- klaude_code/command/status_cmd.py +2 -7
- klaude_code/command/terminal_setup_cmd.py +2 -6
- klaude_code/command/thinking_cmd.py +13 -8
- klaude_code/config/select_model.py +81 -5
- klaude_code/const/__init__.py +1 -1
- klaude_code/core/executor.py +236 -109
- klaude_code/core/manager/__init__.py +2 -4
- klaude_code/core/prompts/prompt-claude-code.md +1 -1
- klaude_code/core/prompts/prompt-sub-agent-web.md +8 -5
- klaude_code/core/reminders.py +9 -35
- klaude_code/core/tool/file/read_tool.py +38 -10
- klaude_code/core/tool/shell/bash_tool.py +22 -2
- klaude_code/core/tool/tool_runner.py +26 -23
- klaude_code/core/tool/truncation.py +23 -9
- klaude_code/core/tool/web/web_fetch_tool.md +1 -1
- klaude_code/core/tool/web/web_fetch_tool.py +36 -1
- klaude_code/core/turn.py +28 -0
- klaude_code/protocol/commands.py +1 -0
- klaude_code/protocol/sub_agent/web.py +3 -2
- klaude_code/session/session.py +2 -2
- klaude_code/session/templates/export_session.html +24 -13
- klaude_code/trace/__init__.py +20 -2
- klaude_code/ui/modes/repl/completers.py +19 -2
- klaude_code/ui/modes/repl/event_handler.py +8 -6
- klaude_code/ui/renderers/metadata.py +2 -4
- klaude_code/ui/renderers/thinking.py +24 -8
- klaude_code/ui/renderers/tools.py +79 -10
- klaude_code/ui/rich/code_panel.py +112 -0
- klaude_code/ui/rich/markdown.py +3 -4
- klaude_code/ui/rich/status.py +0 -2
- klaude_code/ui/rich/theme.py +10 -1
- {klaude_code-1.2.17.dist-info → klaude_code-1.2.18.dist-info}/METADATA +16 -6
- {klaude_code-1.2.17.dist-info → klaude_code-1.2.18.dist-info}/RECORD +53 -52
- klaude_code/core/manager/agent_manager.py +0 -132
- /klaude_code/{config → cli}/list_model.py +0 -0
- {klaude_code-1.2.17.dist-info → klaude_code-1.2.18.dist-info}/WHEEL +0 -0
- {klaude_code-1.2.17.dist-info → klaude_code-1.2.18.dist-info}/entry_points.txt +0 -0
klaude_code/trace/__init__.py
CHANGED
|
@@ -1,3 +1,21 @@
|
|
|
1
|
-
from .log import
|
|
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__ = [
|
|
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 = " [
|
|
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.
|
|
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
|
-
("↓
|
|
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
|
|
15
|
+
def normalize_thinking_content(content: str) -> str:
|
|
14
16
|
"""Normalize thinking content for display."""
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
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
|
-
("
|
|
416
|
-
(
|
|
417
|
-
("
|
|
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)
|
klaude_code/ui/rich/markdown.py
CHANGED
|
@@ -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
|
|
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):
|
klaude_code/ui/rich/status.py
CHANGED
klaude_code/ui/rich/theme.py
CHANGED
|
@@ -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.
|
|
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.
|
|
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`:
|
|
55
|
-
- `--select-model`/`-s`:
|
|
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
|
-
|
|
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
|
+
```
|