klaude-code 1.2.6__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/__init__.py +0 -0
- klaude_code/cli/__init__.py +1 -0
- klaude_code/cli/main.py +298 -0
- klaude_code/cli/runtime.py +331 -0
- klaude_code/cli/session_cmd.py +80 -0
- klaude_code/command/__init__.py +43 -0
- klaude_code/command/clear_cmd.py +20 -0
- klaude_code/command/command_abc.py +92 -0
- klaude_code/command/diff_cmd.py +138 -0
- klaude_code/command/export_cmd.py +86 -0
- klaude_code/command/help_cmd.py +51 -0
- klaude_code/command/model_cmd.py +43 -0
- klaude_code/command/prompt-dev-docs-update.md +56 -0
- klaude_code/command/prompt-dev-docs.md +46 -0
- klaude_code/command/prompt-init.md +45 -0
- klaude_code/command/prompt_command.py +69 -0
- klaude_code/command/refresh_cmd.py +43 -0
- klaude_code/command/registry.py +110 -0
- klaude_code/command/status_cmd.py +111 -0
- klaude_code/command/terminal_setup_cmd.py +252 -0
- klaude_code/config/__init__.py +11 -0
- klaude_code/config/config.py +177 -0
- klaude_code/config/list_model.py +162 -0
- klaude_code/config/select_model.py +67 -0
- klaude_code/const/__init__.py +133 -0
- klaude_code/core/__init__.py +0 -0
- klaude_code/core/agent.py +165 -0
- klaude_code/core/executor.py +485 -0
- klaude_code/core/manager/__init__.py +19 -0
- klaude_code/core/manager/agent_manager.py +127 -0
- klaude_code/core/manager/llm_clients.py +42 -0
- klaude_code/core/manager/llm_clients_builder.py +49 -0
- klaude_code/core/manager/sub_agent_manager.py +86 -0
- klaude_code/core/prompt.py +89 -0
- klaude_code/core/prompts/prompt-claude-code.md +98 -0
- klaude_code/core/prompts/prompt-codex.md +331 -0
- klaude_code/core/prompts/prompt-gemini.md +43 -0
- klaude_code/core/prompts/prompt-subagent-explore.md +27 -0
- klaude_code/core/prompts/prompt-subagent-oracle.md +23 -0
- klaude_code/core/prompts/prompt-subagent-webfetch.md +46 -0
- klaude_code/core/prompts/prompt-subagent.md +8 -0
- klaude_code/core/reminders.py +445 -0
- klaude_code/core/task.py +237 -0
- klaude_code/core/tool/__init__.py +75 -0
- klaude_code/core/tool/file/__init__.py +0 -0
- klaude_code/core/tool/file/apply_patch.py +492 -0
- klaude_code/core/tool/file/apply_patch_tool.md +1 -0
- klaude_code/core/tool/file/apply_patch_tool.py +204 -0
- klaude_code/core/tool/file/edit_tool.md +9 -0
- klaude_code/core/tool/file/edit_tool.py +274 -0
- klaude_code/core/tool/file/multi_edit_tool.md +42 -0
- klaude_code/core/tool/file/multi_edit_tool.py +199 -0
- klaude_code/core/tool/file/read_tool.md +14 -0
- klaude_code/core/tool/file/read_tool.py +326 -0
- klaude_code/core/tool/file/write_tool.md +8 -0
- klaude_code/core/tool/file/write_tool.py +146 -0
- klaude_code/core/tool/memory/__init__.py +0 -0
- klaude_code/core/tool/memory/memory_tool.md +16 -0
- klaude_code/core/tool/memory/memory_tool.py +462 -0
- klaude_code/core/tool/memory/skill_loader.py +245 -0
- klaude_code/core/tool/memory/skill_tool.md +24 -0
- klaude_code/core/tool/memory/skill_tool.py +97 -0
- klaude_code/core/tool/shell/__init__.py +0 -0
- klaude_code/core/tool/shell/bash_tool.md +43 -0
- klaude_code/core/tool/shell/bash_tool.py +123 -0
- klaude_code/core/tool/shell/command_safety.py +363 -0
- klaude_code/core/tool/sub_agent_tool.py +83 -0
- klaude_code/core/tool/todo/__init__.py +0 -0
- klaude_code/core/tool/todo/todo_write_tool.md +182 -0
- klaude_code/core/tool/todo/todo_write_tool.py +121 -0
- klaude_code/core/tool/todo/update_plan_tool.md +3 -0
- klaude_code/core/tool/todo/update_plan_tool.py +104 -0
- klaude_code/core/tool/tool_abc.py +25 -0
- klaude_code/core/tool/tool_context.py +106 -0
- klaude_code/core/tool/tool_registry.py +78 -0
- klaude_code/core/tool/tool_runner.py +252 -0
- klaude_code/core/tool/truncation.py +170 -0
- klaude_code/core/tool/web/__init__.py +0 -0
- klaude_code/core/tool/web/mermaid_tool.md +21 -0
- klaude_code/core/tool/web/mermaid_tool.py +76 -0
- klaude_code/core/tool/web/web_fetch_tool.md +8 -0
- klaude_code/core/tool/web/web_fetch_tool.py +159 -0
- klaude_code/core/turn.py +220 -0
- klaude_code/llm/__init__.py +21 -0
- klaude_code/llm/anthropic/__init__.py +3 -0
- klaude_code/llm/anthropic/client.py +221 -0
- klaude_code/llm/anthropic/input.py +200 -0
- klaude_code/llm/client.py +49 -0
- klaude_code/llm/input_common.py +239 -0
- klaude_code/llm/openai_compatible/__init__.py +3 -0
- klaude_code/llm/openai_compatible/client.py +211 -0
- klaude_code/llm/openai_compatible/input.py +109 -0
- klaude_code/llm/openai_compatible/tool_call_accumulator.py +80 -0
- klaude_code/llm/openrouter/__init__.py +3 -0
- klaude_code/llm/openrouter/client.py +200 -0
- klaude_code/llm/openrouter/input.py +160 -0
- klaude_code/llm/openrouter/reasoning_handler.py +209 -0
- klaude_code/llm/registry.py +22 -0
- klaude_code/llm/responses/__init__.py +3 -0
- klaude_code/llm/responses/client.py +216 -0
- klaude_code/llm/responses/input.py +167 -0
- klaude_code/llm/usage.py +109 -0
- klaude_code/protocol/__init__.py +4 -0
- klaude_code/protocol/commands.py +21 -0
- klaude_code/protocol/events.py +163 -0
- klaude_code/protocol/llm_param.py +147 -0
- klaude_code/protocol/model.py +287 -0
- klaude_code/protocol/op.py +89 -0
- klaude_code/protocol/op_handler.py +28 -0
- klaude_code/protocol/sub_agent.py +348 -0
- klaude_code/protocol/tools.py +15 -0
- klaude_code/session/__init__.py +4 -0
- klaude_code/session/export.py +624 -0
- klaude_code/session/selector.py +76 -0
- klaude_code/session/session.py +474 -0
- klaude_code/session/templates/export_session.html +1434 -0
- klaude_code/trace/__init__.py +3 -0
- klaude_code/trace/log.py +168 -0
- klaude_code/ui/__init__.py +91 -0
- 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/core/stage_manager.py +55 -0
- klaude_code/ui/modes/__init__.py +1 -0
- klaude_code/ui/modes/debug/__init__.py +1 -0
- klaude_code/ui/modes/debug/display.py +36 -0
- klaude_code/ui/modes/exec/__init__.py +1 -0
- klaude_code/ui/modes/exec/display.py +63 -0
- klaude_code/ui/modes/repl/__init__.py +51 -0
- 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/modes/repl/renderer.py +281 -0
- klaude_code/ui/renderers/__init__.py +0 -0
- klaude_code/ui/renderers/assistant.py +21 -0
- klaude_code/ui/renderers/common.py +8 -0
- klaude_code/ui/renderers/developer.py +158 -0
- klaude_code/ui/renderers/diffs.py +215 -0
- klaude_code/ui/renderers/errors.py +16 -0
- klaude_code/ui/renderers/metadata.py +190 -0
- klaude_code/ui/renderers/sub_agent.py +71 -0
- klaude_code/ui/renderers/thinking.py +39 -0
- klaude_code/ui/renderers/tools.py +551 -0
- klaude_code/ui/renderers/user_input.py +65 -0
- klaude_code/ui/rich/__init__.py +1 -0
- klaude_code/ui/rich/live.py +65 -0
- klaude_code/ui/rich/markdown.py +308 -0
- klaude_code/ui/rich/quote.py +34 -0
- klaude_code/ui/rich/searchable_text.py +71 -0
- klaude_code/ui/rich/status.py +240 -0
- klaude_code/ui/rich/theme.py +274 -0
- klaude_code/ui/terminal/__init__.py +1 -0
- klaude_code/ui/terminal/color.py +244 -0
- klaude_code/ui/terminal/control.py +147 -0
- klaude_code/ui/terminal/notifier.py +107 -0
- klaude_code/ui/terminal/progress_bar.py +87 -0
- klaude_code/ui/utils/__init__.py +1 -0
- klaude_code/ui/utils/common.py +108 -0
- klaude_code/ui/utils/debouncer.py +42 -0
- klaude_code/version.py +163 -0
- klaude_code-1.2.6.dist-info/METADATA +178 -0
- klaude_code-1.2.6.dist-info/RECORD +167 -0
- klaude_code-1.2.6.dist-info/WHEEL +4 -0
- klaude_code-1.2.6.dist-info/entry_points.txt +3 -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
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from contextlib import contextmanager
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from typing import Any, Iterator
|
|
6
|
+
|
|
7
|
+
from rich import box
|
|
8
|
+
from rich.box import Box
|
|
9
|
+
from rich.console import Console
|
|
10
|
+
from rich.spinner import Spinner
|
|
11
|
+
from rich.status import Status
|
|
12
|
+
from rich.style import Style, StyleType
|
|
13
|
+
from rich.text import Text
|
|
14
|
+
|
|
15
|
+
from klaude_code.protocol import events, model
|
|
16
|
+
from klaude_code.ui.renderers import assistant as r_assistant
|
|
17
|
+
from klaude_code.ui.renderers import developer as r_developer
|
|
18
|
+
from klaude_code.ui.renderers import errors as r_errors
|
|
19
|
+
from klaude_code.ui.renderers import metadata as r_metadata
|
|
20
|
+
from klaude_code.ui.renderers import sub_agent as r_sub_agent
|
|
21
|
+
from klaude_code.ui.renderers import thinking as r_thinking
|
|
22
|
+
from klaude_code.ui.renderers import tools as r_tools
|
|
23
|
+
from klaude_code.ui.renderers import user_input as r_user_input
|
|
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
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class SessionStatus:
|
|
33
|
+
color: Style | None = None
|
|
34
|
+
sub_agent_state: model.SubAgentState | None = None
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class REPLRenderer:
|
|
38
|
+
"""Render REPL content via a Rich console."""
|
|
39
|
+
|
|
40
|
+
def __init__(self, theme: str | None = None):
|
|
41
|
+
self.themes = get_theme(theme)
|
|
42
|
+
self.console: Console = Console(theme=self.themes.app_theme)
|
|
43
|
+
self.console.push_theme(self.themes.markdown_theme)
|
|
44
|
+
self._spinner: Status = self.console.status(
|
|
45
|
+
ShimmerStatusText("Thinking …", ThemeKey.SPINNER_STATUS_TEXT),
|
|
46
|
+
spinner=r_status.spinner_name(),
|
|
47
|
+
spinner_style=ThemeKey.SPINNER_STATUS,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
self.session_map: dict[str, SessionStatus] = {}
|
|
51
|
+
self.current_sub_agent_color: Style | None = None
|
|
52
|
+
self.subagent_color_index = 0
|
|
53
|
+
|
|
54
|
+
def register_session(self, session_id: str, sub_agent_state: model.SubAgentState | None = None) -> None:
|
|
55
|
+
session_status = SessionStatus(
|
|
56
|
+
sub_agent_state=sub_agent_state,
|
|
57
|
+
)
|
|
58
|
+
if sub_agent_state is not None:
|
|
59
|
+
session_status.color = self.pick_sub_agent_color()
|
|
60
|
+
self.session_map[session_id] = session_status
|
|
61
|
+
|
|
62
|
+
def is_sub_agent_session(self, session_id: str) -> bool:
|
|
63
|
+
return session_id in self.session_map and self.session_map[session_id].sub_agent_state is not None
|
|
64
|
+
|
|
65
|
+
def _advance_sub_agent_color_index(self) -> None:
|
|
66
|
+
palette_size = len(self.themes.sub_agent_colors)
|
|
67
|
+
if palette_size == 0:
|
|
68
|
+
self.subagent_color_index = 0
|
|
69
|
+
return
|
|
70
|
+
self.subagent_color_index = (self.subagent_color_index + 1) % palette_size
|
|
71
|
+
|
|
72
|
+
def pick_sub_agent_color(self) -> Style:
|
|
73
|
+
self._advance_sub_agent_color_index()
|
|
74
|
+
palette = self.themes.sub_agent_colors
|
|
75
|
+
if not palette:
|
|
76
|
+
return Style()
|
|
77
|
+
return palette[self.subagent_color_index]
|
|
78
|
+
|
|
79
|
+
def get_session_sub_agent_color(self, session_id: str) -> Style:
|
|
80
|
+
status = self.session_map.get(session_id)
|
|
81
|
+
if status and status.color:
|
|
82
|
+
return status.color
|
|
83
|
+
return Style()
|
|
84
|
+
|
|
85
|
+
def box_style(self) -> Box:
|
|
86
|
+
return box.ROUNDED
|
|
87
|
+
|
|
88
|
+
@contextmanager
|
|
89
|
+
def session_print_context(self, session_id: str) -> Iterator[None]:
|
|
90
|
+
"""Temporarily switch to sub-agent quote style."""
|
|
91
|
+
if session_id in self.session_map and self.session_map[session_id].color:
|
|
92
|
+
self.current_sub_agent_color = self.session_map[session_id].color
|
|
93
|
+
try:
|
|
94
|
+
yield
|
|
95
|
+
finally:
|
|
96
|
+
self.current_sub_agent_color = None
|
|
97
|
+
|
|
98
|
+
def print(self, *objects: Any, style: StyleType | None = None, end: str = "\n") -> None:
|
|
99
|
+
if self.current_sub_agent_color:
|
|
100
|
+
if objects:
|
|
101
|
+
self.console.print(Quote(*objects, style=self.current_sub_agent_color))
|
|
102
|
+
return
|
|
103
|
+
self.console.print(*objects, style=style, end=end)
|
|
104
|
+
|
|
105
|
+
def display_tool_call(self, e: events.ToolCallEvent) -> None:
|
|
106
|
+
# Handle sub-agent tool calls in replay mode
|
|
107
|
+
if r_tools.is_sub_agent_tool(e.tool_name):
|
|
108
|
+
if e.is_replay:
|
|
109
|
+
state = r_sub_agent.build_sub_agent_state_from_tool_call(e)
|
|
110
|
+
if state is not None:
|
|
111
|
+
sub_agent_default_style = (
|
|
112
|
+
self.themes.sub_agent_colors[0] if self.themes.sub_agent_colors else Style()
|
|
113
|
+
)
|
|
114
|
+
self.print(
|
|
115
|
+
Quote(
|
|
116
|
+
r_sub_agent.render_sub_agent_call(state, sub_agent_default_style),
|
|
117
|
+
style=sub_agent_default_style,
|
|
118
|
+
)
|
|
119
|
+
)
|
|
120
|
+
return
|
|
121
|
+
|
|
122
|
+
renderable = r_tools.render_tool_call(e)
|
|
123
|
+
if renderable is not None:
|
|
124
|
+
self.print(renderable)
|
|
125
|
+
|
|
126
|
+
def display_tool_call_result(self, e: events.ToolResultEvent) -> None:
|
|
127
|
+
# Handle sub-agent tool results in replay mode
|
|
128
|
+
if r_tools.is_sub_agent_tool(e.tool_name):
|
|
129
|
+
if e.is_replay:
|
|
130
|
+
sub_agent_default_style = self.themes.sub_agent_colors[0] if self.themes.sub_agent_colors else Style()
|
|
131
|
+
self.print(
|
|
132
|
+
Quote(
|
|
133
|
+
r_sub_agent.render_sub_agent_result(
|
|
134
|
+
e.result,
|
|
135
|
+
code_theme=self.themes.code_theme,
|
|
136
|
+
style=sub_agent_default_style,
|
|
137
|
+
),
|
|
138
|
+
style=sub_agent_default_style,
|
|
139
|
+
)
|
|
140
|
+
)
|
|
141
|
+
return
|
|
142
|
+
|
|
143
|
+
renderable = r_tools.render_tool_result(e)
|
|
144
|
+
if renderable is not None:
|
|
145
|
+
self.print(renderable)
|
|
146
|
+
|
|
147
|
+
def display_thinking(self, content: str) -> None:
|
|
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:
|
|
154
|
+
self.console.push_theme(theme=self.themes.thinking_markdown_theme)
|
|
155
|
+
self.print(renderable)
|
|
156
|
+
self.console.pop_theme()
|
|
157
|
+
self.print()
|
|
158
|
+
|
|
159
|
+
async def replay_history(self, history_events: events.ReplayHistoryEvent) -> None:
|
|
160
|
+
tool_call_dict: dict[str, events.ToolCallEvent] = {}
|
|
161
|
+
for event in history_events.events:
|
|
162
|
+
match event:
|
|
163
|
+
case events.TurnStartEvent():
|
|
164
|
+
self.print()
|
|
165
|
+
case events.AssistantMessageEvent() as assistant_event:
|
|
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)
|
|
171
|
+
self.print()
|
|
172
|
+
case events.ThinkingEvent() as thinking_event:
|
|
173
|
+
self.display_thinking(thinking_event.content)
|
|
174
|
+
case events.DeveloperMessageEvent() as developer_event:
|
|
175
|
+
self.display_developer_message(developer_event)
|
|
176
|
+
self.display_command_output(developer_event)
|
|
177
|
+
case events.UserMessageEvent() as user_event:
|
|
178
|
+
self.print(r_user_input.render_user_input(user_event.content))
|
|
179
|
+
case events.ToolCallEvent() as tool_call_event:
|
|
180
|
+
tool_call_dict[tool_call_event.tool_call_id] = tool_call_event
|
|
181
|
+
case events.ToolResultEvent() as tool_result_event:
|
|
182
|
+
tool_call_event = tool_call_dict.get(tool_result_event.tool_call_id)
|
|
183
|
+
if tool_call_event is not None:
|
|
184
|
+
self.display_tool_call(tool_call_event)
|
|
185
|
+
tool_call_dict.pop(tool_result_event.tool_call_id, None)
|
|
186
|
+
self.display_tool_call_result(tool_result_event)
|
|
187
|
+
case events.ResponseMetadataEvent() as metadata_event:
|
|
188
|
+
self.print(r_metadata.render_response_metadata(metadata_event))
|
|
189
|
+
self.print()
|
|
190
|
+
case events.InterruptEvent():
|
|
191
|
+
self.print()
|
|
192
|
+
self.print(r_user_input.render_interrupt())
|
|
193
|
+
|
|
194
|
+
def display_developer_message(self, e: events.DeveloperMessageEvent) -> None:
|
|
195
|
+
if not r_developer.need_render_developer_message(e):
|
|
196
|
+
return
|
|
197
|
+
with self.session_print_context(e.session_id):
|
|
198
|
+
self.print(r_developer.render_developer_message(e))
|
|
199
|
+
|
|
200
|
+
def display_command_output(self, e: events.DeveloperMessageEvent) -> None:
|
|
201
|
+
if not e.item.command_output:
|
|
202
|
+
return
|
|
203
|
+
with self.session_print_context(e.session_id):
|
|
204
|
+
self.print(r_developer.render_command_output(e))
|
|
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
|
|
File without changes
|
|
@@ -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
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
from rich.console import Group, RenderableType
|
|
2
|
+
from rich.padding import Padding
|
|
3
|
+
from rich.table import Table
|
|
4
|
+
from rich.text import Text
|
|
5
|
+
|
|
6
|
+
from klaude_code.protocol import commands, events, model
|
|
7
|
+
from klaude_code.ui.renderers import diffs as r_diffs
|
|
8
|
+
from klaude_code.ui.renderers.common import create_grid
|
|
9
|
+
from klaude_code.ui.renderers.tools import render_path
|
|
10
|
+
from klaude_code.ui.rich.theme import ThemeKey
|
|
11
|
+
from klaude_code.ui.utils.common import truncate_display
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def need_render_developer_message(e: events.DeveloperMessageEvent) -> bool:
|
|
15
|
+
return bool(
|
|
16
|
+
e.item.memory_paths
|
|
17
|
+
or e.item.external_file_changes
|
|
18
|
+
or e.item.todo_use
|
|
19
|
+
or e.item.at_files
|
|
20
|
+
or e.item.user_image_count
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def render_developer_message(e: events.DeveloperMessageEvent) -> RenderableType:
|
|
25
|
+
"""Render developer message details into a single group.
|
|
26
|
+
|
|
27
|
+
Includes: memory paths, external file changes, todo reminder, @file operations.
|
|
28
|
+
Command output is excluded; render it separately via `render_command_output`.
|
|
29
|
+
"""
|
|
30
|
+
parts: list[RenderableType] = []
|
|
31
|
+
|
|
32
|
+
if mp := e.item.memory_paths:
|
|
33
|
+
grid = create_grid()
|
|
34
|
+
grid.add_row(
|
|
35
|
+
Text(" +", style=ThemeKey.REMINDER),
|
|
36
|
+
Text.assemble(
|
|
37
|
+
("Load memory ", ThemeKey.REMINDER),
|
|
38
|
+
Text(", ", ThemeKey.REMINDER).join(
|
|
39
|
+
render_path(memory_path, ThemeKey.REMINDER_BOLD) for memory_path in mp
|
|
40
|
+
),
|
|
41
|
+
),
|
|
42
|
+
)
|
|
43
|
+
parts.append(grid)
|
|
44
|
+
|
|
45
|
+
if fc := e.item.external_file_changes:
|
|
46
|
+
grid = create_grid()
|
|
47
|
+
for file_path in fc:
|
|
48
|
+
grid.add_row(
|
|
49
|
+
Text(" +", style=ThemeKey.REMINDER),
|
|
50
|
+
Text.assemble(
|
|
51
|
+
("Read ", ThemeKey.REMINDER),
|
|
52
|
+
render_path(file_path, ThemeKey.REMINDER_BOLD),
|
|
53
|
+
(" after external changes", ThemeKey.REMINDER),
|
|
54
|
+
),
|
|
55
|
+
)
|
|
56
|
+
parts.append(grid)
|
|
57
|
+
|
|
58
|
+
if e.item.todo_use:
|
|
59
|
+
grid = create_grid()
|
|
60
|
+
grid.add_row(
|
|
61
|
+
Text(" +", style=ThemeKey.REMINDER),
|
|
62
|
+
Text("Todo hasn't been updated recently", ThemeKey.REMINDER),
|
|
63
|
+
)
|
|
64
|
+
parts.append(grid)
|
|
65
|
+
|
|
66
|
+
if e.item.at_files:
|
|
67
|
+
grid = create_grid()
|
|
68
|
+
for at_file in e.item.at_files:
|
|
69
|
+
grid.add_row(
|
|
70
|
+
Text(" +", style=ThemeKey.REMINDER),
|
|
71
|
+
Text.assemble(
|
|
72
|
+
(f"{at_file.operation} ", ThemeKey.REMINDER),
|
|
73
|
+
render_path(at_file.path, ThemeKey.REMINDER_BOLD),
|
|
74
|
+
),
|
|
75
|
+
)
|
|
76
|
+
parts.append(grid)
|
|
77
|
+
|
|
78
|
+
if uic := e.item.user_image_count:
|
|
79
|
+
grid = create_grid()
|
|
80
|
+
grid.add_row(
|
|
81
|
+
Text(" +", style=ThemeKey.REMINDER),
|
|
82
|
+
Text(f"Attached {uic} image{'s' if uic > 1 else ''}", style=ThemeKey.REMINDER),
|
|
83
|
+
)
|
|
84
|
+
parts.append(grid)
|
|
85
|
+
|
|
86
|
+
return Group(*parts) if parts else Text("")
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def render_command_output(e: events.DeveloperMessageEvent) -> RenderableType:
|
|
90
|
+
"""Render developer command output content."""
|
|
91
|
+
if not e.item.command_output:
|
|
92
|
+
return Text("")
|
|
93
|
+
|
|
94
|
+
match e.item.command_output.command_name:
|
|
95
|
+
case commands.CommandName.DIFF:
|
|
96
|
+
if e.item.content is None or len(e.item.content) == 0:
|
|
97
|
+
return Padding.indent(Text("(no changes)", style=ThemeKey.TOOL_RESULT), level=2)
|
|
98
|
+
return r_diffs.render_diff_panel(e.item.content, show_file_name=True)
|
|
99
|
+
case commands.CommandName.HELP:
|
|
100
|
+
return Padding.indent(Text.from_markup(e.item.content or ""), level=2)
|
|
101
|
+
case commands.CommandName.STATUS:
|
|
102
|
+
return _render_status_output(e.item.command_output)
|
|
103
|
+
case _:
|
|
104
|
+
content = e.item.content or "(no content)"
|
|
105
|
+
style = ThemeKey.TOOL_RESULT if not e.item.command_output.is_error else ThemeKey.ERROR
|
|
106
|
+
return Padding.indent(Text(truncate_display(content), style=style), level=2)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _format_tokens(tokens: int) -> str:
|
|
110
|
+
"""Format token count with K/M suffix for readability."""
|
|
111
|
+
if tokens >= 1_000_000:
|
|
112
|
+
return f"{tokens / 1_000_000:.2f}M"
|
|
113
|
+
if tokens >= 1_000:
|
|
114
|
+
return f"{tokens / 1_000:.1f}K"
|
|
115
|
+
return str(tokens)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _format_cost(cost: float | None) -> str:
|
|
119
|
+
"""Format cost in USD."""
|
|
120
|
+
if cost is None:
|
|
121
|
+
return "-"
|
|
122
|
+
if cost < 0.01:
|
|
123
|
+
return f"${cost:.4f}"
|
|
124
|
+
return f"${cost:.2f}"
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _render_status_output(command_output: model.CommandOutput) -> RenderableType:
|
|
128
|
+
"""Render session status as a two-column table with sections."""
|
|
129
|
+
if not command_output.ui_extra or not command_output.ui_extra.session_status:
|
|
130
|
+
return Text("(no status data)", style=ThemeKey.TOOL_RESULT)
|
|
131
|
+
|
|
132
|
+
status = command_output.ui_extra.session_status
|
|
133
|
+
usage = status.usage
|
|
134
|
+
|
|
135
|
+
table = Table.grid(padding=(0, 2))
|
|
136
|
+
table.add_column(style=ThemeKey.TOOL_RESULT, no_wrap=True)
|
|
137
|
+
table.add_column(style=ThemeKey.TOOL_RESULT, no_wrap=True)
|
|
138
|
+
# Token Usage section
|
|
139
|
+
table.add_row(Text("Token Usage", style="bold"), "")
|
|
140
|
+
table.add_row("Input Tokens", _format_tokens(usage.input_tokens))
|
|
141
|
+
if usage.cached_tokens > 0:
|
|
142
|
+
table.add_row("Cached Tokens", _format_tokens(usage.cached_tokens))
|
|
143
|
+
if usage.reasoning_tokens > 0:
|
|
144
|
+
table.add_row("Reasoning Tokens", _format_tokens(usage.reasoning_tokens))
|
|
145
|
+
table.add_row("Output Tokens", _format_tokens(usage.output_tokens))
|
|
146
|
+
table.add_row("Total Tokens", _format_tokens(usage.total_tokens))
|
|
147
|
+
|
|
148
|
+
# Cost section
|
|
149
|
+
if usage.total_cost is not None:
|
|
150
|
+
table.add_row("", "") # Empty line
|
|
151
|
+
table.add_row(Text("Cost", style="bold"), "")
|
|
152
|
+
table.add_row("Input Cost", _format_cost(usage.input_cost))
|
|
153
|
+
if usage.cache_read_cost is not None and usage.cache_read_cost > 0:
|
|
154
|
+
table.add_row("Cache Read Cost", _format_cost(usage.cache_read_cost))
|
|
155
|
+
table.add_row("Output Cost", _format_cost(usage.output_cost))
|
|
156
|
+
table.add_row("Total Cost", _format_cost(usage.total_cost))
|
|
157
|
+
|
|
158
|
+
return Padding.indent(table, level=2)
|