Fix settings host sections showing "host not found" when the local daemon is stopped (#1749)

The local daemon's serverId persists while the daemon is stopped, so it isn't
among the connected hosts. The settings host-section resolver fell back to it
without checking it was connected, resolving the section to an unknown id and
rendering "host not found".

Extract the fallback into resolveActiveHostServerId, which only uses a serverId
when it names a currently connected host (covering both the picker selection and
the local daemon), and add regression tests.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
维她命@
2026-07-21 04:48:34 +08:00
committed by GitHub
parent 345d8e5240
commit 8c92c7d423
3 changed files with 109 additions and 13 deletions

View File

@@ -56,7 +56,11 @@ import {
} from "@/hooks/use-settings";
import { useHostRuntimeIsConnected, useHosts } from "@/runtime/host-runtime";
import { useSessionStore } from "@/stores/session-store";
import { orderHostsLocalFirst, type HostProfile } from "@/types/host-connection";
import {
orderHostsLocalFirst,
resolveActiveHostServerId,
type HostProfile,
} from "@/types/host-connection";
import { TitlebarDragRegion } from "@/components/desktop/titlebar-drag-region";
import { WindowChromeRegion, WindowChromeSafeArea } from "@/utils/desktop-window";
import { confirmDialog } from "@/utils/confirm-dialog";
@@ -1144,15 +1148,6 @@ export default function SettingsScreen({ view, openAddHostIntent = null }: Setti
const [selectedSettingsHostServerId, setSelectedSettingsHostServerId] = useState<string | null>(
view.kind === "host" ? view.serverId : null,
);
const knownSelectedSettingsHostServerId = useMemo(() => {
if (!selectedSettingsHostServerId) {
return null;
}
return hosts.some((host) => host.serverId === selectedSettingsHostServerId)
? selectedSettingsHostServerId
: null;
}, [hosts, selectedSettingsHostServerId]);
useEffect(() => {
if (view.kind === "host") {
setSelectedSettingsHostServerId(view.serverId);
@@ -1160,11 +1155,16 @@ export default function SettingsScreen({ view, openAddHostIntent = null }: Setti
}, [view]);
// The host the four sections scope to: the host on the active view, otherwise
// the picker choice, otherwise the local daemon, otherwise the first host.
// the picker choice, otherwise the connected local daemon, otherwise the first host.
const activeHostServerId = useMemo(() => {
if (view.kind === "host") return view.serverId;
return knownSelectedSettingsHostServerId ?? localServerId ?? sortedHosts[0]?.serverId ?? null;
}, [view, knownSelectedSettingsHostServerId, localServerId, sortedHosts]);
return resolveActiveHostServerId({
selectedServerId: selectedSettingsHostServerId,
localServerId,
hosts,
orderedHosts: sortedHosts,
});
}, [view, selectedSettingsHostServerId, localServerId, hosts, sortedHosts]);
const handleSendBehaviorChange = useCallback(
(behavior: SendBehavior) => {

View File

@@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest";
import {
normalizeStoredHostProfile,
orderHostsLocalFirst,
resolveActiveHostServerId,
type HostProfile,
} from "./host-connection";
@@ -114,3 +115,75 @@ describe("normalizeStoredHostProfile", () => {
});
});
});
describe("resolveActiveHostServerId", () => {
it("uses the selected host when one is set", () => {
expect(
resolveActiveHostServerId({
selectedServerId: "srv_selected",
localServerId: "srv_local",
hosts: [makeHost("srv_local"), makeHost("srv_selected")],
orderedHosts: [makeHost("srv_local"), makeHost("srv_selected")],
}),
).toBe("srv_selected");
});
it("falls back to the local host when it is connected", () => {
expect(
resolveActiveHostServerId({
selectedServerId: null,
localServerId: "srv_local",
hosts: [makeHost("srv_local"), makeHost("srv_remote")],
orderedHosts: [makeHost("srv_local"), makeHost("srv_remote")],
}),
).toBe("srv_local");
});
it("skips a stopped local daemon and uses the first connected host", () => {
// Regression: a stopped local daemon's serverId persists but isn't in `hosts`.
// Falling back to it would resolve the section to an unknown id ("host not found").
expect(
resolveActiveHostServerId({
selectedServerId: null,
localServerId: "srv_local_stopped",
hosts: [makeHost("srv_remote")],
orderedHosts: [makeHost("srv_remote")],
}),
).toBe("srv_remote");
});
it("returns null when no hosts are connected", () => {
expect(
resolveActiveHostServerId({
selectedServerId: null,
localServerId: "srv_local_stopped",
hosts: [],
orderedHosts: [],
}),
).toBeNull();
});
it("ignores a selected host that is not connected", () => {
// A stale selection (e.g. the host was removed) must not be used unless it is
// currently connected, or the section resolves to an unknown id ("host not found").
expect(
resolveActiveHostServerId({
selectedServerId: "srv_stale_selection",
localServerId: null,
hosts: [makeHost("srv_remote")],
orderedHosts: [makeHost("srv_remote")],
}),
).toBe("srv_remote");
});
it("falls through a disconnected selection to the connected local host", () => {
expect(
resolveActiveHostServerId({
selectedServerId: "srv_stale_selection",
localServerId: "srv_local",
hosts: [makeHost("srv_local"), makeHost("srv_remote")],
orderedHosts: [makeHost("srv_local"), makeHost("srv_remote")],
}),
).toBe("srv_local");
});
});

View File

@@ -75,6 +75,29 @@ export function orderHostsLocalFirst<T extends { serverId: string }>(
return ordered;
}
/**
* Resolves which host a settings host section should target: the picker
* selection, else the local daemon, else the first connected host.
*
* Only a serverId that names a currently connected host is used. Both the
* selection and the local daemon can name a host that isn't connected (a stale
* selection, or a local daemon whose id persists in storage while it's stopped);
* using one would resolve the section to an unknown id and render "host not found".
*/
export function resolveActiveHostServerId(params: {
selectedServerId: string | null;
localServerId: string | null;
hosts: readonly { serverId: string }[];
orderedHosts: readonly { serverId: string }[];
}): string | null {
const { selectedServerId, localServerId, hosts, orderedHosts } = params;
const connected = (serverId: string | null): string | null =>
serverId && hosts.some((host) => host.serverId === serverId) ? serverId : null;
return (
connected(selectedServerId) ?? connected(localServerId) ?? orderedHosts[0]?.serverId ?? null
);
}
function hostConnectionEquals(left: HostConnection, right: HostConnection): boolean {
if (left.type !== right.type || left.id !== right.id) {
return false;