klaude-code 1.2.21__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.
Files changed (65) hide show
  1. klaude_code/cli/debug.py +8 -10
  2. klaude_code/command/__init__.py +0 -3
  3. klaude_code/command/status_cmd.py +1 -1
  4. klaude_code/const/__init__.py +10 -7
  5. klaude_code/core/manager/sub_agent_manager.py +1 -1
  6. klaude_code/core/prompt.py +5 -2
  7. klaude_code/core/prompts/prompt-codex-gpt-5-2-codex.md +117 -0
  8. klaude_code/core/prompts/{prompt-codex-gpt-5-1.md → prompt-codex.md} +9 -42
  9. klaude_code/core/reminders.py +87 -2
  10. klaude_code/core/task.py +37 -18
  11. klaude_code/core/tool/__init__.py +1 -9
  12. klaude_code/core/tool/file/_utils.py +6 -0
  13. klaude_code/core/tool/file/apply_patch_tool.py +30 -72
  14. klaude_code/core/tool/file/diff_builder.py +151 -0
  15. klaude_code/core/tool/file/edit_tool.py +35 -18
  16. klaude_code/core/tool/file/read_tool.py +45 -86
  17. klaude_code/core/tool/file/write_tool.py +40 -30
  18. klaude_code/core/tool/shell/bash_tool.py +147 -0
  19. klaude_code/core/tool/skill/__init__.py +0 -0
  20. klaude_code/core/tool/{memory → skill}/skill_tool.py +16 -39
  21. klaude_code/protocol/commands.py +0 -1
  22. klaude_code/protocol/model.py +31 -11
  23. klaude_code/protocol/tools.py +1 -2
  24. klaude_code/session/export.py +76 -21
  25. klaude_code/session/store.py +4 -2
  26. klaude_code/session/templates/export_session.html +28 -0
  27. klaude_code/skill/__init__.py +27 -0
  28. klaude_code/skill/assets/deslop/SKILL.md +17 -0
  29. klaude_code/skill/assets/dev-docs/SKILL.md +108 -0
  30. klaude_code/skill/assets/handoff/SKILL.md +39 -0
  31. klaude_code/skill/assets/jj-workspace/SKILL.md +20 -0
  32. klaude_code/skill/assets/skill-creator/SKILL.md +139 -0
  33. klaude_code/{core/tool/memory/skill_loader.py → skill/loader.py} +60 -24
  34. klaude_code/skill/manager.py +70 -0
  35. klaude_code/skill/system_skills.py +192 -0
  36. klaude_code/ui/modes/repl/completers.py +103 -3
  37. klaude_code/ui/modes/repl/event_handler.py +7 -3
  38. klaude_code/ui/modes/repl/input_prompt_toolkit.py +42 -3
  39. klaude_code/ui/renderers/assistant.py +7 -2
  40. klaude_code/ui/renderers/common.py +26 -11
  41. klaude_code/ui/renderers/developer.py +12 -5
  42. klaude_code/ui/renderers/diffs.py +85 -1
  43. klaude_code/ui/renderers/metadata.py +4 -2
  44. klaude_code/ui/renderers/thinking.py +1 -1
  45. klaude_code/ui/renderers/tools.py +75 -129
  46. klaude_code/ui/renderers/user_input.py +32 -2
  47. klaude_code/ui/rich/markdown.py +27 -12
  48. klaude_code/ui/rich/status.py +9 -24
  49. klaude_code/ui/rich/theme.py +17 -5
  50. {klaude_code-1.2.21.dist-info → klaude_code-1.2.23.dist-info}/METADATA +19 -13
  51. {klaude_code-1.2.21.dist-info → klaude_code-1.2.23.dist-info}/RECORD +54 -54
  52. klaude_code/command/diff_cmd.py +0 -136
  53. klaude_code/command/prompt-deslop.md +0 -14
  54. klaude_code/command/prompt-dev-docs-update.md +0 -56
  55. klaude_code/command/prompt-dev-docs.md +0 -46
  56. klaude_code/command/prompt-handoff.md +0 -33
  57. klaude_code/command/prompt-jj-workspace.md +0 -18
  58. klaude_code/core/tool/file/multi_edit_tool.md +0 -42
  59. klaude_code/core/tool/file/multi_edit_tool.py +0 -175
  60. klaude_code/core/tool/memory/__init__.py +0 -5
  61. klaude_code/core/tool/memory/memory_tool.md +0 -20
  62. klaude_code/core/tool/memory/memory_tool.py +0 -456
  63. /klaude_code/core/tool/{memory → skill}/skill_tool.md +0 -0
  64. {klaude_code-1.2.21.dist-info → klaude_code-1.2.23.dist-info}/WHEEL +0 -0
  65. {klaude_code-1.2.21.dist-info → klaude_code-1.2.23.dist-info}/entry_points.txt +0 -0
@@ -15,12 +15,22 @@ class Skill:
15
15
  name: str # Skill identifier (lowercase-hyphen)
16
16
  description: str # What the skill does and when to use it
17
17
  content: str # Full markdown instructions
18
- location: str # Skill location: 'user' or 'project'
18
+ location: str # Skill location: 'system', 'user', or 'project'
19
19
  license: str | None = None
20
20
  allowed_tools: list[str] | None = None
21
21
  metadata: dict[str, str] | None = None
22
22
  skill_path: Path | None = None
23
23
 
24
+ @property
25
+ def short_description(self) -> str:
26
+ """Get short description for display in completions.
27
+
28
+ Returns metadata['short-description'] if available, otherwise falls back to description.
29
+ """
30
+ if self.metadata and "short-description" in self.metadata:
31
+ return self.metadata["short-description"]
32
+ return self.description
33
+
24
34
  def to_prompt(self) -> str:
25
35
  """Convert skill to prompt format for agent consumption"""
26
36
  return f"""# Skill: {self.name}
@@ -36,13 +46,15 @@ class Skill:
36
46
  class SkillLoader:
37
47
  """Load and manage Claude Skills from SKILL.md files"""
38
48
 
49
+ # System-level skills directory (built-in, lowest priority)
50
+ SYSTEM_SKILLS_DIR: ClassVar[Path] = Path("~/.klaude/skills/.system")
51
+
39
52
  # User-level skills directories (checked in order, later ones override earlier ones with same name)
40
53
  USER_SKILLS_DIRS: ClassVar[list[Path]] = [
41
54
  Path("~/.claude/skills"),
42
55
  Path("~/.klaude/skills"),
43
- # Path("~/.claude/plugins/marketplaces"),
44
56
  ]
45
- # Project-level skills directory
57
+ # Project-level skills directory (highest priority)
46
58
  PROJECT_SKILLS_DIR: ClassVar[Path] = Path("./.claude/skills")
47
59
 
48
60
  def __init__(self) -> None:
@@ -54,7 +66,7 @@ class SkillLoader:
54
66
 
55
67
  Args:
56
68
  skill_path: Path to SKILL.md file
57
- location: Skill location ('user' or 'project')
69
+ location: Skill location ('system', 'user', or 'project')
58
70
 
59
71
  Returns:
60
72
  Skill object or None if loading failed
@@ -121,39 +133,57 @@ class SkillLoader:
121
133
  return None
122
134
 
123
135
  def discover_skills(self) -> list[Skill]:
124
- """Recursively find all SKILL.md files and load them from both user and project directories
136
+ """Recursively find all SKILL.md files and load them from system, user and project directories.
137
+
138
+ Loading order (lower priority first, higher priority overrides):
139
+ 1. System skills (~/.klaude/skills/.system/) - built-in, lowest priority
140
+ 2. User skills (~/.claude/skills/, ~/.klaude/skills/) - user-level
141
+ 3. Project skills (./.claude/skills/) - project-level, highest priority
125
142
 
126
143
  Returns:
127
144
  List of successfully loaded Skill objects
128
145
  """
129
146
  skills: list[Skill] = []
130
147
 
131
- # Load user-level skills from all directories
148
+ # Load system-level skills first (lowest priority, can be overridden)
149
+ system_dir = self.SYSTEM_SKILLS_DIR.expanduser()
150
+ if system_dir.exists():
151
+ for skill_file in system_dir.rglob("SKILL.md"):
152
+ skill = self.load_skill(skill_file, location="system")
153
+ if skill:
154
+ skills.append(skill)
155
+ self.loaded_skills[skill.name] = skill
156
+
157
+ # Load user-level skills (override system skills if same name)
132
158
  for user_dir in self.USER_SKILLS_DIRS:
133
159
  expanded_dir = user_dir.expanduser()
134
160
  if expanded_dir.exists():
135
- for pattern in ("SKILL.md", "skill.md"):
136
- for skill_file in expanded_dir.rglob(pattern):
137
- skill = self.load_skill(skill_file, location="user")
138
- if skill:
139
- skills.append(skill)
140
- self.loaded_skills[skill.name] = skill
161
+ for skill_file in expanded_dir.rglob("SKILL.md"):
162
+ # Skip files under .system directory (already loaded above)
163
+ if ".system" in skill_file.parts:
164
+ continue
165
+ skill = self.load_skill(skill_file, location="user")
166
+ if skill:
167
+ skills.append(skill)
168
+ self.loaded_skills[skill.name] = skill
141
169
 
142
170
  # Load project-level skills (override user skills if same name)
143
171
  project_dir = self.PROJECT_SKILLS_DIR.resolve()
144
172
  if project_dir.exists():
145
- for pattern in ("SKILL.md", "skill.md"):
146
- for skill_file in project_dir.rglob(pattern):
147
- skill = self.load_skill(skill_file, location="project")
148
- if skill:
149
- skills.append(skill)
150
- self.loaded_skills[skill.name] = skill
173
+ for skill_file in project_dir.rglob("SKILL.md"):
174
+ skill = self.load_skill(skill_file, location="project")
175
+ if skill:
176
+ skills.append(skill)
177
+ self.loaded_skills[skill.name] = skill
151
178
 
152
179
  # Log discovery summary
153
180
  if skills:
181
+ system_count = sum(1 for s in skills if s.location == "system")
154
182
  user_count = sum(1 for s in skills if s.location == "user")
155
183
  project_count = sum(1 for s in skills if s.location == "project")
156
184
  parts: list[str] = []
185
+ if system_count > 0:
186
+ parts.append(f"{system_count} system")
157
187
  if user_count > 0:
158
188
  parts.append(f"{user_count} user")
159
189
  if project_count > 0:
@@ -171,11 +201,17 @@ class SkillLoader:
171
201
  Returns:
172
202
  Skill object or None if not found
173
203
  """
204
+ # Prefer exact match first (supports namespaced skill names).
205
+ skill = self.loaded_skills.get(name)
206
+ if skill is not None:
207
+ return skill
208
+
174
209
  # Support both formats: 'pdf' and 'document-skills:pdf'
175
210
  if ":" in name:
176
- name = name.split(":")[-1]
211
+ short = name.split(":")[-1]
212
+ return self.loaded_skills.get(short)
177
213
 
178
- return self.loaded_skills.get(name)
214
+ return None
179
215
 
180
216
  def list_skills(self) -> list[str]:
181
217
  """Get list of all loaded skill names"""
@@ -224,25 +260,25 @@ class SkillLoader:
224
260
  content = re.sub(dir_pattern, replace_dir_path, content)
225
261
 
226
262
  # Pattern 2: Markdown links [text](./path or path)
227
- # e.g., "[Guide](./docs/guide.md)" -> "[Guide](`/abs/path/to/docs/guide.md`) (use read_file to access)"
263
+ # e.g., "[Guide](./docs/guide.md)" -> "[Guide](`/abs/path/to/docs/guide.md`) (use the Read tool to access)"
228
264
  link_pattern = r"\[([^\]]+)\]\((\./)?([^\)]+\.md)\)"
229
265
 
230
266
  def replace_link(match: re.Match[str]) -> str:
231
267
  text = match.group(1)
232
268
  filename = match.group(3)
233
269
  abs_path = skill_dir / filename
234
- return f"[{text}](`{abs_path}`) (use read_file to access)"
270
+ return f"[{text}](`{abs_path}`) (use the Read tool to access)"
235
271
 
236
272
  content = re.sub(link_pattern, replace_link, content)
237
273
 
238
274
  # Pattern 3: Standalone markdown references
239
- # e.g., "see reference.md" -> "see `/abs/path/to/reference.md` (use read_file to access)"
275
+ # e.g., "see reference.md" -> "see `/abs/path/to/reference.md` (use the Read tool to access)"
240
276
  standalone_pattern = r"(?<!\])\b(\w+\.md)\b(?!\))"
241
277
 
242
278
  def replace_standalone(match: re.Match[str]) -> str:
243
279
  filename = match.group(1)
244
280
  abs_path = skill_dir / filename
245
- return f"`{abs_path}` (use read_file to access)"
281
+ return f"`{abs_path}` (use the Read tool to access)"
246
282
 
247
283
  content = re.sub(standalone_pattern, replace_standalone, content)
248
284
 
@@ -0,0 +1,70 @@
1
+ """Global skill manager with lazy initialization.
2
+
3
+ This module provides a centralized interface for accessing skills throughout the application.
4
+ Skills are loaded lazily on first access to avoid unnecessary IO at startup.
5
+ """
6
+
7
+ from klaude_code.skill.loader import Skill, SkillLoader
8
+ from klaude_code.skill.system_skills import install_system_skills
9
+
10
+ _loader: SkillLoader | None = None
11
+ _initialized: bool = False
12
+
13
+
14
+ def _ensure_initialized() -> SkillLoader:
15
+ """Ensure the skill system is initialized and return the loader."""
16
+ global _loader, _initialized
17
+ if not _initialized:
18
+ install_system_skills()
19
+ _loader = SkillLoader()
20
+ _loader.discover_skills()
21
+ _initialized = True
22
+ assert _loader is not None
23
+ return _loader
24
+
25
+
26
+ def get_skill_loader() -> SkillLoader:
27
+ """Get the global skill loader instance.
28
+
29
+ Lazily initializes the skill system on first call.
30
+
31
+ Returns:
32
+ The global SkillLoader instance
33
+ """
34
+ return _ensure_initialized()
35
+
36
+
37
+ def get_skill(name: str) -> Skill | None:
38
+ """Get a skill by name.
39
+
40
+ Args:
41
+ name: Skill name (supports both 'skill-name' and 'namespace:skill-name')
42
+
43
+ Returns:
44
+ Skill object or None if not found
45
+ """
46
+ return _ensure_initialized().get_skill(name)
47
+
48
+
49
+ def get_available_skills() -> list[tuple[str, str, str]]:
50
+ """Get list of available skills for completion and display.
51
+
52
+ Returns:
53
+ List of (name, short_description, location) tuples.
54
+ Uses metadata['short-description'] if available, otherwise falls back to description.
55
+ Skills are ordered by priority: project > user > system.
56
+ """
57
+ loader = _ensure_initialized()
58
+ skills = [(s.name, s.short_description, s.location) for s in loader.loaded_skills.values()]
59
+ location_order = {"project": 0, "user": 1, "system": 2}
60
+ skills.sort(key=lambda x: location_order.get(x[2], 3))
61
+ return skills
62
+
63
+
64
+ def list_skill_names() -> list[str]:
65
+ """Get list of all loaded skill names.
66
+
67
+ Returns:
68
+ List of skill names
69
+ """
70
+ return _ensure_initialized().list_skills()
@@ -0,0 +1,192 @@
1
+ """System skills management - install built-in skills to user directory.
2
+
3
+ This module handles extracting bundled skills from the package to ~/.klaude/skills/.system/
4
+ on application startup. It uses a fingerprint mechanism to avoid unnecessary re-extraction.
5
+ """
6
+
7
+ import hashlib
8
+ import shutil
9
+ from collections.abc import Iterator
10
+ from contextlib import contextmanager
11
+ from importlib import resources
12
+ from pathlib import Path
13
+
14
+ from klaude_code.trace import log_debug
15
+
16
+ # Marker file name for tracking installed skills version
17
+ SYSTEM_SKILLS_MARKER_FILENAME = ".klaude-system-skills.marker"
18
+
19
+ # Salt for fingerprint calculation (increment to force re-extraction)
20
+ SYSTEM_SKILLS_MARKER_SALT = "v1"
21
+
22
+
23
+ def get_system_skills_dir() -> Path:
24
+ """Get the system skills installation directory.
25
+
26
+ Returns:
27
+ Path to ~/.klaude/skills/.system/
28
+ """
29
+ return Path.home() / ".klaude" / "skills" / ".system"
30
+
31
+
32
+ def _calculate_fingerprint(assets_dir: Path) -> str:
33
+ """Calculate a fingerprint hash for the embedded skills assets.
34
+
35
+ The fingerprint is based on all file paths and their contents.
36
+
37
+ Args:
38
+ assets_dir: Path to the assets directory
39
+
40
+ Returns:
41
+ Hex string of the hash
42
+ """
43
+ hasher = hashlib.sha256()
44
+ hasher.update(SYSTEM_SKILLS_MARKER_SALT.encode())
45
+
46
+ if not assets_dir.exists():
47
+ return hasher.hexdigest()
48
+
49
+ # Sort entries for consistent ordering
50
+ for entry in sorted(assets_dir.rglob("*")):
51
+ if entry.is_file():
52
+ # Hash the relative path
53
+ rel_path = entry.relative_to(assets_dir)
54
+ hasher.update(str(rel_path).encode())
55
+ # Hash the file contents
56
+ hasher.update(entry.read_bytes())
57
+
58
+ return hasher.hexdigest()
59
+
60
+
61
+ def _read_marker(marker_path: Path) -> str | None:
62
+ """Read the fingerprint from the marker file.
63
+
64
+ Args:
65
+ marker_path: Path to the marker file
66
+
67
+ Returns:
68
+ The stored fingerprint, or None if the file doesn't exist or is invalid
69
+ """
70
+ try:
71
+ if marker_path.exists():
72
+ return marker_path.read_text(encoding="utf-8").strip()
73
+ except OSError:
74
+ pass
75
+ return None
76
+
77
+
78
+ def _write_marker(marker_path: Path, fingerprint: str) -> None:
79
+ """Write the fingerprint to the marker file.
80
+
81
+ Args:
82
+ marker_path: Path to the marker file
83
+ fingerprint: The fingerprint to store
84
+ """
85
+ marker_path.write_text(f"{fingerprint}\n", encoding="utf-8")
86
+
87
+
88
+ @contextmanager
89
+ def _with_embedded_assets_dir() -> Iterator[Path | None]:
90
+ """Resolve the embedded assets directory as a real filesystem path.
91
+
92
+ Uses `importlib.resources.as_file()` so it works for both normal installs
93
+ and zipimport-style environments.
94
+ """
95
+ try:
96
+ assets_ref = resources.files("klaude_code.skill").joinpath("assets")
97
+ with resources.as_file(assets_ref) as assets_path:
98
+ p = Path(assets_path)
99
+ yield p if p.exists() else None
100
+ return
101
+ except (TypeError, AttributeError, ImportError, FileNotFoundError, OSError):
102
+ pass
103
+
104
+ try:
105
+ module_dir = Path(__file__).parent
106
+ assets_path = module_dir / "assets"
107
+ yield assets_path if assets_path.exists() else None
108
+ except (TypeError, NameError, OSError):
109
+ yield None
110
+
111
+
112
+ def install_system_skills() -> bool:
113
+ """Install system skills from the embedded assets to the user directory.
114
+
115
+ This function:
116
+ 1. Calculates a fingerprint of the embedded assets
117
+ 2. Checks if the installed skills match (via marker file)
118
+ 3. If they don't match, clears and re-extracts the skills
119
+
120
+ Returns:
121
+ True if skills were installed/updated, False if already up-to-date
122
+ """
123
+ dest_dir = get_system_skills_dir()
124
+ marker_path = dest_dir / SYSTEM_SKILLS_MARKER_FILENAME
125
+
126
+ with _with_embedded_assets_dir() as assets_path:
127
+ if assets_path is None or not assets_path.exists():
128
+ log_debug("No embedded system skills found")
129
+ return False
130
+
131
+ # Calculate fingerprint of embedded assets
132
+ expected_fingerprint = _calculate_fingerprint(assets_path)
133
+
134
+ # Check if already installed with matching fingerprint
135
+ current_fingerprint = _read_marker(marker_path)
136
+ if current_fingerprint == expected_fingerprint and dest_dir.exists():
137
+ log_debug("System skills already up-to-date")
138
+ return False
139
+
140
+ log_debug(f"Installing system skills to {dest_dir}")
141
+
142
+ # Clear existing installation
143
+ if dest_dir.exists():
144
+ try:
145
+ shutil.rmtree(dest_dir)
146
+ except OSError as e:
147
+ log_debug(f"Failed to clear existing system skills: {e}")
148
+ return False
149
+
150
+ # Create destination directory
151
+ dest_dir.mkdir(parents=True, exist_ok=True)
152
+
153
+ # Copy all skill directories from assets
154
+ try:
155
+ for item in assets_path.iterdir():
156
+ if item.is_dir() and not item.name.startswith("."):
157
+ dest_skill_dir = dest_dir / item.name
158
+ shutil.copytree(item, dest_skill_dir)
159
+ log_debug(f"Installed system skill: {item.name}")
160
+ except OSError as e:
161
+ log_debug(f"Failed to copy system skills: {e}")
162
+ return False
163
+
164
+ # Write marker file
165
+ try:
166
+ _write_marker(marker_path, expected_fingerprint)
167
+ except OSError as e:
168
+ log_debug(f"Failed to write marker file: {e}")
169
+ # Installation succeeded, just marker failed
170
+
171
+ log_debug("System skills installation complete")
172
+ return True
173
+
174
+
175
+ def get_installed_system_skills() -> list[Path]:
176
+ """Get list of installed system skill directories.
177
+
178
+ Returns:
179
+ List of paths to installed skill directories
180
+ """
181
+ dest_dir = get_system_skills_dir()
182
+ if not dest_dir.exists():
183
+ return []
184
+
185
+ skills: list[Path] = []
186
+ for item in dest_dir.iterdir():
187
+ if item.is_dir() and not item.name.startswith("."):
188
+ skill_file = item / "SKILL.md"
189
+ if skill_file.exists():
190
+ skills.append(item)
191
+
192
+ return skills
@@ -1,13 +1,15 @@
1
- """REPL completion handlers for @ file paths and / slash commands.
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 both completers with priority logic
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 both @ file paths and / slash commands."""
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
- indent=2,
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
- indent=2,
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