2121from lazyclaude .services .filter import FilterService
2222from lazyclaude .services .writer import CustomizationWriter
2323from lazyclaude .widgets .combined_panel import CombinedPanel
24+ from lazyclaude .widgets .delete_confirm import DeleteConfirm
2425from lazyclaude .widgets .detail_pane import MainPane
2526from lazyclaude .widgets .filter_input import FilterInput
2627from lazyclaude .widgets .level_selector import LevelSelector
@@ -41,6 +42,7 @@ class LazyClaude(App):
4142 Binding ("e" , "open_in_editor" , "Edit" ),
4243 Binding ("c" , "copy_customization" , "Copy" ),
4344 Binding ("m" , "move_customization" , "Move" ),
45+ Binding ("d" , "delete_customization" , "Delete" ),
4446 Binding ("C" , "copy_config_path" , "Copy Path" ),
4547 Binding ("tab" , "focus_next_panel" , "Next Panel" , show = False ),
4648 Binding ("shift+tab" , "focus_previous_panel" , "Prev Panel" , show = False ),
@@ -67,6 +69,16 @@ class LazyClaude(App):
6769 TITLE = "LazyClaude"
6870 SUB_TITLE = "Claude Code Customization Viewer"
6971
72+ _COPYABLE_TYPES = (
73+ CustomizationType .SLASH_COMMAND ,
74+ CustomizationType .SUBAGENT ,
75+ CustomizationType .SKILL ,
76+ CustomizationType .HOOK ,
77+ CustomizationType .MCP ,
78+ CustomizationType .MEMORY_FILE ,
79+ )
80+ _PROJECT_LOCAL_TYPES = (CustomizationType .HOOK , CustomizationType .MCP )
81+
7082 def __init__ (
7183 self ,
7284 discovery_service : ConfigDiscoveryService | None = None ,
@@ -93,11 +105,13 @@ def __init__(
93105 self ._filter_input : FilterInput | None = None
94106 self ._level_selector : LevelSelector | None = None
95107 self ._plugin_confirm : PluginConfirm | None = None
108+ self ._delete_confirm : DeleteConfirm | None = None
96109 self ._help_visible = False
97110 self ._last_focused_panel : TypePanel | None = None
98111 self ._last_focused_combined : bool = False
99112 self ._pending_customization : Customization | None = None
100113 self ._panel_before_selector : TypePanel | None = None
114+ self ._combined_before_selector : bool = False
101115 self ._config_path_resolver : ConfigPathResolver | None = None
102116
103117 def _fatal_error (self ) -> None :
@@ -138,6 +152,9 @@ def compose(self) -> ComposeResult:
138152 self ._plugin_confirm = PluginConfirm (id = "plugin-confirm" )
139153 yield self ._plugin_confirm
140154
155+ self ._delete_confirm = DeleteConfirm (id = "delete-confirm" )
156+ yield self ._delete_confirm
157+
141158 yield Footer ()
142159
143160 def on_mount (self ) -> None :
@@ -160,21 +177,23 @@ def check_action(
160177 return False
161178 return self ._main_pane .customization .plugin_info is not None
162179
163- if action in ("copy_customization" , "move_customization" ):
180+ if action in (
181+ "copy_customization" ,
182+ "move_customization" ,
183+ "delete_customization" ,
184+ ):
164185 if not self ._main_pane or not self ._main_pane .customization :
165186 return False
166187
188+ if self ._is_skill_subfile_selected ():
189+ return False
190+
167191 customization = self ._main_pane .customization
168192
169- if customization .type not in [
170- CustomizationType .SLASH_COMMAND ,
171- CustomizationType .SUBAGENT ,
172- CustomizationType .SKILL ,
173- ]:
193+ if customization .type not in self ._COPYABLE_TYPES :
174194 return False
175-
176195 if (
177- action == " move_customization"
196+ action in ( "delete_customization" , " move_customization")
178197 and customization .level == ConfigLevel .PLUGIN
179198 ):
180199 return False
@@ -379,28 +398,22 @@ def action_copy_customization(self) -> None:
379398
380399 customization = self ._main_pane .customization
381400
382- if customization .type not in [
383- CustomizationType .SLASH_COMMAND ,
384- CustomizationType .SUBAGENT ,
385- CustomizationType .SKILL ,
386- ]:
401+ if customization .type not in self ._COPYABLE_TYPES :
387402 self ._show_status_error (
388403 f"Cannot copy { customization .type_label } customizations"
389404 )
390405 return
391406
392- available = [
393- level
394- for level in [ConfigLevel .USER , ConfigLevel .PROJECT ]
395- if level != customization .level
396- ]
397-
407+ available = self ._get_available_target_levels (customization )
398408 if not available :
399409 self ._show_status_error ("No available target levels" )
400410 return
401411
402412 self ._pending_customization = customization
403413 self ._panel_before_selector = self ._get_focused_panel ()
414+ self ._combined_before_selector = (
415+ self ._combined_panel .has_focus if self ._combined_panel else False
416+ )
404417 if self ._level_selector :
405418 self ._level_selector .show (available , "copy" )
406419
@@ -411,11 +424,7 @@ def action_move_customization(self) -> None:
411424
412425 customization = self ._main_pane .customization
413426
414- if customization .type not in [
415- CustomizationType .SLASH_COMMAND ,
416- CustomizationType .SUBAGENT ,
417- CustomizationType .SKILL ,
418- ]:
427+ if customization .type not in self ._COPYABLE_TYPES :
419428 self ._show_status_error (
420429 f"Cannot move { customization .type_label } customizations"
421430 )
@@ -425,21 +434,43 @@ def action_move_customization(self) -> None:
425434 self ._show_status_error ("Cannot move from plugin-level customizations" )
426435 return
427436
428- available = [
429- level
430- for level in [ConfigLevel .USER , ConfigLevel .PROJECT ]
431- if level != customization .level
432- ]
433-
437+ available = self ._get_available_target_levels (customization )
434438 if not available :
435439 self ._show_status_error ("No available target levels" )
436440 return
437441
438442 self ._pending_customization = customization
439443 self ._panel_before_selector = self ._get_focused_panel ()
444+ self ._combined_before_selector = (
445+ self ._combined_panel .has_focus if self ._combined_panel else False
446+ )
440447 if self ._level_selector :
441448 self ._level_selector .show (available , "move" )
442449
450+ def action_delete_customization (self ) -> None :
451+ """Delete selected customization."""
452+ if not self ._main_pane or not self ._main_pane .customization :
453+ return
454+
455+ customization = self ._main_pane .customization
456+
457+ if customization .type not in self ._COPYABLE_TYPES :
458+ self ._show_status_error (
459+ f"Cannot delete { customization .type_label } customizations"
460+ )
461+ return
462+
463+ if customization .level == ConfigLevel .PLUGIN :
464+ self ._show_status_error ("Cannot delete plugin-level customizations" )
465+ return
466+
467+ self ._panel_before_selector = self ._get_focused_panel ()
468+ self ._combined_before_selector = (
469+ self ._combined_panel .has_focus if self ._combined_panel else False
470+ )
471+ if self ._delete_confirm :
472+ self ._delete_confirm .show (customization )
473+
443474 async def action_back (self ) -> None :
444475 """Go back - return focus to panel from main pane, keep content visible."""
445476 if self ._main_pane and self ._main_pane .has_focus :
@@ -483,6 +514,46 @@ def _get_focused_panel(self) -> TypePanel | None:
483514 return panel
484515 return None
485516
517+ def _is_skill_subfile_selected (self ) -> bool :
518+ """Check if a skill subfile is currently selected (not root skill)."""
519+ panel = self ._get_focused_panel ()
520+ if (
521+ panel
522+ and panel ._is_skills_panel
523+ and panel ._flat_items
524+ and 0 <= panel .selected_index < len (panel ._flat_items )
525+ ):
526+ _ , file_path = panel ._flat_items [panel .selected_index ]
527+ return file_path is not None
528+ return False
529+
530+ def _get_available_target_levels (
531+ self , customization : Customization
532+ ) -> list [ConfigLevel ]:
533+ """Get available target levels for copy/move based on customization type."""
534+ if customization .type in self ._PROJECT_LOCAL_TYPES :
535+ all_levels = [
536+ ConfigLevel .USER ,
537+ ConfigLevel .PROJECT ,
538+ ConfigLevel .PROJECT_LOCAL ,
539+ ]
540+ else :
541+ all_levels = [ConfigLevel .USER , ConfigLevel .PROJECT ]
542+ return [level for level in all_levels if level != customization .level ]
543+
544+ def _delete_customization (
545+ self , customization : Customization , writer : CustomizationWriter
546+ ) -> tuple [bool , str ]:
547+ """Delete customization using type-specific method."""
548+ if customization .type == CustomizationType .MCP :
549+ return writer .delete_mcp_customization (
550+ customization , self ._discovery_service .project_config_path
551+ )
552+ elif customization .type == CustomizationType .HOOK :
553+ return writer .delete_hook_customization (customization )
554+ else :
555+ return writer .delete_customization (customization )
556+
486557 def _get_focused_panel_index (self ) -> int | None :
487558 """Get the index of the currently focused panel (combined panel = len(panels))."""
488559 for i , panel in enumerate (self ._panels ):
@@ -611,6 +682,9 @@ def action_toggle_plugin_enabled(self) -> None:
611682 return
612683
613684 self ._panel_before_selector = self ._get_focused_panel ()
685+ self ._combined_before_selector = (
686+ self ._combined_panel .has_focus if self ._combined_panel else False
687+ )
614688 if self ._plugin_confirm :
615689 self ._plugin_confirm .show (
616690 plugin_info = customization .plugin_info ,
@@ -714,33 +788,80 @@ def on_plugin_confirm_confirmation_cancelled(
714788 """Handle plugin confirmation cancellation."""
715789 self ._restore_focus_after_selector ()
716790
791+ def on_delete_confirm_delete_confirmed (
792+ self , message : DeleteConfirm .DeleteConfirmed
793+ ) -> None :
794+ """Handle delete confirmation."""
795+ customization = message .customization
796+ writer = CustomizationWriter ()
797+ success , msg = self ._delete_customization (customization , writer )
798+
799+ if success :
800+ self .notify (msg , severity = "information" )
801+ self .action_refresh ()
802+ else :
803+ self .notify (msg , severity = "error" )
804+ self ._restore_focus_after_selector ()
805+
806+ def on_delete_confirm_delete_cancelled (
807+ self ,
808+ message : DeleteConfirm .DeleteCancelled , # noqa: ARG002
809+ ) -> None :
810+ """Handle delete cancellation."""
811+ self ._restore_focus_after_selector ()
812+
717813 def _restore_focus_after_selector (self ) -> None :
718814 """Restore focus to the panel that was focused before the level selector."""
719- if self ._panel_before_selector :
815+ if self ._combined_before_selector and self ._combined_panel :
816+ self ._combined_panel .focus ()
817+ self ._combined_before_selector = False
818+ self ._panel_before_selector = None
819+ elif self ._panel_before_selector :
720820 self ._panel_before_selector .focus ()
721821 self ._panel_before_selector = None
822+ self ._combined_before_selector = False
722823 elif self ._panels :
723824 self ._panels [0 ].focus ()
724825
725826 def _handle_copy_or_move (
726827 self , customization : Customization , target_level : ConfigLevel , operation : str
727828 ) -> None :
728829 """Handle copy or move operation."""
830+ if operation == "move" and customization .level == ConfigLevel .PLUGIN :
831+ self ._show_status_error ("Cannot move from plugin (read-only source)" )
832+ return
833+
729834 writer = CustomizationWriter ()
730835
731- success , msg = writer .write_customization (
732- customization ,
733- target_level ,
734- self ._discovery_service .user_config_path ,
735- self ._discovery_service .project_config_path ,
736- )
836+ if customization .type == CustomizationType .MCP :
837+ success , msg = writer .write_mcp_customization (
838+ customization ,
839+ target_level ,
840+ self ._discovery_service .project_config_path ,
841+ )
842+ elif customization .type == CustomizationType .HOOK :
843+ success , msg = writer .write_hook_customization (
844+ customization ,
845+ target_level ,
846+ self ._discovery_service .user_config_path ,
847+ self ._discovery_service .project_config_path ,
848+ )
849+ else :
850+ success , msg = writer .write_customization (
851+ customization ,
852+ target_level ,
853+ self ._discovery_service .user_config_path ,
854+ self ._discovery_service .project_config_path ,
855+ )
737856
738857 if not success :
739858 self ._show_status_error (msg )
740859 return
741860
742861 if operation == "move" :
743- delete_success , delete_msg = writer .delete_customization (customization )
862+ delete_success , delete_msg = self ._delete_customization (
863+ customization , writer
864+ )
744865 if not delete_success :
745866 self ._show_status_error (
746867 f"Copied but failed to delete source: { delete_msg } "
0 commit comments