klaude-code 1.2.6__py3-none-any.whl → 1.8.0__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/auth/__init__.py +24 -0
- klaude_code/auth/codex/__init__.py +20 -0
- klaude_code/auth/codex/exceptions.py +17 -0
- klaude_code/auth/codex/jwt_utils.py +45 -0
- klaude_code/auth/codex/oauth.py +229 -0
- klaude_code/auth/codex/token_manager.py +84 -0
- klaude_code/cli/auth_cmd.py +73 -0
- klaude_code/cli/config_cmd.py +91 -0
- klaude_code/cli/cost_cmd.py +338 -0
- klaude_code/cli/debug.py +78 -0
- klaude_code/cli/list_model.py +307 -0
- klaude_code/cli/main.py +233 -134
- klaude_code/cli/runtime.py +309 -117
- klaude_code/{version.py → cli/self_update.py} +114 -5
- klaude_code/cli/session_cmd.py +37 -21
- klaude_code/command/__init__.py +88 -27
- klaude_code/command/clear_cmd.py +8 -7
- klaude_code/command/command_abc.py +31 -31
- klaude_code/command/debug_cmd.py +79 -0
- klaude_code/command/export_cmd.py +19 -53
- klaude_code/command/export_online_cmd.py +154 -0
- klaude_code/command/fork_session_cmd.py +267 -0
- klaude_code/command/help_cmd.py +7 -8
- klaude_code/command/model_cmd.py +60 -10
- klaude_code/command/model_select.py +84 -0
- klaude_code/command/prompt-jj-describe.md +32 -0
- klaude_code/command/prompt_command.py +19 -11
- klaude_code/command/refresh_cmd.py +8 -10
- klaude_code/command/registry.py +139 -40
- klaude_code/command/release_notes_cmd.py +84 -0
- klaude_code/command/resume_cmd.py +111 -0
- klaude_code/command/status_cmd.py +104 -60
- klaude_code/command/terminal_setup_cmd.py +7 -9
- klaude_code/command/thinking_cmd.py +98 -0
- klaude_code/config/__init__.py +14 -6
- klaude_code/config/assets/__init__.py +1 -0
- klaude_code/config/assets/builtin_config.yaml +303 -0
- klaude_code/config/builtin_config.py +38 -0
- klaude_code/config/config.py +378 -109
- klaude_code/config/select_model.py +117 -53
- klaude_code/config/thinking.py +269 -0
- klaude_code/{const/__init__.py → const.py} +50 -19
- klaude_code/core/agent.py +20 -28
- klaude_code/core/executor.py +327 -112
- klaude_code/core/manager/__init__.py +2 -4
- klaude_code/core/manager/llm_clients.py +1 -15
- klaude_code/core/manager/llm_clients_builder.py +10 -11
- klaude_code/core/manager/sub_agent_manager.py +37 -6
- klaude_code/core/prompt.py +63 -44
- klaude_code/core/prompts/prompt-claude-code.md +2 -13
- klaude_code/core/prompts/prompt-codex-gpt-5-1-codex-max.md +117 -0
- klaude_code/core/prompts/prompt-codex-gpt-5-2-codex.md +117 -0
- klaude_code/core/prompts/prompt-codex.md +9 -42
- klaude_code/core/prompts/prompt-minimal.md +12 -0
- klaude_code/core/prompts/{prompt-subagent-explore.md → prompt-sub-agent-explore.md} +16 -3
- klaude_code/core/prompts/{prompt-subagent-oracle.md → prompt-sub-agent-oracle.md} +1 -2
- klaude_code/core/prompts/prompt-sub-agent-web.md +51 -0
- klaude_code/core/reminders.py +283 -95
- klaude_code/core/task.py +113 -75
- klaude_code/core/tool/__init__.py +24 -31
- klaude_code/core/tool/file/_utils.py +36 -0
- klaude_code/core/tool/file/apply_patch.py +17 -25
- klaude_code/core/tool/file/apply_patch_tool.py +57 -77
- klaude_code/core/tool/file/diff_builder.py +151 -0
- klaude_code/core/tool/file/edit_tool.py +50 -63
- klaude_code/core/tool/file/move_tool.md +41 -0
- klaude_code/core/tool/file/move_tool.py +435 -0
- klaude_code/core/tool/file/read_tool.md +1 -1
- klaude_code/core/tool/file/read_tool.py +86 -86
- klaude_code/core/tool/file/write_tool.py +59 -69
- klaude_code/core/tool/report_back_tool.py +84 -0
- klaude_code/core/tool/shell/bash_tool.py +265 -22
- klaude_code/core/tool/shell/command_safety.py +3 -6
- klaude_code/core/tool/{memory → skill}/skill_tool.py +16 -26
- klaude_code/core/tool/sub_agent_tool.py +13 -2
- klaude_code/core/tool/todo/todo_write_tool.md +0 -157
- klaude_code/core/tool/todo/todo_write_tool.py +1 -1
- klaude_code/core/tool/todo/todo_write_tool_raw.md +182 -0
- klaude_code/core/tool/todo/update_plan_tool.py +1 -1
- klaude_code/core/tool/tool_abc.py +18 -0
- klaude_code/core/tool/tool_context.py +27 -12
- klaude_code/core/tool/tool_registry.py +7 -7
- klaude_code/core/tool/tool_runner.py +44 -36
- klaude_code/core/tool/truncation.py +29 -14
- klaude_code/core/tool/web/mermaid_tool.md +43 -0
- klaude_code/core/tool/web/mermaid_tool.py +2 -5
- klaude_code/core/tool/web/web_fetch_tool.md +1 -1
- klaude_code/core/tool/web/web_fetch_tool.py +112 -22
- klaude_code/core/tool/web/web_search_tool.md +23 -0
- klaude_code/core/tool/web/web_search_tool.py +130 -0
- klaude_code/core/turn.py +168 -66
- klaude_code/llm/__init__.py +2 -10
- klaude_code/llm/anthropic/client.py +190 -178
- klaude_code/llm/anthropic/input.py +39 -15
- klaude_code/llm/bedrock/__init__.py +3 -0
- klaude_code/llm/bedrock/client.py +60 -0
- klaude_code/llm/client.py +7 -21
- klaude_code/llm/codex/__init__.py +5 -0
- klaude_code/llm/codex/client.py +149 -0
- klaude_code/llm/google/__init__.py +3 -0
- klaude_code/llm/google/client.py +309 -0
- klaude_code/llm/google/input.py +215 -0
- klaude_code/llm/input_common.py +3 -9
- klaude_code/llm/openai_compatible/client.py +72 -164
- klaude_code/llm/openai_compatible/input.py +6 -4
- klaude_code/llm/openai_compatible/stream.py +273 -0
- klaude_code/llm/openai_compatible/tool_call_accumulator.py +17 -1
- klaude_code/llm/openrouter/client.py +89 -160
- klaude_code/llm/openrouter/input.py +18 -30
- klaude_code/llm/openrouter/reasoning.py +118 -0
- klaude_code/llm/registry.py +39 -7
- klaude_code/llm/responses/client.py +184 -171
- klaude_code/llm/responses/input.py +20 -1
- klaude_code/llm/usage.py +17 -12
- klaude_code/protocol/commands.py +17 -1
- klaude_code/protocol/events.py +31 -4
- klaude_code/protocol/llm_param.py +13 -10
- klaude_code/protocol/model.py +232 -29
- klaude_code/protocol/op.py +90 -1
- klaude_code/protocol/op_handler.py +35 -1
- klaude_code/protocol/sub_agent/__init__.py +117 -0
- klaude_code/protocol/sub_agent/explore.py +63 -0
- klaude_code/protocol/sub_agent/oracle.py +91 -0
- klaude_code/protocol/sub_agent/task.py +61 -0
- klaude_code/protocol/sub_agent/web.py +79 -0
- klaude_code/protocol/tools.py +4 -2
- klaude_code/session/__init__.py +2 -2
- klaude_code/session/codec.py +71 -0
- klaude_code/session/export.py +293 -86
- klaude_code/session/selector.py +89 -67
- klaude_code/session/session.py +320 -309
- klaude_code/session/store.py +220 -0
- klaude_code/session/templates/export_session.html +595 -83
- klaude_code/session/templates/mermaid_viewer.html +926 -0
- klaude_code/skill/__init__.py +27 -0
- klaude_code/skill/assets/deslop/SKILL.md +17 -0
- klaude_code/skill/assets/dev-docs/SKILL.md +108 -0
- klaude_code/skill/assets/handoff/SKILL.md +39 -0
- klaude_code/skill/assets/jj-workspace/SKILL.md +20 -0
- klaude_code/skill/assets/skill-creator/SKILL.md +139 -0
- klaude_code/{core/tool/memory/skill_loader.py → skill/loader.py} +55 -15
- klaude_code/skill/manager.py +70 -0
- klaude_code/skill/system_skills.py +192 -0
- klaude_code/trace/__init__.py +20 -2
- klaude_code/trace/log.py +150 -5
- klaude_code/ui/__init__.py +4 -9
- klaude_code/ui/core/input.py +1 -1
- klaude_code/ui/core/stage_manager.py +7 -7
- klaude_code/ui/modes/debug/display.py +2 -1
- klaude_code/ui/modes/repl/__init__.py +3 -48
- klaude_code/ui/modes/repl/clipboard.py +5 -5
- klaude_code/ui/modes/repl/completers.py +487 -123
- klaude_code/ui/modes/repl/display.py +5 -4
- klaude_code/ui/modes/repl/event_handler.py +370 -117
- klaude_code/ui/modes/repl/input_prompt_toolkit.py +552 -105
- klaude_code/ui/modes/repl/key_bindings.py +146 -23
- klaude_code/ui/modes/repl/renderer.py +189 -99
- klaude_code/ui/renderers/assistant.py +9 -2
- klaude_code/ui/renderers/bash_syntax.py +178 -0
- klaude_code/ui/renderers/common.py +78 -0
- klaude_code/ui/renderers/developer.py +104 -48
- klaude_code/ui/renderers/diffs.py +87 -6
- klaude_code/ui/renderers/errors.py +11 -6
- klaude_code/ui/renderers/mermaid_viewer.py +57 -0
- klaude_code/ui/renderers/metadata.py +112 -76
- klaude_code/ui/renderers/sub_agent.py +92 -7
- klaude_code/ui/renderers/thinking.py +40 -18
- klaude_code/ui/renderers/tools.py +405 -227
- klaude_code/ui/renderers/user_input.py +73 -13
- klaude_code/ui/rich/__init__.py +10 -1
- klaude_code/ui/rich/cjk_wrap.py +228 -0
- klaude_code/ui/rich/code_panel.py +131 -0
- klaude_code/ui/rich/live.py +17 -0
- klaude_code/ui/rich/markdown.py +305 -170
- klaude_code/ui/rich/searchable_text.py +10 -13
- klaude_code/ui/rich/status.py +190 -49
- klaude_code/ui/rich/theme.py +135 -39
- klaude_code/ui/terminal/__init__.py +55 -0
- klaude_code/ui/terminal/color.py +1 -1
- klaude_code/ui/terminal/control.py +13 -22
- klaude_code/ui/terminal/notifier.py +44 -4
- klaude_code/ui/terminal/selector.py +658 -0
- klaude_code/ui/utils/common.py +0 -18
- klaude_code-1.8.0.dist-info/METADATA +377 -0
- klaude_code-1.8.0.dist-info/RECORD +219 -0
- {klaude_code-1.2.6.dist-info → klaude_code-1.8.0.dist-info}/entry_points.txt +1 -0
- klaude_code/command/diff_cmd.py +0 -138
- klaude_code/command/prompt-dev-docs-update.md +0 -56
- klaude_code/command/prompt-dev-docs.md +0 -46
- klaude_code/config/list_model.py +0 -162
- klaude_code/core/manager/agent_manager.py +0 -127
- klaude_code/core/prompts/prompt-subagent-webfetch.md +0 -46
- klaude_code/core/tool/file/multi_edit_tool.md +0 -42
- klaude_code/core/tool/file/multi_edit_tool.py +0 -199
- klaude_code/core/tool/memory/memory_tool.md +0 -16
- klaude_code/core/tool/memory/memory_tool.py +0 -462
- klaude_code/llm/openrouter/reasoning_handler.py +0 -209
- klaude_code/protocol/sub_agent.py +0 -348
- klaude_code/ui/utils/debouncer.py +0 -42
- klaude_code-1.2.6.dist-info/METADATA +0 -178
- klaude_code-1.2.6.dist-info/RECORD +0 -167
- /klaude_code/core/prompts/{prompt-subagent.md → prompt-sub-agent.md} +0 -0
- /klaude_code/core/tool/{memory → skill}/__init__.py +0 -0
- /klaude_code/core/tool/{memory → skill}/skill_tool.md +0 -0
- {klaude_code-1.2.6.dist-info → klaude_code-1.8.0.dist-info}/WHEEL +0 -0
|
@@ -1,39 +1,84 @@
|
|
|
1
1
|
import re
|
|
2
|
+
from collections.abc import Callable
|
|
2
3
|
|
|
3
4
|
from rich.console import Group, RenderableType
|
|
4
5
|
from rich.text import Text
|
|
5
6
|
|
|
6
|
-
from klaude_code.
|
|
7
|
+
from klaude_code.skill import get_available_skills
|
|
7
8
|
from klaude_code.ui.renderers.common import create_grid
|
|
8
9
|
from klaude_code.ui.rich.theme import ThemeKey
|
|
9
10
|
|
|
11
|
+
# Module-level command name checker. Set by cli/runtime.py on startup.
|
|
12
|
+
_command_name_checker: Callable[[str], bool] | None = None
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def set_command_name_checker(checker: Callable[[str], bool]) -> None:
|
|
16
|
+
"""Set the command name validation function (called from runtime layer)."""
|
|
17
|
+
global _command_name_checker
|
|
18
|
+
_command_name_checker = checker
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def is_slash_command_name(name: str) -> bool:
|
|
22
|
+
"""Check if name is a valid slash command using the injected checker."""
|
|
23
|
+
if _command_name_checker is None:
|
|
24
|
+
return False
|
|
25
|
+
return _command_name_checker(name)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# Match @-file patterns only when they appear at the beginning of the line
|
|
29
|
+
# or immediately after whitespace, to avoid treating mid-word email-like
|
|
30
|
+
# patterns such as foo@bar.com as file references.
|
|
31
|
+
AT_FILE_RENDER_PATTERN = re.compile(r'(?<!\S)@("([^"]+)"|\S+)')
|
|
32
|
+
|
|
33
|
+
# Match $skill or ¥skill pattern at the beginning of the first line
|
|
34
|
+
SKILL_RENDER_PATTERN = re.compile(r"^[$¥](\S+)")
|
|
35
|
+
|
|
36
|
+
USER_MESSAGE_MARK = "❯ "
|
|
37
|
+
|
|
10
38
|
|
|
11
39
|
def render_at_pattern(
|
|
12
40
|
text: str,
|
|
13
41
|
at_style: str = ThemeKey.USER_INPUT_AT_PATTERN,
|
|
14
42
|
other_style: str = ThemeKey.USER_INPUT,
|
|
15
43
|
) -> Text:
|
|
16
|
-
if "@" in text:
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
44
|
+
if "@" not in text:
|
|
45
|
+
return Text(text, style=other_style)
|
|
46
|
+
|
|
47
|
+
result = Text("")
|
|
48
|
+
last_end = 0
|
|
49
|
+
for match in AT_FILE_RENDER_PATTERN.finditer(text):
|
|
50
|
+
start, end = match.span()
|
|
51
|
+
if start > last_end:
|
|
52
|
+
# Text before the @-pattern
|
|
53
|
+
result.append_text(Text(text[last_end:start], other_style))
|
|
54
|
+
# The @-pattern itself (e.g. @path or @"path with spaces")
|
|
55
|
+
result.append_text(Text(text[start:end], at_style))
|
|
56
|
+
last_end = end
|
|
57
|
+
|
|
58
|
+
if last_end < len(text):
|
|
59
|
+
result.append_text(Text(text[last_end:], other_style))
|
|
60
|
+
|
|
61
|
+
return result
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _is_valid_skill_name(name: str) -> bool:
|
|
65
|
+
"""Check if a skill name is valid (exists in loaded skills)."""
|
|
66
|
+
short = name.split(":")[-1] if ":" in name else name
|
|
67
|
+
available_skills = get_available_skills()
|
|
68
|
+
return any(skill_name in (name, short) for skill_name, _, _ in available_skills)
|
|
26
69
|
|
|
27
70
|
|
|
28
71
|
def render_user_input(content: str) -> RenderableType:
|
|
29
72
|
"""Render a user message as a group of quoted lines with styles.
|
|
30
73
|
|
|
31
74
|
- Highlights slash command on the first line if recognized
|
|
75
|
+
- Highlights $skill pattern on the first line if recognized
|
|
32
76
|
- Highlights @file patterns in all lines
|
|
33
77
|
"""
|
|
34
78
|
lines = content.strip().split("\n")
|
|
35
79
|
renderables: list[RenderableType] = []
|
|
36
80
|
has_command = False
|
|
81
|
+
command_style: str | None = None
|
|
37
82
|
for i, line in enumerate(lines):
|
|
38
83
|
line_text = render_at_pattern(line)
|
|
39
84
|
|
|
@@ -41,6 +86,7 @@ def render_user_input(content: str) -> RenderableType:
|
|
|
41
86
|
splits = line.split(" ", maxsplit=1)
|
|
42
87
|
if is_slash_command_name(splits[0][1:]):
|
|
43
88
|
has_command = True
|
|
89
|
+
command_style = ThemeKey.USER_INPUT_SLASH_COMMAND
|
|
44
90
|
line_text = Text.assemble(
|
|
45
91
|
(f"{splits[0]}", ThemeKey.USER_INPUT_SLASH_COMMAND),
|
|
46
92
|
" ",
|
|
@@ -49,13 +95,27 @@ def render_user_input(content: str) -> RenderableType:
|
|
|
49
95
|
renderables.append(line_text)
|
|
50
96
|
continue
|
|
51
97
|
|
|
98
|
+
if i == 0 and (line.startswith("$") or line.startswith("¥")):
|
|
99
|
+
m = SKILL_RENDER_PATTERN.match(line)
|
|
100
|
+
if m and _is_valid_skill_name(m.group(1)):
|
|
101
|
+
has_command = True
|
|
102
|
+
command_style = ThemeKey.USER_INPUT_SKILL
|
|
103
|
+
skill_token = m.group(0) # e.g. "$skill-name"
|
|
104
|
+
rest = line[len(skill_token) :]
|
|
105
|
+
line_text = Text.assemble(
|
|
106
|
+
(skill_token, ThemeKey.USER_INPUT_SKILL),
|
|
107
|
+
render_at_pattern(rest) if rest else Text(""),
|
|
108
|
+
)
|
|
109
|
+
renderables.append(line_text)
|
|
110
|
+
continue
|
|
111
|
+
|
|
52
112
|
renderables.append(line_text)
|
|
53
113
|
grid = create_grid()
|
|
54
114
|
grid.padding = (0, 0)
|
|
55
115
|
mark = (
|
|
56
|
-
Text(
|
|
116
|
+
Text(USER_MESSAGE_MARK, style=ThemeKey.USER_INPUT_PROMPT)
|
|
57
117
|
if not has_command
|
|
58
|
-
else Text(" ", style=ThemeKey.USER_INPUT_SLASH_COMMAND)
|
|
118
|
+
else Text(" ", style=command_style or ThemeKey.USER_INPUT_SLASH_COMMAND)
|
|
59
119
|
)
|
|
60
120
|
grid.add_row(mark, Group(*renderables))
|
|
61
121
|
return grid
|
klaude_code/ui/rich/__init__.py
CHANGED
|
@@ -1 +1,10 @@
|
|
|
1
|
-
|
|
1
|
+
"""Rich rendering utilities.
|
|
2
|
+
|
|
3
|
+
This package installs a small monkey-patch that improves CJK line breaking in Rich.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from .cjk_wrap import install_rich_cjk_wrap_patch
|
|
9
|
+
|
|
10
|
+
install_rich_cjk_wrap_patch()
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
"""Monkey-patch Rich wrapping for better CJK line breaks."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import unicodedata
|
|
6
|
+
from collections.abc import Callable
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _is_cjk_char(ch: str) -> bool:
|
|
10
|
+
return unicodedata.east_asian_width(ch) in ("W", "F")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _contains_cjk(text: str) -> bool:
|
|
14
|
+
return any(_is_cjk_char(ch) for ch in text)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _is_ascii_word_char(ch: str) -> bool:
|
|
18
|
+
o = ord(ch)
|
|
19
|
+
return (48 <= o <= 57) or (65 <= o <= 90) or (97 <= o <= 122) or ch in "_."
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _find_prefix_len_for_remaining(word: str, remaining_space: int) -> int:
|
|
23
|
+
"""Find a prefix length (in chars) that fits remaining_space.
|
|
24
|
+
|
|
25
|
+
This prefers breakpoints that don't split ASCII word-like runs.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
if remaining_space <= 0:
|
|
29
|
+
return 0
|
|
30
|
+
|
|
31
|
+
# Local import keeps import-time overhead low.
|
|
32
|
+
from rich.cells import get_character_cell_size
|
|
33
|
+
|
|
34
|
+
total = 0
|
|
35
|
+
best = 0
|
|
36
|
+
n = len(word)
|
|
37
|
+
|
|
38
|
+
for i, ch in enumerate(word):
|
|
39
|
+
total += get_character_cell_size(ch)
|
|
40
|
+
if total > remaining_space:
|
|
41
|
+
break
|
|
42
|
+
|
|
43
|
+
boundary = i + 1
|
|
44
|
+
if boundary >= n:
|
|
45
|
+
best = boundary
|
|
46
|
+
break
|
|
47
|
+
|
|
48
|
+
# Avoid leaving a path separator at the start of the next line.
|
|
49
|
+
if word[boundary] in "/":
|
|
50
|
+
continue
|
|
51
|
+
|
|
52
|
+
# Disallow breaks inside ASCII word runs: ...a|b...
|
|
53
|
+
if _is_ascii_word_char(word[boundary - 1]) and _is_ascii_word_char(word[boundary]):
|
|
54
|
+
continue
|
|
55
|
+
|
|
56
|
+
best = boundary
|
|
57
|
+
|
|
58
|
+
return best
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
_rich_cjk_wrap_patch_installed = False
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def install_rich_cjk_wrap_patch() -> bool:
|
|
65
|
+
"""Install a monkey-patch that improves CJK line wrapping in Rich.
|
|
66
|
+
|
|
67
|
+
Rich wraps text by tokenizing on whitespace, which causes long CJK runs to be
|
|
68
|
+
treated as a single "word" and moved to the next line wholesale.
|
|
69
|
+
|
|
70
|
+
This patch keeps ASCII word wrapping behaviour intact, but allows breaking
|
|
71
|
+
CJK-containing tokens at the end of a line to fill remaining space.
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
True if the patch was installed in this process.
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
global _rich_cjk_wrap_patch_installed
|
|
78
|
+
if _rich_cjk_wrap_patch_installed:
|
|
79
|
+
return False
|
|
80
|
+
|
|
81
|
+
import rich._wrap as _wrap
|
|
82
|
+
import rich.text as _text
|
|
83
|
+
from rich._loop import loop_last
|
|
84
|
+
from rich.cells import cell_len, chop_cells
|
|
85
|
+
|
|
86
|
+
_OPEN_TO_CLOSE = {
|
|
87
|
+
"(": ")",
|
|
88
|
+
"(": ")",
|
|
89
|
+
"[": "]",
|
|
90
|
+
"{": "}",
|
|
91
|
+
"“": "”",
|
|
92
|
+
"‘": "’",
|
|
93
|
+
"《": "》",
|
|
94
|
+
"〈": "〉",
|
|
95
|
+
"「": "」",
|
|
96
|
+
"『": "』",
|
|
97
|
+
"【": "】",
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
def _leading_unclosed_delim(word: str) -> str | None:
|
|
101
|
+
stripped = word.lstrip()
|
|
102
|
+
if not stripped:
|
|
103
|
+
return None
|
|
104
|
+
|
|
105
|
+
close_delim = _OPEN_TO_CLOSE.get(stripped[0])
|
|
106
|
+
if close_delim is None:
|
|
107
|
+
return None
|
|
108
|
+
|
|
109
|
+
if close_delim in stripped:
|
|
110
|
+
return None
|
|
111
|
+
|
|
112
|
+
return close_delim
|
|
113
|
+
|
|
114
|
+
def _close_delim_appears_soon(
|
|
115
|
+
word_tokens: list[str],
|
|
116
|
+
*,
|
|
117
|
+
start_index: int,
|
|
118
|
+
close_delim: str,
|
|
119
|
+
max_chars: int = 32,
|
|
120
|
+
max_tokens: int = 4,
|
|
121
|
+
) -> bool:
|
|
122
|
+
consumed = 0
|
|
123
|
+
for token in word_tokens[start_index + 1 : start_index + 1 + max_tokens]:
|
|
124
|
+
if not token:
|
|
125
|
+
continue
|
|
126
|
+
|
|
127
|
+
close_pos = token.find(close_delim)
|
|
128
|
+
if close_pos != -1 and (consumed + close_pos) < max_chars:
|
|
129
|
+
return True
|
|
130
|
+
|
|
131
|
+
consumed += len(token)
|
|
132
|
+
if consumed >= max_chars:
|
|
133
|
+
return False
|
|
134
|
+
|
|
135
|
+
return False
|
|
136
|
+
|
|
137
|
+
def divide_line_patched(text: str, width: int, fold: bool = True) -> list[int]:
|
|
138
|
+
break_positions: list[int] = []
|
|
139
|
+
|
|
140
|
+
def append(pos: int) -> None:
|
|
141
|
+
if pos and (not break_positions or break_positions[-1] != pos):
|
|
142
|
+
break_positions.append(pos)
|
|
143
|
+
|
|
144
|
+
cell_offset = 0
|
|
145
|
+
_cell_len: Callable[[str], int] = cell_len
|
|
146
|
+
|
|
147
|
+
words = list(_wrap.words(text))
|
|
148
|
+
word_tokens = [w for _s, _e, w in words]
|
|
149
|
+
|
|
150
|
+
for index, (start, _end, word) in enumerate(words):
|
|
151
|
+
next_word: str | None = None
|
|
152
|
+
if index + 1 < len(words):
|
|
153
|
+
next_word = words[index + 1][2]
|
|
154
|
+
|
|
155
|
+
# Heuristic: avoid leaving an unclosed opening delimiter fragment (e.g. "(Deep ")
|
|
156
|
+
# at the end of a line when the next token will wrap.
|
|
157
|
+
word_length = _cell_len(word.rstrip())
|
|
158
|
+
remaining_space = width - cell_offset
|
|
159
|
+
if remaining_space >= word_length and cell_offset and start and next_word is not None:
|
|
160
|
+
cell_offset_with_trailing = cell_offset + _cell_len(word)
|
|
161
|
+
next_length = _cell_len(next_word.rstrip())
|
|
162
|
+
next_will_wrap = next_length > width or (width - cell_offset_with_trailing) < next_length
|
|
163
|
+
|
|
164
|
+
close_delim = _leading_unclosed_delim(word)
|
|
165
|
+
if close_delim is not None and next_will_wrap:
|
|
166
|
+
stripped = word.strip()
|
|
167
|
+
if _cell_len(stripped) <= 16 and _close_delim_appears_soon(
|
|
168
|
+
word_tokens, start_index=index, close_delim=close_delim
|
|
169
|
+
):
|
|
170
|
+
append(start)
|
|
171
|
+
cell_offset = _cell_len(word)
|
|
172
|
+
continue
|
|
173
|
+
|
|
174
|
+
while True:
|
|
175
|
+
word_length = _cell_len(word.rstrip())
|
|
176
|
+
remaining_space = width - cell_offset
|
|
177
|
+
|
|
178
|
+
if remaining_space >= word_length:
|
|
179
|
+
cell_offset += _cell_len(word)
|
|
180
|
+
break
|
|
181
|
+
|
|
182
|
+
# Prefer splitting CJK-containing tokens to fill remaining space.
|
|
183
|
+
if fold and cell_offset and start and remaining_space > 0 and _contains_cjk(word):
|
|
184
|
+
prefix_len = _find_prefix_len_for_remaining(word, remaining_space)
|
|
185
|
+
if prefix_len:
|
|
186
|
+
break_at = start + prefix_len
|
|
187
|
+
append(break_at)
|
|
188
|
+
word = word[prefix_len:]
|
|
189
|
+
start = break_at
|
|
190
|
+
|
|
191
|
+
# If the remainder fits on the next (empty) line, keep Rich's
|
|
192
|
+
# existing behaviour and move on.
|
|
193
|
+
if _cell_len(word.rstrip()) <= width:
|
|
194
|
+
cell_offset = _cell_len(word)
|
|
195
|
+
break
|
|
196
|
+
|
|
197
|
+
# Otherwise, continue folding the remainder starting on a new line.
|
|
198
|
+
cell_offset = 0
|
|
199
|
+
continue
|
|
200
|
+
|
|
201
|
+
# Fall back to Rich's original logic.
|
|
202
|
+
if word_length > width:
|
|
203
|
+
if fold:
|
|
204
|
+
folded_word = chop_cells(word, width=width)
|
|
205
|
+
for last, line in loop_last(folded_word):
|
|
206
|
+
if start:
|
|
207
|
+
append(start)
|
|
208
|
+
if last:
|
|
209
|
+
cell_offset = _cell_len(line)
|
|
210
|
+
else:
|
|
211
|
+
start += len(line)
|
|
212
|
+
else:
|
|
213
|
+
if start:
|
|
214
|
+
append(start)
|
|
215
|
+
cell_offset = _cell_len(word)
|
|
216
|
+
break
|
|
217
|
+
|
|
218
|
+
if cell_offset and start:
|
|
219
|
+
append(start)
|
|
220
|
+
cell_offset = _cell_len(word)
|
|
221
|
+
break
|
|
222
|
+
|
|
223
|
+
return break_positions
|
|
224
|
+
|
|
225
|
+
_wrap.divide_line = divide_line_patched # pyright: ignore[reportPrivateImportUsage]
|
|
226
|
+
_text.divide_line = divide_line_patched # pyright: ignore[reportPrivateImportUsage]
|
|
227
|
+
_rich_cjk_wrap_patch_installed = True
|
|
228
|
+
return True
|
|
@@ -0,0 +1,131 @@
|
|
|
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.cells import cell_len
|
|
8
|
+
from rich.console import ConsoleRenderable, RichCast
|
|
9
|
+
from rich.jupyter import JupyterMixin
|
|
10
|
+
from rich.measure import Measurement
|
|
11
|
+
from rich.segment import Segment
|
|
12
|
+
from rich.style import StyleType
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from rich.console import Console, ConsoleOptions, RenderResult
|
|
16
|
+
|
|
17
|
+
# Box drawing characters
|
|
18
|
+
TOP_LEFT = "┌" # ┌
|
|
19
|
+
TOP_RIGHT = "┐" # ┐
|
|
20
|
+
BOTTOM_LEFT = "└" # └
|
|
21
|
+
BOTTOM_RIGHT = "┘" # ┘
|
|
22
|
+
HORIZONTAL = "─" # ─
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class CodePanel(JupyterMixin):
|
|
26
|
+
"""A panel with only top and bottom borders, no left/right borders.
|
|
27
|
+
|
|
28
|
+
This is designed for code blocks where you want easy copy-paste without
|
|
29
|
+
picking up border characters on the sides.
|
|
30
|
+
|
|
31
|
+
Example:
|
|
32
|
+
>>> console.print(CodePanel(Syntax(code, "python")))
|
|
33
|
+
|
|
34
|
+
Renders as:
|
|
35
|
+
┌──────────────────────────┐
|
|
36
|
+
code line 1
|
|
37
|
+
code line 2
|
|
38
|
+
└──────────────────────────┘
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
def __init__(
|
|
42
|
+
self,
|
|
43
|
+
renderable: ConsoleRenderable | RichCast | str,
|
|
44
|
+
*,
|
|
45
|
+
border_style: StyleType = "none",
|
|
46
|
+
expand: bool = False,
|
|
47
|
+
padding: int = 1,
|
|
48
|
+
) -> None:
|
|
49
|
+
"""Initialize the CodePanel.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
renderable: A console renderable object.
|
|
53
|
+
border_style: The style of the border. Defaults to "none".
|
|
54
|
+
expand: If True, expand to fill available width. Defaults to False.
|
|
55
|
+
padding: Left/right padding for content. Defaults to 1.
|
|
56
|
+
"""
|
|
57
|
+
self.renderable = renderable
|
|
58
|
+
self.border_style = border_style
|
|
59
|
+
self.expand = expand
|
|
60
|
+
self.padding = padding
|
|
61
|
+
|
|
62
|
+
@staticmethod
|
|
63
|
+
def _measure_max_line_cells(lines: list[list[Segment]]) -> int:
|
|
64
|
+
max_cells = 0
|
|
65
|
+
for line in lines:
|
|
66
|
+
plain = "".join(segment.text for segment in line).rstrip()
|
|
67
|
+
max_cells = max(max_cells, cell_len(plain))
|
|
68
|
+
return max_cells
|
|
69
|
+
|
|
70
|
+
def __rich_console__(self, console: Console, options: ConsoleOptions) -> RenderResult:
|
|
71
|
+
border_style = console.get_style(self.border_style)
|
|
72
|
+
max_width = options.max_width
|
|
73
|
+
pad = self.padding
|
|
74
|
+
|
|
75
|
+
max_content_width = max(max_width - pad * 2, 1)
|
|
76
|
+
|
|
77
|
+
# Measure the content width (account for padding)
|
|
78
|
+
if self.expand:
|
|
79
|
+
content_width = max_content_width
|
|
80
|
+
else:
|
|
81
|
+
probe_options = options.update(width=max_content_width)
|
|
82
|
+
probe_lines = console.render_lines(self.renderable, probe_options, pad=False)
|
|
83
|
+
content_width = self._measure_max_line_cells(probe_lines)
|
|
84
|
+
content_width = max(1, min(content_width, max_content_width))
|
|
85
|
+
|
|
86
|
+
# Render content lines
|
|
87
|
+
child_options = options.update(width=content_width)
|
|
88
|
+
lines = console.render_lines(self.renderable, child_options)
|
|
89
|
+
|
|
90
|
+
# Calculate border width based on content width + padding
|
|
91
|
+
border_width = content_width + pad * 2
|
|
92
|
+
|
|
93
|
+
new_line = Segment.line()
|
|
94
|
+
pad_segment = Segment(" " * pad) if pad > 0 else None
|
|
95
|
+
|
|
96
|
+
# Top border: ┌───...───┐
|
|
97
|
+
top_border = (
|
|
98
|
+
TOP_LEFT + (HORIZONTAL * (border_width - 2)) + TOP_RIGHT if border_width >= 2 else HORIZONTAL * border_width
|
|
99
|
+
)
|
|
100
|
+
yield Segment(top_border, border_style)
|
|
101
|
+
yield new_line
|
|
102
|
+
|
|
103
|
+
# Content lines with padding
|
|
104
|
+
for line in lines:
|
|
105
|
+
if pad_segment:
|
|
106
|
+
yield pad_segment
|
|
107
|
+
yield from line
|
|
108
|
+
if pad_segment:
|
|
109
|
+
yield pad_segment
|
|
110
|
+
yield new_line
|
|
111
|
+
|
|
112
|
+
# Bottom border: └───...───┘
|
|
113
|
+
bottom_border = (
|
|
114
|
+
BOTTOM_LEFT + (HORIZONTAL * (border_width - 2)) + BOTTOM_RIGHT
|
|
115
|
+
if border_width >= 2
|
|
116
|
+
else HORIZONTAL * border_width
|
|
117
|
+
)
|
|
118
|
+
yield Segment(bottom_border, border_style)
|
|
119
|
+
yield new_line
|
|
120
|
+
|
|
121
|
+
def __rich_measure__(self, console: Console, options: ConsoleOptions) -> Measurement:
|
|
122
|
+
if self.expand:
|
|
123
|
+
return Measurement(options.max_width, options.max_width)
|
|
124
|
+
max_width = options.max_width
|
|
125
|
+
max_content_width = max(max_width - self.padding * 2, 1)
|
|
126
|
+
probe_options = options.update(width=max_content_width)
|
|
127
|
+
probe_lines = console.render_lines(self.renderable, probe_options, pad=False)
|
|
128
|
+
content_width = self._measure_max_line_cells(probe_lines)
|
|
129
|
+
content_width = max(1, min(content_width, max_content_width))
|
|
130
|
+
width = content_width + self.padding * 2
|
|
131
|
+
return Measurement(width, width)
|
klaude_code/ui/rich/live.py
CHANGED
|
@@ -63,3 +63,20 @@ class CropAboveLive(Live):
|
|
|
63
63
|
|
|
64
64
|
def update(self, renderable: RenderableType, refresh: bool = True) -> None: # type: ignore[override]
|
|
65
65
|
super().update(CropAbove(renderable, style=self._crop_style), refresh=refresh)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class SingleLine:
|
|
69
|
+
"""Render only the first line of a renderable.
|
|
70
|
+
|
|
71
|
+
This is used to ensure dynamic UI elements (spinners / status) never wrap
|
|
72
|
+
to multiple lines, which would appear as a vertical "jump".
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
def __init__(self, renderable: RenderableType) -> None:
|
|
76
|
+
self.renderable = renderable
|
|
77
|
+
|
|
78
|
+
def __rich_console__(self, console: Console, options: ConsoleOptions) -> RenderResult:
|
|
79
|
+
line_options = options.update(no_wrap=True, overflow="ellipsis", height=1)
|
|
80
|
+
lines = console.render_lines(self.renderable, line_options, pad=False)
|
|
81
|
+
if lines:
|
|
82
|
+
yield from lines[0]
|