Compare commits

...

1 Commits

Author SHA1 Message Date
J
c205a4ba5c fix(runtimes): show each agent's own CLI version in the runtime list
The per-agent "CLI" column rendered the shared multica daemon `cli_version`
from each runtime's metadata. That value is the version of the multica
daemon binary and is identical for every agent registered by one daemon, so
Claude / Codex / Gemini / Opencode all displayed the same number (e.g.
v0.3.17) even though each tool has its own version (#3838).

Each runtime already reports its own underlying CLI tool version in
`metadata.version` (e.g. "2.1.5 (Claude Code)", "codex-cli 0.118.0"). The
column now shows that. The multica daemon CLI version and its update prompt
stay where they belong — the machine meta strip and the detail page's
UpdateSection — so the per-row multica update arrow (which compared against
the latest multica release) and its now-unused i18n strings are removed.

MUL-3097

Co-authored-by: multica-agent <github@multica.ai>
2026-06-06 22:41:06 +08:00
7 changed files with 99 additions and 76 deletions

View File

@@ -262,8 +262,6 @@
"col_cli": "CLI",
"cost_delta_flat": "flat",
"cli_managed_badge": "Desktop",
"cli_update_available_aria": "Update available",
"cli_update_available_tooltip": "Update available: {{version}}",
"row_actions_aria": "Row actions",
"delete_action": "Delete",
"delete_permission_hint": "Only the runtime owner and workspace admins can delete this runtime",

View File

@@ -250,8 +250,6 @@
"col_cli": "CLI",
"cost_delta_flat": "変化なし",
"cli_managed_badge": "デスクトップ",
"cli_update_available_aria": "更新あり",
"cli_update_available_tooltip": "更新あり: {{version}}",
"row_actions_aria": "行の操作",
"delete_action": "削除",
"delete_permission_hint": "ランタイムの所有者とワークスペース管理者のみが、このランタイムを削除できます",

View File

@@ -262,8 +262,6 @@
"col_cli": "CLI",
"cost_delta_flat": "변화 없음",
"cli_managed_badge": "데스크톱",
"cli_update_available_aria": "업데이트 가능",
"cli_update_available_tooltip": "업데이트 가능: {{version}}",
"row_actions_aria": "행 작업",
"delete_action": "삭제",
"delete_permission_hint": "런타임 소유자와 워크스페이스 관리자만 이 런타임을 삭제할 수 있습니다",

View File

@@ -250,8 +250,6 @@
"col_cli": "CLI",
"cost_delta_flat": "持平",
"cli_managed_badge": "桌面端",
"cli_update_available_aria": "有可用更新",
"cli_update_available_tooltip": "有可用更新:{{version}}",
"row_actions_aria": "行操作",
"delete_action": "删除",
"delete_permission_hint": "只有运行时所有者和工作区管理员可以删除这个运行时",

View File

@@ -1,12 +1,7 @@
"use client";
import { useMemo, useState } from "react";
import {
ArrowUpCircle,
Globe,
MoreHorizontal,
Trash2,
} from "lucide-react";
import { Globe, MoreHorizontal, Trash2 } from "lucide-react";
import { toast } from "sonner";
import type { ColumnDef } from "@tanstack/react-table";
import { useQuery } from "@tanstack/react-query";
@@ -38,7 +33,6 @@ import {
computeCostInWindow,
formatLastSeen,
isSelfHealingRuntime,
isVersionNewer,
pctChange,
} from "../utils";
import { splitRuntimeName } from "./runtime-machines";
@@ -78,7 +72,6 @@ type RuntimesT = ReturnType<typeof useT<"runtimes">>["t"];
interface CreateColumnsArgs {
showOwner: boolean;
latestCliVersion: string | null;
wsId: string;
now: number;
t: RuntimesT;
@@ -86,7 +79,6 @@ interface CreateColumnsArgs {
export function createRuntimeColumns({
showOwner,
latestCliVersion,
wsId,
now,
t,
@@ -167,12 +159,7 @@ export function createRuntimeColumns({
id: "cli",
header: () => t(($) => $.list.col_cli),
size: COL_WIDTHS.cli,
cell: ({ row }) => (
<CliCell
runtime={row.original.runtime}
latestCliVersion={latestCliVersion}
/>
),
cell: ({ row }) => <CliCell runtime={row.original.runtime} />,
},
{
id: "actions",
@@ -373,60 +360,30 @@ function CostCell({ runtimeId }: { runtimeId: string }) {
);
}
function CliCell({
runtime,
latestCliVersion,
}: {
runtime: AgentRuntime;
latestCliVersion: string | null;
}) {
const { t } = useT("runtimes");
function CliCell({ runtime }: { runtime: AgentRuntime }) {
if (runtime.runtime_mode === "cloud") {
return <span className="text-xs text-muted-foreground/50"></span>;
}
const meta = runtime.metadata as Record<string, unknown> | null;
const cliVersion =
meta && typeof meta.cli_version === "string" ? meta.cli_version : null;
const launchedBy =
meta && typeof meta.launched_by === "string" ? meta.launched_by : null;
const isManaged = launchedBy === "desktop";
// `version` is the agent's own underlying CLI tool version — distinct per
// provider (e.g. "2.1.5 (Claude Code)", "codex-cli 0.118.0", "0.42.0").
// The separate `cli_version` is the shared multica daemon CLI, identical
// for every runtime on one machine; surfacing it here made all agents
// show the same number (#3838). The daemon CLI version and its update
// prompt belong to the machine — they live in the machine meta strip and
// the detail page's UpdateSection, not on a per-agent row.
const version =
meta && typeof meta.version === "string" ? meta.version : null;
if (!cliVersion) {
if (!version) {
return <span className="text-xs text-muted-foreground/50"></span>;
}
// Desktop-managed daemons can never self-update from this page (the
// Electron app ships and replaces the binary), so the upgrade marker
// would lie — suppress regardless of version comparison.
const hasUpdate =
!isManaged &&
!!latestCliVersion &&
isVersionNewer(latestCliVersion, cliVersion);
return (
<div className="flex min-w-0 items-center gap-1 text-xs">
<span
className={`truncate font-mono ${
hasUpdate ? "text-warning" : "text-muted-foreground"
}`}
>
{cliVersion}
<div className="flex min-w-0 items-center text-xs">
<span className="truncate font-mono text-muted-foreground">
{version}
</span>
{hasUpdate && latestCliVersion && (
<Tooltip>
<TooltipTrigger
render={
<ArrowUpCircle
className="h-3 w-3 shrink-0 text-warning"
aria-label={t(($) => $.list.cli_update_available_aria)}
/>
}
/>
<TooltipContent>
{t(($) => $.list.cli_update_available_tooltip, { version: latestCliVersion })}
</TooltipContent>
</Tooltip>
)}
</div>
);
}

View File

@@ -15,7 +15,6 @@ import {
agentListOptions,
memberListOptions,
} from "@multica/core/workspace/queries";
import { latestCliVersionOptions } from "@multica/core/runtimes";
import { agentTaskSnapshotOptions } from "@multica/core/agents";
import { paths, useWorkspaceSlug } from "@multica/core/paths";
import { DataTable } from "@multica/ui/components/ui/data-table";
@@ -76,10 +75,11 @@ export function RuntimeList({
now,
}: {
runtimes: AgentRuntime[];
// Kept on the API surface for callers the CLI column re-derives
// update state per row via metadata.cli_version + the GitHub-release
// query, so this prop is now unused. Left to avoid scope creep on the
// page-level wrapper that still computes the set.
// Kept on the API surface for callers, but unused here: the CLI column
// shows each agent's own tool version, while the multica daemon CLI
// update prompt lives at the machine/detail level (UpdateSection), so the
// table no longer derives per-row update state. Left to avoid scope creep
// on the page-level wrapper that still computes the set.
updatableIds?: Set<string>;
now: number;
}) {
@@ -94,7 +94,6 @@ export function RuntimeList({
const { data: agents = [] } = useQuery(agentListOptions(wsId));
const { data: members = [] } = useQuery(memberListOptions(wsId));
const { data: snapshot = [] } = useQuery(agentTaskSnapshotOptions(wsId));
const { data: latestCliVersion = null } = useQuery(latestCliVersionOptions());
const currentMember = user
? members.find((m) => m.user_id === user.id)
@@ -140,12 +139,11 @@ export function RuntimeList({
() =>
createRuntimeColumns({
showOwner,
latestCliVersion,
wsId,
now,
t,
}),
[showOwner, latestCliVersion, wsId, now, t],
[showOwner, wsId, now, t],
);
const table = useReactTable({

View File

@@ -121,7 +121,6 @@ function renderActionsCell(row: RuntimeRow) {
const { t } = useT("runtimes");
const columns = createRuntimeColumns({
showOwner: false,
latestCliVersion: null,
wsId: "ws-1",
now: Date.now(),
t,
@@ -187,3 +186,80 @@ describe("runtime list row menu", () => {
expect(screen.queryByLabelText("Row actions")).not.toBeInTheDocument();
});
});
// The CLI column lives in its own cell renderer; resolve it from
// createRuntimeColumns and render it in isolation, mirroring renderActionsCell.
function renderCliCell(row: RuntimeRow) {
const qc = new QueryClient({ defaultOptions: { queries: { retry: false } } });
function Harness() {
const { t } = useT("runtimes");
const columns = createRuntimeColumns({
showOwner: false,
wsId: "ws-1",
now: Date.now(),
t,
});
const cli = columns.find((c) => c.id === "cli");
if (!cli || typeof cli.cell !== "function") {
throw new Error("cli column missing or has no cell renderer");
}
const cell = cli.cell({
row: { original: row },
} as unknown as Parameters<typeof cli.cell>[0]);
return <>{cell}</>;
}
return render(
<I18nProvider locale="en" resources={TEST_RESOURCES}>
<QueryClientProvider client={qc}>
<Harness />
</QueryClientProvider>
</I18nProvider>,
);
}
describe("runtime list CLI column", () => {
beforeEach(() => vi.clearAllMocks());
// #3838: every agent showed the same number because the column rendered the
// shared multica daemon `cli_version`. It must instead show the agent's own
// tool version from `metadata.version`.
it("shows the agent's own CLI tool version, not the shared daemon version", () => {
renderCliCell(
makeRow(
makeRuntime({
runtime_mode: "local",
metadata: { version: "2.1.5 (Claude Code)", cli_version: "0.3.17" },
}),
),
);
expect(screen.getByText("2.1.5 (Claude Code)")).toBeInTheDocument();
expect(screen.queryByText("0.3.17")).not.toBeInTheDocument();
});
it("falls back to an em dash when the agent version is missing", () => {
renderCliCell(
makeRow(
makeRuntime({
runtime_mode: "local",
metadata: { cli_version: "0.3.17" },
}),
),
);
expect(screen.queryByText("0.3.17")).not.toBeInTheDocument();
expect(screen.getByText("—")).toBeInTheDocument();
});
it("renders an em dash for cloud runtimes", () => {
renderCliCell(
makeRow(
makeRuntime({
runtime_mode: "cloud",
metadata: { version: "2.1.5 (Claude Code)" },
}),
),
);
expect(screen.getByText("—")).toBeInTheDocument();
});
});