fix(runtimes): always show machine header in picker; pre-fill rename with shared name only

Testing + review follow-ups on #5087:

1. The create-agent picker hid the machine group header when there was only
   one machine (e.g. after a search narrowed to one), collapsing to a flat
   list. Always render the machine header so grouping stays consistent.

2. (Elon) RenameMachineDialog pre-filled from the first non-empty custom_name
   on the machine, but the machine title only uses a name when ALL runtimes
   share one (sharedCustomName). A lone per-runtime name would thus pre-fill as
   if it were the machine name — the same runtime-vs-machine confusion this PR
   set out to remove. Export sharedCustomName and use it for the pre-fill;
   otherwise pre-fill empty. Added direct unit tests.

Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
J
2026-07-08 16:51:14 +08:00
parent f895dc4a64
commit 3c2cbb9b18
4 changed files with 50 additions and 15 deletions

View File

@@ -72,7 +72,6 @@ export function RuntimePicker({
}, [filteredRuntimes, search, currentUserId]);
const showSearch = runtimes.length > SEARCH_THRESHOLD;
const showGroupHeaders = machines.length > 1;
const selectedRuntime =
runtimes.find((d) => d.id === selectedRuntimeId) ?? null;
@@ -210,17 +209,19 @@ export function RuntimePicker({
) : (
machines.map((machine) => (
<div key={machine.id}>
{showGroupHeaders && (
<div className="flex items-center justify-between gap-2 px-2 pb-0.5 pt-2 text-[11px] font-medium text-muted-foreground">
<span className="truncate">{machine.title}</span>
<span className="shrink-0 tabular-nums">
{t(($) => $.create_dialog.runtime_group_online, {
online: machine.onlineCount,
total: machine.runtimes.length,
})}
</span>
</div>
)}
{/* Always show the machine header — even when a search or a
single-machine workspace narrows it to one group — so the
grouping stays consistent instead of collapsing to a flat
list. */}
<div className="flex items-center justify-between gap-2 px-2 pb-0.5 pt-2 text-[11px] font-medium text-muted-foreground">
<span className="truncate">{machine.title}</span>
<span className="shrink-0 tabular-nums">
{t(($) => $.create_dialog.runtime_group_online, {
online: machine.onlineCount,
total: machine.runtimes.length,
})}
</span>
</div>
{machine.runtimes.map((device) => {
const ownerMember = getOwnerMember(device.owner_id);
const disabled = !isRuntimeUsableForUser(

View File

@@ -4,6 +4,7 @@ import {
buildRuntimeMachines,
filterRuntimeMachines,
runtimeMachineCounts,
sharedCustomName,
splitRuntimeName,
} from "./runtime-machines";
@@ -349,3 +350,33 @@ describe("splitRuntimeName", () => {
});
});
});
describe("sharedCustomName", () => {
it("returns the name when every runtime shares one non-empty custom_name", () => {
expect(
sharedCustomName([
makeRuntime({ id: "a", custom_name: "Bohan's MacBook" }),
makeRuntime({ id: "b", custom_name: "Bohan's MacBook" }),
]),
).toBe("Bohan's MacBook");
});
it("returns null when only some runtimes are named (a lone per-runtime name is not the machine name)", () => {
expect(
sharedCustomName([
makeRuntime({ id: "a", custom_name: "just this one" }),
makeRuntime({ id: "b", custom_name: null }),
]),
).toBeNull();
});
it("returns null when the names disagree, or the set is empty", () => {
expect(
sharedCustomName([
makeRuntime({ id: "a", custom_name: "Air" }),
makeRuntime({ id: "b", custom_name: "Pro" }),
]),
).toBeNull();
expect(sharedCustomName([])).toBeNull();
});
});

View File

@@ -274,7 +274,7 @@ function runtimeDeviceName(runtime: AgentRuntime): string | null {
// daemon, so a name shared by all of them is the machine's label. A one-off
// per-runtime rename (not shared) is deliberately ignored here so it can't
// masquerade as the whole machine's name.
function sharedCustomName(runtimes: AgentRuntime[]): string | null {
export function sharedCustomName(runtimes: AgentRuntime[]): string | null {
if (runtimes.length === 0) return null;
const names = runtimes.map((r) => r.custom_name?.trim() ?? "");
const first = names[0];

View File

@@ -44,6 +44,7 @@ import {
buildRuntimeMachines,
filterRuntimeMachines,
runtimeMachineCounts,
sharedCustomName,
type RuntimeMachine,
type RuntimeMachineFilter,
} from "./runtime-machines";
@@ -254,8 +255,10 @@ export function RuntimesPage({
? m.runtimes[0]
: m.runtimes.find((r) => r.owner_id === currentUserId);
if (!editable) return null;
const currentName =
m.runtimes.find((r) => r.custom_name?.trim())?.custom_name?.trim() ?? "";
// Pre-fill only a real machine name — the same rule the machine title uses
// (every runtime shares one non-empty custom_name). A lone per-runtime
// custom name must NOT masquerade as the machine's current name.
const currentName = sharedCustomName(m.runtimes) ?? "";
return { runtimeId: editable.id, currentName };
}, [selectedMachine, canManageProfiles, currentUserId]);