Manage the built-in daemon from one place in settings (#1938)

* feat(settings): consolidate daemon management into the host page

The app-level Daemon section and the localhost host page rendered the
same daemon card in two places. The host page is now the single home:
the Host sidebar group leads with Overview, and a desktop-only "Enable
built-in daemon" option in the host switcher recovers a removed
localhost connection. Old settings/daemon deep links redirect
(COMPAT-tagged).

* fix(settings): hide daemon recovery option when the server id probe fails

Review feedback: !query.isLoading treated an errored IPC probe as
resolved, so the recovery affordance could show without knowing the
local server id. Replace the resolved-boolean hook with a single
discriminated state hook (loading | error | resolved) — errors hide the
affordance and fall back to the settings root in the redirect. Also
collapses the two query subscriptions per caller into one.

* fix(settings): keep daemon recovery reachable when no hosts remain

Review feedback: removing localhost as the only host takes the settings
sidebar down its no-host branch, which never mounts the host picker —
so the "Enable built-in daemon" option was unreachable in the most
common recovery case. The sidebar now owns the single hook call, passes
it to the picker, and renders the same action as a sidebar row next to
"Add host" when no hosts exist.

* test(e2e): update settings host header to Overview

Test drift: the host section's sidebar/header label was intentionally
renamed to "Overview"; the spec still asserted the old "Host" title.
Also assert the retired app-level Daemon row stays absent.
This commit is contained in:
Mohamed Boudra
2026-07-08 10:20:30 +02:00
committed by GitHub
parent 06c3161008
commit 4fb70668f2
19 changed files with 232 additions and 41 deletions
+1
View File
@@ -371,6 +371,7 @@ export async function expectRetiredSidebarSectionsAbsent(page: Page): Promise<vo
await expect(sidebar.getByRole("button", { name: "General", exact: true })).toBeVisible(); await expect(sidebar.getByRole("button", { name: "General", exact: true })).toBeVisible();
await expect(sidebar.getByRole("button", { name: "Diagnostics", exact: true })).toBeVisible(); await expect(sidebar.getByRole("button", { name: "Diagnostics", exact: true })).toBeVisible();
await expect(sidebar.getByRole("button", { name: "About", exact: true })).toBeVisible(); await expect(sidebar.getByRole("button", { name: "About", exact: true })).toBeVisible();
await expect(sidebar.getByRole("button", { name: "Daemon", exact: true })).toHaveCount(0);
// Host group rows are now flat top-level sections (no drill-in). // Host group rows are now flat top-level sections (no drill-in).
await expect(sidebar.getByTestId("settings-host-section-connections")).toBeVisible(); await expect(sidebar.getByTestId("settings-host-section-connections")).toBeVisible();
+1 -1
View File
@@ -64,7 +64,7 @@ test.describe("Settings host page", () => {
await openSettingsHost(page, serverId); await openSettingsHost(page, serverId);
await openHostSection(page, serverId, "host"); await openHostSection(page, serverId, "host");
await expectSettingsHeader(page, "Host"); await expectSettingsHeader(page, "Overview");
await expectHostLabelDisplayed(page); await expectHostLabelDisplayed(page);
await expectHostActionCards(page, serverId); await expectHostActionCards(page, serverId);
}); });
+42 -2
View File
@@ -1,7 +1,38 @@
import { useLocalSearchParams } from "expo-router"; import { Redirect, useLocalSearchParams } from "expo-router";
import { useMemo } from "react"; import { useMemo } from "react";
import { useHostRuntimeBootstrapState } from "@/app/_layout";
import { HostRouteBootstrapBoundary } from "@/components/host-route-bootstrap-boundary";
import { useLocalDaemonServerIdState } from "@/hooks/use-is-local-daemon";
import { useHosts } from "@/runtime/host-runtime";
import SettingsScreen from "@/screens/settings-screen"; import SettingsScreen from "@/screens/settings-screen";
import { isSettingsSectionSlug, type SettingsSectionSlug } from "@/utils/host-routes"; import { StartupSplashScreen } from "@/screens/startup-splash-screen";
import {
buildSettingsHostSectionRoute,
buildSettingsRoute,
isSettingsSectionSlug,
type SettingsSectionSlug,
} from "@/utils/host-routes";
// COMPAT(settingsDaemonRedirect): added 2026-07-08, remove after 2027-01-08.
function SettingsDaemonRedirect() {
const hosts = useHosts();
const localDaemon = useLocalDaemonServerIdState();
const bootstrapState = useHostRuntimeBootstrapState();
if (localDaemon.status === "loading") {
return <StartupSplashScreen bootstrapState={bootstrapState} />;
}
if (
localDaemon.status === "resolved" &&
localDaemon.serverId !== null &&
hosts.some((host) => host.serverId === localDaemon.serverId)
) {
return <Redirect href={buildSettingsHostSectionRoute(localDaemon.serverId, "host")} />;
}
return <Redirect href={buildSettingsRoute()} />;
}
export default function SettingsSectionRoute() { export default function SettingsSectionRoute() {
const params = useLocalSearchParams<{ section?: string; addHost?: string }>(); const params = useLocalSearchParams<{ section?: string; addHost?: string }>();
@@ -10,5 +41,14 @@ export default function SettingsSectionRoute() {
const openAddHostIntent = typeof params.addHost === "string" ? params.addHost : null; const openAddHostIntent = typeof params.addHost === "string" ? params.addHost : null;
const view = useMemo(() => ({ kind: "section" as const, section }), [section]); const view = useMemo(() => ({ kind: "section" as const, section }), [section]);
// COMPAT(settingsDaemonRedirect): added 2026-07-08, remove after 2027-01-08.
if (rawSection === "daemon") {
return (
<HostRouteBootstrapBoundary>
<SettingsDaemonRedirect />
</HostRouteBootstrapBoundary>
);
}
return <SettingsScreen view={view} openAddHostIntent={openAddHostIntent} />; return <SettingsScreen view={view} openAddHostIntent={openAddHostIntent} />;
} }
@@ -1,5 +1,6 @@
export const ADD_HOST_OPTION_ID = "__add_host__"; export const ADD_HOST_OPTION_ID = "__add_host__";
export const ALL_HOSTS_OPTION_ID = "__all_hosts__"; export const ALL_HOSTS_OPTION_ID = "__all_hosts__";
export const ENABLE_BUILT_IN_DAEMON_OPTION_ID = "__enable_built_in_daemon__";
export function getHostPickerLabel( export function getHostPickerLabel(
hosts: Array<{ label: string; serverId: string }>, hosts: Array<{ label: string; serverId: string }>,
@@ -11,10 +11,16 @@ import { orderHostsLocalFirst } from "@/types/host-connection";
import { import {
ADD_HOST_OPTION_ID, ADD_HOST_OPTION_ID,
ALL_HOSTS_OPTION_ID, ALL_HOSTS_OPTION_ID,
ENABLE_BUILT_IN_DAEMON_OPTION_ID,
getHostPickerLabel, getHostPickerLabel,
} from "./host-picker-constants"; } from "./host-picker-constants";
export { ADD_HOST_OPTION_ID, ALL_HOSTS_OPTION_ID, getHostPickerLabel }; export {
ADD_HOST_OPTION_ID,
ALL_HOSTS_OPTION_ID,
ENABLE_BUILT_IN_DAEMON_OPTION_ID,
getHostPickerLabel,
};
const SEARCHABLE_THRESHOLD = 10; const SEARCHABLE_THRESHOLD = 10;
type RenderHostOption = NonNullable<ComboboxProps["renderOption"]>; type RenderHostOption = NonNullable<ComboboxProps["renderOption"]>;
@@ -115,6 +121,12 @@ export function HostPickerOption({
); );
} }
const SYSTEM_HOST_PICKER_OPTION_LABELS: Record<"add" | "all" | "enableBuiltInDaemon", string> = {
add: "Add host",
all: "All hosts",
enableBuiltInDaemon: "Enable built-in daemon",
};
function SystemHostPickerOption({ function SystemHostPickerOption({
active, active,
selected, selected,
@@ -125,12 +137,12 @@ function SystemHostPickerOption({
active: boolean; active: boolean;
selected?: boolean; selected?: boolean;
onPress: () => void; onPress: () => void;
kind: "add" | "all"; kind: "add" | "all" | "enableBuiltInDaemon";
testID?: string; testID?: string;
}): ReactElement { }): ReactElement {
const { theme } = useUnistyles(); const { theme } = useUnistyles();
const Icon = kind === "add" ? Plus : Server; const Icon = kind === "add" ? Plus : Server;
const label = kind === "add" ? "Add host" : "All hosts"; const label = SYSTEM_HOST_PICKER_OPTION_LABELS[kind];
const leadingSlot = useMemo( const leadingSlot = useMemo(
() => <Icon size={theme.iconSize.sm} color={theme.colors.foregroundMuted} />, () => <Icon size={theme.iconSize.sm} color={theme.colors.foregroundMuted} />,
[Icon, theme.colors.foregroundMuted, theme.iconSize.sm], [Icon, theme.colors.foregroundMuted, theme.iconSize.sm],
@@ -158,6 +170,8 @@ export interface HostPickerProps {
includeAllHost?: boolean; includeAllHost?: boolean;
includeAddHost?: boolean; includeAddHost?: boolean;
onAddHost?: () => void; onAddHost?: () => void;
includeEnableBuiltInDaemon?: boolean;
onEnableBuiltInDaemon?: () => void;
showActiveConnection?: boolean; showActiveConnection?: boolean;
onOpenHostSettings?: (serverId: string) => void; onOpenHostSettings?: (serverId: string) => void;
searchable?: boolean; searchable?: boolean;
@@ -179,6 +193,8 @@ export function HostPicker({
includeAllHost, includeAllHost,
includeAddHost, includeAddHost,
onAddHost, onAddHost,
includeEnableBuiltInDaemon,
onEnableBuiltInDaemon,
showActiveConnection, showActiveConnection,
onOpenHostSettings, onOpenHostSettings,
searchable, searchable,
@@ -199,8 +215,13 @@ export function HostPicker({
const hostOptions = orderedHosts.map((host) => ({ id: host.serverId, label: host.label })); const hostOptions = orderedHosts.map((host) => ({ id: host.serverId, label: host.label }));
if (includeAllHost) hostOptions.unshift({ id: ALL_HOSTS_OPTION_ID, label: "All hosts" }); if (includeAllHost) hostOptions.unshift({ id: ALL_HOSTS_OPTION_ID, label: "All hosts" });
if (includeAddHost) hostOptions.push({ id: ADD_HOST_OPTION_ID, label: "Add host" }); if (includeAddHost) hostOptions.push({ id: ADD_HOST_OPTION_ID, label: "Add host" });
if (includeEnableBuiltInDaemon)
hostOptions.push({
id: ENABLE_BUILT_IN_DAEMON_OPTION_ID,
label: "Enable built-in daemon",
});
return hostOptions; return hostOptions;
}, [orderedHosts, includeAllHost, includeAddHost]); }, [orderedHosts, includeAllHost, includeAddHost, includeEnableBuiltInDaemon]);
const isSearchable = searchable === true && orderedHosts.length > SEARCHABLE_THRESHOLD; const isSearchable = searchable === true && orderedHosts.length > SEARCHABLE_THRESHOLD;
@@ -208,12 +229,14 @@ export function HostPicker({
(id: string) => { (id: string) => {
if (id === ADD_HOST_OPTION_ID) { if (id === ADD_HOST_OPTION_ID) {
onAddHost?.(); onAddHost?.();
} else if (id === ENABLE_BUILT_IN_DAEMON_OPTION_ID) {
onEnableBuiltInDaemon?.();
} else { } else {
onSelect(id); onSelect(id);
} }
onOpenChange(false); onOpenChange(false);
}, },
[onAddHost, onOpenChange, onSelect], [onAddHost, onEnableBuiltInDaemon, onOpenChange, onSelect],
); );
const handleOpenHostSettings = useCallback( const handleOpenHostSettings = useCallback(
@@ -246,6 +269,11 @@ export function HostPicker({
/> />
); );
} }
if (option.id === ENABLE_BUILT_IN_DAEMON_OPTION_ID) {
return (
<SystemHostPickerOption kind="enableBuiltInDaemon" active={active} onPress={onPress} />
);
}
return ( return (
<HostPickerOption <HostPickerOption
serverId={option.id} serverId={option.id}
@@ -34,6 +34,7 @@ interface UseBuiltInDaemonManagementInput {
interface UseBuiltInDaemonManagementResult { interface UseBuiltInDaemonManagementResult {
isUpdating: boolean; isUpdating: boolean;
toggle: () => void; toggle: () => void;
enable: () => Promise<DaemonManagementToggleResult | null>;
} }
export function useBuiltInDaemonManagement( export function useBuiltInDaemonManagement(
@@ -42,12 +43,16 @@ export function useBuiltInDaemonManagement(
const { t } = useTranslation(); const { t } = useTranslation();
const { daemonStatus, settings, updateSettings, setStatus, refreshStatus } = input; const { daemonStatus, settings, updateSettings, setStatus, refreshStatus } = input;
const reportError = useDesktopIpcErrorReporter(); const reportError = useDesktopIpcErrorReporter();
const { mutate: toggleDaemonManagement, isPending: isUpdating } = useMutation< const {
DaemonManagementToggleResult, mutate: toggleDaemonManagement,
Error mutateAsync: toggleDaemonManagementAsync,
>({ isPending: isUpdating,
mutationFn: async () => { } = useMutation<DaemonManagementToggleResult, Error, { forceEnable: boolean }>({
const wasManagingDaemon = settings.manageBuiltInDaemon; mutationFn: async ({ forceEnable }) => {
// forceEnable takes the enable branch regardless of the persisted
// setting — the recovery affordance must never reach the stop/confirm
// path even if manageBuiltInDaemon was left true.
const wasManagingDaemon = forceEnable ? false : settings.manageBuiltInDaemon;
try { try {
const result = await executeDaemonManagementToggle(wasManagingDaemon, daemonStatus, { const result = await executeDaemonManagementToggle(wasManagingDaemon, daemonStatus, {
confirm: () => confirm: () =>
@@ -109,8 +114,21 @@ export function useBuiltInDaemonManagement(
return; return;
} }
toggleDaemonManagement(); toggleDaemonManagement({ forceEnable: false });
}, [isUpdating, toggleDaemonManagement]); }, [isUpdating, toggleDaemonManagement]);
return { isUpdating, toggle }; const enable = useCallback(async () => {
if (isUpdating) {
return null;
}
try {
return await toggleDaemonManagementAsync({ forceEnable: true });
} catch {
// onError has already surfaced the failure; callers only act on success.
return null;
}
}, [isUpdating, toggleDaemonManagementAsync]);
return { isUpdating, toggle, enable };
} }
@@ -0,0 +1,46 @@
import { useCallback } from "react";
import { router } from "expo-router";
import { getIsElectron } from "@/constants/platform";
import { useLocalDaemonServerIdState } from "@/hooks/use-is-local-daemon";
import { useHosts } from "@/runtime/host-runtime";
import { useDesktopSettings } from "@/desktop/settings/desktop-settings";
import { useDaemonStatus } from "@/desktop/hooks/use-daemon-status";
import { useBuiltInDaemonManagement } from "@/desktop/hooks/use-built-in-daemon-management";
import { buildSettingsHostSectionRoute } from "@/utils/host-routes";
export interface EnableBuiltInDaemonOption {
visible: boolean;
onPress: () => void;
}
export function useEnableBuiltInDaemonOption(): EnableBuiltInDaemonOption {
const isElectron = getIsElectron();
const localDaemon = useLocalDaemonServerIdState();
const hosts = useHosts();
const { settings, updateSettings } = useDesktopSettings();
const { data, setStatus, refetch } = useDaemonStatus();
const { enable } = useBuiltInDaemonManagement({
daemonStatus: data?.status ?? null,
settings: settings.daemon,
updateSettings: (updates) => updateSettings({ daemon: updates }),
setStatus,
refreshStatus: refetch,
});
const isLocalhostConfigured =
localDaemon.status === "resolved" &&
localDaemon.serverId !== null &&
hosts.some((host) => host.serverId === localDaemon.serverId);
const visible = isElectron && localDaemon.status === "resolved" && !isLocalhostConfigured;
const onPress = useCallback(() => {
void (async () => {
const result = await enable();
if (result?.kind === "enabled") {
router.push(buildSettingsHostSectionRoute(result.newStatus.serverId, "host"));
}
})();
}, [enable]);
return { visible, onPress };
}
+28 -2
View File
@@ -15,10 +15,10 @@ async function loadDesktopDaemonServerId(): Promise<DesktopDaemonServerIdResult>
}; };
} }
export function useLocalDaemonServerId(): string | null { function useLocalDaemonServerIdQuery() {
const isDesktopApp = shouldUseDesktopDaemon(); const isDesktopApp = shouldUseDesktopDaemon();
const query = useQuery({ return useQuery({
queryKey: DESKTOP_DAEMON_SERVER_ID_QUERY_KEY, queryKey: DESKTOP_DAEMON_SERVER_ID_QUERY_KEY,
queryFn: loadDesktopDaemonServerId, queryFn: loadDesktopDaemonServerId,
enabled: isDesktopApp, enabled: isDesktopApp,
@@ -30,6 +30,11 @@ export function useLocalDaemonServerId(): string | null {
refetchOnWindowFocus: false, refetchOnWindowFocus: false,
retry: false, retry: false,
}); });
}
export function useLocalDaemonServerId(): string | null {
const isDesktopApp = shouldUseDesktopDaemon();
const query = useLocalDaemonServerIdQuery();
if (!isDesktopApp) { if (!isDesktopApp) {
return null; return null;
@@ -38,6 +43,27 @@ export function useLocalDaemonServerId(): string | null {
return query.data?.serverId ?? null; return query.data?.serverId ?? null;
} }
export type LocalDaemonServerIdState =
| { status: "loading" }
| { status: "error" }
| { status: "resolved"; serverId: string | null };
export function useLocalDaemonServerIdState(): LocalDaemonServerIdState {
const isDesktopApp = shouldUseDesktopDaemon();
const query = useLocalDaemonServerIdQuery();
if (!isDesktopApp) {
return { status: "resolved", serverId: null };
}
if (query.isError) {
return { status: "error" };
}
if (query.isSuccess) {
return { status: "resolved", serverId: query.data.serverId };
}
return { status: "loading" };
}
export function useIsLocalDaemon(serverId: string): boolean { export function useIsLocalDaemon(serverId: string): boolean {
const normalizedServerId = serverId.trim(); const normalizedServerId = serverId.trim();
const localServerId = useLocalDaemonServerId(); const localServerId = useLocalDaemonServerId();
+2 -2
View File
@@ -1404,6 +1404,7 @@ export const ar: TranslationResources = {
}, },
backToWorkspace: "خلف", backToWorkspace: "خلف",
addHost: "أضف مضيفًا", addHost: "أضف مضيفًا",
enableBuiltInDaemon: "تفعيل البرنامج الخفي المدمج",
projects: "المشاريع", projects: "المشاريع",
projectList: { projectList: {
hostLoadFailed: "تعذر تحميل المشاريع من المضيف{{hostName}}:{{message}}", hostLoadFailed: "تعذر تحميل المشاريع من المضيف{{hostName}}:{{message}}",
@@ -1412,7 +1413,6 @@ export const ar: TranslationResources = {
groupInfo: "حول{{title}}", groupInfo: "حول{{title}}",
sections: { sections: {
general: "عام", general: "عام",
daemon: "Daemon",
appearance: "مظهر", appearance: "مظهر",
shortcuts: "الاختصارات", shortcuts: "الاختصارات",
integrations: "التكامل", integrations: "التكامل",
@@ -1427,7 +1427,7 @@ export const ar: TranslationResources = {
providers: "مقدمي الخدمات", providers: "مقدمي الخدمات",
usage: "الاستخدام", usage: "الاستخدام",
terminals: "Terminals", terminals: "Terminals",
host: "Host", host: "نظرة عامة",
}, },
general: { general: {
title: "عام", title: "عام",
+2 -2
View File
@@ -1412,6 +1412,7 @@ export const en = {
}, },
backToWorkspace: "Back", backToWorkspace: "Back",
addHost: "Add host", addHost: "Add host",
enableBuiltInDaemon: "Enable built-in daemon",
projects: "Projects", projects: "Projects",
projectList: { projectList: {
hostLoadFailed: "Couldn't load projects from host {{hostName}}: {{message}}", hostLoadFailed: "Couldn't load projects from host {{hostName}}: {{message}}",
@@ -1420,7 +1421,6 @@ export const en = {
groupInfo: "About {{title}}", groupInfo: "About {{title}}",
sections: { sections: {
general: "General", general: "General",
daemon: "Daemon",
appearance: "Appearance", appearance: "Appearance",
shortcuts: "Shortcuts", shortcuts: "Shortcuts",
integrations: "Integrations", integrations: "Integrations",
@@ -1435,7 +1435,7 @@ export const en = {
providers: "Providers", providers: "Providers",
usage: "Usage", usage: "Usage",
terminals: "Terminals", terminals: "Terminals",
host: "Host", host: "Overview",
}, },
general: { general: {
title: "General", title: "General",
+2 -2
View File
@@ -1443,6 +1443,7 @@ export const es: TranslationResources = {
}, },
backToWorkspace: "Atrás", backToWorkspace: "Atrás",
addHost: "Agregar anfitrión", addHost: "Agregar anfitrión",
enableBuiltInDaemon: "Activar el demonio integrado",
projects: "Proyectos", projects: "Proyectos",
projectList: { projectList: {
hostLoadFailed: "No se pudieron cargar proyectos desde el host{{hostName}}:{{message}}", hostLoadFailed: "No se pudieron cargar proyectos desde el host{{hostName}}:{{message}}",
@@ -1451,7 +1452,6 @@ export const es: TranslationResources = {
groupInfo: "Acerca de{{title}}", groupInfo: "Acerca de{{title}}",
sections: { sections: {
general: "General", general: "General",
daemon: "Daemon",
appearance: "Apariencia", appearance: "Apariencia",
shortcuts: "Atajos", shortcuts: "Atajos",
integrations: "Integraciones", integrations: "Integraciones",
@@ -1466,7 +1466,7 @@ export const es: TranslationResources = {
providers: "Proveedores", providers: "Proveedores",
usage: "Uso", usage: "Uso",
terminals: "Terminals", terminals: "Terminals",
host: "Host", host: "Resumen",
}, },
general: { general: {
title: "General", title: "General",
+2 -2
View File
@@ -1446,6 +1446,7 @@ export const fr: TranslationResources = {
}, },
backToWorkspace: "Dos", backToWorkspace: "Dos",
addHost: "Ajouter un hôte", addHost: "Ajouter un hôte",
enableBuiltInDaemon: "Activer le démon intégré",
projects: "Projets", projects: "Projets",
projectList: { projectList: {
hostLoadFailed: "Impossible de charger les projets depuis l'hôte{{hostName}}:{{message}}", hostLoadFailed: "Impossible de charger les projets depuis l'hôte{{hostName}}:{{message}}",
@@ -1454,7 +1455,6 @@ export const fr: TranslationResources = {
groupInfo: "À propos de{{title}}", groupInfo: "À propos de{{title}}",
sections: { sections: {
general: "Général", general: "Général",
daemon: "Daemon",
appearance: "Apparence", appearance: "Apparence",
shortcuts: "Raccourcis", shortcuts: "Raccourcis",
integrations: "Intégrations", integrations: "Intégrations",
@@ -1469,7 +1469,7 @@ export const fr: TranslationResources = {
providers: "Fournisseurs", providers: "Fournisseurs",
usage: "Utilisation", usage: "Utilisation",
terminals: "Terminals", terminals: "Terminals",
host: "Host", host: "Aperçu",
}, },
general: { general: {
title: "Général", title: "Général",
+2 -2
View File
@@ -1421,6 +1421,7 @@ export const ja: TranslationResources = {
}, },
backToWorkspace: "戻る", backToWorkspace: "戻る",
addHost: "ホストを追加", addHost: "ホストを追加",
enableBuiltInDaemon: "組み込みデーモンを有効にする",
projects: "プロジェクト", projects: "プロジェクト",
projectList: { projectList: {
hostLoadFailed: "ホスト{{hostName}}からプロジェクトを読み込めませんでした: {{message}}", hostLoadFailed: "ホスト{{hostName}}からプロジェクトを読み込めませんでした: {{message}}",
@@ -1429,7 +1430,6 @@ export const ja: TranslationResources = {
groupInfo: "{{title}}について", groupInfo: "{{title}}について",
sections: { sections: {
general: "一般", general: "一般",
daemon: "デーモン",
appearance: "外観", appearance: "外観",
shortcuts: "ショートカット", shortcuts: "ショートカット",
integrations: "連携", integrations: "連携",
@@ -1444,7 +1444,7 @@ export const ja: TranslationResources = {
providers: "プロバイダー", providers: "プロバイダー",
usage: "使用状況", usage: "使用状況",
terminals: "ターミナル", terminals: "ターミナル",
host: "ホスト", host: "概要",
}, },
general: { general: {
title: "一般", title: "一般",
+2 -2
View File
@@ -1429,6 +1429,7 @@ export const ptBR: TranslationResources = {
}, },
backToWorkspace: "Voltar", backToWorkspace: "Voltar",
addHost: "Adicionar host", addHost: "Adicionar host",
enableBuiltInDaemon: "Ativar o daemon integrado",
projects: "Projetos", projects: "Projetos",
projectList: { projectList: {
hostLoadFailed: "Não foi possível carregar projetos do host {{hostName}}: {{message}}", hostLoadFailed: "Não foi possível carregar projetos do host {{hostName}}: {{message}}",
@@ -1437,7 +1438,6 @@ export const ptBR: TranslationResources = {
groupInfo: "Sobre {{title}}", groupInfo: "Sobre {{title}}",
sections: { sections: {
general: "Geral", general: "Geral",
daemon: "Daemon",
appearance: "Aparência", appearance: "Aparência",
shortcuts: "Atalhos", shortcuts: "Atalhos",
integrations: "Integrações", integrations: "Integrações",
@@ -1452,7 +1452,7 @@ export const ptBR: TranslationResources = {
providers: "Provedores", providers: "Provedores",
usage: "Uso", usage: "Uso",
terminals: "Terminais", terminals: "Terminais",
host: "Host", host: "Visão geral",
}, },
general: { general: {
title: "Geral", title: "Geral",
+2 -2
View File
@@ -1435,6 +1435,7 @@ export const ru: TranslationResources = {
}, },
backToWorkspace: "Назад", backToWorkspace: "Назад",
addHost: "Добавить хост", addHost: "Добавить хост",
enableBuiltInDaemon: "Включить встроенный демон",
projects: "Проекты", projects: "Проекты",
projectList: { projectList: {
hostLoadFailed: "Не удалось загрузить проекты с хоста{{hostName}}:{{message}}.", hostLoadFailed: "Не удалось загрузить проекты с хоста{{hostName}}:{{message}}.",
@@ -1443,7 +1444,6 @@ export const ru: TranslationResources = {
groupInfo: "О{{title}}", groupInfo: "О{{title}}",
sections: { sections: {
general: "Общий", general: "Общий",
daemon: "Daemon",
appearance: "Появление", appearance: "Появление",
shortcuts: "Ярлыки", shortcuts: "Ярлыки",
integrations: "Интеграции", integrations: "Интеграции",
@@ -1458,7 +1458,7 @@ export const ru: TranslationResources = {
providers: "Провайдеры", providers: "Провайдеры",
usage: "Использование", usage: "Использование",
terminals: "Terminals", terminals: "Terminals",
host: "Host", host: "Обзор",
}, },
general: { general: {
title: "Общий", title: "Общий",
+2 -2
View File
@@ -1387,6 +1387,7 @@ export const zhCN: TranslationResources = {
}, },
backToWorkspace: "返回", backToWorkspace: "返回",
addHost: "添加主机", addHost: "添加主机",
enableBuiltInDaemon: "启用内置 daemon",
projects: "项目", projects: "项目",
projectList: { projectList: {
hostLoadFailed: "无法从 Host {{hostName}} 加载 projects:{{message}}", hostLoadFailed: "无法从 Host {{hostName}} 加载 projects:{{message}}",
@@ -1395,7 +1396,6 @@ export const zhCN: TranslationResources = {
groupInfo: "关于 {{title}}", groupInfo: "关于 {{title}}",
sections: { sections: {
general: "通用", general: "通用",
daemon: "Daemon",
appearance: "外观", appearance: "外观",
shortcuts: "快捷键", shortcuts: "快捷键",
integrations: "集成", integrations: "集成",
@@ -1410,7 +1410,7 @@ export const zhCN: TranslationResources = {
providers: "Providers", providers: "Providers",
usage: "使用情况", usage: "使用情况",
terminals: "Terminals", terminals: "Terminals",
host: "Host", host: "概览",
}, },
general: { general: {
title: "通用", title: "通用",
+31 -6
View File
@@ -72,7 +72,6 @@ import { SegmentedControl } from "@/components/ui/segmented-control";
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem } from "@/components/ui/dropdown-menu"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem } from "@/components/ui/dropdown-menu";
import { DesktopPermissionsSection } from "@/desktop/components/desktop-permissions-section"; import { DesktopPermissionsSection } from "@/desktop/components/desktop-permissions-section";
import { IntegrationsSection } from "@/desktop/components/integrations-section"; import { IntegrationsSection } from "@/desktop/components/integrations-section";
import { LocalDaemonSection } from "@/desktop/components/desktop-updates-section";
import { isElectronRuntime } from "@/desktop/host"; import { isElectronRuntime } from "@/desktop/host";
import { useDesktopAppUpdater } from "@/desktop/updates/use-desktop-app-updater"; import { useDesktopAppUpdater } from "@/desktop/updates/use-desktop-app-updater";
import { formatVersionWithPrefix } from "@/desktop/updates/desktop-updates"; import { formatVersionWithPrefix } from "@/desktop/updates/desktop-updates";
@@ -100,6 +99,10 @@ import ProjectsScreen from "@/screens/projects-screen";
import ProjectSettingsScreen from "@/screens/project-settings-screen"; import ProjectSettingsScreen from "@/screens/project-settings-screen";
import { useIsCompactFormFactor } from "@/constants/layout"; import { useIsCompactFormFactor } from "@/constants/layout";
import { useLocalDaemonServerId } from "@/hooks/use-is-local-daemon"; import { useLocalDaemonServerId } from "@/hooks/use-is-local-daemon";
import {
type EnableBuiltInDaemonOption,
useEnableBuiltInDaemonOption,
} from "@/desktop/hooks/use-enable-built-in-daemon-option";
import { useWebScrollbarStyle } from "@/hooks/use-web-scrollbar-style"; import { useWebScrollbarStyle } from "@/hooks/use-web-scrollbar-style";
import { import {
buildOpenProjectRoute, buildOpenProjectRoute,
@@ -131,7 +134,6 @@ interface SidebarSectionItem {
const SIDEBAR_SECTION_ITEMS: SidebarSectionItem[] = [ const SIDEBAR_SECTION_ITEMS: SidebarSectionItem[] = [
{ id: "general", labelKey: "settings.sections.general", icon: Settings }, { id: "general", labelKey: "settings.sections.general", icon: Settings },
{ id: "daemon", labelKey: "settings.sections.daemon", icon: Server, desktopOnly: true },
{ id: "appearance", labelKey: "settings.sections.appearance", icon: Palette }, { id: "appearance", labelKey: "settings.sections.appearance", icon: Palette },
{ id: "shortcuts", labelKey: "settings.sections.shortcuts", icon: Keyboard, desktopOnly: true }, { id: "shortcuts", labelKey: "settings.sections.shortcuts", icon: Keyboard, desktopOnly: true },
{ {
@@ -157,13 +159,13 @@ interface HostSectionItem {
} }
const HOST_SECTION_ITEMS: HostSectionItem[] = [ const HOST_SECTION_ITEMS: HostSectionItem[] = [
{ id: "host", labelKey: "settings.hostSections.host", icon: Server },
{ id: "connections", labelKey: "settings.hostSections.connections", icon: Network }, { id: "connections", labelKey: "settings.hostSections.connections", icon: Network },
{ id: "agents", labelKey: "settings.hostSections.agents", icon: Bot }, { id: "agents", labelKey: "settings.hostSections.agents", icon: Bot },
{ id: "workspaces", labelKey: "settings.hostSections.workspaces", icon: FolderGit2 }, { id: "workspaces", labelKey: "settings.hostSections.workspaces", icon: FolderGit2 },
{ id: "providers", labelKey: "settings.hostSections.providers", icon: Boxes }, { id: "providers", labelKey: "settings.hostSections.providers", icon: Boxes },
{ id: "usage", labelKey: "settings.hostSections.usage", icon: Gauge }, { id: "usage", labelKey: "settings.hostSections.usage", icon: Gauge },
{ id: "terminals", labelKey: "settings.hostSections.terminals", icon: SquareTerminal }, { id: "terminals", labelKey: "settings.hostSections.terminals", icon: SquareTerminal },
{ id: "host", labelKey: "settings.hostSections.host", icon: Server },
]; ];
function renderHostSettingsContent( function renderHostSettingsContent(
@@ -893,6 +895,7 @@ interface HostPickerProps {
sortedHosts: HostProfile[]; sortedHosts: HostProfile[];
onSelectHost: (serverId: string) => void; onSelectHost: (serverId: string) => void;
onAddHost: () => void; onAddHost: () => void;
enableBuiltInDaemonOption: EnableBuiltInDaemonOption;
} }
/** /**
@@ -902,7 +905,13 @@ interface HostPickerProps {
* is using right now; an "Add host" row is always reachable from the list — * is using right now; an "Add host" row is always reachable from the list —
* even with a single host. * even with a single host.
*/ */
function HostPicker({ activeServerId, sortedHosts, onSelectHost, onAddHost }: HostPickerProps) { function HostPicker({
activeServerId,
sortedHosts,
onSelectHost,
onAddHost,
enableBuiltInDaemonOption,
}: HostPickerProps) {
const { t } = useTranslation(); const { t } = useTranslation();
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const triggerRef = useRef<View | null>(null); const triggerRef = useRef<View | null>(null);
@@ -932,6 +941,8 @@ function HostPicker({ activeServerId, sortedHosts, onSelectHost, onAddHost }: Ho
anchorRef={triggerRef} anchorRef={triggerRef}
includeAddHost includeAddHost
onAddHost={onAddHost} onAddHost={onAddHost}
includeEnableBuiltInDaemon={enableBuiltInDaemonOption.visible}
onEnableBuiltInDaemon={enableBuiltInDaemonOption.onPress}
showActiveConnection showActiveConnection
searchable={false} searchable={false}
title={t("settings.hostPicker.switchHost")} title={t("settings.hostPicker.switchHost")}
@@ -990,6 +1001,7 @@ function SettingsSidebar({
const localServerId = useLocalDaemonServerId(); const localServerId = useLocalDaemonServerId();
const sortedHosts = useSortedHosts(hosts, localServerId); const sortedHosts = useSortedHosts(hosts, localServerId);
const hasHosts = sortedHosts.length > 0; const hasHosts = sortedHosts.length > 0;
const enableBuiltInDaemonOption = useEnableBuiltInDaemonOption();
const isDesktopApp = isElectronRuntime(); const isDesktopApp = isElectronRuntime();
const items = SIDEBAR_SECTION_ITEMS.filter((item) => !item.desktopOnly || isDesktopApp); const items = SIDEBAR_SECTION_ITEMS.filter((item) => !item.desktopOnly || isDesktopApp);
const insets = useSafeAreaInsets(); const insets = useSafeAreaInsets();
@@ -1036,6 +1048,7 @@ function SettingsSidebar({
sortedHosts={sortedHosts} sortedHosts={sortedHosts}
onSelectHost={onSelectHost} onSelectHost={onSelectHost}
onAddHost={onAddHost} onAddHost={onAddHost}
enableBuiltInDaemonOption={enableBuiltInDaemonOption}
/> />
{HOST_SECTION_ITEMS.map((item) => ( {HOST_SECTION_ITEMS.map((item) => (
<SidebarHostSectionButton <SidebarHostSectionButton
@@ -1062,6 +1075,20 @@ function SettingsSidebar({
{t("settings.addHost")} {t("settings.addHost")}
</Text> </Text>
</Pressable> </Pressable>
{enableBuiltInDaemonOption.visible ? (
<Pressable
accessibilityRole="button"
accessibilityLabel={t("settings.enableBuiltInDaemon")}
onPress={enableBuiltInDaemonOption.onPress}
testID="settings-enable-built-in-daemon"
style={sidebarItemStyle}
>
<Server size={theme.iconSize.md} color={theme.colors.foregroundMuted} />
<Text style={sidebarStyles.label} numberOfLines={1}>
{t("settings.enableBuiltInDaemon")}
</Text>
</Pressable>
) : null}
</View> </View>
)} )}
</> </>
@@ -1387,8 +1414,6 @@ export default function SettingsScreen({ view, openAddHostIntent = null }: Setti
handleTerminalScrollbackLinesChange={handleTerminalScrollbackLinesChange} handleTerminalScrollbackLinesChange={handleTerminalScrollbackLinesChange}
/> />
); );
case "daemon":
return <LocalDaemonSection />;
case "appearance": case "appearance":
return <AppearanceSection />; return <AppearanceSection />;
case "shortcuts": case "shortcuts":
@@ -15,6 +15,7 @@ import {
decodeWorkspaceIdFromPathSegment, decodeWorkspaceIdFromPathSegment,
encodeFilePathForPathSegment, encodeFilePathForPathSegment,
encodeWorkspaceIdForPathSegment, encodeWorkspaceIdForPathSegment,
isSettingsSectionSlug,
normalizeHostSectionSlug, normalizeHostSectionSlug,
parseHostAgentRouteFromPathname, parseHostAgentRouteFromPathname,
parseHostWorkspaceOpenIntentFromPathname, parseHostWorkspaceOpenIntentFromPathname,
@@ -266,6 +267,12 @@ describe("host settings section slugs", () => {
}); });
}); });
describe("settings section slugs", () => {
it("no longer treats daemon as a valid app-level settings section", () => {
expect(isSettingsSectionSlug("daemon")).toBe(false);
});
});
describe("resolveKnownHostRoute", () => { describe("resolveKnownHostRoute", () => {
it("renders when the route host is still saved", () => { it("renders when the route host is still saved", () => {
expect( expect(
-1
View File
@@ -482,7 +482,6 @@ export function resolveKnownHostRoute(input: {
export const SETTINGS_SECTION_SLUGS = [ export const SETTINGS_SECTION_SLUGS = [
"general", "general",
"daemon",
"appearance", "appearance",
"shortcuts", "shortcuts",
"integrations", "integrations",