Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 25 additions & 9 deletions crates/hk-desktop/src/commands/install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<dyn hk_core::adapter::AgentAdapter>],
) -> Vec<String> {
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>,
Expand Down Expand Up @@ -67,11 +88,7 @@ pub async fn install_from_local(
});

let agents: Vec<String> = 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
};
Expand Down Expand Up @@ -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 {
Expand Down
36 changes: 26 additions & 10 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand All @@ -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();
})
Expand Down
6 changes: 3 additions & 3 deletions src/components/agents/agent-detail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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("");

Expand Down Expand Up @@ -64,7 +64,7 @@ export function AgentDetail() {
if (!agent) {
return (
<div className="flex flex-1 items-center justify-center text-muted-foreground text-sm">
{agentVisibility === "detected" && !agentDetails.some((a) => a.detected)
{agents.length > 0 && !agents.some((a) => a.enabled)
? t("list.noDetected")
: t("detail.selectAgent")}
</div>
Expand Down
36 changes: 18 additions & 18 deletions src/components/agents/agent-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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(
() =>
Expand All @@ -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 } }),
Expand Down
2 changes: 1 addition & 1 deletion src/components/extensions/extension-detail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"]);
Expand Down
8 changes: 2 additions & 6 deletions src/components/extensions/extension-filters.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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;

Expand Down
2 changes: 1 addition & 1 deletion src/components/extensions/install-dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
);

Expand Down
2 changes: 1 addition & 1 deletion src/components/extensions/new-skills-dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
);

Expand Down
2 changes: 1 addition & 1 deletion src/pages/marketplace.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
10 changes: 2 additions & 8 deletions src/pages/overview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<AgentDetail[]>([]);
const [auditLoading, setAuditLoading] = useState(false);
Expand Down Expand Up @@ -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],
);

// -----------------------------------------------------------------------
Expand Down
Loading