diff --git a/crates/hk-desktop/src/commands/install.rs b/crates/hk-desktop/src/commands/install.rs index a4664ce..6c4e50a 100644 --- a/crates/hk-desktop/src/commands/install.rs +++ b/crates/hk-desktop/src/commands/install.rs @@ -17,6 +17,27 @@ pub enum ScanResult { NoSkills, } +/// Names of agents that are both detected on disk and enabled in settings — +/// the targets an "install to all detected agents" fallback should use when the +/// caller passes no explicit list. Mirrors the frontend, which only offers +/// detected + enabled agents as install targets. +fn enabled_detected_agents( + store: &hk_core::store::Store, + adapters: &[Box], +) -> Vec { + adapters + .iter() + .filter(|a| { + a.detect() + && store + .get_agent_setting(a.name()) + .map(|(_, enabled)| enabled) + .unwrap_or(true) + }) + .map(|a| a.name().to_string()) + .collect() +} + #[tauri::command] pub async fn list_hermes_categories( state: State<'_, AppState>, @@ -67,11 +88,7 @@ pub async fn install_from_local( }); let agents: Vec = if target_agents.is_empty() { - adapters - .iter() - .filter(|a| a.detect()) - .map(|a| a.name().to_string()) - .collect() + enabled_detected_agents(&store.lock(), &adapters) } else { target_agents }; @@ -270,10 +287,9 @@ pub async fn scan_git_repo( // Auto-install single skill let agents = if target_agents.is_empty() { vec![ - adapters - .iter() - .find(|a| a.detect()) - .map(|a| a.name().to_string()) + enabled_detected_agents(&store_clone.lock(), &adapters) + .into_iter() + .next() .ok_or_else(|| HkError::NotFound("No detected agent found".into()))?, ] } else { diff --git a/src/App.tsx b/src/App.tsx index d6902ca..7dc45e0 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -56,20 +56,32 @@ export default function App() { return () => mq.removeEventListener("change", onChange); }, [mode]); - // Keep "Detected only" honest over time: undetected agents must stay disabled - // — including ones added by a later app update while the user stays in this - // mode (the per-click handler in Settings can't catch those). Reconcile - // whenever the agent list or visibility changes. Converges: once disabled an - // agent is no longer "enabled", so the next run finds nothing to do. + // Keep "Detected only" honest over time, in both directions: disable agents + // that aren't detected, and re-enable ones we disabled once they're detected + // again (e.g. their config dir was removed then restored). Reconcile whenever + // the agent list or visibility changes. Converges: a disabled agent is no + // longer "enabled" and a re-enabled one leaves the snapshot, so the next run + // finds nothing to do. useEffect(() => { if (agentVisibility !== "detected") return; - const stray = agents + const { autoDisabledAgents, setAutoDisabledAgents } = useUIStore.getState(); + const snapshot = new Set(autoDisabledAgents); + + const toDisable = agents .filter((a) => !a.detected && a.enabled) .map((a) => a.name); - if (stray.length === 0) return; - useAgentStore.getState().setEnabledBulk(stray, false); - const { autoDisabledAgents, setAutoDisabledAgents } = useUIStore.getState(); - setAutoDisabledAgents([...new Set([...autoDisabledAgents, ...stray])]); + const toReEnable = agents + .filter((a) => a.detected && !a.enabled && snapshot.has(a.name)) + .map((a) => a.name); + if (toDisable.length === 0 && toReEnable.length === 0) return; + + const setEnabledBulk = useAgentStore.getState().setEnabledBulk; + if (toDisable.length > 0) setEnabledBulk(toDisable, false); + if (toReEnable.length > 0) setEnabledBulk(toReEnable, true); + + for (const n of toDisable) snapshot.add(n); + for (const n of toReEnable) snapshot.delete(n); + setAutoDisabledAgents([...snapshot]); }, [agents, agentVisibility]); // Check for updates on startup (non-blocking, silent failure). @@ -91,6 +103,10 @@ export default function App() { api .scanAndSync() .then(() => { + // Refresh agent detection too (detect() is live), so an agent dir + // added/removed outside the app shows up on focus — not just + // extensions. + useAgentStore.getState().fetch(); fetchExtensions(); loadCachedAudit(); }) diff --git a/src/components/agents/agent-detail.tsx b/src/components/agents/agent-detail.tsx index f5b768f..b1153ef 100644 --- a/src/components/agents/agent-detail.tsx +++ b/src/components/agents/agent-detail.tsx @@ -12,8 +12,8 @@ import { type ExtensionCounts, } from "@/lib/types"; import { useAgentConfigStore } from "@/stores/agent-config-store"; +import { useAgentStore } from "@/stores/agent-store"; import { useExtensionStore } from "@/stores/extension-store"; -import { useUIStore } from "@/stores/ui-store"; import { ConfigSection } from "./config-section"; import { ExtensionsSummaryCard } from "./extensions-summary-card"; import { SectionAnchorRail } from "./section-anchor-rail"; @@ -27,7 +27,7 @@ export function AgentDetail() { const allExtensions = useExtensionStore((s) => s.extensions); const { scope } = useScope(); const agent = agentDetails.find((a) => a.name === selectedAgent); - const agentVisibility = useUIStore((s) => s.agentVisibility); + const agents = useAgentStore((s) => s.agents); const [showAddForm, setShowAddForm] = useState(false); const [customPath, setCustomPath] = useState(""); @@ -64,7 +64,7 @@ export function AgentDetail() { if (!agent) { return (
- {agentVisibility === "detected" && !agentDetails.some((a) => a.detected) + {agents.length > 0 && !agents.some((a) => a.enabled) ? t("list.noDetected") : t("detail.selectAgent")}
diff --git a/src/components/agents/agent-list.tsx b/src/components/agents/agent-list.tsx index b808c4e..6e6da61 100644 --- a/src/components/agents/agent-list.tsx +++ b/src/components/agents/agent-list.tsx @@ -25,7 +25,6 @@ import type { AgentDetail } from "@/lib/types"; import { agentDisplayName } from "@/lib/types"; import { useAgentConfigStore } from "@/stores/agent-config-store"; import { useAgentStore } from "@/stores/agent-store"; -import { useUIStore } from "@/stores/ui-store"; function SortableAgentItem({ agent, @@ -100,7 +99,7 @@ export function AgentList() { const selectAgent = useAgentConfigStore((s) => s.selectAgent); const agentOrder = useAgentStore((s) => s.agentOrder); const reorderAgents = useAgentStore((s) => s.reorderAgents); - const agentVisibility = useUIStore((s) => s.agentVisibility); + const agents = useAgentStore((s) => s.agents); const sorted = useMemo( () => @@ -112,36 +111,37 @@ export function AgentList() { [agentDetails, agentOrder], ); + // A disabled agent (including ones "Detected only" auto-disabled) is hidden + // from the sidebar. AgentDetail doesn't carry enabled state, so cross- + // reference the agent store; default to visible when an agent is unknown + // (store not loaded yet) so the list never flashes empty. + const disabledNames = useMemo( + () => new Set(agents.filter((a) => !a.enabled).map((a) => a.name)), + [agents], + ); + const visible = useMemo( - () => - agentVisibility === "detected" - ? sorted.filter((a) => a.detected) - : sorted, - [sorted, agentVisibility], + () => sorted.filter((a) => !disabledNames.has(a.name)), + [sorted, disabledNames], ); const hidden = useMemo( () => new Set( - agentVisibility === "detected" - ? sorted.filter((a) => !a.detected).map((a) => a.name) - : [], + sorted.filter((a) => disabledNames.has(a.name)).map((a) => a.name), ), - [sorted, agentVisibility], + [sorted, disabledNames], ); + // Keep the selection on a visible agent — if the selected one gets hidden + // (disabled), fall back to the first visible, or clear it. useEffect(() => { - if (agentVisibility !== "detected") return; if (!selectedAgent) return; const isSelectedVisible = visible.some((a) => a.name === selectedAgent); if (!isSelectedVisible) { - if (visible.length > 0) { - selectAgent(visible[0].name); - } else { - selectAgent(null); - } + selectAgent(visible.length > 0 ? visible[0].name : null); } - }, [agentVisibility, visible, selectedAgent, selectAgent]); + }, [visible, selectedAgent, selectAgent]); const sensors = useSensors( useSensor(PointerSensor, { activationConstraint: { distance: 5 } }), diff --git a/src/components/extensions/extension-detail.tsx b/src/components/extensions/extension-detail.tsx index a61b289..2d67f5a 100644 --- a/src/components/extensions/extension-detail.tsx +++ b/src/components/extensions/extension-detail.tsx @@ -479,7 +479,7 @@ export function ExtensionDetail() { group.kind === "cli") && (() => { const detectedAgents = sortAgents( - agents.filter((a) => a.detected), + agents.filter((a) => a.detected && a.enabled), agentOrder, ); const AGENTS_WITHOUT_HOOKS = new Set(["antigravity", "opencode"]); diff --git a/src/components/extensions/extension-filters.tsx b/src/components/extensions/extension-filters.tsx index ce5a2ae..7d62dd9 100644 --- a/src/components/extensions/extension-filters.tsx +++ b/src/components/extensions/extension-filters.tsx @@ -7,7 +7,6 @@ import { isWeb as web, webSelectStyle } from "@/lib/web-select"; import { useAgentStore } from "@/stores/agent-store"; import { useExtensionStore } from "@/stores/extension-store"; import { useScopeStore } from "@/stores/scope-store"; -import { useUIStore } from "@/stores/ui-store"; const TAG_COLORS = [ "bg-primary/10 text-primary", @@ -87,16 +86,13 @@ export function ExtensionFilters() { }, [grouped, extensions, scope]); const agents = useAgentStore((s) => s.agents); const agentOrder = useAgentStore((s) => s.agentOrder); - const agentVisibility = useUIStore((s) => s.agentVisibility); const enabledAgents = useMemo( () => sortAgents( - agents.filter((a) => - agentVisibility === "detected" ? a.enabled && a.detected : a.enabled, - ), + agents.filter((a) => a.enabled), agentOrder, ), - [agents, agentOrder, agentVisibility], + [agents, agentOrder], ); const resultCount = filtered().length; diff --git a/src/components/extensions/install-dialog.tsx b/src/components/extensions/install-dialog.tsx index 4147cc6..45b6613 100644 --- a/src/components/extensions/install-dialog.tsx +++ b/src/components/extensions/install-dialog.tsx @@ -61,7 +61,7 @@ export function InstallDialog({ open, mode, onClose }: InstallDialogProps) { }, [fetchAgents]); const detectedAgents = sortAgents( - agents.filter((a) => a.detected), + agents.filter((a) => a.detected && a.enabled), agentOrder, ); diff --git a/src/components/extensions/new-skills-dialog.tsx b/src/components/extensions/new-skills-dialog.tsx index 3f6a46d..fdc7dd3 100644 --- a/src/components/extensions/new-skills-dialog.tsx +++ b/src/components/extensions/new-skills-dialog.tsx @@ -42,7 +42,7 @@ export function NewSkillsDialog({ const agents = useAgentStore((s) => s.agents); const agentOrder = useAgentStore((s) => s.agentOrder); const detectedAgents = sortAgents( - agents.filter((a) => a.detected), + agents.filter((a) => a.detected && a.enabled), agentOrder, ); diff --git a/src/pages/marketplace.tsx b/src/pages/marketplace.tsx index ac86954..9d97ae3 100644 --- a/src/pages/marketplace.tsx +++ b/src/pages/marketplace.tsx @@ -405,7 +405,7 @@ export default function MarketplacePage() { }; const detectedAgents = sortAgents( - agents.filter((a) => a.detected), + agents.filter((a) => a.detected && a.enabled), agentOrder, ); const displayItems = query.length >= 2 ? results : trending; diff --git a/src/pages/overview.tsx b/src/pages/overview.tsx index a81965b..db5314b 100644 --- a/src/pages/overview.tsx +++ b/src/pages/overview.tsx @@ -27,7 +27,6 @@ import { useAgentStore } from "@/stores/agent-store"; import { useAuditStore } from "@/stores/audit-store"; import { buildGroups, useExtensionStore } from "@/stores/extension-store"; import { toast } from "@/stores/toast-store"; -import { useUIStore } from "@/stores/ui-store"; // --------------------------------------------------------------------------- // Tip of the Day types & helpers @@ -200,7 +199,6 @@ export default function OverviewPage() { const agents = useAgentStore((s) => s.agents); const fetchAgents = useAgentStore((s) => s.fetch); const agentOrder = useAgentStore((s) => s.agentOrder); - const agentVisibility = useUIStore((s) => s.agentVisibility); const [agentConfigs, setAgentConfigs] = useState([]); const [auditLoading, setAuditLoading] = useState(false); @@ -317,18 +315,14 @@ export default function OverviewPage() { () => sortAgents( agents - .filter((a) => - agentVisibility === "detected" - ? a.enabled && a.detected - : a.enabled, - ) + .filter((a) => a.enabled) .map((a) => ({ ...a, extension_count: agentExtCounts.get(a.name) ?? 0, })), agentOrder, ), - [agents, agentExtCounts, agentOrder, agentVisibility], + [agents, agentExtCounts, agentOrder], ); // -----------------------------------------------------------------------