klaude-code 1.2.16__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 +20 -13
- klaude_code/command/__init__.py +7 -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 +145 -0
- 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/config.py +16 -17
- 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/manager/sub_agent_manager.py +1 -1
- klaude_code/core/prompts/prompt-claude-code.md +1 -1
- klaude_code/core/prompts/prompt-sub-agent-oracle.md +0 -1
- klaude_code/core/prompts/prompt-sub-agent-web.md +51 -0
- klaude_code/core/reminders.py +9 -35
- klaude_code/core/task.py +8 -0
- klaude_code/core/tool/__init__.py +2 -0
- klaude_code/core/tool/file/read_tool.py +38 -10
- klaude_code/core/tool/report_back_tool.py +28 -2
- 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/tool/web/web_search_tool.md +23 -0
- klaude_code/core/tool/web/web_search_tool.py +126 -0
- klaude_code/core/turn.py +28 -0
- klaude_code/protocol/commands.py +2 -0
- klaude_code/protocol/events.py +8 -0
- klaude_code/protocol/sub_agent/__init__.py +1 -1
- klaude_code/protocol/sub_agent/explore.py +1 -1
- klaude_code/protocol/sub_agent/web.py +79 -0
- klaude_code/protocol/tools.py +1 -0
- klaude_code/session/session.py +2 -2
- klaude_code/session/templates/export_session.html +123 -37
- klaude_code/trace/__init__.py +20 -2
- klaude_code/ui/modes/repl/completers.py +19 -2
- klaude_code/ui/modes/repl/event_handler.py +44 -15
- klaude_code/ui/modes/repl/renderer.py +3 -3
- klaude_code/ui/renderers/metadata.py +2 -4
- klaude_code/ui/renderers/sub_agent.py +14 -10
- klaude_code/ui/renderers/thinking.py +24 -8
- klaude_code/ui/renderers/tools.py +83 -20
- klaude_code/ui/rich/code_panel.py +112 -0
- klaude_code/ui/rich/markdown.py +3 -4
- klaude_code/ui/rich/status.py +30 -6
- klaude_code/ui/rich/theme.py +10 -1
- {klaude_code-1.2.16.dist-info → klaude_code-1.2.18.dist-info}/METADATA +126 -25
- {klaude_code-1.2.16.dist-info → klaude_code-1.2.18.dist-info}/RECORD +67 -63
- klaude_code/core/manager/agent_manager.py +0 -132
- klaude_code/core/prompts/prompt-sub-agent-webfetch.md +0 -46
- klaude_code/protocol/sub_agent/web_fetch.py +0 -74
- /klaude_code/{config → cli}/list_model.py +0 -0
- {klaude_code-1.2.16.dist-info → klaude_code-1.2.18.dist-info}/WHEEL +0 -0
- {klaude_code-1.2.16.dist-info → klaude_code-1.2.18.dist-info}/entry_points.txt +0 -0
|
@@ -16,7 +16,7 @@ from klaude_code.ui.rich.theme import ThemeKey
|
|
|
16
16
|
|
|
17
17
|
def _compact_schema_value(value: dict[str, Any]) -> str | list[Any] | dict[str, Any]:
|
|
18
18
|
"""Convert a JSON Schema value to compact representation."""
|
|
19
|
-
value_type = value.get("type", "any")
|
|
19
|
+
value_type = value.get("type", "any").lower()
|
|
20
20
|
desc = value.get("description", "")
|
|
21
21
|
|
|
22
22
|
if value_type == "object":
|
|
@@ -64,16 +64,20 @@ def render_sub_agent_result(
|
|
|
64
64
|
|
|
65
65
|
# Use rich JSON for structured output
|
|
66
66
|
if has_structured_output:
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
67
|
+
try:
|
|
68
|
+
return Panel.fit(
|
|
69
|
+
Group(
|
|
70
|
+
Text(
|
|
71
|
+
"use /export to view full output",
|
|
72
|
+
style=ThemeKey.TOOL_RESULT,
|
|
73
|
+
),
|
|
74
|
+
JSON(stripped_result),
|
|
72
75
|
),
|
|
73
|
-
|
|
74
|
-
)
|
|
75
|
-
|
|
76
|
-
|
|
76
|
+
border_style=ThemeKey.LINES,
|
|
77
|
+
)
|
|
78
|
+
except json.JSONDecodeError:
|
|
79
|
+
# Fall back to markdown if not valid JSON
|
|
80
|
+
pass
|
|
77
81
|
|
|
78
82
|
lines = stripped_result.splitlines()
|
|
79
83
|
if len(lines) > const.SUB_AGENT_RESULT_MAX_LINES:
|
|
@@ -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
|
|
|
@@ -186,14 +186,7 @@ def render_write_tool_call(arguments: str) -> RenderableType:
|
|
|
186
186
|
try:
|
|
187
187
|
json_dict = json.loads(arguments)
|
|
188
188
|
file_path = json_dict.get("file_path")
|
|
189
|
-
|
|
190
|
-
if isinstance(file_path, str):
|
|
191
|
-
abs_path = Path(file_path)
|
|
192
|
-
if not abs_path.is_absolute():
|
|
193
|
-
abs_path = (Path().cwd() / abs_path).resolve()
|
|
194
|
-
if abs_path.exists():
|
|
195
|
-
op_label = "Overwrite"
|
|
196
|
-
tool_name_column = Text.assemble(("→", ThemeKey.TOOL_MARK), " ", (op_label, ThemeKey.TOOL_NAME))
|
|
189
|
+
tool_name_column = Text.assemble(("→", ThemeKey.TOOL_MARK), " ", ("Write", ThemeKey.TOOL_NAME))
|
|
197
190
|
arguments_column = render_path(file_path, ThemeKey.TOOL_PARAM_FILE_PATH)
|
|
198
191
|
except json.JSONDecodeError:
|
|
199
192
|
tool_name_column = Text.assemble(("→", ThemeKey.TOOL_MARK), " ", ("Write", ThemeKey.TOOL_NAME))
|
|
@@ -386,6 +379,75 @@ def render_mermaid_tool_call(arguments: str) -> RenderableType:
|
|
|
386
379
|
return grid
|
|
387
380
|
|
|
388
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
|
+
|
|
389
451
|
def render_mermaid_tool_result(tr: events.ToolResultEvent) -> RenderableType:
|
|
390
452
|
from klaude_code.ui.terminal import supports_osc8_hyperlinks
|
|
391
453
|
|
|
@@ -416,16 +478,12 @@ def _extract_truncation(
|
|
|
416
478
|
|
|
417
479
|
def render_truncation_info(ui_extra: model.TruncationUIExtra) -> RenderableType:
|
|
418
480
|
"""Render truncation info for the user."""
|
|
419
|
-
original_kb = ui_extra.original_length / 1024
|
|
420
481
|
truncated_kb = ui_extra.truncated_length / 1024
|
|
482
|
+
|
|
421
483
|
text = Text.assemble(
|
|
422
|
-
("
|
|
423
|
-
(
|
|
424
|
-
("
|
|
425
|
-
(f"{truncated_kb:.1f}KB", ThemeKey.TOOL_RESULT_BOLD),
|
|
426
|
-
(" hidden\nFull output saved to ", ThemeKey.TOOL_RESULT),
|
|
427
|
-
(ui_extra.saved_file_path, ThemeKey.TOOL_RESULT),
|
|
428
|
-
("\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),
|
|
429
487
|
)
|
|
430
488
|
return Padding.indent(text, level=2)
|
|
431
489
|
|
|
@@ -455,8 +513,9 @@ _TOOL_ACTIVE_FORM: dict[str, str] = {
|
|
|
455
513
|
tools.SKILL: "Skilling",
|
|
456
514
|
tools.MERMAID: "Diagramming",
|
|
457
515
|
tools.MEMORY: "Memorizing",
|
|
458
|
-
tools.WEB_FETCH: "Fetching",
|
|
459
|
-
tools.
|
|
516
|
+
tools.WEB_FETCH: "Fetching Web",
|
|
517
|
+
tools.WEB_SEARCH: "Searching Web",
|
|
518
|
+
tools.REPORT_BACK: "Reporting",
|
|
460
519
|
}
|
|
461
520
|
|
|
462
521
|
|
|
@@ -512,6 +571,10 @@ def render_tool_call(e: events.ToolCallEvent) -> RenderableType | None:
|
|
|
512
571
|
return render_generic_tool_call(e.tool_name, e.arguments, "◈")
|
|
513
572
|
case tools.REPORT_BACK:
|
|
514
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)
|
|
515
578
|
case _:
|
|
516
579
|
return render_generic_tool_call(e.tool_name, e.arguments)
|
|
517
580
|
|
|
@@ -540,7 +603,7 @@ def render_tool_result(e: events.ToolResultEvent) -> RenderableType | None:
|
|
|
540
603
|
# Show truncation info if output was truncated and saved to file
|
|
541
604
|
truncation_info = get_truncation_info(e)
|
|
542
605
|
if truncation_info:
|
|
543
|
-
return render_truncation_info(truncation_info)
|
|
606
|
+
return Group(render_truncation_info(truncation_info), render_generic_tool_result(e.result))
|
|
544
607
|
|
|
545
608
|
diff_text = _extract_diff_text(e.ui_extra)
|
|
546
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
|
@@ -21,15 +21,20 @@ from klaude_code.ui.terminal.color import get_last_terminal_background_rgb
|
|
|
21
21
|
BREATHING_SPINNER_NAME = "dots"
|
|
22
22
|
|
|
23
23
|
# Alternating glyphs for the breathing spinner - switches at each "transparent" point
|
|
24
|
-
# All glyphs are center-symmetric (point-symmetric)
|
|
25
24
|
_BREATHING_SPINNER_GLYPHS_BASE = [
|
|
26
|
-
# Stars
|
|
27
25
|
"✦",
|
|
28
26
|
"✶",
|
|
29
27
|
"✲",
|
|
30
|
-
"⏺",
|
|
31
28
|
"◆",
|
|
32
29
|
"❖",
|
|
30
|
+
"✧",
|
|
31
|
+
"❋",
|
|
32
|
+
"✸",
|
|
33
|
+
"✻",
|
|
34
|
+
"◇",
|
|
35
|
+
"✴",
|
|
36
|
+
"✷",
|
|
37
|
+
"⟡",
|
|
33
38
|
]
|
|
34
39
|
|
|
35
40
|
# Shuffle glyphs on module load for variety across sessions
|
|
@@ -172,15 +177,34 @@ def _breathing_style(console: Console, base_style: Style, intensity: float) -> S
|
|
|
172
177
|
|
|
173
178
|
|
|
174
179
|
class ShimmerStatusText:
|
|
175
|
-
"""Renderable status line with shimmer effect on the main text and hint.
|
|
180
|
+
"""Renderable status line with shimmer effect on the main text and hint.
|
|
176
181
|
|
|
177
|
-
|
|
182
|
+
Supports optional right-aligned text that stays fixed at the right edge.
|
|
183
|
+
"""
|
|
184
|
+
|
|
185
|
+
def __init__(self, main_text: str | Text, main_style: ThemeKey, right_text: Text | None = None) -> None:
|
|
178
186
|
self._main_text = main_text if isinstance(main_text, Text) else Text(main_text)
|
|
179
187
|
self._main_style = main_style
|
|
180
188
|
self._hint_text = Text(const.STATUS_HINT_TEXT)
|
|
181
189
|
self._hint_style = ThemeKey.STATUS_HINT
|
|
190
|
+
self._right_text = right_text
|
|
182
191
|
|
|
183
192
|
def __rich_console__(self, console: Console, options: ConsoleOptions) -> RenderResult:
|
|
193
|
+
left_text = self._render_left_text(console)
|
|
194
|
+
|
|
195
|
+
if self._right_text is None:
|
|
196
|
+
yield left_text
|
|
197
|
+
return
|
|
198
|
+
|
|
199
|
+
# Use Table.grid to create left-right aligned layout
|
|
200
|
+
table = Table.grid(expand=True)
|
|
201
|
+
table.add_column(justify="left", ratio=1)
|
|
202
|
+
table.add_column(justify="right")
|
|
203
|
+
table.add_row(left_text, self._right_text)
|
|
204
|
+
yield table
|
|
205
|
+
|
|
206
|
+
def _render_left_text(self, console: Console) -> Text:
|
|
207
|
+
"""Render the left part with shimmer effect."""
|
|
184
208
|
result = Text()
|
|
185
209
|
main_style = console.get_style(str(self._main_style))
|
|
186
210
|
hint_style = console.get_style(str(self._hint_style))
|
|
@@ -198,7 +222,7 @@ class ShimmerStatusText:
|
|
|
198
222
|
style = _shimmer_style(console, base_style, intensity)
|
|
199
223
|
result.append(ch, style=style)
|
|
200
224
|
|
|
201
|
-
|
|
225
|
+
return result
|
|
202
226
|
|
|
203
227
|
|
|
204
228
|
def spinner_name() -> str:
|
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
|
),
|