klaude-code 1.2.22__py3-none-any.whl → 1.2.23__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/command/status_cmd.py +1 -1
- klaude_code/const/__init__.py +8 -2
- klaude_code/core/manager/sub_agent_manager.py +1 -1
- klaude_code/core/reminders.py +51 -0
- klaude_code/core/task.py +37 -18
- klaude_code/core/tool/__init__.py +1 -4
- klaude_code/core/tool/skill/__init__.py +0 -0
- klaude_code/core/tool/{memory → skill}/skill_tool.py +16 -39
- klaude_code/protocol/model.py +2 -1
- klaude_code/session/export.py +1 -1
- klaude_code/session/store.py +4 -2
- klaude_code/skill/__init__.py +27 -0
- klaude_code/skill/assets/deslop/SKILL.md +17 -0
- klaude_code/skill/assets/dev-docs/SKILL.md +108 -0
- klaude_code/skill/assets/handoff/SKILL.md +39 -0
- klaude_code/skill/assets/jj-workspace/SKILL.md +20 -0
- klaude_code/skill/assets/skill-creator/SKILL.md +139 -0
- klaude_code/{core/tool/memory/skill_loader.py → skill/loader.py} +60 -24
- klaude_code/skill/manager.py +70 -0
- klaude_code/skill/system_skills.py +192 -0
- klaude_code/ui/modes/repl/completers.py +103 -3
- klaude_code/ui/modes/repl/event_handler.py +7 -3
- klaude_code/ui/modes/repl/input_prompt_toolkit.py +42 -3
- klaude_code/ui/renderers/assistant.py +7 -2
- klaude_code/ui/renderers/developer.py +12 -0
- klaude_code/ui/renderers/diffs.py +1 -1
- klaude_code/ui/renderers/metadata.py +4 -2
- klaude_code/ui/renderers/thinking.py +1 -1
- klaude_code/ui/renderers/tools.py +57 -32
- klaude_code/ui/renderers/user_input.py +32 -2
- klaude_code/ui/rich/markdown.py +22 -17
- klaude_code/ui/rich/status.py +1 -13
- klaude_code/ui/rich/theme.py +7 -5
- {klaude_code-1.2.22.dist-info → klaude_code-1.2.23.dist-info}/METADATA +18 -13
- {klaude_code-1.2.22.dist-info → klaude_code-1.2.23.dist-info}/RECORD +38 -35
- klaude_code/command/prompt-deslop.md +0 -14
- klaude_code/command/prompt-dev-docs-update.md +0 -56
- klaude_code/command/prompt-dev-docs.md +0 -46
- klaude_code/command/prompt-handoff.md +0 -33
- klaude_code/command/prompt-jj-workspace.md +0 -18
- klaude_code/core/tool/memory/__init__.py +0 -5
- /klaude_code/core/tool/{memory → skill}/skill_tool.md +0 -0
- {klaude_code-1.2.22.dist-info → klaude_code-1.2.23.dist-info}/WHEEL +0 -0
- {klaude_code-1.2.22.dist-info → klaude_code-1.2.23.dist-info}/entry_points.txt +0 -0
|
@@ -1,13 +1,15 @@
|
|
|
1
|
-
"""REPL completion handlers for @ file paths
|
|
1
|
+
"""REPL completion handlers for @ file paths, / slash commands, and $ skills.
|
|
2
2
|
|
|
3
3
|
This module provides completers for the REPL input:
|
|
4
4
|
- _SlashCommandCompleter: Completes slash commands on the first line
|
|
5
|
+
- _SkillCompleter: Completes skill names on the first line with $ prefix
|
|
5
6
|
- _AtFilesCompleter: Completes @path segments using fd or ripgrep
|
|
6
|
-
- _ComboCompleter: Combines
|
|
7
|
+
- _ComboCompleter: Combines all completers with priority logic
|
|
7
8
|
|
|
8
9
|
Public API:
|
|
9
10
|
- create_repl_completer(): Factory function to create the combined completer
|
|
10
11
|
- AT_TOKEN_PATTERN: Regex pattern for @token matching (used by key bindings)
|
|
12
|
+
- SKILL_TOKEN_PATTERN: Regex pattern for $skill matching (used by key bindings)
|
|
11
13
|
"""
|
|
12
14
|
|
|
13
15
|
from __future__ import annotations
|
|
@@ -34,6 +36,9 @@ from klaude_code.trace.log import DebugType, log_debug
|
|
|
34
36
|
# single logical token.
|
|
35
37
|
AT_TOKEN_PATTERN = re.compile(r'(^|\s)@(?P<frag>"[^"]*"|[^\s]*)$')
|
|
36
38
|
|
|
39
|
+
# Pattern to match $skill or ¥skill token for skill completion (used by key bindings).
|
|
40
|
+
SKILL_TOKEN_PATTERN = re.compile(r"^[$¥](?P<frag>\S*)$")
|
|
41
|
+
|
|
37
42
|
|
|
38
43
|
def create_repl_completer() -> Completer:
|
|
39
44
|
"""Create and return the combined REPL completer.
|
|
@@ -121,12 +126,102 @@ class _SlashCommandCompleter(Completer):
|
|
|
121
126
|
return bool(self._SLASH_TOKEN_RE.search(text_before))
|
|
122
127
|
|
|
123
128
|
|
|
129
|
+
class _SkillCompleter(Completer):
|
|
130
|
+
"""Complete skill names at the beginning of the first line.
|
|
131
|
+
|
|
132
|
+
Behavior:
|
|
133
|
+
- Only triggers when cursor is on first line and text matches $ or ¥...
|
|
134
|
+
- Shows available skills with descriptions
|
|
135
|
+
- Inserts trailing space after completion
|
|
136
|
+
"""
|
|
137
|
+
|
|
138
|
+
_SKILL_TOKEN_RE = SKILL_TOKEN_PATTERN
|
|
139
|
+
|
|
140
|
+
def get_completions(
|
|
141
|
+
self,
|
|
142
|
+
document: Document,
|
|
143
|
+
complete_event, # type: ignore[override]
|
|
144
|
+
) -> Iterable[Completion]:
|
|
145
|
+
# Only complete on first line
|
|
146
|
+
if document.cursor_position_row != 0:
|
|
147
|
+
return
|
|
148
|
+
|
|
149
|
+
text_before = document.current_line_before_cursor
|
|
150
|
+
m = self._SKILL_TOKEN_RE.search(text_before)
|
|
151
|
+
if not m:
|
|
152
|
+
return
|
|
153
|
+
|
|
154
|
+
frag = m.group("frag").lower()
|
|
155
|
+
# Get the prefix character ($ or ¥)
|
|
156
|
+
prefix_char = text_before[0]
|
|
157
|
+
token_start = len(text_before) - len(f"{prefix_char}{m.group('frag')}")
|
|
158
|
+
start_position = token_start - len(text_before) # negative offset
|
|
159
|
+
|
|
160
|
+
# Get available skills from SkillTool
|
|
161
|
+
skills = self._get_available_skills()
|
|
162
|
+
if not skills:
|
|
163
|
+
return
|
|
164
|
+
|
|
165
|
+
# Filter skills that match the fragment (case-insensitive)
|
|
166
|
+
matched: list[tuple[str, str, str]] = [] # (name, description, location)
|
|
167
|
+
for name, desc, location in skills:
|
|
168
|
+
if frag in name.lower() or frag in desc.lower():
|
|
169
|
+
matched.append((name, desc, location))
|
|
170
|
+
|
|
171
|
+
if not matched:
|
|
172
|
+
return
|
|
173
|
+
|
|
174
|
+
# Calculate max width for alignment
|
|
175
|
+
max_name_len = max(len(name) for name, _, _ in matched)
|
|
176
|
+
align_width = max(max_name_len, 20) + 2
|
|
177
|
+
|
|
178
|
+
for name, desc, location in matched:
|
|
179
|
+
# Format: name [location] description
|
|
180
|
+
# Align location tags (max length is "project" = 7, plus brackets = 9)
|
|
181
|
+
padding_name = " " * (align_width - len(name))
|
|
182
|
+
location_tag = f"[{location}]".ljust(9)
|
|
183
|
+
|
|
184
|
+
# Using HTML for formatting: bold skill name, cyan location tag, gray description
|
|
185
|
+
display_text = HTML(
|
|
186
|
+
f"<b>{name}</b>{padding_name}<style color='ansicyan'>{location_tag}</style> "
|
|
187
|
+
f"<style color='ansibrightblack'>{desc}</style>"
|
|
188
|
+
)
|
|
189
|
+
completion_text = f"${name} "
|
|
190
|
+
yield Completion(
|
|
191
|
+
text=completion_text,
|
|
192
|
+
start_position=start_position,
|
|
193
|
+
display=display_text,
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
def _get_available_skills(self) -> list[tuple[str, str, str]]:
|
|
197
|
+
"""Get available skills from skill module.
|
|
198
|
+
|
|
199
|
+
Returns:
|
|
200
|
+
List of (name, description, location) tuples
|
|
201
|
+
"""
|
|
202
|
+
try:
|
|
203
|
+
# Import here to avoid circular imports
|
|
204
|
+
from klaude_code.skill import get_available_skills
|
|
205
|
+
|
|
206
|
+
return get_available_skills()
|
|
207
|
+
except Exception:
|
|
208
|
+
return []
|
|
209
|
+
|
|
210
|
+
def is_skill_context(self, document: Document) -> bool:
|
|
211
|
+
"""Check if current context is a skill completion."""
|
|
212
|
+
if document.cursor_position_row != 0:
|
|
213
|
+
return False
|
|
214
|
+
text_before = document.current_line_before_cursor
|
|
215
|
+
return bool(self._SKILL_TOKEN_RE.search(text_before))
|
|
216
|
+
|
|
217
|
+
|
|
124
218
|
class _ComboCompleter(Completer):
|
|
125
|
-
"""Combined completer that handles
|
|
219
|
+
"""Combined completer that handles @ file paths, / slash commands, and $ skills."""
|
|
126
220
|
|
|
127
221
|
def __init__(self) -> None:
|
|
128
222
|
self._at_completer = _AtFilesCompleter()
|
|
129
223
|
self._slash_completer = _SlashCommandCompleter()
|
|
224
|
+
self._skill_completer = _SkillCompleter()
|
|
130
225
|
|
|
131
226
|
def get_completions(
|
|
132
227
|
self,
|
|
@@ -138,6 +233,11 @@ class _ComboCompleter(Completer):
|
|
|
138
233
|
yield from self._slash_completer.get_completions(document, complete_event)
|
|
139
234
|
return
|
|
140
235
|
|
|
236
|
+
# Try skill completion (only on first line with $ prefix)
|
|
237
|
+
if document.cursor_position_row == 0 and self._skill_completer.is_skill_context(document):
|
|
238
|
+
yield from self._skill_completer.get_completions(document, complete_event)
|
|
239
|
+
return
|
|
240
|
+
|
|
141
241
|
# Fall back to @ file completion
|
|
142
242
|
yield from self._at_completer.get_completions(document, complete_event)
|
|
143
243
|
|
|
@@ -2,12 +2,14 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
from dataclasses import dataclass
|
|
4
4
|
|
|
5
|
+
from rich.rule import Rule
|
|
5
6
|
from rich.text import Text
|
|
6
7
|
|
|
7
8
|
from klaude_code import const
|
|
8
9
|
from klaude_code.protocol import events
|
|
9
10
|
from klaude_code.ui.core.stage_manager import Stage, StageManager
|
|
10
11
|
from klaude_code.ui.modes.repl.renderer import REPLRenderer
|
|
12
|
+
from klaude_code.ui.renderers.assistant import ASSISTANT_MESSAGE_MARK
|
|
11
13
|
from klaude_code.ui.renderers.thinking import normalize_thinking_content
|
|
12
14
|
from klaude_code.ui.rich.markdown import MarkdownStream, ThinkingMarkdown
|
|
13
15
|
from klaude_code.ui.rich.theme import ThemeKey
|
|
@@ -327,7 +329,7 @@ class DisplayEventHandler:
|
|
|
327
329
|
theme=self.renderer.themes.thinking_markdown_theme,
|
|
328
330
|
console=self.renderer.console,
|
|
329
331
|
spinner=self.renderer.spinner_renderable(),
|
|
330
|
-
|
|
332
|
+
left_margin=const.MARKDOWN_LEFT_MARGIN,
|
|
331
333
|
markdown_class=ThinkingMarkdown,
|
|
332
334
|
)
|
|
333
335
|
self.thinking_stream.start(mdstream)
|
|
@@ -358,8 +360,8 @@ class DisplayEventHandler:
|
|
|
358
360
|
theme=self.renderer.themes.markdown_theme,
|
|
359
361
|
console=self.renderer.console,
|
|
360
362
|
spinner=self.renderer.spinner_renderable(),
|
|
361
|
-
mark=
|
|
362
|
-
|
|
363
|
+
mark=ASSISTANT_MESSAGE_MARK,
|
|
364
|
+
left_margin=const.MARKDOWN_LEFT_MARGIN,
|
|
363
365
|
)
|
|
364
366
|
self.assistant_stream.start(mdstream)
|
|
365
367
|
self.assistant_stream.append(event.content)
|
|
@@ -430,6 +432,8 @@ class DisplayEventHandler:
|
|
|
430
432
|
emit_osc94(OSC94States.HIDDEN)
|
|
431
433
|
self.spinner_status.reset()
|
|
432
434
|
self.renderer.spinner_stop()
|
|
435
|
+
self.renderer.console.print(Rule(characters="-", style=ThemeKey.LINES))
|
|
436
|
+
self.renderer.print()
|
|
433
437
|
await self.stage_manager.transition_to(Stage.WAITING)
|
|
434
438
|
self._maybe_notify_task_finish(event)
|
|
435
439
|
|
|
@@ -7,6 +7,7 @@ from typing import NamedTuple, override
|
|
|
7
7
|
|
|
8
8
|
from prompt_toolkit import PromptSession
|
|
9
9
|
from prompt_toolkit.completion import ThreadedCompleter
|
|
10
|
+
from prompt_toolkit.cursor_shapes import CursorShape
|
|
10
11
|
from prompt_toolkit.formatted_text import FormattedText
|
|
11
12
|
from prompt_toolkit.history import FileHistory
|
|
12
13
|
from prompt_toolkit.patch_stdout import patch_stdout
|
|
@@ -17,6 +18,8 @@ from klaude_code.ui.core.input import InputProviderABC
|
|
|
17
18
|
from klaude_code.ui.modes.repl.clipboard import capture_clipboard_tag, copy_to_clipboard, extract_images_from_text
|
|
18
19
|
from klaude_code.ui.modes.repl.completers import AT_TOKEN_PATTERN, create_repl_completer
|
|
19
20
|
from klaude_code.ui.modes.repl.key_bindings import create_key_bindings
|
|
21
|
+
from klaude_code.ui.renderers.user_input import USER_MESSAGE_MARK
|
|
22
|
+
from klaude_code.ui.terminal.color import is_light_terminal_background
|
|
20
23
|
from klaude_code.ui.utils.common import get_current_git_branch, show_path_with_tilde
|
|
21
24
|
|
|
22
25
|
|
|
@@ -32,16 +35,23 @@ class REPLStatusSnapshot(NamedTuple):
|
|
|
32
35
|
|
|
33
36
|
COMPLETION_SELECTED = "#5869f7"
|
|
34
37
|
COMPLETION_MENU = "ansibrightblack"
|
|
35
|
-
INPUT_PROMPT_STYLE = "ansimagenta"
|
|
38
|
+
INPUT_PROMPT_STYLE = "ansimagenta bold"
|
|
39
|
+
PLACEHOLDER_TEXT_STYLE_DARK_BG = "fg:#5a5a5a italic"
|
|
40
|
+
PLACEHOLDER_TEXT_STYLE_LIGHT_BG = "fg:#7a7a7a italic"
|
|
41
|
+
PLACEHOLDER_TEXT_STYLE_UNKNOWN_BG = "fg:#8a8a8a italic"
|
|
42
|
+
PLACEHOLDER_SYMBOL_STYLE_DARK_BG = "bg:#2a2a2a fg:#5a5a5a"
|
|
43
|
+
PLACEHOLDER_SYMBOL_STYLE_LIGHT_BG = "bg:#e6e6e6 fg:#7a7a7a"
|
|
44
|
+
PLACEHOLDER_SYMBOL_STYLE_UNKNOWN_BG = "bg:#2a2a2a fg:#8a8a8a"
|
|
36
45
|
|
|
37
46
|
|
|
38
47
|
class PromptToolkitInput(InputProviderABC):
|
|
39
48
|
def __init__(
|
|
40
49
|
self,
|
|
41
|
-
prompt: str =
|
|
50
|
+
prompt: str = USER_MESSAGE_MARK,
|
|
42
51
|
status_provider: Callable[[], REPLStatusSnapshot] | None = None,
|
|
43
52
|
): # ▌
|
|
44
53
|
self._status_provider = status_provider
|
|
54
|
+
self._is_light_terminal_background = is_light_terminal_background(timeout=0.2)
|
|
45
55
|
|
|
46
56
|
project = str(Path.cwd()).strip("/").replace("/", "-")
|
|
47
57
|
history_path = Path.home() / ".klaude" / "projects" / project / "input" / "input_history.txt"
|
|
@@ -60,6 +70,7 @@ class PromptToolkitInput(InputProviderABC):
|
|
|
60
70
|
[(INPUT_PROMPT_STYLE, prompt)],
|
|
61
71
|
history=FileHistory(str(history_path)),
|
|
62
72
|
multiline=True,
|
|
73
|
+
cursor=CursorShape.BEAM,
|
|
63
74
|
prompt_continuation=[(INPUT_PROMPT_STYLE, " ")],
|
|
64
75
|
key_bindings=kb,
|
|
65
76
|
completer=ThreadedCompleter(create_repl_completer()),
|
|
@@ -144,6 +155,34 @@ class PromptToolkitInput(InputProviderABC):
|
|
|
144
155
|
toolbar_text = left_text + padding + right_text
|
|
145
156
|
return FormattedText([("#2c7eac", toolbar_text)])
|
|
146
157
|
|
|
158
|
+
def _render_input_placeholder(self) -> FormattedText:
|
|
159
|
+
if self._is_light_terminal_background is True:
|
|
160
|
+
text_style = PLACEHOLDER_TEXT_STYLE_LIGHT_BG
|
|
161
|
+
symbol_style = PLACEHOLDER_SYMBOL_STYLE_LIGHT_BG
|
|
162
|
+
elif self._is_light_terminal_background is False:
|
|
163
|
+
text_style = PLACEHOLDER_TEXT_STYLE_DARK_BG
|
|
164
|
+
symbol_style = PLACEHOLDER_SYMBOL_STYLE_DARK_BG
|
|
165
|
+
else:
|
|
166
|
+
text_style = PLACEHOLDER_TEXT_STYLE_UNKNOWN_BG
|
|
167
|
+
symbol_style = PLACEHOLDER_SYMBOL_STYLE_UNKNOWN_BG
|
|
168
|
+
|
|
169
|
+
return FormattedText(
|
|
170
|
+
[
|
|
171
|
+
(text_style, " " * 10),
|
|
172
|
+
(symbol_style, " @ "),
|
|
173
|
+
(text_style, " "),
|
|
174
|
+
(text_style, "files"),
|
|
175
|
+
(text_style, " "),
|
|
176
|
+
(symbol_style, " $ "),
|
|
177
|
+
(text_style, " "),
|
|
178
|
+
(text_style, "skills"),
|
|
179
|
+
(text_style, " "),
|
|
180
|
+
(symbol_style, " / "),
|
|
181
|
+
(text_style, " "),
|
|
182
|
+
(text_style, "commands"),
|
|
183
|
+
]
|
|
184
|
+
)
|
|
185
|
+
|
|
147
186
|
async def start(self) -> None:
|
|
148
187
|
pass
|
|
149
188
|
|
|
@@ -154,7 +193,7 @@ class PromptToolkitInput(InputProviderABC):
|
|
|
154
193
|
async def iter_inputs(self) -> AsyncIterator[UserInputPayload]:
|
|
155
194
|
while True:
|
|
156
195
|
with patch_stdout():
|
|
157
|
-
line: str = await self._session.prompt_async()
|
|
196
|
+
line: str = await self._session.prompt_async(placeholder=self._render_input_placeholder())
|
|
158
197
|
|
|
159
198
|
# Extract images referenced in the input text
|
|
160
199
|
images = extract_images_from_text(line)
|
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
from rich.console import RenderableType
|
|
2
|
+
from rich.padding import Padding
|
|
2
3
|
|
|
4
|
+
from klaude_code import const
|
|
3
5
|
from klaude_code.ui.renderers.common import create_grid
|
|
4
6
|
from klaude_code.ui.rich.markdown import NoInsetMarkdown
|
|
5
7
|
|
|
8
|
+
# UI markers
|
|
9
|
+
ASSISTANT_MESSAGE_MARK = "➤"
|
|
10
|
+
|
|
6
11
|
|
|
7
12
|
def render_assistant_message(content: str, *, code_theme: str) -> RenderableType | None:
|
|
8
13
|
"""Render assistant message for replay history display.
|
|
@@ -15,7 +20,7 @@ def render_assistant_message(content: str, *, code_theme: str) -> RenderableType
|
|
|
15
20
|
|
|
16
21
|
grid = create_grid()
|
|
17
22
|
grid.add_row(
|
|
18
|
-
|
|
19
|
-
NoInsetMarkdown(stripped, code_theme=code_theme),
|
|
23
|
+
ASSISTANT_MESSAGE_MARK,
|
|
24
|
+
Padding(NoInsetMarkdown(stripped, code_theme=code_theme), (0, const.MARKDOWN_RIGHT_MARGIN, 0, 0)),
|
|
20
25
|
)
|
|
21
26
|
return grid
|
|
@@ -17,6 +17,7 @@ def need_render_developer_message(e: events.DeveloperMessageEvent) -> bool:
|
|
|
17
17
|
or e.item.todo_use
|
|
18
18
|
or e.item.at_files
|
|
19
19
|
or e.item.user_image_count
|
|
20
|
+
or e.item.skill_name
|
|
20
21
|
)
|
|
21
22
|
|
|
22
23
|
|
|
@@ -93,6 +94,17 @@ def render_developer_message(e: events.DeveloperMessageEvent) -> RenderableType:
|
|
|
93
94
|
)
|
|
94
95
|
parts.append(grid)
|
|
95
96
|
|
|
97
|
+
if sn := e.item.skill_name:
|
|
98
|
+
grid = create_grid()
|
|
99
|
+
grid.add_row(
|
|
100
|
+
Text(" +", style=ThemeKey.REMINDER),
|
|
101
|
+
Text.assemble(
|
|
102
|
+
("Activated skill ", ThemeKey.REMINDER),
|
|
103
|
+
(sn, ThemeKey.REMINDER_BOLD),
|
|
104
|
+
),
|
|
105
|
+
)
|
|
106
|
+
parts.append(grid)
|
|
107
|
+
|
|
96
108
|
return Group(*parts) if parts else Text("")
|
|
97
109
|
|
|
98
110
|
|
|
@@ -45,7 +45,7 @@ def _render_task_metadata_block(
|
|
|
45
45
|
currency_symbol = "¥" if currency == "CNY" else "$"
|
|
46
46
|
|
|
47
47
|
# First column: mark only
|
|
48
|
-
mark = Text("└", style=ThemeKey.METADATA_DIM) if is_sub_agent else Text("
|
|
48
|
+
mark = Text("└", style=ThemeKey.METADATA_DIM) if is_sub_agent else Text("⇅", style=ThemeKey.METADATA)
|
|
49
49
|
|
|
50
50
|
# Second column: model@provider / tokens / cost / ...
|
|
51
51
|
content = Text()
|
|
@@ -151,7 +151,9 @@ def render_task_metadata(e: events.TaskMetadataEvent) -> RenderableType:
|
|
|
151
151
|
"""Render task metadata including main agent and sub-agents, aggregated by model+provider."""
|
|
152
152
|
renderables: list[RenderableType] = []
|
|
153
153
|
|
|
154
|
-
renderables.append(
|
|
154
|
+
renderables.append(
|
|
155
|
+
_render_task_metadata_block(e.metadata.main_agent, is_sub_agent=False, show_context_and_time=True)
|
|
156
|
+
)
|
|
155
157
|
|
|
156
158
|
# Aggregate by (model_name, provider), sorted by total_cost descending
|
|
157
159
|
sorted_items = model.TaskMetadata.aggregate_by_model(e.metadata.sub_agent_task_metadata)
|
|
@@ -9,7 +9,7 @@ from klaude_code.ui.rich.theme import ThemeKey
|
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
def thinking_prefix() -> Text:
|
|
12
|
-
return Text.from_markup("[not italic]⸫[/not italic] Thinking …", style=ThemeKey.
|
|
12
|
+
return Text.from_markup("[not italic]⸫[/not italic] Thinking …", style=ThemeKey.THINKING_BOLD)
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
def normalize_thinking_content(content: str) -> str:
|
|
@@ -13,6 +13,24 @@ from klaude_code.ui.renderers import diffs as r_diffs
|
|
|
13
13
|
from klaude_code.ui.renderers.common import create_grid, truncate_display
|
|
14
14
|
from klaude_code.ui.rich.theme import ThemeKey
|
|
15
15
|
|
|
16
|
+
# Tool markers (Unicode symbols for UI display)
|
|
17
|
+
MARK_GENERIC = "⚒"
|
|
18
|
+
MARK_BASH = "→"
|
|
19
|
+
MARK_PLAN = "▪"
|
|
20
|
+
MARK_READ = "←"
|
|
21
|
+
MARK_EDIT = "±"
|
|
22
|
+
MARK_WRITE = "+"
|
|
23
|
+
MARK_MERMAID = "⧉"
|
|
24
|
+
MARK_WEB_FETCH = "←"
|
|
25
|
+
MARK_WEB_SEARCH = ""
|
|
26
|
+
MARK_DONE = "✔"
|
|
27
|
+
MARK_SKILL = "✪"
|
|
28
|
+
|
|
29
|
+
# Todo status markers
|
|
30
|
+
MARK_TODO_PENDING = "▢"
|
|
31
|
+
MARK_TODO_IN_PROGRESS = "◉"
|
|
32
|
+
MARK_TODO_COMPLETED = "✔"
|
|
33
|
+
|
|
16
34
|
|
|
17
35
|
def is_sub_agent_tool(tool_name: str) -> bool:
|
|
18
36
|
return _is_sub_agent_tool(tool_name)
|
|
@@ -30,7 +48,7 @@ def render_path(path: str, style: str, is_directory: bool = False) -> Text:
|
|
|
30
48
|
return Text(path, style=style)
|
|
31
49
|
|
|
32
50
|
|
|
33
|
-
def render_generic_tool_call(tool_name: str, arguments: str, markup: str =
|
|
51
|
+
def render_generic_tool_call(tool_name: str, arguments: str, markup: str = MARK_GENERIC) -> RenderableType:
|
|
34
52
|
grid = create_grid()
|
|
35
53
|
|
|
36
54
|
tool_name_column = Text.assemble((markup, ThemeKey.TOOL_MARK), " ", (tool_name, ThemeKey.TOOL_NAME))
|
|
@@ -60,7 +78,7 @@ def render_generic_tool_call(tool_name: str, arguments: str, markup: str = "•"
|
|
|
60
78
|
|
|
61
79
|
def render_bash_tool_call(arguments: str) -> RenderableType:
|
|
62
80
|
grid = create_grid()
|
|
63
|
-
tool_name_column = Text.assemble((
|
|
81
|
+
tool_name_column = Text.assemble((MARK_BASH, ThemeKey.TOOL_MARK), " ", ("Bash", ThemeKey.TOOL_NAME))
|
|
64
82
|
|
|
65
83
|
try:
|
|
66
84
|
payload_raw: Any = json.loads(arguments) if arguments else {}
|
|
@@ -103,7 +121,7 @@ def render_bash_tool_call(arguments: str) -> RenderableType:
|
|
|
103
121
|
|
|
104
122
|
def render_update_plan_tool_call(arguments: str) -> RenderableType:
|
|
105
123
|
grid = create_grid()
|
|
106
|
-
tool_name_column = Text.assemble((
|
|
124
|
+
tool_name_column = Text.assemble((MARK_PLAN, ThemeKey.TOOL_MARK), " ", ("Update Plan", ThemeKey.TOOL_NAME))
|
|
107
125
|
explanation_column = Text("")
|
|
108
126
|
|
|
109
127
|
if arguments:
|
|
@@ -160,13 +178,13 @@ def render_read_tool_call(arguments: str) -> RenderableType:
|
|
|
160
178
|
style=ThemeKey.INVALID_TOOL_CALL_ARGS,
|
|
161
179
|
)
|
|
162
180
|
)
|
|
163
|
-
grid.add_row(Text(
|
|
181
|
+
grid.add_row(Text(MARK_READ, ThemeKey.TOOL_MARK), render_result)
|
|
164
182
|
return grid
|
|
165
183
|
|
|
166
184
|
|
|
167
185
|
def render_edit_tool_call(arguments: str) -> RenderableType:
|
|
168
186
|
grid = create_grid()
|
|
169
|
-
tool_name_column = Text.assemble((
|
|
187
|
+
tool_name_column = Text.assemble((MARK_EDIT, ThemeKey.TOOL_MARK), " ", ("Edit", ThemeKey.TOOL_NAME))
|
|
170
188
|
try:
|
|
171
189
|
json_dict = json.loads(arguments)
|
|
172
190
|
file_path = json_dict.get("file_path")
|
|
@@ -185,10 +203,10 @@ def render_write_tool_call(arguments: str) -> RenderableType:
|
|
|
185
203
|
try:
|
|
186
204
|
json_dict = json.loads(arguments)
|
|
187
205
|
file_path = json_dict.get("file_path")
|
|
188
|
-
tool_name_column = Text.assemble((
|
|
206
|
+
tool_name_column = Text.assemble((MARK_WRITE, ThemeKey.TOOL_MARK), " ", ("Write", ThemeKey.TOOL_NAME))
|
|
189
207
|
arguments_column = render_path(file_path, ThemeKey.TOOL_PARAM_FILE_PATH)
|
|
190
208
|
except json.JSONDecodeError:
|
|
191
|
-
tool_name_column = Text.assemble((
|
|
209
|
+
tool_name_column = Text.assemble((MARK_WRITE, ThemeKey.TOOL_MARK), " ", ("Write", ThemeKey.TOOL_NAME))
|
|
192
210
|
arguments_column = Text(
|
|
193
211
|
arguments.strip()[: const.INVALID_TOOL_CALL_MAX_LENGTH],
|
|
194
212
|
style=ThemeKey.INVALID_TOOL_CALL_ARGS,
|
|
@@ -199,7 +217,7 @@ def render_write_tool_call(arguments: str) -> RenderableType:
|
|
|
199
217
|
|
|
200
218
|
def render_apply_patch_tool_call(arguments: str) -> RenderableType:
|
|
201
219
|
grid = create_grid()
|
|
202
|
-
tool_name_column = Text.assemble((
|
|
220
|
+
tool_name_column = Text.assemble((MARK_EDIT, ThemeKey.TOOL_MARK), " ", ("Apply Patch", ThemeKey.TOOL_NAME))
|
|
203
221
|
|
|
204
222
|
try:
|
|
205
223
|
payload = json.loads(arguments)
|
|
@@ -215,9 +233,27 @@ def render_apply_patch_tool_call(arguments: str) -> RenderableType:
|
|
|
215
233
|
arguments_column = Text("", ThemeKey.TOOL_PARAM)
|
|
216
234
|
|
|
217
235
|
if isinstance(patch_content, str):
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
236
|
+
update_count = 0
|
|
237
|
+
add_count = 0
|
|
238
|
+
delete_count = 0
|
|
239
|
+
for line in patch_content.splitlines():
|
|
240
|
+
if line.startswith("*** Update File:"):
|
|
241
|
+
update_count += 1
|
|
242
|
+
elif line.startswith("*** Add File:"):
|
|
243
|
+
add_count += 1
|
|
244
|
+
elif line.startswith("*** Delete File:"):
|
|
245
|
+
delete_count += 1
|
|
246
|
+
|
|
247
|
+
parts: list[str] = []
|
|
248
|
+
if update_count > 0:
|
|
249
|
+
parts.append(f"Update File × {update_count}" if update_count > 1 else "Update File")
|
|
250
|
+
if add_count > 0:
|
|
251
|
+
parts.append(f"Add File × {add_count}" if add_count > 1 else "Add File")
|
|
252
|
+
if delete_count > 0:
|
|
253
|
+
parts.append(f"Delete File × {delete_count}" if delete_count > 1 else "Delete File")
|
|
254
|
+
|
|
255
|
+
if parts:
|
|
256
|
+
arguments_column = Text(", ".join(parts), ThemeKey.TOOL_PARAM)
|
|
221
257
|
else:
|
|
222
258
|
arguments_column = Text(
|
|
223
259
|
str(patch_content)[: const.INVALID_TOOL_CALL_MAX_LENGTH],
|
|
@@ -229,34 +265,24 @@ def render_apply_patch_tool_call(arguments: str) -> RenderableType:
|
|
|
229
265
|
|
|
230
266
|
|
|
231
267
|
def render_todo(tr: events.ToolResultEvent) -> RenderableType:
|
|
232
|
-
|
|
233
|
-
return Text.assemble(
|
|
234
|
-
(" ✘", ThemeKey.ERROR_BOLD),
|
|
235
|
-
" ",
|
|
236
|
-
Text("(no content)" if tr.ui_extra is None else "(invalid ui_extra)", style=ThemeKey.ERROR),
|
|
237
|
-
)
|
|
238
|
-
|
|
268
|
+
assert isinstance(tr.ui_extra, model.TodoListUIExtra)
|
|
239
269
|
ui_extra = tr.ui_extra.todo_list
|
|
240
270
|
todo_grid = create_grid()
|
|
241
271
|
for todo in ui_extra.todos:
|
|
242
272
|
is_new_completed = todo.content in ui_extra.new_completed
|
|
243
273
|
match todo.status:
|
|
244
274
|
case "pending":
|
|
245
|
-
mark =
|
|
275
|
+
mark = MARK_TODO_PENDING
|
|
246
276
|
mark_style = ThemeKey.TODO_PENDING_MARK
|
|
247
277
|
text_style = ThemeKey.TODO_PENDING
|
|
248
278
|
case "in_progress":
|
|
249
|
-
mark =
|
|
279
|
+
mark = MARK_TODO_IN_PROGRESS
|
|
250
280
|
mark_style = ThemeKey.TODO_IN_PROGRESS_MARK
|
|
251
281
|
text_style = ThemeKey.TODO_IN_PROGRESS
|
|
252
282
|
case "completed":
|
|
253
|
-
mark =
|
|
283
|
+
mark = MARK_TODO_COMPLETED
|
|
254
284
|
mark_style = ThemeKey.TODO_NEW_COMPLETED_MARK if is_new_completed else ThemeKey.TODO_COMPLETED_MARK
|
|
255
285
|
text_style = ThemeKey.TODO_NEW_COMPLETED if is_new_completed else ThemeKey.TODO_COMPLETED
|
|
256
|
-
case _:
|
|
257
|
-
mark = "?"
|
|
258
|
-
mark_style = ThemeKey.TODO_PENDING_MARK
|
|
259
|
-
text_style = ThemeKey.TODO_PENDING
|
|
260
286
|
text = Text(todo.content)
|
|
261
287
|
text.stylize(text_style)
|
|
262
288
|
todo_grid.add_row(Text(mark, style=mark_style), text)
|
|
@@ -280,7 +306,7 @@ def _extract_mermaid_link(
|
|
|
280
306
|
|
|
281
307
|
def render_mermaid_tool_call(arguments: str) -> RenderableType:
|
|
282
308
|
grid = create_grid()
|
|
283
|
-
tool_name_column = Text.assemble((
|
|
309
|
+
tool_name_column = Text.assemble((MARK_MERMAID, ThemeKey.TOOL_MARK), " ", ("Mermaid", ThemeKey.TOOL_NAME))
|
|
284
310
|
summary = Text("", ThemeKey.TOOL_PARAM)
|
|
285
311
|
|
|
286
312
|
try:
|
|
@@ -320,7 +346,7 @@ def _truncate_url(url: str, max_length: int = 400) -> str:
|
|
|
320
346
|
|
|
321
347
|
def render_web_fetch_tool_call(arguments: str) -> RenderableType:
|
|
322
348
|
grid = create_grid()
|
|
323
|
-
tool_name_column = Text.assemble((
|
|
349
|
+
tool_name_column = Text.assemble((MARK_WEB_FETCH, ThemeKey.TOOL_MARK), " ", ("Fetch", ThemeKey.TOOL_NAME))
|
|
324
350
|
|
|
325
351
|
try:
|
|
326
352
|
payload: dict[str, str] = json.loads(arguments)
|
|
@@ -341,7 +367,7 @@ def render_web_fetch_tool_call(arguments: str) -> RenderableType:
|
|
|
341
367
|
|
|
342
368
|
def render_web_search_tool_call(arguments: str) -> RenderableType:
|
|
343
369
|
grid = create_grid()
|
|
344
|
-
tool_name_column = Text.assemble((
|
|
370
|
+
tool_name_column = Text.assemble((MARK_WEB_SEARCH, ThemeKey.TOOL_MARK), " ", ("Web Search", ThemeKey.TOOL_NAME))
|
|
345
371
|
|
|
346
372
|
try:
|
|
347
373
|
payload: dict[str, Any] = json.loads(arguments)
|
|
@@ -418,7 +444,7 @@ def get_truncation_info(tr: events.ToolResultEvent) -> model.TruncationUIExtra |
|
|
|
418
444
|
|
|
419
445
|
def render_report_back_tool_call() -> RenderableType:
|
|
420
446
|
grid = create_grid()
|
|
421
|
-
tool_name_column = Text.assemble((
|
|
447
|
+
tool_name_column = Text.assemble((MARK_DONE, ThemeKey.TOOL_MARK), " ", ("Report Back", ThemeKey.TOOL_NAME))
|
|
422
448
|
grid.add_row(tool_name_column, "")
|
|
423
449
|
return grid
|
|
424
450
|
|
|
@@ -474,19 +500,18 @@ def render_tool_call(e: events.ToolCallEvent) -> RenderableType | None:
|
|
|
474
500
|
return render_edit_tool_call(e.arguments)
|
|
475
501
|
case tools.WRITE:
|
|
476
502
|
return render_write_tool_call(e.arguments)
|
|
477
|
-
|
|
478
503
|
case tools.BASH:
|
|
479
504
|
return render_bash_tool_call(e.arguments)
|
|
480
505
|
case tools.APPLY_PATCH:
|
|
481
506
|
return render_apply_patch_tool_call(e.arguments)
|
|
482
507
|
case tools.TODO_WRITE:
|
|
483
|
-
return render_generic_tool_call("Update Todos", "",
|
|
508
|
+
return render_generic_tool_call("Update Todos", "", MARK_PLAN)
|
|
484
509
|
case tools.UPDATE_PLAN:
|
|
485
510
|
return render_update_plan_tool_call(e.arguments)
|
|
486
511
|
case tools.MERMAID:
|
|
487
512
|
return render_mermaid_tool_call(e.arguments)
|
|
488
513
|
case tools.SKILL:
|
|
489
|
-
return render_generic_tool_call(e.tool_name, e.arguments,
|
|
514
|
+
return render_generic_tool_call(e.tool_name, e.arguments, MARK_SKILL)
|
|
490
515
|
case tools.REPORT_BACK:
|
|
491
516
|
return render_report_back_tool_call()
|
|
492
517
|
case tools.WEB_FETCH:
|
|
@@ -4,6 +4,7 @@ from rich.console import Group, RenderableType
|
|
|
4
4
|
from rich.text import Text
|
|
5
5
|
|
|
6
6
|
from klaude_code.command import is_slash_command_name
|
|
7
|
+
from klaude_code.skill import get_available_skills
|
|
7
8
|
from klaude_code.ui.renderers.common import create_grid
|
|
8
9
|
from klaude_code.ui.rich.theme import ThemeKey
|
|
9
10
|
|
|
@@ -12,6 +13,11 @@ from klaude_code.ui.rich.theme import ThemeKey
|
|
|
12
13
|
# patterns such as foo@bar.com as file references.
|
|
13
14
|
AT_FILE_RENDER_PATTERN = re.compile(r'(?<!\S)@("([^"]+)"|\S+)')
|
|
14
15
|
|
|
16
|
+
# Match $skill or ¥skill pattern at the beginning of the first line
|
|
17
|
+
SKILL_RENDER_PATTERN = re.compile(r"^[$¥](\S+)")
|
|
18
|
+
|
|
19
|
+
USER_MESSAGE_MARK = "❯ "
|
|
20
|
+
|
|
15
21
|
|
|
16
22
|
def render_at_pattern(
|
|
17
23
|
text: str,
|
|
@@ -38,15 +44,24 @@ def render_at_pattern(
|
|
|
38
44
|
return result
|
|
39
45
|
|
|
40
46
|
|
|
47
|
+
def _is_valid_skill_name(name: str) -> bool:
|
|
48
|
+
"""Check if a skill name is valid (exists in loaded skills)."""
|
|
49
|
+
short = name.split(":")[-1] if ":" in name else name
|
|
50
|
+
available_skills = get_available_skills()
|
|
51
|
+
return any(skill_name in (name, short) for skill_name, _, _ in available_skills)
|
|
52
|
+
|
|
53
|
+
|
|
41
54
|
def render_user_input(content: str) -> RenderableType:
|
|
42
55
|
"""Render a user message as a group of quoted lines with styles.
|
|
43
56
|
|
|
44
57
|
- Highlights slash command on the first line if recognized
|
|
58
|
+
- Highlights $skill pattern on the first line if recognized
|
|
45
59
|
- Highlights @file patterns in all lines
|
|
46
60
|
"""
|
|
47
61
|
lines = content.strip().split("\n")
|
|
48
62
|
renderables: list[RenderableType] = []
|
|
49
63
|
has_command = False
|
|
64
|
+
command_style: str | None = None
|
|
50
65
|
for i, line in enumerate(lines):
|
|
51
66
|
line_text = render_at_pattern(line)
|
|
52
67
|
|
|
@@ -54,6 +69,7 @@ def render_user_input(content: str) -> RenderableType:
|
|
|
54
69
|
splits = line.split(" ", maxsplit=1)
|
|
55
70
|
if is_slash_command_name(splits[0][1:]):
|
|
56
71
|
has_command = True
|
|
72
|
+
command_style = ThemeKey.USER_INPUT_SLASH_COMMAND
|
|
57
73
|
line_text = Text.assemble(
|
|
58
74
|
(f"{splits[0]}", ThemeKey.USER_INPUT_SLASH_COMMAND),
|
|
59
75
|
" ",
|
|
@@ -62,13 +78,27 @@ def render_user_input(content: str) -> RenderableType:
|
|
|
62
78
|
renderables.append(line_text)
|
|
63
79
|
continue
|
|
64
80
|
|
|
81
|
+
if i == 0 and (line.startswith("$") or line.startswith("¥")):
|
|
82
|
+
m = SKILL_RENDER_PATTERN.match(line)
|
|
83
|
+
if m and _is_valid_skill_name(m.group(1)):
|
|
84
|
+
has_command = True
|
|
85
|
+
command_style = ThemeKey.USER_INPUT_SKILL
|
|
86
|
+
skill_token = m.group(0) # e.g. "$skill-name"
|
|
87
|
+
rest = line[len(skill_token) :]
|
|
88
|
+
line_text = Text.assemble(
|
|
89
|
+
(skill_token, ThemeKey.USER_INPUT_SKILL),
|
|
90
|
+
render_at_pattern(rest) if rest else Text(""),
|
|
91
|
+
)
|
|
92
|
+
renderables.append(line_text)
|
|
93
|
+
continue
|
|
94
|
+
|
|
65
95
|
renderables.append(line_text)
|
|
66
96
|
grid = create_grid()
|
|
67
97
|
grid.padding = (0, 0)
|
|
68
98
|
mark = (
|
|
69
|
-
Text(
|
|
99
|
+
Text(USER_MESSAGE_MARK, style=ThemeKey.USER_INPUT_PROMPT)
|
|
70
100
|
if not has_command
|
|
71
|
-
else Text(" ", style=ThemeKey.USER_INPUT_SLASH_COMMAND)
|
|
101
|
+
else Text(" ", style=command_style or ThemeKey.USER_INPUT_SLASH_COMMAND)
|
|
72
102
|
)
|
|
73
103
|
grid.add_row(mark, Group(*renderables))
|
|
74
104
|
return grid
|