klaude-code 1.2.1__py3-none-any.whl → 1.2.3__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/main.py +9 -4
- klaude_code/cli/runtime.py +42 -43
- klaude_code/command/__init__.py +7 -5
- klaude_code/command/clear_cmd.py +6 -29
- klaude_code/command/command_abc.py +44 -8
- klaude_code/command/diff_cmd.py +33 -27
- klaude_code/command/export_cmd.py +18 -26
- klaude_code/command/help_cmd.py +10 -8
- klaude_code/command/model_cmd.py +11 -40
- klaude_code/command/{prompt-update-dev-doc.md → prompt-dev-docs-update.md} +3 -2
- klaude_code/command/{prompt-dev-doc.md → prompt-dev-docs.md} +3 -2
- klaude_code/command/prompt-init.md +2 -5
- klaude_code/command/prompt_command.py +6 -6
- klaude_code/command/refresh_cmd.py +4 -5
- klaude_code/command/registry.py +16 -19
- klaude_code/command/terminal_setup_cmd.py +12 -11
- klaude_code/config/__init__.py +4 -0
- klaude_code/config/config.py +25 -26
- klaude_code/config/list_model.py +8 -3
- klaude_code/config/select_model.py +1 -1
- klaude_code/const/__init__.py +1 -1
- klaude_code/core/__init__.py +0 -3
- klaude_code/core/agent.py +25 -50
- klaude_code/core/executor.py +268 -101
- klaude_code/core/prompt.py +12 -12
- klaude_code/core/{prompt → prompts}/prompt-gemini.md +1 -1
- klaude_code/core/reminders.py +76 -95
- klaude_code/core/task.py +21 -14
- klaude_code/core/tool/__init__.py +45 -11
- klaude_code/core/tool/file/apply_patch.py +5 -1
- klaude_code/core/tool/file/apply_patch_tool.py +11 -13
- klaude_code/core/tool/file/edit_tool.py +27 -23
- klaude_code/core/tool/file/multi_edit_tool.py +15 -17
- klaude_code/core/tool/file/read_tool.py +41 -36
- klaude_code/core/tool/file/write_tool.py +13 -15
- klaude_code/core/tool/memory/memory_tool.py +85 -68
- klaude_code/core/tool/memory/skill_tool.py +10 -12
- klaude_code/core/tool/shell/bash_tool.py +24 -22
- klaude_code/core/tool/shell/command_safety.py +12 -1
- klaude_code/core/tool/sub_agent_tool.py +11 -12
- klaude_code/core/tool/todo/todo_write_tool.py +21 -28
- klaude_code/core/tool/todo/update_plan_tool.py +14 -24
- klaude_code/core/tool/tool_abc.py +3 -4
- klaude_code/core/tool/tool_context.py +7 -7
- klaude_code/core/tool/tool_registry.py +30 -47
- klaude_code/core/tool/tool_runner.py +35 -43
- klaude_code/core/tool/truncation.py +14 -20
- klaude_code/core/tool/web/mermaid_tool.py +12 -14
- klaude_code/core/tool/web/web_fetch_tool.py +15 -17
- klaude_code/core/turn.py +19 -7
- klaude_code/llm/__init__.py +3 -4
- klaude_code/llm/anthropic/client.py +30 -46
- klaude_code/llm/anthropic/input.py +4 -11
- klaude_code/llm/client.py +29 -8
- klaude_code/llm/input_common.py +66 -36
- klaude_code/llm/openai_compatible/client.py +42 -84
- klaude_code/llm/openai_compatible/input.py +11 -16
- klaude_code/llm/openai_compatible/tool_call_accumulator.py +2 -2
- klaude_code/llm/openrouter/client.py +40 -289
- klaude_code/llm/openrouter/input.py +13 -35
- klaude_code/llm/openrouter/reasoning_handler.py +209 -0
- klaude_code/llm/registry.py +5 -75
- klaude_code/llm/responses/client.py +34 -55
- klaude_code/llm/responses/input.py +24 -26
- klaude_code/llm/usage.py +109 -0
- klaude_code/protocol/__init__.py +4 -0
- klaude_code/protocol/events.py +3 -2
- klaude_code/protocol/{llm_parameter.py → llm_param.py} +12 -32
- klaude_code/protocol/model.py +49 -4
- klaude_code/protocol/op.py +18 -16
- klaude_code/protocol/op_handler.py +28 -0
- klaude_code/{core → protocol}/sub_agent.py +7 -0
- klaude_code/session/export.py +150 -70
- klaude_code/session/session.py +28 -14
- klaude_code/session/templates/export_session.html +180 -42
- klaude_code/trace/__init__.py +2 -2
- klaude_code/trace/log.py +11 -5
- klaude_code/ui/__init__.py +91 -8
- klaude_code/ui/core/__init__.py +1 -0
- klaude_code/ui/core/display.py +103 -0
- klaude_code/ui/core/input.py +71 -0
- klaude_code/ui/modes/__init__.py +1 -0
- klaude_code/ui/modes/debug/__init__.py +1 -0
- klaude_code/ui/{base/debug_event_display.py → modes/debug/display.py} +9 -5
- klaude_code/ui/modes/exec/__init__.py +1 -0
- klaude_code/ui/{base/exec_display.py → modes/exec/display.py} +28 -2
- klaude_code/ui/{repl → modes/repl}/__init__.py +5 -6
- klaude_code/ui/modes/repl/clipboard.py +152 -0
- klaude_code/ui/modes/repl/completers.py +429 -0
- klaude_code/ui/modes/repl/display.py +60 -0
- klaude_code/ui/modes/repl/event_handler.py +375 -0
- klaude_code/ui/modes/repl/input_prompt_toolkit.py +198 -0
- klaude_code/ui/modes/repl/key_bindings.py +170 -0
- klaude_code/ui/{repl → modes/repl}/renderer.py +109 -132
- klaude_code/ui/renderers/assistant.py +21 -0
- klaude_code/ui/renderers/common.py +0 -16
- klaude_code/ui/renderers/developer.py +18 -18
- klaude_code/ui/renderers/diffs.py +36 -14
- klaude_code/ui/renderers/errors.py +1 -1
- klaude_code/ui/renderers/metadata.py +50 -27
- klaude_code/ui/renderers/sub_agent.py +43 -9
- klaude_code/ui/renderers/thinking.py +33 -1
- klaude_code/ui/renderers/tools.py +212 -20
- klaude_code/ui/renderers/user_input.py +19 -23
- klaude_code/ui/rich/__init__.py +1 -0
- klaude_code/ui/{rich_ext → rich}/searchable_text.py +3 -1
- klaude_code/ui/{renderers → rich}/status.py +29 -18
- klaude_code/ui/{base → rich}/theme.py +8 -2
- klaude_code/ui/terminal/__init__.py +1 -0
- klaude_code/ui/{base/terminal_color.py → terminal/color.py} +4 -1
- klaude_code/ui/{base/terminal_control.py → terminal/control.py} +1 -0
- klaude_code/ui/{base/terminal_notifier.py → terminal/notifier.py} +5 -2
- klaude_code/ui/utils/__init__.py +1 -0
- klaude_code/ui/{base/utils.py → utils/common.py} +35 -3
- {klaude_code-1.2.1.dist-info → klaude_code-1.2.3.dist-info}/METADATA +1 -1
- klaude_code-1.2.3.dist-info/RECORD +161 -0
- klaude_code/core/clipboard_manifest.py +0 -124
- klaude_code/llm/openrouter/tool_call_accumulator.py +0 -80
- klaude_code/ui/base/__init__.py +0 -1
- klaude_code/ui/base/display_abc.py +0 -36
- klaude_code/ui/base/input_abc.py +0 -20
- klaude_code/ui/repl/display.py +0 -36
- klaude_code/ui/repl/event_handler.py +0 -247
- klaude_code/ui/repl/input.py +0 -773
- klaude_code/ui/rich_ext/__init__.py +0 -1
- klaude_code-1.2.1.dist-info/RECORD +0 -151
- /klaude_code/core/{prompt → prompts}/prompt-claude-code.md +0 -0
- /klaude_code/core/{prompt → prompts}/prompt-codex.md +0 -0
- /klaude_code/core/{prompt → prompts}/prompt-subagent-explore.md +0 -0
- /klaude_code/core/{prompt → prompts}/prompt-subagent-oracle.md +0 -0
- /klaude_code/core/{prompt → prompts}/prompt-subagent-webfetch.md +0 -0
- /klaude_code/core/{prompt → prompts}/prompt-subagent.md +0 -0
- /klaude_code/ui/{base → core}/stage_manager.py +0 -0
- /klaude_code/ui/{rich_ext → rich}/live.py +0 -0
- /klaude_code/ui/{rich_ext → rich}/markdown.py +0 -0
- /klaude_code/ui/{rich_ext → rich}/quote.py +0 -0
- /klaude_code/ui/{base → terminal}/progress_bar.py +0 -0
- /klaude_code/ui/{base → utils}/debouncer.py +0 -0
- {klaude_code-1.2.1.dist-info → klaude_code-1.2.3.dist-info}/WHEEL +0 -0
- {klaude_code-1.2.1.dist-info → klaude_code-1.2.3.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
"""REPL keyboard bindings for prompt_toolkit.
|
|
2
|
+
|
|
3
|
+
This module provides the factory function to create key bindings for the REPL input,
|
|
4
|
+
with dependencies injected to avoid circular imports.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import re
|
|
10
|
+
from collections.abc import Callable
|
|
11
|
+
from typing import cast
|
|
12
|
+
|
|
13
|
+
from prompt_toolkit.key_binding import KeyBindings
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def create_key_bindings(
|
|
17
|
+
capture_clipboard_tag: Callable[[], str | None],
|
|
18
|
+
copy_to_clipboard: Callable[[str], None],
|
|
19
|
+
at_token_pattern: re.Pattern[str],
|
|
20
|
+
) -> KeyBindings:
|
|
21
|
+
"""Create REPL key bindings with injected dependencies.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
capture_clipboard_tag: Callable to capture clipboard image and return tag
|
|
25
|
+
copy_to_clipboard: Callable to copy text to system clipboard
|
|
26
|
+
at_token_pattern: Pattern to match @token for completion refresh
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
KeyBindings instance with all REPL handlers configured
|
|
30
|
+
"""
|
|
31
|
+
kb = KeyBindings()
|
|
32
|
+
|
|
33
|
+
@kb.add("c-v")
|
|
34
|
+
def _(event): # type: ignore
|
|
35
|
+
"""Paste image from clipboard as [Image #N]."""
|
|
36
|
+
tag = capture_clipboard_tag()
|
|
37
|
+
if tag:
|
|
38
|
+
try:
|
|
39
|
+
event.current_buffer.insert_text(tag) # pyright: ignore[reportUnknownMemberType]
|
|
40
|
+
except Exception:
|
|
41
|
+
pass
|
|
42
|
+
|
|
43
|
+
@kb.add("enter")
|
|
44
|
+
def _(event): # type: ignore
|
|
45
|
+
buf = event.current_buffer # type: ignore
|
|
46
|
+
doc = buf.document # type: ignore
|
|
47
|
+
|
|
48
|
+
# If VS Code/Windsurf/Cursor sent a "\\" sentinel before Enter (Shift+Enter mapping),
|
|
49
|
+
# treat it as a request for a newline instead of submit.
|
|
50
|
+
# This allows Shift+Enter to insert a newline in our multiline prompt.
|
|
51
|
+
try:
|
|
52
|
+
if doc.text_before_cursor.endswith("\\"): # type: ignore[reportUnknownMemberType]
|
|
53
|
+
buf.delete_before_cursor() # remove the sentinel backslash # type: ignore[reportUnknownMemberType]
|
|
54
|
+
buf.insert_text("\n") # type: ignore[reportUnknownMemberType]
|
|
55
|
+
return
|
|
56
|
+
except Exception:
|
|
57
|
+
# Fall through to default behavior if anything goes wrong
|
|
58
|
+
pass
|
|
59
|
+
|
|
60
|
+
# If the entire buffer is whitespace-only, insert a newline rather than submitting.
|
|
61
|
+
if len(buf.text.strip()) == 0: # type: ignore
|
|
62
|
+
buf.insert_text("\n") # type: ignore
|
|
63
|
+
return
|
|
64
|
+
|
|
65
|
+
# No need to persist manifest anymore - iter_inputs will handle image extraction
|
|
66
|
+
buf.validate_and_handle() # type: ignore
|
|
67
|
+
|
|
68
|
+
@kb.add("c-j")
|
|
69
|
+
def _(event): # type: ignore
|
|
70
|
+
event.current_buffer.insert_text("\n") # type: ignore
|
|
71
|
+
|
|
72
|
+
@kb.add("c")
|
|
73
|
+
def _(event): # type: ignore
|
|
74
|
+
"""Copy selected text to system clipboard, or insert 'c' if no selection."""
|
|
75
|
+
buf = event.current_buffer # type: ignore
|
|
76
|
+
if buf.selection_state: # type: ignore[reportUnknownMemberType]
|
|
77
|
+
doc = buf.document # type: ignore[reportUnknownMemberType]
|
|
78
|
+
start, end = doc.selection_range() # type: ignore[reportUnknownMemberType]
|
|
79
|
+
selected_text: str = doc.text[start:end] # type: ignore[reportUnknownMemberType]
|
|
80
|
+
|
|
81
|
+
if selected_text:
|
|
82
|
+
copy_to_clipboard(selected_text) # type: ignore[reportUnknownArgumentType]
|
|
83
|
+
buf.exit_selection() # type: ignore[reportUnknownMemberType]
|
|
84
|
+
else:
|
|
85
|
+
buf.insert_text("c") # type: ignore[reportUnknownMemberType]
|
|
86
|
+
|
|
87
|
+
@kb.add("backspace")
|
|
88
|
+
def _(event): # type: ignore
|
|
89
|
+
"""Ensure completions refresh on backspace when editing an @token.
|
|
90
|
+
|
|
91
|
+
We delete the character before cursor (default behavior), then explicitly
|
|
92
|
+
trigger completion refresh if the caret is still within an @... token.
|
|
93
|
+
"""
|
|
94
|
+
buf = event.current_buffer # type: ignore
|
|
95
|
+
# Handle selection: cut selection if present, otherwise delete one character
|
|
96
|
+
if buf.selection_state: # type: ignore[reportUnknownMemberType]
|
|
97
|
+
buf.cut_selection() # type: ignore[reportUnknownMemberType]
|
|
98
|
+
else:
|
|
99
|
+
buf.delete_before_cursor() # type: ignore[reportUnknownMemberType]
|
|
100
|
+
# If the token pattern still applies, refresh completion popup
|
|
101
|
+
try:
|
|
102
|
+
text_before = buf.document.text_before_cursor # type: ignore[reportUnknownMemberType, reportUnknownVariableType]
|
|
103
|
+
# Check for both @ tokens and / tokens (slash commands on first line only)
|
|
104
|
+
should_refresh = False
|
|
105
|
+
if at_token_pattern.search(text_before): # type: ignore[reportUnknownArgumentType]
|
|
106
|
+
should_refresh = True
|
|
107
|
+
elif buf.document.cursor_position_row == 0: # type: ignore[reportUnknownMemberType]
|
|
108
|
+
# Check for slash command pattern without accessing protected attribute
|
|
109
|
+
text_before_str = cast(str, text_before or "")
|
|
110
|
+
if text_before_str.strip().startswith("/") and " " not in text_before_str:
|
|
111
|
+
should_refresh = True
|
|
112
|
+
|
|
113
|
+
if should_refresh:
|
|
114
|
+
buf.start_completion(select_first=False) # type: ignore[reportUnknownMemberType]
|
|
115
|
+
except Exception:
|
|
116
|
+
pass
|
|
117
|
+
|
|
118
|
+
@kb.add("left")
|
|
119
|
+
def _(event): # type: ignore
|
|
120
|
+
"""Support wrapping to previous line when pressing left at column 0."""
|
|
121
|
+
buf = event.current_buffer # type: ignore
|
|
122
|
+
try:
|
|
123
|
+
doc = buf.document # type: ignore[reportUnknownMemberType]
|
|
124
|
+
row = cast(int, doc.cursor_position_row) # type: ignore[reportUnknownMemberType]
|
|
125
|
+
col = cast(int, doc.cursor_position_col) # type: ignore[reportUnknownMemberType]
|
|
126
|
+
|
|
127
|
+
# At the beginning of a non-first line: jump to previous line end.
|
|
128
|
+
if col == 0 and row > 0:
|
|
129
|
+
lines = cast(list[str], doc.lines) # type: ignore[reportUnknownMemberType]
|
|
130
|
+
prev_row = row - 1
|
|
131
|
+
if 0 <= prev_row < len(lines):
|
|
132
|
+
prev_line = lines[prev_row]
|
|
133
|
+
new_index = doc.translate_row_col_to_index(prev_row, len(prev_line)) # type: ignore[reportUnknownMemberType]
|
|
134
|
+
buf.cursor_position = new_index # type: ignore[reportUnknownMemberType]
|
|
135
|
+
return
|
|
136
|
+
|
|
137
|
+
# Default behavior: move one character left when possible.
|
|
138
|
+
if doc.cursor_position > 0: # type: ignore[reportUnknownMemberType]
|
|
139
|
+
buf.cursor_left() # type: ignore[reportUnknownMemberType]
|
|
140
|
+
except Exception:
|
|
141
|
+
pass
|
|
142
|
+
|
|
143
|
+
@kb.add("right")
|
|
144
|
+
def _(event): # type: ignore
|
|
145
|
+
"""Support wrapping to next line when pressing right at line end."""
|
|
146
|
+
buf = event.current_buffer # type: ignore
|
|
147
|
+
try:
|
|
148
|
+
doc = buf.document # type: ignore[reportUnknownMemberType]
|
|
149
|
+
row = cast(int, doc.cursor_position_row) # type: ignore[reportUnknownMemberType]
|
|
150
|
+
col = cast(int, doc.cursor_position_col) # type: ignore[reportUnknownMemberType]
|
|
151
|
+
lines = cast(list[str], doc.lines) # type: ignore[reportUnknownMemberType]
|
|
152
|
+
|
|
153
|
+
current_line = lines[row] if 0 <= row < len(lines) else ""
|
|
154
|
+
at_line_end = col >= len(current_line)
|
|
155
|
+
is_last_line = row >= len(lines) - 1 if lines else True
|
|
156
|
+
|
|
157
|
+
# At end of a non-last line: jump to next line start.
|
|
158
|
+
if at_line_end and not is_last_line:
|
|
159
|
+
next_row = row + 1
|
|
160
|
+
new_index = doc.translate_row_col_to_index(next_row, 0) # type: ignore[reportUnknownMemberType]
|
|
161
|
+
buf.cursor_position = new_index # type: ignore[reportUnknownMemberType]
|
|
162
|
+
return
|
|
163
|
+
|
|
164
|
+
# Default behavior: move one character right when possible.
|
|
165
|
+
if doc.cursor_position < len(doc.text): # type: ignore[reportUnknownMemberType]
|
|
166
|
+
buf.cursor_right() # type: ignore[reportUnknownMemberType]
|
|
167
|
+
except Exception:
|
|
168
|
+
pass
|
|
169
|
+
|
|
170
|
+
return kb
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
import json
|
|
4
3
|
from contextlib import contextmanager
|
|
5
4
|
from dataclasses import dataclass
|
|
6
5
|
from typing import Any, Iterator
|
|
@@ -8,25 +7,25 @@ from typing import Any, Iterator
|
|
|
8
7
|
from rich import box
|
|
9
8
|
from rich.box import Box
|
|
10
9
|
from rich.console import Console
|
|
11
|
-
from rich.
|
|
10
|
+
from rich.spinner import Spinner
|
|
12
11
|
from rich.status import Status
|
|
13
12
|
from rich.style import Style, StyleType
|
|
14
13
|
from rich.text import Text
|
|
15
14
|
|
|
16
|
-
from klaude_code.
|
|
17
|
-
from klaude_code.
|
|
18
|
-
from klaude_code.ui.base.theme import ThemeKey, get_theme
|
|
15
|
+
from klaude_code.protocol import events, model
|
|
16
|
+
from klaude_code.ui.renderers import assistant as r_assistant
|
|
19
17
|
from klaude_code.ui.renderers import developer as r_developer
|
|
20
|
-
from klaude_code.ui.renderers import diffs as r_diffs
|
|
21
18
|
from klaude_code.ui.renderers import errors as r_errors
|
|
22
19
|
from klaude_code.ui.renderers import metadata as r_metadata
|
|
23
|
-
from klaude_code.ui.renderers import status as r_status
|
|
24
20
|
from klaude_code.ui.renderers import sub_agent as r_sub_agent
|
|
21
|
+
from klaude_code.ui.renderers import thinking as r_thinking
|
|
25
22
|
from klaude_code.ui.renderers import tools as r_tools
|
|
26
23
|
from klaude_code.ui.renderers import user_input as r_user_input
|
|
27
|
-
from klaude_code.ui.
|
|
28
|
-
from klaude_code.ui.
|
|
29
|
-
from klaude_code.ui.
|
|
24
|
+
from klaude_code.ui.rich import status as r_status
|
|
25
|
+
from klaude_code.ui.rich.quote import Quote
|
|
26
|
+
from klaude_code.ui.rich.status import ShimmerStatusText
|
|
27
|
+
from klaude_code.ui.rich.theme import ThemeKey, get_theme
|
|
28
|
+
from klaude_code.ui.utils.common import truncate_display
|
|
30
29
|
|
|
31
30
|
|
|
32
31
|
@dataclass
|
|
@@ -42,8 +41,8 @@ class REPLRenderer:
|
|
|
42
41
|
self.themes = get_theme(theme)
|
|
43
42
|
self.console: Console = Console(theme=self.themes.app_theme)
|
|
44
43
|
self.console.push_theme(self.themes.markdown_theme)
|
|
45
|
-
self.
|
|
46
|
-
|
|
44
|
+
self._spinner: Status = self.console.status(
|
|
45
|
+
ShimmerStatusText("Thinking …", ThemeKey.SPINNER_STATUS_TEXT),
|
|
47
46
|
spinner=r_status.spinner_name(),
|
|
48
47
|
spinner_style=ThemeKey.SPINNER_STATUS,
|
|
49
48
|
)
|
|
@@ -86,14 +85,6 @@ class REPLRenderer:
|
|
|
86
85
|
def box_style(self) -> Box:
|
|
87
86
|
return box.ROUNDED
|
|
88
87
|
|
|
89
|
-
@staticmethod
|
|
90
|
-
def _extract_diff_text(ui_extra: model.ToolResultUIExtra | None) -> str | None:
|
|
91
|
-
if ui_extra is None:
|
|
92
|
-
return None
|
|
93
|
-
if ui_extra.type == model.ToolResultUIExtraType.DIFF_TEXT:
|
|
94
|
-
return ui_extra.diff_text
|
|
95
|
-
return None
|
|
96
|
-
|
|
97
88
|
@contextmanager
|
|
98
89
|
def session_print_context(self, session_id: str) -> Iterator[None]:
|
|
99
90
|
"""Temporarily switch to sub-agent quote style."""
|
|
@@ -112,11 +103,10 @@ class REPLRenderer:
|
|
|
112
103
|
self.console.print(*objects, style=style, end=end)
|
|
113
104
|
|
|
114
105
|
def display_tool_call(self, e: events.ToolCallEvent) -> None:
|
|
106
|
+
# Handle sub-agent tool calls in replay mode
|
|
115
107
|
if r_tools.is_sub_agent_tool(e.tool_name):
|
|
116
|
-
# In replay mode, render sub-agent call here
|
|
117
|
-
# In normal execution, handled by TaskStartEvent
|
|
118
108
|
if e.is_replay:
|
|
119
|
-
state =
|
|
109
|
+
state = r_sub_agent.build_sub_agent_state_from_tool_call(e)
|
|
120
110
|
if state is not None:
|
|
121
111
|
sub_agent_default_style = (
|
|
122
112
|
self.themes.sub_agent_colors[0] if self.themes.sub_agent_colors else Style()
|
|
@@ -128,36 +118,14 @@ class REPLRenderer:
|
|
|
128
118
|
)
|
|
129
119
|
)
|
|
130
120
|
return
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
self.print(r_tools.render_edit_tool_call(e.arguments))
|
|
136
|
-
case tools.WRITE:
|
|
137
|
-
self.print(r_tools.render_write_tool_call(e.arguments))
|
|
138
|
-
case tools.MULTI_EDIT:
|
|
139
|
-
self.print(r_tools.render_multi_edit_tool_call(e.arguments))
|
|
140
|
-
case tools.BASH:
|
|
141
|
-
self.print(r_tools.render_generic_tool_call(e.tool_name, e.arguments, ">"))
|
|
142
|
-
case tools.APPLY_PATCH:
|
|
143
|
-
self.print(r_tools.render_apply_patch_tool_call(e.arguments))
|
|
144
|
-
case tools.TODO_WRITE:
|
|
145
|
-
self.print(r_tools.render_generic_tool_call("Update Todos", "", "◎"))
|
|
146
|
-
case tools.UPDATE_PLAN:
|
|
147
|
-
self.print(r_tools.render_update_plan_tool_call(e.arguments))
|
|
148
|
-
case tools.MERMAID:
|
|
149
|
-
self.print(r_tools.render_mermaid_tool_call(e.arguments))
|
|
150
|
-
case tools.MEMORY:
|
|
151
|
-
self.print(r_tools.render_memory_tool_call(e.arguments))
|
|
152
|
-
case tools.SKILL:
|
|
153
|
-
self.print(r_tools.render_generic_tool_call(e.tool_name, e.arguments, "◈"))
|
|
154
|
-
case _:
|
|
155
|
-
self.print(r_tools.render_generic_tool_call(e.tool_name, e.arguments))
|
|
121
|
+
|
|
122
|
+
renderable = r_tools.render_tool_call(e)
|
|
123
|
+
if renderable is not None:
|
|
124
|
+
self.print(renderable)
|
|
156
125
|
|
|
157
126
|
def display_tool_call_result(self, e: events.ToolResultEvent) -> None:
|
|
127
|
+
# Handle sub-agent tool results in replay mode
|
|
158
128
|
if r_tools.is_sub_agent_tool(e.tool_name):
|
|
159
|
-
# In replay mode, render sub-agent result here
|
|
160
|
-
# In normal execution, handled by TaskFinishEvent
|
|
161
129
|
if e.is_replay:
|
|
162
130
|
sub_agent_default_style = self.themes.sub_agent_colors[0] if self.themes.sub_agent_colors else Style()
|
|
163
131
|
self.print(
|
|
@@ -171,82 +139,20 @@ class REPLRenderer:
|
|
|
171
139
|
)
|
|
172
140
|
)
|
|
173
141
|
return
|
|
174
|
-
if e.status == "error" and e.ui_extra is None:
|
|
175
|
-
self.print(r_errors.render_error(Text(truncate_display(e.result))))
|
|
176
|
-
return
|
|
177
|
-
|
|
178
|
-
# Show truncation info if output was truncated and saved to file
|
|
179
|
-
truncation_info = r_tools.get_truncation_info(e)
|
|
180
|
-
if truncation_info:
|
|
181
|
-
self.print(r_tools.render_truncation_info(truncation_info))
|
|
182
|
-
return
|
|
183
142
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
case tools.READ:
|
|
188
|
-
pass
|
|
189
|
-
case tools.EDIT | tools.MULTI_EDIT | tools.WRITE:
|
|
190
|
-
self.print(Padding.indent(r_diffs.render_diff(diff_text or ""), level=2))
|
|
191
|
-
case tools.MEMORY:
|
|
192
|
-
if diff_text:
|
|
193
|
-
self.print(Padding.indent(r_diffs.render_diff(diff_text), level=2))
|
|
194
|
-
elif len(e.result.strip()) > 0:
|
|
195
|
-
self.print(r_tools.render_generic_tool_result(e.result))
|
|
196
|
-
case tools.TODO_WRITE | tools.UPDATE_PLAN:
|
|
197
|
-
self.print(r_tools.render_todo(e))
|
|
198
|
-
case tools.MERMAID:
|
|
199
|
-
self.print(r_tools.render_mermaid_tool_result(e))
|
|
200
|
-
case _:
|
|
201
|
-
if e.tool_name in (tools.BASH, tools.APPLY_PATCH) and e.result.startswith("diff --git"):
|
|
202
|
-
self.print(r_diffs.render_diff_panel(e.result, show_file_name=True))
|
|
203
|
-
return
|
|
204
|
-
if e.tool_name == tools.APPLY_PATCH and diff_text:
|
|
205
|
-
self.print(Padding.indent(r_diffs.render_diff(diff_text, show_file_name=True), level=2))
|
|
206
|
-
return
|
|
207
|
-
if len(e.result.strip()) == 0:
|
|
208
|
-
e.result = "(no content)"
|
|
209
|
-
self.print(r_tools.render_generic_tool_result(e.result))
|
|
210
|
-
|
|
211
|
-
def _build_sub_agent_state_from_tool_call(self, e: events.ToolCallEvent) -> model.SubAgentState | None:
|
|
212
|
-
profile = get_sub_agent_profile_by_tool(e.tool_name)
|
|
213
|
-
if profile is None:
|
|
214
|
-
return None
|
|
215
|
-
description = profile.name
|
|
216
|
-
prompt = ""
|
|
217
|
-
if e.arguments:
|
|
218
|
-
try:
|
|
219
|
-
payload: dict[str, object] = json.loads(e.arguments)
|
|
220
|
-
except json.JSONDecodeError:
|
|
221
|
-
payload = {}
|
|
222
|
-
desc_value = payload.get("description")
|
|
223
|
-
if isinstance(desc_value, str) and desc_value.strip():
|
|
224
|
-
description = desc_value.strip()
|
|
225
|
-
prompt_value = payload.get("prompt") or payload.get("task")
|
|
226
|
-
if isinstance(prompt_value, str):
|
|
227
|
-
prompt = prompt_value.strip()
|
|
228
|
-
return model.SubAgentState(
|
|
229
|
-
sub_agent_type=profile.name,
|
|
230
|
-
sub_agent_desc=description,
|
|
231
|
-
sub_agent_prompt=prompt,
|
|
232
|
-
)
|
|
143
|
+
renderable = r_tools.render_tool_result(e)
|
|
144
|
+
if renderable is not None:
|
|
145
|
+
self.print(renderable)
|
|
233
146
|
|
|
234
147
|
def display_thinking(self, content: str) -> None:
|
|
235
|
-
|
|
148
|
+
renderable = r_thinking.render_thinking(
|
|
149
|
+
content,
|
|
150
|
+
code_theme=self.themes.code_theme,
|
|
151
|
+
style=ThemeKey.THINKING,
|
|
152
|
+
)
|
|
153
|
+
if renderable is not None:
|
|
236
154
|
self.console.push_theme(theme=self.themes.thinking_markdown_theme)
|
|
237
|
-
self.print(
|
|
238
|
-
Padding.indent(
|
|
239
|
-
NoInsetMarkdown(
|
|
240
|
-
content.rstrip()
|
|
241
|
-
.replace("**\n\n", "** \n")
|
|
242
|
-
.replace("\\n\\n\n\n", "") # Weird case of Gemini 3
|
|
243
|
-
.replace("****", "**\n\n**"), # remove extra newlines after bold titles
|
|
244
|
-
code_theme=self.themes.code_theme,
|
|
245
|
-
style=self.console.get_style(ThemeKey.THINKING),
|
|
246
|
-
),
|
|
247
|
-
level=2,
|
|
248
|
-
)
|
|
249
|
-
)
|
|
155
|
+
self.print(renderable)
|
|
250
156
|
self.console.pop_theme()
|
|
251
157
|
self.print()
|
|
252
158
|
|
|
@@ -257,16 +163,11 @@ class REPLRenderer:
|
|
|
257
163
|
case events.TurnStartEvent():
|
|
258
164
|
self.print()
|
|
259
165
|
case events.AssistantMessageEvent() as assistant_event:
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
assistant_event.content.strip(),
|
|
266
|
-
code_theme=self.themes.code_theme,
|
|
267
|
-
),
|
|
268
|
-
)
|
|
269
|
-
self.print(grid)
|
|
166
|
+
renderable = r_assistant.render_assistant_message(
|
|
167
|
+
assistant_event.content, code_theme=self.themes.code_theme
|
|
168
|
+
)
|
|
169
|
+
if renderable is not None:
|
|
170
|
+
self.print(renderable)
|
|
270
171
|
self.print()
|
|
271
172
|
case events.ThinkingEvent() as thinking_event:
|
|
272
173
|
self.display_thinking(thinking_event.content)
|
|
@@ -302,3 +203,79 @@ class REPLRenderer:
|
|
|
302
203
|
with self.session_print_context(e.session_id):
|
|
303
204
|
self.print(r_developer.render_command_output(e))
|
|
304
205
|
self.print()
|
|
206
|
+
|
|
207
|
+
def display_welcome(self, event: events.WelcomeEvent) -> None:
|
|
208
|
+
self.print(r_metadata.render_welcome(event, box_style=self.box_style()))
|
|
209
|
+
|
|
210
|
+
def display_user_message(self, event: events.UserMessageEvent) -> None:
|
|
211
|
+
self.print(r_user_input.render_user_input(event.content))
|
|
212
|
+
|
|
213
|
+
def display_task_start(self, event: events.TaskStartEvent) -> None:
|
|
214
|
+
self.register_session(event.session_id, event.sub_agent_state)
|
|
215
|
+
if event.sub_agent_state is not None:
|
|
216
|
+
with self.session_print_context(event.session_id):
|
|
217
|
+
self.print(
|
|
218
|
+
r_sub_agent.render_sub_agent_call(
|
|
219
|
+
event.sub_agent_state,
|
|
220
|
+
self.get_session_sub_agent_color(event.session_id),
|
|
221
|
+
)
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
def display_turn_start(self, event: events.TurnStartEvent) -> None:
|
|
225
|
+
if not self.is_sub_agent_session(event.session_id):
|
|
226
|
+
self.print()
|
|
227
|
+
|
|
228
|
+
def display_assistant_message(self, content: str) -> None:
|
|
229
|
+
renderable = r_assistant.render_assistant_message(content, code_theme=self.themes.code_theme)
|
|
230
|
+
if renderable is not None:
|
|
231
|
+
self.print(renderable)
|
|
232
|
+
self.print()
|
|
233
|
+
|
|
234
|
+
def display_response_metadata(self, event: events.ResponseMetadataEvent) -> None:
|
|
235
|
+
with self.session_print_context(event.session_id):
|
|
236
|
+
self.print(r_metadata.render_response_metadata(event))
|
|
237
|
+
self.print()
|
|
238
|
+
|
|
239
|
+
def display_task_finish(self, event: events.TaskFinishEvent) -> None:
|
|
240
|
+
if self.is_sub_agent_session(event.session_id):
|
|
241
|
+
with self.session_print_context(event.session_id):
|
|
242
|
+
self.print(
|
|
243
|
+
r_sub_agent.render_sub_agent_result(
|
|
244
|
+
event.task_result,
|
|
245
|
+
code_theme=self.themes.code_theme,
|
|
246
|
+
)
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
def display_interrupt(self) -> None:
|
|
250
|
+
self.print(r_user_input.render_interrupt())
|
|
251
|
+
|
|
252
|
+
def display_error(self, event: events.ErrorEvent) -> None:
|
|
253
|
+
self.print(
|
|
254
|
+
r_errors.render_error(
|
|
255
|
+
self.console.render_str(truncate_display(event.error_message)),
|
|
256
|
+
indent=0,
|
|
257
|
+
)
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
def display_thinking_prefix(self) -> None:
|
|
261
|
+
self.print(r_thinking.thinking_prefix())
|
|
262
|
+
|
|
263
|
+
# -------------------------------------------------------------------------
|
|
264
|
+
# Spinner control methods
|
|
265
|
+
# -------------------------------------------------------------------------
|
|
266
|
+
|
|
267
|
+
def spinner_start(self) -> None:
|
|
268
|
+
"""Start the spinner animation."""
|
|
269
|
+
self._spinner.start()
|
|
270
|
+
|
|
271
|
+
def spinner_stop(self) -> None:
|
|
272
|
+
"""Stop the spinner animation."""
|
|
273
|
+
self._spinner.stop()
|
|
274
|
+
|
|
275
|
+
def spinner_update(self, status_text: str | Text) -> None:
|
|
276
|
+
"""Update the spinner status text."""
|
|
277
|
+
self._spinner.update(ShimmerStatusText(status_text, ThemeKey.SPINNER_STATUS_TEXT))
|
|
278
|
+
|
|
279
|
+
def spinner_renderable(self) -> Spinner:
|
|
280
|
+
"""Return the spinner's renderable for embedding in other components."""
|
|
281
|
+
return self._spinner.renderable
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from rich.console import RenderableType
|
|
2
|
+
|
|
3
|
+
from klaude_code.ui.renderers.common import create_grid
|
|
4
|
+
from klaude_code.ui.rich.markdown import NoInsetMarkdown
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def render_assistant_message(content: str, *, code_theme: str) -> RenderableType | None:
|
|
8
|
+
"""Render assistant message for replay history display.
|
|
9
|
+
|
|
10
|
+
Returns None if content is empty.
|
|
11
|
+
"""
|
|
12
|
+
stripped = content.strip()
|
|
13
|
+
if len(stripped) == 0:
|
|
14
|
+
return None
|
|
15
|
+
|
|
16
|
+
grid = create_grid()
|
|
17
|
+
grid.add_row(
|
|
18
|
+
"•",
|
|
19
|
+
NoInsetMarkdown(stripped, code_theme=code_theme),
|
|
20
|
+
)
|
|
21
|
+
return grid
|
|
@@ -1,24 +1,8 @@
|
|
|
1
1
|
from rich.table import Table
|
|
2
2
|
|
|
3
|
-
from klaude_code.const import TRUNCATE_DISPLAY_MAX_LINE_LENGTH, TRUNCATE_DISPLAY_MAX_LINES
|
|
4
|
-
|
|
5
3
|
|
|
6
4
|
def create_grid() -> Table:
|
|
7
5
|
grid = Table.grid(padding=(0, 1))
|
|
8
6
|
grid.add_column(no_wrap=True)
|
|
9
7
|
grid.add_column(overflow="fold")
|
|
10
8
|
return grid
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
def truncate_display(
|
|
14
|
-
text: str, max_lines: int = TRUNCATE_DISPLAY_MAX_LINES, max_line_length: int = TRUNCATE_DISPLAY_MAX_LINE_LENGTH
|
|
15
|
-
) -> str:
|
|
16
|
-
lines = text.split("\n")
|
|
17
|
-
if len(lines) > max_lines:
|
|
18
|
-
lines = lines[:max_lines] + ["… (more " + str(len(lines) - max_lines) + " lines)"]
|
|
19
|
-
for i, line in enumerate(lines):
|
|
20
|
-
if len(line) > max_line_length:
|
|
21
|
-
lines[i] = (
|
|
22
|
-
line[:max_line_length] + "… (more " + str(len(line) - max_line_length) + " characters in this line)"
|
|
23
|
-
)
|
|
24
|
-
return "\n".join(lines)
|
|
@@ -2,12 +2,12 @@ from rich.console import Group, RenderableType
|
|
|
2
2
|
from rich.padding import Padding
|
|
3
3
|
from rich.text import Text
|
|
4
4
|
|
|
5
|
-
from klaude_code.protocol import events
|
|
6
|
-
from klaude_code.protocol.commands import CommandName
|
|
7
|
-
from klaude_code.ui.base.theme import ThemeKey
|
|
5
|
+
from klaude_code.protocol import commands, events
|
|
8
6
|
from klaude_code.ui.renderers import diffs as r_diffs
|
|
9
|
-
from klaude_code.ui.renderers.common import create_grid
|
|
7
|
+
from klaude_code.ui.renderers.common import create_grid
|
|
10
8
|
from klaude_code.ui.renderers.tools import render_path
|
|
9
|
+
from klaude_code.ui.rich.theme import ThemeKey
|
|
10
|
+
from klaude_code.ui.utils.common import truncate_display
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
def need_render_developer_message(e: events.DeveloperMessageEvent) -> bool:
|
|
@@ -16,7 +16,7 @@ def need_render_developer_message(e: events.DeveloperMessageEvent) -> bool:
|
|
|
16
16
|
or e.item.external_file_changes
|
|
17
17
|
or e.item.todo_use
|
|
18
18
|
or e.item.at_files
|
|
19
|
-
or e.item.
|
|
19
|
+
or e.item.user_image_count
|
|
20
20
|
)
|
|
21
21
|
|
|
22
22
|
|
|
@@ -56,7 +56,10 @@ def render_developer_message(e: events.DeveloperMessageEvent) -> RenderableType:
|
|
|
56
56
|
|
|
57
57
|
if e.item.todo_use:
|
|
58
58
|
grid = create_grid()
|
|
59
|
-
grid.add_row(
|
|
59
|
+
grid.add_row(
|
|
60
|
+
Text(" +", style=ThemeKey.REMINDER),
|
|
61
|
+
Text("Todo hasn't been updated recently", ThemeKey.REMINDER),
|
|
62
|
+
)
|
|
60
63
|
parts.append(grid)
|
|
61
64
|
|
|
62
65
|
if e.item.at_files:
|
|
@@ -65,21 +68,18 @@ def render_developer_message(e: events.DeveloperMessageEvent) -> RenderableType:
|
|
|
65
68
|
grid.add_row(
|
|
66
69
|
Text(" +", style=ThemeKey.REMINDER),
|
|
67
70
|
Text.assemble(
|
|
68
|
-
(f"{at_file.operation} ", ThemeKey.REMINDER),
|
|
71
|
+
(f"{at_file.operation} ", ThemeKey.REMINDER),
|
|
72
|
+
render_path(at_file.path, ThemeKey.REMINDER_BOLD),
|
|
69
73
|
),
|
|
70
74
|
)
|
|
71
75
|
parts.append(grid)
|
|
72
76
|
|
|
73
|
-
if
|
|
77
|
+
if uic := e.item.user_image_count:
|
|
74
78
|
grid = create_grid()
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
("Read ", ThemeKey.REMINDER),
|
|
80
|
-
Text(f"{img_tag} Image", style=ThemeKey.REMINDER_BOLD),
|
|
81
|
-
),
|
|
82
|
-
)
|
|
79
|
+
grid.add_row(
|
|
80
|
+
Text(" +", style=ThemeKey.REMINDER),
|
|
81
|
+
Text(f"Attached {uic} image{'s' if uic > 1 else ''}", style=ThemeKey.REMINDER),
|
|
82
|
+
)
|
|
83
83
|
parts.append(grid)
|
|
84
84
|
|
|
85
85
|
return Group(*parts) if parts else Text("")
|
|
@@ -91,11 +91,11 @@ def render_command_output(e: events.DeveloperMessageEvent) -> RenderableType:
|
|
|
91
91
|
return Text("")
|
|
92
92
|
|
|
93
93
|
match e.item.command_output.command_name:
|
|
94
|
-
case CommandName.DIFF:
|
|
94
|
+
case commands.CommandName.DIFF:
|
|
95
95
|
if e.item.content is None or len(e.item.content) == 0:
|
|
96
96
|
return Padding.indent(Text("(no changes)", style=ThemeKey.TOOL_RESULT), level=2)
|
|
97
97
|
return r_diffs.render_diff_panel(e.item.content, show_file_name=True)
|
|
98
|
-
case CommandName.HELP:
|
|
98
|
+
case commands.CommandName.HELP:
|
|
99
99
|
return Padding.indent(Text.from_markup(e.item.content or ""), level=2)
|
|
100
100
|
case _:
|
|
101
101
|
content = e.item.content or "(no content)"
|