feat(agents): add runtime machine filter to Agents tab (MUL-2846)

Add a dropdown filter to the Agents tab toolbar that lets the user
narrow the list to agents bound to a specific runtime machine. The
filter reuses `buildRuntimeMachines` from the runtimes package so the
machine grouping (Local / Remote / Cloud) matches the Runtimes page
sidebar, and the per-machine agent counts respect the current scope
(Mine/All) so the numbers reflect what the user would see if they
clicked the row.

Only rendered in the Active view; the Archived view's toolbar is
unchanged. If the selected machine is GC'd while the user is on the
page (daemon stopped, runtime deleted), the filter auto-resets to
'All runtimes' instead of leaving the list empty. The no-matches state
now surfaces 'No agents on <machine>' when the machine filter is the
reason for zero results.

Adds new `runtime_filter` and `no_matches.runtime_filtered` /
`no_matches.search_runtime_filtered` i18n keys in en, zh-Hans, and
ko. 7 new unit tests in
`runtime-machine-filter-dropdown.test.tsx`.

Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
Lambda
2026-06-01 08:50:15 +08:00
parent 4ae4722ef0
commit 7071f33efa
6 changed files with 594 additions and 15 deletions

View File

@@ -48,18 +48,28 @@ import { CreateAgentDialog } from "./create-agent-dialog";
import { type AgentRow, createAgentColumns } from "./agent-columns";
import { useT } from "../../i18n";
import { matchesPinyin } from "../../editor/extensions/pinyin-match";
import {
buildRuntimeMachines,
type RuntimeMachine,
} from "../../runtimes/components/runtime-machines";
import { RuntimeMachineFilterDropdown } from "./runtime-machine-filter-dropdown";
// Filter axes:
//
// View = active vs archived dataset. Archived is low-frequency,
// accessed through a ghost link in the toolbar.
// Scope = ownership lens (All vs Mine). Layer-1 segment.
// Availability = "Can the agent take work right now?" — 3-state chip
// group (online / unstable / offline) sourced from
// AgentAvailability. The only chip filter we keep —
// the previous Workload axis was dropped because its
// "queued / failed / cancelled" buckets became
// meaningless once Failed left the workload model.
// View = active vs archived dataset. Archived is low-frequency,
// accessed through a ghost link in the toolbar.
// Scope = ownership lens (All vs Mine). Layer-1 segment.
// Runtime machine = "Which host is the agent bound to?" — dropdown
// filter grouped by section (Local / Remote / Cloud).
// Mirrors the machine grouping on the Runtimes page
// so a user can drill from a machine into the agents
// hosted on it.
// Availability = "Can the agent take work right now?" — 3-state chip
// group (online / unstable / offline) sourced from
// AgentAvailability. The only chip filter we keep —
// the previous Workload axis was dropped because its
// "queued / failed / cancelled" buckets became
// meaningless once Failed left the workload model.
type View = "active" | "archived";
type Scope = "all" | "mine";
type AvailabilityFilter = "all" | AgentAvailability;
@@ -107,6 +117,11 @@ export function AgentsPage() {
const setScope = useAgentsViewStore((s) => s.setScope);
const [availabilityFilter, setAvailabilityFilter] =
useState<AvailabilityFilter>("all");
// `null` means "all runtimes" (the default). When set, the value is a
// RuntimeMachine id from `buildRuntimeMachines` (the same grouping the
// Runtimes page uses), so the user can drill from a machine on that
// page into the agents bound to it.
const [runtimeMachineId, setRuntimeMachineId] = useState<string | null>(null);
const [sort, setSort] = useState<SortKey>("recent");
const [search, setSearch] = useState("");
const [showCreate, setShowCreate] = useState(false);
@@ -185,7 +200,71 @@ export function AgentsPage() {
return visibleInView.filter((a) => a.owner_id === currentUser.id);
}, [visibleInView, scope, currentUser, view]);
// Final cut — availability chip + search.
// Build the workspace's runtime machines (local / remote / cloud
// groupings) the same way the Runtimes page does, so the filter
// dropdown labels match the machines the user sees there. The
// `now` clock only affects health rollups — we don't render health
// chips in this list, so a stale snapshot is fine; a single derive
// per render is cheap and avoids pulling in a 30s tick on a page
// that doesn't show health.
const machines = useMemo(
() => buildRuntimeMachines(runtimes, { now: Date.now() }),
[runtimes],
);
// Reverse map: runtime_id → machine id. Lets the filter step look up
// an agent's machine in O(1). Built off the machine grouping rather
// than `runtimesById` so a runtime's machine identity matches the
// dropdown labels (machines dedupe across providers by daemon).
const runtimeIdToMachineId = useMemo(() => {
const m = new Map<string, string>();
for (const machine of machines) {
for (const r of machine.runtimes) m.set(r.id, machine.id);
}
return m;
}, [machines]);
// Per-machine agent counts in `inScope` — used both for the chip
// badges in the dropdown AND to make the runtime filter respect the
// current scope (e.g. "Mine" only shows machines that have one of
// my agents). Computed against `inScope` (not `visibleInView`) so the
// number next to "All" is exactly `inScope.length`.
const agentCountByMachine = useMemo(() => {
const counts = new Map<string, number>();
for (const a of inScope) {
const machineId = runtimeIdToMachineId.get(a.runtime_id);
if (!machineId) continue;
counts.set(machineId, (counts.get(machineId) ?? 0) + 1);
}
return counts;
}, [inScope, runtimeIdToMachineId]);
// If the selected machine is GC'd while we're on the page (daemon
// stopped, runtime deleted), the filter would zero out the list with
// no UI to clear it. Bounce back to "all" so the user always sees
// something actionable.
useEffect(() => {
if (
runtimeMachineId !== null &&
!machines.some((machine) => machine.id === runtimeMachineId)
) {
setRuntimeMachineId(null);
}
}, [runtimeMachineId, machines]);
// Resolved title for the current machine filter — used by the
// no-matches state so the user sees "No agents on `dev.local`" rather
// than a bare "No agents match this filter" when the search is empty
// but the machine filter is doing the narrowing.
const selectedMachine = useMemo(
() =>
runtimeMachineId === null
? null
: machines.find((machine) => machine.id === runtimeMachineId) ?? null,
[runtimeMachineId, machines],
);
// Final cut — availability chip + runtime machine + search.
const filteredAgents = useMemo(() => {
const q = search.trim().toLowerCase();
return inScope.filter((a) => {
@@ -195,6 +274,16 @@ export function AgentsPage() {
const detail = presenceMap.get(a.id);
if (detail?.availability !== availabilityFilter) return false;
}
// Runtime machine filter only applies to the Active view — the
// archived toolbar has no machine dropdown, so an archived agent
// would never get selected but might still slip through scope.
if (
view === "active" &&
runtimeMachineId !== null &&
runtimeIdToMachineId.get(a.runtime_id) !== runtimeMachineId
) {
return false;
}
if (q) {
if (
!a.name.toLowerCase().includes(q) &&
@@ -206,7 +295,15 @@ export function AgentsPage() {
}
return true;
});
}, [inScope, view, availabilityFilter, presenceMap, search]);
}, [
inScope,
view,
availabilityFilter,
presenceMap,
runtimeMachineId,
runtimeIdToMachineId,
search,
]);
// Per-availability counts for the chip badges. Computed against
// `inScope` (ignoring the availability filter itself) so the numbers
@@ -421,6 +518,10 @@ export function AgentsPage() {
totalCount={inScope.length}
archivedCount={archivedCount}
onShowArchived={() => setView("archived")}
machines={machines}
runtimeMachineId={runtimeMachineId}
onRuntimeMachineChange={setRuntimeMachineId}
agentCountByMachine={agentCountByMachine}
/>
<AvailabilityFilterRow
value={availabilityFilter}
@@ -439,7 +540,12 @@ export function AgentsPage() {
)}
{sortedAgents.length === 0 ? (
<NoMatches view={view} search={search} scope={scope} />
<NoMatches
view={view}
search={search}
scope={scope}
runtimeMachineTitle={selectedMachine?.title ?? null}
/>
) : (
<DataTable
table={table}
@@ -565,6 +671,10 @@ function ActiveToolbarRow({
totalCount,
archivedCount,
onShowArchived,
machines,
runtimeMachineId,
onRuntimeMachineChange,
agentCountByMachine,
}: {
scope: Scope;
setScope: (v: Scope) => void;
@@ -577,6 +687,10 @@ function ActiveToolbarRow({
totalCount: number;
archivedCount: number;
onShowArchived: () => void;
machines: RuntimeMachine[];
runtimeMachineId: string | null;
onRuntimeMachineChange: (id: string | null) => void;
agentCountByMachine: Map<string, number>;
}) {
const { t } = useT("agents");
return (
@@ -592,6 +706,12 @@ function ActiveToolbarRow({
</div>
<ScopeSegment scope={scope} setScope={setScope} counts={scopeCounts} />
<div className="ml-auto flex items-center gap-3">
<RuntimeMachineFilterDropdown
machines={machines}
value={runtimeMachineId}
onChange={onRuntimeMachineChange}
agentCountByMachine={agentCountByMachine}
/>
{archivedCount > 0 && (
<button
type="button"
@@ -849,24 +969,36 @@ function NoMatches({
view,
search,
scope,
runtimeMachineTitle,
}: {
view: View;
search: string;
scope: Scope;
runtimeMachineTitle: string | null;
}) {
const { t } = useT("agents");
const hasSearch = search.length > 0;
const hasFilter = scope === "mine";
const hasRuntimeFilter = runtimeMachineTitle !== null;
let body: string;
if (view === "archived") {
body = hasSearch
? t(($) => $.no_matches.search_archived, { query: search })
: t(($) => $.no_matches.no_archived);
} else if (hasSearch && hasRuntimeFilter) {
body = t(($) => $.no_matches.search_runtime_filtered, {
query: search,
machine: runtimeMachineTitle,
});
} else if (hasSearch) {
body = hasFilter
? t(($) => $.no_matches.search_active_filtered, { query: search })
: t(($) => $.no_matches.search_active, { query: search });
} else if (hasRuntimeFilter) {
body = t(($) => $.no_matches.runtime_filtered, {
machine: runtimeMachineTitle,
});
} else {
body = t(($) => $.no_matches.no_filter_match);
}

View File

@@ -0,0 +1,213 @@
// @vitest-environment jsdom
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { render, screen, fireEvent, cleanup } from "@testing-library/react";
import { I18nProvider } from "@multica/core/i18n/react";
import enCommon from "../../locales/en/common.json";
import enAgents from "../../locales/en/agents.json";
import type { RuntimeMachine } from "../../runtimes/components/runtime-machines";
import { RuntimeMachineFilterDropdown } from "./runtime-machine-filter-dropdown";
const TEST_RESOURCES = { en: { common: enCommon, agents: enAgents } };
function makeMachine(
overrides: Partial<RuntimeMachine> = {},
): RuntimeMachine {
return {
id: "machine-1",
daemonId: "daemon-1",
title: "dev.local",
subtitle: "x86_64 macOS",
deviceInfo: "dev.local · x86_64 macOS",
cliVersion: "1.0.0",
mode: "local",
section: "local",
isCurrent: true,
health: "online",
runtimes: [],
onlineCount: 1,
issueCount: 0,
runningCount: 0,
queuedCount: 0,
providerNames: ["claude"],
lastSeenAt: "2026-05-17T11:59:50Z",
...overrides,
};
}
function renderDropdown(
machines: RuntimeMachine[],
value: string | null,
onChange: (id: string | null) => void,
agentCountByMachine: Map<string, number>,
) {
const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false } },
});
return render(
<I18nProvider locale="en" resources={TEST_RESOURCES}>
<QueryClientProvider client={queryClient}>
<RuntimeMachineFilterDropdown
machines={machines}
value={value}
onChange={onChange}
agentCountByMachine={agentCountByMachine}
/>
</QueryClientProvider>
</I18nProvider>,
);
}
describe("RuntimeMachineFilterDropdown", () => {
beforeEach(() => vi.clearAllMocks());
// Base UI DropdownMenu renders the menu content into a portal on
// document.body, so leftover portals from a prior test would surface
// duplicate "All runtimes" / "LOCAL" labels. Wipe body between tests.
afterEach(() => {
cleanup();
document.body.innerHTML = "";
});
it("shows the All-runtimes label and total scope count when nothing is selected", () => {
const machines = [
makeMachine({ id: "m-local", title: "dev.local" }),
makeMachine({
id: "m-remote",
title: "build-server",
section: "remote",
isCurrent: false,
}),
];
const counts = new Map([
["m-local", 2],
["m-remote", 5],
]);
renderDropdown(machines, null, vi.fn(), counts);
// Trigger button uses the "All runtimes" label.
const trigger = screen.getByTestId("agents-runtime-filter");
expect(trigger.textContent).toContain("All runtimes");
// Sum across machines surfaces as the trigger count.
expect(trigger.textContent).toContain("7");
});
it("shows the selected machine's title and per-machine count in the trigger", () => {
const machines = [makeMachine({ id: "m-local", title: "dev.local" })];
const counts = new Map([["m-local", 4]]);
renderDropdown(machines, "m-local", vi.fn(), counts);
const trigger = screen.getByTestId("agents-runtime-filter");
expect(trigger.textContent).toContain("dev.local");
expect(trigger.textContent).toContain("4");
});
it("groups machines under their section headers in the menu", () => {
const machines = [
makeMachine({ id: "m-local", title: "dev.local", section: "local" }),
makeMachine({
id: "m-remote",
title: "build-server",
section: "remote",
isCurrent: false,
}),
makeMachine({
id: "m-cloud",
title: "Multica cloud",
section: "cloud",
isCurrent: false,
mode: "cloud",
}),
];
const counts = new Map([
["m-local", 1],
["m-remote", 2],
["m-cloud", 3],
]);
renderDropdown(machines, null, vi.fn(), counts);
fireEvent.click(screen.getByTestId("agents-runtime-filter"));
// Section labels render as plain text (uppercase is CSS-only).
expect(screen.getByText("Local")).toBeTruthy();
expect(screen.getByText("Remote")).toBeTruthy();
expect(screen.getByText("Cloud")).toBeTruthy();
// The menu items themselves also render.
expect(screen.getByText("dev.local")).toBeTruthy();
expect(screen.getByText("build-server")).toBeTruthy();
expect(screen.getByText("Multica cloud")).toBeTruthy();
});
it("fires onChange(null) when the All-runtimes row is clicked", () => {
const machines = [makeMachine({ id: "m-local", title: "dev.local" })];
const counts = new Map([["m-local", 1]]);
const onChange = vi.fn();
// Pre-select a machine so the "All runtimes" row is the one that
// gets the data-testid="agents-runtime-filter-active" marker.
renderDropdown(machines, "m-local", onChange, counts);
fireEvent.click(screen.getByTestId("agents-runtime-filter"));
const allRow = screen
.getByTestId("agents-runtime-filter-active")
.closest("button") as HTMLButtonElement;
expect(allRow).not.toBeNull();
// The "All runtimes" row sits at the top of the menu; fire a click
// on the explicit "All runtimes" text instead to make the assertion
// unambiguous.
const allRuntimesItem = Array.from(
document.querySelectorAll("button"),
).find(
(button) =>
button.textContent?.includes("All runtimes") &&
!button.hasAttribute("data-testid"),
);
expect(allRuntimesItem).toBeDefined();
fireEvent.click(allRuntimesItem as HTMLButtonElement);
expect(onChange).toHaveBeenCalledWith(null);
});
it("fires onChange(machineId) when a specific machine row is clicked", () => {
const machines = [
makeMachine({ id: "m-local", title: "dev.local", section: "local" }),
makeMachine({
id: "m-remote",
title: "build-server",
section: "remote",
isCurrent: false,
}),
];
const counts = new Map([
["m-local", 1],
["m-remote", 2],
]);
const onChange = vi.fn();
renderDropdown(machines, null, onChange, counts);
fireEvent.click(screen.getByTestId("agents-runtime-filter"));
fireEvent.click(screen.getByText("build-server"));
expect(onChange).toHaveBeenCalledWith("m-remote");
});
it("shows the per-machine count next to each item", () => {
const machines = [makeMachine({ id: "m-local", title: "dev.local" })];
const counts = new Map([["m-local", 7]]);
renderDropdown(machines, null, vi.fn(), counts);
fireEvent.click(screen.getByTestId("agents-runtime-filter"));
// The menu item renders the count via the i18n plural key.
const item = screen.getByText("dev.local").closest("button") as HTMLButtonElement;
expect(item.textContent).toMatch(/7/);
});
it("renders an empty-state hint when no machines exist", () => {
renderDropdown([], null, vi.fn(), new Map());
fireEvent.click(screen.getByTestId("agents-runtime-filter"));
expect(screen.getByText("No machines yet")).toBeTruthy();
});
});

View File

@@ -0,0 +1,193 @@
import { useMemo } from "react";
import { ChevronDown, Server } from "lucide-react";
import { Button } from "@multica/ui/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuTrigger,
} from "@multica/ui/components/ui/dropdown-menu";
import type { RuntimeMachine, RuntimeMachineSection } from "../../runtimes/components/runtime-machines";
import { useT } from "../../i18n";
// ---------------------------------------------------------------------------
// Runtime machine filter — dropdown next to the search input. The trigger
// shows the active machine's title (or "All runtimes"); the menu groups
// machines by section (Local / Remote / Cloud) the same way the
// Runtimes page sidebar does, so a user moving between the two pages
// sees consistent labels and counts.
// ---------------------------------------------------------------------------
const RUNTIME_MACHINE_SECTIONS: RuntimeMachineSection[] = [
"local",
"remote",
"cloud",
];
export function RuntimeMachineFilterDropdown({
machines,
value,
onChange,
agentCountByMachine,
}: {
machines: RuntimeMachine[];
value: string | null;
onChange: (id: string | null) => void;
agentCountByMachine: Map<string, number>;
}) {
const { t } = useT("agents");
const selected =
value === null
? null
: machines.find((machine) => machine.id === value) ?? null;
// Total count of agents in the current scope, regardless of which
// machine they're on. Used as the trailing count on the "All runtimes"
// entry so the user can see what they'd see if they cleared the filter.
const totalAgentCount = useMemo(() => {
let total = 0;
for (const count of agentCountByMachine.values()) total += count;
return total;
}, [agentCountByMachine]);
const triggerLabel = selected ? selected.title : t(($) => $.runtime_filter.all);
// Always show a count, even when the trigger is "All runtimes" — keeps
// the affordance scannable next to the other toolbar controls.
const triggerCount = selected
? (agentCountByMachine.get(selected.id) ?? 0)
: totalAgentCount;
return (
<DropdownMenu>
<DropdownMenuTrigger
render={
<Button
variant="outline"
size="sm"
className="h-8 gap-1.5 px-2 text-xs"
data-testid="agents-runtime-filter"
/>
}
>
<Server className="h-3 w-3 text-muted-foreground" />
<span className="max-w-[12rem] truncate">{triggerLabel}</span>
<span className="font-mono tabular-nums text-muted-foreground/70">
{triggerCount}
</span>
<ChevronDown className="h-3 w-3 text-muted-foreground/60" />
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-72 p-0">
<RuntimeMachineFilterMenu
machines={machines}
value={value}
onChange={onChange}
totalAgentCount={totalAgentCount}
agentCountByMachine={agentCountByMachine}
/>
</DropdownMenuContent>
</DropdownMenu>
);
}
function RuntimeMachineFilterMenu({
machines,
value,
onChange,
totalAgentCount,
agentCountByMachine,
}: {
machines: RuntimeMachine[];
value: string | null;
onChange: (id: string | null) => void;
totalAgentCount: number;
agentCountByMachine: Map<string, number>;
}) {
const { t } = useT("agents");
// Group machines by section while preserving the order
// `buildRuntimeMachines` already sorts them by (section, online count,
// title). We iterate the section list and slice to keep that order.
const grouped = useMemo(() => {
const result: Array<{
section: RuntimeMachineSection;
machines: RuntimeMachine[];
}> = [];
for (const section of RUNTIME_MACHINE_SECTIONS) {
const inSection = machines.filter((machine) => machine.section === section);
if (inSection.length > 0) result.push({ section, machines: inSection });
}
return result;
}, [machines]);
return (
<div className="max-h-80 overflow-y-auto py-1">
<RuntimeMachineFilterItem
active={value === null}
onClick={() => onChange(null)}
label={t(($) => $.runtime_filter.all)}
count={totalAgentCount}
/>
{grouped.map((group) => (
<div key={group.section}>
<div className="flex items-center gap-2 px-3 pb-1 pt-3 text-[10px] font-medium uppercase tracking-wider text-muted-foreground">
<span>{t(($) => $.runtime_filter[`section_${group.section}`])}</span>
<span className="h-px flex-1 bg-border" />
</div>
{group.machines.map((machine) => (
<RuntimeMachineFilterItem
key={machine.id}
active={value === machine.id}
onClick={() => onChange(machine.id)}
label={machine.title}
subtitle={machine.subtitle}
count={agentCountByMachine.get(machine.id) ?? 0}
/>
))}
</div>
))}
{machines.length === 0 && (
<div className="px-3 py-2 text-xs text-muted-foreground">
{t(($) => $.runtime_filter.empty)}
</div>
)}
</div>
);
}
function RuntimeMachineFilterItem({
active,
onClick,
label,
subtitle,
count,
}: {
active: boolean;
onClick: () => void;
label: string;
subtitle?: string | null;
count: number;
}) {
const { t } = useT("agents");
return (
<button
type="button"
onClick={onClick}
data-active={active || undefined}
data-testid={active ? "agents-runtime-filter-active" : undefined}
className={`flex w-full items-center gap-2 px-3 py-1.5 text-left text-xs transition-colors ${
active
? "bg-accent text-accent-foreground"
: "text-foreground hover:bg-muted/60"
}`}
>
<span className="min-w-0 flex-1 truncate">
<span className="block truncate font-medium">{label}</span>
{subtitle && (
<span className="block truncate text-[11px] font-normal text-muted-foreground">
{subtitle}
</span>
)}
</span>
<span className="font-mono tabular-nums text-muted-foreground/70">
{t(($) => $.runtime_filter.agent_count, { count })}
</span>
</button>
);
}

View File

@@ -46,7 +46,21 @@
"no_archived": "No archived agents yet.",
"search_active": "No agents match \"{{query}}\".",
"search_active_filtered": "No agents match \"{{query}}\" in this filter.",
"no_filter_match": "No agents match this filter."
"no_filter_match": "No agents match this filter.",
"search_runtime_filtered": "No agents on \"{{machine}}\" match \"{{query}}\".",
"runtime_filtered": "No agents on \"{{machine}}\"."
},
"runtime_filter": {
"all": "All runtimes",
"all_description": "Agents on any machine",
"section_local": "Local",
"section_remote": "Remote",
"section_cloud": "Cloud",
"agent_count_one": "{{count}} agent",
"agent_count_other": "{{count}} agents",
"this_machine": "This machine",
"empty": "No machines yet",
"reset": "All runtimes"
},
"columns": {
"agent": "Agent",

View File

@@ -46,7 +46,21 @@
"no_archived": "아직 보관된 에이전트가 없습니다.",
"search_active": "\"{{query}}\"와 일치하는 에이전트가 없습니다.",
"search_active_filtered": "이 필터에서 \"{{query}}\"와 일치하는 에이전트가 없습니다.",
"no_filter_match": "이 필터와 일치하는 에이전트가 없습니다."
"no_filter_match": "이 필터와 일치하는 에이전트가 없습니다.",
"search_runtime_filtered": "\"{{machine}}\"에서 \"{{query}}\"와 일치하는 에이전트가 없습니다.",
"runtime_filtered": "\"{{machine}}\"에 에이전트가 없습니다."
},
"runtime_filter": {
"all": "모든 런타임",
"all_description": "모든 기기의 에이전트",
"section_local": "로컬",
"section_remote": "원격",
"section_cloud": "클라우드",
"agent_count_one": "에이전트 {{count}}개",
"agent_count_other": "에이전트 {{count}}개",
"this_machine": "이 기기",
"empty": "아직 기기가 없습니다",
"reset": "모든 런타임"
},
"columns": {
"agent": "에이전트",

View File

@@ -46,7 +46,20 @@
"no_archived": "还没有已归档智能体。",
"search_active": "没有智能体匹配\"{{query}}\"。",
"search_active_filtered": "在该筛选下没有智能体匹配\"{{query}}\"。",
"no_filter_match": "该筛选下没有匹配的智能体。"
"no_filter_match": "该筛选下没有匹配的智能体。",
"search_runtime_filtered": "\"{{machine}}\"上没有匹配\"{{query}}\"的智能体。",
"runtime_filtered": "\"{{machine}}\"上还没有智能体。"
},
"runtime_filter": {
"all": "全部运行时",
"all_description": "所有机器上的智能体",
"section_local": "本机",
"section_remote": "远程",
"section_cloud": "云端",
"agent_count_other": "{{count}} 个智能体",
"this_machine": "本机",
"empty": "还没有机器",
"reset": "全部运行时"
},
"columns": {
"agent": "智能体",