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
@@ -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 = "•") -> RenderableType:
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((">", ThemeKey.TOOL_MARK), " ", ("Bash", ThemeKey.TOOL_NAME))
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(("◎", ThemeKey.TOOL_MARK), " ", ("Update Plan", ThemeKey.TOOL_NAME))
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("←", ThemeKey.TOOL_MARK), render_result)
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(("→", ThemeKey.TOOL_MARK), " ", ("Edit", ThemeKey.TOOL_NAME))
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,32 +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(("→", ThemeKey.TOOL_MARK), " ", ("Write", ThemeKey.TOOL_NAME))
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(("→", ThemeKey.TOOL_MARK), " ", ("Write", ThemeKey.TOOL_NAME))
192
- arguments_column = Text(
193
- arguments.strip()[: const.INVALID_TOOL_CALL_MAX_LENGTH],
194
- style=ThemeKey.INVALID_TOOL_CALL_ARGS,
195
- )
196
- grid.add_row(tool_name_column, arguments_column)
197
- return grid
198
-
199
-
200
- def render_multi_edit_tool_call(arguments: str) -> RenderableType:
201
- grid = create_grid()
202
- tool_name_column = Text.assemble(("→", ThemeKey.TOOL_MARK), " ", ("MultiEdit", ThemeKey.TOOL_NAME))
203
- try:
204
- json_dict = json.loads(arguments)
205
- file_path = json_dict.get("file_path")
206
- edits = json_dict.get("edits", [])
207
- arguments_column = Text.assemble(
208
- render_path(file_path, ThemeKey.TOOL_PARAM_FILE_PATH),
209
- Text(" - "),
210
- Text(f"{len(edits)}", ThemeKey.TOOL_PARAM_BOLD),
211
- Text(" updates", ThemeKey.TOOL_PARAM_FILE_PATH),
212
- )
213
- except json.JSONDecodeError:
209
+ tool_name_column = Text.assemble((MARK_WRITE, ThemeKey.TOOL_MARK), " ", ("Write", ThemeKey.TOOL_NAME))
214
210
  arguments_column = Text(
215
211
  arguments.strip()[: const.INVALID_TOOL_CALL_MAX_LENGTH],
216
212
  style=ThemeKey.INVALID_TOOL_CALL_ARGS,
@@ -221,7 +217,7 @@ def render_multi_edit_tool_call(arguments: str) -> RenderableType:
221
217
 
222
218
  def render_apply_patch_tool_call(arguments: str) -> RenderableType:
223
219
  grid = create_grid()
224
- tool_name_column = Text.assemble(("→", ThemeKey.TOOL_MARK), " ", ("Apply Patch", ThemeKey.TOOL_NAME))
220
+ tool_name_column = Text.assemble((MARK_EDIT, ThemeKey.TOOL_MARK), " ", ("Apply Patch", ThemeKey.TOOL_NAME))
225
221
 
226
222
  try:
227
223
  payload = json.loads(arguments)
@@ -237,9 +233,27 @@ def render_apply_patch_tool_call(arguments: str) -> RenderableType:
237
233
  arguments_column = Text("", ThemeKey.TOOL_PARAM)
238
234
 
239
235
  if isinstance(patch_content, str):
240
- lines = [line for line in patch_content.splitlines() if line and not line.startswith("*** Begin Patch")]
241
- if lines:
242
- arguments_column = Text(lines[0][: const.INVALID_TOOL_CALL_MAX_LENGTH], ThemeKey.TOOL_PARAM)
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)
243
257
  else:
244
258
  arguments_column = Text(
245
259
  str(patch_content)[: const.INVALID_TOOL_CALL_MAX_LENGTH],
@@ -251,34 +265,24 @@ def render_apply_patch_tool_call(arguments: str) -> RenderableType:
251
265
 
252
266
 
253
267
  def render_todo(tr: events.ToolResultEvent) -> RenderableType:
254
- if not isinstance(tr.ui_extra, model.TodoListUIExtra):
255
- return Text.assemble(
256
- (" ✘", ThemeKey.ERROR_BOLD),
257
- " ",
258
- Text("(no content)" if tr.ui_extra is None else "(invalid ui_extra)", style=ThemeKey.ERROR),
259
- )
260
-
268
+ assert isinstance(tr.ui_extra, model.TodoListUIExtra)
261
269
  ui_extra = tr.ui_extra.todo_list
262
270
  todo_grid = create_grid()
263
271
  for todo in ui_extra.todos:
264
272
  is_new_completed = todo.content in ui_extra.new_completed
265
273
  match todo.status:
266
274
  case "pending":
267
- mark = "▢"
275
+ mark = MARK_TODO_PENDING
268
276
  mark_style = ThemeKey.TODO_PENDING_MARK
269
277
  text_style = ThemeKey.TODO_PENDING
270
278
  case "in_progress":
271
- mark = "◉"
279
+ mark = MARK_TODO_IN_PROGRESS
272
280
  mark_style = ThemeKey.TODO_IN_PROGRESS_MARK
273
281
  text_style = ThemeKey.TODO_IN_PROGRESS
274
282
  case "completed":
275
- mark = "✔"
283
+ mark = MARK_TODO_COMPLETED
276
284
  mark_style = ThemeKey.TODO_NEW_COMPLETED_MARK if is_new_completed else ThemeKey.TODO_COMPLETED_MARK
277
285
  text_style = ThemeKey.TODO_NEW_COMPLETED if is_new_completed else ThemeKey.TODO_COMPLETED
278
- case _:
279
- mark = "?"
280
- mark_style = ThemeKey.TODO_PENDING_MARK
281
- text_style = ThemeKey.TODO_PENDING
282
286
  text = Text(todo.content)
283
287
  text.stylize(text_style)
284
288
  todo_grid.add_row(Text(mark, style=mark_style), text)
@@ -300,63 +304,9 @@ def _extract_mermaid_link(
300
304
  return None
301
305
 
302
306
 
303
- def render_memory_tool_call(arguments: str) -> RenderableType:
304
- grid = create_grid()
305
- command_display_names: dict[str, str] = {
306
- "view": "View",
307
- "create": "Create",
308
- "str_replace": "Replace",
309
- "insert": "Insert",
310
- "delete": "Delete",
311
- "rename": "Rename",
312
- }
313
-
314
- try:
315
- payload: dict[str, str] = json.loads(arguments)
316
- except json.JSONDecodeError:
317
- tool_name_column = Text.assemble(("★", ThemeKey.TOOL_MARK), " ", ("Memory", ThemeKey.TOOL_NAME))
318
- summary = Text(
319
- arguments.strip()[: const.INVALID_TOOL_CALL_MAX_LENGTH],
320
- style=ThemeKey.INVALID_TOOL_CALL_ARGS,
321
- )
322
- grid.add_row(tool_name_column, summary)
323
- return grid
324
-
325
- command = payload.get("command", "")
326
- display_name = command_display_names.get(command, command.title())
327
- tool_name_column = Text.assemble(("★", ThemeKey.TOOL_MARK), " ", (f"{display_name} Memory", ThemeKey.TOOL_NAME))
328
-
329
- summary = Text("", ThemeKey.TOOL_PARAM)
330
- path = payload.get("path")
331
- old_path = payload.get("old_path")
332
- new_path = payload.get("new_path")
333
-
334
- if command == "rename" and old_path and new_path:
335
- summary = Text.assemble(
336
- Text(old_path, ThemeKey.TOOL_PARAM_FILE_PATH),
337
- Text(" -> ", ThemeKey.TOOL_PARAM),
338
- Text(new_path, ThemeKey.TOOL_PARAM_FILE_PATH),
339
- )
340
- elif command == "insert" and path:
341
- insert_line = payload.get("insert_line")
342
- summary = Text(path, ThemeKey.TOOL_PARAM_FILE_PATH)
343
- if insert_line is not None:
344
- summary.append(f" line {insert_line}", ThemeKey.TOOL_PARAM)
345
- elif command == "view" and path:
346
- view_range = payload.get("view_range")
347
- summary = Text(path, ThemeKey.TOOL_PARAM_FILE_PATH)
348
- if view_range and isinstance(view_range, list) and len(view_range) >= 2:
349
- summary.append(f" {view_range[0]}:{view_range[1]}", ThemeKey.TOOL_PARAM)
350
- elif path:
351
- summary = Text(path, ThemeKey.TOOL_PARAM_FILE_PATH)
352
-
353
- grid.add_row(tool_name_column, summary)
354
- return grid
355
-
356
-
357
307
  def render_mermaid_tool_call(arguments: str) -> RenderableType:
358
308
  grid = create_grid()
359
- tool_name_column = Text.assemble(("⧉", ThemeKey.TOOL_MARK), " ", ("Mermaid", ThemeKey.TOOL_NAME))
309
+ tool_name_column = Text.assemble((MARK_MERMAID, ThemeKey.TOOL_MARK), " ", ("Mermaid", ThemeKey.TOOL_NAME))
360
310
  summary = Text("", ThemeKey.TOOL_PARAM)
361
311
 
362
312
  try:
@@ -396,7 +346,7 @@ def _truncate_url(url: str, max_length: int = 400) -> str:
396
346
 
397
347
  def render_web_fetch_tool_call(arguments: str) -> RenderableType:
398
348
  grid = create_grid()
399
- tool_name_column = Text.assemble(("↓", ThemeKey.TOOL_MARK), " ", ("Fetch", ThemeKey.TOOL_NAME))
349
+ tool_name_column = Text.assemble((MARK_WEB_FETCH, ThemeKey.TOOL_MARK), " ", ("Fetch", ThemeKey.TOOL_NAME))
400
350
 
401
351
  try:
402
352
  payload: dict[str, str] = json.loads(arguments)
@@ -417,7 +367,7 @@ def render_web_fetch_tool_call(arguments: str) -> RenderableType:
417
367
 
418
368
  def render_web_search_tool_call(arguments: str) -> RenderableType:
419
369
  grid = create_grid()
420
- tool_name_column = Text.assemble(("◉", ThemeKey.TOOL_MARK), " ", ("Search", ThemeKey.TOOL_NAME))
370
+ tool_name_column = Text.assemble((MARK_WEB_SEARCH, ThemeKey.TOOL_MARK), " ", ("Web Search", ThemeKey.TOOL_NAME))
421
371
 
422
372
  try:
423
373
  payload: dict[str, Any] = json.loads(arguments)
@@ -494,7 +444,7 @@ def get_truncation_info(tr: events.ToolResultEvent) -> model.TruncationUIExtra |
494
444
 
495
445
  def render_report_back_tool_call() -> RenderableType:
496
446
  grid = create_grid()
497
- tool_name_column = Text.assemble(("✔", ThemeKey.TOOL_MARK), " ", ("Report Back", ThemeKey.TOOL_NAME))
447
+ tool_name_column = Text.assemble((MARK_DONE, ThemeKey.TOOL_MARK), " ", ("Report Back", ThemeKey.TOOL_NAME))
498
448
  grid.add_row(tool_name_column, "")
499
449
  return grid
500
450
 
@@ -504,14 +454,12 @@ _TOOL_ACTIVE_FORM: dict[str, str] = {
504
454
  tools.BASH: "Bashing",
505
455
  tools.APPLY_PATCH: "Patching",
506
456
  tools.EDIT: "Editing",
507
- tools.MULTI_EDIT: "Editing",
508
457
  tools.READ: "Reading",
509
458
  tools.WRITE: "Writing",
510
459
  tools.TODO_WRITE: "Planning",
511
460
  tools.UPDATE_PLAN: "Planning",
512
461
  tools.SKILL: "Skilling",
513
462
  tools.MERMAID: "Diagramming",
514
- tools.MEMORY: "Memorizing",
515
463
  tools.WEB_FETCH: "Fetching Web",
516
464
  tools.WEB_SEARCH: "Searching Web",
517
465
  tools.REPORT_BACK: "Reporting",
@@ -552,22 +500,18 @@ def render_tool_call(e: events.ToolCallEvent) -> RenderableType | None:
552
500
  return render_edit_tool_call(e.arguments)
553
501
  case tools.WRITE:
554
502
  return render_write_tool_call(e.arguments)
555
- case tools.MULTI_EDIT:
556
- return render_multi_edit_tool_call(e.arguments)
557
503
  case tools.BASH:
558
504
  return render_bash_tool_call(e.arguments)
559
505
  case tools.APPLY_PATCH:
560
506
  return render_apply_patch_tool_call(e.arguments)
561
507
  case tools.TODO_WRITE:
562
- return render_generic_tool_call("Update Todos", "", "◎")
508
+ return render_generic_tool_call("Update Todos", "", MARK_PLAN)
563
509
  case tools.UPDATE_PLAN:
564
510
  return render_update_plan_tool_call(e.arguments)
565
511
  case tools.MERMAID:
566
512
  return render_mermaid_tool_call(e.arguments)
567
- case tools.MEMORY:
568
- return render_memory_tool_call(e.arguments)
569
513
  case tools.SKILL:
570
- return render_generic_tool_call(e.tool_name, e.arguments, "◈")
514
+ return render_generic_tool_call(e.tool_name, e.arguments, MARK_SKILL)
571
515
  case tools.REPORT_BACK:
572
516
  return render_report_back_tool_call()
573
517
  case tools.WEB_FETCH:
@@ -578,9 +522,9 @@ def render_tool_call(e: events.ToolCallEvent) -> RenderableType | None:
578
522
  return render_generic_tool_call(e.tool_name, e.arguments)
579
523
 
580
524
 
581
- def _extract_diff_text(ui_extra: model.ToolResultUIExtra | None) -> str | None:
582
- if isinstance(ui_extra, model.DiffTextUIExtra):
583
- return ui_extra.diff_text
525
+ def _extract_diff(ui_extra: model.ToolResultUIExtra | None) -> model.DiffUIExtra | None:
526
+ if isinstance(ui_extra, model.DiffUIExtra):
527
+ return ui_extra
584
528
  return None
585
529
 
586
530
 
@@ -604,28 +548,30 @@ def render_tool_result(e: events.ToolResultEvent) -> RenderableType | None:
604
548
  if truncation_info:
605
549
  return Group(render_truncation_info(truncation_info), render_generic_tool_result(e.result))
606
550
 
607
- diff_text = _extract_diff_text(e.ui_extra)
551
+ diff_ui = _extract_diff(e.ui_extra)
608
552
 
609
553
  match e.tool_name:
610
554
  case tools.READ:
611
555
  return None
612
- case tools.EDIT | tools.MULTI_EDIT | tools.WRITE:
613
- return Padding.indent(r_diffs.render_diff(diff_text or ""), level=2)
614
- case tools.MEMORY:
615
- if diff_text:
616
- return Padding.indent(r_diffs.render_diff(diff_text), level=2)
617
- elif len(e.result.strip()) > 0:
618
- return render_generic_tool_result(e.result)
619
- return None
556
+ case tools.EDIT | tools.WRITE:
557
+ return Padding.indent(r_diffs.render_structured_diff(diff_ui) if diff_ui else Text(""), level=2)
558
+ case tools.APPLY_PATCH:
559
+ if diff_ui:
560
+ return Padding.indent(r_diffs.render_structured_diff(diff_ui, show_file_name=True), level=2)
561
+ if len(e.result.strip()) == 0:
562
+ return render_generic_tool_result("(no content)")
563
+ return render_generic_tool_result(e.result)
620
564
  case tools.TODO_WRITE | tools.UPDATE_PLAN:
621
565
  return render_todo(e)
622
566
  case tools.MERMAID:
623
567
  return render_mermaid_tool_result(e)
624
- case _:
625
- if e.tool_name in (tools.BASH, tools.APPLY_PATCH) and e.result.startswith("diff --git"):
568
+ case tools.BASH:
569
+ if e.result.startswith("diff --git"):
626
570
  return r_diffs.render_diff_panel(e.result, show_file_name=True)
627
- if e.tool_name == tools.APPLY_PATCH and diff_text:
628
- return Padding.indent(r_diffs.render_diff(diff_text, show_file_name=True), level=2)
571
+ if len(e.result.strip()) == 0:
572
+ return render_generic_tool_result("(no content)")
573
+ return render_generic_tool_result(e.result)
574
+ case _:
629
575
  if len(e.result.strip()) == 0:
630
576
  return render_generic_tool_result("(no content)")
631
577
  return render_generic_tool_result(e.result)
@@ -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("❯ ", style=ThemeKey.USER_INPUT_PROMPT)
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
@@ -9,7 +9,7 @@ from typing import Any, ClassVar
9
9
 
10
10
  from rich.console import Console, ConsoleOptions, Group, RenderableType, RenderResult
11
11
  from rich.live import Live
12
- from rich.markdown import CodeBlock, Heading, Markdown
12
+ from rich.markdown import CodeBlock, Heading, Markdown, MarkdownElement
13
13
  from rich.rule import Rule
14
14
  from rich.spinner import Spinner
15
15
  from rich.style import Style
@@ -45,6 +45,14 @@ class ThinkingCodeBlock(CodeBlock):
45
45
  yield CodePanel(text, border_style="markdown.code.border")
46
46
 
47
47
 
48
+ class Divider(MarkdownElement):
49
+ """A horizontal rule with an extra blank line below."""
50
+
51
+ def __rich_console__(self, console: Console, options: ConsoleOptions) -> RenderResult:
52
+ style = console.get_style("markdown.hr", default="none")
53
+ yield Rule(style=style, characters="-")
54
+
55
+
48
56
  class LeftHeading(Heading):
49
57
  """A heading class that renders left-justified."""
50
58
 
@@ -69,6 +77,7 @@ class NoInsetMarkdown(Markdown):
69
77
  "fence": NoInsetCodeBlock,
70
78
  "code_block": NoInsetCodeBlock,
71
79
  "heading_open": LeftHeading,
80
+ "hr": Divider,
72
81
  }
73
82
 
74
83
 
@@ -80,6 +89,7 @@ class ThinkingMarkdown(Markdown):
80
89
  "fence": ThinkingCodeBlock,
81
90
  "code_block": ThinkingCodeBlock,
82
91
  "heading_open": LeftHeading,
92
+ "hr": Divider,
83
93
  }
84
94
 
85
95
 
@@ -98,7 +108,8 @@ class MarkdownStream:
98
108
  console: Console | None = None,
99
109
  spinner: Spinner | None = None,
100
110
  mark: str | None = None,
101
- indent: int = 0,
111
+ left_margin: int = 0,
112
+ right_margin: int = const.MARKDOWN_RIGHT_MARGIN,
102
113
  markdown_class: Callable[..., Markdown] | None = None,
103
114
  ) -> None:
104
115
  """Initialize the markdown stream.
@@ -107,8 +118,9 @@ class MarkdownStream:
107
118
  mdargs (dict, optional): Additional arguments to pass to rich Markdown renderer
108
119
  theme (Theme, optional): Theme for rendering markdown
109
120
  console (Console, optional): External console to use for rendering
110
- mark (str | None, optional): Marker shown before the first non-empty line when indent >= 2
111
- indent (int, optional): Number of spaces to indent all rendered lines on the left
121
+ mark (str | None, optional): Marker shown before the first non-empty line when left_margin >= 2
122
+ left_margin (int, optional): Number of columns to reserve on the left side
123
+ right_margin (int, optional): Number of columns to reserve on the right side
112
124
  markdown_class: Markdown class to use for rendering (defaults to NoInsetMarkdown)
113
125
  """
114
126
  self.printed: list[str] = [] # Stores lines that have already been printed
@@ -130,7 +142,10 @@ class MarkdownStream:
130
142
  self.console = console
131
143
  self.spinner: Spinner | None = spinner
132
144
  self.mark: str | None = mark
133
- self.indent: int = max(indent, 0)
145
+
146
+ self.left_margin: int = max(left_margin, 0)
147
+
148
+ self.right_margin: int = max(right_margin, 0)
134
149
  self.markdown_class: Callable[..., Markdown] = markdown_class or NoInsetMarkdown
135
150
 
136
151
  @property
@@ -150,15 +165,15 @@ class MarkdownStream:
150
165
  # Render the markdown to a string buffer
151
166
  string_io = io.StringIO()
152
167
 
153
- # Determine console width and adjust for left indent so that
154
- # the rendered content plus indent does not exceed the available width.
168
+ # Determine console width and adjust for left margin so that
169
+ # the rendered content plus margins does not exceed the available width.
155
170
  if self.console is not None:
156
171
  base_width = self.console.options.max_width
157
172
  else:
158
173
  probe_console = Console(theme=self.theme)
159
174
  base_width = probe_console.options.max_width
160
175
 
161
- effective_width = max(base_width - self.indent, 1)
176
+ effective_width = max(base_width - self.left_margin - self.right_margin, 1)
162
177
 
163
178
  # Use external console for consistent theming, or create temporary one
164
179
  temp_console = Console(
@@ -172,17 +187,17 @@ class MarkdownStream:
172
187
  temp_console.print(markdown)
173
188
  output = string_io.getvalue()
174
189
 
175
- # Split rendered output into lines, strip trailing spaces, and apply left indent.
190
+ # Split rendered output into lines, strip trailing spaces, and apply left margin.
176
191
  lines = output.splitlines(keepends=True)
177
- indent_prefix = " " * self.indent if self.indent > 0 else ""
192
+ indent_prefix = " " * self.left_margin if self.left_margin > 0 else ""
178
193
  processed_lines: list[str] = []
179
194
  mark_applied = False
180
- use_mark = bool(self.mark) and self.indent >= 2
195
+ use_mark = bool(self.mark) and self.left_margin >= 2
181
196
 
182
197
  for line in lines:
183
198
  stripped = line.rstrip()
184
199
 
185
- # Apply mark to the first non-empty line only when indent is at least 2.
200
+ # Apply mark to the first non-empty line only when left_margin is at least 2.
186
201
  if use_mark and not mark_applied and stripped:
187
202
  stripped = f"{self.mark} {stripped}"
188
203
  mark_applied = True
@@ -22,18 +22,7 @@ BREATHING_SPINNER_NAME = "dots"
22
22
 
23
23
  # Alternating glyphs for the breathing spinner - switches at each "transparent" point
24
24
  _BREATHING_SPINNER_GLYPHS_BASE = [
25
- "",
26
- "✶",
27
- "✲",
28
- "◆",
29
- "❖",
30
- "✧",
31
- "❋",
32
- "✸",
33
- "✻",
34
- "◇",
35
- "✴",
36
- "✷",
25
+ "",
37
26
  ]
38
27
 
39
28
  # Shuffle glyphs on module load for variety across sessions
@@ -114,7 +103,6 @@ def _shimmer_style(console: Console, base_style: Style, intensity: float) -> Sty
114
103
 
115
104
  base_r, base_g, base_b = base_triplet
116
105
  bg_r, bg_g, bg_b = bg_triplet
117
-
118
106
  r = int(bg_r * alpha + base_r * (1.0 - alpha))
119
107
  g = int(bg_g * alpha + base_g * (1.0 - alpha))
120
108
  b = int(bg_b * alpha + base_b * (1.0 - alpha))
@@ -203,24 +191,21 @@ class ShimmerStatusText:
203
191
  yield table
204
192
 
205
193
  def _render_left_text(self, console: Console) -> Text:
206
- """Render the left part with shimmer effect."""
194
+ """Render the left part with shimmer effect on main text only."""
207
195
  result = Text()
208
196
  main_style = console.get_style(str(self._main_style))
209
197
  hint_style = console.get_style(str(self._hint_style))
210
198
 
211
- combined_text = self._main_text.plain + self._hint_text.plain
212
- split_index = len(self._main_text.plain)
213
-
214
- for index, (ch, intensity) in enumerate(_shimmer_profile(combined_text)):
215
- if index < split_index:
216
- # Get style from main_text, merge with main_style
217
- char_style = self._main_text.get_style_at_offset(console, index)
218
- base_style = main_style + char_style
219
- else:
220
- base_style = hint_style
199
+ # Apply shimmer only to main text
200
+ for index, (ch, intensity) in enumerate(_shimmer_profile(self._main_text.plain)):
201
+ char_style = self._main_text.get_style_at_offset(console, index)
202
+ base_style = main_style + char_style
221
203
  style = _shimmer_style(console, base_style, intensity)
222
204
  result.append(ch, style=style)
223
205
 
206
+ # Append hint text without shimmer
207
+ result.append(self._hint_text.plain, style=hint_style)
208
+
224
209
  return result
225
210
 
226
211