klaude-code 2.9.0__py3-none-any.whl → 2.10.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/app/runtime.py +1 -1
- klaude_code/auth/antigravity/oauth.py +33 -29
- klaude_code/auth/claude/oauth.py +34 -49
- klaude_code/cli/cost_cmd.py +4 -4
- klaude_code/cli/list_model.py +1 -2
- klaude_code/config/assets/builtin_config.yaml +17 -0
- klaude_code/const.py +4 -3
- klaude_code/core/agent_profile.py +2 -5
- klaude_code/core/bash_mode.py +276 -0
- klaude_code/core/executor.py +40 -7
- klaude_code/core/manager/llm_clients.py +1 -0
- klaude_code/core/manager/llm_clients_builder.py +2 -2
- klaude_code/core/memory.py +140 -0
- klaude_code/core/reminders.py +17 -89
- klaude_code/core/task.py +1 -1
- klaude_code/core/tool/file/read_tool.py +13 -2
- klaude_code/core/tool/shell/bash_tool.py +1 -1
- klaude_code/core/turn.py +10 -4
- klaude_code/llm/bedrock_anthropic/__init__.py +3 -0
- klaude_code/llm/input_common.py +18 -0
- klaude_code/llm/{codex → openai_codex}/__init__.py +1 -1
- klaude_code/llm/{codex → openai_codex}/client.py +3 -3
- klaude_code/llm/openai_compatible/client.py +3 -1
- klaude_code/llm/openai_compatible/stream.py +19 -9
- klaude_code/llm/{responses → openai_responses}/client.py +1 -1
- klaude_code/llm/registry.py +3 -3
- klaude_code/llm/stream_parts.py +3 -1
- klaude_code/llm/usage.py +1 -1
- klaude_code/protocol/events.py +17 -1
- klaude_code/protocol/message.py +1 -0
- klaude_code/protocol/model.py +14 -1
- klaude_code/protocol/op.py +12 -0
- klaude_code/protocol/op_handler.py +5 -0
- klaude_code/session/session.py +22 -1
- klaude_code/tui/command/resume_cmd.py +1 -1
- klaude_code/tui/commands.py +15 -0
- klaude_code/tui/components/bash_syntax.py +4 -0
- klaude_code/tui/components/command_output.py +4 -5
- klaude_code/tui/components/developer.py +1 -3
- klaude_code/tui/components/diffs.py +3 -2
- klaude_code/tui/components/metadata.py +23 -26
- klaude_code/tui/components/rich/code_panel.py +31 -16
- klaude_code/tui/components/rich/markdown.py +44 -28
- klaude_code/tui/components/rich/status.py +2 -2
- klaude_code/tui/components/rich/theme.py +28 -16
- klaude_code/tui/components/tools.py +23 -0
- klaude_code/tui/components/user_input.py +49 -58
- klaude_code/tui/components/welcome.py +47 -2
- klaude_code/tui/display.py +15 -7
- klaude_code/tui/input/completers.py +8 -0
- klaude_code/tui/input/key_bindings.py +37 -1
- klaude_code/tui/input/prompt_toolkit.py +58 -31
- klaude_code/tui/machine.py +87 -49
- klaude_code/tui/renderer.py +148 -30
- klaude_code/tui/runner.py +22 -0
- klaude_code/tui/terminal/image.py +24 -3
- klaude_code/tui/terminal/notifier.py +11 -12
- klaude_code/tui/terminal/selector.py +1 -1
- klaude_code/ui/terminal/title.py +4 -2
- {klaude_code-2.9.0.dist-info → klaude_code-2.10.0.dist-info}/METADATA +1 -1
- {klaude_code-2.9.0.dist-info → klaude_code-2.10.0.dist-info}/RECORD +67 -66
- klaude_code/llm/bedrock/__init__.py +0 -3
- klaude_code/tui/components/assistant.py +0 -2
- /klaude_code/llm/{bedrock → bedrock_anthropic}/client.py +0 -0
- /klaude_code/llm/{codex → openai_codex}/prompt_sync.py +0 -0
- /klaude_code/llm/{responses → openai_responses}/__init__.py +0 -0
- /klaude_code/llm/{responses → openai_responses}/input.py +0 -0
- {klaude_code-2.9.0.dist-info → klaude_code-2.10.0.dist-info}/WHEEL +0 -0
- {klaude_code-2.9.0.dist-info → klaude_code-2.10.0.dist-info}/entry_points.txt +0 -0
|
@@ -14,7 +14,6 @@ from rich import box
|
|
|
14
14
|
from rich._loop import loop_first
|
|
15
15
|
from rich.console import Console, ConsoleOptions, RenderableType, RenderResult
|
|
16
16
|
from rich.markdown import CodeBlock, Heading, ImageItem, ListItem, Markdown, MarkdownElement, TableElement
|
|
17
|
-
from rich.rule import Rule
|
|
18
17
|
from rich.segment import Segment
|
|
19
18
|
from rich.style import Style, StyleType
|
|
20
19
|
from rich.syntax import Syntax
|
|
@@ -23,12 +22,10 @@ from rich.text import Text
|
|
|
23
22
|
from rich.theme import Theme
|
|
24
23
|
|
|
25
24
|
from klaude_code.const import (
|
|
26
|
-
MARKDOWN_RIGHT_MARGIN,
|
|
27
25
|
MARKDOWN_STREAM_LIVE_REPAINT_ENABLED,
|
|
28
26
|
MARKDOWN_STREAM_SYNCHRONIZED_OUTPUT_ENABLED,
|
|
29
27
|
UI_REFRESH_RATE_FPS,
|
|
30
28
|
)
|
|
31
|
-
from klaude_code.tui.components.rich.code_panel import CodePanel
|
|
32
29
|
|
|
33
30
|
_THINKING_HTML_BLOCK_RE = re.compile(
|
|
34
31
|
r"\A\s*<thinking>\s*\n?(?P<body>.*?)(?:\n\s*)?</thinking>\s*\Z",
|
|
@@ -92,10 +89,15 @@ class ThinkingHTMLBlock(MarkdownElement):
|
|
|
92
89
|
|
|
93
90
|
|
|
94
91
|
class NoInsetCodeBlock(CodeBlock):
|
|
95
|
-
"""A code block with syntax highlighting
|
|
92
|
+
"""A code block with syntax highlighting using markdown fence style."""
|
|
96
93
|
|
|
97
94
|
def __rich_console__(self, console: Console, options: ConsoleOptions) -> RenderResult:
|
|
98
95
|
code = str(self.text).rstrip()
|
|
96
|
+
lang = self.lexer_name if self.lexer_name != "text" else ""
|
|
97
|
+
fence_style = console.get_style("markdown.code.fence", default="none")
|
|
98
|
+
fence_title_style = console.get_style("markdown.code.fence.title", default="none")
|
|
99
|
+
|
|
100
|
+
yield Text.assemble(("```", fence_style), (lang, fence_title_style))
|
|
99
101
|
syntax = Syntax(
|
|
100
102
|
code,
|
|
101
103
|
self.lexer_name,
|
|
@@ -103,16 +105,21 @@ class NoInsetCodeBlock(CodeBlock):
|
|
|
103
105
|
word_wrap=True,
|
|
104
106
|
padding=(0, 0),
|
|
105
107
|
)
|
|
106
|
-
yield
|
|
108
|
+
yield syntax
|
|
109
|
+
yield Text("```", style=fence_style)
|
|
107
110
|
|
|
108
111
|
|
|
109
112
|
class ThinkingCodeBlock(CodeBlock):
|
|
110
|
-
"""A code block for thinking content that uses
|
|
113
|
+
"""A code block for thinking content that uses simple ``` delimiters."""
|
|
111
114
|
|
|
112
115
|
def __rich_console__(self, console: Console, options: ConsoleOptions) -> RenderResult:
|
|
113
116
|
code = str(self.text).rstrip()
|
|
114
|
-
|
|
115
|
-
|
|
117
|
+
fence_style = "markdown.code.fence"
|
|
118
|
+
code_style = "markdown.code.block"
|
|
119
|
+
lang = self.lexer_name if self.lexer_name != "text" else ""
|
|
120
|
+
yield Text(f"```{lang}", style=fence_style)
|
|
121
|
+
yield Text(code, style=code_style)
|
|
122
|
+
yield Text("```", style=fence_style)
|
|
116
123
|
|
|
117
124
|
|
|
118
125
|
class Divider(MarkdownElement):
|
|
@@ -120,7 +127,8 @@ class Divider(MarkdownElement):
|
|
|
120
127
|
|
|
121
128
|
def __rich_console__(self, console: Console, options: ConsoleOptions) -> RenderResult:
|
|
122
129
|
style = console.get_style("markdown.hr", default="none")
|
|
123
|
-
|
|
130
|
+
width = min(options.max_width, 100)
|
|
131
|
+
yield Text("-" * width, style=style)
|
|
124
132
|
|
|
125
133
|
|
|
126
134
|
class MarkdownTable(TableElement):
|
|
@@ -153,7 +161,8 @@ class LeftHeading(Heading):
|
|
|
153
161
|
h1_text = text.assemble((" ", "markdown.h1"), text, (" ", "markdown.h1"))
|
|
154
162
|
yield h1_text
|
|
155
163
|
elif self.tag == "h2":
|
|
156
|
-
|
|
164
|
+
h2_style = console.get_style("markdown.h2", default="bold")
|
|
165
|
+
text.stylize(h2_style + Style(underline=False))
|
|
157
166
|
yield text
|
|
158
167
|
else:
|
|
159
168
|
yield text
|
|
@@ -288,7 +297,7 @@ class MarkdownStream:
|
|
|
288
297
|
mark: str | None = None,
|
|
289
298
|
mark_style: StyleType | None = None,
|
|
290
299
|
left_margin: int = 0,
|
|
291
|
-
right_margin: int =
|
|
300
|
+
right_margin: int = 0,
|
|
292
301
|
markdown_class: Callable[..., Markdown] | None = None,
|
|
293
302
|
image_callback: Callable[[str], None] | None = None,
|
|
294
303
|
) -> None:
|
|
@@ -324,10 +333,10 @@ class MarkdownStream:
|
|
|
324
333
|
|
|
325
334
|
self.theme = theme
|
|
326
335
|
self.console = console
|
|
327
|
-
self.mark: str | None = mark
|
|
328
|
-
self.mark_style: StyleType | None = mark_style
|
|
329
|
-
|
|
330
336
|
self.left_margin: int = max(left_margin, 0)
|
|
337
|
+
# Default mark "•" when left_margin >= 2 and no mark specified
|
|
338
|
+
self.mark: str | None = mark if mark is not None else ("•" if self.left_margin >= 2 else None)
|
|
339
|
+
self.mark_style: StyleType | None = mark_style
|
|
331
340
|
|
|
332
341
|
self.right_margin: int = max(right_margin, 0)
|
|
333
342
|
self.markdown_class: Callable[..., Markdown] = markdown_class or NoInsetMarkdown
|
|
@@ -512,12 +521,17 @@ class MarkdownStream:
|
|
|
512
521
|
|
|
513
522
|
collected_images = getattr(markdown, "collected_images", [])
|
|
514
523
|
|
|
515
|
-
# Split rendered output into lines, strip trailing spaces, and apply left margin.
|
|
516
524
|
lines = output.splitlines(keepends=True)
|
|
517
|
-
|
|
525
|
+
use_mark = apply_mark and bool(self.mark) and self.left_margin >= 2
|
|
526
|
+
|
|
527
|
+
# Fast path: no margin, no mark -> just rstrip each line
|
|
528
|
+
if self.left_margin == 0 and not use_mark:
|
|
529
|
+
processed_lines = [line.rstrip() + "\n" if line.endswith("\n") else line.rstrip() for line in lines]
|
|
530
|
+
return processed_lines, list(collected_images)
|
|
531
|
+
|
|
532
|
+
indent_prefix = " " * self.left_margin
|
|
518
533
|
processed_lines: list[str] = []
|
|
519
534
|
mark_applied = False
|
|
520
|
-
use_mark = apply_mark and bool(self.mark) and self.left_margin >= 2
|
|
521
535
|
|
|
522
536
|
# Pre-render styled mark if needed
|
|
523
537
|
styled_mark: str | None = None
|
|
@@ -538,7 +552,7 @@ class MarkdownStream:
|
|
|
538
552
|
if use_mark and not mark_applied and stripped:
|
|
539
553
|
stripped = f"{styled_mark} {stripped}"
|
|
540
554
|
mark_applied = True
|
|
541
|
-
|
|
555
|
+
else:
|
|
542
556
|
stripped = indent_prefix + stripped
|
|
543
557
|
|
|
544
558
|
if line.endswith("\n"):
|
|
@@ -595,8 +609,11 @@ class MarkdownStream:
|
|
|
595
609
|
|
|
596
610
|
live_text_to_set: Text | None = None
|
|
597
611
|
if not final and MARKDOWN_STREAM_LIVE_REPAINT_ENABLED and self._live_sink is not None:
|
|
598
|
-
|
|
599
|
-
|
|
612
|
+
# When nothing is stable yet, we still want to show incremental output.
|
|
613
|
+
# Apply the mark only for the first (all-live) frame so it stays anchored
|
|
614
|
+
# to the first visible line of the full message.
|
|
615
|
+
apply_mark_to_live = stable_line == 0
|
|
616
|
+
live_lines, _ = self._render_markdown_to_lines(live_source, apply_mark=apply_mark_to_live)
|
|
600
617
|
|
|
601
618
|
if self._stable_rendered_lines:
|
|
602
619
|
stable_trailing_blank = 0
|
|
@@ -619,6 +636,13 @@ class MarkdownStream:
|
|
|
619
636
|
live_text_to_set = Text.from_ansi("".join(live_lines))
|
|
620
637
|
|
|
621
638
|
with self._synchronized_output():
|
|
639
|
+
# Update/clear live area first to avoid blank padding when stable block appears
|
|
640
|
+
if final:
|
|
641
|
+
if self._live_sink is not None:
|
|
642
|
+
self._live_sink(None)
|
|
643
|
+
elif live_text_to_set is not None and self._live_sink is not None:
|
|
644
|
+
self._live_sink(live_text_to_set)
|
|
645
|
+
|
|
622
646
|
if stable_chunk_to_print:
|
|
623
647
|
self.console.print(Text.from_ansi(stable_chunk_to_print), end="\n")
|
|
624
648
|
|
|
@@ -626,13 +650,5 @@ class MarkdownStream:
|
|
|
626
650
|
for img_path in new_images:
|
|
627
651
|
self._image_callback(img_path)
|
|
628
652
|
|
|
629
|
-
if final:
|
|
630
|
-
if self._live_sink is not None:
|
|
631
|
-
self._live_sink(None)
|
|
632
|
-
return
|
|
633
|
-
|
|
634
|
-
if live_text_to_set is not None and self._live_sink is not None:
|
|
635
|
-
self._live_sink(live_text_to_set)
|
|
636
|
-
|
|
637
653
|
elapsed = time.time() - start
|
|
638
654
|
self.min_delay = min(max(elapsed * 6, 1.0 / 30), 0.5)
|
|
@@ -277,7 +277,7 @@ def truncate_left(text: Text, max_cells: int, *, console: Console, ellipsis: str
|
|
|
277
277
|
if cell_len(text.plain) <= max_cells:
|
|
278
278
|
return text
|
|
279
279
|
|
|
280
|
-
ellipsis_cells = cell_len(ellipsis)
|
|
280
|
+
ellipsis_cells = cell_len(ellipsis) + 1 # +1 for trailing space
|
|
281
281
|
if max_cells <= ellipsis_cells:
|
|
282
282
|
# Not enough space to show any meaningful suffix.
|
|
283
283
|
clipped = Text(ellipsis, style=text.style)
|
|
@@ -307,7 +307,7 @@ def truncate_left(text: Text, max_cells: int, *, console: Console, ellipsis: str
|
|
|
307
307
|
except Exception:
|
|
308
308
|
ellipsis_style = suffix.style or text.style
|
|
309
309
|
|
|
310
|
-
return Text.assemble(Text(ellipsis, style=ellipsis_style), suffix)
|
|
310
|
+
return Text.assemble(Text(ellipsis + " ", style=ellipsis_style), suffix)
|
|
311
311
|
|
|
312
312
|
|
|
313
313
|
class ShimmerStatusText:
|
|
@@ -21,6 +21,7 @@ class Palette:
|
|
|
21
21
|
grey_green: str
|
|
22
22
|
purple: str
|
|
23
23
|
lavender: str
|
|
24
|
+
black: str
|
|
24
25
|
diff_add: str
|
|
25
26
|
diff_add_char: str
|
|
26
27
|
diff_remove: str
|
|
@@ -38,6 +39,7 @@ class Palette:
|
|
|
38
39
|
red_background: str
|
|
39
40
|
grey_background: str
|
|
40
41
|
yellow_background: str
|
|
42
|
+
user_message_background: str
|
|
41
43
|
|
|
42
44
|
|
|
43
45
|
LIGHT_PALETTE = Palette(
|
|
@@ -55,6 +57,7 @@ LIGHT_PALETTE = Palette(
|
|
|
55
57
|
grey_green="#96a096",
|
|
56
58
|
purple="#5f5fb7",
|
|
57
59
|
lavender="#7878b0",
|
|
60
|
+
black="#101827",
|
|
58
61
|
diff_add="#2e5a32 on #dafbe1",
|
|
59
62
|
diff_add_char="#2e5a32 on #aceebb",
|
|
60
63
|
diff_remove="#82071e on #ffecec",
|
|
@@ -71,6 +74,7 @@ LIGHT_PALETTE = Palette(
|
|
|
71
74
|
red_background="#f9ecec",
|
|
72
75
|
grey_background="#f0f0f0",
|
|
73
76
|
yellow_background="#f9f9ec",
|
|
77
|
+
user_message_background="#f0f0f0",
|
|
74
78
|
)
|
|
75
79
|
|
|
76
80
|
DARK_PALETTE = Palette(
|
|
@@ -88,6 +92,7 @@ DARK_PALETTE = Palette(
|
|
|
88
92
|
grey_green="#6d8672",
|
|
89
93
|
purple="#afbafe",
|
|
90
94
|
lavender="#9898b8",
|
|
95
|
+
black="white",
|
|
91
96
|
diff_add="#c8e6c9 on #1b3928",
|
|
92
97
|
diff_add_char="#c8e6c9 on #2d6b42",
|
|
93
98
|
diff_remove="#ffcdd2 on #3d1f23",
|
|
@@ -104,6 +109,7 @@ DARK_PALETTE = Palette(
|
|
|
104
109
|
red_background="#3d1f23",
|
|
105
110
|
grey_background="#2a2d30",
|
|
106
111
|
yellow_background="#3d3a1a",
|
|
112
|
+
user_message_background="#2a2d30",
|
|
107
113
|
)
|
|
108
114
|
|
|
109
115
|
|
|
@@ -113,6 +119,7 @@ class ThemeKey(str, Enum):
|
|
|
113
119
|
|
|
114
120
|
# CODE
|
|
115
121
|
CODE_BACKGROUND = "code_background"
|
|
122
|
+
CODE_PANEL_TITLE = "code_panel.title"
|
|
116
123
|
|
|
117
124
|
# PANEL
|
|
118
125
|
SUB_AGENT_RESULT_PANEL = "panel.sub_agent_result"
|
|
@@ -255,18 +262,18 @@ def get_theme(theme: str | None = None) -> Themes:
|
|
|
255
262
|
ThemeKey.ERROR_DIM.value: "dim " + palette.red,
|
|
256
263
|
ThemeKey.INTERRUPT.value: palette.red,
|
|
257
264
|
# USER_INPUT
|
|
258
|
-
ThemeKey.USER_INPUT.value: palette.magenta,
|
|
259
|
-
ThemeKey.USER_INPUT_PROMPT.value: "bold
|
|
260
|
-
ThemeKey.USER_INPUT_AT_PATTERN.value: palette.purple,
|
|
261
|
-
ThemeKey.USER_INPUT_SLASH_COMMAND.value: "bold
|
|
262
|
-
ThemeKey.USER_INPUT_SKILL.value: "bold
|
|
265
|
+
ThemeKey.USER_INPUT.value: f"{palette.magenta} on {palette.user_message_background}",
|
|
266
|
+
ThemeKey.USER_INPUT_PROMPT.value: f"bold {palette.magenta} on {palette.user_message_background}",
|
|
267
|
+
ThemeKey.USER_INPUT_AT_PATTERN.value: f"{palette.purple} on {palette.user_message_background}",
|
|
268
|
+
ThemeKey.USER_INPUT_SLASH_COMMAND.value: f"bold {palette.blue} on {palette.user_message_background}",
|
|
269
|
+
ThemeKey.USER_INPUT_SKILL.value: f"bold {palette.green} on {palette.user_message_background}",
|
|
263
270
|
# ASSISTANT
|
|
264
271
|
ThemeKey.ASSISTANT_MESSAGE_MARK.value: "bold",
|
|
265
272
|
# METADATA
|
|
266
|
-
ThemeKey.METADATA.value: palette.
|
|
267
|
-
ThemeKey.METADATA_DIM.value: "dim " + palette.
|
|
268
|
-
ThemeKey.METADATA_BOLD.value: "bold " + palette.
|
|
269
|
-
ThemeKey.METADATA_ITALIC.value: "italic " + palette.
|
|
273
|
+
ThemeKey.METADATA.value: palette.blue,
|
|
274
|
+
ThemeKey.METADATA_DIM.value: "dim " + palette.blue,
|
|
275
|
+
ThemeKey.METADATA_BOLD.value: "bold " + palette.blue,
|
|
276
|
+
ThemeKey.METADATA_ITALIC.value: "italic " + palette.blue,
|
|
270
277
|
# STATUS
|
|
271
278
|
ThemeKey.STATUS_SPINNER.value: palette.blue,
|
|
272
279
|
ThemeKey.STATUS_TEXT.value: palette.blue,
|
|
@@ -301,9 +308,9 @@ def get_theme(theme: str | None = None) -> Themes:
|
|
|
301
308
|
ThemeKey.BASH_HEREDOC_DELIMITER.value: "bold " + palette.grey1,
|
|
302
309
|
# THINKING
|
|
303
310
|
ThemeKey.THINKING.value: "italic " + palette.grey2,
|
|
304
|
-
ThemeKey.THINKING_BOLD.value: "
|
|
311
|
+
ThemeKey.THINKING_BOLD.value: "italic " + palette.grey1,
|
|
305
312
|
# COMPACTION
|
|
306
|
-
ThemeKey.COMPACTION_SUMMARY.value:
|
|
313
|
+
ThemeKey.COMPACTION_SUMMARY.value: palette.grey1,
|
|
307
314
|
# TODO_ITEM
|
|
308
315
|
ThemeKey.TODO_EXPLANATION.value: palette.grey1 + " italic",
|
|
309
316
|
ThemeKey.TODO_PENDING_MARK.value: "bold " + palette.grey1,
|
|
@@ -345,11 +352,13 @@ def get_theme(theme: str | None = None) -> Themes:
|
|
|
345
352
|
"markdown.thinking": "italic " + palette.grey2,
|
|
346
353
|
"markdown.thinking.tag": palette.grey2,
|
|
347
354
|
"markdown.code.border": palette.grey3,
|
|
355
|
+
"markdown.code.fence": palette.grey3,
|
|
356
|
+
"markdown.code.fence.title": palette.grey1,
|
|
348
357
|
# Used by ThinkingMarkdown when rendering `<thinking>` blocks.
|
|
349
358
|
"markdown.code.block": palette.grey1,
|
|
350
|
-
"markdown.h1": "bold reverse",
|
|
359
|
+
"markdown.h1": "bold reverse " + palette.black,
|
|
351
360
|
"markdown.h1.border": palette.grey3,
|
|
352
|
-
"markdown.h2": "bold underline",
|
|
361
|
+
"markdown.h2": "bold underline " + palette.black,
|
|
353
362
|
"markdown.h3": "bold " + palette.grey1,
|
|
354
363
|
"markdown.h4": "bold " + palette.grey2,
|
|
355
364
|
"markdown.hr": palette.grey3,
|
|
@@ -359,15 +368,18 @@ def get_theme(theme: str | None = None) -> Themes:
|
|
|
359
368
|
"markdown.link_url": "underline " + palette.blue,
|
|
360
369
|
"markdown.table.border": palette.grey2,
|
|
361
370
|
"markdown.checkbox.checked": palette.green,
|
|
371
|
+
"markdown.block_quote": palette.cyan,
|
|
362
372
|
}
|
|
363
373
|
),
|
|
364
374
|
thinking_markdown_theme=Theme(
|
|
365
375
|
styles={
|
|
366
376
|
# THINKING (used for left-side mark in thinking output)
|
|
367
377
|
ThemeKey.THINKING.value: "italic " + palette.grey2,
|
|
368
|
-
ThemeKey.THINKING_BOLD.value: "
|
|
378
|
+
ThemeKey.THINKING_BOLD.value: "italic " + palette.grey1,
|
|
379
|
+
"markdown.strong": "italic " + palette.grey1,
|
|
369
380
|
"markdown.code": palette.grey1 + " italic on " + palette.code_background,
|
|
370
|
-
"markdown.code.block": palette.
|
|
381
|
+
"markdown.code.block": palette.grey2,
|
|
382
|
+
"markdown.code.fence": palette.grey3,
|
|
371
383
|
"markdown.code.border": palette.grey3,
|
|
372
384
|
"markdown.thinking.tag": palette.grey2 + " dim",
|
|
373
385
|
"markdown.h1": "bold reverse",
|
|
@@ -379,9 +391,9 @@ def get_theme(theme: str | None = None) -> Themes:
|
|
|
379
391
|
"markdown.item.number": palette.grey2,
|
|
380
392
|
"markdown.link": "underline " + palette.blue,
|
|
381
393
|
"markdown.link_url": "underline " + palette.blue,
|
|
382
|
-
"markdown.strong": "bold italic " + palette.grey1,
|
|
383
394
|
"markdown.table.border": palette.grey2,
|
|
384
395
|
"markdown.checkbox.checked": palette.green,
|
|
396
|
+
"markdown.block_quote": palette.grey1,
|
|
385
397
|
}
|
|
386
398
|
),
|
|
387
399
|
code_theme=palette.code_theme,
|
|
@@ -10,8 +10,10 @@ from rich.text import Text
|
|
|
10
10
|
|
|
11
11
|
from klaude_code.const import (
|
|
12
12
|
BASH_OUTPUT_PANEL_THRESHOLD,
|
|
13
|
+
DIFF_PREFIX_WIDTH,
|
|
13
14
|
INVALID_TOOL_CALL_MAX_LENGTH,
|
|
14
15
|
QUERY_DISPLAY_TRUNCATE_LENGTH,
|
|
16
|
+
TAB_EXPAND_WIDTH,
|
|
15
17
|
URL_TRUNCATE_MAX_LENGTH,
|
|
16
18
|
WEB_SEARCH_DEFAULT_MAX_RESULTS,
|
|
17
19
|
)
|
|
@@ -387,6 +389,24 @@ def render_generic_tool_result(result: str, *, is_error: bool = False) -> Render
|
|
|
387
389
|
return text
|
|
388
390
|
|
|
389
391
|
|
|
392
|
+
def render_read_preview(ui_extra: model.ReadPreviewUIExtra) -> RenderableType:
|
|
393
|
+
"""Render read preview with line numbers aligned to diff style."""
|
|
394
|
+
grid = create_grid()
|
|
395
|
+
grid.padding = (0, 0)
|
|
396
|
+
|
|
397
|
+
for line in ui_extra.lines:
|
|
398
|
+
prefix = f"{line.line_no:>{DIFF_PREFIX_WIDTH}} "
|
|
399
|
+
content = line.content.expandtabs(TAB_EXPAND_WIDTH)
|
|
400
|
+
grid.add_row(Text(prefix, ThemeKey.TOOL_RESULT), Text(content, ThemeKey.TOOL_RESULT))
|
|
401
|
+
|
|
402
|
+
if ui_extra.remaining_lines > 0:
|
|
403
|
+
remaining_prefix = f"{'⋮':>{DIFF_PREFIX_WIDTH}} "
|
|
404
|
+
remaining_text = Text(f"(more {ui_extra.remaining_lines} lines)", ThemeKey.TOOL_RESULT_TRUNCATED)
|
|
405
|
+
grid.add_row(Text(remaining_prefix, ThemeKey.TOOL_RESULT_TRUNCATED), remaining_text)
|
|
406
|
+
|
|
407
|
+
return grid
|
|
408
|
+
|
|
409
|
+
|
|
390
410
|
def _extract_mermaid_link(
|
|
391
411
|
ui_extra: model.ToolResultUIExtra | None,
|
|
392
412
|
) -> model.MermaidLinkUIExtra | None:
|
|
@@ -545,6 +565,7 @@ _TOOL_ACTIVE_FORM: dict[str, str] = {
|
|
|
545
565
|
tools.WEB_SEARCH: "Searching Web",
|
|
546
566
|
tools.REPORT_BACK: "Reporting",
|
|
547
567
|
tools.IMAGE_GEN: "Generating Image",
|
|
568
|
+
tools.TASK: "Spawning Task",
|
|
548
569
|
}
|
|
549
570
|
|
|
550
571
|
|
|
@@ -678,6 +699,8 @@ def render_tool_result(
|
|
|
678
699
|
|
|
679
700
|
match e.tool_name:
|
|
680
701
|
case tools.READ:
|
|
702
|
+
if isinstance(e.ui_extra, model.ReadPreviewUIExtra):
|
|
703
|
+
return wrap(render_read_preview(e.ui_extra))
|
|
681
704
|
return None
|
|
682
705
|
case tools.EDIT:
|
|
683
706
|
return wrap(r_diffs.render_structured_diff(diff_ui) if diff_ui else Text(""))
|
|
@@ -1,20 +1,19 @@
|
|
|
1
1
|
import re
|
|
2
2
|
|
|
3
3
|
from rich.console import Group, RenderableType
|
|
4
|
+
from rich.padding import Padding
|
|
4
5
|
from rich.text import Text
|
|
5
6
|
|
|
6
|
-
from klaude_code.
|
|
7
|
-
from klaude_code.
|
|
7
|
+
from klaude_code.const import TAB_EXPAND_WIDTH
|
|
8
|
+
from klaude_code.skill import list_skill_names
|
|
9
|
+
from klaude_code.tui.components.bash_syntax import highlight_bash_command
|
|
8
10
|
from klaude_code.tui.components.rich.theme import ThemeKey
|
|
9
11
|
|
|
10
|
-
# Match
|
|
12
|
+
# Match inline patterns only when they appear at the beginning of the line
|
|
11
13
|
# or immediately after whitespace, to avoid treating mid-word email-like
|
|
12
14
|
# patterns such as foo@bar.com as file references.
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
# Match $skill or ¥skill pattern inline (at start of line or after whitespace)
|
|
16
|
-
SKILL_RENDER_PATTERN = re.compile(r"(?<!\S)[$¥](\S+)")
|
|
17
|
-
|
|
15
|
+
# Group 1 is present only for $/¥ skills and captures the skill token (without the $/¥).
|
|
16
|
+
INLINE_RENDER_PATTERN = re.compile(r'(?<!\S)(?:@(?:"[^"]+"|\S+)|[$¥](\S+))')
|
|
18
17
|
USER_MESSAGE_MARK = "❯ "
|
|
19
18
|
|
|
20
19
|
|
|
@@ -23,85 +22,77 @@ def render_at_and_skill_patterns(
|
|
|
23
22
|
at_style: str = ThemeKey.USER_INPUT_AT_PATTERN,
|
|
24
23
|
skill_style: str = ThemeKey.USER_INPUT_SKILL,
|
|
25
24
|
other_style: str = ThemeKey.USER_INPUT,
|
|
25
|
+
available_skill_names: set[str] | None = None,
|
|
26
26
|
) -> Text:
|
|
27
27
|
"""Render text with highlighted @file and $skill patterns."""
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
# Collect all matches with their styles
|
|
35
|
-
matches: list[tuple[int, int, str]] = [] # (start, end, style)
|
|
36
|
-
|
|
37
|
-
if has_at:
|
|
38
|
-
for match in AT_FILE_RENDER_PATTERN.finditer(text):
|
|
39
|
-
matches.append((match.start(), match.end(), at_style))
|
|
40
|
-
|
|
41
|
-
if has_skill:
|
|
42
|
-
for match in SKILL_RENDER_PATTERN.finditer(text):
|
|
43
|
-
skill_name = match.group(1)
|
|
44
|
-
if _is_valid_skill_name(skill_name):
|
|
45
|
-
matches.append((match.start(), match.end(), skill_style))
|
|
46
|
-
|
|
47
|
-
if not matches:
|
|
48
|
-
return Text(text, style=other_style)
|
|
49
|
-
|
|
50
|
-
# Sort by start position
|
|
51
|
-
matches.sort(key=lambda x: x[0])
|
|
28
|
+
result = Text(text, style=other_style)
|
|
29
|
+
for match in INLINE_RENDER_PATTERN.finditer(text):
|
|
30
|
+
skill_name = match.group(1)
|
|
31
|
+
if skill_name is None:
|
|
32
|
+
result.stylize(at_style, match.start(), match.end())
|
|
33
|
+
continue
|
|
52
34
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
for start, end, style in matches:
|
|
56
|
-
if start < last_end:
|
|
57
|
-
continue # Skip overlapping matches
|
|
58
|
-
if start > last_end:
|
|
59
|
-
result.append_text(Text(text[last_end:start], other_style))
|
|
60
|
-
result.append_text(Text(text[start:end], style))
|
|
61
|
-
last_end = end
|
|
35
|
+
if available_skill_names is None:
|
|
36
|
+
available_skill_names = set(list_skill_names())
|
|
62
37
|
|
|
63
|
-
|
|
64
|
-
|
|
38
|
+
short = skill_name.split(":")[-1] if ":" in skill_name else skill_name
|
|
39
|
+
if skill_name in available_skill_names or short in available_skill_names:
|
|
40
|
+
result.stylize(skill_style, match.start(), match.end())
|
|
65
41
|
|
|
66
42
|
return result
|
|
67
43
|
|
|
68
44
|
|
|
69
|
-
def _is_valid_skill_name(name: str) -> bool:
|
|
70
|
-
"""Check if a skill name is valid (exists in loaded skills)."""
|
|
71
|
-
short = name.split(":")[-1] if ":" in name else name
|
|
72
|
-
available_skills = get_available_skills()
|
|
73
|
-
return any(skill_name in (name, short) for skill_name, _, _ in available_skills)
|
|
74
|
-
|
|
75
|
-
|
|
76
45
|
def render_user_input(content: str) -> RenderableType:
|
|
77
46
|
"""Render a user message as a group of quoted lines with styles.
|
|
78
47
|
|
|
79
48
|
- Highlights slash command token on the first line
|
|
80
49
|
- Highlights @file and $skill patterns in all lines
|
|
50
|
+
- Wrapped in a Panel for block-style background
|
|
81
51
|
"""
|
|
82
52
|
lines = content.strip().split("\n")
|
|
53
|
+
is_bash_mode = bool(lines) and lines[0].startswith("!")
|
|
54
|
+
|
|
55
|
+
available_skill_names: set[str] | None = None
|
|
56
|
+
|
|
83
57
|
renderables: list[RenderableType] = []
|
|
84
58
|
for i, line in enumerate(lines):
|
|
59
|
+
if not line.strip():
|
|
60
|
+
continue
|
|
61
|
+
if "\t" in line:
|
|
62
|
+
line = line.expandtabs(TAB_EXPAND_WIDTH)
|
|
63
|
+
|
|
64
|
+
if is_bash_mode and i == 0:
|
|
65
|
+
renderables.append(highlight_bash_command(line[1:]))
|
|
66
|
+
continue
|
|
67
|
+
if is_bash_mode and i > 0:
|
|
68
|
+
renderables.append(highlight_bash_command(line))
|
|
69
|
+
continue
|
|
70
|
+
|
|
71
|
+
if available_skill_names is None and ("$" in line or "\u00a5" in line):
|
|
72
|
+
available_skill_names = set(list_skill_names())
|
|
85
73
|
# Handle slash command on first line
|
|
86
74
|
if i == 0 and line.startswith("/"):
|
|
87
75
|
splits = line.split(" ", maxsplit=1)
|
|
88
76
|
line_text = Text.assemble(
|
|
89
77
|
(splits[0], ThemeKey.USER_INPUT_SLASH_COMMAND),
|
|
90
78
|
" ",
|
|
91
|
-
render_at_and_skill_patterns(splits[1]
|
|
79
|
+
render_at_and_skill_patterns(splits[1], available_skill_names=available_skill_names)
|
|
80
|
+
if len(splits) > 1
|
|
81
|
+
else Text(""),
|
|
92
82
|
)
|
|
93
83
|
renderables.append(line_text)
|
|
94
84
|
continue
|
|
95
85
|
|
|
96
86
|
# Render @file and $skill patterns
|
|
97
|
-
renderables.append(render_at_and_skill_patterns(line))
|
|
87
|
+
renderables.append(render_at_and_skill_patterns(line, available_skill_names=available_skill_names))
|
|
98
88
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
89
|
+
return Padding(
|
|
90
|
+
Group(*renderables),
|
|
91
|
+
pad=(0, 1),
|
|
92
|
+
style=ThemeKey.USER_INPUT,
|
|
93
|
+
expand=False,
|
|
94
|
+
)
|
|
104
95
|
|
|
105
96
|
|
|
106
97
|
def render_interrupt() -> RenderableType:
|
|
107
|
-
return Text("
|
|
98
|
+
return Text("Interrupted by user", style=ThemeKey.INTERRUPT)
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from importlib.metadata import PackageNotFoundError, version
|
|
2
|
+
from pathlib import Path
|
|
2
3
|
|
|
3
4
|
from rich.console import Group, RenderableType
|
|
4
5
|
from rich.text import Text
|
|
@@ -10,6 +11,19 @@ from klaude_code.tui.components.rich.theme import ThemeKey
|
|
|
10
11
|
from klaude_code.ui.common import format_model_params
|
|
11
12
|
|
|
12
13
|
|
|
14
|
+
def _format_memory_path(path: str, *, work_dir: Path) -> str:
|
|
15
|
+
"""Format memory path for display - show relative path or ~ for home."""
|
|
16
|
+
p = Path(path)
|
|
17
|
+
try:
|
|
18
|
+
return str(p.relative_to(work_dir))
|
|
19
|
+
except ValueError:
|
|
20
|
+
pass
|
|
21
|
+
try:
|
|
22
|
+
return f"~/{p.relative_to(Path.home())}"
|
|
23
|
+
except ValueError:
|
|
24
|
+
return path
|
|
25
|
+
|
|
26
|
+
|
|
13
27
|
def _get_version() -> str:
|
|
14
28
|
"""Get the current version of klaude-code."""
|
|
15
29
|
try:
|
|
@@ -50,7 +64,7 @@ def render_welcome(e: events.WelcomeEvent) -> RenderableType:
|
|
|
50
64
|
# Render config items with tree-style prefixes
|
|
51
65
|
for i, param_str in enumerate(param_strings):
|
|
52
66
|
is_last = i == len(param_strings) - 1
|
|
53
|
-
prefix = "
|
|
67
|
+
prefix = "╰─ " if is_last else "├─ "
|
|
54
68
|
panel_content.append_text(
|
|
55
69
|
Text.assemble(
|
|
56
70
|
("\n", ThemeKey.WELCOME_INFO),
|
|
@@ -59,6 +73,37 @@ def render_welcome(e: events.WelcomeEvent) -> RenderableType:
|
|
|
59
73
|
)
|
|
60
74
|
)
|
|
61
75
|
|
|
76
|
+
# Loaded memories summary
|
|
77
|
+
work_dir = Path(e.work_dir)
|
|
78
|
+
loaded_memories = e.loaded_memories or {}
|
|
79
|
+
user_memories = loaded_memories.get("user") or []
|
|
80
|
+
project_memories = loaded_memories.get("project") or []
|
|
81
|
+
|
|
82
|
+
memory_groups: list[tuple[str, list[str]]] = []
|
|
83
|
+
if user_memories:
|
|
84
|
+
memory_groups.append(("user", user_memories))
|
|
85
|
+
if project_memories:
|
|
86
|
+
memory_groups.append(("project", project_memories))
|
|
87
|
+
|
|
88
|
+
if memory_groups:
|
|
89
|
+
panel_content.append_text(Text("\n\n", style=ThemeKey.WELCOME_INFO))
|
|
90
|
+
panel_content.append_text(Text("context", style=ThemeKey.WELCOME_HIGHLIGHT))
|
|
91
|
+
|
|
92
|
+
label_width = len("[project]")
|
|
93
|
+
|
|
94
|
+
for i, (group_name, paths) in enumerate(memory_groups):
|
|
95
|
+
is_last = i == len(memory_groups) - 1
|
|
96
|
+
prefix = "╰─ " if is_last else "├─ "
|
|
97
|
+
label = f"[{group_name}]"
|
|
98
|
+
formatted_paths = ", ".join(_format_memory_path(p, work_dir=work_dir) for p in paths)
|
|
99
|
+
panel_content.append_text(
|
|
100
|
+
Text.assemble(
|
|
101
|
+
("\n", ThemeKey.WELCOME_INFO),
|
|
102
|
+
(prefix, ThemeKey.LINES),
|
|
103
|
+
(f"{label.ljust(label_width)} {formatted_paths}", ThemeKey.WELCOME_INFO),
|
|
104
|
+
)
|
|
105
|
+
)
|
|
106
|
+
|
|
62
107
|
# Loaded skills summary is provided by core via WelcomeEvent to keep TUI decoupled.
|
|
63
108
|
loaded_skills = e.loaded_skills or {}
|
|
64
109
|
user_skills = loaded_skills.get("user") or []
|
|
@@ -81,7 +126,7 @@ def render_welcome(e: events.WelcomeEvent) -> RenderableType:
|
|
|
81
126
|
|
|
82
127
|
for i, (group_name, skills) in enumerate(skill_groups):
|
|
83
128
|
is_last = i == len(skill_groups) - 1
|
|
84
|
-
prefix = "
|
|
129
|
+
prefix = "╰─ " if is_last else "├─ "
|
|
85
130
|
label = f"[{group_name}]"
|
|
86
131
|
panel_content.append_text(
|
|
87
132
|
Text.assemble(
|
klaude_code/tui/display.py
CHANGED
|
@@ -31,12 +31,20 @@ class TUIDisplay(DisplayABC):
|
|
|
31
31
|
@override
|
|
32
32
|
async def consume_event(self, event: events.Event) -> None:
|
|
33
33
|
if isinstance(event, events.ReplayHistoryEvent):
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
34
|
+
# Replay does not need streaming UI; disable bottom Live rendering to avoid
|
|
35
|
+
# repaint overhead and flicker while reconstructing history.
|
|
36
|
+
self._renderer.stop_bottom_live()
|
|
37
|
+
self._renderer.set_stream_renderable(None)
|
|
38
|
+
self._renderer.set_replay_mode(True)
|
|
39
|
+
try:
|
|
40
|
+
await self._renderer.execute(self._machine.begin_replay())
|
|
41
|
+
for item in event.events:
|
|
42
|
+
commands = self._machine.transition_replay(item)
|
|
43
|
+
if commands:
|
|
44
|
+
await self._renderer.execute(commands)
|
|
45
|
+
await self._renderer.execute(self._machine.end_replay())
|
|
46
|
+
finally:
|
|
47
|
+
self._renderer.set_replay_mode(False)
|
|
40
48
|
return
|
|
41
49
|
|
|
42
50
|
commands = self._machine.transition(event)
|
|
@@ -92,4 +100,4 @@ class TUIDisplay(DisplayABC):
|
|
|
92
100
|
with contextlib.suppress(Exception):
|
|
93
101
|
self._renderer.spinner_stop()
|
|
94
102
|
with contextlib.suppress(Exception):
|
|
95
|
-
self._renderer.stop_bottom_live()
|
|
103
|
+
self._renderer.stop_bottom_live()
|