Complete client UI i18n migration
Adds client-side i18n support, language settings, translated UI copy, and locale parity coverage.
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import type { DesktopDialogBridge } from "@/desktop/host";
|
||||
import { i18n } from "@/i18n/i18next";
|
||||
import { isAbsolutePath } from "@/utils/path";
|
||||
|
||||
export type PickedImageSource = { kind: "file_uri"; uri: string } | { kind: "blob"; blob: Blob };
|
||||
@@ -85,8 +86,13 @@ export async function openImagePathsWithDesktopDialog(
|
||||
const options = {
|
||||
directory: false,
|
||||
multiple: true,
|
||||
filters: [{ name: "Images", extensions: IMAGE_FILE_EXTENSIONS }],
|
||||
title: "Attach images",
|
||||
filters: [
|
||||
{
|
||||
name: i18n.t("imageAttachmentPicker.dialogFilterName"),
|
||||
extensions: IMAGE_FILE_EXTENSIONS,
|
||||
},
|
||||
],
|
||||
title: i18n.t("imageAttachmentPicker.dialogTitle"),
|
||||
};
|
||||
|
||||
const dialogOpen = dialog?.open;
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { keepPreviousData, useQuery } from "@tanstack/react-query";
|
||||
import type { TFunction } from "i18next";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import type { AutocompleteOption } from "@/components/ui/autocomplete";
|
||||
import {
|
||||
useAgentCommandsQuery,
|
||||
@@ -115,13 +117,14 @@ function mapDirectorySuggestionsToEntries(payload: {
|
||||
}));
|
||||
}
|
||||
|
||||
function mapCommandToOption(entry: AvailableCommand): AgentAutocompleteOption {
|
||||
function mapCommandToOption(entry: AvailableCommand, t: TFunction): AgentAutocompleteOption {
|
||||
const command = entry.command;
|
||||
const base = {
|
||||
id: command.name,
|
||||
label: `/${command.name}`,
|
||||
detail: command.argumentHint || undefined,
|
||||
description: command.description,
|
||||
description:
|
||||
entry.source === "client" ? t(entry.command.descriptionKey) : entry.command.description,
|
||||
kind: "command" as const,
|
||||
};
|
||||
if (entry.source === "client") {
|
||||
@@ -148,6 +151,7 @@ interface BuildAutocompleteOptionsInput {
|
||||
activeSlashCommand: SlashCommandRange | null;
|
||||
activeFileMention: FileMentionRange | null;
|
||||
fileSuggestions: DirectorySuggestionEntry[];
|
||||
t: TFunction;
|
||||
}
|
||||
|
||||
function buildCommandAutocompleteOptions(input: BuildAutocompleteOptionsInput) {
|
||||
@@ -177,7 +181,7 @@ function buildCommandAutocompleteOptions(input: BuildAutocompleteOptionsInput) {
|
||||
input.commandFilterQuery,
|
||||
);
|
||||
const orderedMatches = orderAutocompleteOptions(matches);
|
||||
return orderedMatches.map(mapCommandToOption);
|
||||
return orderedMatches.map((entry) => mapCommandToOption(entry, input.t));
|
||||
}
|
||||
|
||||
const activeFileMention = input.activeFileMention;
|
||||
@@ -258,9 +262,12 @@ function resolveAutocompleteErrorMessage(args: {
|
||||
isCommandError: boolean;
|
||||
commandError: Error | null;
|
||||
fileSuggestionsError: unknown;
|
||||
t: TFunction;
|
||||
}): string | undefined {
|
||||
if (args.mode === "command") {
|
||||
return args.isCommandError ? (args.commandError?.message ?? "Failed to load") : undefined;
|
||||
return args.isCommandError
|
||||
? (args.commandError?.message ?? args.t("agentAutocomplete.failedToLoad"))
|
||||
: undefined;
|
||||
}
|
||||
if (args.mode === "file") {
|
||||
return args.fileSuggestionsError instanceof Error
|
||||
@@ -271,6 +278,7 @@ function resolveAutocompleteErrorMessage(args: {
|
||||
}
|
||||
|
||||
export function useAgentAutocomplete(input: UseAgentAutocompleteInput): AgentAutocompleteResult {
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
userInput,
|
||||
cursorIndex,
|
||||
@@ -366,7 +374,7 @@ export function useAgentAutocomplete(input: UseAgentAutocompleteInput): AgentAut
|
||||
],
|
||||
queryFn: async (): Promise<DirectorySuggestionEntry[]> => {
|
||||
if (!client) {
|
||||
throw new Error("Daemon client unavailable");
|
||||
throw new Error(t("common.errors.daemonClientUnavailable"));
|
||||
}
|
||||
const response = await client.getDirectorySuggestions({
|
||||
cwd: autocompleteCwd,
|
||||
@@ -402,6 +410,7 @@ export function useAgentAutocomplete(input: UseAgentAutocompleteInput): AgentAut
|
||||
isDraftContext,
|
||||
isVisible,
|
||||
mode,
|
||||
t,
|
||||
}),
|
||||
[
|
||||
activeFileMention,
|
||||
@@ -412,6 +421,7 @@ export function useAgentAutocomplete(input: UseAgentAutocompleteInput): AgentAut
|
||||
isDraftContext,
|
||||
isVisible,
|
||||
mode,
|
||||
t,
|
||||
],
|
||||
);
|
||||
|
||||
@@ -488,10 +498,15 @@ export function useAgentAutocomplete(input: UseAgentAutocompleteInput): AgentAut
|
||||
isCommandError: isError,
|
||||
commandError: error,
|
||||
fileSuggestionsError: fileSuggestionsQuery.error,
|
||||
t,
|
||||
});
|
||||
|
||||
const loadingText = mode === "file" ? "Searching workspace..." : "Loading commands...";
|
||||
const emptyText = mode === "file" ? "No files or directories found" : "No commands found";
|
||||
const loadingText =
|
||||
mode === "file"
|
||||
? t("agentAutocomplete.searchingWorkspace")
|
||||
: t("agentAutocomplete.loadingCommands");
|
||||
const emptyText =
|
||||
mode === "file" ? t("agentAutocomplete.noFiles") : t("agentAutocomplete.noCommands");
|
||||
|
||||
return {
|
||||
isVisible,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import type { DaemonClient } from "@getpaseo/client/internal/daemon-client";
|
||||
import { useHostRuntimeClient, useHostRuntimeIsConnected } from "@/runtime/host-runtime";
|
||||
import type { AgentProvider } from "@getpaseo/protocol/agent-types";
|
||||
@@ -65,6 +66,7 @@ export function useAgentCommandsQuery({
|
||||
enabled = true,
|
||||
draftConfig,
|
||||
}: UseAgentCommandsQueryOptions) {
|
||||
const { t } = useTranslation();
|
||||
const client = useHostRuntimeClient(serverId);
|
||||
const isConnected = useHostRuntimeIsConnected(serverId);
|
||||
|
||||
@@ -72,7 +74,7 @@ export function useAgentCommandsQuery({
|
||||
queryKey: agentCommandsQueryKey(serverId, agentId, draftConfig),
|
||||
queryFn: async () => {
|
||||
if (!client) {
|
||||
throw new Error("Daemon client not available");
|
||||
throw new Error(t("common.errors.daemonClientUnavailable"));
|
||||
}
|
||||
return fetchAgentCommands({ client, agentId, draftConfig });
|
||||
},
|
||||
|
||||
@@ -5,6 +5,7 @@ import type {
|
||||
} from "@getpaseo/client/internal/daemon-client";
|
||||
import { useInfiniteQuery } from "@tanstack/react-query";
|
||||
import { useCallback, useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import type { AggregatedAgent } from "@/hooks/use-aggregated-agents";
|
||||
import { useHostRuntimeClient, useHostRuntimeIsConnected, useHosts } from "@/runtime/host-runtime";
|
||||
import { buildAgentDirectoryState } from "@/utils/agent-directory-sync";
|
||||
@@ -76,6 +77,7 @@ export function useAgentHistory(options: {
|
||||
serverId?: string | null;
|
||||
enabled?: boolean;
|
||||
}): AgentHistoryResult {
|
||||
const { t } = useTranslation();
|
||||
const daemons = useHosts();
|
||||
const serverId = useMemo(() => {
|
||||
const value = options.serverId;
|
||||
@@ -102,7 +104,7 @@ export function useAgentHistory(options: {
|
||||
lastPage.pageInfo.hasMore ? lastPage.pageInfo.nextCursor : null,
|
||||
queryFn: async ({ pageParam }) => {
|
||||
if (!serverId || !client) {
|
||||
throw new Error("Host is not connected");
|
||||
throw new Error(t("workspace.terminal.hostDisconnected"));
|
||||
}
|
||||
return fetchAgentHistoryPage({ client, serverId, cursor: pageParam });
|
||||
},
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useCallback, useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import type { DaemonClient } from "@getpaseo/client/internal/daemon-client";
|
||||
import { useSessionStore } from "@/stores/session-store";
|
||||
import {
|
||||
@@ -9,6 +10,7 @@ import {
|
||||
rejectInitDeferred,
|
||||
} from "@/utils/agent-initialization";
|
||||
import { planInitialAgentTimelineSync, planTimelineTailFetch } from "@/timeline/timeline-sync-plan";
|
||||
import { i18n } from "@/i18n/i18next";
|
||||
|
||||
export const INIT_TIMEOUT_MS = 30_000;
|
||||
|
||||
@@ -19,6 +21,7 @@ export interface EnsureAgentIsInitializedInput {
|
||||
agentId: string;
|
||||
client: Pick<DaemonClient, "fetchAgentTimeline"> | null;
|
||||
setAgentInitializing: SetAgentInitializing;
|
||||
hostDisconnectedMessage?: string;
|
||||
}
|
||||
|
||||
export function ensureAgentIsInitialized(input: EnsureAgentIsInitializedInput): Promise<void> {
|
||||
@@ -48,7 +51,10 @@ export function ensureAgentIsInitialized(input: EnsureAgentIsInitializedInput):
|
||||
|
||||
if (!client) {
|
||||
setAgentInitializing(agentId, false);
|
||||
rejectInitDeferred(key, new Error("Host is not connected"));
|
||||
rejectInitDeferred(
|
||||
key,
|
||||
new Error(input.hostDisconnectedMessage ?? i18n.t("workspace.terminal.hostDisconnected")),
|
||||
);
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
@@ -64,12 +70,13 @@ export interface RefreshAgentInput {
|
||||
agentId: string;
|
||||
client: Pick<DaemonClient, "refreshAgent" | "fetchAgentTimeline"> | null;
|
||||
setAgentInitializing: SetAgentInitializing;
|
||||
hostDisconnectedMessage?: string;
|
||||
}
|
||||
|
||||
export async function refreshAgent(input: RefreshAgentInput): Promise<void> {
|
||||
const { agentId, client, setAgentInitializing } = input;
|
||||
if (!client) {
|
||||
throw new Error("Host is not connected");
|
||||
throw new Error(input.hostDisconnectedMessage ?? i18n.t("workspace.terminal.hostDisconnected"));
|
||||
}
|
||||
setAgentInitializing(agentId, true);
|
||||
|
||||
@@ -105,6 +112,7 @@ export function useAgentInitialization({
|
||||
serverId: string;
|
||||
client: DaemonClient | null;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const setInitializingAgents = useSessionStore((state) => state.setInitializingAgents);
|
||||
const setAgentInitializing = useMemo(
|
||||
() => createSetAgentInitializing(serverId, setInitializingAgents),
|
||||
@@ -113,13 +121,25 @@ export function useAgentInitialization({
|
||||
|
||||
const ensureAgentIsInitializedCallback = useCallback(
|
||||
(agentId: string): Promise<void> =>
|
||||
ensureAgentIsInitialized({ serverId, agentId, client, setAgentInitializing }),
|
||||
[client, serverId, setAgentInitializing],
|
||||
ensureAgentIsInitialized({
|
||||
serverId,
|
||||
agentId,
|
||||
client,
|
||||
setAgentInitializing,
|
||||
hostDisconnectedMessage: t("workspace.terminal.hostDisconnected"),
|
||||
}),
|
||||
[client, serverId, setAgentInitializing, t],
|
||||
);
|
||||
|
||||
const refreshAgentCallback = useCallback(
|
||||
(agentId: string): Promise<void> => refreshAgent({ agentId, client, setAgentInitializing }),
|
||||
[client, setAgentInitializing],
|
||||
(agentId: string): Promise<void> =>
|
||||
refreshAgent({
|
||||
agentId,
|
||||
client,
|
||||
setAgentInitializing,
|
||||
hostDisconnectedMessage: t("workspace.terminal.hostDisconnected"),
|
||||
}),
|
||||
[client, setAgentInitializing, t],
|
||||
);
|
||||
|
||||
return {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useCallback, useMemo } from "react";
|
||||
import { useMutation, useQuery, useQueryClient, type QueryClient } from "@tanstack/react-query";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useSessionStore } from "@/stores/session-store";
|
||||
import { agentHistoryQueryKey } from "./agent-history-query-key";
|
||||
|
||||
@@ -382,6 +383,7 @@ export function usePendingArchiveAgentIds(serverId: string): ReadonlySet<string>
|
||||
|
||||
export function useArchiveAgent() {
|
||||
const queryClient = useQueryClient();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const pendingQuery = useArchiveAgentPendingQuery();
|
||||
|
||||
@@ -389,7 +391,7 @@ export function useArchiveAgent() {
|
||||
mutationFn: async (input: ArchiveAgentInput): Promise<{ archivedAt: string }> => {
|
||||
const client = useSessionStore.getState().sessions[input.serverId]?.client ?? null;
|
||||
if (!client) {
|
||||
throw new Error("Daemon client not available");
|
||||
throw new Error(t("common.errors.daemonClientUnavailable"));
|
||||
}
|
||||
return await client.archiveAgent(input.agentId);
|
||||
},
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useState, useCallback, useMemo } from "react";
|
||||
import { useQuery, type QueryClient } from "@tanstack/react-query";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import type { DaemonClient } from "@getpaseo/client/internal/daemon-client";
|
||||
import type { ComboboxOption } from "@/components/ui/combobox";
|
||||
import type { ToastApi } from "@/components/toast-host";
|
||||
@@ -35,13 +36,14 @@ export function useBranchSwitcher({
|
||||
toast,
|
||||
queryClient,
|
||||
}: UseBranchSwitcherInput): UseBranchSwitcherResult {
|
||||
const { t } = useTranslation();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const branchSuggestionsQuery = useQuery({
|
||||
queryKey: ["branchSuggestions", normalizedServerId, normalizedWorkspaceId],
|
||||
queryFn: async () => {
|
||||
if (!client) {
|
||||
throw new Error("Daemon client unavailable");
|
||||
throw new Error(t("common.errors.daemonClientUnavailable"));
|
||||
}
|
||||
const payload = await client.getBranchSuggestions({
|
||||
cwd: normalizedWorkspaceId,
|
||||
@@ -85,35 +87,34 @@ export function useBranchSwitcher({
|
||||
const targetStash = stashPayload.entries.find((e) => e.branch === branchId);
|
||||
if (!targetStash) return;
|
||||
const shouldRestore = await confirmDialog({
|
||||
title: "Restore stashed changes?",
|
||||
message:
|
||||
"This branch has stashed changes from a previous session. Would you like to restore them?",
|
||||
confirmLabel: "Restore",
|
||||
cancelLabel: "Later",
|
||||
title: t("branchSwitcher.restoreStashTitle"),
|
||||
message: t("branchSwitcher.restoreStashMessage"),
|
||||
confirmLabel: t("branchSwitcher.restore"),
|
||||
cancelLabel: t("branchSwitcher.later"),
|
||||
});
|
||||
if (!shouldRestore) return;
|
||||
const popPayload = await client.stashPop(normalizedWorkspaceId, targetStash.index);
|
||||
if (popPayload.error) {
|
||||
toast.error(popPayload.error.message);
|
||||
} else {
|
||||
toast.show("Stashed changes restored");
|
||||
toast.show(t("branchSwitcher.stashRestored"));
|
||||
}
|
||||
await invalidateStashAndCheckout();
|
||||
} catch {
|
||||
// Non-critical — user can still restore on next branch switch
|
||||
}
|
||||
},
|
||||
[client, invalidateStashAndCheckout, normalizedWorkspaceId, toast],
|
||||
[client, invalidateStashAndCheckout, normalizedWorkspaceId, toast, t],
|
||||
);
|
||||
|
||||
const stashAndSwitch = useCallback(
|
||||
async (branchId: string) => {
|
||||
if (!client) return;
|
||||
const shouldStash = await confirmDialog({
|
||||
title: "Uncommitted changes",
|
||||
message: "You have uncommitted changes. Stash them before switching branches?",
|
||||
confirmLabel: "Stash & Switch",
|
||||
cancelLabel: "Cancel",
|
||||
title: t("branchSwitcher.uncommittedTitle"),
|
||||
message: t("branchSwitcher.uncommittedMessage"),
|
||||
confirmLabel: t("branchSwitcher.stashAndSwitch"),
|
||||
cancelLabel: t("common.actions.cancel"),
|
||||
});
|
||||
if (!shouldStash) return;
|
||||
|
||||
@@ -133,10 +134,10 @@ export function useBranchSwitcher({
|
||||
}
|
||||
await invalidateStashAndCheckout();
|
||||
} catch (err) {
|
||||
toast.error(err instanceof Error ? err.message : "Failed to stash changes");
|
||||
toast.error(err instanceof Error ? err.message : t("branchSwitcher.failedToStash"));
|
||||
}
|
||||
},
|
||||
[client, currentBranchName, invalidateStashAndCheckout, normalizedWorkspaceId, toast],
|
||||
[client, currentBranchName, invalidateStashAndCheckout, normalizedWorkspaceId, toast, t],
|
||||
);
|
||||
|
||||
const handleBranchSelect = useCallback(
|
||||
@@ -160,7 +161,7 @@ export function useBranchSwitcher({
|
||||
await invalidateStashAndCheckout();
|
||||
await maybeRestoreStashForBranch(branchId);
|
||||
} catch (err) {
|
||||
toast.error(err instanceof Error ? err.message : "Failed to switch branch");
|
||||
toast.error(err instanceof Error ? err.message : t("branchSwitcher.failedToSwitch"));
|
||||
}
|
||||
})();
|
||||
},
|
||||
@@ -171,6 +172,7 @@ export function useBranchSwitcher({
|
||||
maybeRestoreStashForBranch,
|
||||
normalizedWorkspaceId,
|
||||
stashAndSwitch,
|
||||
t,
|
||||
toast,
|
||||
],
|
||||
);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useCallback, useMemo } from "react";
|
||||
import { i18n } from "@/i18n/i18next";
|
||||
import { getHostRuntimeStore } from "@/runtime/host-runtime";
|
||||
import { useSessionStore } from "@/stores/session-store";
|
||||
|
||||
@@ -25,7 +26,7 @@ export function useClearWorkspaceAttention({
|
||||
}
|
||||
const client = getHostRuntimeStore().getClient(serverId);
|
||||
if (!client) {
|
||||
throw new Error("Host is not connected");
|
||||
throw new Error(i18n.t("workspace.terminal.hostDisconnected"));
|
||||
}
|
||||
await client.clearWorkspaceAttention(workspaceId);
|
||||
}, [hasClearableAttention, serverId, workspaceId]);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import type { TextInput } from "react-native";
|
||||
import { router, usePathname, type Href } from "expo-router";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useKeyboardShortcutsStore } from "@/stores/keyboard-shortcuts-store";
|
||||
import { keyboardActionDispatcher } from "@/keyboard/keyboard-action-dispatcher";
|
||||
import { useAllAgentsList } from "@/hooks/use-all-agents-list";
|
||||
@@ -52,7 +53,10 @@ function sortAgents(left: AggregatedAgent, right: AggregatedAgent): number {
|
||||
|
||||
interface CommandCenterActionDefinition {
|
||||
id: string;
|
||||
title: string;
|
||||
titleKey:
|
||||
| "shell.commandCenter.openProject"
|
||||
| "shell.commandCenter.home"
|
||||
| "sidebar.actions.settings";
|
||||
icon?: "plus" | "settings" | "home";
|
||||
actionId?: string;
|
||||
keywords: string[];
|
||||
@@ -62,7 +66,7 @@ interface CommandCenterActionDefinition {
|
||||
const COMMAND_CENTER_ACTIONS: readonly CommandCenterActionDefinition[] = [
|
||||
{
|
||||
id: "new-agent",
|
||||
title: "Open project",
|
||||
titleKey: "shell.commandCenter.openProject",
|
||||
icon: "plus",
|
||||
actionId: "new-agent",
|
||||
keywords: ["open", "project", "folder", "workspace", "repo"],
|
||||
@@ -70,24 +74,28 @@ const COMMAND_CENTER_ACTIONS: readonly CommandCenterActionDefinition[] = [
|
||||
},
|
||||
{
|
||||
id: "home",
|
||||
title: "Home",
|
||||
titleKey: "shell.commandCenter.home",
|
||||
icon: "home",
|
||||
keywords: ["home", "start", "import", "session", "pair", "device", "providers"],
|
||||
routeKind: "home",
|
||||
},
|
||||
{
|
||||
id: "settings",
|
||||
title: "Settings",
|
||||
titleKey: "sidebar.actions.settings",
|
||||
icon: "settings",
|
||||
keywords: ["settings", "preferences", "config", "configuration"],
|
||||
routeKind: "settings",
|
||||
},
|
||||
];
|
||||
|
||||
function matchesActionQuery(query: string, action: CommandCenterActionDefinition): boolean {
|
||||
function matchesActionQuery(
|
||||
query: string,
|
||||
action: CommandCenterActionDefinition,
|
||||
title: string,
|
||||
): boolean {
|
||||
const normalized = query.trim().toLowerCase();
|
||||
if (!normalized) return true;
|
||||
if (action.title.toLowerCase().includes(normalized)) {
|
||||
if (title.toLowerCase().includes(normalized)) {
|
||||
return true;
|
||||
}
|
||||
return action.keywords.some((keyword) => keyword.includes(normalized));
|
||||
@@ -129,6 +137,7 @@ function resolveActionShortcutKeys(
|
||||
}
|
||||
|
||||
export function useCommandCenter() {
|
||||
const { t } = useTranslation();
|
||||
const pathname = usePathname();
|
||||
const routeActiveServerId = useActiveServerId();
|
||||
const { overrides } = useKeyboardShortcutOverrides();
|
||||
@@ -174,7 +183,7 @@ export function useCommandCenter() {
|
||||
}
|
||||
return COMMAND_CENTER_ACTIONS.filter((action) => {
|
||||
if (action.routeKind === "home" && !homeRoute) return false;
|
||||
return matchesActionQuery(query, action);
|
||||
return matchesActionQuery(query, action, t(action.titleKey));
|
||||
}).map<CommandCenterActionItem>((action) => {
|
||||
let route: Href | undefined;
|
||||
if (action.routeKind === "settings") route = settingsRoute;
|
||||
@@ -182,13 +191,13 @@ export function useCommandCenter() {
|
||||
return {
|
||||
kind: "action",
|
||||
id: action.id,
|
||||
title: action.title,
|
||||
title: t(action.titleKey),
|
||||
icon: action.icon,
|
||||
route,
|
||||
shortcutKeys: resolveActionShortcutKeys(action.actionId, overrides),
|
||||
};
|
||||
});
|
||||
}, [open, query, settingsRoute, homeRoute, overrides]);
|
||||
}, [open, query, settingsRoute, homeRoute, overrides, t]);
|
||||
|
||||
const items = useMemo(() => {
|
||||
if (!open) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useCallback, useEffect, useMemo } from "react";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import type { MutableDaemonConfig, MutableDaemonConfigPatch } from "@getpaseo/protocol/messages";
|
||||
import { useHostRuntimeClient, useHostRuntimeIsConnected } from "@/runtime/host-runtime";
|
||||
|
||||
@@ -14,6 +15,7 @@ interface UseDaemonConfigResult {
|
||||
}
|
||||
|
||||
export function useDaemonConfig(serverId: string | null): UseDaemonConfigResult {
|
||||
const { t } = useTranslation();
|
||||
const queryClient = useQueryClient();
|
||||
const client = useHostRuntimeClient(serverId ?? "");
|
||||
const isConnected = useHostRuntimeIsConnected(serverId ?? "");
|
||||
@@ -25,7 +27,7 @@ export function useDaemonConfig(serverId: string | null): UseDaemonConfigResult
|
||||
staleTime: Infinity,
|
||||
queryFn: async () => {
|
||||
if (!client) {
|
||||
throw new Error("Host is not connected");
|
||||
throw new Error(t("workspace.terminal.hostDisconnected"));
|
||||
}
|
||||
const result = await client.getDaemonConfig();
|
||||
return result.config;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { i18n } from "@/i18n/i18next";
|
||||
|
||||
export type DictationStatus = "idle" | "recording" | "uploading" | "failed";
|
||||
|
||||
export interface UseDictationOptions {
|
||||
@@ -37,5 +39,5 @@ export const toError = (error: unknown): Error => {
|
||||
if (typeof error === "string" && error.trim().length > 0) {
|
||||
return new Error(error);
|
||||
}
|
||||
return new Error("An unexpected error occurred while handling dictation.");
|
||||
return new Error(i18n.t("common.errors.unexpectedDictationError"));
|
||||
};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { DictationStreamSender } from "@/dictation/dictation-stream-sender";
|
||||
import { useDictationAudioSource } from "@/hooks/use-dictation-audio-source";
|
||||
@@ -14,6 +15,7 @@ import {
|
||||
} from "./use-dictation.shared";
|
||||
|
||||
export function useDictation(options: UseDictationOptions): UseDictationResult {
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
client,
|
||||
onTranscript,
|
||||
@@ -389,7 +391,7 @@ export function useDictation(options: UseDictationOptions): UseDictationResult {
|
||||
|
||||
try {
|
||||
if (!client?.isConnected) {
|
||||
throw new Error("Daemon client is disconnected");
|
||||
throw new Error(t("common.errors.daemonClientDisconnected"));
|
||||
}
|
||||
senderRef.current.resetStreamForReplay();
|
||||
const finalSeq = senderRef.current.getFinalSeq();
|
||||
@@ -401,7 +403,13 @@ export function useDictation(options: UseDictationOptions): UseDictationResult {
|
||||
}
|
||||
handleDictationFailure(err);
|
||||
}
|
||||
}, [client, ensureFinalTranscript, handleDictationFailure, handleStreamingTranscriptionSuccess]);
|
||||
}, [
|
||||
client,
|
||||
ensureFinalTranscript,
|
||||
handleDictationFailure,
|
||||
handleStreamingTranscriptionSuccess,
|
||||
t,
|
||||
]);
|
||||
|
||||
const discardFailedDictation = useCallback(() => {
|
||||
setIsProcessing(false);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import type { AgentProvider, AgentSessionConfig } from "@getpaseo/protocol/agent-types";
|
||||
import { useHostRuntimeClient, useHostRuntimeIsConnected } from "@/runtime/host-runtime";
|
||||
import { mergeProviderPreferences, useFormPreferences } from "./use-form-preferences";
|
||||
@@ -23,6 +24,7 @@ export function useDraftAgentFeatures(input: {
|
||||
thinkingOptionId: string | null | undefined;
|
||||
initialFeatureValues?: Record<string, unknown>;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const { serverId, provider, cwd, modeId, modelId, thinkingOptionId, initialFeatureValues } =
|
||||
input;
|
||||
const [localFeatureValues, setLocalFeatureValues] = useState<Record<string, unknown>>(
|
||||
@@ -67,7 +69,7 @@ export function useDraftAgentFeatures(input: {
|
||||
staleTime: 5 * 60 * 1000,
|
||||
queryFn: async () => {
|
||||
if (!client || !draftConfig) {
|
||||
throw new Error("Host is not connected");
|
||||
throw new Error(t("workspace.terminal.hostDisconnected"));
|
||||
}
|
||||
const payload = await client.listProviderFeatures(draftConfig);
|
||||
if (payload.error) {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useCallback, useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useSessionStore, type AgentFileExplorerState } from "@/stores/session-store";
|
||||
import { explorerFileFromReadResult } from "@/file-explorer/read-result";
|
||||
|
||||
@@ -51,6 +52,7 @@ export function buildWorkspaceExplorerStateKey(scope: FileExplorerWorkspaceScope
|
||||
}
|
||||
|
||||
export function useFileExplorerActions(params: { serverId: string } & FileExplorerWorkspaceScope) {
|
||||
const { t } = useTranslation();
|
||||
const { serverId, workspaceId, workspaceRoot } = params;
|
||||
const client = useSessionStore((state) => state.sessions[serverId]?.client ?? null);
|
||||
const setFileExplorer = useSessionStore((state) => state.setFileExplorer);
|
||||
@@ -114,7 +116,7 @@ export function useFileExplorerActions(params: { serverId: string } & FileExplor
|
||||
updateExplorerState((state) => ({
|
||||
...state,
|
||||
isLoading: false,
|
||||
lastError: "Workspace is unavailable",
|
||||
lastError: t("workspace.fileExplorer.states.unavailable"),
|
||||
pendingRequest: null,
|
||||
}));
|
||||
return false;
|
||||
@@ -124,7 +126,7 @@ export function useFileExplorerActions(params: { serverId: string } & FileExplor
|
||||
updateExplorerState((state) => ({
|
||||
...state,
|
||||
isLoading: false,
|
||||
lastError: "Host is not connected",
|
||||
lastError: t("workspace.terminal.hostDisconnected"),
|
||||
pendingRequest: null,
|
||||
}));
|
||||
return false;
|
||||
@@ -153,13 +155,16 @@ export function useFileExplorerActions(params: { serverId: string } & FileExplor
|
||||
updateExplorerState((state) => ({
|
||||
...state,
|
||||
isLoading: false,
|
||||
lastError: error instanceof Error ? error.message : "Failed to list directory",
|
||||
lastError:
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: t("workspace.fileExplorer.errors.failedToListDirectory"),
|
||||
pendingRequest: null,
|
||||
}));
|
||||
return false;
|
||||
}
|
||||
},
|
||||
[client, normalizedWorkspaceRoot, updateExplorerState, workspaceStateKey],
|
||||
[client, normalizedWorkspaceRoot, t, updateExplorerState, workspaceStateKey],
|
||||
);
|
||||
|
||||
const requestFilePreview = useCallback(
|
||||
@@ -179,7 +184,7 @@ export function useFileExplorerActions(params: { serverId: string } & FileExplor
|
||||
updateExplorerState((state) => ({
|
||||
...state,
|
||||
isLoading: false,
|
||||
lastError: "Workspace is unavailable",
|
||||
lastError: t("workspace.fileExplorer.states.unavailable"),
|
||||
pendingRequest: null,
|
||||
}));
|
||||
return;
|
||||
@@ -189,7 +194,7 @@ export function useFileExplorerActions(params: { serverId: string } & FileExplor
|
||||
updateExplorerState((state) => ({
|
||||
...state,
|
||||
isLoading: false,
|
||||
lastError: "Host is not connected",
|
||||
lastError: t("workspace.terminal.hostDisconnected"),
|
||||
pendingRequest: null,
|
||||
}));
|
||||
return;
|
||||
@@ -218,21 +223,21 @@ export function useFileExplorerActions(params: { serverId: string } & FileExplor
|
||||
updateExplorerState((state) => ({
|
||||
...state,
|
||||
isLoading: false,
|
||||
lastError: error instanceof Error ? error.message : "Failed to load file preview",
|
||||
lastError: error instanceof Error ? error.message : t("panels.file.failedToLoadPreview"),
|
||||
pendingRequest: null,
|
||||
}));
|
||||
}
|
||||
},
|
||||
[client, normalizedWorkspaceRoot, updateExplorerState, workspaceStateKey],
|
||||
[client, normalizedWorkspaceRoot, t, updateExplorerState, workspaceStateKey],
|
||||
);
|
||||
|
||||
const requestFileDownloadToken = useCallback(
|
||||
async (path: string) => {
|
||||
if (!normalizedWorkspaceRoot) {
|
||||
throw new Error("Workspace is unavailable");
|
||||
throw new Error(t("workspace.fileExplorer.states.unavailable"));
|
||||
}
|
||||
if (!client) {
|
||||
throw new Error("Host is not connected");
|
||||
throw new Error(t("workspace.terminal.hostDisconnected"));
|
||||
}
|
||||
const payload = await client.requestDownloadToken(normalizedWorkspaceRoot, path);
|
||||
if (payload.error) {
|
||||
@@ -240,7 +245,7 @@ export function useFileExplorerActions(params: { serverId: string } & FileExplor
|
||||
}
|
||||
return payload;
|
||||
},
|
||||
[client, normalizedWorkspaceRoot],
|
||||
[client, normalizedWorkspaceRoot, t],
|
||||
);
|
||||
|
||||
const selectExplorerEntry = useCallback(
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useCallback, useRef } from "react";
|
||||
import { Alert } from "react-native";
|
||||
import * as ImagePicker from "expo-image-picker";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { getDesktopHost, isElectronRuntime } from "@/desktop/host";
|
||||
import {
|
||||
normalizePickedImageAssets,
|
||||
@@ -14,6 +15,7 @@ interface UseImageAttachmentPickerResult {
|
||||
}
|
||||
|
||||
export function useImageAttachmentPicker(): UseImageAttachmentPickerResult {
|
||||
const { t } = useTranslation();
|
||||
const [mediaPermission, requestMediaPermission] = ImagePicker.useMediaLibraryPermissions();
|
||||
const isPickingRef = useRef(false);
|
||||
|
||||
@@ -31,14 +33,14 @@ export function useImageAttachmentPicker(): UseImageAttachmentPickerResult {
|
||||
|
||||
if (!currentPermission?.granted) {
|
||||
Alert.alert(
|
||||
"Permission required",
|
||||
"Please allow access to your photo library to attach images.",
|
||||
t("imageAttachmentPicker.permissionTitle"),
|
||||
t("imageAttachmentPicker.permissionMessage"),
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}, [mediaPermission, requestMediaPermission]);
|
||||
}, [mediaPermission, requestMediaPermission, t]);
|
||||
|
||||
const pickImages = useCallback(async () => {
|
||||
if (isPickingRef.current) {
|
||||
@@ -83,12 +85,12 @@ export function useImageAttachmentPicker(): UseImageAttachmentPickerResult {
|
||||
return await normalizePickedImageAssets(result.assets);
|
||||
} catch (error) {
|
||||
console.error("[ImageAttachmentPicker] Failed to pick image:", error);
|
||||
Alert.alert("Error", "Failed to select image");
|
||||
Alert.alert(t("imageAttachmentPicker.errorTitle"), t("imageAttachmentPicker.failedToSelect"));
|
||||
return null;
|
||||
} finally {
|
||||
isPickingRef.current = false;
|
||||
}
|
||||
}, [ensurePermission]);
|
||||
}, [ensurePermission, t]);
|
||||
|
||||
return { pickImages };
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { useCallback } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import type { ToastApi } from "@/components/toast-host";
|
||||
import { i18n } from "@/i18n/i18next";
|
||||
import { useSessionStore, type AgentTimelineCursorState } from "@/stores/session-store";
|
||||
import { planTimelineOlderFetch } from "@/timeline/timeline-sync-plan";
|
||||
|
||||
@@ -27,13 +29,15 @@ export interface LoadOlderAgentHistoryDeps {
|
||||
setInFlight: (value: boolean) => void;
|
||||
toast?: ToastApi | null;
|
||||
logger?: LoadOlderAgentHistoryLogger;
|
||||
failedMessage?: string;
|
||||
}
|
||||
|
||||
export async function loadOlderAgentHistory(
|
||||
agentId: string,
|
||||
deps: LoadOlderAgentHistoryDeps,
|
||||
): Promise<void> {
|
||||
const { client, cursor, hasOlder, isLoadingOlder, setInFlight, toast, logger } = deps;
|
||||
const { client, cursor, hasOlder, isLoadingOlder, setInFlight, toast, logger, failedMessage } =
|
||||
deps;
|
||||
if (!client || !cursor || !hasOlder || isLoadingOlder) {
|
||||
return;
|
||||
}
|
||||
@@ -46,7 +50,7 @@ export async function loadOlderAgentHistory(
|
||||
);
|
||||
} catch (error) {
|
||||
(logger ?? console).warn("[Timeline] failed to load older agent history", agentId, error);
|
||||
toast?.show("Couldn't load older history", {
|
||||
toast?.show(failedMessage ?? i18n.t("loadOlderHistory.failed"), {
|
||||
durationMs: 2200,
|
||||
testID: "agent-load-older-history-toast",
|
||||
});
|
||||
@@ -64,6 +68,7 @@ export function useLoadOlderAgentHistory({
|
||||
agentId: string;
|
||||
toast?: ToastApi | null;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const hasOlder =
|
||||
useSessionStore((state) => state.sessions[serverId]?.agentTimelineHasOlder.get(agentId)) ===
|
||||
true;
|
||||
@@ -98,8 +103,9 @@ export function useLoadOlderAgentHistory({
|
||||
isLoadingOlder: session?.agentTimelineOlderFetchInFlight.get(agentId) === true,
|
||||
setInFlight,
|
||||
toast,
|
||||
failedMessage: t("loadOlderHistory.failed"),
|
||||
});
|
||||
}, [agentId, serverId, setInFlight, toast]);
|
||||
}, [agentId, serverId, setInFlight, toast, t]);
|
||||
|
||||
return {
|
||||
isLoadingOlder,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { keepPreviousData, useQuery } from "@tanstack/react-query";
|
||||
import { useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useHostRuntimeClient, useHostRuntimeIsConnected } from "@/runtime/host-runtime";
|
||||
import type {
|
||||
CheckoutPrStatusResponse,
|
||||
@@ -9,6 +10,7 @@ import type { DaemonClient } from "@getpaseo/client/internal/daemon-client";
|
||||
import { mapPrPaneData, type PrPaneData } from "@/git/pr-pane-data";
|
||||
import { useCheckoutPrStatusQuery } from "@/git/use-pr-status-query";
|
||||
import { prPaneTimelineQueryKey } from "@/git/query-keys";
|
||||
import { i18n } from "@/i18n/i18next";
|
||||
|
||||
type CheckoutPrStatus = CheckoutPrStatusResponse["payload"]["status"];
|
||||
type CheckoutPrStatusPayloadError = CheckoutPrStatusResponse["payload"]["error"];
|
||||
@@ -153,6 +155,8 @@ export interface SelectPrPaneStateInput {
|
||||
timelineError: Error | null;
|
||||
timelineIsLoading: boolean;
|
||||
timelineIsFetching: boolean;
|
||||
statusLoadFailedLabel?: string;
|
||||
activityLoadFailedLabel?: string;
|
||||
}
|
||||
|
||||
export function selectPrPaneState(input: SelectPrPaneStateInput): UsePrPaneDataResult {
|
||||
@@ -176,6 +180,10 @@ export function selectPrPaneState(input: SelectPrPaneStateInput): UsePrPaneDataR
|
||||
statusError: input.statusError,
|
||||
timelineError: input.timelineError,
|
||||
timelinePayloadError: input.timelinePayload?.error ?? null,
|
||||
statusLoadFailedLabel:
|
||||
input.statusLoadFailedLabel ?? i18n.t("workspace.git.pr.errors.statusLoadFailed"),
|
||||
activityLoadFailedLabel:
|
||||
input.activityLoadFailedLabel ?? i18n.t("workspace.git.pr.errors.activityLoadFailed"),
|
||||
}),
|
||||
githubFeaturesEnabled: input.githubFeaturesEnabled,
|
||||
};
|
||||
@@ -187,6 +195,7 @@ export function usePrPaneData({
|
||||
enabled = true,
|
||||
timelineEnabled = enabled,
|
||||
}: UsePrPaneDataOptions): UsePrPaneDataResult {
|
||||
const { t } = useTranslation();
|
||||
const daemonClient = useHostRuntimeClient(serverId);
|
||||
const isConnected = useHostRuntimeIsConnected(serverId);
|
||||
const checkoutPrStatus = useCheckoutPrStatusQuery({ serverId, cwd, enabled });
|
||||
@@ -221,7 +230,7 @@ export function usePrPaneData({
|
||||
identity.repoOwner === null ||
|
||||
identity.repoName === null
|
||||
) {
|
||||
throw new Error("Daemon client not available");
|
||||
throw new Error(t("common.errors.daemonClientUnavailable"));
|
||||
}
|
||||
return fetchPrPaneTimelinePage({
|
||||
client: daemonClient,
|
||||
@@ -255,6 +264,8 @@ export function usePrPaneData({
|
||||
timelineError: timelineQuery.error,
|
||||
timelineIsLoading: timelineQuery.isLoading,
|
||||
timelineIsFetching: timelineQuery.isFetching,
|
||||
statusLoadFailedLabel: t("workspace.git.pr.errors.statusLoadFailed"),
|
||||
activityLoadFailedLabel: t("workspace.git.pr.errors.activityLoadFailed"),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -263,14 +274,18 @@ function firstNonSuppressedError({
|
||||
statusError,
|
||||
timelineError,
|
||||
timelinePayloadError,
|
||||
statusLoadFailedLabel,
|
||||
activityLoadFailedLabel,
|
||||
}: {
|
||||
statusPayloadError: CheckoutPrStatusPayloadError;
|
||||
statusError: Error | null;
|
||||
timelineError: Error | null;
|
||||
timelinePayloadError: PullRequestTimeline["error"];
|
||||
statusLoadFailedLabel: string;
|
||||
activityLoadFailedLabel: string;
|
||||
}): Error | null {
|
||||
if (statusPayloadError) {
|
||||
return new Error(statusPayloadError.message || "Unable to load pull request status");
|
||||
return new Error(statusPayloadError.message || statusLoadFailedLabel);
|
||||
}
|
||||
|
||||
if (statusError) {
|
||||
@@ -282,7 +297,7 @@ function firstNonSuppressedError({
|
||||
}
|
||||
|
||||
if (timelinePayloadError) {
|
||||
return new Error(timelinePayloadError.message || "Unable to load pull request activity");
|
||||
return new Error(timelinePayloadError.message || activityLoadFailedLabel);
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useHostRuntimeClient, useHostRuntimeIsConnected } from "@/runtime/host-runtime";
|
||||
import type { ProjectIcon } from "@getpaseo/protocol/messages";
|
||||
|
||||
@@ -19,6 +20,7 @@ interface UseProjectIconQueryOptions {
|
||||
}
|
||||
|
||||
export function useProjectIconQuery({ serverId, cwd }: UseProjectIconQueryOptions) {
|
||||
const { t } = useTranslation();
|
||||
const client = useHostRuntimeClient(serverId);
|
||||
const isConnected = useHostRuntimeIsConnected(serverId);
|
||||
|
||||
@@ -26,7 +28,7 @@ export function useProjectIconQuery({ serverId, cwd }: UseProjectIconQueryOption
|
||||
queryKey: projectIconQueryKey(serverId, cwd),
|
||||
queryFn: async (): Promise<ProjectIcon | null> => {
|
||||
if (!client) {
|
||||
throw new Error("Daemon client not available");
|
||||
throw new Error(t("common.errors.daemonClientUnavailable"));
|
||||
}
|
||||
const result = await client.requestProjectIcon(cwd);
|
||||
return result.icon;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useCallback, useEffect, useMemo } from "react";
|
||||
import { useMutation, useQuery, useQueryClient, type QueryClient } from "@tanstack/react-query";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import type { AgentProvider, ProviderSnapshotEntry } from "@getpaseo/protocol/agent-types";
|
||||
import type { DaemonClient } from "@getpaseo/client/internal/daemon-client";
|
||||
import { useHostRuntimeClient, useHostRuntimeIsConnected } from "@/runtime/host-runtime";
|
||||
@@ -112,6 +113,7 @@ export function useProvidersSnapshot(
|
||||
serverId: string | null,
|
||||
options: UseProvidersSnapshotOptions = {},
|
||||
): UseProvidersSnapshotResult {
|
||||
const { t } = useTranslation();
|
||||
const queryClient = useQueryClient();
|
||||
const client = useHostRuntimeClient(serverId ?? "");
|
||||
const isConnected = useHostRuntimeIsConnected(serverId ?? "");
|
||||
@@ -129,7 +131,7 @@ export function useProvidersSnapshot(
|
||||
staleTime: 60_000,
|
||||
queryFn: async () => {
|
||||
if (!client) {
|
||||
throw new Error("Host is not connected");
|
||||
throw new Error(t("workspace.terminal.hostDisconnected"));
|
||||
}
|
||||
return fetchProvidersSnapshot({ client, cwd });
|
||||
},
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useCallback } from "react";
|
||||
import AsyncStorage from "@react-native-async-storage/async-storage";
|
||||
import { useQuery, useQueryClient, type QueryClient } from "@tanstack/react-query";
|
||||
import { queryClient as appQueryClient } from "@/query/query-client";
|
||||
import type { AppLanguage } from "@/i18n/locales";
|
||||
import {
|
||||
DEFAULT_DESKTOP_SETTINGS,
|
||||
loadDesktopSettings,
|
||||
@@ -58,6 +59,7 @@ export {
|
||||
};
|
||||
export type {
|
||||
AppSettings,
|
||||
AppLanguage,
|
||||
DesktopSettingsBridge,
|
||||
KeyValueStorage,
|
||||
ReleaseChannel,
|
||||
@@ -143,6 +145,9 @@ export function useSettings(): UseSettingsReturn {
|
||||
if (updates.theme !== undefined) {
|
||||
appUpdates.theme = updates.theme;
|
||||
}
|
||||
if (updates.language !== undefined) {
|
||||
appUpdates.language = updates.language;
|
||||
}
|
||||
if (updates.sendBehavior !== undefined) {
|
||||
appUpdates.sendBehavior = updates.sendBehavior;
|
||||
}
|
||||
|
||||
@@ -47,11 +47,20 @@ describe("loadAppSettingsFromStorage", () => {
|
||||
const result = await loadAppSettingsFromStorage(deps);
|
||||
|
||||
expect(result).toEqual(DEFAULT_CLIENT_SETTINGS);
|
||||
expect(DEFAULT_CLIENT_SETTINGS.language).toBe("system");
|
||||
expect(deps.storage.entries.get(APP_SETTINGS_KEY)).toBe(
|
||||
JSON.stringify(DEFAULT_CLIENT_SETTINGS),
|
||||
);
|
||||
});
|
||||
|
||||
it("defaults language to system when storage is empty", async () => {
|
||||
const deps = makeDeps();
|
||||
|
||||
const result = await loadAppSettingsFromStorage(deps);
|
||||
|
||||
expect(result.language).toBe("system");
|
||||
});
|
||||
|
||||
it("loads configured terminal scrollback lines from app settings", async () => {
|
||||
const deps = makeDeps({
|
||||
storage: createInMemoryKeyValueStorage({
|
||||
@@ -95,6 +104,30 @@ describe("loadAppSettingsFromStorage", () => {
|
||||
});
|
||||
expect(deps.storage.entries.get(APP_SETTINGS_KEY)).toBe(JSON.stringify(result));
|
||||
});
|
||||
|
||||
it("loads a persisted explicit language", async () => {
|
||||
const deps = makeDeps({
|
||||
storage: createInMemoryKeyValueStorage({
|
||||
[APP_SETTINGS_KEY]: JSON.stringify({ language: "zh-CN" }),
|
||||
}),
|
||||
});
|
||||
|
||||
const result = await loadAppSettingsFromStorage(deps);
|
||||
|
||||
expect(result.language).toBe("zh-CN");
|
||||
});
|
||||
|
||||
it("drops an unknown persisted language back to system", async () => {
|
||||
const deps = makeDeps({
|
||||
storage: createInMemoryKeyValueStorage({
|
||||
[APP_SETTINGS_KEY]: JSON.stringify({ language: "klingon" }),
|
||||
}),
|
||||
});
|
||||
|
||||
const result = await loadAppSettingsFromStorage(deps);
|
||||
|
||||
expect(result.language).toBe("system");
|
||||
});
|
||||
});
|
||||
|
||||
describe("loadSettingsFromStorage", () => {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { isSyntaxThemeId, type SyntaxThemeId } from "@getpaseo/highlight";
|
||||
import type { QueryClient } from "@tanstack/react-query";
|
||||
import type { DesktopSettings } from "@/desktop/settings/desktop-settings";
|
||||
import { parseAppLanguage, type AppLanguage } from "@/i18n/locales";
|
||||
import { THEME_TO_UNISTYLES, type ThemeName } from "@/styles/theme";
|
||||
|
||||
export const APP_SETTINGS_KEY = "@paseo:app-settings";
|
||||
@@ -26,6 +27,7 @@ export const MAX_FONT_FAMILY_LENGTH = 200;
|
||||
|
||||
export interface AppSettings {
|
||||
theme: ThemeName | "auto";
|
||||
language: AppLanguage;
|
||||
sendBehavior: SendBehavior;
|
||||
serviceUrlBehavior: ServiceUrlBehavior;
|
||||
terminalScrollbackLines: number;
|
||||
@@ -43,6 +45,7 @@ export interface Settings extends AppSettings {
|
||||
|
||||
export const DEFAULT_CLIENT_SETTINGS: AppSettings = {
|
||||
theme: "auto",
|
||||
language: "system",
|
||||
sendBehavior: "interrupt",
|
||||
serviceUrlBehavior: "ask",
|
||||
terminalScrollbackLines: DEFAULT_TERMINAL_SCROLLBACK_LINES,
|
||||
@@ -149,6 +152,10 @@ function pickAppSettings(stored: Partial<AppSettings>): Partial<AppSettings> {
|
||||
if (typeof stored.theme === "string" && VALID_THEMES.has(stored.theme)) {
|
||||
result.theme = stored.theme;
|
||||
}
|
||||
const language = parseAppLanguage(stored.language);
|
||||
if (language !== null) {
|
||||
result.language = language;
|
||||
}
|
||||
if (stored.sendBehavior === "interrupt" || stored.sendBehavior === "queue") {
|
||||
result.sendBehavior = stored.sendBehavior;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user