mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-30 05:02:58 +00:00
Remove the out-of-date daemon sidebar notice (#8133)
The notice arrived with the terminal worker change (#7644) but is a separate product change. Remove it with the native/compact callout filtering it needed, restoring the sidebar callout slot to web-only. Restart and close terminals in Settings stays.
This commit is contained in:
+1
-1
@@ -203,7 +203,7 @@ Inline errors are a single sentence in `palette.red[300]` `xs`, sitting under th
|
||||
|
||||
Page-level alerts — informational notices, success confirmations, warnings, or recoverable errors that need a small visible block on the page — use `<Alert>` (`packages/app/src/components/ui/alert.tsx`). Variants: `default`, `info`, `success`, `warning`, `error`. The chrome is quiet by design: a 1px tinted border, transparent background, a small variant-tinted icon, the title in the variant accent, the description in `foregroundMuted`. Actions go in the `children` slot as `<Button variant="outline" size="sm">` — recovery actions are low-frequency and outline keeps them quiet alongside the alert's accent (`packages/app/src/screens/project-settings-screen.tsx`). One `<Alert>` at a time per region.
|
||||
|
||||
Sidebar callouts — cross-cutting alerts that apply across the whole app, like worktree setup, Rosetta install, and desktop update available — register through `useSidebarCallouts()` and render in the left sidebar via `<SidebarCallout>` (`packages/app/src/components/sidebar-callout.tsx`). The chrome (top-border-only, full-width action buttons) is tuned for that ~280px column. Native apps and every compact sidebar show only callouts registered with `native: true`; leave it unset for callouts about the desktop app. Canonical sources: `packages/app/src/components/worktree-setup-callout-source.tsx`, `packages/app/src/desktop/updates/rosetta-callout-source.tsx`, `packages/app/src/desktop/updates/update-callout-source.tsx`. Never import `<SidebarCallout>` into a page — that's what `<Alert>` is for.
|
||||
Sidebar callouts — cross-cutting alerts that apply across the whole app, like worktree setup, Rosetta install, and desktop update available — register through `useSidebarCallouts()` and render in the left sidebar via `<SidebarCallout>` (`packages/app/src/components/sidebar-callout.tsx`). The chrome (top-border-only, full-width action buttons) is tuned for that ~280px column. Canonical sources: `packages/app/src/components/worktree-setup-callout-source.tsx`, `packages/app/src/desktop/updates/rosetta-callout-source.tsx`, `packages/app/src/desktop/updates/update-callout-source.tsx`. Never import `<SidebarCallout>` into a page — that's what `<Alert>` is for.
|
||||
|
||||
Imperative errors are `Alert.alert("Error", "Unable to ...")` (the React Native `Alert` API, not this component) for failures that interrupt the flow and have no place on the page.
|
||||
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import type { Page } from "@playwright/test";
|
||||
import { expect, metroTest as test } from "../support/fixtures";
|
||||
import { gotoAppShell } from "../support/helpers/app";
|
||||
import { buildSeededHost } from "../support/helpers/daemon-registry";
|
||||
import { startIsolatedHostDaemon } from "../support/helpers/isolated-host-daemon";
|
||||
|
||||
// 0.8.0 predates terminalsSurviveRestart and supports daemonSelfUpdate.
|
||||
const PUBLISHED_DAEMON_VERSION = "0.8.0";
|
||||
const COMPACT = { width: 390, height: 844 };
|
||||
|
||||
// Opens the app against a published 0.8.0 daemon, with the developer daemon blocked.
|
||||
async function withOutdatedDaemon(page: Page, run: () => Promise<void>): Promise<void> {
|
||||
const serverId = `srv_daemon_outdated_${randomUUID().replaceAll("-", "").slice(0, 12)}`;
|
||||
const daemon = await startIsolatedHostDaemon(serverId, {
|
||||
publishedVersion: PUBLISHED_DAEMON_VERSION,
|
||||
});
|
||||
|
||||
try {
|
||||
const host = buildSeededHost({
|
||||
serverId,
|
||||
endpoint: `127.0.0.1:${daemon.port}`,
|
||||
nowIso: new Date().toISOString(),
|
||||
});
|
||||
await page.route(/:6767\b/, (route) => route.abort());
|
||||
await page.routeWebSocket(/:6767\b/, async (webSocket) => {
|
||||
await webSocket.close({ code: 1008, reason: "Blocked developer daemon during e2e." });
|
||||
});
|
||||
await page.addInitScript(
|
||||
({ seededHost }) => {
|
||||
localStorage.setItem("@paseo:e2e", "1");
|
||||
localStorage.setItem("@paseo:daemon-registry", JSON.stringify([seededHost]));
|
||||
},
|
||||
{ seededHost: host },
|
||||
);
|
||||
|
||||
await gotoAppShell(page);
|
||||
await run();
|
||||
} finally {
|
||||
await daemon.close();
|
||||
}
|
||||
}
|
||||
|
||||
async function expectOutdatedNotice(page: Page): Promise<void> {
|
||||
const callout = page.getByTestId("daemon-outdated-callout").first();
|
||||
await expect(callout).toBeVisible({ timeout: 30_000 });
|
||||
await expect(callout).toContainText("Daemon out of date");
|
||||
await expect(callout).toContainText(`daemon v${PUBLISHED_DAEMON_VERSION}`);
|
||||
await expect(callout).toContainText("Updating restarts the daemon and closes open terminals.");
|
||||
const update = page.getByTestId("daemon-outdated-callout-update").first();
|
||||
await expect(update).toBeVisible();
|
||||
await expect(update).toBeEnabled();
|
||||
}
|
||||
|
||||
test("sidebar shows an update notice when the active host's daemon is older than the app", async ({
|
||||
page,
|
||||
}) => {
|
||||
test.setTimeout(180_000);
|
||||
await withOutdatedDaemon(page, () => expectOutdatedNotice(page));
|
||||
});
|
||||
|
||||
test("compact sidebar shows the update notice, which is a native callout", async ({ page }) => {
|
||||
test.setTimeout(180_000);
|
||||
await page.setViewportSize(COMPACT);
|
||||
await withOutdatedDaemon(page, async () => {
|
||||
await page.getByRole("button", { name: "Open menu", exact: true }).first().click();
|
||||
await expectOutdatedNotice(page);
|
||||
});
|
||||
});
|
||||
@@ -415,19 +415,8 @@ test.describe("Settings — compact master-detail", () => {
|
||||
const workspace = await withWorkspace({ prefix: "host-picker-settings-back-" });
|
||||
const workspaceRoute = buildHostWorkspaceRoute(getServerId(), workspace.workspaceId);
|
||||
|
||||
const worktreeSetupCallout = page.locator('[data-testid^="worktree-setup-callout-"]');
|
||||
|
||||
await test.step("the wide sidebar shows the worktree setup prompt for this workspace", async () => {
|
||||
await page.setViewportSize({ width: 1280, height: 900 });
|
||||
await openWorkspace(page, workspace);
|
||||
await expect(worktreeSetupCallout.first()).toBeVisible({ timeout: 30_000 });
|
||||
await page.setViewportSize({ width: 390, height: 844 });
|
||||
});
|
||||
|
||||
await openWorkspace(page, workspace);
|
||||
await page.getByRole("button", { name: "Open menu", exact: true }).click();
|
||||
// The compact sidebar shows only native callouts, so the prompt stays out.
|
||||
await expect(page.getByTestId("sidebar-hosts-trigger")).toBeVisible();
|
||||
await expect(worktreeSetupCallout).toHaveCount(0);
|
||||
await page.getByTestId("sidebar-hosts-trigger").click();
|
||||
await page.getByRole("button", { name: /Open .* settings/ }).click();
|
||||
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
# Native daemon-outdated notice
|
||||
|
||||
The flow needs the development app connected to an isolated daemon older than the app. Install a published daemon that predates `terminalsSurviveRestart`, start it in its own home, and bundle the app against it. `<checkout>` is the root of this repository; `<port>`, `<metro-port>` and `<udid>` are a free daemon port, a free Metro port and the booted simulator.
|
||||
|
||||
Run this once, in any directory. It installs into `/tmp/old-daemon`, not globally:
|
||||
|
||||
```sh
|
||||
npm install --prefix /tmp/old-daemon --no-package-lock @getpaseo/server@0.8.0
|
||||
```
|
||||
|
||||
Terminal 1, the old daemon. It runs in the foreground; keep it running:
|
||||
|
||||
```sh
|
||||
cd /tmp/old-daemon/node_modules/@getpaseo/server
|
||||
PASEO_HOME=/tmp/old-daemon-home PASEO_LISTEN=0.0.0.0:<port> PASEO_RELAY_ENABLED=0 node dist/scripts/supervisor-entrypoint.js
|
||||
```
|
||||
|
||||
Terminal 2, Metro. It runs in the foreground; keep it running:
|
||||
|
||||
```sh
|
||||
cd <checkout>/packages/app
|
||||
APP_VARIANT=development EXPO_PUBLIC_LOCAL_DAEMON=localhost:<port> npx expo start --port <metro-port> --clear
|
||||
```
|
||||
|
||||
Terminal 3, the flow, while terminals 1 and 2 are still running:
|
||||
|
||||
```sh
|
||||
cd <checkout>
|
||||
xcrun simctl openurl <udid> "paseo://expo-development-client/?url=http%3A%2F%2F127.0.0.1%3A<metro-port>"
|
||||
maestro --udid <udid> test packages/app/e2e/mobile/agent-device/native-daemon-outdated-notice.ios.yaml --env DAEMON_VERSION=0.8.0
|
||||
```
|
||||
|
||||
Start with the sidebar closed. The flow opens it, then asserts the notice, its daemon version, the terminals line for a daemon without `terminalsSurviveRestart`, and an enabled Update button. Do not tap Update: on 0.8.0 it runs a global `npm install -g @getpaseo/cli@latest`.
|
||||
|
||||
When you are done, stop terminals 1 and 2 and remove `/tmp/old-daemon` and `/tmp/old-daemon-home`.
|
||||
@@ -1,26 +0,0 @@
|
||||
appId: sh.paseo.debug
|
||||
---
|
||||
# The app must be connected to a daemon older than the app that predates
|
||||
# terminalsSurviveRestart (0.8.0). See native-daemon-outdated-notice-preconditions.md.
|
||||
- launchApp
|
||||
- runFlow:
|
||||
when:
|
||||
visible: ".*getRegistrationInfoAsync.*"
|
||||
commands:
|
||||
- tapOn: ".*getRegistrationInfoAsync.*"
|
||||
- tapOn: Dismiss
|
||||
- extendedWaitUntil:
|
||||
visible: "Open menu"
|
||||
timeout: 60000
|
||||
- tapOn: "Open menu"
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
id: daemon-outdated-callout
|
||||
timeout: 30000
|
||||
- assertVisible: "Daemon out of date"
|
||||
- assertVisible: ".*daemon v${DAEMON_VERSION}.*"
|
||||
- assertVisible: "Updating restarts the daemon and closes open terminals."
|
||||
- assertVisible:
|
||||
id: daemon-outdated-callout-update
|
||||
enabled: true
|
||||
- takeScreenshot: native-daemon-outdated-notice
|
||||
@@ -84,7 +84,6 @@ import { getDesktopHost } from "@/desktop/host";
|
||||
import { loadDesktopSettings } from "@/desktop/settings/desktop-settings";
|
||||
import { RosettaCalloutSource } from "@/desktop/updates/rosetta-callout-source";
|
||||
import { UpdateCalloutSource } from "@/desktop/updates/update-callout-source";
|
||||
import { DaemonOutdatedCalloutSource } from "@/components/daemon-outdated-callout-source";
|
||||
import { useActiveWorktreeNewAction } from "@/hooks/use-active-worktree-new-action";
|
||||
import { useGlobalNewWorkspaceAction } from "@/hooks/use-global-new-workspace-action";
|
||||
import { useLatchedBoolean } from "@/hooks/use-latched-boolean";
|
||||
@@ -487,7 +486,6 @@ function AppContainer({ children, chromeEnabled: chromeEnabledOverride }: AppCon
|
||||
<DownloadToast />
|
||||
<RosettaCalloutSource />
|
||||
<UpdateCalloutSource />
|
||||
<DaemonOutdatedCalloutSource />
|
||||
<LegacyAgentSkillsMigration />
|
||||
<WorktreeSetupCalloutSource />
|
||||
<CommandCenterRootActions />
|
||||
|
||||
@@ -1,182 +0,0 @@
|
||||
import type { DaemonClient } from "@getpaseo/client/internal/daemon-client";
|
||||
import type { TFunction } from "i18next";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
type SidebarCalloutAction,
|
||||
SidebarCalloutDescriptionText,
|
||||
} from "@/components/sidebar-callout";
|
||||
import {
|
||||
type DaemonOutdatedUpdateState,
|
||||
resolveDaemonOutdatedCallout,
|
||||
} from "@/components/resolve-daemon-outdated-callout";
|
||||
import { useSidebarCallouts } from "@/contexts/sidebar-callout-context";
|
||||
import { useLocalDaemonServerId } from "@/hooks/use-is-local-daemon";
|
||||
import { useStableEvent } from "@/hooks/use-stable-event";
|
||||
import { useHostRuntimeClient, useHostRuntimeIsConnected, useHosts } from "@/runtime/host-runtime";
|
||||
import { updateDaemonFromSettings } from "@/screens/settings/daemon-lifecycle";
|
||||
import { useLastWorkspaceSelection } from "@/stores/navigation-active-workspace-store";
|
||||
import { useSessionStore } from "@/stores/session-store";
|
||||
import { orderHostsLocalFirst, resolveActiveHostServerId } from "@/types/host-connection";
|
||||
import { resolveAppVersion } from "@/utils/app-version";
|
||||
import { confirmDialog } from "@/utils/confirm-dialog";
|
||||
|
||||
interface HostUpdateState {
|
||||
serverId: string;
|
||||
update: DaemonOutdatedUpdateState;
|
||||
}
|
||||
|
||||
const IDLE: DaemonOutdatedUpdateState = { status: "idle" };
|
||||
|
||||
function DaemonOutdatedDescription({ lines }: { lines: string[] }) {
|
||||
return (
|
||||
<>
|
||||
{lines.map((line) => (
|
||||
<SidebarCalloutDescriptionText key={line}>{line}</SidebarCalloutDescriptionText>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
interface ConfirmAndUpdateDaemonInput {
|
||||
serverId: string;
|
||||
hostLabel: string;
|
||||
client: DaemonClient;
|
||||
t: TFunction;
|
||||
onState: (next: DaemonOutdatedUpdateState) => void;
|
||||
}
|
||||
|
||||
async function confirmAndUpdateDaemon(input: ConfirmAndUpdateDaemonInput): Promise<void> {
|
||||
const { serverId, hostLabel, client, t, onState } = input;
|
||||
let confirmed: boolean;
|
||||
try {
|
||||
confirmed = await confirmDialog({
|
||||
title: t("settings.host.daemon.update.confirmTitle", { name: hostLabel }),
|
||||
message: t("settings.host.daemon.update.confirmMessage"),
|
||||
confirmLabel: t("settings.host.daemon.update.confirm"),
|
||||
cancelLabel: t("common.actions.cancel"),
|
||||
destructive: false,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("[DaemonOutdatedCallout] Failed to open update confirmation", error);
|
||||
onState({ status: "failed", message: t("settings.host.daemon.update.dialogFailedMessage") });
|
||||
return;
|
||||
}
|
||||
if (!confirmed) return;
|
||||
|
||||
const phaseLabels = {
|
||||
starting: t("settings.host.daemon.update.phaseStarting"),
|
||||
downloading: t("settings.host.daemon.update.phaseDownloading"),
|
||||
installing: t("settings.host.daemon.update.phaseInstalling"),
|
||||
complete: t("settings.host.daemon.update.phaseComplete"),
|
||||
};
|
||||
onState({ status: "updating", phase: phaseLabels.starting });
|
||||
const requestId = `sidebar_daemon_update_${serverId}`;
|
||||
const unsubscribe = client.on("daemon.update.progress", (message) => {
|
||||
if (message.payload.requestId !== requestId) return;
|
||||
onState({ status: "updating", phase: phaseLabels[message.payload.phase] });
|
||||
});
|
||||
try {
|
||||
await updateDaemonFromSettings(serverId, {
|
||||
updateDaemon: () => client.updateDaemon(requestId),
|
||||
getStatus: async () => ({
|
||||
...(await client.getDaemonStatus()),
|
||||
serverId: client.getLastServerInfoMessage()?.serverId ?? "",
|
||||
version: client.getLastServerInfoMessage()?.version ?? null,
|
||||
}),
|
||||
});
|
||||
onState({ status: "idle" });
|
||||
} catch (error) {
|
||||
console.error(`[DaemonOutdatedCallout] Failed to update daemon ${hostLabel}`, error);
|
||||
onState({
|
||||
status: "failed",
|
||||
message: t("settings.host.daemon.update.requestFailedMessage", {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
}),
|
||||
});
|
||||
} finally {
|
||||
unsubscribe();
|
||||
}
|
||||
}
|
||||
|
||||
/** Sidebar notice when the active host's daemon is older than this app. */
|
||||
export function DaemonOutdatedCalloutSource() {
|
||||
const { t } = useTranslation();
|
||||
const callouts = useSidebarCallouts();
|
||||
const hosts = useHosts();
|
||||
const localServerId = useLocalDaemonServerId();
|
||||
const lastWorkspaceSelection = useLastWorkspaceSelection();
|
||||
const orderedHosts = useMemo(
|
||||
() => orderHostsLocalFirst(hosts, localServerId),
|
||||
[hosts, localServerId],
|
||||
);
|
||||
// Same host resolution as settings: the last workspace's host, else the local daemon, else the first.
|
||||
const serverId = resolveActiveHostServerId({
|
||||
selectedServerId: lastWorkspaceSelection?.serverId ?? null,
|
||||
localServerId,
|
||||
hosts,
|
||||
orderedHosts,
|
||||
});
|
||||
const hostLabel = hosts.find((host) => host.serverId === serverId)?.label ?? "";
|
||||
const daemonClient = useHostRuntimeClient(serverId ?? "");
|
||||
const isConnected = useHostRuntimeIsConnected(serverId ?? "");
|
||||
const serverInfo = useSessionStore((state) =>
|
||||
serverId ? (state.sessions[serverId]?.serverInfo ?? null) : null,
|
||||
);
|
||||
const [hostUpdate, setHostUpdate] = useState<HostUpdateState | null>(null);
|
||||
const update = hostUpdate && hostUpdate.serverId === serverId ? hostUpdate.update : IDLE;
|
||||
|
||||
const runUpdate = useStableEvent(() => {
|
||||
if (!serverId || !daemonClient) return;
|
||||
void confirmAndUpdateDaemon({
|
||||
serverId,
|
||||
hostLabel,
|
||||
client: daemonClient,
|
||||
t,
|
||||
onState: (next) => setHostUpdate({ serverId, update: next }),
|
||||
});
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!serverId || !serverInfo) return;
|
||||
const descriptor = resolveDaemonOutdatedCallout({
|
||||
serverId,
|
||||
hostLabel,
|
||||
appVersion: resolveAppVersion(),
|
||||
daemonVersion: serverInfo.version,
|
||||
supportsSelfUpdate: serverInfo.features?.daemonSelfUpdate === true,
|
||||
desktopManaged: serverInfo.desktopManaged === true,
|
||||
// COMPAT(terminalsSurviveRestart): added in v0.12.0, remove gate after 2027-04-21.
|
||||
terminalsSurviveRestart: serverInfo.features?.terminalsSurviveRestart === true,
|
||||
isConnected,
|
||||
update,
|
||||
});
|
||||
if (!descriptor) return;
|
||||
|
||||
const actions: SidebarCalloutAction[] = descriptor.updateAction
|
||||
? [
|
||||
{
|
||||
label: descriptor.updateAction.label,
|
||||
onPress: runUpdate,
|
||||
variant: "primary",
|
||||
disabled: descriptor.updateAction.disabled,
|
||||
testID: "daemon-outdated-callout-update",
|
||||
},
|
||||
]
|
||||
: [];
|
||||
return callouts.show({
|
||||
id: descriptor.id,
|
||||
dismissalKey: descriptor.dismissalKey,
|
||||
priority: descriptor.priority,
|
||||
title: descriptor.title,
|
||||
description: <DaemonOutdatedDescription lines={descriptor.description} />,
|
||||
variant: descriptor.variant,
|
||||
actions,
|
||||
testID: descriptor.testID,
|
||||
native: true,
|
||||
});
|
||||
// t: the resolver reads the active language through i18n, so a language change rebuilds the copy.
|
||||
}, [callouts, hostLabel, isConnected, runUpdate, serverId, serverInfo, t, update]);
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -40,7 +40,6 @@ import { SidebarSectionHeader } from "@/components/sidebar/sidebar-section-heade
|
||||
import { Shortcut } from "@/components/ui/shortcut";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import { HEADER_INNER_HEIGHT, useIsCompactFormFactor } from "@/constants/layout";
|
||||
import { isNative } from "@/constants/platform";
|
||||
import { useOpenAddProject } from "@/hooks/use-open-add-project";
|
||||
import { useImportSession } from "@/hooks/use-import-session";
|
||||
import { useShortcutKeys } from "@/hooks/use-shortcut-keys";
|
||||
@@ -606,8 +605,6 @@ function MobileSidebar({
|
||||
/>
|
||||
)}
|
||||
|
||||
<SidebarCalloutSlot nativeOnly />
|
||||
|
||||
<SidebarFooter
|
||||
handleOpenProject={handleOpenProject}
|
||||
handleSettings={handleSettings}
|
||||
@@ -787,7 +784,7 @@ function DesktopSidebar({
|
||||
/>
|
||||
)}
|
||||
|
||||
<SidebarCalloutSlot nativeOnly={isNative} />
|
||||
<SidebarCalloutSlot />
|
||||
|
||||
<SidebarFooter
|
||||
handleOpenProject={handleOpenProject}
|
||||
|
||||
@@ -1,195 +0,0 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { i18n } from "@/i18n/i18next";
|
||||
import {
|
||||
resolveDaemonOutdatedCallout,
|
||||
type ResolveDaemonOutdatedCalloutInput,
|
||||
} from "./resolve-daemon-outdated-callout";
|
||||
|
||||
function input(
|
||||
overrides: Partial<ResolveDaemonOutdatedCalloutInput> = {},
|
||||
): ResolveDaemonOutdatedCalloutInput {
|
||||
return {
|
||||
serverId: "srv_alpha",
|
||||
hostLabel: "Workstation",
|
||||
appVersion: "0.12.0",
|
||||
daemonVersion: "0.11.4",
|
||||
supportsSelfUpdate: true,
|
||||
desktopManaged: false,
|
||||
terminalsSurviveRestart: true,
|
||||
isConnected: true,
|
||||
update: { status: "idle" },
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe("resolveDaemonOutdatedCallout", () => {
|
||||
it("shows an update notice when the daemon is older than the app", () => {
|
||||
expect(resolveDaemonOutdatedCallout(input())).toEqual({
|
||||
id: "daemon-outdated",
|
||||
dismissalKey: "daemon-outdated:srv_alpha:0.11.4",
|
||||
priority: 250,
|
||||
title: "Daemon out of date",
|
||||
description: [
|
||||
"Workstation runs daemon v0.11.4. This app is v0.12.0.",
|
||||
"Updating restarts the daemon. Terminals stay open.",
|
||||
],
|
||||
variant: "default",
|
||||
updateAction: { label: "Update", disabled: false },
|
||||
testID: "daemon-outdated-callout",
|
||||
});
|
||||
});
|
||||
|
||||
it("hides when the daemon matches or is newer than the app", () => {
|
||||
expect(resolveDaemonOutdatedCallout(input({ daemonVersion: "0.12.0" }))).toBeNull();
|
||||
expect(resolveDaemonOutdatedCallout(input({ daemonVersion: "v0.12.0" }))).toBeNull();
|
||||
expect(resolveDaemonOutdatedCallout(input({ daemonVersion: "0.12.1" }))).toBeNull();
|
||||
expect(resolveDaemonOutdatedCallout(input({ daemonVersion: "1.0.0" }))).toBeNull();
|
||||
});
|
||||
|
||||
it("compares numeric components numerically, not as strings", () => {
|
||||
expect(
|
||||
resolveDaemonOutdatedCallout(input({ appVersion: "0.10.0", daemonVersion: "0.9.9" })),
|
||||
).not.toBeNull();
|
||||
expect(
|
||||
resolveDaemonOutdatedCallout(input({ appVersion: "0.9.9", daemonVersion: "0.10.0" })),
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it("orders prereleases before their release", () => {
|
||||
expect(
|
||||
resolveDaemonOutdatedCallout(input({ appVersion: "0.12.0", daemonVersion: "0.12.0-beta.1" })),
|
||||
).not.toBeNull();
|
||||
expect(
|
||||
resolveDaemonOutdatedCallout(input({ appVersion: "0.12.0-beta.1", daemonVersion: "0.12.0" })),
|
||||
).toBeNull();
|
||||
expect(
|
||||
resolveDaemonOutdatedCallout(
|
||||
input({ appVersion: "0.12.0-beta.10", daemonVersion: "0.12.0-beta.2" }),
|
||||
),
|
||||
).not.toBeNull();
|
||||
expect(
|
||||
resolveDaemonOutdatedCallout(
|
||||
input({ appVersion: "0.12.0-beta.2", daemonVersion: "0.12.0-beta.2" }),
|
||||
),
|
||||
).toBeNull();
|
||||
expect(
|
||||
resolveDaemonOutdatedCallout(input({ appVersion: "0.12.0-rc.1", daemonVersion: "0.11.9" })),
|
||||
).not.toBeNull();
|
||||
});
|
||||
|
||||
it("hides when either version is missing or unparseable", () => {
|
||||
expect(resolveDaemonOutdatedCallout(input({ daemonVersion: null }))).toBeNull();
|
||||
expect(resolveDaemonOutdatedCallout(input({ appVersion: null }))).toBeNull();
|
||||
expect(resolveDaemonOutdatedCallout(input({ daemonVersion: "dev" }))).toBeNull();
|
||||
expect(resolveDaemonOutdatedCallout(input({ daemonVersion: "0.11" }))).toBeNull();
|
||||
expect(resolveDaemonOutdatedCallout(input({ appVersion: "unknown" }))).toBeNull();
|
||||
});
|
||||
|
||||
it("offers no button for a desktop-managed daemon and points to Paseo on that computer", () => {
|
||||
const descriptor = resolveDaemonOutdatedCallout(input({ desktopManaged: true }));
|
||||
|
||||
expect(descriptor?.updateAction).toBeNull();
|
||||
expect(descriptor?.description).toEqual([
|
||||
"Workstation runs daemon v0.11.4. This app is v0.12.0.",
|
||||
"Paseo Desktop manages this daemon. Update Paseo on that computer.",
|
||||
"Updating Paseo Desktop keeps terminals open on macOS and Linux, and closes them on Windows.",
|
||||
]);
|
||||
});
|
||||
|
||||
it("never promises terminals survive a desktop update, which closes them on Windows", () => {
|
||||
const descriptor = resolveDaemonOutdatedCallout(
|
||||
input({ desktopManaged: true, terminalsSurviveRestart: true }),
|
||||
);
|
||||
|
||||
expect(descriptor?.description).not.toContain(
|
||||
"Updating restarts the daemon. Terminals stay open.",
|
||||
);
|
||||
expect(descriptor?.description.at(-1)).toContain("closes them on Windows");
|
||||
});
|
||||
|
||||
it("says a desktop update closes terminals when the daemon does not keep them", () => {
|
||||
const descriptor = resolveDaemonOutdatedCallout(
|
||||
input({ desktopManaged: true, terminalsSurviveRestart: false }),
|
||||
);
|
||||
|
||||
expect(descriptor?.description.at(-1)).toBe(
|
||||
"Updating restarts the daemon and closes open terminals.",
|
||||
);
|
||||
});
|
||||
|
||||
it("offers no button when the daemon does not support self-update", () => {
|
||||
const descriptor = resolveDaemonOutdatedCallout(input({ supportsSelfUpdate: false }));
|
||||
|
||||
expect(descriptor?.updateAction).toBeNull();
|
||||
expect(descriptor?.description).toContain(
|
||||
"This daemon can't update itself. Update Paseo on that computer.",
|
||||
);
|
||||
});
|
||||
|
||||
it("says the update closes terminals when the daemon does not keep them", () => {
|
||||
const descriptor = resolveDaemonOutdatedCallout(input({ terminalsSurviveRestart: false }));
|
||||
|
||||
expect(descriptor?.description).toEqual([
|
||||
"Workstation runs daemon v0.11.4. This app is v0.12.0.",
|
||||
"Updating restarts the daemon and closes open terminals.",
|
||||
]);
|
||||
});
|
||||
|
||||
it("disables the button while the host is disconnected", () => {
|
||||
expect(resolveDaemonOutdatedCallout(input({ isConnected: false }))?.updateAction).toEqual({
|
||||
label: "Update",
|
||||
disabled: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("shows the update phase on a disabled button while updating", () => {
|
||||
const descriptor = resolveDaemonOutdatedCallout(
|
||||
input({ update: { status: "updating", phase: "Downloading packages..." } }),
|
||||
);
|
||||
|
||||
expect(descriptor?.variant).toBe("default");
|
||||
expect(descriptor?.updateAction).toEqual({ label: "Downloading packages...", disabled: true });
|
||||
});
|
||||
|
||||
it("surfaces a failed update as an error with the button available again", () => {
|
||||
const descriptor = resolveDaemonOutdatedCallout(
|
||||
input({ update: { status: "failed", message: "Failed to update the daemon: boom" } }),
|
||||
);
|
||||
|
||||
expect(descriptor?.title).toBe("Daemon update failed");
|
||||
expect(descriptor?.variant).toBe("error");
|
||||
expect(descriptor?.description).toEqual(["Failed to update the daemon: boom"]);
|
||||
expect(descriptor?.updateAction).toEqual({ label: "Update", disabled: false });
|
||||
});
|
||||
|
||||
it("keys dismissal by host and daemon version", () => {
|
||||
const key = resolveDaemonOutdatedCallout(input())?.dismissalKey;
|
||||
|
||||
expect(resolveDaemonOutdatedCallout(input({ daemonVersion: "v0.11.4" }))?.dismissalKey).toBe(
|
||||
key,
|
||||
);
|
||||
expect(resolveDaemonOutdatedCallout(input({ appVersion: "0.13.0" }))?.dismissalKey).toBe(key);
|
||||
expect(resolveDaemonOutdatedCallout(input({ daemonVersion: "0.11.5" }))?.dismissalKey).toBe(
|
||||
"daemon-outdated:srv_alpha:0.11.5",
|
||||
);
|
||||
expect(resolveDaemonOutdatedCallout(input({ serverId: "srv_beta" }))?.dismissalKey).toBe(
|
||||
"daemon-outdated:srv_beta:0.11.4",
|
||||
);
|
||||
});
|
||||
|
||||
it("uses the active app language", async () => {
|
||||
await i18n.changeLanguage("es");
|
||||
try {
|
||||
const descriptor = resolveDaemonOutdatedCallout(input({ terminalsSurviveRestart: false }));
|
||||
|
||||
expect(descriptor?.title).toBe("Daemon desactualizado");
|
||||
expect(descriptor?.description).toEqual([
|
||||
"Workstation ejecuta el daemon v0.11.4. Esta app es v0.12.0.",
|
||||
"Actualizar reinicia el daemon y cierra las terminales abiertas.",
|
||||
]);
|
||||
expect(descriptor?.updateAction?.label).toBe("Actualizar");
|
||||
} finally {
|
||||
await i18n.changeLanguage("en");
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,147 +0,0 @@
|
||||
import { i18n } from "@/i18n/i18next";
|
||||
|
||||
export type DaemonOutdatedUpdateState =
|
||||
| { status: "idle" }
|
||||
| { status: "updating"; phase: string }
|
||||
| { status: "failed"; message: string };
|
||||
|
||||
export interface ResolveDaemonOutdatedCalloutInput {
|
||||
serverId: string;
|
||||
hostLabel: string;
|
||||
appVersion: string | null;
|
||||
daemonVersion: string | null;
|
||||
supportsSelfUpdate: boolean;
|
||||
desktopManaged: boolean;
|
||||
terminalsSurviveRestart: boolean;
|
||||
isConnected: boolean;
|
||||
update: DaemonOutdatedUpdateState;
|
||||
}
|
||||
|
||||
export interface DaemonOutdatedUpdateAction {
|
||||
label: string;
|
||||
disabled: boolean;
|
||||
}
|
||||
|
||||
export interface DaemonOutdatedCalloutDescriptor {
|
||||
id: "daemon-outdated";
|
||||
dismissalKey: string;
|
||||
priority: number;
|
||||
title: string;
|
||||
description: string[];
|
||||
variant: "default" | "error";
|
||||
updateAction: DaemonOutdatedUpdateAction | null;
|
||||
testID: "daemon-outdated-callout";
|
||||
}
|
||||
|
||||
interface ParsedVersion {
|
||||
core: [number, number, number];
|
||||
prerelease: string[];
|
||||
}
|
||||
|
||||
const VERSION_PATTERN =
|
||||
/^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z.-]+)?$/i;
|
||||
const NUMERIC_IDENTIFIER = /^\d+$/;
|
||||
|
||||
function parseVersion(value: string | null): ParsedVersion | null {
|
||||
const match = value?.trim().match(VERSION_PATTERN);
|
||||
if (!match) return null;
|
||||
return {
|
||||
core: [Number(match[1]), Number(match[2]), Number(match[3])],
|
||||
prerelease: match[4] ? match[4].split(".") : [],
|
||||
};
|
||||
}
|
||||
|
||||
function compareIdentifiers(left: string, right: string): number {
|
||||
const isLeftNumeric = NUMERIC_IDENTIFIER.test(left);
|
||||
const isRightNumeric = NUMERIC_IDENTIFIER.test(right);
|
||||
if (isLeftNumeric && isRightNumeric) return Number(left) - Number(right);
|
||||
if (isLeftNumeric) return -1;
|
||||
if (isRightNumeric) return 1;
|
||||
if (left < right) return -1;
|
||||
if (left > right) return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Semver precedence: a prerelease sorts before its release (0.12.0-beta.1 < 0.12.0).
|
||||
function compareVersions(left: ParsedVersion, right: ParsedVersion): number {
|
||||
for (let index = 0; index < 3; index += 1) {
|
||||
const difference = left.core[index] - right.core[index];
|
||||
if (difference !== 0) return difference;
|
||||
}
|
||||
if (left.prerelease.length === 0 || right.prerelease.length === 0) {
|
||||
return right.prerelease.length - left.prerelease.length;
|
||||
}
|
||||
const sharedLength = Math.min(left.prerelease.length, right.prerelease.length);
|
||||
for (let index = 0; index < sharedLength; index += 1) {
|
||||
const difference = compareIdentifiers(left.prerelease[index], right.prerelease[index]);
|
||||
if (difference !== 0) return difference;
|
||||
}
|
||||
return left.prerelease.length - right.prerelease.length;
|
||||
}
|
||||
|
||||
function resolveUpdateAction(input: ResolveDaemonOutdatedCalloutInput): DaemonOutdatedUpdateAction {
|
||||
if (input.update.status === "updating") {
|
||||
return { label: input.update.phase, disabled: true };
|
||||
}
|
||||
return { label: i18n.t("daemonOutdated.update"), disabled: !input.isConnected };
|
||||
}
|
||||
|
||||
function formatVersion(version: ParsedVersion): string {
|
||||
const core = version.core.join(".");
|
||||
return version.prerelease.length > 0 ? `${core}-${version.prerelease.join(".")}` : core;
|
||||
}
|
||||
|
||||
// A daemon that keeps terminals across restarts does not keep them across a Paseo Desktop update
|
||||
// on Windows, which closes them. server_info does not name the host's platform, so the line says
|
||||
// both.
|
||||
function resolveTerminalsLine(input: ResolveDaemonOutdatedCalloutInput): string {
|
||||
if (!input.terminalsSurviveRestart) return i18n.t("daemonOutdated.terminalsClose");
|
||||
if (input.desktopManaged) return i18n.t("daemonOutdated.desktopUpdateTerminals");
|
||||
return i18n.t("daemonOutdated.terminalsStayOpen");
|
||||
}
|
||||
|
||||
function resolveDescription(
|
||||
input: ResolveDaemonOutdatedCalloutInput,
|
||||
versions: { daemon: ParsedVersion; app: ParsedVersion },
|
||||
): string[] {
|
||||
if (input.update.status === "failed") return [input.update.message];
|
||||
|
||||
const lines = [
|
||||
i18n.t("daemonOutdated.versions", {
|
||||
host: input.hostLabel,
|
||||
daemonVersion: `v${formatVersion(versions.daemon)}`,
|
||||
appVersion: `v${formatVersion(versions.app)}`,
|
||||
}),
|
||||
];
|
||||
if (input.desktopManaged) {
|
||||
lines.push(i18n.t("daemonOutdated.desktopManaged"));
|
||||
} else if (!input.supportsSelfUpdate) {
|
||||
lines.push(i18n.t("daemonOutdated.selfUpdateUnsupported"));
|
||||
}
|
||||
lines.push(resolveTerminalsLine(input));
|
||||
return lines;
|
||||
}
|
||||
|
||||
export function resolveDaemonOutdatedCallout(
|
||||
input: ResolveDaemonOutdatedCalloutInput,
|
||||
): DaemonOutdatedCalloutDescriptor | null {
|
||||
const app = parseVersion(input.appVersion);
|
||||
const daemon = parseVersion(input.daemonVersion);
|
||||
if (!app || !daemon) return null;
|
||||
if (compareVersions(daemon, app) >= 0) return null;
|
||||
|
||||
// The update RPC refuses desktop-managed daemons; those update with Paseo Desktop on that computer.
|
||||
const canSelfUpdate = input.supportsSelfUpdate && !input.desktopManaged;
|
||||
const isFailed = input.update.status === "failed";
|
||||
|
||||
return {
|
||||
id: "daemon-outdated",
|
||||
dismissalKey: `daemon-outdated:${input.serverId}:${formatVersion(daemon)}`,
|
||||
priority: 250,
|
||||
title: isFailed ? i18n.t("daemonOutdated.updateFailedTitle") : i18n.t("daemonOutdated.title"),
|
||||
description: resolveDescription(input, { daemon, app }),
|
||||
variant: isFailed ? "error" : "default",
|
||||
updateAction: canSelfUpdate ? resolveUpdateAction(input) : null,
|
||||
testID: "daemon-outdated-callout",
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./sidebar-callout-slot.native";
|
||||
@@ -0,0 +1,3 @@
|
||||
export function SidebarCalloutSlot() {
|
||||
return null;
|
||||
}
|
||||
@@ -91,7 +91,7 @@ describe("SidebarCalloutSlot", () => {
|
||||
root?.render(
|
||||
<SidebarCalloutProvider>
|
||||
<RegisterCallout />
|
||||
<SidebarCalloutSlot nativeOnly={false} />
|
||||
<SidebarCalloutSlot />
|
||||
</SidebarCalloutProvider>,
|
||||
);
|
||||
await Promise.resolve();
|
||||
@@ -105,7 +105,7 @@ describe("SidebarCalloutSlot", () => {
|
||||
await act(async () => {
|
||||
root?.render(
|
||||
<SidebarCalloutProvider>
|
||||
<SidebarCalloutSlot nativeOnly={false} />
|
||||
<SidebarCalloutSlot />
|
||||
</SidebarCalloutProvider>,
|
||||
);
|
||||
await Promise.resolve();
|
||||
|
||||
+2
-6
@@ -2,14 +2,10 @@ import { View } from "react-native";
|
||||
import { StyleSheet } from "react-native-unistyles";
|
||||
import { SidebarCalloutViewport } from "@/contexts/sidebar-callout-context";
|
||||
|
||||
/**
|
||||
* `nativeOnly` limits the slot to callouts marked `native`. Native apps and every compact
|
||||
* sidebar pass it: the other callouts are about the desktop app.
|
||||
*/
|
||||
export function SidebarCalloutSlot({ nativeOnly }: { nativeOnly: boolean }) {
|
||||
export function SidebarCalloutSlot() {
|
||||
return (
|
||||
<View style={styles.slot} collapsable={false}>
|
||||
<SidebarCalloutViewport nativeOnly={nativeOnly} />
|
||||
<SidebarCalloutViewport />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -21,7 +21,6 @@ import {
|
||||
showSidebarCallout,
|
||||
type SidebarCalloutEntry,
|
||||
type SidebarCalloutOptions,
|
||||
type SidebarCalloutSelectionState,
|
||||
type SidebarCalloutState,
|
||||
unregisterSidebarCallout,
|
||||
} from "./sidebar-callout-state";
|
||||
@@ -38,7 +37,7 @@ export interface SidebarCalloutsApi {
|
||||
const DISMISSED_CALLOUTS_STORAGE_KEY = "@paseo:sidebar-callout-dismissals";
|
||||
|
||||
const SidebarCalloutApiContext = createContext<SidebarCalloutsApi | null>(null);
|
||||
const SidebarCalloutStateContext = createContext<SidebarCalloutSelectionState | null>(null);
|
||||
const SidebarCalloutStateContext = createContext<SidebarCalloutEntry | null>(null);
|
||||
|
||||
function persistDismissedCalloutKeys(keys: ReadonlySet<string>): void {
|
||||
void AsyncStorage.setItem(
|
||||
@@ -114,10 +113,11 @@ export function SidebarCalloutProvider({ children }: { children: ReactNode }) {
|
||||
});
|
||||
|
||||
const api = useMemo<SidebarCalloutsApi>(() => ({ show, dismiss, clear }), [clear, dismiss, show]);
|
||||
const activeCallout = useMemo(() => selectActiveSidebarCallout(state), [state]);
|
||||
|
||||
return (
|
||||
<SidebarCalloutApiContext.Provider value={api}>
|
||||
<SidebarCalloutStateContext.Provider value={state}>
|
||||
<SidebarCalloutStateContext.Provider value={activeCallout}>
|
||||
{children}
|
||||
</SidebarCalloutStateContext.Provider>
|
||||
</SidebarCalloutApiContext.Provider>
|
||||
@@ -132,20 +132,12 @@ export function useSidebarCallouts(): SidebarCalloutsApi {
|
||||
return api;
|
||||
}
|
||||
|
||||
export function useActiveSidebarCallout(
|
||||
options: { nativeOnly?: boolean } = {},
|
||||
): SidebarCalloutEntry | null {
|
||||
const state = useContext(SidebarCalloutStateContext);
|
||||
const nativeOnly = options.nativeOnly === true;
|
||||
return useMemo(
|
||||
() => (state ? selectActiveSidebarCallout(state, { nativeOnly }) : null),
|
||||
[nativeOnly, state],
|
||||
);
|
||||
export function useActiveSidebarCallout(): SidebarCalloutEntry | null {
|
||||
return useContext(SidebarCalloutStateContext);
|
||||
}
|
||||
|
||||
/** The active callout. `nativeOnly` limits it to callouts marked `native`, for native apps and compact sidebars. */
|
||||
export function SidebarCalloutViewport({ nativeOnly = false }: { nativeOnly?: boolean }) {
|
||||
const activeCallout = useActiveSidebarCallout({ nativeOnly });
|
||||
export function SidebarCalloutViewport() {
|
||||
const activeCallout = useActiveSidebarCallout();
|
||||
const api = useSidebarCallouts();
|
||||
if (!activeCallout) {
|
||||
return null;
|
||||
|
||||
@@ -31,30 +31,6 @@ describe("sidebar callout state", () => {
|
||||
expect(selectActiveSidebarCallout(state)?.title).toBe("Set up scripts");
|
||||
});
|
||||
|
||||
it("selects only callouts marked native for the native sidebar", () => {
|
||||
let state = createSidebarCalloutState();
|
||||
state = showSidebarCallout(state, {
|
||||
id: "desktop-update",
|
||||
priority: 300,
|
||||
title: "Update available",
|
||||
}).state;
|
||||
state = showSidebarCallout(state, {
|
||||
id: "daemon-outdated",
|
||||
priority: 250,
|
||||
title: "Daemon out of date",
|
||||
native: true,
|
||||
}).state;
|
||||
|
||||
expect(selectActiveSidebarCallout(state)?.title).toBe("Update available");
|
||||
expect(selectActiveSidebarCallout(state, { nativeOnly: true })?.title).toBe(
|
||||
"Daemon out of date",
|
||||
);
|
||||
|
||||
state = dismissSidebarCallout(state, "daemon-outdated").state;
|
||||
|
||||
expect(selectActiveSidebarCallout(state, { nativeOnly: true })).toBeNull();
|
||||
});
|
||||
|
||||
it("replaces a callout by id without duplicating the queue item", () => {
|
||||
let state = createSidebarCalloutState();
|
||||
state = showSidebarCallout(state, {
|
||||
|
||||
@@ -14,8 +14,6 @@ export interface SidebarCalloutOptions {
|
||||
priority?: number;
|
||||
onDismiss?: () => void;
|
||||
testID?: string;
|
||||
/** Also shown in the iOS and Android sidebar and every compact sidebar. Callouts about the desktop app leave it unset. */
|
||||
native?: boolean;
|
||||
}
|
||||
|
||||
export interface SidebarCalloutEntry extends SidebarCalloutOptions {
|
||||
@@ -131,19 +129,10 @@ export function clearSidebarCallouts(state: SidebarCalloutState): SidebarCallout
|
||||
return { ...state, callouts: [] };
|
||||
}
|
||||
|
||||
export type SidebarCalloutSelectionState = Pick<
|
||||
SidebarCalloutState,
|
||||
"callouts" | "dismissedKeys" | "dismissalStorageLoaded"
|
||||
>;
|
||||
|
||||
export function selectActiveSidebarCallout(
|
||||
state: SidebarCalloutSelectionState,
|
||||
options: { nativeOnly?: boolean } = {},
|
||||
state: Pick<SidebarCalloutState, "callouts" | "dismissedKeys" | "dismissalStorageLoaded">,
|
||||
): SidebarCalloutEntry | null {
|
||||
const visibleCallouts = state.callouts.filter((entry) => {
|
||||
if (options.nativeOnly && entry.native !== true) {
|
||||
return false;
|
||||
}
|
||||
const dismissalKey = normalizeDismissalKey(entry.dismissalKey);
|
||||
if (!dismissalKey) {
|
||||
return true;
|
||||
|
||||
@@ -2058,18 +2058,6 @@ export const ar: TranslationResources = {
|
||||
sidebarCallout: {
|
||||
dismiss: "رفض",
|
||||
},
|
||||
daemonOutdated: {
|
||||
title: "الخدمة قديمة",
|
||||
versions: "يشغّل {{host}} الخدمة {{daemonVersion}}. إصدار هذا التطبيق {{appVersion}}.",
|
||||
terminalsStayOpen: "يعيد التحديث تشغيل الخدمة. تبقى الطرفيات مفتوحة.",
|
||||
terminalsClose: "يعيد التحديث تشغيل الخدمة ويغلق الطرفيات المفتوحة.",
|
||||
desktopUpdateTerminals:
|
||||
"يُبقي تحديث Paseo Desktop الطرفيات مفتوحة على macOS وLinux، ويغلقها على Windows.",
|
||||
desktopManaged: "يدير Paseo Desktop هذه الخدمة. حدّث Paseo على ذلك الحاسوب.",
|
||||
selfUpdateUnsupported: "لا يمكن لهذه الخدمة تحديث نفسها. حدّث Paseo على ذلك الحاسوب.",
|
||||
update: "تحديث",
|
||||
updateFailedTitle: "فشل تحديث الخدمة",
|
||||
},
|
||||
contextWindow: {
|
||||
noData: "لا توجد بيانات للسياق",
|
||||
accessibilityNoData: "نافذة السياق: لا توجد بيانات للسياق",
|
||||
|
||||
@@ -2083,18 +2083,6 @@ export const en = {
|
||||
sidebarCallout: {
|
||||
dismiss: "Dismiss",
|
||||
},
|
||||
daemonOutdated: {
|
||||
title: "Daemon out of date",
|
||||
versions: "{{host}} runs daemon {{daemonVersion}}. This app is {{appVersion}}.",
|
||||
terminalsStayOpen: "Updating restarts the daemon. Terminals stay open.",
|
||||
terminalsClose: "Updating restarts the daemon and closes open terminals.",
|
||||
desktopUpdateTerminals:
|
||||
"Updating Paseo Desktop keeps terminals open on macOS and Linux, and closes them on Windows.",
|
||||
desktopManaged: "Paseo Desktop manages this daemon. Update Paseo on that computer.",
|
||||
selfUpdateUnsupported: "This daemon can't update itself. Update Paseo on that computer.",
|
||||
update: "Update",
|
||||
updateFailedTitle: "Daemon update failed",
|
||||
},
|
||||
contextWindow: {
|
||||
noData: "No context data",
|
||||
accessibilityNoData: "Context window: No context data",
|
||||
|
||||
@@ -2106,19 +2106,6 @@ export const es: TranslationResources = {
|
||||
sidebarCallout: {
|
||||
dismiss: "Despedir",
|
||||
},
|
||||
daemonOutdated: {
|
||||
title: "Daemon desactualizado",
|
||||
versions: "{{host}} ejecuta el daemon {{daemonVersion}}. Esta app es {{appVersion}}.",
|
||||
terminalsStayOpen: "Actualizar reinicia el daemon. Las terminales siguen abiertas.",
|
||||
terminalsClose: "Actualizar reinicia el daemon y cierra las terminales abiertas.",
|
||||
desktopUpdateTerminals:
|
||||
"Actualizar Paseo Desktop mantiene las terminales abiertas en macOS y Linux, y las cierra en Windows.",
|
||||
desktopManaged: "Paseo Desktop administra este daemon. Actualiza Paseo en esa computadora.",
|
||||
selfUpdateUnsupported:
|
||||
"Este daemon no puede actualizarse solo. Actualiza Paseo en esa computadora.",
|
||||
update: "Actualizar",
|
||||
updateFailedTitle: "No se pudo actualizar el daemon",
|
||||
},
|
||||
contextWindow: {
|
||||
noData: "No hay datos de contexto",
|
||||
accessibilityNoData: "Ventana de contexto: No hay datos de contexto",
|
||||
|
||||
@@ -2105,19 +2105,6 @@ export const fr: TranslationResources = {
|
||||
sidebarCallout: {
|
||||
dismiss: "Ignorer",
|
||||
},
|
||||
daemonOutdated: {
|
||||
title: "Daemon obsolète",
|
||||
versions: "{{host}} exécute le daemon {{daemonVersion}}. Cette app est en {{appVersion}}.",
|
||||
terminalsStayOpen: "La mise à jour redémarre le daemon. Les terminaux restent ouverts.",
|
||||
terminalsClose: "La mise à jour redémarre le daemon et ferme les terminaux ouverts.",
|
||||
desktopUpdateTerminals:
|
||||
"La mise à jour de Paseo Desktop garde les terminaux ouverts sur macOS et Linux, et les ferme sur Windows.",
|
||||
desktopManaged: "Paseo Desktop gère ce daemon. Mettez à jour Paseo sur cet ordinateur.",
|
||||
selfUpdateUnsupported:
|
||||
"Ce daemon ne peut pas se mettre à jour lui-même. Mettez à jour Paseo sur cet ordinateur.",
|
||||
update: "Mettre à jour",
|
||||
updateFailedTitle: "Échec de la mise à jour du daemon",
|
||||
},
|
||||
contextWindow: {
|
||||
noData: "Aucune donnée de contexte",
|
||||
accessibilityNoData: "Fenêtre de contexte : aucune donnée de contexte",
|
||||
|
||||
@@ -2078,20 +2078,6 @@ export const ja: TranslationResources = {
|
||||
sidebarCallout: {
|
||||
dismiss: "閉じる",
|
||||
},
|
||||
daemonOutdated: {
|
||||
title: "デーモンが古くなっています",
|
||||
versions: "{{host}}のデーモンは{{daemonVersion}}です。このアプリは{{appVersion}}です。",
|
||||
terminalsStayOpen: "更新するとデーモンが再起動します。ターミナルは開いたままです。",
|
||||
terminalsClose: "更新するとデーモンが再起動し、開いているターミナルは閉じられます。",
|
||||
desktopUpdateTerminals:
|
||||
"Paseo Desktop を更新すると、macOS と Linux ではターミナルは開いたままですが、Windows では閉じられます。",
|
||||
desktopManaged:
|
||||
"このデーモンはPaseo Desktopによって管理されています。そのコンピューターでPaseoを更新してください。",
|
||||
selfUpdateUnsupported:
|
||||
"このデーモンは自身を更新できません。そのコンピューターでPaseoを更新してください。",
|
||||
update: "更新",
|
||||
updateFailedTitle: "デーモンを更新できませんでした",
|
||||
},
|
||||
contextWindow: {
|
||||
noData: "コンテキストデータがありません",
|
||||
accessibilityNoData: "コンテキストウィンドウ:コンテキストデータがありません",
|
||||
|
||||
@@ -2068,20 +2068,6 @@ export const ko: TranslationResources = {
|
||||
sidebarCallout: {
|
||||
dismiss: "닫기",
|
||||
},
|
||||
daemonOutdated: {
|
||||
title: "데몬이 오래되었습니다",
|
||||
versions: "{{host}}의 데몬은 {{daemonVersion}}입니다. 이 앱은 {{appVersion}}입니다.",
|
||||
terminalsStayOpen: "업데이트하면 데몬이 다시 시작됩니다. 터미널은 열린 상태로 유지됩니다.",
|
||||
terminalsClose: "업데이트하면 데몬이 다시 시작되고 열려 있는 터미널이 닫힙니다.",
|
||||
desktopUpdateTerminals:
|
||||
"Paseo Desktop을 업데이트하면 macOS와 Linux에서는 터미널이 열린 상태로 유지되고, Windows에서는 닫힙니다.",
|
||||
desktopManaged:
|
||||
"이 데몬은 Paseo Desktop에서 관리됩니다. 해당 컴퓨터에서 Paseo를 업데이트하세요.",
|
||||
selfUpdateUnsupported:
|
||||
"이 데몬은 스스로 업데이트할 수 없습니다. 해당 컴퓨터에서 Paseo를 업데이트하세요.",
|
||||
update: "업데이트",
|
||||
updateFailedTitle: "데몬 업데이트 실패",
|
||||
},
|
||||
contextWindow: {
|
||||
noData: "컨텍스트 데이터 없음",
|
||||
accessibilityNoData: "컨텍스트 창: 컨텍스트 데이터 없음",
|
||||
|
||||
@@ -2091,19 +2091,6 @@ export const ptBR: TranslationResources = {
|
||||
sidebarCallout: {
|
||||
dismiss: "Dispensar",
|
||||
},
|
||||
daemonOutdated: {
|
||||
title: "Daemon desatualizado",
|
||||
versions: "{{host}} executa o daemon {{daemonVersion}}. Este app é {{appVersion}}.",
|
||||
terminalsStayOpen: "Atualizar reinicia o daemon. Os terminais continuam abertos.",
|
||||
terminalsClose: "Atualizar reinicia o daemon e fecha os terminais abertos.",
|
||||
desktopUpdateTerminals:
|
||||
"Atualizar o Paseo Desktop mantém os terminais abertos no macOS e no Linux, e os fecha no Windows.",
|
||||
desktopManaged: "O Paseo Desktop gerencia este daemon. Atualize o Paseo nesse computador.",
|
||||
selfUpdateUnsupported:
|
||||
"Este daemon não consegue se atualizar sozinho. Atualize o Paseo nesse computador.",
|
||||
update: "Atualizar",
|
||||
updateFailedTitle: "Falha ao atualizar o daemon",
|
||||
},
|
||||
contextWindow: {
|
||||
noData: "Sem dados de contexto",
|
||||
accessibilityNoData: "Janela de contexto: sem dados de contexto",
|
||||
|
||||
@@ -2091,18 +2091,6 @@ export const ru: TranslationResources = {
|
||||
sidebarCallout: {
|
||||
dismiss: "Закрыть",
|
||||
},
|
||||
daemonOutdated: {
|
||||
title: "Демон устарел",
|
||||
versions: "На {{host}} работает демон {{daemonVersion}}. Версия приложения: {{appVersion}}.",
|
||||
terminalsStayOpen: "Обновление перезапустит демон. Терминалы останутся открытыми.",
|
||||
terminalsClose: "Обновление перезапустит демон и закроет открытые терминалы.",
|
||||
desktopUpdateTerminals:
|
||||
"Обновление Paseo Desktop оставляет терминалы открытыми в macOS и Linux и закрывает их в Windows.",
|
||||
desktopManaged: "Этим демоном управляет Paseo Desktop. Обновите Paseo на том компьютере.",
|
||||
selfUpdateUnsupported: "Этот демон не может обновиться сам. Обновите Paseo на том компьютере.",
|
||||
update: "Обновить",
|
||||
updateFailedTitle: "Не удалось обновить демон",
|
||||
},
|
||||
contextWindow: {
|
||||
noData: "Нет данных о контексте",
|
||||
accessibilityNoData: "Контекстное окно: нет данных о контексте",
|
||||
|
||||
@@ -2036,18 +2036,6 @@ export const zhCN: TranslationResources = {
|
||||
sidebarCallout: {
|
||||
dismiss: "关闭",
|
||||
},
|
||||
daemonOutdated: {
|
||||
title: "守护进程版本过旧",
|
||||
versions: "{{host}} 运行的守护进程为 {{daemonVersion}},此应用为 {{appVersion}}。",
|
||||
terminalsStayOpen: "更新会重启守护进程,终端将保持打开。",
|
||||
terminalsClose: "更新会重启守护进程,并关闭已打开的终端。",
|
||||
desktopUpdateTerminals:
|
||||
"更新 Paseo Desktop 时,macOS 和 Linux 上的终端保持打开,Windows 上的终端会被关闭。",
|
||||
desktopManaged: "此守护进程由 Paseo Desktop 管理。请在那台电脑上更新 Paseo。",
|
||||
selfUpdateUnsupported: "此守护进程无法自行更新。请在那台电脑上更新 Paseo。",
|
||||
update: "更新",
|
||||
updateFailedTitle: "守护进程更新失败",
|
||||
},
|
||||
contextWindow: {
|
||||
noData: "暂无上下文数据",
|
||||
accessibilityNoData: "上下文窗口:暂无上下文数据",
|
||||
|
||||
Reference in New Issue
Block a user