Skip to content

Commit 54e59c7

Browse files
RealZSTclaude
andauthored
fix(agents): hide disabled agents everywhere and reflect install/removal live (#82)
Follow-up to #81. Makes "Detected only" / agent-disable behaviour consistent and live. - A disabled agent (including ones Detected only auto-disables) is now hidden everywhere — Marketplace, the Agents page, and install targets, not just the Overview. Every surface filters by enabled; the redundant per-view agentVisibility checks are removed, leaving one concept (enabled) and one owner of visibility→enabled (the reconcile). Backend "install to all detected" fallbacks skip disabled agents too. - Agent install/removal is reflected live: runScan refetches agents on focus, and the reconcile is bidirectional (disable undetected, re-enable once detected again). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 6814dfe commit 54e59c7

10 files changed

Lines changed: 80 additions & 58 deletions

File tree

crates/hk-desktop/src/commands/install.rs

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,27 @@ pub enum ScanResult {
1717
NoSkills,
1818
}
1919

20+
/// Names of agents that are both detected on disk and enabled in settings —
21+
/// the targets an "install to all detected agents" fallback should use when the
22+
/// caller passes no explicit list. Mirrors the frontend, which only offers
23+
/// detected + enabled agents as install targets.
24+
fn enabled_detected_agents(
25+
store: &hk_core::store::Store,
26+
adapters: &[Box<dyn hk_core::adapter::AgentAdapter>],
27+
) -> Vec<String> {
28+
adapters
29+
.iter()
30+
.filter(|a| {
31+
a.detect()
32+
&& store
33+
.get_agent_setting(a.name())
34+
.map(|(_, enabled)| enabled)
35+
.unwrap_or(true)
36+
})
37+
.map(|a| a.name().to_string())
38+
.collect()
39+
}
40+
2041
#[tauri::command]
2142
pub async fn list_hermes_categories(
2243
state: State<'_, AppState>,
@@ -67,11 +88,7 @@ pub async fn install_from_local(
6788
});
6889

6990
let agents: Vec<String> = if target_agents.is_empty() {
70-
adapters
71-
.iter()
72-
.filter(|a| a.detect())
73-
.map(|a| a.name().to_string())
74-
.collect()
91+
enabled_detected_agents(&store.lock(), &adapters)
7592
} else {
7693
target_agents
7794
};
@@ -270,10 +287,9 @@ pub async fn scan_git_repo(
270287
// Auto-install single skill
271288
let agents = if target_agents.is_empty() {
272289
vec![
273-
adapters
274-
.iter()
275-
.find(|a| a.detect())
276-
.map(|a| a.name().to_string())
290+
enabled_detected_agents(&store_clone.lock(), &adapters)
291+
.into_iter()
292+
.next()
277293
.ok_or_else(|| HkError::NotFound("No detected agent found".into()))?,
278294
]
279295
} else {

src/App.tsx

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -56,20 +56,32 @@ export default function App() {
5656
return () => mq.removeEventListener("change", onChange);
5757
}, [mode]);
5858

59-
// Keep "Detected only" honest over time: undetected agents must stay disabled
60-
// — including ones added by a later app update while the user stays in this
61-
// mode (the per-click handler in Settings can't catch those). Reconcile
62-
// whenever the agent list or visibility changes. Converges: once disabled an
63-
// agent is no longer "enabled", so the next run finds nothing to do.
59+
// Keep "Detected only" honest over time, in both directions: disable agents
60+
// that aren't detected, and re-enable ones we disabled once they're detected
61+
// again (e.g. their config dir was removed then restored). Reconcile whenever
62+
// the agent list or visibility changes. Converges: a disabled agent is no
63+
// longer "enabled" and a re-enabled one leaves the snapshot, so the next run
64+
// finds nothing to do.
6465
useEffect(() => {
6566
if (agentVisibility !== "detected") return;
66-
const stray = agents
67+
const { autoDisabledAgents, setAutoDisabledAgents } = useUIStore.getState();
68+
const snapshot = new Set(autoDisabledAgents);
69+
70+
const toDisable = agents
6771
.filter((a) => !a.detected && a.enabled)
6872
.map((a) => a.name);
69-
if (stray.length === 0) return;
70-
useAgentStore.getState().setEnabledBulk(stray, false);
71-
const { autoDisabledAgents, setAutoDisabledAgents } = useUIStore.getState();
72-
setAutoDisabledAgents([...new Set([...autoDisabledAgents, ...stray])]);
73+
const toReEnable = agents
74+
.filter((a) => a.detected && !a.enabled && snapshot.has(a.name))
75+
.map((a) => a.name);
76+
if (toDisable.length === 0 && toReEnable.length === 0) return;
77+
78+
const setEnabledBulk = useAgentStore.getState().setEnabledBulk;
79+
if (toDisable.length > 0) setEnabledBulk(toDisable, false);
80+
if (toReEnable.length > 0) setEnabledBulk(toReEnable, true);
81+
82+
for (const n of toDisable) snapshot.add(n);
83+
for (const n of toReEnable) snapshot.delete(n);
84+
setAutoDisabledAgents([...snapshot]);
7385
}, [agents, agentVisibility]);
7486

7587
// Check for updates on startup (non-blocking, silent failure).
@@ -91,6 +103,10 @@ export default function App() {
91103
api
92104
.scanAndSync()
93105
.then(() => {
106+
// Refresh agent detection too (detect() is live), so an agent dir
107+
// added/removed outside the app shows up on focus — not just
108+
// extensions.
109+
useAgentStore.getState().fetch();
94110
fetchExtensions();
95111
loadCachedAudit();
96112
})

src/components/agents/agent-detail.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ import {
1212
type ExtensionCounts,
1313
} from "@/lib/types";
1414
import { useAgentConfigStore } from "@/stores/agent-config-store";
15+
import { useAgentStore } from "@/stores/agent-store";
1516
import { useExtensionStore } from "@/stores/extension-store";
16-
import { useUIStore } from "@/stores/ui-store";
1717
import { ConfigSection } from "./config-section";
1818
import { ExtensionsSummaryCard } from "./extensions-summary-card";
1919
import { SectionAnchorRail } from "./section-anchor-rail";
@@ -27,7 +27,7 @@ export function AgentDetail() {
2727
const allExtensions = useExtensionStore((s) => s.extensions);
2828
const { scope } = useScope();
2929
const agent = agentDetails.find((a) => a.name === selectedAgent);
30-
const agentVisibility = useUIStore((s) => s.agentVisibility);
30+
const agents = useAgentStore((s) => s.agents);
3131
const [showAddForm, setShowAddForm] = useState(false);
3232
const [customPath, setCustomPath] = useState("");
3333

@@ -64,7 +64,7 @@ export function AgentDetail() {
6464
if (!agent) {
6565
return (
6666
<div className="flex flex-1 items-center justify-center text-muted-foreground text-sm">
67-
{agentVisibility === "detected" && !agentDetails.some((a) => a.detected)
67+
{agents.length > 0 && !agents.some((a) => a.enabled)
6868
? t("list.noDetected")
6969
: t("detail.selectAgent")}
7070
</div>

src/components/agents/agent-list.tsx

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ import type { AgentDetail } from "@/lib/types";
2525
import { agentDisplayName } from "@/lib/types";
2626
import { useAgentConfigStore } from "@/stores/agent-config-store";
2727
import { useAgentStore } from "@/stores/agent-store";
28-
import { useUIStore } from "@/stores/ui-store";
2928

3029
function SortableAgentItem({
3130
agent,
@@ -100,7 +99,7 @@ export function AgentList() {
10099
const selectAgent = useAgentConfigStore((s) => s.selectAgent);
101100
const agentOrder = useAgentStore((s) => s.agentOrder);
102101
const reorderAgents = useAgentStore((s) => s.reorderAgents);
103-
const agentVisibility = useUIStore((s) => s.agentVisibility);
102+
const agents = useAgentStore((s) => s.agents);
104103

105104
const sorted = useMemo(
106105
() =>
@@ -112,36 +111,37 @@ export function AgentList() {
112111
[agentDetails, agentOrder],
113112
);
114113

114+
// A disabled agent (including ones "Detected only" auto-disabled) is hidden
115+
// from the sidebar. AgentDetail doesn't carry enabled state, so cross-
116+
// reference the agent store; default to visible when an agent is unknown
117+
// (store not loaded yet) so the list never flashes empty.
118+
const disabledNames = useMemo(
119+
() => new Set(agents.filter((a) => !a.enabled).map((a) => a.name)),
120+
[agents],
121+
);
122+
115123
const visible = useMemo(
116-
() =>
117-
agentVisibility === "detected"
118-
? sorted.filter((a) => a.detected)
119-
: sorted,
120-
[sorted, agentVisibility],
124+
() => sorted.filter((a) => !disabledNames.has(a.name)),
125+
[sorted, disabledNames],
121126
);
122127

123128
const hidden = useMemo(
124129
() =>
125130
new Set(
126-
agentVisibility === "detected"
127-
? sorted.filter((a) => !a.detected).map((a) => a.name)
128-
: [],
131+
sorted.filter((a) => disabledNames.has(a.name)).map((a) => a.name),
129132
),
130-
[sorted, agentVisibility],
133+
[sorted, disabledNames],
131134
);
132135

136+
// Keep the selection on a visible agent — if the selected one gets hidden
137+
// (disabled), fall back to the first visible, or clear it.
133138
useEffect(() => {
134-
if (agentVisibility !== "detected") return;
135139
if (!selectedAgent) return;
136140
const isSelectedVisible = visible.some((a) => a.name === selectedAgent);
137141
if (!isSelectedVisible) {
138-
if (visible.length > 0) {
139-
selectAgent(visible[0].name);
140-
} else {
141-
selectAgent(null);
142-
}
142+
selectAgent(visible.length > 0 ? visible[0].name : null);
143143
}
144-
}, [agentVisibility, visible, selectedAgent, selectAgent]);
144+
}, [visible, selectedAgent, selectAgent]);
145145

146146
const sensors = useSensors(
147147
useSensor(PointerSensor, { activationConstraint: { distance: 5 } }),

src/components/extensions/extension-detail.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -479,7 +479,7 @@ export function ExtensionDetail() {
479479
group.kind === "cli") &&
480480
(() => {
481481
const detectedAgents = sortAgents(
482-
agents.filter((a) => a.detected),
482+
agents.filter((a) => a.detected && a.enabled),
483483
agentOrder,
484484
);
485485
const AGENTS_WITHOUT_HOOKS = new Set(["antigravity", "opencode"]);

src/components/extensions/extension-filters.tsx

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import { isWeb as web, webSelectStyle } from "@/lib/web-select";
77
import { useAgentStore } from "@/stores/agent-store";
88
import { useExtensionStore } from "@/stores/extension-store";
99
import { useScopeStore } from "@/stores/scope-store";
10-
import { useUIStore } from "@/stores/ui-store";
1110

1211
const TAG_COLORS = [
1312
"bg-primary/10 text-primary",
@@ -87,16 +86,13 @@ export function ExtensionFilters() {
8786
}, [grouped, extensions, scope]);
8887
const agents = useAgentStore((s) => s.agents);
8988
const agentOrder = useAgentStore((s) => s.agentOrder);
90-
const agentVisibility = useUIStore((s) => s.agentVisibility);
9189
const enabledAgents = useMemo(
9290
() =>
9391
sortAgents(
94-
agents.filter((a) =>
95-
agentVisibility === "detected" ? a.enabled && a.detected : a.enabled,
96-
),
92+
agents.filter((a) => a.enabled),
9793
agentOrder,
9894
),
99-
[agents, agentOrder, agentVisibility],
95+
[agents, agentOrder],
10096
);
10197
const resultCount = filtered().length;
10298

src/components/extensions/install-dialog.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ export function InstallDialog({ open, mode, onClose }: InstallDialogProps) {
6161
}, [fetchAgents]);
6262

6363
const detectedAgents = sortAgents(
64-
agents.filter((a) => a.detected),
64+
agents.filter((a) => a.detected && a.enabled),
6565
agentOrder,
6666
);
6767

src/components/extensions/new-skills-dialog.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ export function NewSkillsDialog({
4242
const agents = useAgentStore((s) => s.agents);
4343
const agentOrder = useAgentStore((s) => s.agentOrder);
4444
const detectedAgents = sortAgents(
45-
agents.filter((a) => a.detected),
45+
agents.filter((a) => a.detected && a.enabled),
4646
agentOrder,
4747
);
4848

src/pages/marketplace.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -405,7 +405,7 @@ export default function MarketplacePage() {
405405
};
406406

407407
const detectedAgents = sortAgents(
408-
agents.filter((a) => a.detected),
408+
agents.filter((a) => a.detected && a.enabled),
409409
agentOrder,
410410
);
411411
const displayItems = query.length >= 2 ? results : trending;

src/pages/overview.tsx

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ import { useAgentStore } from "@/stores/agent-store";
2727
import { useAuditStore } from "@/stores/audit-store";
2828
import { buildGroups, useExtensionStore } from "@/stores/extension-store";
2929
import { toast } from "@/stores/toast-store";
30-
import { useUIStore } from "@/stores/ui-store";
3130

3231
// ---------------------------------------------------------------------------
3332
// Tip of the Day types & helpers
@@ -200,7 +199,6 @@ export default function OverviewPage() {
200199
const agents = useAgentStore((s) => s.agents);
201200
const fetchAgents = useAgentStore((s) => s.fetch);
202201
const agentOrder = useAgentStore((s) => s.agentOrder);
203-
const agentVisibility = useUIStore((s) => s.agentVisibility);
204202

205203
const [agentConfigs, setAgentConfigs] = useState<AgentDetail[]>([]);
206204
const [auditLoading, setAuditLoading] = useState(false);
@@ -317,18 +315,14 @@ export default function OverviewPage() {
317315
() =>
318316
sortAgents(
319317
agents
320-
.filter((a) =>
321-
agentVisibility === "detected"
322-
? a.enabled && a.detected
323-
: a.enabled,
324-
)
318+
.filter((a) => a.enabled)
325319
.map((a) => ({
326320
...a,
327321
extension_count: agentExtCounts.get(a.name) ?? 0,
328322
})),
329323
agentOrder,
330324
),
331-
[agents, agentExtCounts, agentOrder, agentVisibility],
325+
[agents, agentExtCounts, agentOrder],
332326
);
333327

334328
// -----------------------------------------------------------------------

0 commit comments

Comments
 (0)