MUL-4781: keep machine CLI status visible

Co-authored-by: Eve <eve@multica-ai.local>
Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
Multica Eve
2026-07-15 13:35:44 +08:00
committed by GitHub
parent 6c9f58a2ca
commit bf64bb9214
7 changed files with 190 additions and 26 deletions

View File

@@ -21,6 +21,7 @@ function makeMachine(
subtitle: "x86_64 macOS",
deviceInfo: "dev.local · x86_64 macOS",
cliVersion: "1.0.0",
launchedBy: null,
mode: "local",
section: "local",
isCurrent: true,

View File

@@ -10,7 +10,11 @@ const mockUpdateSection = vi.hoisted(() => vi.fn());
vi.mock("./update-section", () => ({
UpdateSection: (props: Record<string, unknown>) => {
mockUpdateSection(props);
return <button type="button">Update</button>;
return props.runtimeId ? (
<button type="button">Update</button>
) : (
<span>CLI status</span>
);
},
}));
@@ -38,6 +42,11 @@ function runtime(overrides: Partial<AgentRuntime> = {}): AgentRuntime {
}
function machine(runtimes: AgentRuntime[]): RuntimeMachine {
const launchedBy = runtimes
.filter((item) => item.status === "online")
.map((item) => item.metadata?.launched_by)
.find((value): value is string => typeof value === "string");
return {
id: "local:daemon-1",
daemonId: "daemon-1",
@@ -45,6 +54,7 @@ function machine(runtimes: AgentRuntime[]): RuntimeMachine {
subtitle: null,
deviceInfo: "dev.local",
cliVersion: "0.3.17",
launchedBy: launchedBy ?? null,
mode: "local",
section: "remote",
isCurrent: false,
@@ -92,18 +102,31 @@ describe("MachineCliSection", () => {
});
});
it("shows read-only machine CLI version when the viewer owns no runtime", () => {
it("shows read-only machine CLI status when the viewer owns no runtime", () => {
render(
<MachineCliSection
machine={machine([runtime({ owner_id: "user-2" })])}
machine={machine([
runtime({
owner_id: "user-2",
metadata: {
cli_version: "0.3.17",
launched_by: "desktop",
},
}),
])}
currentUserId="user-1"
/>,
);
expect(screen.getByText("CLI 0.3.17")).toBeInTheDocument();
expect(screen.getByText("CLI status")).toBeInTheDocument();
expect(
screen.queryByRole("button", { name: "Update" }),
).not.toBeInTheDocument();
expect(mockUpdateSection).not.toHaveBeenCalled();
expect(mockUpdateSection).toHaveBeenCalledWith({
runtimeId: null,
currentVersion: "0.3.17",
isOnline: false,
launchedBy: "desktop",
});
});
});

View File

@@ -2,11 +2,6 @@ import type { AgentRuntime } from "@multica/core/types";
import type { RuntimeMachine } from "./runtime-machines";
import { UpdateSection } from "./update-section";
function launchedBy(runtime: AgentRuntime): string | null {
const value = runtime.metadata?.launched_by;
return typeof value === "string" && value ? value : null;
}
/**
* Pick one viewer-owned runtime as the command channel for a machine-wide
* daemon update. An online runtime wins so the daemon can receive the request
@@ -34,18 +29,30 @@ export function MachineCliSection({
}) {
const updateRuntime = machineUpdateRuntime(machine, currentUserId);
if (!updateRuntime) {
if (machine.mode !== "local") {
return machine.cliVersion ? (
<span className="font-mono">CLI {machine.cliVersion}</span>
) : null;
}
// A viewer's ability to send an update command must not gate the
// machine-level version and manager information. The only local machine
// without anything to report is Desktop's synthesized stopped-daemon row.
if (
!updateRuntime &&
machine.runtimes.length === 0 &&
!machine.cliVersion &&
!machine.launchedBy
) {
return null;
}
return (
<UpdateSection
runtimeId={updateRuntime.id}
runtimeId={updateRuntime?.id ?? null}
currentVersion={machine.cliVersion}
isOnline={updateRuntime.status === "online"}
launchedBy={launchedBy(updateRuntime)}
isOnline={updateRuntime?.status === "online"}
launchedBy={machine.launchedBy}
/>
);
}

View File

@@ -53,6 +53,30 @@ describe("runtime machine grouping", () => {
});
});
it("uses the online daemon CLI version instead of a stale offline report", () => {
const machines = buildRuntimeMachines(
[
makeRuntime({
id: "rt-online",
provider: "claude",
metadata: { cli_version: "0.4.0", launched_by: "desktop" },
}),
makeRuntime({
id: "rt-stale",
provider: "copilot",
status: "offline",
last_seen_at: new Date(NOW - 4 * 24 * 60 * 60_000).toISOString(),
metadata: { cli_version: "0.3.17", launched_by: "desktop" },
}),
],
{ now: NOW },
);
expect(machines).toHaveLength(1);
expect(machines[0]?.cliVersion).toBe("0.4.0");
expect(machines[0]?.launchedBy).toBe("desktop");
});
it("uses a machine-wide custom name as the machine title, over the local name", () => {
const machines = buildRuntimeMachines(
[

View File

@@ -17,6 +17,7 @@ export interface RuntimeMachine {
subtitle: string | null;
deviceInfo: string | null;
cliVersion: string | null;
launchedBy: string | null;
mode: AgentRuntime["runtime_mode"];
section: RuntimeMachineSection;
isCurrent: boolean;
@@ -133,6 +134,7 @@ function placeholderLocalMachine(
subtitle: null,
deviceInfo: null,
cliVersion: null,
launchedBy: null,
mode: "local",
section: "local",
isCurrent: true,
@@ -252,7 +254,8 @@ function finalizeRuntimeMachine(
title,
subtitle,
deviceInfo,
cliVersion: commonCliVersion(runtimes),
cliVersion: currentMachineMetadata(runtimes, "cli_version"),
launchedBy: currentMachineMetadata(runtimes, "launched_by"),
mode: draft.mode,
section: isCurrent ? "local" : draft.mode === "cloud" ? "cloud" : "remote",
isCurrent,
@@ -383,15 +386,28 @@ function latestLastSeenAt(runtimes: AgentRuntime[]): string | null {
return latest;
}
function commonCliVersion(runtimes: AgentRuntime[]): string | null {
const versions = new Set<string>();
for (const runtime of runtimes) {
const version = runtime.metadata?.cli_version;
if (typeof version === "string" && version.trim()) {
versions.add(version.trim());
}
function currentMachineMetadata(
runtimes: AgentRuntime[],
key: "cli_version" | "launched_by",
): string | null {
const online = runtimes.filter((runtime) => runtime.status === "online");
const candidates = online.length > 0 ? online : runtimes;
for (const runtime of candidates.toSorted(compareRuntimeReports)) {
const value = runtime.metadata?.[key];
if (typeof value === "string" && value.trim()) return value.trim();
}
return versions.size === 1 ? Array.from(versions)[0] ?? null : null;
return null;
}
function compareRuntimeReports(a: AgentRuntime, b: AgentRuntime): number {
return runtimeReportTime(b) - runtimeReportTime(a);
}
function runtimeReportTime(runtime: AgentRuntime): number {
const reportedAt = runtime.last_seen_at ?? runtime.updated_at;
const timestamp = Date.parse(reportedAt);
return Number.isNaN(timestamp) ? 0 : timestamp;
}
function shortDaemonId(daemonId: string): string {

View File

@@ -0,0 +1,92 @@
// @vitest-environment jsdom
import { afterEach, describe, expect, it, vi } from "vitest";
import { cleanup, render, screen } from "@testing-library/react";
import { I18nProvider } from "@multica/core/i18n/react";
import enCommon from "../../locales/en/common.json";
import enRuntimes from "../../locales/en/runtimes.json";
import { UpdateSection } from "./update-section";
const TEST_RESOURCES = { en: { common: enCommon, runtimes: enRuntimes } };
vi.mock("@multica/core/api", () => ({
api: {
initiateUpdate: vi.fn(),
getUpdateResult: vi.fn(),
},
}));
function renderSection(props: {
runtimeId: string | null;
launchedBy?: string | null;
currentVersion?: string;
}) {
return render(
<I18nProvider locale="en" resources={TEST_RESOURCES}>
<UpdateSection
runtimeId={props.runtimeId}
currentVersion={props.currentVersion ?? "v0.4.0"}
isOnline
launchedBy={props.launchedBy}
/>
</I18nProvider>,
);
}
afterEach(() => {
cleanup();
vi.unstubAllGlobals();
});
describe("UpdateSection read-only status", () => {
it("shows Latest without exposing an update action", async () => {
vi.stubGlobal(
"fetch",
vi.fn().mockResolvedValue({
ok: true,
json: async () => ({ tag_name: "v0.4.0" }),
}),
);
renderSection({ runtimeId: null });
expect(await screen.findByText("Latest")).toBeInTheDocument();
expect(
screen.queryByRole("button", { name: "Update" }),
).not.toBeInTheDocument();
});
it("shows the Desktop manager without exposing an update action", () => {
vi.stubGlobal(
"fetch",
vi.fn().mockResolvedValue({
ok: true,
json: async () => ({ tag_name: "v0.4.0" }),
}),
);
renderSection({ runtimeId: null, launchedBy: "desktop" });
expect(screen.getByText("Managed by Desktop")).toBeInTheDocument();
expect(
screen.queryByRole("button", { name: "Update" }),
).not.toBeInTheDocument();
});
it("shows an available version without an action for a read-only viewer", async () => {
vi.stubGlobal(
"fetch",
vi.fn().mockResolvedValue({
ok: true,
json: async () => ({ tag_name: "v0.4.0" }),
}),
);
renderSection({ runtimeId: null, currentVersion: "v0.3.17" });
expect(await screen.findByText("available")).toBeInTheDocument();
expect(
screen.queryByRole("button", { name: "Update" }),
).not.toBeInTheDocument();
});
});

View File

@@ -64,7 +64,8 @@ const statusConfig: Record<
};
interface UpdateSectionProps {
runtimeId: string;
/** Null for a read-only viewer who cannot use a runtime as the command channel. */
runtimeId: string | null;
currentVersion: string | null;
isOnline: boolean;
/**
@@ -128,7 +129,7 @@ export function UpdateSection({
}, [currentVersion, markCompleted, targetVersion, updating]);
const handleUpdate = async () => {
if (!latestVersion) return;
if (!latestVersion || !runtimeId) return;
cleanup();
setUpdating(true);
setTargetVersion(latestVersion);
@@ -212,7 +213,7 @@ export function UpdateSection({
</>
)}
{hasUpdate && isOnline && !status && (
{hasUpdate && runtimeId && isOnline && !status && (
<Button
variant="outline"
size="xs"