lazyopencode 0.2.0__tar.gz → 0.2.1__tar.gz

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 (128) hide show
  1. {lazyopencode-0.2.0 → lazyopencode-0.2.1}/AGENTS.md +1 -1
  2. {lazyopencode-0.2.0 → lazyopencode-0.2.1}/PKG-INFO +22 -1
  3. {lazyopencode-0.2.0 → lazyopencode-0.2.1}/README.md +21 -0
  4. lazyopencode-0.2.1/artifacts/demo.png +0 -0
  5. {lazyopencode-0.2.0 → lazyopencode-0.2.1}/src/lazyopencode/_version.py +2 -2
  6. {lazyopencode-0.2.0 → lazyopencode-0.2.1}/src/lazyopencode/app.py +64 -1
  7. {lazyopencode-0.2.0 → lazyopencode-0.2.1}/src/lazyopencode/bindings.py +1 -0
  8. {lazyopencode-0.2.0 → lazyopencode-0.2.1}/src/lazyopencode/models/customization.py +11 -0
  9. {lazyopencode-0.2.0 → lazyopencode-0.2.1}/src/lazyopencode/services/writer.py +28 -0
  10. {lazyopencode-0.2.0 → lazyopencode-0.2.1}/src/lazyopencode/widgets/app_footer.py +8 -1
  11. lazyopencode-0.2.1/src/lazyopencode/widgets/delete_confirm.py +115 -0
  12. lazyopencode-0.2.0/artifacts/demo.png +0 -0
  13. {lazyopencode-0.2.0 → lazyopencode-0.2.1}/.github/release-drafter.yml +0 -0
  14. {lazyopencode-0.2.0 → lazyopencode-0.2.1}/.github/workflows/ci.yml +0 -0
  15. {lazyopencode-0.2.0 → lazyopencode-0.2.1}/.github/workflows/publish.yml +0 -0
  16. {lazyopencode-0.2.0 → lazyopencode-0.2.1}/.github/workflows/release-drafter.yml +0 -0
  17. {lazyopencode-0.2.0 → lazyopencode-0.2.1}/.gitignore +0 -0
  18. {lazyopencode-0.2.0 → lazyopencode-0.2.1}/.opencode/command/run-quality-gates.md +0 -0
  19. {lazyopencode-0.2.0 → lazyopencode-0.2.1}/.opencode/skill/quality-gates/SKILL.md +0 -0
  20. {lazyopencode-0.2.0 → lazyopencode-0.2.1}/.pre-commit-config.yaml +0 -0
  21. {lazyopencode-0.2.0 → lazyopencode-0.2.1}/LICENSE +0 -0
  22. {lazyopencode-0.2.0 → lazyopencode-0.2.1}/artifacts/lazyclaude-reference.png +0 -0
  23. {lazyopencode-0.2.0 → lazyopencode-0.2.1}/opencode.json +0 -0
  24. {lazyopencode-0.2.0 → lazyopencode-0.2.1}/pyproject.toml +0 -0
  25. {lazyopencode-0.2.0 → lazyopencode-0.2.1}/scripts/check_quality.sh +0 -0
  26. {lazyopencode-0.2.0 → lazyopencode-0.2.1}/src/lazyopencode/__init__.py +0 -0
  27. {lazyopencode-0.2.0 → lazyopencode-0.2.1}/src/lazyopencode/__main__.py +0 -0
  28. {lazyopencode-0.2.0 → lazyopencode-0.2.1}/src/lazyopencode/mixins/filtering.py +0 -0
  29. {lazyopencode-0.2.0 → lazyopencode-0.2.1}/src/lazyopencode/mixins/help.py +0 -0
  30. {lazyopencode-0.2.0 → lazyopencode-0.2.1}/src/lazyopencode/mixins/navigation.py +0 -0
  31. {lazyopencode-0.2.0 → lazyopencode-0.2.1}/src/lazyopencode/models/__init__.py +0 -0
  32. {lazyopencode-0.2.0 → lazyopencode-0.2.1}/src/lazyopencode/services/__init__.py +0 -0
  33. {lazyopencode-0.2.0 → lazyopencode-0.2.1}/src/lazyopencode/services/claude_code/__init__.py +0 -0
  34. {lazyopencode-0.2.0 → lazyopencode-0.2.1}/src/lazyopencode/services/claude_code/discovery.py +0 -0
  35. {lazyopencode-0.2.0 → lazyopencode-0.2.1}/src/lazyopencode/services/claude_code/parsers/__init__.py +0 -0
  36. {lazyopencode-0.2.0 → lazyopencode-0.2.1}/src/lazyopencode/services/claude_code/parsers/agent.py +0 -0
  37. {lazyopencode-0.2.0 → lazyopencode-0.2.1}/src/lazyopencode/services/claude_code/parsers/command.py +0 -0
  38. {lazyopencode-0.2.0 → lazyopencode-0.2.1}/src/lazyopencode/services/claude_code/parsers/skill.py +0 -0
  39. {lazyopencode-0.2.0 → lazyopencode-0.2.1}/src/lazyopencode/services/claude_code/plugin_loader.py +0 -0
  40. {lazyopencode-0.2.0 → lazyopencode-0.2.1}/src/lazyopencode/services/discovery.py +0 -0
  41. {lazyopencode-0.2.0 → lazyopencode-0.2.1}/src/lazyopencode/services/gitignore_filter.py +0 -0
  42. {lazyopencode-0.2.0 → lazyopencode-0.2.1}/src/lazyopencode/services/parsers/__init__.py +0 -0
  43. {lazyopencode-0.2.0 → lazyopencode-0.2.1}/src/lazyopencode/services/parsers/agent.py +0 -0
  44. {lazyopencode-0.2.0 → lazyopencode-0.2.1}/src/lazyopencode/services/parsers/command.py +0 -0
  45. {lazyopencode-0.2.0 → lazyopencode-0.2.1}/src/lazyopencode/services/parsers/mcp.py +0 -0
  46. {lazyopencode-0.2.0 → lazyopencode-0.2.1}/src/lazyopencode/services/parsers/plugin.py +0 -0
  47. {lazyopencode-0.2.0 → lazyopencode-0.2.1}/src/lazyopencode/services/parsers/rules.py +0 -0
  48. {lazyopencode-0.2.0 → lazyopencode-0.2.1}/src/lazyopencode/services/parsers/skill.py +0 -0
  49. {lazyopencode-0.2.0 → lazyopencode-0.2.1}/src/lazyopencode/services/parsers/tool.py +0 -0
  50. {lazyopencode-0.2.0 → lazyopencode-0.2.1}/src/lazyopencode/styles/app.tcss +0 -0
  51. {lazyopencode-0.2.0 → lazyopencode-0.2.1}/src/lazyopencode/themes.py +0 -0
  52. {lazyopencode-0.2.0 → lazyopencode-0.2.1}/src/lazyopencode/widgets/__init__.py +0 -0
  53. {lazyopencode-0.2.0 → lazyopencode-0.2.1}/src/lazyopencode/widgets/combined_panel.py +0 -0
  54. {lazyopencode-0.2.0 → lazyopencode-0.2.1}/src/lazyopencode/widgets/detail_pane.py +0 -0
  55. {lazyopencode-0.2.0 → lazyopencode-0.2.1}/src/lazyopencode/widgets/filter_input.py +0 -0
  56. {lazyopencode-0.2.0 → lazyopencode-0.2.1}/src/lazyopencode/widgets/helpers/__init__.py +0 -0
  57. {lazyopencode-0.2.0 → lazyopencode-0.2.1}/src/lazyopencode/widgets/helpers/rendering.py +0 -0
  58. {lazyopencode-0.2.0 → lazyopencode-0.2.1}/src/lazyopencode/widgets/level_selector.py +0 -0
  59. {lazyopencode-0.2.0 → lazyopencode-0.2.1}/src/lazyopencode/widgets/status_panel.py +0 -0
  60. {lazyopencode-0.2.0 → lazyopencode-0.2.1}/src/lazyopencode/widgets/type_panel.py +0 -0
  61. {lazyopencode-0.2.0 → lazyopencode-0.2.1}/tests/AGENTS.md +0 -0
  62. {lazyopencode-0.2.0 → lazyopencode-0.2.1}/tests/conftest.py +0 -0
  63. {lazyopencode-0.2.0 → lazyopencode-0.2.1}/tests/integration/__init__.py +0 -0
  64. {lazyopencode-0.2.0 → lazyopencode-0.2.1}/tests/integration/discovery/__init__.py +0 -0
  65. {lazyopencode-0.2.0 → lazyopencode-0.2.1}/tests/integration/discovery/test_agents.py +0 -0
  66. {lazyopencode-0.2.0 → lazyopencode-0.2.1}/tests/integration/discovery/test_commands.py +0 -0
  67. {lazyopencode-0.2.0 → lazyopencode-0.2.1}/tests/integration/discovery/test_full_discovery.py +0 -0
  68. {lazyopencode-0.2.0 → lazyopencode-0.2.1}/tests/integration/discovery/test_mcps.py +0 -0
  69. {lazyopencode-0.2.0 → lazyopencode-0.2.1}/tests/integration/discovery/test_rules.py +0 -0
  70. {lazyopencode-0.2.0 → lazyopencode-0.2.1}/tests/integration/discovery/test_skills.py +0 -0
  71. {lazyopencode-0.2.0 → lazyopencode-0.2.1}/tests/integration/fixtures/agent/explorer.md +0 -0
  72. {lazyopencode-0.2.0 → lazyopencode-0.2.1}/tests/integration/fixtures/command/greet.md +0 -0
  73. {lazyopencode-0.2.0 → lazyopencode-0.2.1}/tests/integration/fixtures/mcp/project-opencode.json +0 -0
  74. {lazyopencode-0.2.0 → lazyopencode-0.2.1}/tests/integration/fixtures/mcp/user-opencode.json +0 -0
  75. {lazyopencode-0.2.0 → lazyopencode-0.2.1}/tests/integration/fixtures/memory/AGENTS.md +0 -0
  76. {lazyopencode-0.2.0 → lazyopencode-0.2.1}/tests/integration/fixtures/project/AGENTS.md +0 -0
  77. {lazyopencode-0.2.0 → lazyopencode-0.2.1}/tests/integration/fixtures/project/agent/reviewer.md +0 -0
  78. {lazyopencode-0.2.0 → lazyopencode-0.2.1}/tests/integration/fixtures/project/command/project-cmd.md +0 -0
  79. {lazyopencode-0.2.0 → lazyopencode-0.2.1}/tests/integration/fixtures/project/docs/guidelines.md +0 -0
  80. {lazyopencode-0.2.0 → lazyopencode-0.2.1}/tests/integration/fixtures/project/skill/project-skill/SKILL.md +0 -0
  81. {lazyopencode-0.2.0 → lazyopencode-0.2.1}/tests/integration/fixtures/project/skill/project-skill/src/helper.py +0 -0
  82. {lazyopencode-0.2.0 → lazyopencode-0.2.1}/tests/integration/fixtures/skill/task-tracker/SKILL.md +0 -0
  83. {lazyopencode-0.2.0 → lazyopencode-0.2.1}/tests/integration/fixtures/skill/task-tracker/reference.md +0 -0
  84. {lazyopencode-0.2.0 → lazyopencode-0.2.1}/tests/integration/fixtures/skill/task-tracker/scripts/run.sh +0 -0
  85. {lazyopencode-0.2.0 → lazyopencode-0.2.1}/tests/spec/SPEC.md +0 -0
  86. {lazyopencode-0.2.0 → lazyopencode-0.2.1}/tests/spec/__init__.py +0 -0
  87. {lazyopencode-0.2.0 → lazyopencode-0.2.1}/tests/spec/conftest.py +0 -0
  88. {lazyopencode-0.2.0 → lazyopencode-0.2.1}/tests/spec/scenarios/complex/.opencode/agent/reviewer.md +0 -0
  89. {lazyopencode-0.2.0 → lazyopencode-0.2.1}/tests/spec/scenarios/complex/.opencode/command/verify.md +0 -0
  90. {lazyopencode-0.2.0 → lazyopencode-0.2.1}/tests/spec/scenarios/complex/.opencode/plugin/metrics.ts +0 -0
  91. {lazyopencode-0.2.0 → lazyopencode-0.2.1}/tests/spec/scenarios/complex/.opencode/skill/deploy-helper/SKILL.md +0 -0
  92. {lazyopencode-0.2.0 → lazyopencode-0.2.1}/tests/spec/scenarios/complex/.opencode/skill/deploy-helper/scripts/deploy.sh +0 -0
  93. {lazyopencode-0.2.0 → lazyopencode-0.2.1}/tests/spec/scenarios/complex/.opencode/tool/search.ts +0 -0
  94. {lazyopencode-0.2.0 → lazyopencode-0.2.1}/tests/spec/scenarios/complex/AGENTS.md +0 -0
  95. {lazyopencode-0.2.0 → lazyopencode-0.2.1}/tests/spec/scenarios/complex/README.md +0 -0
  96. {lazyopencode-0.2.0 → lazyopencode-0.2.1}/tests/spec/scenarios/complex/docs/guidelines.md +0 -0
  97. {lazyopencode-0.2.0 → lazyopencode-0.2.1}/tests/spec/scenarios/complex/opencode.json +0 -0
  98. {lazyopencode-0.2.0 → lazyopencode-0.2.1}/tests/spec/scenarios/complex/prompts/auditor.txt +0 -0
  99. {lazyopencode-0.2.0 → lazyopencode-0.2.1}/tests/spec/scenarios/file_commands/.opencode/command/deploy.md +0 -0
  100. {lazyopencode-0.2.0 → lazyopencode-0.2.1}/tests/spec/scenarios/file_commands/.opencode/command/greet.md +0 -0
  101. {lazyopencode-0.2.0 → lazyopencode-0.2.1}/tests/spec/scenarios/file_commands/README.md +0 -0
  102. {lazyopencode-0.2.0 → lazyopencode-0.2.1}/tests/spec/scenarios/file_references/README.md +0 -0
  103. {lazyopencode-0.2.0 → lazyopencode-0.2.1}/tests/spec/scenarios/file_references/opencode.json +0 -0
  104. {lazyopencode-0.2.0 → lazyopencode-0.2.1}/tests/spec/scenarios/file_references/prompts/agent.txt +0 -0
  105. {lazyopencode-0.2.0 → lazyopencode-0.2.1}/tests/spec/scenarios/file_references/templates/cmd.txt +0 -0
  106. {lazyopencode-0.2.0 → lazyopencode-0.2.1}/tests/spec/scenarios/inline_commands/README.md +0 -0
  107. {lazyopencode-0.2.0 → lazyopencode-0.2.1}/tests/spec/scenarios/inline_commands/opencode.json +0 -0
  108. {lazyopencode-0.2.0 → lazyopencode-0.2.1}/tests/spec/scenarios/minimal/AGENTS.md +0 -0
  109. {lazyopencode-0.2.0 → lazyopencode-0.2.1}/tests/spec/scenarios/minimal/README.md +0 -0
  110. {lazyopencode-0.2.0 → lazyopencode-0.2.1}/tests/spec/scenarios/mixed_config/.opencode/agent/file-agent.md +0 -0
  111. {lazyopencode-0.2.0 → lazyopencode-0.2.1}/tests/spec/scenarios/mixed_config/.opencode/command/file-cmd.md +0 -0
  112. {lazyopencode-0.2.0 → lazyopencode-0.2.1}/tests/spec/scenarios/mixed_config/AGENTS.md +0 -0
  113. {lazyopencode-0.2.0 → lazyopencode-0.2.1}/tests/spec/scenarios/mixed_config/README.md +0 -0
  114. {lazyopencode-0.2.0 → lazyopencode-0.2.1}/tests/spec/scenarios/mixed_config/opencode.json +0 -0
  115. {lazyopencode-0.2.0 → lazyopencode-0.2.1}/tests/spec/scenarios/rich_opencode_json/README.md +0 -0
  116. {lazyopencode-0.2.0 → lazyopencode-0.2.1}/tests/spec/scenarios/rich_opencode_json/docs/api-standards.md +0 -0
  117. {lazyopencode-0.2.0 → lazyopencode-0.2.1}/tests/spec/scenarios/rich_opencode_json/opencode.json +0 -0
  118. {lazyopencode-0.2.0 → lazyopencode-0.2.1}/tests/spec/scenarios/rich_opencode_json/prompts/security-audit.txt +0 -0
  119. {lazyopencode-0.2.0 → lazyopencode-0.2.1}/tests/spec/scenarios/rich_opencode_json/scripts/fake-lint.sh +0 -0
  120. {lazyopencode-0.2.0 → lazyopencode-0.2.1}/tests/spec/scenarios/skills_with_tree/.opencode/skill/my-skill/SKILL.md +0 -0
  121. {lazyopencode-0.2.0 → lazyopencode-0.2.1}/tests/spec/scenarios/skills_with_tree/.opencode/skill/my-skill/docs/guide.md +0 -0
  122. {lazyopencode-0.2.0 → lazyopencode-0.2.1}/tests/spec/scenarios/skills_with_tree/.opencode/skill/my-skill/scripts/run.sh +0 -0
  123. {lazyopencode-0.2.0 → lazyopencode-0.2.1}/tests/spec/scenarios/skills_with_tree/README.md +0 -0
  124. {lazyopencode-0.2.0 → lazyopencode-0.2.1}/tests/spec/test_scenarios.py +0 -0
  125. {lazyopencode-0.2.0 → lazyopencode-0.2.1}/tests/test_inline.py +0 -0
  126. {lazyopencode-0.2.0 → lazyopencode-0.2.1}/tests/test_version.py +0 -0
  127. {lazyopencode-0.2.0 → lazyopencode-0.2.1}/tests/unit/__init__.py +0 -0
  128. {lazyopencode-0.2.0 → lazyopencode-0.2.1}/uv.lock +0 -0
@@ -5,7 +5,7 @@ A keyboard-driven TUI for visualizing and managing OpenCode customizations.
5
5
  ## Environment Rules
6
6
  - **OS**: Windows (Git Bash). Use forward slashes `/` and `/c/` prefix for absolute paths.
7
7
  - **Search**: `rg` and `fd` are installed. Use them for fast searching.
8
- - **Quality Gates**: Always run quality gates before asking the user to commit changes. Use the `quality-gates` skill by calling the Skill tool. Do NOT run `uv run ruff`, `uv run mypy`, or other individual linting tools directly - always use the skill instead.
8
+ - **Quality Gates**: Always run quality gates before asking the user to commit changes. Run `bash scripts/check_quality.sh` or individual checks: `uv run ruff check src/ && uv run ruff format --check src/ && uv run mypy src/ && uv run pytest tests/ -q`
9
9
  - **TUI Verification**: Do NOT run `uv run lazyopencode` to verify the application. It is a TUI and output cannot be captured effectively. Use unit tests or static analysis instead.
10
10
 
11
11
  ## Project Overview
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lazyopencode
3
- Version: 0.2.0
3
+ Version: 0.2.1
4
4
  Summary: A lazygit-style TUI for visualizing OpenCode customizations
5
5
  Project-URL: Homepage, https://github.com/nikiforovall/lazyopencode
6
6
  Project-URL: Repository, https://github.com/nikiforovall/lazyopencode
@@ -44,6 +44,7 @@ A keyboard-driven TUI for managing OpenCode customizations.
44
44
  - View commands, agents, skills, rules, MCPs, and plugins
45
45
  - Filter by configuration level (global/project)
46
46
  - Search within customizations
47
+ - Claude Code compatibility mode (`--claude-code`)
47
48
 
48
49
  ## Installation
49
50
 
@@ -71,6 +72,8 @@ pip install lazyopencode
71
72
  | `p` | Project filter |
72
73
  | `/` | Search |
73
74
  | `e` | Edit selected |
75
+ | `c` | Copy to level |
76
+ | `C` | Copy path |
74
77
  | `r` | Refresh |
75
78
  | `ctrl+u` | User Config |
76
79
  | `?` | Help |
@@ -90,6 +93,24 @@ LazyOpenCode discovers customizations from:
90
93
  | Tools | `~/.config/opencode/tool/` | `.opencode/tool/` |
91
94
  | Plugins | `~/.config/opencode/plugin/` | `.opencode/plugin/` |
92
95
 
96
+ ## Claude Code Mode
97
+
98
+ Enable Claude Code compatibility to also discover customizations from `~/.claude/`:
99
+
100
+ ```bash
101
+ lazyopencode --claude-code
102
+ ```
103
+
104
+ This discovers commands, agents, and skills from:
105
+
106
+ | Scope | Path |
107
+ | ------- | ----------------------------------------- |
108
+ | User | `~/.claude/commands/`, `~/.claude/agents/` |
109
+ | Project | `.claude/commands/`, `.claude/agents/` |
110
+ | Plugins | Installed plugins from registry |
111
+
112
+ Claude Code items are marked with 👾 and can be copied to OpenCode paths using `c`.
113
+
93
114
  ## Inspired By
94
115
 
95
116
  - [LazyClaude](https://github.com/NikiforovAll/lazyclaude) - Similar TUI for Claude Code
@@ -11,6 +11,7 @@ A keyboard-driven TUI for managing OpenCode customizations.
11
11
  - View commands, agents, skills, rules, MCPs, and plugins
12
12
  - Filter by configuration level (global/project)
13
13
  - Search within customizations
14
+ - Claude Code compatibility mode (`--claude-code`)
14
15
 
15
16
  ## Installation
16
17
 
@@ -38,6 +39,8 @@ pip install lazyopencode
38
39
  | `p` | Project filter |
39
40
  | `/` | Search |
40
41
  | `e` | Edit selected |
42
+ | `c` | Copy to level |
43
+ | `C` | Copy path |
41
44
  | `r` | Refresh |
42
45
  | `ctrl+u` | User Config |
43
46
  | `?` | Help |
@@ -57,6 +60,24 @@ LazyOpenCode discovers customizations from:
57
60
  | Tools | `~/.config/opencode/tool/` | `.opencode/tool/` |
58
61
  | Plugins | `~/.config/opencode/plugin/` | `.opencode/plugin/` |
59
62
 
63
+ ## Claude Code Mode
64
+
65
+ Enable Claude Code compatibility to also discover customizations from `~/.claude/`:
66
+
67
+ ```bash
68
+ lazyopencode --claude-code
69
+ ```
70
+
71
+ This discovers commands, agents, and skills from:
72
+
73
+ | Scope | Path |
74
+ | ------- | ----------------------------------------- |
75
+ | User | `~/.claude/commands/`, `~/.claude/agents/` |
76
+ | Project | `.claude/commands/`, `.claude/agents/` |
77
+ | Plugins | Installed plugins from registry |
78
+
79
+ Claude Code items are marked with 👾 and can be copied to OpenCode paths using `c`.
80
+
60
81
  ## Inspired By
61
82
 
62
83
  - [LazyClaude](https://github.com/NikiforovAll/lazyclaude) - Similar TUI for Claude Code
Binary file
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
28
28
  commit_id: COMMIT_ID
29
29
  __commit_id__: COMMIT_ID
30
30
 
31
- __version__ = version = '0.2.0'
32
- __version_tuple__ = version_tuple = (0, 2, 0)
31
+ __version__ = version = '0.2.1'
32
+ __version_tuple__ = version_tuple = (0, 2, 1)
33
33
 
34
34
  __commit_id__ = commit_id = None
@@ -24,6 +24,7 @@ from lazyopencode.services.discovery import ConfigDiscoveryService
24
24
  from lazyopencode.themes import CUSTOM_THEMES
25
25
  from lazyopencode.widgets.app_footer import AppFooter
26
26
  from lazyopencode.widgets.combined_panel import CombinedPanel
27
+ from lazyopencode.widgets.delete_confirm import DeleteConfirm
27
28
  from lazyopencode.widgets.detail_pane import MainPane
28
29
  from lazyopencode.widgets.filter_input import FilterInput
29
30
  from lazyopencode.widgets.level_selector import LevelSelector
@@ -65,6 +66,7 @@ class LazyOpenCode(App, NavigationMixin, FilteringMixin, HelpMixin):
65
66
  self._filter_input: FilterInput | None = None
66
67
  self._app_footer: AppFooter | None = None
67
68
  self._level_selector: LevelSelector | None = None
69
+ self._delete_confirm: DeleteConfirm | None = None
68
70
  self._last_focused_panel: TypePanel | CombinedPanel | None = None
69
71
  self._pending_customization: Customization | None = None
70
72
 
@@ -114,6 +116,9 @@ class LazyOpenCode(App, NavigationMixin, FilteringMixin, HelpMixin):
114
116
  self._level_selector = LevelSelector(id="level-selector")
115
117
  yield self._level_selector
116
118
 
119
+ self._delete_confirm = DeleteConfirm(id="delete-confirm")
120
+ yield self._delete_confirm
121
+
117
122
  self._app_footer = AppFooter(id="app-footer")
118
123
  yield self._app_footer
119
124
 
@@ -181,6 +186,7 @@ class LazyOpenCode(App, NavigationMixin, FilteringMixin, HelpMixin):
181
186
  """Handle selection change in a type panel."""
182
187
  if self._main_pane:
183
188
  self._main_pane.customization = message.customization
189
+ self._update_footer_can_delete(message.customization)
184
190
 
185
191
  def on_type_panel_drill_down(self, message: TypePanel.DrillDown) -> None:
186
192
  """Handle drill down into a customization."""
@@ -202,6 +208,7 @@ class LazyOpenCode(App, NavigationMixin, FilteringMixin, HelpMixin):
202
208
  """Handle selection change in the combined panel."""
203
209
  if self._main_pane:
204
210
  self._main_pane.customization = message.customization
211
+ self._update_footer_can_delete(message.customization)
205
212
 
206
213
  def on_combined_panel_drill_down(self, message: CombinedPanel.DrillDown) -> None:
207
214
  """Handle drill down from the combined panel."""
@@ -267,7 +274,6 @@ class LazyOpenCode(App, NavigationMixin, FilteringMixin, HelpMixin):
267
274
  """Refresh customizations from disk."""
268
275
  self._discovery_service.refresh()
269
276
  self._load_customizations()
270
- self.notify("Refreshed", severity="information")
271
277
 
272
278
  # action_toggle_help handled by HelpMixin
273
279
 
@@ -421,6 +427,63 @@ class LazyOpenCode(App, NavigationMixin, FilteringMixin, HelpMixin):
421
427
  elif self._panels:
422
428
  self._panels[0].focus()
423
429
 
430
+ def _update_footer_can_delete(self, customization: Customization | None) -> None:
431
+ """Update footer delete indicator based on current selection."""
432
+ if self._app_footer:
433
+ self._app_footer.can_delete = (
434
+ customization is not None and customization.is_deletable()
435
+ )
436
+
437
+ # Delete actions
438
+
439
+ def action_delete_customization(self) -> None:
440
+ """Delete selected customization."""
441
+ panel = self._get_focused_panel()
442
+ customization = panel.selected_customization if panel else None
443
+
444
+ if not customization:
445
+ self.notify("No customization selected", severity="warning")
446
+ return
447
+
448
+ if not customization.is_deletable():
449
+ self.notify(
450
+ f"Cannot delete {customization.type_label} customizations",
451
+ severity="warning",
452
+ )
453
+ return
454
+
455
+ self._last_focused_panel = panel
456
+ if self._delete_confirm:
457
+ self._delete_confirm.show(customization)
458
+
459
+ def on_delete_confirm_delete_confirmed(
460
+ self, message: DeleteConfirm.DeleteConfirmed
461
+ ) -> None:
462
+ """Handle delete confirmation."""
463
+ from lazyopencode.services.writer import CustomizationWriter
464
+
465
+ writer = CustomizationWriter(
466
+ global_config_path=self._discovery_service.global_config_path,
467
+ project_config_path=self._discovery_service.project_config_path,
468
+ )
469
+
470
+ success, msg = writer.delete_customization(message.customization)
471
+
472
+ if success:
473
+ self.notify(msg, severity="information")
474
+ self.action_refresh()
475
+ else:
476
+ self.notify(msg, severity="error")
477
+
478
+ self._restore_focus_after_selector()
479
+
480
+ def on_delete_confirm_delete_cancelled(
481
+ self,
482
+ message: DeleteConfirm.DeleteCancelled, # noqa: ARG002
483
+ ) -> None:
484
+ """Handle delete cancellation."""
485
+ self._restore_focus_after_selector()
486
+
424
487
 
425
488
  def create_app(
426
489
  project_root: Path | None = None,
@@ -8,6 +8,7 @@ APP_BINDINGS: list[BindingType] = [
8
8
  Binding("r", "refresh", "Refresh"),
9
9
  Binding("e", "open_in_editor", "Edit"),
10
10
  Binding("c", "copy_customization", "Copy"),
11
+ Binding("d", "delete_customization", "Delete"),
11
12
  Binding("C", "copy_path_to_clipboard", "Copy Path", key_display="shift+c"),
12
13
  Binding("ctrl+u", "open_user_config", "User Config"),
13
14
  Binding("tab", "focus_next_panel", "Next Panel", show=False),
@@ -149,3 +149,14 @@ class Customization:
149
149
  return [ConfigLevel.PROJECT]
150
150
  else:
151
151
  return [ConfigLevel.GLOBAL]
152
+
153
+ def is_deletable(self) -> bool:
154
+ """Check if this customization can be deleted."""
155
+ if self.source != ConfigSource.OPENCODE:
156
+ return False
157
+ deletable_types = (
158
+ CustomizationType.COMMAND,
159
+ CustomizationType.AGENT,
160
+ CustomizationType.SKILL,
161
+ )
162
+ return self.type in deletable_types
@@ -117,3 +117,31 @@ class CustomizationWriter:
117
117
  target_dir,
118
118
  dirs_exist_ok=False,
119
119
  )
120
+
121
+ def delete_customization(
122
+ self,
123
+ customization: Customization,
124
+ ) -> tuple[bool, str]:
125
+ """
126
+ Delete customization from disk.
127
+
128
+ Args:
129
+ customization: The customization to delete
130
+
131
+ Returns:
132
+ Tuple of (success: bool, message: str)
133
+ """
134
+ try:
135
+ if customization.type == CustomizationType.SKILL:
136
+ shutil.rmtree(customization.path.parent)
137
+ else:
138
+ customization.path.unlink()
139
+
140
+ return (True, f"Deleted '{customization.name}'")
141
+
142
+ except PermissionError as e:
143
+ return (False, f"Permission denied deleting {e.filename}")
144
+ except FileNotFoundError:
145
+ return (False, f"File not found: {customization.path}")
146
+ except OSError as e:
147
+ return (False, f"Failed to delete '{customization.name}': {e}")
@@ -26,6 +26,7 @@ class AppFooter(Widget):
26
26
 
27
27
  filter_level: reactive[str] = reactive("All")
28
28
  search_active: reactive[bool] = reactive(False)
29
+ can_delete: reactive[bool] = reactive(False)
29
30
 
30
31
  def compose(self) -> ComposeResult:
31
32
  """Compose the footer content."""
@@ -42,9 +43,11 @@ class AppFooter(Widget):
42
43
  )
43
44
  search_key = format_keybinding("/", "Search", active=self.search_active)
44
45
 
46
+ delete_part = " [bold]d[/] Delete" if self.can_delete else ""
47
+
45
48
  return (
46
49
  f"[bold]q[/] Quit [bold]?[/] Help [bold]r[/] Refresh "
47
- f"[bold]e[/] Edit [bold]c[/] Copy "
50
+ f"[bold]e[/] Edit [bold]c[/] Copy{delete_part} "
48
51
  f"{all_key} {user_key} {project_key} "
49
52
  f"{search_key} │ [bold][$accent]^p[/][/] Palette"
50
53
  )
@@ -69,3 +72,7 @@ class AppFooter(Widget):
69
72
  def watch_search_active(self, _active: bool) -> None:
70
73
  """React to search active changes."""
71
74
  self._update_content()
75
+
76
+ def watch_can_delete(self, _can: bool) -> None:
77
+ """React to can_delete changes."""
78
+ self._update_content()
@@ -0,0 +1,115 @@
1
+ """Confirmation widget for delete operations."""
2
+
3
+ from textual.app import ComposeResult
4
+ from textual.binding import Binding
5
+ from textual.message import Message
6
+ from textual.widget import Widget
7
+ from textual.widgets import Static
8
+
9
+ from lazyopencode.models.customization import Customization
10
+
11
+
12
+ class DeleteConfirm(Widget):
13
+ """Bottom bar for confirming delete operations."""
14
+
15
+ BINDINGS = [
16
+ Binding("y", "confirm", "Yes", show=False),
17
+ Binding("n", "deny", "No", show=False),
18
+ Binding("escape", "cancel", "Cancel", show=False),
19
+ ]
20
+
21
+ DEFAULT_CSS = """
22
+ DeleteConfirm {
23
+ dock: bottom;
24
+ height: 4;
25
+ border: solid $error;
26
+ padding: 0 1;
27
+ margin-bottom: 1;
28
+ display: none;
29
+ background: $surface;
30
+ }
31
+
32
+ DeleteConfirm.visible {
33
+ display: block;
34
+ }
35
+
36
+ DeleteConfirm:focus {
37
+ border: double $error;
38
+ }
39
+
40
+ DeleteConfirm #prompt {
41
+ width: 100%;
42
+ text-align: center;
43
+ }
44
+ """
45
+
46
+ can_focus = True
47
+
48
+ class DeleteConfirmed(Message):
49
+ """Emitted when delete is confirmed."""
50
+
51
+ def __init__(self, customization: Customization) -> None:
52
+ self.customization = customization
53
+ super().__init__()
54
+
55
+ class DeleteCancelled(Message):
56
+ """Emitted when delete is cancelled."""
57
+
58
+ pass
59
+
60
+ def __init__(
61
+ self,
62
+ name: str | None = None,
63
+ id: str | None = None,
64
+ classes: str | None = None,
65
+ ) -> None:
66
+ """Initialize DeleteConfirm."""
67
+ super().__init__(name=name, id=id, classes=classes)
68
+ self._customization: Customization | None = None
69
+
70
+ def compose(self) -> ComposeResult:
71
+ """Compose the confirmation bar."""
72
+ yield Static("", id="prompt")
73
+
74
+ def show(self, customization: Customization) -> None:
75
+ """Show the confirmation bar and focus it."""
76
+ self._customization = customization
77
+ self._update_prompt(customization)
78
+ self.add_class("visible")
79
+ self.focus()
80
+
81
+ def hide(self) -> None:
82
+ """Hide the confirmation bar."""
83
+ self.remove_class("visible")
84
+ self._customization = None
85
+
86
+ def _update_prompt(self, customization: Customization) -> None:
87
+ """Update the prompt text."""
88
+ prompt_widget = self.query_one("#prompt", Static)
89
+ error_color = self.app.get_css_variables().get("error", "red")
90
+ prompt_widget.update(
91
+ f'Delete [{error_color}]"{customization.name}"[/] ({customization.type_label})?\n'
92
+ "\\[y] Yes \\[n] No \\[Esc] Cancel"
93
+ )
94
+
95
+ def action_confirm(self) -> None:
96
+ """Confirm the delete."""
97
+ if self._customization:
98
+ customization = self._customization
99
+ self.hide()
100
+ self.post_message(self.DeleteConfirmed(customization))
101
+
102
+ def action_deny(self) -> None:
103
+ """Deny the delete."""
104
+ self.hide()
105
+ self.post_message(self.DeleteCancelled())
106
+
107
+ def action_cancel(self) -> None:
108
+ """Cancel the delete."""
109
+ self.hide()
110
+ self.post_message(self.DeleteCancelled())
111
+
112
+ @property
113
+ def is_visible(self) -> bool:
114
+ """Check if the confirmation bar is visible."""
115
+ return self.has_class("visible")
Binary file
File without changes
File without changes
File without changes
File without changes