Skip to content

Commit 42d68a2

Browse files
committed
✨ feat(marketplace): add plugin preview mode
Add ability to preview plugin customizations directly from marketplace: - Press 'p' or 'Enter' on a plugin to enter preview mode - Shows plugin's slash commands, subagents, skills, MCPs, and hooks - Separate footer with only relevant actions (Search, Exit Preview) - Search/filter works within plugin customizations - Press 'Esc' to return to marketplace browser - Defaults to MCPs panel [5] on preview (more relevant for plugins) Implementation: - Add discover_from_directory() to ConfigDiscoveryService - Add get_plugin_source_dir() to MarketplaceLoader - Add preview mode state management in App - Add PluginPreview message and bindings in MarketplaceModal
1 parent 2e6e9e8 commit 42d68a2

File tree

6 files changed

+548
-11
lines changed

6 files changed

+548
-11
lines changed

_plans/41-marketplace-modal.md

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
# Marketplace Browser Modal Implementation Plan
2+
3+
## Overview
4+
Add a marketplace browser modal to LazyClaude that displays available plugins from configured marketplaces in a tree structure, allowing users to browse and toggle/install plugins.
5+
6+
## Requirements
7+
- **Trigger**: `Ctrl+m` keyboard shortcut
8+
- **Size**: 80% of screen (large overlay modal)
9+
- **Tree Structure**: Marketplace → Plugins (flat) using Textual Tree widget
10+
- **Toggle Action ('i')**: Combined behavior - install if not installed, toggle enabled state if installed
11+
- **Uninstall Action ('d')**: Uninstall an installed plugin
12+
- **Close**: `Escape` to close modal
13+
14+
## Data Sources
15+
- Index: `~/.claude/plugins/known_marketplaces.json`
16+
- Per-marketplace: `<installLocation>/.claude-plugin/marketplace.json`
17+
18+
## Implementation Steps
19+
20+
### Step 1: Create Marketplace Models
21+
**File**: `src/lazyclaude/models/marketplace.py`
22+
23+
Create dataclasses:
24+
- `MarketplaceSource` - source type (github/directory), repo, path
25+
- `MarketplaceEntry` - name, source, install_location, last_updated
26+
- `MarketplacePlugin` - name, description, source, marketplace_name, full_plugin_id, is_installed, is_enabled
27+
- `Marketplace` - entry, plugins list, error string
28+
29+
### Step 2: Create MarketplaceLoader Service
30+
**File**: `src/lazyclaude/services/marketplace_loader.py`
31+
32+
Service that:
33+
- Reads `known_marketplaces.json` index file
34+
- Loads `marketplace.json` from each marketplace's install location
35+
- Checks installed state via PluginLoader registry
36+
- Checks enabled state via settings.json enabledPlugins dict
37+
38+
### Step 3: Create MarketplaceModal Widget
39+
**File**: `src/lazyclaude/widgets/marketplace_modal.py`
40+
41+
Widget using Textual Tree with:
42+
- Header: "Marketplace Browser" title with keybinding hints
43+
- Tree: Marketplaces as expandable roots, plugins as leaves
44+
- Footer: Keybinding reference
45+
- Bindings: `escape` (close), `i` (install/toggle), `d` (uninstall), `j/k` (navigation), `enter` (expand)
46+
- Messages: `PluginToggled(plugin)`, `PluginUninstall(plugin)`, `ModalClosed()`
47+
- Status icons in labels: `[green]I[/]` (installed+enabled), `[yellow]D[/]` (disabled), `[ ]` (not installed)
48+
49+
CSS positioning:
50+
```css
51+
MarketplaceModal {
52+
display: none;
53+
layer: overlay;
54+
width: 80%;
55+
height: 80%;
56+
border: double $accent;
57+
background: $surface;
58+
}
59+
MarketplaceModal.visible {
60+
display: block;
61+
align: center middle;
62+
}
63+
```
64+
65+
### Step 4: Update app.tcss
66+
**File**: `src/lazyclaude/styles/app.tcss`
67+
68+
Add MarketplaceModal styles (can also use DEFAULT_CSS in widget).
69+
70+
### Step 5: Integrate with App
71+
**File**: `src/lazyclaude/app.py`
72+
73+
1. Add import: `from lazyclaude.widgets.marketplace_modal import MarketplaceModal`
74+
2. Add binding: `Binding("ctrl+m", "open_marketplace", "Marketplace", show=True)`
75+
3. Add instance vars in `__init__`: `_marketplace_modal`, `_marketplace_loader`
76+
4. Yield modal in `compose()` after `_delete_confirm`
77+
5. Initialize loader in `on_mount()`
78+
6. Add action: `action_open_marketplace()` - calls `_marketplace_modal.show()`
79+
7. Add message handlers:
80+
- `on_marketplace_modal_plugin_toggled()` - handle toggle/install
81+
- `on_marketplace_modal_modal_closed()` - restore focus
82+
83+
### Step 6: Update Exports
84+
- `src/lazyclaude/widgets/__init__.py` - add MarketplaceModal
85+
- `src/lazyclaude/models/__init__.py` - add marketplace models
86+
87+
## Critical Files to Modify/Create
88+
| File | Action |
89+
|------|--------|
90+
| `src/lazyclaude/models/marketplace.py` | Create |
91+
| `src/lazyclaude/services/marketplace_loader.py` | Create |
92+
| `src/lazyclaude/widgets/marketplace_modal.py` | Create |
93+
| `src/lazyclaude/app.py` | Modify |
94+
| `src/lazyclaude/widgets/__init__.py` | Modify |
95+
| `src/lazyclaude/models/__init__.py` | Modify |
96+
97+
## Action Behavior Logic
98+
99+
Uses Claude CLI commands:
100+
- `claude plugin install <plugin>` - Install a plugin from marketplace
101+
- `claude plugin enable <plugin>` - Enable a disabled plugin
102+
- `claude plugin disable <plugin>` - Disable an enabled plugin
103+
- `claude plugin uninstall <plugin>` - Uninstall an installed plugin
104+
105+
```python
106+
# 'i' key - Install/Toggle
107+
def on_marketplace_modal_plugin_toggled(self, message):
108+
plugin = message.plugin
109+
110+
if not plugin.is_installed:
111+
cmd = ["claude", "plugin", "install", plugin.full_plugin_id]
112+
subprocess.run(cmd, check=True)
113+
self.notify(f"Installed {plugin.name}")
114+
elif plugin.is_enabled:
115+
cmd = ["claude", "plugin", "disable", plugin.full_plugin_id]
116+
subprocess.run(cmd, check=True)
117+
self.notify(f"Disabled {plugin.name}")
118+
else:
119+
cmd = ["claude", "plugin", "enable", plugin.full_plugin_id]
120+
subprocess.run(cmd, check=True)
121+
self.notify(f"Enabled {plugin.name}")
122+
123+
self._marketplace_modal.refresh_tree()
124+
self.action_refresh()
125+
126+
# 'd' key - Uninstall
127+
def on_marketplace_modal_plugin_uninstall(self, message):
128+
plugin = message.plugin
129+
if not plugin.is_installed:
130+
self.notify("Plugin not installed", severity="warning")
131+
return
132+
133+
cmd = ["claude", "plugin", "uninstall", plugin.full_plugin_id]
134+
subprocess.run(cmd, check=True)
135+
self.notify(f"Uninstalled {plugin.name}")
136+
137+
self._marketplace_modal.refresh_tree()
138+
self.action_refresh()
139+
```
140+
141+
## Future Enhancements (Out of Scope)
142+
- Search/filter within marketplace
143+
- Plugin details drill-down view
144+
- Version comparison for updates
145+
146+
# This file is a copy of original plan ~/.claude/plans/jaunty-waddling-mango.md
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
# Marketplace Plugin Preview - Implementation Plan
2+
3+
## Goal
4+
When viewing a marketplace plugin, replicate the existing [1]-[6] panel layout to show:
5+
- **Same panels**: Slash Commands, Subagents, Skills, Memory, MCPs, Hooks
6+
- **Plugin's content**: Show what customizations the plugin provides
7+
- **MainPane**: Show selected item content (same as normal mode)
8+
- Works for **both installed and uninstalled** plugins
9+
10+
This gives users a full preview of plugin contents before installing.
11+
12+
## UX Flow
13+
1. Press `M` to open marketplace (current modal or new view)
14+
2. Navigate to a plugin, press `Enter` or `p` to preview
15+
3. Sidebar shows plugin's [1]-[6] panels with its customizations
16+
4. Browse like normal mode, MainPane shows content
17+
5. Press `Esc` to return to marketplace browser
18+
19+
## Complexity Assessment: **Medium-High**
20+
21+
Key challenge: Loading customizations from plugin source directory (not installed path).
22+
23+
## Implementation Steps
24+
25+
### Step 1: Add plugin discovery to ConfigDiscoveryService
26+
**File:** `src/lazyclaude/services/discovery.py`
27+
28+
Add method to discover customizations from a specific plugin directory (reuses existing `_discover_plugins` pattern):
29+
```python
30+
def discover_from_directory(
31+
self, plugin_dir: Path, plugin_info: PluginInfo | None = None
32+
) -> list[Customization]:
33+
"""Discover customizations from a specific directory (for plugin preview)."""
34+
customizations: list[Customization] = []
35+
level = ConfigLevel.PLUGIN
36+
37+
# Scan for commands, agents, skills using existing SCAN_CONFIGS
38+
for config in SCAN_CONFIGS.values():
39+
customizations.extend(
40+
self._scanner.scan_directory(plugin_dir, config, level, plugin_info)
41+
)
42+
43+
# Scan for MCPs and hooks using existing methods
44+
customizations.extend(self._discover_plugin_mcps(plugin_dir, plugin_info))
45+
customizations.extend(self._discover_plugin_hooks(plugin_dir, plugin_info))
46+
47+
return self._sort_customizations(customizations)
48+
```
49+
50+
### Step 2: Add plugin path resolution to MarketplaceLoader
51+
**File:** `src/lazyclaude/services/marketplace_loader.py`
52+
53+
Add method to get plugin source directory:
54+
```python
55+
def get_plugin_source_dir(self, plugin: MarketplacePlugin) -> Path | None:
56+
"""Get the source directory for a plugin (installed or from marketplace)."""
57+
# For installed: use install_path
58+
if plugin.install_path and plugin.install_path.exists():
59+
return plugin.install_path
60+
61+
# For uninstalled: look in marketplace install_location/plugins/<name>/
62+
marketplace = self._find_marketplace(plugin.marketplace_name)
63+
if marketplace:
64+
return marketplace.entry.install_location / "plugins" / plugin.name
65+
return None
66+
```
67+
68+
### Step 3: Add plugin preview mode to App
69+
**File:** `src/lazyclaude/app.py`
70+
71+
Add state:
72+
```python
73+
self._plugin_preview_mode: bool = False
74+
self._previewing_plugin: MarketplacePlugin | None = None
75+
self._plugin_customizations: list[Customization] = []
76+
```
77+
78+
Add preview action:
79+
```python
80+
def _enter_plugin_preview(self, plugin: MarketplacePlugin) -> None:
81+
"""Enter plugin preview mode - show plugin's customizations in panels."""
82+
plugin_dir = self._marketplace_loader.get_plugin_source_dir(plugin)
83+
if not plugin_dir or not plugin_dir.exists():
84+
self.notify("Plugin source not found", severity="warning")
85+
return
86+
87+
# Discover plugin's customizations
88+
self._plugin_customizations = self._discovery.discover_plugin_customizations(plugin_dir)
89+
self._previewing_plugin = plugin
90+
self._plugin_preview_mode = True
91+
92+
# Hide marketplace modal/panel
93+
if self._marketplace_modal:
94+
self._marketplace_modal.hide()
95+
96+
# Update panels with plugin customizations
97+
self._update_panels() # Modified to use _plugin_customizations when in preview mode
98+
99+
self._update_subtitle() # Show "Preview: <plugin_name>"
100+
```
101+
102+
Modify `_update_panels()`:
103+
```python
104+
def _update_panels(self) -> None:
105+
if self._plugin_preview_mode:
106+
# Use plugin customizations instead of global
107+
customizations = self._plugin_customizations
108+
else:
109+
customizations = self._filter_service.filter(...)
110+
# ... rest of panel update logic
111+
```
112+
113+
Exit preview:
114+
```python
115+
def _exit_plugin_preview(self) -> None:
116+
self._plugin_preview_mode = False
117+
self._previewing_plugin = None
118+
self._plugin_customizations = []
119+
self._update_panels()
120+
self._update_subtitle()
121+
# Re-show marketplace
122+
if self._marketplace_modal:
123+
self._marketplace_modal.show()
124+
```
125+
126+
### Step 4: Add preview binding to MarketplaceModal
127+
**File:** `src/lazyclaude/widgets/marketplace_modal.py`
128+
129+
Add binding and message:
130+
```python
131+
Binding("enter", "preview_plugin", "Preview", show=False),
132+
# or
133+
Binding("p", "preview_plugin", "Preview", show=False),
134+
135+
class PluginPreview(Message):
136+
def __init__(self, plugin: MarketplacePlugin) -> None:
137+
self.plugin = plugin
138+
super().__init__()
139+
140+
def action_preview_plugin(self) -> None:
141+
node = self._tree.cursor_node
142+
if node and isinstance(node.data, MarketplacePlugin):
143+
self.post_message(self.PluginPreview(node.data))
144+
```
145+
146+
Update footer for plugins:
147+
```python
148+
"[bold]Enter[/] Preview [bold]i[/] Install ..."
149+
```
150+
151+
### Step 5: Handle Esc in preview mode
152+
**File:** `src/lazyclaude/app.py`
153+
154+
Modify escape handling:
155+
```python
156+
def action_escape(self) -> None:
157+
if self._plugin_preview_mode:
158+
self._exit_plugin_preview()
159+
elif self._marketplace_modal and self._marketplace_modal.is_visible:
160+
# ... existing modal close logic
161+
```
162+
163+
### Step 6: Update StatusPanel/subtitle
164+
Show current mode in status:
165+
- Normal: `~/.claude | All`
166+
- Preview: `Preview: handbook | Plugin`
167+
168+
## Files Summary
169+
170+
| File | Action | Changes |
171+
|------|--------|---------|
172+
| `src/lazyclaude/services/discovery.py` | Modify | Add `discover_from_directory()` method |
173+
| `src/lazyclaude/services/marketplace_loader.py` | Modify | Add `get_plugin_source_dir()` method |
174+
| `src/lazyclaude/app.py` | Modify | Add preview mode state, handlers, modify `_update_panels()` |
175+
| `src/lazyclaude/widgets/marketplace_modal.py` | Modify | Add `Enter` preview binding, `PluginPreview` message |
176+
177+
## Key Advantages
178+
179+
1. **Full reuse**: All existing panels, MainPane, navigation work as-is
180+
2. **Accurate preview**: Users see exactly what they'll get
181+
3. **No new widgets**: Just mode switching and customization source change
182+
4. **Works for uninstalled**: Load from marketplace source directory
183+
184+
---
185+
# This file is a copy of original plan ~/.claude/plans/abundant-baking-rose.md

0 commit comments

Comments
 (0)