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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
</p>

<p align="center">
<small><i>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.</i></small>
<small><i>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.</i></small>
</p>

<br/>
Expand Down
2 changes: 1 addition & 1 deletion README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
</p>

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

<br/>
Expand Down
19 changes: 19 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import KitsPage from "./pages/kits";
import MarketplacePage from "./pages/marketplace";
import OverviewPage from "./pages/overview";
import SettingsPage from "./pages/settings";
import { useAgentStore } from "./stores/agent-store";
import { useAuditStore } from "./stores/audit-store";
import { useExtensionStore } from "./stores/extension-store";
import { resolveMode, useUIStore } from "./stores/ui-store";
Expand All @@ -36,6 +37,8 @@ export default function App() {
const [showConfetti, setShowConfetti] = useState(false);
const lastScanRef = useRef(0);
const appIcon = useUIStore((s) => s.appIcon);
const agents = useAgentStore((s) => s.agents);
const agentVisibility = useUIStore((s) => s.agentVisibility);

// Track resolved dark/light (reacts to OS changes when mode === "system")
const [resolved, setResolved] = useState<"dark" | "light">(() =>
Expand All @@ -53,6 +56,22 @@ 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.
useEffect(() => {
if (agentVisibility !== "detected") return;
const stray = 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])]);
}, [agents, agentVisibility]);

// Check for updates on startup (non-blocking, silent failure).
// Desktop uses Tauri's native updater; web mode polls GitHub releases.
useEffect(() => {
Expand Down
6 changes: 5 additions & 1 deletion src/components/agents/agent-detail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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("");

Expand Down Expand Up @@ -62,7 +64,9 @@ export function AgentDetail() {
if (!agent) {
return (
<div className="flex flex-1 items-center justify-center text-muted-foreground text-sm">
{t("detail.selectAgent")}
{agentVisibility === "detected" && !agentDetails.some((a) => a.detected)
? t("list.noDetected")
: t("detail.selectAgent")}
</div>
);
}
Expand Down
87 changes: 79 additions & 8 deletions src/components/agents/agent-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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(
() =>
Expand All @@ -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, {
Expand All @@ -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 (
<div className="flex flex-col gap-0.5 p-2">
<div className="px-3 py-2 text-[10px] font-semibold uppercase tracking-wider text-muted-foreground">
{t("list.header")}
</div>
<div className="px-3 py-6 text-xs text-muted-foreground text-center">
{t("list.noDetected")}
</div>
</div>
);
}

return (
<div className="flex flex-col gap-0.5 p-2">
<div className="px-3 py-2 text-[10px] font-semibold uppercase tracking-wider text-muted-foreground">
Expand All @@ -144,10 +215,10 @@ export function AgentList() {
onDragEnd={handleDragEnd}
>
<SortableContext
items={sorted.map((a) => a.name)}
items={visible.map((a) => a.name)}
strategy={verticalListSortingStrategy}
>
{sorted.map((agent) => (
{visible.map((agent) => (
<SortableAgentItem
key={agent.name}
agent={agent}
Expand Down
17 changes: 12 additions & 5 deletions src/components/extensions/extension-filters.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ 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 @@ -86,25 +87,31 @@ 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) => 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 (
<div className="space-y-2.5">
{/* Filters: kind pills + result count + dropdowns + search */}
Expand Down
6 changes: 4 additions & 2 deletions src/lib/i18n/locales/en/agents.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down Expand Up @@ -45,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",
Expand Down
5 changes: 5 additions & 0 deletions src/lib/i18n/locales/en/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@
"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 disables undetected agents.",
"lockedHint": "Switch to \"All agents\" to change this.",
"enabled": "Enabled",
"disabled": "Disabled",
"notDetected": "Not detected",
Expand Down
6 changes: 4 additions & 2 deletions src/lib/i18n/locales/zh/agents.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
{
"list": {
"header": "智能体",
"notDetected": "未检测到"
"notDetected": "未检测到",
"noDetected": "未检测到智能体"
},
"detail": {
"selectAgent": "选择一个智能体以查看其配置",
Expand Down Expand Up @@ -45,7 +46,8 @@
"pathUpdateFailed": "更新 {{agent}} 路径失败",
"agentEnabled": "已启用 {{agent}}",
"agentDisabled": "已禁用 {{agent}}",
"updateFailed": "更新 {{agent}} 失败"
"updateFailed": "更新 {{agent}} 失败",
"bulkUpdateFailed": "部分智能体更新失败"
},
"summary": {
"title": "扩展",
Expand Down
5 changes: 5 additions & 0 deletions src/lib/i18n/locales/zh/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@
"agentPaths": {
"section": "智能体路径",
"description": "下方显示自动检测到的路径。点击编辑按钮可选择自定义路径。",
"visibilityAll": "全部智能体",
"visibilityDetected": "仅已检测到",
"visibilityToast": "可见性:{{label}}",
"visibilityHint": "切换“仅已检测到”会禁用未检测到的智能体。",
"lockedHint": "切回“全部智能体”才能修改。",
"enabled": "已启用",
"disabled": "已禁用",
"notDetected": "未检测到",
Expand Down
10 changes: 8 additions & 2 deletions src/pages/overview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<AgentDetail[]>([]);
const [auditLoading, setAuditLoading] = useState(false);
Expand Down Expand Up @@ -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],
);

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