From 3c2cbb9b186d9cffd2dd21c467726db9e6f57398 Mon Sep 17 00:00:00 2001 From: J Date: Wed, 8 Jul 2026 16:51:14 +0800 Subject: [PATCH] fix(runtimes): always show machine header in picker; pre-fill rename with shared name only MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../agents/components/runtime-picker.tsx | 25 ++++++++------- .../components/runtime-machines.test.ts | 31 +++++++++++++++++++ .../runtimes/components/runtime-machines.ts | 2 +- .../runtimes/components/runtimes-page.tsx | 7 +++-- 4 files changed, 50 insertions(+), 15 deletions(-) diff --git a/packages/views/agents/components/runtime-picker.tsx b/packages/views/agents/components/runtime-picker.tsx index 0bba620768..9893b38b8c 100644 --- a/packages/views/agents/components/runtime-picker.tsx +++ b/packages/views/agents/components/runtime-picker.tsx @@ -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) => (
- {showGroupHeaders && ( -
- {machine.title} - - {t(($) => $.create_dialog.runtime_group_online, { - online: machine.onlineCount, - total: machine.runtimes.length, - })} - -
- )} + {/* 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. */} +
+ {machine.title} + + {t(($) => $.create_dialog.runtime_group_online, { + online: machine.onlineCount, + total: machine.runtimes.length, + })} + +
{machine.runtimes.map((device) => { const ownerMember = getOwnerMember(device.owner_id); const disabled = !isRuntimeUsableForUser( diff --git a/packages/views/runtimes/components/runtime-machines.test.ts b/packages/views/runtimes/components/runtime-machines.test.ts index 0218822222..b2887f338f 100644 --- a/packages/views/runtimes/components/runtime-machines.test.ts +++ b/packages/views/runtimes/components/runtime-machines.test.ts @@ -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(); + }); +}); diff --git a/packages/views/runtimes/components/runtime-machines.ts b/packages/views/runtimes/components/runtime-machines.ts index dcc1cc2463..bd123bac8b 100644 --- a/packages/views/runtimes/components/runtime-machines.ts +++ b/packages/views/runtimes/components/runtime-machines.ts @@ -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]; diff --git a/packages/views/runtimes/components/runtimes-page.tsx b/packages/views/runtimes/components/runtimes-page.tsx index e234c7978b..acff03dfed 100644 --- a/packages/views/runtimes/components/runtimes-page.tsx +++ b/packages/views/runtimes/components/runtimes-page.tsx @@ -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]);