Skip to content

Commit 1b9b788

Browse files
authored
Merge pull request #64 from NikiforovAll/feature/63-marketplace-ux
feat: Marketplace UX improvements and automated quality gates
2 parents 4069638 + df8b5fc commit 1b9b788

File tree

15 files changed

+853
-58
lines changed

15 files changed

+853
-58
lines changed

.claude/hooks/quality-gates.sh

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
#!/bin/bash
2+
# Hook: Run quality gates after Claude finishes responding
3+
# Triggered by: Stop hook event
4+
# Exit 2 + stderr = blocks stop, shows output to Claude
5+
# Filters checks based on changed file types
6+
7+
# Read input from stdin
8+
input=$(cat)
9+
10+
# Check if stop hook is already active (prevent infinite loops)
11+
if echo "$input" | grep -q '"stop_hook_active":true'; then
12+
exit 0
13+
fi
14+
15+
# Check for changed Python files (staged or unstaged)
16+
python_changed=$(git diff --name-only HEAD 2>/dev/null | grep -E '\.py$' || true)
17+
python_staged=$(git diff --cached --name-only 2>/dev/null | grep -E '\.py$' || true)
18+
19+
if [ -z "$python_changed" ] && [ -z "$python_staged" ]; then
20+
# No Python files changed, skip quality gates
21+
exit 0
22+
fi
23+
24+
# Run the quality gates script with passed arguments
25+
if [ -f ".claude/skills/quality-gates/scripts/check.sh" ]; then
26+
output=$(bash ".claude/skills/quality-gates/scripts/check.sh" "$@" 2>&1)
27+
exit_code=$?
28+
29+
if [ $exit_code -eq 0 ]; then
30+
# Silent on success
31+
exit 0
32+
else
33+
# Pass the full output back to Claude via stderr
34+
echo "Quality gates failed. Please fix the issues:" >&2
35+
echo "" >&2
36+
echo "$output" >&2
37+
exit 2
38+
fi
39+
fi
40+
41+
exit 0

.claude/settings.json

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,18 @@
1010
],
1111
"deny": [],
1212
"ask": []
13+
},
14+
"hooks": {
15+
"Stop": [
16+
{
17+
"hooks": [
18+
{
19+
"type": "command",
20+
"command": "bash .claude/hooks/quality-gates.sh --lint --format --mypy",
21+
"timeout": 120
22+
}
23+
]
24+
}
25+
]
1326
}
1427
}
Lines changed: 68 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,81 @@
11
#!/usr/bin/env bash
2-
# Quality Gates - Run all code quality checks for lazyclaude
2+
# Quality Gates - Run code quality checks for lazyclaude
3+
# Usage: check.sh [--lint] [--format] [--mypy] [--test] [--all]
4+
# --lint Run ruff lint
5+
# --format Run ruff format
6+
# --mypy Run mypy type check
7+
# --test Run pytest
8+
# --all Run all checks (default if no flags)
39
set -e
410

511
cd "$(git rev-parse --show-toplevel)"
612

13+
RUN_LINT=false
14+
RUN_FORMAT=false
15+
RUN_MYPY=false
16+
RUN_TEST=false
17+
18+
# Parse arguments
19+
if [[ $# -eq 0 ]]; then
20+
# No args = run all checks
21+
RUN_LINT=true
22+
RUN_FORMAT=true
23+
RUN_MYPY=true
24+
RUN_TEST=true
25+
fi
26+
27+
for arg in "$@"; do
28+
case $arg in
29+
--lint) RUN_LINT=true ;;
30+
--format) RUN_FORMAT=true ;;
31+
--mypy) RUN_MYPY=true ;;
32+
--test) RUN_TEST=true ;;
33+
--all)
34+
RUN_LINT=true
35+
RUN_FORMAT=true
36+
RUN_MYPY=true
37+
RUN_TEST=true
38+
;;
39+
esac
40+
done
41+
742
echo "=== Running Quality Gates ==="
843
echo ""
944

10-
echo "[1/4] Ruff Lint (with auto-fix)..."
11-
uv run ruff check src tests --fix
12-
echo "OK"
13-
echo ""
45+
step=1
1446

15-
echo "[2/4] Ruff Format..."
16-
uv run ruff format src tests
17-
echo "OK"
18-
echo ""
47+
if $RUN_LINT; then
48+
echo "[$step] Ruff Lint (with auto-fix)..."
49+
uv run ruff check src tests --fix
50+
echo "OK"
51+
echo ""
52+
((step++))
53+
fi
1954

20-
echo "[3/4] Mypy Type Check..."
21-
uv run mypy src
22-
echo "OK"
23-
echo ""
55+
if $RUN_FORMAT; then
56+
echo "[$step] Ruff Format..."
57+
uv run ruff format src tests
58+
echo "OK"
59+
echo ""
60+
((step++))
61+
fi
2462

25-
echo "[4/4] Pytest..."
26-
uv run pytest tests/ -q
27-
echo ""
63+
if $RUN_MYPY; then
64+
echo "[$step] Mypy Type Check..."
65+
uv run mypy src
66+
echo "OK"
67+
echo ""
68+
((step++))
69+
fi
70+
71+
if $RUN_TEST; then
72+
echo -n "[$step] Pytest... "
73+
uv run pytest tests/ -q --tb=no --no-header 2>&1 | tail -1
74+
pytest_status=${PIPESTATUS[0]}
75+
echo ""
76+
if [[ $pytest_status -ne 0 ]]; then
77+
exit "$pytest_status"
78+
fi
79+
fi
2880

2981
echo "=== All Quality Gates Passed ==="

src/lazyclaude/app.py

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
from textual.app import App, ComposeResult
1010
from textual.containers import Container
1111
from textual.theme import Theme
12-
from textual.widgets import Footer
1312

1413
from lazyclaude import __version__
1514
from lazyclaude.bindings import APP_BINDINGS
@@ -34,12 +33,15 @@
3433
from lazyclaude.services.marketplace_loader import MarketplaceLoader
3534
from lazyclaude.services.settings import SettingsService
3635
from lazyclaude.themes import CUSTOM_THEMES
36+
from lazyclaude.widgets.app_footer import AppFooter
3737
from lazyclaude.widgets.combined_panel import CombinedPanel
3838
from lazyclaude.widgets.delete_confirm import DeleteConfirm
3939
from lazyclaude.widgets.detail_pane import MainPane
4040
from lazyclaude.widgets.filter_input import FilterInput
4141
from lazyclaude.widgets.level_selector import LevelSelector
42+
from lazyclaude.widgets.marketplace_confirm import MarketplaceConfirm
4243
from lazyclaude.widgets.marketplace_modal import MarketplaceModal
44+
from lazyclaude.widgets.marketplace_source_input import MarketplaceSourceInput
4345
from lazyclaude.widgets.plugin_confirm import PluginConfirm
4446
from lazyclaude.widgets.status_panel import StatusPanel
4547
from lazyclaude.widgets.type_panel import TypePanel
@@ -100,7 +102,10 @@ def __init__(
100102
self._plugin_confirm: PluginConfirm | None = None
101103
self._delete_confirm: DeleteConfirm | None = None
102104
self._marketplace_modal: MarketplaceModal | None = None
105+
self._marketplace_confirm: MarketplaceConfirm | None = None
106+
self._marketplace_source_input: MarketplaceSourceInput | None = None
103107
self._marketplace_loader: MarketplaceLoader | None = None
108+
self._app_footer: AppFooter | None = None
104109
self._help_visible = False
105110
self._last_focused_panel: TypePanel | None = None
106111
self._last_focused_combined: bool = False
@@ -158,7 +163,16 @@ def compose(self) -> ComposeResult:
158163
self._marketplace_modal = MarketplaceModal(id="marketplace-modal")
159164
yield self._marketplace_modal
160165

161-
yield Footer()
166+
self._marketplace_confirm = MarketplaceConfirm(id="marketplace-confirm")
167+
yield self._marketplace_confirm
168+
169+
self._marketplace_source_input = MarketplaceSourceInput(
170+
id="marketplace-source-input"
171+
)
172+
yield self._marketplace_source_input
173+
174+
self._app_footer = AppFooter(id="app-footer")
175+
yield self._app_footer
162176

163177
def on_mount(self) -> None:
164178
"""Handle mount event - load customizations."""
@@ -199,6 +213,20 @@ def check_action(
199213
if action == "exit_preview":
200214
return self._plugin_preview_mode
201215

216+
marketplace_blocked_actions = {
217+
"filter_all",
218+
"filter_user",
219+
"filter_project",
220+
"filter_plugin",
221+
"toggle_plugin_enabled_filter",
222+
}
223+
if (
224+
self._marketplace_modal
225+
and self._marketplace_modal.is_visible
226+
and action in marketplace_blocked_actions
227+
):
228+
return False
229+
202230
if self._plugin_preview_mode:
203231
preview_allowed_actions = {
204232
"quit",
@@ -414,6 +442,11 @@ def on_filter_input_filter_changed(
414442
self._last_focused_panel = None
415443
if self._main_pane:
416444
self._main_pane.customization = None
445+
search_active = bool(message.query)
446+
if self._status_panel:
447+
self._status_panel.search_active = search_active
448+
if self._app_footer:
449+
self._app_footer.search_active = search_active
417450
self._update_panels()
418451
self._update_subtitle()
419452

@@ -426,6 +459,10 @@ def on_filter_input_filter_cancelled(
426459
self._last_focused_panel = None
427460
if self._main_pane:
428461
self._main_pane.customization = None
462+
if self._status_panel:
463+
self._status_panel.search_active = False
464+
if self._app_footer:
465+
self._app_footer.search_active = False
429466
self._update_panels()
430467
self._update_subtitle()
431468
self.refresh_bindings()

src/lazyclaude/mixins/filtering.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
if TYPE_CHECKING:
88
from lazyclaude.services.discovery import ConfigDiscoveryService
9+
from lazyclaude.widgets.app_footer import AppFooter
910
from lazyclaude.widgets.detail_pane import MainPane
1011
from lazyclaude.widgets.filter_input import FilterInput
1112
from lazyclaude.widgets.status_panel import StatusPanel
@@ -21,6 +22,7 @@ class FilterMixin:
2122
_main_pane: "MainPane | None"
2223
_filter_input: "FilterInput | None"
2324
_status_panel: "StatusPanel | None"
25+
_app_footer: "AppFooter | None"
2426
_discovery_service: "ConfigDiscoveryService"
2527

2628
def action_filter_all(self) -> None:
@@ -73,6 +75,11 @@ def action_toggle_plugin_enabled_filter(self) -> None:
7375
self._last_focused_panel = None
7476
if self._main_pane:
7577
self._main_pane.customization = None
78+
disabled_active = self._plugin_enabled_filter is None
79+
if self._status_panel:
80+
self._status_panel.disabled_filter_active = disabled_active
81+
if self._app_footer:
82+
self._app_footer.disabled_filter_active = disabled_active
7683
self._update_panels() # type: ignore[attr-defined]
7784
self._update_subtitle() # type: ignore[attr-defined]
7885

@@ -82,7 +89,7 @@ def action_search(self) -> None:
8289
self._filter_input.show()
8390

8491
def _update_status_filter(self, level: str) -> None:
85-
"""Update status panel filter level and path display."""
92+
"""Update status panel and footer filter level and path display."""
8693
if self._status_panel:
8794
self._status_panel.filter_level = level
8895
if level == "User":
@@ -96,3 +103,5 @@ def _update_status_filter(self, level: str) -> None:
96103
else:
97104
project_name = self._discovery_service.project_root.name
98105
self._status_panel.config_path = project_name
106+
if self._app_footer:
107+
self._app_footer.filter_level = level

0 commit comments

Comments
 (0)