From 33d49ee71236d3727156372b9633508cba6b5e43 Mon Sep 17 00:00:00 2001 From: Orchard Date: Mon, 8 Jun 2026 11:40:15 +0800 Subject: [PATCH 1/5] feat(ui): add Agent Visibility preference to hide undetected agents --- src/components/agents/agent-detail.tsx | 6 +- src/components/agents/agent-list.tsx | 87 +++++++++++++++++-- .../extensions/extension-filters.tsx | 17 ++-- src/lib/i18n/locales/en/agents.json | 3 +- src/lib/i18n/locales/en/settings.json | 6 +- src/lib/i18n/locales/zh/agents.json | 3 +- src/lib/i18n/locales/zh/settings.json | 6 +- src/pages/overview.tsx | 10 ++- src/pages/settings.tsx | 50 ++++++++++- src/stores/__tests__/ui-store.test.ts | 12 +++ src/stores/agent-config-store.ts | 4 +- src/stores/ui-store.ts | 17 ++++ 12 files changed, 198 insertions(+), 23 deletions(-) diff --git a/src/components/agents/agent-detail.tsx b/src/components/agents/agent-detail.tsx index 58979623..f5b768fb 100644 --- a/src/components/agents/agent-detail.tsx +++ b/src/components/agents/agent-detail.tsx @@ -13,6 +13,7 @@ import { } from "@/lib/types"; import { useAgentConfigStore } from "@/stores/agent-config-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"; @@ -26,6 +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 [showAddForm, setShowAddForm] = useState(false); const [customPath, setCustomPath] = useState(""); @@ -62,7 +64,9 @@ export function AgentDetail() { if (!agent) { return (
- {t("detail.selectAgent")} + {agentVisibility === "detected" && !agentDetails.some((a) => a.detected) + ? t("list.noDetected") + : t("detail.selectAgent")}
); } diff --git a/src/components/agents/agent-list.tsx b/src/components/agents/agent-list.tsx index 23db2577..b808c4e4 100644 --- a/src/components/agents/agent-list.tsx +++ b/src/components/agents/agent-list.tsx @@ -18,13 +18,14 @@ import { import { CSS } from "@dnd-kit/utilities"; import { clsx } from "clsx"; import { GripVertical } from "lucide-react"; -import { useCallback, useMemo } from "react"; +import { useCallback, useEffect, useMemo } from "react"; import { useTranslation } from "react-i18next"; import { AgentMascot } from "@/components/shared/agent-mascot/agent-mascot"; 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, @@ -99,6 +100,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 sorted = useMemo( () => @@ -110,6 +112,37 @@ export function AgentList() { [agentDetails, agentOrder], ); + const visible = useMemo( + () => + agentVisibility === "detected" + ? sorted.filter((a) => a.detected) + : sorted, + [sorted, agentVisibility], + ); + + const hidden = useMemo( + () => + new Set( + agentVisibility === "detected" + ? sorted.filter((a) => !a.detected).map((a) => a.name) + : [], + ), + [sorted, agentVisibility], + ); + + 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); + } + } + }, [agentVisibility, visible, selectedAgent, selectAgent]); + const sensors = useSensors( useSensor(PointerSensor, { activationConstraint: { distance: 5 } }), useSensor(KeyboardSensor, { @@ -122,16 +155,54 @@ export function AgentList() { const { active, over } = event; if (!over || active.id === over.id) return; - const names = sorted.map((a) => a.name); - const oldIndex = names.indexOf(active.id as string); - const newIndex = names.indexOf(over.id as string); + const visibleNames = visible.map((a) => a.name); + const oldIndex = visibleNames.indexOf(active.id as string); + const newIndex = visibleNames.indexOf(over.id as string); if (oldIndex === -1 || newIndex === -1) return; - reorderAgents(arrayMove(names, oldIndex, newIndex)); + const reorderedVisible = arrayMove(visibleNames, oldIndex, newIndex); + + let fullOrder: string[]; + if (hidden.size === 0) { + fullOrder = reorderedVisible; + } else { + const result: string[] = []; + let vi = 0; + for (const name of agentOrder) { + if (hidden.has(name)) { + result.push(name); + } else { + if (vi < reorderedVisible.length) { + result.push(reorderedVisible[vi]); + vi++; + } + } + } + while (vi < reorderedVisible.length) { + result.push(reorderedVisible[vi]); + vi++; + } + fullOrder = result; + } + + reorderAgents(fullOrder); }, - [sorted, reorderAgents], + [visible, hidden, agentOrder, reorderAgents], ); + if (visible.length === 0) { + return ( +
+
+ {t("list.header")} +
+
+ {t("list.noDetected")} +
+
+ ); + } + return (
@@ -144,10 +215,10 @@ export function AgentList() { onDragEnd={handleDragEnd} > a.name)} + items={visible.map((a) => a.name)} strategy={verticalListSortingStrategy} > - {sorted.map((agent) => ( + {visible.map((agent) => ( s.agents); const agentOrder = useAgentStore((s) => s.agentOrder); + const agentVisibility = useUIStore((s) => s.agentVisibility); const enabledAgents = useMemo( () => sortAgents( - agents.filter((a) => a.enabled), + agents.filter((a) => + agentVisibility === "detected" ? a.enabled && a.detected : a.enabled, + ), agentOrder, ), - [agents, agentOrder], + [agents, agentOrder, agentVisibility], ); const resultCount = filtered().length; - // Clear packFilter when the selected pack no longer exists in the current - // scope — otherwise the dropdown shows a stale value not in options and - // results read empty. useEffect(() => { if (packFilter && !scopedPacks.includes(packFilter)) { setPackFilter(null); } }, [packFilter, scopedPacks, setPackFilter]); + useEffect(() => { + if (agentFilter && !enabledAgents.some((a) => a.name === agentFilter)) { + setAgentFilter(null); + } + }, [agentFilter, enabledAgents, setAgentFilter]); + return (
{/* Filters: kind pills + result count + dropdowns + search */} diff --git a/src/lib/i18n/locales/en/agents.json b/src/lib/i18n/locales/en/agents.json index 51f91fd7..84a73f8f 100644 --- a/src/lib/i18n/locales/en/agents.json +++ b/src/lib/i18n/locales/en/agents.json @@ -1,7 +1,8 @@ { "list": { "header": "Agents", - "notDetected": "Not detected" + "notDetected": "Not detected", + "noDetected": "No detected agents" }, "detail": { "selectAgent": "Select an agent to view its configuration", diff --git a/src/lib/i18n/locales/en/settings.json b/src/lib/i18n/locales/en/settings.json index 4d59c29b..ade20e5f 100644 --- a/src/lib/i18n/locales/en/settings.json +++ b/src/lib/i18n/locales/en/settings.json @@ -20,7 +20,11 @@ "modeToast": "Mode: {{label}}", "appIcon": "App Icon", "iconToast": "Icon: {{label}}", - "iconFailed": "Failed to set icon" + "iconFailed": "Failed to set icon", + "agentVisibility": "Agent Visibility", + "agentVisibilityAll": "All agents", + "agentVisibilityDetected": "Detected only", + "agentVisibilityToast": "Visibility: {{label}}" }, "agentPaths": { "section": "Agent Paths", diff --git a/src/lib/i18n/locales/zh/agents.json b/src/lib/i18n/locales/zh/agents.json index 095470da..ddd1c5ed 100644 --- a/src/lib/i18n/locales/zh/agents.json +++ b/src/lib/i18n/locales/zh/agents.json @@ -1,7 +1,8 @@ { "list": { "header": "智能体", - "notDetected": "未检测到" + "notDetected": "未检测到", + "noDetected": "未检测到智能体" }, "detail": { "selectAgent": "选择一个智能体以查看其配置", diff --git a/src/lib/i18n/locales/zh/settings.json b/src/lib/i18n/locales/zh/settings.json index 83288a53..86ef9de7 100644 --- a/src/lib/i18n/locales/zh/settings.json +++ b/src/lib/i18n/locales/zh/settings.json @@ -20,7 +20,11 @@ "modeToast": "模式:{{label}}", "appIcon": "应用图标", "iconToast": "图标:{{label}}", - "iconFailed": "设置图标失败" + "iconFailed": "设置图标失败", + "agentVisibility": "智能体可见性", + "agentVisibilityAll": "全部智能体", + "agentVisibilityDetected": "仅已检测到", + "agentVisibilityToast": "可见性:{{label}}" }, "agentPaths": { "section": "智能体路径", diff --git a/src/pages/overview.tsx b/src/pages/overview.tsx index db5314b8..a81965b4 100644 --- a/src/pages/overview.tsx +++ b/src/pages/overview.tsx @@ -27,6 +27,7 @@ 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 @@ -199,6 +200,7 @@ 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); @@ -315,14 +317,18 @@ export default function OverviewPage() { () => sortAgents( agents - .filter((a) => a.enabled) + .filter((a) => + agentVisibility === "detected" + ? a.enabled && a.detected + : a.enabled, + ) .map((a) => ({ ...a, extension_count: agentExtCounts.get(a.name) ?? 0, })), agentOrder, ), - [agents, agentExtCounts, agentOrder], + [agents, agentExtCounts, agentOrder, agentVisibility], ); // ----------------------------------------------------------------------- diff --git a/src/pages/settings.tsx b/src/pages/settings.tsx index 8dc05a5c..4d805756 100644 --- a/src/pages/settings.tsx +++ b/src/pages/settings.tsx @@ -27,7 +27,7 @@ import { agentDisplayName, type DiscoveredProject } from "@/lib/types"; import { useAgentStore } from "@/stores/agent-store"; import { useProjectStore } from "@/stores/project-store"; import { toast } from "@/stores/toast-store"; -import type { AppIcon, ThemeName } from "@/stores/ui-store"; +import type { AgentVisibility, AppIcon, ThemeName } from "@/stores/ui-store"; import { useUIStore } from "@/stores/ui-store"; import { useUpdateStore } from "@/stores/update-store"; import { useWebUpdateStore } from "@/stores/web-update-store"; @@ -71,6 +71,16 @@ const LANGUAGE_OPTIONS: { { value: "zh", labelKey: "language.zh" }, ]; +const AGENT_VISIBILITY_OPTIONS: { + value: AgentVisibility; + labelKey: + | "appearance.agentVisibilityAll" + | "appearance.agentVisibilityDetected"; +}[] = [ + { value: "all", labelKey: "appearance.agentVisibilityAll" }, + { value: "detected", labelKey: "appearance.agentVisibilityDetected" }, +]; + function UpdateSection() { const { t } = useTranslation("settings"); const available = useUpdateStore((s) => s.available); @@ -172,9 +182,11 @@ export default function SettingsPage() { themeName, mode, appIcon, + agentVisibility, setThemeName, setMode, setAppIcon: setAppIconState, + setAgentVisibility, } = useUIStore(); const { projects, loading, loadProjects, addProject, removeProject } = useProjectStore(); @@ -766,6 +778,42 @@ export default function SettingsPage() {
)} + +
+ + {/* Agent Visibility */} +
+ + {t("appearance.agentVisibility")} + +
+ {AGENT_VISIBILITY_OPTIONS.map((opt, i) => ( + + ))} +
+
diff --git a/src/stores/__tests__/ui-store.test.ts b/src/stores/__tests__/ui-store.test.ts index b09a6cc8..a5270542 100644 --- a/src/stores/__tests__/ui-store.test.ts +++ b/src/stores/__tests__/ui-store.test.ts @@ -13,30 +13,35 @@ describe("ui-store localStorage validation", () => { expect(state.themeName).toBe("tiesen"); expect(state.appIcon).toBe("icon-1"); expect(state.sidebarOpen).toBe(true); + expect(state.agentVisibility).toBe("all"); }); it("reads valid localStorage values", async () => { localStorage.setItem("hk-theme", "dark"); localStorage.setItem("hk-theme-name", "claude"); localStorage.setItem("hk-app-icon", "icon-2"); + localStorage.setItem("hk-agent-visibility", "detected"); const { useUIStore } = await import("../ui-store"); const state = useUIStore.getState(); expect(state.mode).toBe("dark"); expect(state.themeName).toBe("claude"); expect(state.appIcon).toBe("icon-2"); + expect(state.agentVisibility).toBe("detected"); }); it("ignores invalid localStorage values and falls back to defaults", async () => { localStorage.setItem("hk-theme", "INVALID_MODE"); localStorage.setItem("hk-theme-name", "INVALID_THEME"); localStorage.setItem("hk-app-icon", "INVALID_ICON"); + localStorage.setItem("hk-agent-visibility", "INVALID_VISIBILITY"); const { useUIStore } = await import("../ui-store"); const state = useUIStore.getState(); expect(state.mode).toBe("system"); expect(state.themeName).toBe("tiesen"); expect(state.appIcon).toBe("icon-1"); + expect(state.agentVisibility).toBe("all"); }); it("setMode persists to localStorage", async () => { @@ -61,4 +66,11 @@ describe("ui-store localStorage validation", () => { useUIStore.getState().toggleSidebar(); expect(useUIStore.getState().sidebarOpen).toBe(true); }); + + it("setAgentVisibility persists to localStorage", async () => { + const { useUIStore } = await import("../ui-store"); + useUIStore.getState().setAgentVisibility("detected"); + expect(localStorage.getItem("hk-agent-visibility")).toBe("detected"); + expect(useUIStore.getState().agentVisibility).toBe("detected"); + }); }); diff --git a/src/stores/agent-config-store.ts b/src/stores/agent-config-store.ts index 5db2ced6..d20e0b02 100644 --- a/src/stores/agent-config-store.ts +++ b/src/stores/agent-config-store.ts @@ -21,7 +21,7 @@ interface AgentConfigState { pendingFocusFile: string | null; fetch: () => Promise; - selectAgent: (name: string) => void; + selectAgent: (name: string | null) => void; expandFile: (path: string) => void; toggleFile: (path: string) => void; setPendingFocusFile: (path: string | null) => void; @@ -82,7 +82,7 @@ export const useAgentConfigStore = create((set, get) => ({ } }, - selectAgent(name: string) { + selectAgent(name: string | null) { set({ selectedAgent: name, expandedFiles: new Set() }); }, diff --git a/src/stores/ui-store.ts b/src/stores/ui-store.ts index d32343f9..30775385 100644 --- a/src/stores/ui-store.ts +++ b/src/stores/ui-store.ts @@ -3,6 +3,7 @@ import { create } from "zustand"; export type ThemeName = "tiesen" | "claude"; export type Mode = "system" | "dark" | "light"; export type AppIcon = "icon-1" | "icon-2"; +export type AgentVisibility = "all" | "detected"; /** * Safely retrieves and validates a localStorage value against allowed values. @@ -25,15 +26,21 @@ interface UIState { themeName: ThemeName; mode: Mode; appIcon: AppIcon; + agentVisibility: AgentVisibility; toggleSidebar: () => void; setThemeName: (name: ThemeName) => void; setMode: (mode: Mode) => void; setAppIcon: (icon: AppIcon) => void; + setAgentVisibility: (visibility: AgentVisibility) => void; } const ALLOWED_MODES: readonly Mode[] = ["system", "dark", "light"]; const ALLOWED_THEME_NAMES: readonly ThemeName[] = ["tiesen", "claude"]; const ALLOWED_APP_ICONS: readonly AppIcon[] = ["icon-1", "icon-2"]; +const ALLOWED_AGENT_VISIBILITY: readonly AgentVisibility[] = [ + "all", + "detected", +]; const storedMode = getValidItem("hk-theme", ALLOWED_MODES, "system"); const storedThemeName = getValidItem( @@ -42,6 +49,11 @@ const storedThemeName = getValidItem( "tiesen", ); const storedAppIcon = getValidItem("hk-app-icon", ALLOWED_APP_ICONS, "icon-1"); +const storedAgentVisibility = getValidItem( + "hk-agent-visibility", + ALLOWED_AGENT_VISIBILITY, + "all", +); /** Resolve "system" to actual light/dark based on OS preference */ export function resolveMode(mode: Mode): "dark" | "light" { @@ -56,6 +68,7 @@ export const useUIStore = create((set) => ({ themeName: storedThemeName, mode: storedMode, appIcon: storedAppIcon, + agentVisibility: storedAgentVisibility, toggleSidebar() { set((s) => ({ sidebarOpen: !s.sidebarOpen })); }, @@ -71,4 +84,8 @@ export const useUIStore = create((set) => ({ localStorage.setItem("hk-app-icon", appIcon); set({ appIcon }); }, + setAgentVisibility(agentVisibility) { + localStorage.setItem("hk-agent-visibility", agentVisibility); + set({ agentVisibility }); + }, })); From 3f3ceb9d0a487ddc1385ee74864c2bc1f2656a50 Mon Sep 17 00:00:00 2001 From: RealZST Date: Mon, 8 Jun 2026 14:13:10 +0300 Subject: [PATCH 2/5] feat(settings): move Agent Visibility into Agent Paths and sync enabled state - Relocate the All/Detected toggle from Appearance into the Agent Paths header - "Detected only" now hides and disables undetected agents (reversible) - Lock undetected agents' Enabled toggle while in Detected only mode - Persist the auto-disabled snapshot so the restore survives a restart - Add setEnabledBulk to batch-toggle agents without per-agent toasts Co-Authored-By: Claude Opus 4.8 (1M context) --- src/lib/i18n/locales/en/settings.json | 10 +-- src/lib/i18n/locales/zh/settings.json | 10 +-- src/pages/settings.tsx | 122 ++++++++++++++++---------- src/stores/__tests__/ui-store.test.ts | 34 +++++++ src/stores/agent-store.ts | 15 ++++ src/stores/ui-store.ts | 34 +++++++ 6 files changed, 167 insertions(+), 58 deletions(-) diff --git a/src/lib/i18n/locales/en/settings.json b/src/lib/i18n/locales/en/settings.json index ade20e5f..9be541ad 100644 --- a/src/lib/i18n/locales/en/settings.json +++ b/src/lib/i18n/locales/en/settings.json @@ -20,15 +20,15 @@ "modeToast": "Mode: {{label}}", "appIcon": "App Icon", "iconToast": "Icon: {{label}}", - "iconFailed": "Failed to set icon", - "agentVisibility": "Agent Visibility", - "agentVisibilityAll": "All agents", - "agentVisibilityDetected": "Detected only", - "agentVisibilityToast": "Visibility: {{label}}" + "iconFailed": "Failed to set icon" }, "agentPaths": { "section": "Agent Paths", "description": "Auto-detected paths shown below. Click the edit button to choose a custom path.", + "visibilityAll": "All agents", + "visibilityDetected": "Detected only", + "visibilityToast": "Visibility: {{label}}", + "visibilityHint": "Detected only also hides and disables undetected agents.", "enabled": "Enabled", "disabled": "Disabled", "notDetected": "Not detected", diff --git a/src/lib/i18n/locales/zh/settings.json b/src/lib/i18n/locales/zh/settings.json index 86ef9de7..e5b301a4 100644 --- a/src/lib/i18n/locales/zh/settings.json +++ b/src/lib/i18n/locales/zh/settings.json @@ -20,15 +20,15 @@ "modeToast": "模式:{{label}}", "appIcon": "应用图标", "iconToast": "图标:{{label}}", - "iconFailed": "设置图标失败", - "agentVisibility": "智能体可见性", - "agentVisibilityAll": "全部智能体", - "agentVisibilityDetected": "仅已检测到", - "agentVisibilityToast": "可见性:{{label}}" + "iconFailed": "设置图标失败" }, "agentPaths": { "section": "智能体路径", "description": "下方显示自动检测到的路径。点击编辑按钮可选择自定义路径。", + "visibilityAll": "全部智能体", + "visibilityDetected": "仅已检测到", + "visibilityToast": "可见性:{{label}}", + "visibilityHint": "切换“仅已检测到”会隐藏并禁用未检测到的智能体。", "enabled": "已启用", "disabled": "已禁用", "notDetected": "未检测到", diff --git a/src/pages/settings.tsx b/src/pages/settings.tsx index 4d805756..1f91a945 100644 --- a/src/pages/settings.tsx +++ b/src/pages/settings.tsx @@ -73,12 +73,10 @@ const LANGUAGE_OPTIONS: { const AGENT_VISIBILITY_OPTIONS: { value: AgentVisibility; - labelKey: - | "appearance.agentVisibilityAll" - | "appearance.agentVisibilityDetected"; + labelKey: "agentPaths.visibilityAll" | "agentPaths.visibilityDetected"; }[] = [ - { value: "all", labelKey: "appearance.agentVisibilityAll" }, - { value: "detected", labelKey: "appearance.agentVisibilityDetected" }, + { value: "all", labelKey: "agentPaths.visibilityAll" }, + { value: "detected", labelKey: "agentPaths.visibilityDetected" }, ]; function UpdateSection() { @@ -183,10 +181,12 @@ export default function SettingsPage() { mode, appIcon, agentVisibility, + autoDisabledAgents, setThemeName, setMode, setAppIcon: setAppIconState, setAgentVisibility, + setAutoDisabledAgents, } = useUIStore(); const { projects, loading, loadProjects, addProject, removeProject } = useProjectStore(); @@ -196,6 +196,7 @@ export default function SettingsPage() { fetch: fetchAgents, updatePath, setEnabled, + setEnabledBulk, } = useAgentStore(); const [searchParams, setSearchParams] = useSearchParams(); @@ -234,6 +235,32 @@ export default function SettingsPage() { const agentNames = agentOrder; const agentMap = new Map(agents.map((a) => [a.name.toLowerCase(), a])); + // Switching to "Detected only" snapshots which enabled-but-undetected agents + // get auto-disabled, so switching back to "All agents" re-enables exactly + // those — never agents the user disabled by hand. + const handleVisibilityChange = (next: AgentVisibility) => { + if (next === agentVisibility) return; + if (next === "detected") { + const toDisable = agents + .filter((a) => !a.detected && a.enabled) + .map((a) => a.name); + setAutoDisabledAgents(toDisable); + setEnabledBulk(toDisable, false); + } else { + setEnabledBulk(autoDisabledAgents, true); + setAutoDisabledAgents([]); + } + setAgentVisibility(next); + toast.success( + t("agentPaths.visibilityToast", { + label: t( + AGENT_VISIBILITY_OPTIONS.find((o) => o.value === next)?.labelKey ?? + "agentPaths.visibilityAll", + ), + }), + ); + }; + const existingPaths = new Set(projects.map((p) => p.path)); const handleAddPath = async (path: string) => { @@ -321,18 +348,48 @@ export default function SettingsPage() {
{/* Agent Paths */}
-
-

- {t("agentPaths.section")} -

-

- {t("agentPaths.description")} -

+ {/* Header: title + description, with visibility toggle top-right */} +
+
+

+ {t("agentPaths.section")} +

+

+ {t("agentPaths.description")} +

+

+ {t("agentPaths.visibilityHint")} +

+
+
+ {AGENT_VISIBILITY_OPTIONS.map((opt, i) => ( + + ))} +
+
{agentNames.map((agent) => { const info = agentMap.get(agent); const isEnabled = info?.enabled ?? true; + const locked = + agentVisibility === "detected" && !(info?.detected ?? false); return (
)} - -
- - {/* Agent Visibility */} -
- - {t("appearance.agentVisibility")} - -
- {AGENT_VISIBILITY_OPTIONS.map((opt, i) => ( - - ))} -
-
diff --git a/src/stores/__tests__/ui-store.test.ts b/src/stores/__tests__/ui-store.test.ts index a5270542..eef1c075 100644 --- a/src/stores/__tests__/ui-store.test.ts +++ b/src/stores/__tests__/ui-store.test.ts @@ -14,6 +14,7 @@ describe("ui-store localStorage validation", () => { expect(state.appIcon).toBe("icon-1"); expect(state.sidebarOpen).toBe(true); expect(state.agentVisibility).toBe("all"); + expect(state.autoDisabledAgents).toEqual([]); }); it("reads valid localStorage values", async () => { @@ -21,6 +22,10 @@ describe("ui-store localStorage validation", () => { localStorage.setItem("hk-theme-name", "claude"); localStorage.setItem("hk-app-icon", "icon-2"); localStorage.setItem("hk-agent-visibility", "detected"); + localStorage.setItem( + "hk-agent-auto-disabled", + JSON.stringify(["cursor", "gemini"]), + ); const { useUIStore } = await import("../ui-store"); const state = useUIStore.getState(); @@ -28,6 +33,7 @@ describe("ui-store localStorage validation", () => { expect(state.themeName).toBe("claude"); expect(state.appIcon).toBe("icon-2"); expect(state.agentVisibility).toBe("detected"); + expect(state.autoDisabledAgents).toEqual(["cursor", "gemini"]); }); it("ignores invalid localStorage values and falls back to defaults", async () => { @@ -35,6 +41,7 @@ describe("ui-store localStorage validation", () => { localStorage.setItem("hk-theme-name", "INVALID_THEME"); localStorage.setItem("hk-app-icon", "INVALID_ICON"); localStorage.setItem("hk-agent-visibility", "INVALID_VISIBILITY"); + localStorage.setItem("hk-agent-auto-disabled", "not-json{["); const { useUIStore } = await import("../ui-store"); const state = useUIStore.getState(); @@ -42,6 +49,17 @@ describe("ui-store localStorage validation", () => { expect(state.themeName).toBe("tiesen"); expect(state.appIcon).toBe("icon-1"); expect(state.agentVisibility).toBe("all"); + expect(state.autoDisabledAgents).toEqual([]); + }); + + it("ignores a non-string-array auto-disabled value", async () => { + localStorage.setItem( + "hk-agent-auto-disabled", + JSON.stringify([1, "ok", true]), + ); + + const { useUIStore } = await import("../ui-store"); + expect(useUIStore.getState().autoDisabledAgents).toEqual([]); }); it("setMode persists to localStorage", async () => { @@ -73,4 +91,20 @@ describe("ui-store localStorage validation", () => { expect(localStorage.getItem("hk-agent-visibility")).toBe("detected"); expect(useUIStore.getState().agentVisibility).toBe("detected"); }); + + it("setAutoDisabledAgents persists the snapshot as JSON", async () => { + const { useUIStore } = await import("../ui-store"); + useUIStore.getState().setAutoDisabledAgents(["cursor", "gemini"]); + expect(localStorage.getItem("hk-agent-auto-disabled")).toBe( + JSON.stringify(["cursor", "gemini"]), + ); + expect(useUIStore.getState().autoDisabledAgents).toEqual([ + "cursor", + "gemini", + ]); + + useUIStore.getState().setAutoDisabledAgents([]); + expect(localStorage.getItem("hk-agent-auto-disabled")).toBe("[]"); + expect(useUIStore.getState().autoDisabledAgents).toEqual([]); + }); }); diff --git a/src/stores/agent-store.ts b/src/stores/agent-store.ts index 80651095..38f0e968 100644 --- a/src/stores/agent-store.ts +++ b/src/stores/agent-store.ts @@ -12,6 +12,7 @@ interface AgentState { fetch: () => Promise; updatePath: (name: string, path: string) => Promise; setEnabled: (name: string, enabled: boolean) => Promise; + setEnabledBulk: (names: string[], enabled: boolean) => Promise; reorderAgents: (orderedNames: string[]) => Promise; } @@ -71,6 +72,20 @@ export const useAgentStore = create((set, get) => ({ ); } }, + async setEnabledBulk(names: string[], enabled: boolean) { + if (names.length === 0) return; + try { + await Promise.all(names.map((n) => api.setAgentEnabled(n, enabled))); + const target = new Set(names); + set({ + agents: get().agents.map((a) => + target.has(a.name) ? { ...a, enabled } : a, + ), + }); + } catch { + toast.error(i18n.t("agents:toast.updateFailed", { agent: "" })); + } + }, async reorderAgents(orderedNames: string[]) { // Optimistic update const agents = get().agents; diff --git a/src/stores/ui-store.ts b/src/stores/ui-store.ts index 30775385..0c17761e 100644 --- a/src/stores/ui-store.ts +++ b/src/stores/ui-store.ts @@ -21,17 +21,42 @@ function getValidItem( : fallback; } +/** + * Reads a JSON string array from localStorage, falling back to an empty array + * if storage is unavailable, missing, or malformed. + */ +function getStoredStringArray(key: string): string[] { + if (typeof localStorage === "undefined") return []; + try { + const raw = localStorage.getItem(key); + if (!raw) return []; + const parsed = JSON.parse(raw); + return Array.isArray(parsed) && parsed.every((v) => typeof v === "string") + ? parsed + : []; + } catch { + return []; + } +} + interface UIState { sidebarOpen: boolean; themeName: ThemeName; mode: Mode; appIcon: AppIcon; agentVisibility: AgentVisibility; + /** + * Agents that "Detected only" visibility auto-disabled, so switching back to + * "All agents" can re-enable exactly those (and not agents the user disabled + * manually). Persisted so the restore survives a restart. + */ + autoDisabledAgents: string[]; toggleSidebar: () => void; setThemeName: (name: ThemeName) => void; setMode: (mode: Mode) => void; setAppIcon: (icon: AppIcon) => void; setAgentVisibility: (visibility: AgentVisibility) => void; + setAutoDisabledAgents: (names: string[]) => void; } const ALLOWED_MODES: readonly Mode[] = ["system", "dark", "light"]; @@ -54,6 +79,7 @@ const storedAgentVisibility = getValidItem( ALLOWED_AGENT_VISIBILITY, "all", ); +const storedAutoDisabledAgents = getStoredStringArray("hk-agent-auto-disabled"); /** Resolve "system" to actual light/dark based on OS preference */ export function resolveMode(mode: Mode): "dark" | "light" { @@ -69,6 +95,7 @@ export const useUIStore = create((set) => ({ mode: storedMode, appIcon: storedAppIcon, agentVisibility: storedAgentVisibility, + autoDisabledAgents: storedAutoDisabledAgents, toggleSidebar() { set((s) => ({ sidebarOpen: !s.sidebarOpen })); }, @@ -88,4 +115,11 @@ export const useUIStore = create((set) => ({ localStorage.setItem("hk-agent-visibility", agentVisibility); set({ agentVisibility }); }, + setAutoDisabledAgents(autoDisabledAgents) { + localStorage.setItem( + "hk-agent-auto-disabled", + JSON.stringify(autoDisabledAgents), + ); + set({ autoDisabledAgents }); + }, })); From 84e36e9bd0122196bc285eb370d3ef03e46742de Mon Sep 17 00:00:00 2001 From: RealZST Date: Mon, 8 Jun 2026 15:31:17 +0300 Subject: [PATCH 3/5] docs: mention Detected only visibility toggle in README Update the EN and zh-CN Overview captions to describe hiding/disabling undetected agents via the Detected only toggle, alongside the per-agent Enabled switch. Co-Authored-By: Claude Opus 4.8 (1M context) --- README.md | 2 +- README.zh-CN.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 3935bc6c..59e9ccaa 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@

- Every supported agent shows on the Overview by default, installed or not — to hide one, go to Settings → Agent Paths and click its "Enabled" toggle to switch it off. + Every supported agent shows on the Overview by default, installed or not. In Settings → Agent Paths, switch to "Detected only" to hide and disable undetected agents, or flip a single agent's "Enabled" toggle off.


diff --git a/README.zh-CN.md b/README.zh-CN.md index d699ea85..320761b3 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -31,7 +31,7 @@

- 「概览」页默认展示所有受支持的 Agent,无论是否已安装 —— 想隐藏某个 Agent,可在「设置 → 智能体路径」点它的「已启用」开关将其关闭。 + 「概览」页默认展示所有受支持的 Agent,无论是否已安装。在「设置 → 智能体路径」切到「仅已检测到」即可隐藏并禁用未检测到的 Agent,或单独关闭某个 Agent 的「已启用」开关。


From cdfae5953509089998e928f3cb4bf0713d53bf62 Mon Sep 17 00:00:00 2001 From: RealZST Date: Mon, 8 Jun 2026 15:56:39 +0300 Subject: [PATCH 4/5] fix(settings): polish Detected only locking and harden bulk toggle - Only lock undetected agents in Detected only mode; detected agents stay toggleable (disabling them is the user's call) - Drop hover darkening on locked toggles and show a clearer "switch to All agents" tooltip - Trim the visibility hint copy (remove the contradictory "hides") - setEnabledBulk: use Promise.allSettled so one failed/stale agent does not drop store updates for the rest, with a generic failure toast Co-Authored-By: Claude Opus 4.8 (1M context) --- src/lib/i18n/locales/en/agents.json | 3 ++- src/lib/i18n/locales/en/settings.json | 3 ++- src/lib/i18n/locales/zh/agents.json | 3 ++- src/lib/i18n/locales/zh/settings.json | 3 ++- src/pages/settings.tsx | 16 +++++++++++----- src/stores/agent-store.ts | 19 +++++++++++++------ 6 files changed, 32 insertions(+), 15 deletions(-) diff --git a/src/lib/i18n/locales/en/agents.json b/src/lib/i18n/locales/en/agents.json index 84a73f8f..b3a62802 100644 --- a/src/lib/i18n/locales/en/agents.json +++ b/src/lib/i18n/locales/en/agents.json @@ -46,7 +46,8 @@ "pathUpdateFailed": "Failed to update {{agent}} path", "agentEnabled": "{{agent}} enabled", "agentDisabled": "{{agent}} disabled", - "updateFailed": "Failed to update {{agent}}" + "updateFailed": "Failed to update {{agent}}", + "bulkUpdateFailed": "Failed to update some agents" }, "summary": { "title": "Extensions", diff --git a/src/lib/i18n/locales/en/settings.json b/src/lib/i18n/locales/en/settings.json index 9be541ad..217452c0 100644 --- a/src/lib/i18n/locales/en/settings.json +++ b/src/lib/i18n/locales/en/settings.json @@ -28,7 +28,8 @@ "visibilityAll": "All agents", "visibilityDetected": "Detected only", "visibilityToast": "Visibility: {{label}}", - "visibilityHint": "Detected only also hides and disables undetected agents.", + "visibilityHint": "Detected only disables undetected agents.", + "lockedHint": "Switch to \"All agents\" to change this.", "enabled": "Enabled", "disabled": "Disabled", "notDetected": "Not detected", diff --git a/src/lib/i18n/locales/zh/agents.json b/src/lib/i18n/locales/zh/agents.json index ddd1c5ed..2cfcde51 100644 --- a/src/lib/i18n/locales/zh/agents.json +++ b/src/lib/i18n/locales/zh/agents.json @@ -46,7 +46,8 @@ "pathUpdateFailed": "更新 {{agent}} 路径失败", "agentEnabled": "已启用 {{agent}}", "agentDisabled": "已禁用 {{agent}}", - "updateFailed": "更新 {{agent}} 失败" + "updateFailed": "更新 {{agent}} 失败", + "bulkUpdateFailed": "部分智能体更新失败" }, "summary": { "title": "扩展", diff --git a/src/lib/i18n/locales/zh/settings.json b/src/lib/i18n/locales/zh/settings.json index e5b301a4..159a911d 100644 --- a/src/lib/i18n/locales/zh/settings.json +++ b/src/lib/i18n/locales/zh/settings.json @@ -28,7 +28,8 @@ "visibilityAll": "全部智能体", "visibilityDetected": "仅已检测到", "visibilityToast": "可见性:{{label}}", - "visibilityHint": "切换“仅已检测到”会隐藏并禁用未检测到的智能体。", + "visibilityHint": "切换“仅已检测到”会禁用未检测到的智能体。", + "lockedHint": "切回“全部智能体”才能修改。", "enabled": "已启用", "disabled": "已禁用", "notDetected": "未检测到", diff --git a/src/pages/settings.tsx b/src/pages/settings.tsx index 1f91a945..42a95620 100644 --- a/src/pages/settings.tsx +++ b/src/pages/settings.tsx @@ -388,6 +388,10 @@ export default function SettingsPage() { {agentNames.map((agent) => { const info = agentMap.get(agent); const isEnabled = info?.enabled ?? true; + // In "Detected only" we auto-disable undetected agents, so lock + // their toggle — switch to "All agents" to change them. Detected + // agents stay toggleable: enabling/disabling them is the user's + // call. const locked = agentVisibility === "detected" && !(info?.detected ?? false); return ( @@ -401,15 +405,17 @@ export default function SettingsPage() {