diff --git a/packages/server/src/config/schema.test.ts b/packages/server/src/config/schema.test.ts new file mode 100644 index 000000000..1e7cf13b0 --- /dev/null +++ b/packages/server/src/config/schema.test.ts @@ -0,0 +1,15 @@ +import assert from "node:assert/strict" +import { describe, it } from "node:test" +import { PreferencesSchema } from "./schema" + +describe("chat visibility preferences", () => { + it("accepts hidden diagnostics and collapsed usage metrics", () => { + const preferences = PreferencesSchema.parse({ + diagnosticsExpansion: "hidden", + usageMetricsExpansion: "collapsed", + }) + + assert.equal(preferences.diagnosticsExpansion, "hidden") + assert.equal(preferences.usageMetricsExpansion, "collapsed") + }) +}) diff --git a/packages/server/src/config/schema.ts b/packages/server/src/config/schema.ts index ec65a5d24..c2ca2a0e0 100644 --- a/packages/server/src/config/schema.ts +++ b/packages/server/src/config/schema.ts @@ -22,8 +22,9 @@ const PreferencesSchema = z modelThinkingSelections: z.record(z.string(), z.string()).default({}), diffViewMode: z.enum(["split", "unified"]).default("split"), toolOutputExpansion: z.enum(["expanded", "collapsed"]).default("expanded"), - diagnosticsExpansion: z.enum(["expanded", "collapsed"]).default("expanded"), + diagnosticsExpansion: z.enum(["hidden", "expanded", "collapsed"]).default("expanded"), showUsageMetrics: z.boolean().default(true), + usageMetricsExpansion: z.enum(["expanded", "collapsed"]).default("collapsed"), autoCleanupBlankSessions: z.boolean().default(true), listeningMode: z.enum(["local", "all"]).default("local"), logLevel: z.enum(["DEBUG", "INFO", "WARN", "ERROR"]).default("DEBUG"), diff --git a/packages/ui/src/components/folder-selection-view.tsx b/packages/ui/src/components/folder-selection-view.tsx index bccbe83d7..af499dcfe 100644 --- a/packages/ui/src/components/folder-selection-view.tsx +++ b/packages/ui/src/components/folder-selection-view.tsx @@ -1,7 +1,6 @@ import { Dialog } from "@kobalte/core/dialog" -import { Select } from "@kobalte/core/select" import { Component, createMemo, createSignal, Show, For, onMount, onCleanup, createEffect } from "solid-js" -import { Folder, Clock, Trash2, FolderPlus, Settings, ChevronRight, MonitorUp, Star, Languages, ChevronDown, X, Globe, Loader2, GitBranch, Pencil } from "lucide-solid" +import { Folder, Clock, Trash2, FolderPlus, Settings, ChevronRight, MonitorUp, Star, X, Globe, Loader2, GitBranch, Pencil } from "lucide-solid" import { useConfig } from "../stores/preferences" import DirectoryBrowserDialog from "./directory-browser-dialog" import Kbd from "./kbd" @@ -12,14 +11,16 @@ import VersionPill from "./version-pill" import { DiscordSymbolIcon, GitHubMarkIcon } from "./brand-icons" import { githubStars } from "../stores/github-stars" import { formatCompactCount } from "../lib/formatters" -import { useI18n, type Locale } from "../lib/i18n" +import { useI18n } from "../lib/i18n" import { showAlertDialog } from "../stores/alerts" import { openSettings, settingsOpen } from "../stores/settings-screen" import { openExternalUrl } from "../lib/external-url" import { serverApi } from "../lib/api-client" -import { canOpenRemoteWindows, isTauriHost } from "../lib/runtime-env" -import { openRemoteServerWindow } from "../lib/native/remote-window" +import { canOpenRemoteWindows } from "../lib/runtime-env" import { getExistingInstanceForFolder, updateProjectNameForFolder } from "../stores/instances" +import { LocaleSelector } from "./locale-selector" +import { RemoteServerDialog } from "./remote-server-dialog" +import { useRemoteServerProfiles } from "../lib/hooks/use-remote-server-profiles" const codeNomadLogo = new URL("../images/CodeNomad-Icon.png", import.meta.url).href const GITHUB_URL = "https://github.com/NeuralNomadsAI/CodeNomad" @@ -41,15 +42,10 @@ const FolderSelectionView: Component = (props) => { recentFolders, removeRecentFolder, renameRecentFolderProject, - preferences, - updatePreferences, serverSettings, - remoteServers, - saveRemoteServerProfile, - markRemoteServerConnected, - removeRemoteServerProfile, } = useConfig() - const { t, locale } = useI18n() + const { remoteServers, connectingServerId, saveServer, connectSavedServer, removeRemoteServerProfile } = useRemoteServerProfiles() + const { t } = useI18n() const [selectedIndex, setSelectedIndex] = createSignal(0) const [hoveredRecentActionPath, setHoveredRecentActionPath] = createSignal(null) const [focusedRecentActionPath, setFocusedRecentActionPath] = createSignal(null) @@ -67,32 +63,10 @@ const FolderSelectionView: Component = (props) => { const [isCloningRepository, setIsCloningRepository] = createSignal(false) const [activeTab, setActiveTab] = createSignal("local") const [isServerDialogOpen, setIsServerDialogOpen] = createSignal(false) - const [serverName, setServerName] = createSignal("") - const [serverUrl, setServerUrl] = createSignal("") - const [skipTlsVerify, setSkipTlsVerify] = createSignal(false) - const [serverDialogError, setServerDialogError] = createSignal(null) - const [isSavingServer, setIsSavingServer] = createSignal(false) - const [connectingServerId, setConnectingServerId] = createSignal(null) let homeRootRef: HTMLDivElement | undefined let actionsColumnRef: HTMLDivElement | undefined let recentListRef: HTMLDivElement | undefined - type LanguageOption = { value: Locale; label: string } - - const languageOptions: LanguageOption[] = [ - { value: "en", label: "English" }, - { value: "es", label: "Español" }, - { value: "fr", label: "Français" }, - { value: "ru", label: "Русский" }, - { value: "ja", label: "日本語" }, - { value: "zh-Hans", label: "简体中文" }, - { value: "he", label: "עברית" }, - { value: "de", label: "Deutsch" }, - { value: "ne", label: "नेपाली" }, - ] - - const selectedLanguageOption = () => languageOptions.find((opt) => opt.value === locale()) ?? languageOptions[0] - const folders = () => recentFolders() const serverList = () => remoteServers() const isLoading = () => Boolean(props.isLoading) @@ -222,7 +196,7 @@ const FolderSelectionView: Component = (props) => { const server = serverList()[index] if (server) { - void handleConnectSavedServer(server.id) + void connectSavedServer(server.id) } } @@ -378,109 +352,11 @@ const FolderSelectionView: Component = (props) => { } } - function resetServerDialog() { - setServerName("") - setServerUrl("") - setSkipTlsVerify(false) - setServerDialogError(null) - } - function openServerDialog() { if (!canUseRemoteServerWindows()) return - resetServerDialog() setIsServerDialogOpen(true) } - async function probeAndOpenServer(input: { id?: string; name: string; baseUrl: string; skipTlsVerify: boolean }, openWindow: boolean) { - if (openWindow && !canUseRemoteServerWindows()) { - throw new Error("Remote server windows can only be opened from a local desktop window") - } - - const trimmedName = input.name.trim() - const trimmedUrl = input.baseUrl.trim() - if (!trimmedName || !trimmedUrl) { - throw new Error(t("folderSelection.servers.dialog.errorRequired")) - } - - const probe = await serverApi.probeRemoteServer({ - baseUrl: trimmedUrl, - skipTlsVerify: input.skipTlsVerify, - }) - - if (!probe.ok) { - throw new Error(probe.error || t("folderSelection.servers.dialog.errorConnect")) - } - - const profile = await saveRemoteServerProfile({ - id: input.id, - name: trimmedName, - baseUrl: probe.normalizedUrl, - skipTlsVerify: input.skipTlsVerify, - }) - - if (openWindow) { - const remoteProxySession = - isTauriHost() && profile.skipTlsVerify && profile.baseUrl.startsWith("https://") - ? await serverApi.createRemoteProxySession({ - baseUrl: profile.baseUrl, - skipTlsVerify: profile.skipTlsVerify, - }) - : undefined - - try { - await openRemoteServerWindow(profile, remoteProxySession?.windowUrl, remoteProxySession?.sessionId) - } catch (error) { - if (remoteProxySession) { - void serverApi.deleteRemoteProxySession(remoteProxySession.sessionId).catch(() => {}) - } - throw error - } - - await markRemoteServerConnected(profile.id) - } - - return profile - } - - async function handleSaveServer(openWindow: boolean) { - if (isSavingServer()) return - setIsSavingServer(true) - setServerDialogError(null) - try { - await probeAndOpenServer( - { - name: serverName(), - baseUrl: serverUrl(), - skipTlsVerify: skipTlsVerify(), - }, - openWindow, - ) - setIsServerDialogOpen(false) - resetServerDialog() - } catch (error) { - setServerDialogError(error instanceof Error ? error.message : String(error)) - } finally { - setIsSavingServer(false) - } - } - - async function handleConnectSavedServer(id: string) { - if (!canUseRemoteServerWindows()) return - const target = remoteServers().find((entry) => entry.id === id) - if (!target || connectingServerId()) return - setConnectingServerId(id) - try { - await probeAndOpenServer(target, true) - } catch (error) { - showAlertDialog(error instanceof Error ? error.message : String(error), { - title: t("folderSelection.servers.errorTitle"), - variant: "warning", - }) - } finally { - setConnectingServerId(null) - } - } - async function handleBrowse() { if (isLoading()) return setFocusMode("new") @@ -647,54 +523,13 @@ const FolderSelectionView: Component = (props) => { aria-busy={isLoading() ? "true" : "false"} >
- - value={selectedLanguageOption()} - onChange={(value) => { - if (!value) return - if (value.value === locale()) return - updatePreferences({ locale: value.value }) - }} - options={languageOptions} - optionValue="value" - optionTextValue="label" - itemComponent={(itemProps) => ( - - {itemProps.item.rawValue.label} - - )} - > - - - - - - - - - +
@@ -1326,81 +1162,7 @@ const FolderSelectionView: Component = (props) => { - !open && setIsServerDialogOpen(false)}> - - -
- -
- - {t("folderSelection.servers.dialog.title")} - - - {t("folderSelection.servers.dialog.description")} - -
- - - - - - - - - {(message) =>

{message()}

} -
- -
- - - -
-
-
-
-
+ ) } diff --git a/packages/ui/src/components/instance-tabs.tsx b/packages/ui/src/components/instance-tabs.tsx index 548667085..6de898c03 100644 --- a/packages/ui/src/components/instance-tabs.tsx +++ b/packages/ui/src/components/instance-tabs.tsx @@ -226,7 +226,7 @@ const InstanceTabs: Component = (props) => {
) } diff --git a/packages/ui/src/components/message-preview.tsx b/packages/ui/src/components/message-preview.tsx index fbac99f9d..6b7c855e6 100644 --- a/packages/ui/src/components/message-preview.tsx +++ b/packages/ui/src/components/message-preview.tsx @@ -29,7 +29,8 @@ const MessagePreview: Component = (props) => { lastAssistantIndex={lastAssistantIndex} showThinking={() => false} thinkingDefaultExpanded={() => false} - showUsageMetrics={() => false} + usageMetricsVisibility={() => "hidden"} + toolVisibility={() => "expanded"} deleteHover={props.deleteHover} onDeleteHoverChange={props.onDeleteHoverChange} onDeleteMessagesUpTo={props.onDeleteMessagesUpTo} diff --git a/packages/ui/src/components/message-section.tsx b/packages/ui/src/components/message-section.tsx index 88a69c00a..ee34a83f3 100644 --- a/packages/ui/src/components/message-section.tsx +++ b/packages/ui/src/components/message-section.tsx @@ -23,7 +23,7 @@ import { buildRecordDisplayData } from "../stores/message-v2/record-display-cach import { getPartCharCount } from "../lib/token-utils" import { buildSessionSearchMatches } from "../lib/session-search" import type { SessionSearchMatch } from "../lib/session-search" -import { resolveThinkingExpansionDefault } from "./tool-call/tool-registry" +import { resolveThinkingExpansionDefault, resolveToolVisibility } from "./tool-call/tool-registry" import { collectToolDeletionCompanionPartIds, executeBulkDeletionPlan } from "./tool-deletion-companions" const MESSAGE_SCROLL_CACHE_SCOPE = "message-stream" @@ -57,7 +57,8 @@ export interface MessageSectionProps { export default function MessageSection(props: MessageSectionProps) { const { preferences, updatePreferences } = useConfig() const { t } = useI18n() - const showUsagePreference = () => preferences().showUsageMetrics ?? true + const usageMetricsVisibility = () => + preferences().showUsageMetrics ? preferences().usageMetricsExpansion : "hidden" const showMessageTimelinePreference = () => preferences().showMessageTimeline ?? true const showTimelineToolsPreference = () => preferences().showTimelineTools ?? true const holdLongAssistantRepliesEnabled = () => preferences().holdLongAssistantReplies ?? true @@ -121,8 +122,8 @@ export default function MessageSection(props: MessageSectionProps) { const pref = preferences() const showThinking = pref.showThinkingBlocks ? 1 : 0 const thinkingExpansion = resolveThinkingExpansionDefault(pref) ? "expanded" : "collapsed" - const showUsage = (pref.showUsageMetrics ?? true) ? 1 : 0 - return `${showThinking}|${thinkingExpansion}|${showUsage}` + const usageVisibility = pref.showUsageMetrics ? pref.usageMetricsExpansion : "hidden" + return `${showThinking}|${thinkingExpansion}|${usageVisibility}` }) const handleTimelineSegmentClick = (segment: TimelineSegment) => { @@ -1605,7 +1606,8 @@ export default function MessageSection(props: MessageSectionProps) { lastAssistantIndex={lastAssistantIndex} showThinking={() => preferences().showThinkingBlocks} thinkingDefaultExpanded={() => resolveThinkingExpansionDefault(preferences())} - showUsageMetrics={showUsagePreference} + usageMetricsVisibility={usageMetricsVisibility} + toolVisibility={(toolName) => resolveToolVisibility(preferences(), toolName)} deleteHover={deleteHover} onDeleteHoverChange={setDeleteHover} selectedMessageIds={selectedForDeletion} diff --git a/packages/ui/src/components/provider-auth/provider-manager-modal.tsx b/packages/ui/src/components/provider-auth/provider-manager-modal.tsx index 89cbf3b62..a7f4e5b81 100644 --- a/packages/ui/src/components/provider-auth/provider-manager-modal.tsx +++ b/packages/ui/src/components/provider-auth/provider-manager-modal.tsx @@ -1,6 +1,6 @@ import { Dialog } from "@kobalte/core/dialog" import { Select } from "@kobalte/core/select" -import { createEffect, createMemo, createSignal, For, Show, type Component } from "solid-js" +import { createEffect, createMemo, createSignal, For, onCleanup, Show, type Component } from "solid-js" import { Check, ChevronDown, ExternalLink, KeyRound, Loader2, PlugZap, RefreshCw, ShieldCheck, X } from "lucide-solid" import type { OpencodeClient } from "@opencode-ai/sdk/v2/client" import { openExternalUrl } from "../../lib/external-url" @@ -45,8 +45,9 @@ type DisconnectMode = "auth-remove" | "disable-in-config" | "not-disconnectable" interface ProviderManagerModalProps { instanceId: string - open: boolean - onOpenChange: (open: boolean) => void + open?: boolean + embedded?: boolean + onOpenChange?: (open: boolean) => void } function modelCountFromProvider(provider: any): number { @@ -78,9 +79,15 @@ export const ProviderManagerModal: Component = (props const [authorizationLinkCopied, setAuthorizationLinkCopied] = createSignal(false) let callbackAbortController: AbortController | null = null let pendingOauthPopup: Window | null = null + let loadVersion = 0 + let authOperationVersion = 0 + let oauthCodeInput: HTMLInputElement | undefined const instance = createMemo(() => instances().get(props.instanceId) ?? null) - const client = createMemo(() => instance()?.client ?? null) + const client = createMemo(() => { + const current = instance() + return current?.status === "ready" ? current.client ?? null : null + }) const providerNameById = createMemo(() => { const names = new Map() @@ -175,7 +182,7 @@ export const ProviderManagerModal: Component = (props function handleModalOpenChange(open: boolean) { if (!open) resetFlow(null) - props.onOpenChange(open) + props.onOpenChange?.(open) } function isBrowserHostForOAuth(): boolean { @@ -234,13 +241,24 @@ export const ProviderManagerModal: Component = (props } createEffect(() => { - if (!props.open) return + const version = ++loadVersion + resetProviderData() + if (!props.embedded && !props.open) return const authClient = client() if (!authClient) return - void loadProviderData(authClient) + void loadProviderData(authClient, version) + }) + + createEffect(() => { + if (stage() === "code") queueMicrotask(() => oauthCodeInput?.focus()) + }) + + onCleanup(() => { + loadVersion += 1 + disposePendingAuth() }) - async function loadProviderData(authClient: OpencodeClient): Promise { + async function loadProviderData(authClient: OpencodeClient, version = ++loadVersion): Promise { setLoading(true) setLoadError(null) try { @@ -249,6 +267,7 @@ export const ProviderManagerModal: Component = (props (authClient as any).provider.auth(), (authClient as any).config.get(), ]) + if (version !== loadVersion) return const nextConfigData = (configResponse?.data ?? {}) as Record const nextConfiguredIds = new Set(Object.keys((nextConfigData.provider ?? {}) as Record)) const listed = ((providerListResponse?.data?.all ?? []) as any[]).map((provider) => ({ @@ -267,19 +286,35 @@ export const ProviderManagerModal: Component = (props setMethodsByProvider((authResponse?.data ?? {}) as Record) setSelectedProviderId((current) => current ?? listed[0]?.id ?? Object.keys(authResponse?.data ?? {})[0] ?? null) } catch (error) { + if (version !== loadVersion) return setLoadError(extractProviderAuthErrorMessage(error, t("settings.providers.errors.loadFailed"))) } finally { - setLoading(false) + if (version === loadVersion) setLoading(false) } } - function resetFlow(nextProviderId: string | null = null) { + function disposePendingAuth() { + authOperationVersion += 1 callbackAbortController?.abort() callbackAbortController = null - if (pendingOauthPopup && !pendingOauthPopup.closed) { - pendingOauthPopup.close() - } + if (pendingOauthPopup && !pendingOauthPopup.closed) pendingOauthPopup.close() pendingOauthPopup = null + } + + function resetProviderData() { + resetFlow(null) + setMethodsByProvider({}) + setAvailableProviders([]) + setConnectedProviderIds(new Set()) + setConfiguredProviderIds(new Set()) + setConfigData({}) + setSelectedProviderId(null) + setLoadError(null) + setLoading(false) + } + + function resetFlow(nextProviderId: string | null = null) { + disposePendingAuth() setActiveProviderId(nextProviderId) setSelectedMethodIndex(0) setApiKey("") @@ -296,30 +331,40 @@ export const ProviderManagerModal: Component = (props setPromptValues((current) => ({ ...current, [key]: value })) } - async function refreshAfterAuth(authClient: OpencodeClient) { + function isCurrentOperation(version: number, instanceId: string, authClient: OpencodeClient) { + return version === authOperationVersion && props.instanceId === instanceId && client() === authClient + } + + async function refreshAfterAuth(authClient: OpencodeClient, instanceId: string, operationVersion: number) { + if (!isCurrentOperation(operationVersion, instanceId, authClient)) return await (authClient as any).global.dispose().catch(() => undefined) - await fetchProviders(props.instanceId).catch(() => undefined) + if (!isCurrentOperation(operationVersion, instanceId, authClient)) return + await fetchProviders(instanceId).catch(() => undefined) + if (!isCurrentOperation(operationVersion, instanceId, authClient)) return await loadProviderData(authClient).catch(() => undefined) } - async function submitApiAuth(providerId: string, authClient: OpencodeClient) { + async function submitApiAuth(providerId: string, authClient: OpencodeClient, instanceId: string, operationVersion: number) { await requestData( (authClient as any).auth.set({ providerID: providerId, auth: { type: "api", key: apiKey().trim() } }), "auth.set", ) - await refreshAfterAuth(authClient) - resetFlow(null) + if (!isCurrentOperation(operationVersion, instanceId, authClient)) return + await refreshAfterAuth(authClient, instanceId, operationVersion) + if (isCurrentOperation(operationVersion, instanceId, authClient)) resetFlow(null) } - async function submitOAuthAuthorize(providerId: string, authClient: OpencodeClient) { + async function submitOAuthAuthorize(providerId: string, authClient: OpencodeClient, instanceId: string, operationVersion: number) { const response = await (authClient as any).provider.oauth.authorize( { providerID: providerId, method: selectedMethodIndex() }, { throwOnError: true }, ) + if (!isCurrentOperation(operationVersion, instanceId, authClient)) return const data = response?.data as ProviderAuthAuthorization | undefined if (!data) throw new Error(t("settings.providers.errors.noAuthorization")) setAuthorization(data) const opened = await launchAuthorizationUrl(data.url, { popup: pendingOauthPopup }) + if (!isCurrentOperation(operationVersion, instanceId, authClient)) return pendingOauthPopup = null setAuthorizationLaunchBlocked(!opened) if (data.method === "code") { @@ -335,26 +380,30 @@ export const ProviderManagerModal: Component = (props ), "provider.oauth.callback", ) + if (!isCurrentOperation(operationVersion, instanceId, authClient)) return callbackAbortController = null - await refreshAfterAuth(authClient) - resetFlow(null) + await refreshAfterAuth(authClient, instanceId, operationVersion) + if (isCurrentOperation(operationVersion, instanceId, authClient)) resetFlow(null) } async function submitAuth() { const providerId = activeProviderId() const authClient = client() if (!providerId || !authClient || !canSubmit()) return + const instanceId = props.instanceId + const operationVersion = ++authOperationVersion setStage("authorizing") setActionError(null) try { if (selectedMethod().type === "api") { - await submitApiAuth(providerId, authClient) + await submitApiAuth(providerId, authClient, instanceId, operationVersion) return } pendingOauthPopup = prepareOAuthPopupWindow() setAuthorizationLaunchBlocked(isBrowserHostForOAuth() && pendingOauthPopup === null) - await submitOAuthAuthorize(providerId, authClient) + await submitOAuthAuthorize(providerId, authClient, instanceId, operationVersion) } catch (error) { + if (!isCurrentOperation(operationVersion, instanceId, authClient)) return if (pendingOauthPopup && !pendingOauthPopup.closed) { pendingOauthPopup.close() } @@ -372,6 +421,8 @@ export const ProviderManagerModal: Component = (props const providerId = activeProviderId() const authClient = client() if (!providerId || !authClient || !code().trim()) return + const instanceId = props.instanceId + const operationVersion = ++authOperationVersion setStage("authorizing") setActionError(null) try { @@ -379,9 +430,11 @@ export const ProviderManagerModal: Component = (props (authClient as any).provider.oauth.callback({ providerID: providerId, method: selectedMethodIndex(), code: code().trim() }), "provider.oauth.callback", ) - await refreshAfterAuth(authClient) - resetFlow(null) + if (!isCurrentOperation(operationVersion, instanceId, authClient)) return + await refreshAfterAuth(authClient, instanceId, operationVersion) + if (isCurrentOperation(operationVersion, instanceId, authClient)) resetFlow(null) } catch (error) { + if (!isCurrentOperation(operationVersion, instanceId, authClient)) return setActionError(extractProviderAuthErrorMessage(error, t("settings.providers.errors.authorizationFailed"))) setStage("code") } @@ -391,6 +444,9 @@ export const ProviderManagerModal: Component = (props const authClient = client() const provider = availableProviders().find((item) => item.id === providerId) if (!authClient || !provider) return + const instanceId = props.instanceId + disposePendingAuth() + const operationVersion = ++authOperationVersion setActionError(null) setStage("authorizing") try { @@ -420,17 +476,18 @@ export const ProviderManagerModal: Component = (props } else { await requestData((authClient as any).auth.remove({ providerID: providerId }), "auth.remove") } - await refreshAfterAuth(authClient) - resetFlow(null) + if (!isCurrentOperation(operationVersion, instanceId, authClient)) return + await refreshAfterAuth(authClient, instanceId, operationVersion) + if (isCurrentOperation(operationVersion, instanceId, authClient)) resetFlow(null) } catch (error) { + if (!isCurrentOperation(operationVersion, instanceId, authClient)) return setActionError(extractProviderAuthErrorMessage(error, t("settings.providers.errors.removeFailed"))) - setStage("error") + setStage("idle") } } function cancelOAuthWait() { - callbackAbortController?.abort() - callbackAbortController = null + disposePendingAuth() setStage("prompts") setAuthorization(null) setActionError(null) @@ -445,27 +502,31 @@ export const ProviderManagerModal: Component = (props return t("settings.providers.method.api") } - return ( - - - - + const content = () => ( + <>
- {t("settings.providers.title")} + {t("settings.providers.title")}} + > + {t("settings.providers.title")} +

{t("settings.providers.subtitle")}

- + + +
-
{t("settings.providers.empty.noInstance")}
+
{t("settings.providers.empty.noInstance")}
@@ -524,7 +585,10 @@ export const ProviderManagerModal: Component = (props
-
{loadError()}
+ +
+ + @@ -563,6 +627,7 @@ export const ProviderManagerModal: Component = (props options={methodOptions()} optionValue="value" optionTextValue="label" + disabled={stage() !== "prompts" && stage() !== "error"} itemComponent={(itemProps) => {itemProps.item.rawValue.label}} > @@ -581,9 +646,9 @@ export const ProviderManagerModal: Component = (props
-
{t("settings.providers.oauth.promptsUnsupported")}
+
-
{t("settings.providers.oauth.noPrompts")}
+
{t("settings.providers.oauth.noPrompts")}
{(prompt) => (
@@ -598,8 +663,8 @@ export const ProviderManagerModal: Component = (props
-
{authorization()?.instructions || t("settings.providers.oauth.enterCode")}
-
{t("settings.providers.oauth.waitingTitle")}
{authorization()?.instructions}
+
{authorization()?.instructions || t("settings.providers.oauth.enterCode")}
+
{t("settings.providers.oauth.waitingTitle")}
{authorization()?.instructions}
-
{t("settings.providers.oauth.popupBlocked")}
+
-
{t("settings.providers.success")}
-
{actionError()}
+
{t("settings.providers.success")}
void submitAuth()}>{t("settings.providers.actions.working")}}> @@ -630,19 +694,32 @@ export const ProviderManagerModal: Component = (props

{t("settings.providers.configured.title")}

-
{t("settings.providers.loading")}
-
{t("settings.providers.empty.noConfiguredProviders")}
+
{t("settings.providers.loading")}
+
{t("settings.providers.empty.noConfiguredProviders")}
{(provider) => (

{provider.name || provider.id}

{provider.id}

{methodSummary(provider.id)}

{describeProviderSource(provider)}

- +
)}
+ + ) + + if (props.embedded) { + return
{content()}
+ } + + return ( + + + + + {content()} diff --git a/packages/ui/src/components/remote-server-dialog.tsx b/packages/ui/src/components/remote-server-dialog.tsx new file mode 100644 index 000000000..dc5509f37 --- /dev/null +++ b/packages/ui/src/components/remote-server-dialog.tsx @@ -0,0 +1,80 @@ +import { Dialog } from "@kobalte/core/dialog" +import { Loader2 } from "lucide-solid" +import { createEffect, createSignal, Show, type Component } from "solid-js" +import { useI18n } from "../lib/i18n" +import type { RemoteServerInput } from "../lib/hooks/use-remote-server-profiles" + +interface RemoteServerDialogProps { + open: boolean + onOpenChange: (open: boolean) => void + onSubmit: (input: RemoteServerInput, openWindow: boolean) => Promise +} + +export const RemoteServerDialog: Component = (props) => { + const { t } = useI18n() + const [name, setName] = createSignal("") + const [baseUrl, setBaseUrl] = createSignal("") + const [skipTlsVerify, setSkipTlsVerify] = createSignal(false) + const [error, setError] = createSignal(null) + const [busy, setBusy] = createSignal(false) + + createEffect(() => { + if (!props.open) return + setName("") + setBaseUrl("") + setSkipTlsVerify(false) + setError(null) + }) + + const submit = async (openWindow: boolean) => { + if (busy()) return + setBusy(true) + setError(null) + try { + await props.onSubmit({ name: name(), baseUrl: baseUrl(), skipTlsVerify: skipTlsVerify() }, openWindow) + props.onOpenChange(false) + } catch (submitError) { + setError(submitError instanceof Error ? submitError.message : String(submitError)) + } finally { + setBusy(false) + } + } + + return ( + + + +
+ +
+ {t("folderSelection.servers.dialog.title")} + {t("folderSelection.servers.dialog.description")} +
+ + + + {(message) =>

{message()}

}
+
+ + + +
+
+
+
+
+ ) +} diff --git a/packages/ui/src/components/settings-screen.tsx b/packages/ui/src/components/settings-screen.tsx index 989bfe5b2..adc07b773 100644 --- a/packages/ui/src/components/settings-screen.tsx +++ b/packages/ui/src/components/settings-screen.tsx @@ -1,6 +1,6 @@ import { Dialog } from "@kobalte/core/dialog" import { Select } from "@kobalte/core/select" -import { Settings, Bell, ChevronDown, FileCog, Info, MonitorUp, Paintbrush, Terminal, Volume2, Globe, X } from "lucide-solid" +import { Settings, Bell, ChevronDown, FileCog, Globe, Info, MessageSquare, MonitorUp, PlugZap, SlidersHorizontal, Terminal, Volume2, X } from "lucide-solid" import { createMemo, For, type Component } from "solid-js" import { useI18n } from "../lib/i18n" import { @@ -10,13 +10,17 @@ import { setActiveSettingsSection, type SettingsSectionId, } from "../stores/settings-screen" -import { AppearanceSettingsSection } from "./settings/appearance-settings-section" +import { GeneralSettingsSection } from "./settings/general-settings-section" +import { ChatSettingsSection } from "./settings/chat-settings-section" import { InfoSettingsSection } from "./settings/info-settings-section" import { NotificationsSettingsSection } from "./settings/notifications-settings-section" +import { SpeechSettingsSection } from "./settings/speech-settings-section" +import { ProvidersSettingsSection } from "./settings/providers-settings-section" import { OpenCodeSettingsSection } from "./settings/opencode-settings-section" +import { AdvancedSettingsSection } from "./settings/advanced-settings-section" import { ConfigFilesSettingsSection } from "./settings/config-files-settings-section" import { RemoteAccessSettingsSection } from "./settings/remote-access-settings-section" -import { SpeechSettingsSection } from "./settings/speech-settings-section" +import { SavedRemoteServersCard } from "./settings/saved-remote-servers-card" import { SideCarsSettingsSection } from "./settings/sidecars-settings-section" import { canOpenRemoteWindows } from "../lib/runtime-env" import { confirmSettingsDiscard } from "../stores/settings-dirty-guard" @@ -32,19 +36,20 @@ export const SettingsScreen: Component = () => { const sections = createMemo(() => { const items: SettingsSectionOption[] = [ - { id: "appearance", icon: Paintbrush, label: t("settings.nav.appearance") }, + { id: "general", icon: SlidersHorizontal, label: t("settings.nav.general") }, + { id: "chat", icon: MessageSquare, label: t("settings.nav.chat") }, { id: "notifications", icon: Bell, label: t("settings.nav.notifications") }, { id: "speech", icon: Volume2, label: t("settings.nav.speech") }, - { id: "sidecars", icon: Globe, label: t("settings.nav.sidecars") }, { id: "opencode", icon: Terminal, label: t("settings.nav.opencode") }, + { id: "providers", icon: PlugZap, label: t("settings.nav.providers") }, + { id: "sidecars", icon: Globe, label: t("settings.nav.sidecars") }, { id: "config-files", icon: FileCog, label: t("settings.nav.configFiles") }, + { id: "advanced", icon: Settings, label: t("settings.nav.advanced") }, { id: "info", icon: Info, label: t("settings.nav.info") }, ] - if (canOpenRemoteWindows()) { - items.splice(2, 0, { id: "remote", icon: MonitorUp, label: t("settings.nav.remote") }) + items.splice(4, 0, { id: "remote", icon: MonitorUp, label: t("settings.nav.remote") }) } - return items }) @@ -52,23 +57,34 @@ export const SettingsScreen: Component = () => { const renderSection = () => { switch (activeSettingsSection()) { + case "chat": + return case "notifications": return - case "remote": - return canOpenRemoteWindows() ? : case "speech": return - case "sidecars": - return + case "remote": + return canOpenRemoteWindows() ? ( +
+ + +
+ ) : case "opencode": return + case "providers": + return + case "sidecars": + return case "config-files": return + case "advanced": + return case "info": return - case "appearance": + case "general": default: - return + return } } @@ -171,6 +187,7 @@ export const SettingsScreen: Component = () => { type="button" class="settings-nav-button" data-selected={activeSettingsSection() === section.id ? "true" : "false"} + aria-current={activeSettingsSection() === section.id ? "page" : undefined} onClick={() => void handleSectionChange(section.id)} > diff --git a/packages/ui/src/components/settings/advanced-settings-section.tsx b/packages/ui/src/components/settings/advanced-settings-section.tsx new file mode 100644 index 000000000..6de2da029 --- /dev/null +++ b/packages/ui/src/components/settings/advanced-settings-section.tsx @@ -0,0 +1,40 @@ +import { createMemo, type Component } from "solid-js" +import { useI18n } from "../../lib/i18n" +import { getBehaviorSettings } from "../../lib/settings/behavior-registry" +import { useConfig } from "../../stores/preferences" +import EnvironmentVariablesEditor from "../environment-variables-editor" +import { BehaviorSettingRows } from "./behavior-setting-rows" + +export const AdvancedSettingsSection: Component = () => { + const { t } = useI18n() + const config = useConfig() + const advancedSettings = createMemo(() => + getBehaviorSettings(config).filter( + (setting) => + setting.id === "behavior.autoCleanupBlankSessions" || + setting.id === "behavior.keepUnseenSubagentIdleStatus" || + setting.id === "behavior.tauriNativeEventTransport", + ), + ) + + return ( +
+
+
+
+

{t("advancedSettings.environmentVariables.title")}

+

{t("advancedSettings.environmentVariables.subtitle")}

+
+ {t("settings.scope.server")} +
+ +
+ +
+
+ +
+
+
+ ) +} diff --git a/packages/ui/src/components/settings/appearance-settings-section.tsx b/packages/ui/src/components/settings/appearance-settings-section.tsx deleted file mode 100644 index cac4e62b4..000000000 --- a/packages/ui/src/components/settings/appearance-settings-section.tsx +++ /dev/null @@ -1,437 +0,0 @@ -import { Select } from "@kobalte/core/select" -import { createEffect, createMemo, createSignal, For, type Component } from "solid-js" -import { Check, ChevronDown, Laptop, Moon, Sun } from "lucide-solid" -import { useI18n } from "../../lib/i18n" -import { useTheme, type ThemeMode } from "../../lib/theme" -import { useConfig, type ExpansionPreference, type ToolCallExpansionPreset } from "../../stores/preferences" -import { getBehaviorSettings, type BehaviorSetting } from "../../lib/settings/behavior-registry" -import { StartupStateSettingsCard } from "./startup-state-settings-card" -import { - buildToolExpansionPresetDefaults, - getConfigurableToolEntries, - OTHER_TOOL_NAME, - THINKING_EXPANSION_PRESETS, -} from "../tool-call/tool-registry" - -const themeModeOptions: Array<{ value: ThemeMode; icon: typeof Laptop }> = [ - { value: "system", icon: Laptop }, - { value: "light", icon: Sun }, - { value: "dark", icon: Moon }, -] - -const toolExpansionPresetOptions: ToolCallExpansionPreset[] = ["minimal", "balanced", "detailed", "everything"] - -export const AppearanceSettingsSection: Component = () => { - const { t } = useI18n() - const { themeMode, setThemeMode } = useTheme() - const { - preferences, - useTauriNativeEventTransport, - setUseTauriNativeEventTransport, - updatePreferences, - toggleShowThinkingBlocks, - toggleKeyboardShortcutHints, - toggleShowMessageTimeline, - toggleShowTimelineTools, - toggleUsageMetrics, - toggleAutoCleanupBlankSessions, - togglePromptSubmitOnEnter, - toggleShowPromptVoiceInput, - setDiffViewMode, - setToolOutputExpansion, - setDiagnosticsExpansion, - setThinkingBlocksExpansion, - setToolInputsVisibility, - } = useConfig() - const behaviorSettings = createMemo(() => - getBehaviorSettings({ - preferences, - useTauriNativeEventTransport, - setUseTauriNativeEventTransport, - updatePreferences, - toggleShowThinkingBlocks, - toggleKeyboardShortcutHints, - toggleShowMessageTimeline, - toggleShowTimelineTools, - toggleUsageMetrics, - toggleAutoCleanupBlankSessions, - togglePromptSubmitOnEnter, - toggleShowPromptVoiceInput, - setDiffViewMode, - setToolOutputExpansion, - setDiagnosticsExpansion, - setThinkingBlocksExpansion, - setToolInputsVisibility, - }).filter( - (setting) => setting.id !== "behavior.thinkingBlocksDefault" && setting.id !== "behavior.toolOutputsDefault", - ), - ) - - const [overrides, setOverrides] = createSignal>(new Map()) - - const setOverride = (id: string, value: unknown) => { - setOverrides((prev) => { - const next = new Map(prev) - next.set(id, value) - return next - }) - } - - createEffect(() => { - const current = overrides() - if (current.size === 0) return - - const prefs = preferences() - const settings = behaviorSettings() - - let changed = false - const next = new Map(current) - for (const setting of settings) { - if (!next.has(setting.id)) continue - const overrideValue = next.get(setting.id) - const actualValue = setting.get(prefs) - if (Object.is(actualValue, overrideValue)) { - next.delete(setting.id) - changed = true - } - } - - if (changed) { - setOverrides(next) - } - }) - - const readSettingValue = (setting: BehaviorSetting) => { - const current = overrides() - if (current.has(setting.id)) return current.get(setting.id) - return setting.get(preferences()) - } - - type SelectOption = { value: string; label: string } - - type ExpansionRow = - | { kind: "thinking"; key: "thinking"; label: string } - | { kind: "tool"; key: string; label: string } - - const expansionOptions = createMemo(() => [ - { value: "collapsed", label: t("commands.common.collapsed") }, - { value: "expanded", label: t("commands.common.expanded") }, - ]) - - const toolExpansionRows = createMemo(() => [ - { kind: "thinking", key: "thinking", label: t("settings.behavior.expansionDefaults.thinking") }, - ...getConfigurableToolEntries().map((entry) => ({ - kind: "tool" as const, - key: entry.tool, - label: entry.labelKey ? t(entry.labelKey) : entry.label, - })), - ]) - - const currentPreset = createMemo(() => preferences().toolCallExpansionDefaults.preset) - - const currentToolMode = (tool: string): ExpansionPreference => { - const pref = preferences().toolCallExpansionDefaults - const entry = getConfigurableToolEntries().find((item) => item.tool === tool) - if (pref.tools[tool]) return pref.tools[tool] - if (pref.preset !== "custom" && entry) return entry.expansionPresets[pref.preset] - return pref.tools[OTHER_TOOL_NAME] ?? "expanded" - } - - const currentThinkingMode = (): ExpansionPreference => { - const pref = preferences().toolCallExpansionDefaults - if (pref.thinking) return pref.thinking - if (pref.preset !== "custom") return THINKING_EXPANSION_PRESETS[pref.preset] - return preferences().thinkingBlocksExpansion ?? "expanded" - } - - const materializeToolModes = () => { - const tools: Record = {} - for (const entry of getConfigurableToolEntries()) { - tools[entry.tool] = currentToolMode(entry.tool) - } - return tools - } - - const applyExpansionPreset = (preset: ToolCallExpansionPreset) => { - const tools = buildToolExpansionPresetDefaults(preset) - const thinking = THINKING_EXPANSION_PRESETS[preset] - updatePreferences({ - toolCallExpansionDefaults: { preset, thinking, tools }, - thinkingBlocksExpansion: thinking, - toolOutputExpansion: tools[OTHER_TOOL_NAME] ?? "expanded", - }) - } - - const setExpansionRowMode = (row: ExpansionRow, mode: ExpansionPreference) => { - const tools = materializeToolModes() - const thinking = row.kind === "thinking" ? mode : currentThinkingMode() - if (row.kind === "tool") { - tools[row.key] = mode - } - updatePreferences({ - toolCallExpansionDefaults: { preset: "custom", thinking, tools }, - thinkingBlocksExpansion: thinking, - toolOutputExpansion: tools[OTHER_TOOL_NAME] ?? preferences().toolOutputExpansion, - }) - } - - const rowMode = (row: ExpansionRow): ExpansionPreference => - row.kind === "thinking" ? currentThinkingMode() : currentToolMode(row.key) - - const selectedExpansionOption = (mode: ExpansionPreference) => - expansionOptions().find((opt) => opt.value === mode) - - const BehaviorRow: Component<{ setting: BehaviorSetting }> = (props) => { - const setting = props.setting - const disabled = createMemo(() => (setting.disabled ? Boolean(setting.disabled()) : false)) - - if (setting.kind === "toggle") { - const options = createMemo(() => [ - { value: "true", label: t("settings.common.enabled") }, - { value: "false", label: t("settings.common.disabled") }, - ]) - const currentValue = createMemo(() => String(Boolean(readSettingValue(setting)))) - const selectedOption = createMemo(() => options().find((opt) => opt.value === currentValue())) - - return ( -
-
-
{t(setting.titleKey)}
-
{t(setting.subtitleKey)}
-
- - value={selectedOption()} - onChange={(opt) => { - if (!opt) return - const next = opt.value === "true" - setOverride(setting.id, next) - setting.set(next) - }} - options={options()} - optionValue="value" - optionTextValue="label" - disabled={disabled()} - itemComponent={(itemProps) => ( - - {itemProps.item.rawValue.label} - - )} - > - -
- > - {(state) => ( - - {state.selectedOption()?.label} - - )} - -
- - - -
- - - - - - - -
- ) - } - - const enumSetting = setting as Extract - const options = createMemo(() => - enumSetting.options.map((opt: { value: string; labelKey: string }) => ({ - value: String(opt.value), - label: t(opt.labelKey), - })), - ) - const currentValue = createMemo(() => String(readSettingValue(setting) ?? "")) - const selectedOption = createMemo(() => options().find((opt) => opt.value === currentValue())) - - return ( -
-
-
{t(setting.titleKey)}
-
{t(setting.subtitleKey)}
-
- - value={selectedOption()} - onChange={(opt) => { - if (!opt) return - setOverride(setting.id, opt.value) - enumSetting.set(opt.value as any) - }} - options={options()} - optionValue="value" - optionTextValue="label" - disabled={disabled()} - itemComponent={(itemProps) => ( - - {itemProps.item.rawValue.label} - - )} - > - -
- > - {(state) => ( - - {state.selectedOption()?.label} - - )} - -
- - - -
- - - - - - - -
- ) - } - - const modeLabel = (mode: ThemeMode) => { - if (mode === "system") return t("theme.mode.system") - if (mode === "light") return t("theme.mode.light") - return t("theme.mode.dark") - } - - const presetLabel = (preset: ToolCallExpansionPreset | "custom") => t(`settings.behavior.expansionPreset.${preset}.title`) - - return ( -
-
-
-
-

{t("settings.appearance.theme.title")}

-

{t("settings.appearance.theme.subtitle")}

-
- {t("settings.scope.device")} -
-
- {themeModeOptions.map((option) => { - const Icon = option.icon - return ( - - ) - })} -
-
- - - -
-
-
-

{t("settings.appearance.behavior.title")}

-

{t("settings.appearance.behavior.subtitle")}

-
- {presetLabel(currentPreset())} -
- -
- - {(preset) => ( - - )} - -
- -
-
- {t("settings.behavior.expansionDefaults.itemColumn")} - {t("settings.behavior.expansionDefaults.stateColumn")} -
- - {(row) => { - const selected = createMemo(() => selectedExpansionOption(rowMode(row))) - return ( -
-
- {row.label} -
-
- - value={selected()} - onChange={(opt) => { - if (!opt) return - setExpansionRowMode(row, opt.value as ExpansionPreference) - }} - options={expansionOptions()} - optionValue="value" - optionTextValue="label" - itemComponent={(itemProps) => ( - - {itemProps.item.rawValue.label} - - )} - > - -
- > - {(state) => ( - - {state.selectedOption()?.label} - - )} - -
- - - -
- - - - - - - -
-
- ) - }} -
-
- -
- {(setting) => } -
-
-
- ) -} diff --git a/packages/ui/src/components/settings/behavior-setting-rows.tsx b/packages/ui/src/components/settings/behavior-setting-rows.tsx new file mode 100644 index 000000000..bc5762069 --- /dev/null +++ b/packages/ui/src/components/settings/behavior-setting-rows.tsx @@ -0,0 +1,153 @@ +import { Select } from "@kobalte/core/select" +import { ChevronDown } from "lucide-solid" +import { createEffect, createMemo, createSignal, For, type Accessor, type Component } from "solid-js" +import { useI18n } from "../../lib/i18n" +import type { BehaviorSetting } from "../../lib/settings/behavior-registry" +import type { Preferences } from "../../stores/preferences" + +type SelectOption = { value: string; label: string } + +interface BehaviorSettingRowsProps { + settings: Accessor + preferences: Accessor +} + +export const BehaviorSettingRows: Component = (props) => { + const { t } = useI18n() + const [overrides, setOverrides] = createSignal>(new Map()) + + const setOverride = (id: string, value: unknown) => { + setOverrides((previous) => { + const next = new Map(previous) + next.set(id, value) + return next + }) + } + + createEffect(() => { + const current = overrides() + if (current.size === 0) return + + let changed = false + const next = new Map(current) + for (const setting of props.settings()) { + if (!next.has(setting.id)) continue + if (Object.is(setting.get(props.preferences()), next.get(setting.id))) { + next.delete(setting.id) + changed = true + } + } + + if (changed) setOverrides(next) + }) + + const readValue = (setting: BehaviorSetting) => { + const current = overrides() + return current.has(setting.id) ? current.get(setting.id) : setting.get(props.preferences()) + } + + const BehaviorRow: Component<{ setting: BehaviorSetting }> = (rowProps) => { + const setting = rowProps.setting + const disabled = createMemo(() => Boolean(setting.disabled?.())) + + if (setting.kind === "toggle") { + const options = createMemo(() => [ + { value: "true", label: t("settings.common.enabled") }, + { value: "false", label: t("settings.common.disabled") }, + ]) + const selectedOption = createMemo(() => options().find((option) => option.value === String(Boolean(readValue(setting))))) + + return ( +
+
+
{t(setting.titleKey)}
+
{t(setting.subtitleKey)}
+
+ + value={selectedOption()} + onChange={(option) => { + if (!option) return + const next = option.value === "true" + setOverride(setting.id, next) + setting.set(next) + }} + options={options()} + optionValue="value" + optionTextValue="label" + disabled={disabled()} + itemComponent={(itemProps) => ( + + {itemProps.item.rawValue.label} + + )} + > + +
+ > + {(state) => {state.selectedOption()?.label}} + +
+ + + +
+ + + + + + +
+ ) + } + + const options = createMemo(() => + setting.options.map((option) => ({ value: String(option.value), label: t(option.labelKey) })), + ) + const selectedOption = createMemo(() => options().find((option) => option.value === String(readValue(setting) ?? ""))) + + return ( +
+
+
{t(setting.titleKey)}
+
{t(setting.subtitleKey)}
+
+ + value={selectedOption()} + onChange={(option) => { + if (!option) return + setOverride(setting.id, option.value) + setting.set(option.value) + }} + options={options()} + optionValue="value" + optionTextValue="label" + disabled={disabled()} + itemComponent={(itemProps) => ( + + {itemProps.item.rawValue.label} + + )} + > + +
+ > + {(state) => {state.selectedOption()?.label}} + +
+ + + +
+ + + + + + +
+ ) + } + + return {(setting) => } +} diff --git a/packages/ui/src/components/settings/chat-settings-section.tsx b/packages/ui/src/components/settings/chat-settings-section.tsx new file mode 100644 index 000000000..595448b7c --- /dev/null +++ b/packages/ui/src/components/settings/chat-settings-section.tsx @@ -0,0 +1,270 @@ +import { Select } from "@kobalte/core/select" +import { createMemo, For, type Component } from "solid-js" +import { ChevronDown } from "lucide-solid" +import { useI18n } from "../../lib/i18n" +import { + useConfig, + type Preferences, + type ToolCallExpansionPreset, + type VisibilityPreference, +} from "../../stores/preferences" +import { + buildToolExpansionPresetDefaults, + getConfigurableToolEntries, + OTHER_TOOL_NAME, + THINKING_EXPANSION_PRESETS, +} from "../tool-call/tool-registry" + +const toolExpansionPresetOptions: ToolCallExpansionPreset[] = ["minimal", "balanced", "detailed", "everything"] + +const transcriptDetailPresets = { + minimal: { + showThinkingBlocks: false, + diagnosticsExpansion: "collapsed", + toolInputsVisibility: "hidden", + showUsageMetrics: false, + usageMetricsExpansion: "collapsed", + }, + balanced: { + showThinkingBlocks: false, + diagnosticsExpansion: "expanded", + toolInputsVisibility: "collapsed", + showUsageMetrics: true, + usageMetricsExpansion: "collapsed", + }, + detailed: { + showThinkingBlocks: true, + diagnosticsExpansion: "expanded", + toolInputsVisibility: "collapsed", + showUsageMetrics: true, + usageMetricsExpansion: "expanded", + }, + everything: { + showThinkingBlocks: true, + diagnosticsExpansion: "expanded", + toolInputsVisibility: "expanded", + showUsageMetrics: true, + usageMetricsExpansion: "expanded", + }, +} as const satisfies Record< + ToolCallExpansionPreset, + Pick< + Preferences, + | "showThinkingBlocks" + | "diagnosticsExpansion" + | "toolInputsVisibility" + | "showUsageMetrics" + | "usageMetricsExpansion" + > +> + +type SelectOption = { value: VisibilityPreference; label: string } + +type VisibilityRow = + | { kind: "thinking"; key: "thinking"; label: string } + | { kind: "tool"; key: string; label: string } + | { kind: "diagnostics"; key: "diagnostics"; label: string } + | { kind: "inputs"; key: "inputs"; label: string } + | { kind: "usage"; key: "usage"; label: string } + +export const ChatSettingsSection: Component = () => { + const { t } = useI18n() + const { preferences, updatePreferences } = useConfig() + + const visibilityOptions = createMemo(() => [ + { value: "hidden", label: t("commands.common.hidden") }, + { value: "collapsed", label: t("commands.common.collapsed") }, + { value: "expanded", label: t("commands.common.expanded") }, + ]) + + const visibilityRows = createMemo(() => [ + { kind: "thinking", key: "thinking", label: t("settings.behavior.expansionDefaults.thinking") }, + ...getConfigurableToolEntries().map((entry) => ({ + kind: "tool" as const, + key: entry.tool, + label: entry.labelKey ? t(entry.labelKey) : entry.label, + })), + { kind: "diagnostics", key: "diagnostics", label: t("settings.behavior.diagnosticsDefault.title") }, + { kind: "inputs", key: "inputs", label: t("settings.behavior.toolInputsVisibility.title") }, + { kind: "usage", key: "usage", label: t("settings.behavior.usageMetrics.title") }, + ]) + + const currentToolMode = (tool: string): VisibilityPreference => { + const pref = preferences().toolCallExpansionDefaults + const entry = getConfigurableToolEntries().find((item) => item.tool === tool) + if (pref.tools[tool]) return pref.tools[tool] + if (pref.preset !== "custom" && entry) return entry.expansionPresets[pref.preset] + return pref.tools[OTHER_TOOL_NAME] ?? "expanded" + } + + const currentThinkingExpansion = () => { + const current = preferences() + const pref = current.toolCallExpansionDefaults + if (pref.thinking) return pref.thinking + if (pref.preset !== "custom") return THINKING_EXPANSION_PRESETS[pref.preset] + return current.thinkingBlocksExpansion ?? "expanded" + } + + const currentThinkingMode = (): VisibilityPreference => + preferences().showThinkingBlocks ? currentThinkingExpansion() : "hidden" + + const currentUsageMode = (): VisibilityPreference => { + const current = preferences() + return current.showUsageMetrics ? current.usageMetricsExpansion : "hidden" + } + + const currentPreset = createMemo(() => { + const current = preferences() + const preset = current.toolCallExpansionDefaults.preset + if (preset === "custom") return preset + const detail = transcriptDetailPresets[preset] + const expectedThinking = detail.showThinkingBlocks ? THINKING_EXPANSION_PRESETS[preset] : "hidden" + return currentThinkingMode() === expectedThinking && + current.diagnosticsExpansion === detail.diagnosticsExpansion && + current.toolInputsVisibility === detail.toolInputsVisibility && + current.showUsageMetrics === detail.showUsageMetrics && + current.usageMetricsExpansion === detail.usageMetricsExpansion + ? preset + : "custom" + }) + + const materializeToolModes = () => { + const tools: Record = {} + for (const entry of getConfigurableToolEntries()) tools[entry.tool] = currentToolMode(entry.tool) + return tools + } + + const applyExpansionPreset = (preset: ToolCallExpansionPreset) => { + const tools = buildToolExpansionPresetDefaults(preset) + const thinking = THINKING_EXPANSION_PRESETS[preset] + updatePreferences({ + ...transcriptDetailPresets[preset], + toolCallExpansionDefaults: { preset, thinking, tools }, + thinkingBlocksExpansion: thinking, + toolOutputExpansion: tools[OTHER_TOOL_NAME] === "expanded" ? "expanded" : "collapsed", + }) + } + + const setVisibilityRowMode = (row: VisibilityRow, mode: VisibilityPreference) => { + const current = preferences() + if (row.kind === "diagnostics") { + updatePreferences({ diagnosticsExpansion: mode }) + return + } + if (row.kind === "inputs") { + updatePreferences({ toolInputsVisibility: mode }) + return + } + if (row.kind === "usage") { + updatePreferences({ + showUsageMetrics: mode !== "hidden", + usageMetricsExpansion: mode === "hidden" ? current.usageMetricsExpansion : mode, + }) + return + } + + const tools = materializeToolModes() + const thinking = row.kind === "thinking" && mode !== "hidden" ? mode : currentThinkingExpansion() + if (row.kind === "tool") tools[row.key] = mode + updatePreferences({ + showThinkingBlocks: row.kind === "thinking" ? mode !== "hidden" : current.showThinkingBlocks, + toolCallExpansionDefaults: { preset: "custom", thinking, tools }, + thinkingBlocksExpansion: + row.kind === "thinking" && mode !== "hidden" ? mode : current.thinkingBlocksExpansion, + toolOutputExpansion: + row.kind === "tool" && row.key === OTHER_TOOL_NAME && mode !== "hidden" + ? mode + : current.toolOutputExpansion, + }) + } + + const rowMode = (row: VisibilityRow): VisibilityPreference => { + if (row.kind === "thinking") return currentThinkingMode() + if (row.kind === "tool") return currentToolMode(row.key) + if (row.kind === "diagnostics") return preferences().diagnosticsExpansion + if (row.kind === "inputs") return preferences().toolInputsVisibility + return currentUsageMode() + } + + const selectedVisibilityOption = (mode: VisibilityPreference) => + visibilityOptions().find((option) => option.value === mode) + + const presetLabel = (preset: ToolCallExpansionPreset | "custom") => + t(`settings.behavior.expansionPreset.${preset}.title`) + + return ( +
+
+
+
+

{t("settings.behavior.expansionPresets.ariaLabel")}

+

{t("settings.appearance.behavior.subtitle")}

+
+ {presetLabel(currentPreset())} +
+ +
+ + {(preset) => ( + + )} + +
+ +
+
+ {t("settings.behavior.expansionDefaults.itemColumn")} + {t("settings.behavior.expansionDefaults.stateColumn")} +
+ + {(row) => { + const selected = createMemo(() => selectedVisibilityOption(rowMode(row))) + return ( +
+
{row.label}
+
+ + value={selected()} + onChange={(option) => option && setVisibilityRowMode(row, option.value)} + options={visibilityOptions()} + optionValue="value" + optionTextValue="label" + itemComponent={(itemProps) => ( + + {itemProps.item.rawValue.label} + + )} + > + +
+ > + {(state) => {state.selectedOption()?.label}} + +
+ +
+ + + + +
+
+ ) + }} +
+
+
+
+ ) +} diff --git a/packages/ui/src/components/settings/config-files-settings-section.tsx b/packages/ui/src/components/settings/config-files-settings-section.tsx index e1f3f9867..9c08312e2 100644 --- a/packages/ui/src/components/settings/config-files-settings-section.tsx +++ b/packages/ui/src/components/settings/config-files-settings-section.tsx @@ -122,6 +122,7 @@ export const ConfigFilesSettingsSection: Component = () => { return (
+

{t("settings.configFiles.title")}

diff --git a/packages/ui/src/components/settings/general-settings-section.tsx b/packages/ui/src/components/settings/general-settings-section.tsx new file mode 100644 index 000000000..f55502c02 --- /dev/null +++ b/packages/ui/src/components/settings/general-settings-section.tsx @@ -0,0 +1,94 @@ +import { Check, Laptop, Moon, Sun } from "lucide-solid" +import { createMemo, type Component } from "solid-js" +import { useI18n } from "../../lib/i18n" +import { getBehaviorSettings, type BehaviorSetting } from "../../lib/settings/behavior-registry" +import { useTheme, type ThemeMode } from "../../lib/theme" +import { useConfig } from "../../stores/preferences" +import { LocaleSelector } from "../locale-selector" +import { BehaviorSettingRows } from "./behavior-setting-rows" +import { StartupStateSettingsCard } from "./startup-state-settings-card" + +const themeModeOptions: Array<{ value: ThemeMode; icon: typeof Laptop }> = [ + { value: "system", icon: Laptop }, + { value: "light", icon: Sun }, + { value: "dark", icon: Moon }, +] + +export const GeneralSettingsSection: Component = () => { + const { t } = useI18n() + const { themeMode, setThemeMode } = useTheme() + const config = useConfig() + const { updatePreferences } = config + const generalSettings = createMemo(() => [ + ...getBehaviorSettings(config).filter( + (setting) => + setting.id === "behavior.keyboardShortcutHints" || + setting.id === "behavior.messageTimeline" || + setting.id === "behavior.timelineToolCalls" || + setting.id === "behavior.diffViewMode" || + setting.id === "behavior.promptSubmitOnEnter", + ), + { + kind: "toggle", + id: "behavior.holdLongAssistantReplies", + titleKey: "settings.behavior.holdLongAssistantReplies.title", + subtitleKey: "settings.behavior.holdLongAssistantReplies.subtitle", + get: (current) => Boolean(current.holdLongAssistantReplies ?? true), + set: (next) => updatePreferences({ holdLongAssistantReplies: next }), + }, + ]) + + const modeLabel = (mode: ThemeMode) => { + if (mode === "system") return t("theme.mode.system") + if (mode === "light") return t("theme.mode.light") + return t("theme.mode.dark") + } + + return ( +
+
+
+
+

{t("settings.appearance.theme.title")}

+

{t("settings.appearance.theme.subtitle")}

+
+ {t("settings.scope.device")} +
+
+ {themeModeOptions.map((option) => { + const Icon = option.icon + return ( + + ) + })} +
+
+ +
+
+
+

{t("folderSelection.language.ariaLabel")}

+

{t("settings.general.language.subtitle")}

+
+ {t("settings.scope.device")} +
+ +
+ + + +
+
+ +
+
+
+ ) +} diff --git a/packages/ui/src/components/settings/opencode-settings-section.tsx b/packages/ui/src/components/settings/opencode-settings-section.tsx index 0f8799aca..f919dbf01 100644 --- a/packages/ui/src/components/settings/opencode-settings-section.tsx +++ b/packages/ui/src/components/settings/opencode-settings-section.tsx @@ -2,16 +2,11 @@ import { Select } from "@kobalte/core/select" import { createEffect, createMemo, createSignal, type Component } from "solid-js" import { ChevronDown, Terminal } from "lucide-solid" import OpenCodeBinarySelector from "../opencode-binary-selector" -import EnvironmentVariablesEditor from "../environment-variables-editor" -import { useConfig } from "../../stores/preferences" -import type { ServerLogLevel } from "../../stores/preferences" +import { useConfig, type ServerLogLevel } from "../../stores/preferences" import { useI18n } from "../../lib/i18n" import { OpenCodeUpdateCard } from "./opencode-update-card" -type LogLevelOption = { - value: ServerLogLevel - label: string -} +type LogLevelOption = { value: ServerLogLevel; label: string } export const OpenCodeSettingsSection: Component = () => { const { t } = useI18n() @@ -72,10 +67,7 @@ export const OpenCodeSettingsSection: Component = () => {
value={selectedLogLevel()} - onChange={(option) => { - if (!option) return - updateLogLevel(option.value) - }} + onChange={(option) => option && updateLogLevel(option.value)} options={logLevelOptions()} optionValue="value" optionTextValue="label" @@ -88,38 +80,18 @@ export const OpenCodeSettingsSection: Component = () => {
> - {(state) => ( - - {state.selectedOption()?.label} - - )} + {(state) => {state.selectedOption()?.label}}
- - - +
- - - - +
- -
-
-
-

{t("advancedSettings.environmentVariables.title")}

-

{t("advancedSettings.environmentVariables.subtitle")}

-
- {t("settings.scope.server")} -
- -
) } diff --git a/packages/ui/src/components/settings/providers-settings-section.tsx b/packages/ui/src/components/settings/providers-settings-section.tsx new file mode 100644 index 000000000..50988162c --- /dev/null +++ b/packages/ui/src/components/settings/providers-settings-section.tsx @@ -0,0 +1,7 @@ +import { type Component } from "solid-js" +import { activeInstanceId } from "../../stores/instances" +import { ProviderManagerModal } from "../provider-auth/provider-manager-modal" + +export const ProvidersSettingsSection: Component = () => { + return +} diff --git a/packages/ui/src/components/settings/saved-remote-servers-card.tsx b/packages/ui/src/components/settings/saved-remote-servers-card.tsx new file mode 100644 index 000000000..139a0a530 --- /dev/null +++ b/packages/ui/src/components/settings/saved-remote-servers-card.tsx @@ -0,0 +1,67 @@ +import { Globe, Loader2, Plus, Trash2 } from "lucide-solid" +import { createSignal, For, Show, type Component } from "solid-js" +import { useI18n } from "../../lib/i18n" +import { useRemoteServerProfiles } from "../../lib/hooks/use-remote-server-profiles" +import { RemoteServerDialog } from "../remote-server-dialog" + +export const SavedRemoteServersCard: Component = () => { + const { t } = useI18n() + const { remoteServers, connectingServerId, saveServer, connectSavedServer, removeRemoteServerProfile } = useRemoteServerProfiles() + const [dialogOpen, setDialogOpen] = createSignal(false) + + return ( + <> +
+
+
+ +
+

{t("folderSelection.tabs.servers")}

+

{t("folderSelection.servers.empty.description")}

+
+
+ +
+
+ 0} fallback={
{t("folderSelection.servers.empty.title")}
}> + + {(server) => ( +
+
+
{server.name}
+
{server.baseUrl}
+
+
+ + +
+
+ )} +
+
+
+
+ + + ) +} diff --git a/packages/ui/src/components/settings/speech-settings-section.tsx b/packages/ui/src/components/settings/speech-settings-section.tsx index 939b279cc..681161e5a 100644 --- a/packages/ui/src/components/settings/speech-settings-section.tsx +++ b/packages/ui/src/components/settings/speech-settings-section.tsx @@ -1,9 +1,22 @@ -import type { Component } from "solid-js" +import { createMemo, type Component } from "solid-js" +import { getBehaviorSettings } from "../../lib/settings/behavior-registry" +import { useConfig } from "../../stores/preferences" +import { BehaviorSettingRows } from "./behavior-setting-rows" import SpeechSettingsCard from "./speech-settings-card" export const SpeechSettingsSection: Component = () => { + const config = useConfig() + const voiceSettings = createMemo(() => + getBehaviorSettings(config).filter((setting) => setting.id === "behavior.promptVoiceInput"), + ) + return (
+
+
+ +
+
) diff --git a/packages/ui/src/components/tool-call.tsx b/packages/ui/src/components/tool-call.tsx index 2ecd50a63..0446ca23b 100644 --- a/packages/ui/src/components/tool-call.tsx +++ b/packages/ui/src/components/tool-call.tsx @@ -12,7 +12,7 @@ import { getPermissionSessionId } from "../types/permission" import type { QuestionRequest } from "../types/question" import { useI18n } from "../lib/i18n" import { resolveToolRenderer } from "./tool-call/renderers" -import { resolveToolExpansionDefault } from "./tool-call/tool-registry" +import { resolveToolExpansionDefault, resolveToolVisibility } from "./tool-call/tool-registry" import { QuestionToolBlock } from "./tool-call/question-block" import { PermissionToolBlock } from "./tool-call/permission-block" import { createAnsiContentRenderer } from "./tool-call/ansi-render" @@ -744,7 +744,9 @@ export default function ToolCall(props: ToolCallProps) { return undefined }) - const diagnosticsDefaultExpanded = createMemo(() => (preferences().diagnosticsExpansion || "expanded") === "expanded") + const diagnosticsVisibility = createMemo(() => preferences().diagnosticsExpansion || "expanded") + const diagnosticsDefaultExpanded = createMemo(() => diagnosticsVisibility() === "expanded") + const toolVisibility = createMemo(() => resolveToolVisibility(preferences(), toolCallMemo()?.tool || "")) const defaultExpandedForTool = createMemo(() => { if (props.forceCollapsed) { @@ -793,6 +795,8 @@ export default function ToolCall(props: ToolCallProps) { return active?.kind === "question" && active.id === pending.request.id }) + const isToolVisible = createMemo(() => toolVisibility() !== "hidden" || isPermissionActive() || isQuestionActive()) + const expanded = () => { if (isPermissionActive() || isQuestionActive()) return true const override = userExpanded() @@ -1046,6 +1050,7 @@ export default function ToolCall(props: ToolCallProps) { const status = () => toolState()?.status || "" return ( +
{ @@ -1168,7 +1173,7 @@ export default function ToolCall(props: ToolCallProps) { /> - + {renderDiagnosticsSection( t, @@ -1182,5 +1187,6 @@ export default function ToolCall(props: ToolCallProps) { )}
+
) } diff --git a/packages/ui/src/components/tool-call/tool-registry.ts b/packages/ui/src/components/tool-call/tool-registry.ts index 2358e129d..004a6c2ca 100644 --- a/packages/ui/src/components/tool-call/tool-registry.ts +++ b/packages/ui/src/components/tool-call/tool-registry.ts @@ -1,4 +1,9 @@ -import type { ExpansionPreference, Preferences, ToolCallExpansionPreset } from "../../stores/preferences" +import type { + ExpansionPreference, + Preferences, + ToolCallExpansionPreset, + VisibilityPreference, +} from "../../stores/preferences" import type { ToolRenderer } from "./types" import { applyPatchRenderer } from "./renderers/apply-patch" import { bashRenderer } from "./renderers/bash" @@ -30,14 +35,14 @@ export interface ToolRegistryEntry { labelKey?: string renderer: ToolRenderer configurable: boolean - expansionPresets: Record + expansionPresets: Record aliases?: string[] } const expanded = "expanded" satisfies ExpansionPreference const collapsed = "collapsed" satisfies ExpansionPreference -function presets(values: Record) { +function presets(values: Record) { return values } @@ -172,26 +177,29 @@ export function getConfigurableToolEntries(): ToolRegistryEntry[] { return TOOL_REGISTRY.filter((entry) => entry.configurable) } -export function buildToolExpansionPresetDefaults(preset: ToolCallExpansionPreset): Record { - const defaults: Record = {} +export function buildToolExpansionPresetDefaults(preset: ToolCallExpansionPreset): Record { + const defaults: Record = {} for (const entry of getConfigurableToolEntries()) { defaults[entry.tool] = entry.expansionPresets[preset] } return defaults } -export function resolveToolExpansionDefault(preferences: Preferences, toolName: string): boolean { +export function resolveToolVisibility(preferences: Preferences, toolName: string): VisibilityPreference { const entry = getToolRegistryEntry(toolName) const defaults = preferences.toolCallExpansionDefaults const presetMode = defaults.preset === "custom" ? undefined : entry.expansionPresets[defaults.preset] const otherPresetMode = defaults.preset === "custom" ? undefined : otherToolEntry.expansionPresets[defaults.preset] - const mode = defaults.tools[entry.tool] + return defaults.tools[entry.tool] ?? presetMode ?? defaults.tools[OTHER_TOOL_NAME] ?? otherPresetMode ?? preferences.toolOutputExpansion ?? "expanded" - return mode === "expanded" +} + +export function resolveToolExpansionDefault(preferences: Preferences, toolName: string): boolean { + return resolveToolVisibility(preferences, toolName) === "expanded" } export function resolveThinkingExpansionDefault(preferences: Preferences): boolean { diff --git a/packages/ui/src/lib/hooks/use-commands.ts b/packages/ui/src/lib/hooks/use-commands.ts index 734ceb4e7..e11c97650 100644 --- a/packages/ui/src/lib/hooks/use-commands.ts +++ b/packages/ui/src/lib/hooks/use-commands.ts @@ -1,6 +1,11 @@ import { createSignal, onMount } from "solid-js" import type { Accessor } from "solid-js" -import type { Preferences, ExpansionPreference, ToolInputsVisibilityPreference } from "../../stores/preferences" +import type { + Preferences, + ExpansionPreference, + ToolInputsVisibilityPreference, + VisibilityPreference, +} from "../../stores/preferences" import { createCommandRegistry, type Command } from "../commands" import { activeInstanceId } from "../../stores/instances" import { selectNextAppTab, selectPreviousAppTab } from "../../stores/app-tabs" @@ -40,8 +45,8 @@ export interface UseCommandsOptions { togglePromptSubmitOnEnter: () => void toggleShowPromptVoiceInput: () => void setDiffViewMode: (mode: "split" | "unified") => void - setToolOutputExpansion: (mode: ExpansionPreference) => void - setDiagnosticsExpansion: (mode: ExpansionPreference) => void + setToolOutputExpansion: (mode: VisibilityPreference) => void + setDiagnosticsExpansion: (mode: VisibilityPreference) => void setThinkingBlocksExpansion: (mode: ExpansionPreference) => void setToolInputsVisibility: (mode: ToolInputsVisibilityPreference) => void handleNewInstanceRequest: () => void diff --git a/packages/ui/src/lib/hooks/use-remote-server-profiles.ts b/packages/ui/src/lib/hooks/use-remote-server-profiles.ts new file mode 100644 index 000000000..1fea22b11 --- /dev/null +++ b/packages/ui/src/lib/hooks/use-remote-server-profiles.ts @@ -0,0 +1,77 @@ +import { createSignal } from "solid-js" +import { serverApi } from "../api-client" +import { useI18n } from "../i18n" +import { openRemoteServerWindow } from "../native/remote-window" +import { canOpenRemoteWindows, isTauriHost } from "../runtime-env" +import { showAlertDialog } from "../../stores/alerts" +import { useConfig } from "../../stores/preferences" + +export type RemoteServerInput = { + id?: string + name: string + baseUrl: string + skipTlsVerify: boolean +} + +export function useRemoteServerProfiles() { + const { t } = useI18n() + const { remoteServers, saveRemoteServerProfile, markRemoteServerConnected, removeRemoteServerProfile } = useConfig() + const [connectingServerId, setConnectingServerId] = createSignal(null) + + const saveServer = async (input: RemoteServerInput, openWindow: boolean) => { + if (openWindow && !canOpenRemoteWindows()) { + throw new Error("Remote server windows can only be opened from a local desktop window") + } + + const name = input.name.trim() + const baseUrl = input.baseUrl.trim() + if (!name || !baseUrl) throw new Error(t("folderSelection.servers.dialog.errorRequired")) + + const probe = await serverApi.probeRemoteServer({ baseUrl, skipTlsVerify: input.skipTlsVerify }) + if (!probe.ok) throw new Error(probe.error || t("folderSelection.servers.dialog.errorConnect")) + + const profile = await saveRemoteServerProfile({ + id: input.id, + name, + baseUrl: probe.normalizedUrl, + skipTlsVerify: input.skipTlsVerify, + }) + + if (openWindow) { + const proxySession = + isTauriHost() && profile.skipTlsVerify && profile.baseUrl.startsWith("https://") + ? await serverApi.createRemoteProxySession({ baseUrl: profile.baseUrl, skipTlsVerify: true }) + : undefined + + try { + await openRemoteServerWindow(profile, proxySession?.windowUrl, proxySession?.sessionId) + } catch (error) { + if (proxySession) void serverApi.deleteRemoteProxySession(proxySession.sessionId).catch(() => {}) + throw error + } + await markRemoteServerConnected(profile.id) + } + + return profile + } + + const connectSavedServer = async (id: string) => { + if (!canOpenRemoteWindows() || connectingServerId()) return + const target = remoteServers().find((server) => server.id === id) + if (!target) return + + setConnectingServerId(id) + try { + await saveServer(target, true) + } catch (error) { + showAlertDialog(error instanceof Error ? error.message : String(error), { + title: t("folderSelection.servers.errorTitle"), + variant: "warning", + }) + } finally { + setConnectingServerId(null) + } + } + + return { remoteServers, connectingServerId, saveServer, connectSavedServer, removeRemoteServerProfile } +} diff --git a/packages/ui/src/lib/i18n/messages/de/commands.ts b/packages/ui/src/lib/i18n/messages/de/commands.ts index 8480b6484..5962a1aa5 100644 --- a/packages/ui/src/lib/i18n/messages/de/commands.ts +++ b/packages/ui/src/lib/i18n/messages/de/commands.ts @@ -123,12 +123,12 @@ export const commandMessages = { "commands.diffViewUnified.keywords": "diff, einheitlich, ansicht", "commands.toolOutputsDefault.label": "Standard für Tool-Ausgaben · {state}", - "commands.toolOutputsDefault.description": "Standardmäßige Erweiterung für Tool-Ausgaben umschalten", - "commands.toolOutputsDefault.keywords": "tool, ausgabe, erweitern, einklappen", + "commands.toolOutputsDefault.description": "Standardsichtbarkeit für Tool-Ausgaben festlegen", + "commands.toolOutputsDefault.keywords": "tool, ausgabe, ausblenden, erweitern, einklappen", "commands.diagnosticsDefault.label": "Standard für Diagnose · {state}", - "commands.diagnosticsDefault.description": "Standardmäßige Erweiterung für die Diagnoseausgabe umschalten", - "commands.diagnosticsDefault.keywords": "diagnose, erweitern, einklappen", + "commands.diagnosticsDefault.description": "Standardsichtbarkeit für Diagnoseausgaben festlegen", + "commands.diagnosticsDefault.keywords": "diagnose, ausblenden, erweitern, einklappen", "commands.toolInputsVisibility.label": "Sichtbarkeit von Tool-Eingaben · {state}", "commands.toolInputsVisibility.description": "Standardsichtbarkeit für Argumente von Tool-Aufrufen festlegen", diff --git a/packages/ui/src/lib/i18n/messages/de/messaging.ts b/packages/ui/src/lib/i18n/messages/de/messaging.ts index f458cd175..238db557f 100644 --- a/packages/ui/src/lib/i18n/messages/de/messaging.ts +++ b/packages/ui/src/lib/i18n/messages/de/messaging.ts @@ -72,6 +72,8 @@ export const messagingMessages = { "messageBlock.usage.cacheRead": "Cache-Lesen", "messageBlock.usage.cacheWrite": "Cache-Schreiben", "messageBlock.usage.cost": "Kosten", + "messageBlock.usage.expandAriaLabel": "Token-Metriken erweitern", + "messageBlock.usage.collapseAriaLabel": "Token-Metriken einklappen", "messageBlock.step.agentLabel": "Agent: {agent}", "messageBlock.step.modelLabel": "Modell: {model}", "messageBlock.reasoning.thinkingLabel": "Denken", diff --git a/packages/ui/src/lib/i18n/messages/de/settings.ts b/packages/ui/src/lib/i18n/messages/de/settings.ts index 233bd75e3..49794fa81 100644 --- a/packages/ui/src/lib/i18n/messages/de/settings.ts +++ b/packages/ui/src/lib/i18n/messages/de/settings.ts @@ -98,6 +98,16 @@ export const settingsMessages = { "settings.open.title": "Einstellungen öffnen", "settings.open.ariaLabel": "Einstellungen öffnen", "settings.nav.appearance": "Aussehen", + "settings.nav.general": "Allgemein", + "settings.nav.chat": "Chat", + "settings.nav.voice": "Sprache", + "settings.nav.connections": "Verbindungen", + "settings.nav.runtime": "Laufzeit", + "settings.nav.advanced": "Erweitert", + "settings.nav.about": "Über", + "settings.general.language.subtitle": "Wähle die Sprache für die gesamte Anwendung.", + "settings.behavior.holdLongAssistantReplies.title": "Lange Assistentenantworten anhalten", + "settings.behavior.holdLongAssistantReplies.subtitle": "Automatisches Folgen beenden, wenn eine laufende Antwort über das Ansichtsfenster hinausgeht.", "settings.nav.notifications": "Benachrichtigungen", "settings.nav.remote": "Remote-Zugriff", "settings.nav.speech": "Sprache", @@ -200,6 +210,7 @@ export const settingsMessages = { "settings.configFiles.confirmDiscard.cancelLabel": "Weiter bearbeiten", "settings.providers.title": "Anbieter-Authentifizierung", + "settings.providers.target": "OpenCode-Instanz: {workspace}", "settings.providers.subtitle": "API-Schlüssel oder OAuth-Konten für die aktive OpenCode-Instanz verbinden.", "settings.providers.refresh": "Aktualisieren", "settings.providers.selectProvider": "Anbieter auswählen", @@ -258,8 +269,8 @@ export const settingsMessages = { "settings.appearance.behavior.title": "Interaktion", - "settings.appearance.behavior.subtitle": "Standardwerte für Nachrichten, Diffs und Eingaben.", - "settings.behavior.expansionPresets.ariaLabel": "Transcript detail presets", + "settings.appearance.behavior.subtitle": "Steuere Details fuer Nachrichten, Tools, Diagnosen und Nutzung gemeinsam.", + "settings.behavior.expansionPresets.ariaLabel": "Voreinstellungen fuer Gespraechsdetails", "settings.behavior.expansionPreset.minimal.title": "Minimal", "settings.behavior.expansionPreset.minimal.description": "Keep most generated detail tucked away until needed.", "settings.behavior.expansionPreset.balanced.title": "Balanced", diff --git a/packages/ui/src/lib/i18n/messages/en/commands.ts b/packages/ui/src/lib/i18n/messages/en/commands.ts index a61bddd2c..3dabac04e 100644 --- a/packages/ui/src/lib/i18n/messages/en/commands.ts +++ b/packages/ui/src/lib/i18n/messages/en/commands.ts @@ -123,12 +123,12 @@ export const commandMessages = { "commands.diffViewUnified.keywords": "diff, unified, view", "commands.toolOutputsDefault.label": "Tool Outputs Default · {state}", - "commands.toolOutputsDefault.description": "Toggle default expansion for tool outputs", - "commands.toolOutputsDefault.keywords": "tool, output, expand, collapse", + "commands.toolOutputsDefault.description": "Set default visibility for tool outputs", + "commands.toolOutputsDefault.keywords": "tool, output, hide, expand, collapse", "commands.diagnosticsDefault.label": "Diagnostics Default · {state}", - "commands.diagnosticsDefault.description": "Toggle default expansion for diagnostics output", - "commands.diagnosticsDefault.keywords": "diagnostics, expand, collapse", + "commands.diagnosticsDefault.description": "Set default visibility for diagnostics output", + "commands.diagnosticsDefault.keywords": "diagnostics, hide, expand, collapse", "commands.toolInputsVisibility.label": "Tool Inputs Visibility · {state}", "commands.toolInputsVisibility.description": "Set default visibility for tool call input arguments", diff --git a/packages/ui/src/lib/i18n/messages/en/messaging.ts b/packages/ui/src/lib/i18n/messages/en/messaging.ts index dd0005c92..d337618c2 100644 --- a/packages/ui/src/lib/i18n/messages/en/messaging.ts +++ b/packages/ui/src/lib/i18n/messages/en/messaging.ts @@ -72,6 +72,8 @@ export const messagingMessages = { "messageBlock.usage.cacheRead": "Cache Read", "messageBlock.usage.cacheWrite": "Cache Write", "messageBlock.usage.cost": "Cost", + "messageBlock.usage.expandAriaLabel": "Expand token metrics", + "messageBlock.usage.collapseAriaLabel": "Collapse token metrics", "messageBlock.step.agentLabel": "Agent: {agent}", "messageBlock.step.modelLabel": "Model: {model}", "messageBlock.reasoning.thinkingLabel": "Thinking", diff --git a/packages/ui/src/lib/i18n/messages/en/settings.ts b/packages/ui/src/lib/i18n/messages/en/settings.ts index cdd440122..7e79f5e06 100644 --- a/packages/ui/src/lib/i18n/messages/en/settings.ts +++ b/packages/ui/src/lib/i18n/messages/en/settings.ts @@ -98,6 +98,16 @@ export const settingsMessages = { "settings.open.title": "Open settings", "settings.open.ariaLabel": "Open settings", "settings.nav.appearance": "Appearance", + "settings.nav.general": "General", + "settings.nav.chat": "Chat", + "settings.nav.voice": "Voice", + "settings.nav.connections": "Connections", + "settings.nav.runtime": "Runtime", + "settings.nav.advanced": "Advanced", + "settings.nav.about": "About", + "settings.general.language.subtitle": "Choose the language used throughout the application.", + "settings.behavior.holdLongAssistantReplies.title": "Hold long assistant replies", + "settings.behavior.holdLongAssistantReplies.subtitle": "Stop following automatically when a streaming reply grows beyond the viewport.", "settings.nav.notifications": "Notifications", "settings.nav.remote": "Remote Access", "settings.nav.speech": "Speech", @@ -200,6 +210,7 @@ export const settingsMessages = { "settings.configFiles.confirmDiscard.cancelLabel": "Keep editing", "settings.providers.title": "Provider Authentication", + "settings.providers.target": "OpenCode instance: {workspace}", "settings.providers.subtitle": "Connect API keys or OAuth accounts for the active OpenCode instance.", "settings.providers.refresh": "Refresh", "settings.providers.selectProvider": "Select provider", @@ -258,7 +269,7 @@ export const settingsMessages = { "settings.appearance.behavior.title": "Interaction", - "settings.appearance.behavior.subtitle": "Message, diff, and input defaults.", + "settings.appearance.behavior.subtitle": "Control message, tool, diagnostics, and usage detail together.", "settings.behavior.expansionPresets.ariaLabel": "Transcript detail presets", "settings.behavior.expansionPreset.minimal.title": "Minimal", "settings.behavior.expansionPreset.minimal.description": "Keep most generated detail tucked away until needed.", diff --git a/packages/ui/src/lib/i18n/messages/es/commands.ts b/packages/ui/src/lib/i18n/messages/es/commands.ts index 10b42ff59..68e9c3029 100644 --- a/packages/ui/src/lib/i18n/messages/es/commands.ts +++ b/packages/ui/src/lib/i18n/messages/es/commands.ts @@ -123,12 +123,12 @@ export const commandMessages = { "commands.diffViewUnified.keywords": "diff, unificada, vista", "commands.toolOutputsDefault.label": "Salidas de herramientas por defecto · {state}", - "commands.toolOutputsDefault.description": "Alternar la expansión por defecto de las salidas de herramientas", - "commands.toolOutputsDefault.keywords": "herramienta, salida, expandir, colapsar", + "commands.toolOutputsDefault.description": "Configurar la visibilidad por defecto de las salidas de herramientas", + "commands.toolOutputsDefault.keywords": "herramienta, salida, ocultar, expandir, colapsar", "commands.diagnosticsDefault.label": "Diagnósticos por defecto · {state}", - "commands.diagnosticsDefault.description": "Alternar la expansión por defecto de la salida de diagnósticos", - "commands.diagnosticsDefault.keywords": "diagnósticos, expandir, colapsar", + "commands.diagnosticsDefault.description": "Configurar la visibilidad por defecto de la salida de diagnósticos", + "commands.diagnosticsDefault.keywords": "diagnósticos, ocultar, expandir, colapsar", "commands.toolInputsVisibility.label": "Visibilidad de entradas de herramientas · {state}", "commands.toolInputsVisibility.description": "Configurar la visibilidad por defecto de los argumentos de entrada de llamadas de herramienta", diff --git a/packages/ui/src/lib/i18n/messages/es/messaging.ts b/packages/ui/src/lib/i18n/messages/es/messaging.ts index 051d9f2ff..43d6530b1 100644 --- a/packages/ui/src/lib/i18n/messages/es/messaging.ts +++ b/packages/ui/src/lib/i18n/messages/es/messaging.ts @@ -74,6 +74,8 @@ export const messagingMessages = { "messageBlock.usage.cacheRead": "Lectura de cache", "messageBlock.usage.cacheWrite": "Escritura de cache", "messageBlock.usage.cost": "Costo", + "messageBlock.usage.expandAriaLabel": "Expandir métricas de tokens", + "messageBlock.usage.collapseAriaLabel": "Contraer métricas de tokens", "messageBlock.step.agentLabel": "Agente: {agent}", "messageBlock.step.modelLabel": "Modelo: {model}", "messageBlock.reasoning.thinkingLabel": "Pensamiento", diff --git a/packages/ui/src/lib/i18n/messages/es/settings.ts b/packages/ui/src/lib/i18n/messages/es/settings.ts index 021bae380..268c30623 100644 --- a/packages/ui/src/lib/i18n/messages/es/settings.ts +++ b/packages/ui/src/lib/i18n/messages/es/settings.ts @@ -98,6 +98,16 @@ export const settingsMessages = { "settings.open.title": "Abrir configuración", "settings.open.ariaLabel": "Abrir configuración", "settings.nav.appearance": "Apariencia", + "settings.nav.general": "General", + "settings.nav.chat": "Chat", + "settings.nav.voice": "Voz", + "settings.nav.connections": "Conexiones", + "settings.nav.runtime": "Entorno de ejecución", + "settings.nav.advanced": "Avanzado", + "settings.nav.about": "Acerca de", + "settings.general.language.subtitle": "Elige el idioma utilizado en toda la aplicación.", + "settings.behavior.holdLongAssistantReplies.title": "Retener respuestas largas del asistente", + "settings.behavior.holdLongAssistantReplies.subtitle": "Deja de seguir automáticamente cuando una respuesta en curso supera la ventana.", "settings.nav.notifications": "Notificaciones", "settings.nav.remote": "Acceso remoto", "settings.nav.speech": "Voz", @@ -200,6 +210,7 @@ export const settingsMessages = { "settings.configFiles.confirmDiscard.cancelLabel": "Seguir editando", "settings.providers.title": "Autenticación de proveedores", + "settings.providers.target": "Instancia de OpenCode: {workspace}", "settings.providers.subtitle": "Conecta claves API o cuentas OAuth para la instancia activa de OpenCode.", "settings.providers.refresh": "Actualizar", "settings.providers.selectProvider": "Seleccionar proveedor", @@ -257,8 +268,8 @@ export const settingsMessages = { "settings.providers.errors.envDisconnectUnavailable": "Este proveedor está configurado desde variables de entorno y no se puede desconectar desde la UI.", "settings.appearance.behavior.title": "Interacción", - "settings.appearance.behavior.subtitle": "Valores predeterminados de mensajes, diffs y entrada.", - "settings.behavior.expansionPresets.ariaLabel": "Transcript detail presets", + "settings.appearance.behavior.subtitle": "Controla conjuntamente el detalle de mensajes, herramientas, diagnosticos y uso.", + "settings.behavior.expansionPresets.ariaLabel": "Preajustes de detalle de conversacion", "settings.behavior.expansionPreset.minimal.title": "Minimal", "settings.behavior.expansionPreset.minimal.description": "Keep most generated detail tucked away until needed.", "settings.behavior.expansionPreset.balanced.title": "Balanced", diff --git a/packages/ui/src/lib/i18n/messages/fr/commands.ts b/packages/ui/src/lib/i18n/messages/fr/commands.ts index 6fbd59c02..f846bc883 100644 --- a/packages/ui/src/lib/i18n/messages/fr/commands.ts +++ b/packages/ui/src/lib/i18n/messages/fr/commands.ts @@ -123,12 +123,12 @@ export const commandMessages = { "commands.diffViewUnified.keywords": "diff, unifiée, vue", "commands.toolOutputsDefault.label": "Sorties d'outil par défaut · {state}", - "commands.toolOutputsDefault.description": "Choisir l'ouverture par défaut des sorties d'outil", - "commands.toolOutputsDefault.keywords": "outil, sortie, développer, réduire", + "commands.toolOutputsDefault.description": "Choisir la visibilité par défaut des sorties d'outil", + "commands.toolOutputsDefault.keywords": "outil, sortie, masquer, développer, réduire", "commands.diagnosticsDefault.label": "Diagnostics par défaut · {state}", - "commands.diagnosticsDefault.description": "Choisir l'ouverture par défaut de la sortie des diagnostics", - "commands.diagnosticsDefault.keywords": "diagnostics, développer, réduire", + "commands.diagnosticsDefault.description": "Choisir la visibilité par défaut de la sortie des diagnostics", + "commands.diagnosticsDefault.keywords": "diagnostics, masquer, développer, réduire", "commands.toolInputsVisibility.label": "Visibilité des entrées d'outil · {state}", "commands.toolInputsVisibility.description": "Définir la visibilité par défaut des arguments d'entrée des appels d'outil", diff --git a/packages/ui/src/lib/i18n/messages/fr/messaging.ts b/packages/ui/src/lib/i18n/messages/fr/messaging.ts index d02c6835a..5ae20972a 100644 --- a/packages/ui/src/lib/i18n/messages/fr/messaging.ts +++ b/packages/ui/src/lib/i18n/messages/fr/messaging.ts @@ -74,6 +74,8 @@ export const messagingMessages = { "messageBlock.usage.cacheRead": "Lecture du cache", "messageBlock.usage.cacheWrite": "Écriture du cache", "messageBlock.usage.cost": "Coût", + "messageBlock.usage.expandAriaLabel": "Développer les métriques de tokens", + "messageBlock.usage.collapseAriaLabel": "Réduire les métriques de tokens", "messageBlock.step.agentLabel": "Agent : {agent}", "messageBlock.step.modelLabel": "Modèle : {model}", "messageBlock.reasoning.thinkingLabel": "Réflexion", diff --git a/packages/ui/src/lib/i18n/messages/fr/settings.ts b/packages/ui/src/lib/i18n/messages/fr/settings.ts index 2d1aacaa0..65c924f73 100644 --- a/packages/ui/src/lib/i18n/messages/fr/settings.ts +++ b/packages/ui/src/lib/i18n/messages/fr/settings.ts @@ -98,6 +98,16 @@ export const settingsMessages = { "settings.open.title": "Ouvrir les paramètres", "settings.open.ariaLabel": "Ouvrir les paramètres", "settings.nav.appearance": "Apparence", + "settings.nav.general": "Général", + "settings.nav.chat": "Chat", + "settings.nav.voice": "Voix", + "settings.nav.connections": "Connexions", + "settings.nav.runtime": "Exécution", + "settings.nav.advanced": "Avancé", + "settings.nav.about": "À propos", + "settings.general.language.subtitle": "Choisissez la langue utilisée dans toute l'application.", + "settings.behavior.holdLongAssistantReplies.title": "Suspendre le suivi des longues réponses", + "settings.behavior.holdLongAssistantReplies.subtitle": "Arrêter le suivi automatique lorsqu'une réponse en cours dépasse la fenêtre.", "settings.nav.notifications": "Notifications", "settings.nav.remote": "Accès distant", "settings.nav.speech": "Voix", @@ -200,6 +210,7 @@ export const settingsMessages = { "settings.configFiles.confirmDiscard.cancelLabel": "Continuer la modification", "settings.providers.title": "Authentification des fournisseurs", + "settings.providers.target": "Instance OpenCode : {workspace}", "settings.providers.subtitle": "Connectez des clés API ou des comptes OAuth pour l'instance OpenCode active.", "settings.providers.refresh": "Actualiser", "settings.providers.selectProvider": "Sélectionner un fournisseur", @@ -257,8 +268,8 @@ export const settingsMessages = { "settings.providers.errors.envDisconnectUnavailable": "Ce fournisseur est configuré depuis des variables d'environnement et ne peut pas être déconnecté depuis l'interface.", "settings.appearance.behavior.title": "Interaction", - "settings.appearance.behavior.subtitle": "Parametres par defaut pour les messages, les diffs et la saisie.", - "settings.behavior.expansionPresets.ariaLabel": "Transcript detail presets", + "settings.appearance.behavior.subtitle": "Controlez ensemble le niveau de detail des messages, outils, diagnostics et metriques.", + "settings.behavior.expansionPresets.ariaLabel": "Prereglages de detail de conversation", "settings.behavior.expansionPreset.minimal.title": "Minimal", "settings.behavior.expansionPreset.minimal.description": "Keep most generated detail tucked away until needed.", "settings.behavior.expansionPreset.balanced.title": "Balanced", diff --git a/packages/ui/src/lib/i18n/messages/he/commands.ts b/packages/ui/src/lib/i18n/messages/he/commands.ts index 1969ebf4f..a432e042e 100644 --- a/packages/ui/src/lib/i18n/messages/he/commands.ts +++ b/packages/ui/src/lib/i18n/messages/he/commands.ts @@ -123,12 +123,12 @@ export const commandMessages = { "commands.diffViewUnified.keywords": "diff, מאוחד, תצוגה", "commands.toolOutputsDefault.label": "ברירת מחדל לפלטי כלים · {state}", - "commands.toolOutputsDefault.description": "החלף ברירת מחדל לפריסת פלטי כלים", - "commands.toolOutputsDefault.keywords": "כלי, פלט, פרוס, כווץ", + "commands.toolOutputsDefault.description": "הגדר נראות ברירת מחדל לפלטי כלים", + "commands.toolOutputsDefault.keywords": "כלי, פלט, הסתר, פרוס, כווץ", "commands.diagnosticsDefault.label": "ברירת מחדל לאבחון · {state}", - "commands.diagnosticsDefault.description": "החלף ברירת מחדל לפריסת פלט אבחון", - "commands.diagnosticsDefault.keywords": "אבחון, פרוס, כווץ", + "commands.diagnosticsDefault.description": "הגדר נראות ברירת מחדל לפלט אבחון", + "commands.diagnosticsDefault.keywords": "אבחון, הסתר, פרוס, כווץ", "commands.toolInputsVisibility.label": "נראות קלטי כלים · {state}", "commands.toolInputsVisibility.description": "הגדר נראות ברירת מחדל לארגומנטים של קריאות כלי", diff --git a/packages/ui/src/lib/i18n/messages/he/messaging.ts b/packages/ui/src/lib/i18n/messages/he/messaging.ts index f8baf246c..a342b71e2 100644 --- a/packages/ui/src/lib/i18n/messages/he/messaging.ts +++ b/packages/ui/src/lib/i18n/messages/he/messaging.ts @@ -72,6 +72,8 @@ export const messagingMessages = { "messageBlock.usage.cacheRead": "קריאת מטמון", "messageBlock.usage.cacheWrite": "כתיבת מטמון", "messageBlock.usage.cost": "עלות", + "messageBlock.usage.expandAriaLabel": "הרחבת מדדי טוקנים", + "messageBlock.usage.collapseAriaLabel": "כיווץ מדדי טוקנים", "messageBlock.step.agentLabel": "סוכן: {agent}", "messageBlock.step.modelLabel": "מודל: {model}", "messageBlock.reasoning.thinkingLabel": "חשיבה", diff --git a/packages/ui/src/lib/i18n/messages/he/settings.ts b/packages/ui/src/lib/i18n/messages/he/settings.ts index 741dbd089..a7f9c7fb6 100644 --- a/packages/ui/src/lib/i18n/messages/he/settings.ts +++ b/packages/ui/src/lib/i18n/messages/he/settings.ts @@ -98,6 +98,16 @@ export const settingsMessages = { "settings.open.title": "פתח הגדרות", "settings.open.ariaLabel": "פתח הגדרות", "settings.nav.appearance": "מראה", + "settings.nav.general": "כללי", + "settings.nav.chat": "צ׳אט", + "settings.nav.voice": "קול", + "settings.nav.connections": "חיבורים", + "settings.nav.runtime": "זמן ריצה", + "settings.nav.advanced": "מתקדם", + "settings.nav.about": "אודות", + "settings.general.language.subtitle": "בחר את השפה שתשמש בכל היישום.", + "settings.behavior.holdLongAssistantReplies.title": "השהיית מעקב אחר תשובות ארוכות", + "settings.behavior.holdLongAssistantReplies.subtitle": "הפסק מעקב אוטומטי כאשר תשובה זורמת חורגת מחלון התצוגה.", "settings.nav.notifications": "התראות", "settings.nav.remote": "גישה מרוחקת", "settings.nav.speech": "קול", @@ -200,6 +210,7 @@ export const settingsMessages = { "settings.configFiles.confirmDiscard.cancelLabel": "המשך לערוך", "settings.providers.title": "אימות ספקים", + "settings.providers.target": "מופע OpenCode: {workspace}", "settings.providers.subtitle": "חבר מפתחות API או חשבונות OAuth עבור מופע OpenCode הפעיל.", "settings.providers.refresh": "רענן", "settings.providers.selectProvider": "בחר ספק", @@ -257,8 +268,8 @@ export const settingsMessages = { "settings.providers.errors.envDisconnectUnavailable": "ספק זה מוגדר ממשתני סביבה ולא ניתן לנתק אותו מהממשק.", "settings.appearance.behavior.title": "אינטראקציה", - "settings.appearance.behavior.subtitle": "ברירות מחדל להודעות, diff וקלט.", - "settings.behavior.expansionPresets.ariaLabel": "Transcript detail presets", + "settings.appearance.behavior.subtitle": "שליטה משותפת ברמת הפירוט של הודעות, כלים, אבחון ושימוש.", + "settings.behavior.expansionPresets.ariaLabel": "הגדרות קבועות מראש לפירוט השיחה", "settings.behavior.expansionPreset.minimal.title": "Minimal", "settings.behavior.expansionPreset.minimal.description": "Keep most generated detail tucked away until needed.", "settings.behavior.expansionPreset.balanced.title": "Balanced", diff --git a/packages/ui/src/lib/i18n/messages/ja/commands.ts b/packages/ui/src/lib/i18n/messages/ja/commands.ts index 78ddd61d6..c00b13d44 100644 --- a/packages/ui/src/lib/i18n/messages/ja/commands.ts +++ b/packages/ui/src/lib/i18n/messages/ja/commands.ts @@ -123,12 +123,12 @@ export const commandMessages = { "commands.diffViewUnified.keywords": "diff, ユニファイド, 表示, unified, view", "commands.toolOutputsDefault.label": "ツール出力の既定 · {state}", - "commands.toolOutputsDefault.description": "ツール出力を既定で展開するか切り替え", - "commands.toolOutputsDefault.keywords": "ツール, 出力, 展開, 折りたたみ, tool, output, expand, collapse", + "commands.toolOutputsDefault.description": "ツール出力の既定の表示状態を設定", + "commands.toolOutputsDefault.keywords": "ツール, 出力, 非表示, 展開, 折りたたみ, tool, output, hide, expand, collapse", "commands.diagnosticsDefault.label": "診断の既定 · {state}", - "commands.diagnosticsDefault.description": "診断出力を既定で展開するか切り替え", - "commands.diagnosticsDefault.keywords": "診断, 展開, 折りたたみ, diagnostics, expand, collapse", + "commands.diagnosticsDefault.description": "診断出力の既定の表示状態を設定", + "commands.diagnosticsDefault.keywords": "診断, 非表示, 展開, 折りたたみ, diagnostics, hide, expand, collapse", "commands.toolInputsVisibility.label": "ツール入力の表示 · {state}", "commands.toolInputsVisibility.description": "ツール呼び出しの入力引数の既定の表示状態を設定します", diff --git a/packages/ui/src/lib/i18n/messages/ja/messaging.ts b/packages/ui/src/lib/i18n/messages/ja/messaging.ts index c71aea59f..587ddc177 100644 --- a/packages/ui/src/lib/i18n/messages/ja/messaging.ts +++ b/packages/ui/src/lib/i18n/messages/ja/messaging.ts @@ -74,6 +74,8 @@ export const messagingMessages = { "messageBlock.usage.cacheRead": "キャッシュ読み込み", "messageBlock.usage.cacheWrite": "キャッシュ書き込み", "messageBlock.usage.cost": "コスト", + "messageBlock.usage.expandAriaLabel": "トークン使用量を展開", + "messageBlock.usage.collapseAriaLabel": "トークン使用量を折りたたむ", "messageBlock.step.agentLabel": "エージェント: {agent}", "messageBlock.step.modelLabel": "モデル: {model}", "messageBlock.reasoning.thinkingLabel": "思考", diff --git a/packages/ui/src/lib/i18n/messages/ja/settings.ts b/packages/ui/src/lib/i18n/messages/ja/settings.ts index 1d2448689..6b266a108 100644 --- a/packages/ui/src/lib/i18n/messages/ja/settings.ts +++ b/packages/ui/src/lib/i18n/messages/ja/settings.ts @@ -98,6 +98,16 @@ export const settingsMessages = { "settings.open.title": "設定を開く", "settings.open.ariaLabel": "設定を開く", "settings.nav.appearance": "外観", + "settings.nav.general": "一般", + "settings.nav.chat": "チャット", + "settings.nav.voice": "音声", + "settings.nav.connections": "接続", + "settings.nav.runtime": "ランタイム", + "settings.nav.advanced": "詳細設定", + "settings.nav.about": "情報", + "settings.general.language.subtitle": "アプリケーション全体で使用する言語を選択します。", + "settings.behavior.holdLongAssistantReplies.title": "長いアシスタント応答を保持", + "settings.behavior.holdLongAssistantReplies.subtitle": "ストリーミング応答が画面を超えたときに自動追従を停止します。", "settings.nav.notifications": "通知", "settings.nav.remote": "リモートアクセス", "settings.nav.speech": "音声", @@ -200,6 +210,7 @@ export const settingsMessages = { "settings.configFiles.confirmDiscard.cancelLabel": "編集を続ける", "settings.providers.title": "プロバイダー認証", + "settings.providers.target": "OpenCode インスタンス: {workspace}", "settings.providers.subtitle": "アクティブな OpenCode インスタンスで使う API キーまたは OAuth アカウントを接続します。", "settings.providers.refresh": "更新", "settings.providers.selectProvider": "プロバイダーを選択", @@ -257,8 +268,8 @@ export const settingsMessages = { "settings.providers.errors.envDisconnectUnavailable": "このプロバイダーは環境変数から設定されているため、UI から切断できません。", "settings.appearance.behavior.title": "操作", - "settings.appearance.behavior.subtitle": "メッセージ、差分、入力の既定値。", - "settings.behavior.expansionPresets.ariaLabel": "Transcript detail presets", + "settings.appearance.behavior.subtitle": "メッセージ、ツール、診断、使用量の詳細表示をまとめて制御します。", + "settings.behavior.expansionPresets.ariaLabel": "会話詳細プリセット", "settings.behavior.expansionPreset.minimal.title": "Minimal", "settings.behavior.expansionPreset.minimal.description": "Keep most generated detail tucked away until needed.", "settings.behavior.expansionPreset.balanced.title": "Balanced", diff --git a/packages/ui/src/lib/i18n/messages/ne/commands.ts b/packages/ui/src/lib/i18n/messages/ne/commands.ts index 89f1d10be..f7c9eab64 100644 --- a/packages/ui/src/lib/i18n/messages/ne/commands.ts +++ b/packages/ui/src/lib/i18n/messages/ne/commands.ts @@ -123,12 +123,12 @@ export const commandMessages = { "commands.diffViewUnified.keywords": "डिफ, एकीकृत, दृश्य", "commands.toolOutputsDefault.label": "उपकरण आउटपुट पूर्वनिर्धारित · {state}", - "commands.toolOutputsDefault.description": "उपकरण आउटपुटहरूको लागि पूर्वनिर्धारित विस्तार टगल गर्नुहोस्", - "commands.toolOutputsDefault.keywords": "उपकरण, आउटपुट, विस्तार, संकुचित", + "commands.toolOutputsDefault.description": "उपकरण आउटपुटहरूको पूर्वनिर्धारित दृश्यता सेट गर्नुहोस्", + "commands.toolOutputsDefault.keywords": "उपकरण, आउटपुट, लुकाउनुहोस्, विस्तार, संकुचित", "commands.diagnosticsDefault.label": "निदान (Diagnostics) पूर्वनिर्धारित · {state}", - "commands.diagnosticsDefault.description": "निदान आउटपुटको लागि पूर्वनिर्धारित विस्तार टगल गर्नुहोस्", - "commands.diagnosticsDefault.keywords": "निदान, विस्तार, संकुचित", + "commands.diagnosticsDefault.description": "निदान आउटपुटको पूर्वनिर्धारित दृश्यता सेट गर्नुहोस्", + "commands.diagnosticsDefault.keywords": "निदान, लुकाउनुहोस्, विस्तार, संकुचित", "commands.toolInputsVisibility.label": "उपकरण इनपुट दृश्यता · {state}", "commands.toolInputsVisibility.description": "उपकरण कल इनपुट तर्कहरूको लागि पूर्वनिर्धारित दृश्यता सेट गर्नुहोस्", diff --git a/packages/ui/src/lib/i18n/messages/ne/messaging.ts b/packages/ui/src/lib/i18n/messages/ne/messaging.ts index 023a31e93..5ba82e028 100644 --- a/packages/ui/src/lib/i18n/messages/ne/messaging.ts +++ b/packages/ui/src/lib/i18n/messages/ne/messaging.ts @@ -72,6 +72,8 @@ export const messagingMessages = { "messageBlock.usage.cacheRead": "क्यास पढाइ", "messageBlock.usage.cacheWrite": "क्यास लेखाइ", "messageBlock.usage.cost": "लागत (Cost)", + "messageBlock.usage.expandAriaLabel": "टोकन मेट्रिक्स विस्तार गर्नुहोस्", + "messageBlock.usage.collapseAriaLabel": "टोकन मेट्रिक्स संकुचित गर्नुहोस्", "messageBlock.step.agentLabel": "एजेन्ट: {agent}", "messageBlock.step.modelLabel": "मोडेल: {model}", "messageBlock.reasoning.thinkingLabel": "सोच्दै", diff --git a/packages/ui/src/lib/i18n/messages/ne/settings.ts b/packages/ui/src/lib/i18n/messages/ne/settings.ts index 0da3e9cfd..cbfbb0953 100644 --- a/packages/ui/src/lib/i18n/messages/ne/settings.ts +++ b/packages/ui/src/lib/i18n/messages/ne/settings.ts @@ -98,6 +98,16 @@ export const settingsMessages = { "settings.open.title": "सेटिङहरू खोल्नुहोस्", "settings.open.ariaLabel": "सेटिङहरू खोल्नुहोस्", "settings.nav.appearance": "रूप (Appearance)", + "settings.nav.general": "सामान्य", + "settings.nav.chat": "च्याट", + "settings.nav.voice": "आवाज", + "settings.nav.connections": "जडानहरू", + "settings.nav.runtime": "रनटाइम", + "settings.nav.advanced": "उन्नत", + "settings.nav.about": "बारेमा", + "settings.general.language.subtitle": "सम्पूर्ण अनुप्रयोगमा प्रयोग हुने भाषा छान्नुहोस्।", + "settings.behavior.holdLongAssistantReplies.title": "लामो सहायक जवाफहरू रोक्नुहोस्", + "settings.behavior.holdLongAssistantReplies.subtitle": "स्ट्रिमिङ जवाफ दृश्यभन्दा बाहिर जाँदा स्वचालित पछ्याइ रोक्नुहोस्।", "settings.nav.notifications": "सूचनाहरू", "settings.nav.remote": "रिमोट पहुँच", "settings.nav.speech": "वाचन (Speech)", @@ -200,6 +210,7 @@ export const settingsMessages = { "settings.configFiles.confirmDiscard.cancelLabel": "सम्पादन जारी राख्नुहोस्", "settings.providers.title": "प्रदायक प्रमाणीकरण", + "settings.providers.target": "OpenCode इन्स्ट्यान्स: {workspace}", "settings.providers.subtitle": "सक्रिय OpenCode उदाहरणका लागि API कुञ्जीहरू वा OAuth खाताहरू जडान गर्नुहोस्।", "settings.providers.refresh": "रिफ्रेस", "settings.providers.selectProvider": "प्रदायक चयन गर्नुहोस्", @@ -258,8 +269,8 @@ export const settingsMessages = { "settings.appearance.behavior.title": "अन्तरक्रिया", - "settings.appearance.behavior.subtitle": "सन्देश, डिफ र इनपुट पूर्वनिर्धारितहरू।", - "settings.behavior.expansionPresets.ariaLabel": "Transcript detail presets", + "settings.appearance.behavior.subtitle": "सन्देश, उपकरण, निदान र प्रयोग विवरण एकै ठाउँबाट नियन्त्रण गर्नुहोस्।", + "settings.behavior.expansionPresets.ariaLabel": "संवाद विवरण प्रिसेटहरू", "settings.behavior.expansionPreset.minimal.title": "Minimal", "settings.behavior.expansionPreset.minimal.description": "Keep most generated detail tucked away until needed.", "settings.behavior.expansionPreset.balanced.title": "Balanced", diff --git a/packages/ui/src/lib/i18n/messages/ru/commands.ts b/packages/ui/src/lib/i18n/messages/ru/commands.ts index 3f1551a97..8d674f48a 100644 --- a/packages/ui/src/lib/i18n/messages/ru/commands.ts +++ b/packages/ui/src/lib/i18n/messages/ru/commands.ts @@ -123,12 +123,12 @@ export const commandMessages = { "commands.diffViewUnified.keywords": "diff, unified, просмотр", "commands.toolOutputsDefault.label": "Выводы инструментов по умолчанию · {state}", - "commands.toolOutputsDefault.description": "Переключить, разворачивать ли выводы инструментов по умолчанию", - "commands.toolOutputsDefault.keywords": "tool, output, развернуть, свернуть", + "commands.toolOutputsDefault.description": "Задать видимость выводов инструментов по умолчанию", + "commands.toolOutputsDefault.keywords": "tool, output, скрыть, развернуть, свернуть", "commands.diagnosticsDefault.label": "Диагностика по умолчанию · {state}", - "commands.diagnosticsDefault.description": "Переключить, разворачивать ли вывод диагностики по умолчанию", - "commands.diagnosticsDefault.keywords": "diagnostics, развернуть, свернуть", + "commands.diagnosticsDefault.description": "Задать видимость вывода диагностики по умолчанию", + "commands.diagnosticsDefault.keywords": "diagnostics, скрыть, развернуть, свернуть", "commands.toolInputsVisibility.label": "Видимость входных данных инструмента · {state}", "commands.toolInputsVisibility.description": "Установить видимость аргументов входа вызовов инструментов по умолчанию", diff --git a/packages/ui/src/lib/i18n/messages/ru/messaging.ts b/packages/ui/src/lib/i18n/messages/ru/messaging.ts index 3cdb943eb..02aaa1c3c 100644 --- a/packages/ui/src/lib/i18n/messages/ru/messaging.ts +++ b/packages/ui/src/lib/i18n/messages/ru/messaging.ts @@ -74,6 +74,8 @@ export const messagingMessages = { "messageBlock.usage.cacheRead": "Чтение кэша", "messageBlock.usage.cacheWrite": "Запись кэша", "messageBlock.usage.cost": "Стоимость", + "messageBlock.usage.expandAriaLabel": "Развернуть метрики токенов", + "messageBlock.usage.collapseAriaLabel": "Свернуть метрики токенов", "messageBlock.step.agentLabel": "Агент: {agent}", "messageBlock.step.modelLabel": "Модель: {model}", "messageBlock.reasoning.thinkingLabel": "Размышления", diff --git a/packages/ui/src/lib/i18n/messages/ru/settings.ts b/packages/ui/src/lib/i18n/messages/ru/settings.ts index 201da2216..d364ab5a6 100644 --- a/packages/ui/src/lib/i18n/messages/ru/settings.ts +++ b/packages/ui/src/lib/i18n/messages/ru/settings.ts @@ -98,6 +98,16 @@ export const settingsMessages = { "settings.open.title": "Открыть настройки", "settings.open.ariaLabel": "Открыть настройки", "settings.nav.appearance": "Внешний вид", + "settings.nav.general": "Общие", + "settings.nav.chat": "Чат", + "settings.nav.voice": "Голос", + "settings.nav.connections": "Подключения", + "settings.nav.runtime": "Среда выполнения", + "settings.nav.advanced": "Расширенные", + "settings.nav.about": "О программе", + "settings.general.language.subtitle": "Выберите язык, используемый во всём приложении.", + "settings.behavior.holdLongAssistantReplies.title": "Удерживать длинные ответы ассистента", + "settings.behavior.holdLongAssistantReplies.subtitle": "Прекращать автоматическое следование, когда потоковый ответ выходит за пределы окна.", "settings.nav.notifications": "Уведомления", "settings.nav.remote": "Удалённый доступ", "settings.nav.speech": "Речь", @@ -200,6 +210,7 @@ export const settingsMessages = { "settings.configFiles.confirmDiscard.cancelLabel": "Продолжить редактирование", "settings.providers.title": "Аутентификация провайдеров", + "settings.providers.target": "Экземпляр OpenCode: {workspace}", "settings.providers.subtitle": "Подключите API-ключи или OAuth-аккаунты для активного экземпляра OpenCode.", "settings.providers.refresh": "Обновить", "settings.providers.selectProvider": "Выберите провайдера", @@ -257,8 +268,8 @@ export const settingsMessages = { "settings.providers.errors.envDisconnectUnavailable": "Этот провайдер настроен через переменные окружения и не может быть отключён из интерфейса.", "settings.appearance.behavior.title": "Взаимодействие", - "settings.appearance.behavior.subtitle": "Значения по умолчанию для сообщений, диффов и ввода.", - "settings.behavior.expansionPresets.ariaLabel": "Transcript detail presets", + "settings.appearance.behavior.subtitle": "Общее управление детализацией сообщений, инструментов, диагностики и использования.", + "settings.behavior.expansionPresets.ariaLabel": "Предустановки детализации диалога", "settings.behavior.expansionPreset.minimal.title": "Minimal", "settings.behavior.expansionPreset.minimal.description": "Keep most generated detail tucked away until needed.", "settings.behavior.expansionPreset.balanced.title": "Balanced", diff --git a/packages/ui/src/lib/i18n/messages/zh-Hans/commands.ts b/packages/ui/src/lib/i18n/messages/zh-Hans/commands.ts index e3c31d41e..a8078c143 100644 --- a/packages/ui/src/lib/i18n/messages/zh-Hans/commands.ts +++ b/packages/ui/src/lib/i18n/messages/zh-Hans/commands.ts @@ -123,12 +123,12 @@ export const commandMessages = { "commands.diffViewUnified.keywords": "diff, unified, view, 差异, 统一, 视图", "commands.toolOutputsDefault.label": "工具输出默认 · {state}", - "commands.toolOutputsDefault.description": "切换工具输出是否默认展开", - "commands.toolOutputsDefault.keywords": "tool, output, expand, collapse, 工具, 输出, 展开, 折叠", + "commands.toolOutputsDefault.description": "设置工具输出的默认可见性", + "commands.toolOutputsDefault.keywords": "tool, output, hide, expand, collapse, 工具, 输出, 隐藏, 展开, 折叠", "commands.diagnosticsDefault.label": "诊断默认 · {state}", - "commands.diagnosticsDefault.description": "切换诊断输出是否默认展开", - "commands.diagnosticsDefault.keywords": "diagnostics, expand, collapse, 诊断, 展开, 折叠", + "commands.diagnosticsDefault.description": "设置诊断输出的默认可见性", + "commands.diagnosticsDefault.keywords": "diagnostics, hide, expand, collapse, 诊断, 隐藏, 展开, 折叠", "commands.toolInputsVisibility.label": "工具输入可见性 · {state}", "commands.toolInputsVisibility.description": "设置工具调用输入参数的默认可见性", diff --git a/packages/ui/src/lib/i18n/messages/zh-Hans/messaging.ts b/packages/ui/src/lib/i18n/messages/zh-Hans/messaging.ts index 5b32cbaa9..bb36a3814 100644 --- a/packages/ui/src/lib/i18n/messages/zh-Hans/messaging.ts +++ b/packages/ui/src/lib/i18n/messages/zh-Hans/messaging.ts @@ -74,6 +74,8 @@ export const messagingMessages = { "messageBlock.usage.cacheRead": "缓存读取", "messageBlock.usage.cacheWrite": "缓存写入", "messageBlock.usage.cost": "费用", + "messageBlock.usage.expandAriaLabel": "展开令牌指标", + "messageBlock.usage.collapseAriaLabel": "折叠令牌指标", "messageBlock.step.agentLabel": "智能体:{agent}", "messageBlock.step.modelLabel": "模型:{model}", "messageBlock.reasoning.thinkingLabel": "思考", diff --git a/packages/ui/src/lib/i18n/messages/zh-Hans/settings.ts b/packages/ui/src/lib/i18n/messages/zh-Hans/settings.ts index 4aafaa980..1eed86ce0 100644 --- a/packages/ui/src/lib/i18n/messages/zh-Hans/settings.ts +++ b/packages/ui/src/lib/i18n/messages/zh-Hans/settings.ts @@ -98,6 +98,16 @@ export const settingsMessages = { "settings.open.title": "打开设置", "settings.open.ariaLabel": "打开设置", "settings.nav.appearance": "外观", + "settings.nav.general": "常规", + "settings.nav.chat": "聊天", + "settings.nav.voice": "语音", + "settings.nav.connections": "连接", + "settings.nav.runtime": "运行时", + "settings.nav.advanced": "高级", + "settings.nav.about": "关于", + "settings.general.language.subtitle": "选择整个应用程序使用的语言。", + "settings.behavior.holdLongAssistantReplies.title": "暂停跟随较长的助手回复", + "settings.behavior.holdLongAssistantReplies.subtitle": "当流式回复超出视口时停止自动跟随。", "settings.nav.notifications": "通知", "settings.nav.remote": "远程访问", "settings.nav.speech": "语音", @@ -200,6 +210,7 @@ export const settingsMessages = { "settings.configFiles.confirmDiscard.cancelLabel": "继续编辑", "settings.providers.title": "提供商认证", + "settings.providers.target": "OpenCode 实例:{workspace}", "settings.providers.subtitle": "为当前 OpenCode 实例连接 API 密钥或 OAuth 账号。", "settings.providers.refresh": "刷新", "settings.providers.selectProvider": "选择提供商", @@ -257,8 +268,8 @@ export const settingsMessages = { "settings.providers.errors.envDisconnectUnavailable": "此提供商由环境变量配置,无法从界面断开连接。", "settings.appearance.behavior.title": "交互", - "settings.appearance.behavior.subtitle": "消息、差异与输入的默认值。", - "settings.behavior.expansionPresets.ariaLabel": "Transcript detail presets", + "settings.appearance.behavior.subtitle": "统一控制消息、工具、诊断和用量详情。", + "settings.behavior.expansionPresets.ariaLabel": "对话详情预设", "settings.behavior.expansionPreset.minimal.title": "Minimal", "settings.behavior.expansionPreset.minimal.description": "Keep most generated detail tucked away until needed.", "settings.behavior.expansionPreset.balanced.title": "Balanced", diff --git a/packages/ui/src/lib/settings/behavior-registry.ts b/packages/ui/src/lib/settings/behavior-registry.ts index b28229a12..0cca660f7 100644 --- a/packages/ui/src/lib/settings/behavior-registry.ts +++ b/packages/ui/src/lib/settings/behavior-registry.ts @@ -3,6 +3,7 @@ import type { Preferences, ExpansionPreference, ToolInputsVisibilityPreference, + VisibilityPreference, } from "../../stores/preferences" import type { Command } from "../commands" import { tGlobal } from "../i18n" @@ -47,8 +48,8 @@ export type BehaviorRegistryActions = { togglePromptSubmitOnEnter: () => void toggleShowPromptVoiceInput: () => void setDiffViewMode: (mode: "split" | "unified") => void - setToolOutputExpansion: (mode: ExpansionPreference) => void - setDiagnosticsExpansion: (mode: ExpansionPreference) => void + setToolOutputExpansion: (mode: VisibilityPreference) => void + setDiagnosticsExpansion: (mode: VisibilityPreference) => void setThinkingBlocksExpansion: (mode: ExpansionPreference) => void setToolInputsVisibility: (mode: ToolInputsVisibilityPreference) => void } @@ -65,6 +66,14 @@ function setBooleanByToggle(getCurrent: () => boolean, toggle: () => void, next: toggle() } +function getToolOutputVisibility(preferences: Preferences): VisibilityPreference { + const defaults = preferences.toolCallExpansionDefaults + if (defaults.tools.other) return defaults.tools.other + if (defaults.preset === "minimal" || defaults.preset === "balanced") return "collapsed" + if (defaults.preset === "detailed" || defaults.preset === "everything") return "expanded" + return preferences.toolOutputExpansion ?? "expanded" +} + export function getBehaviorSettings(actions: BehaviorRegistryActions): BehaviorSetting[] { const prefs = actions.preferences const updatePreferences = actions.updatePreferences @@ -184,15 +193,25 @@ export function getBehaviorSettings(actions: BehaviorRegistryActions): BehaviorS id: "behavior.toolOutputsDefault", titleKey: "settings.behavior.toolOutputsDefault.title", subtitleKey: "settings.behavior.toolOutputsDefault.subtitle", - get: (p) => (p.toolOutputExpansion ?? "expanded") as ExpansionPreference, + get: getToolOutputVisibility, set: (next) => { if (updatePreferences) { - updatePreferences({ toolOutputExpansion: next as ExpansionPreference }) + const mode = next as VisibilityPreference + const current = prefs() + updatePreferences({ + toolOutputExpansion: mode === "hidden" ? current.toolOutputExpansion : mode, + toolCallExpansionDefaults: { + ...current.toolCallExpansionDefaults, + preset: "custom", + tools: { ...current.toolCallExpansionDefaults.tools, other: mode }, + }, + }) return } - actions.setToolOutputExpansion(next as ExpansionPreference) + actions.setToolOutputExpansion(next as VisibilityPreference) }, options: [ + { value: "hidden", labelKey: "commands.common.hidden" }, { value: "expanded", labelKey: "commands.common.expanded" }, { value: "collapsed", labelKey: "commands.common.collapsed" }, ], @@ -202,15 +221,16 @@ export function getBehaviorSettings(actions: BehaviorRegistryActions): BehaviorS id: "behavior.diagnosticsDefault", titleKey: "settings.behavior.diagnosticsDefault.title", subtitleKey: "settings.behavior.diagnosticsDefault.subtitle", - get: (p) => (p.diagnosticsExpansion ?? "expanded") as ExpansionPreference, + get: (p) => (p.diagnosticsExpansion ?? "expanded") as VisibilityPreference, set: (next) => { if (updatePreferences) { - updatePreferences({ diagnosticsExpansion: next as ExpansionPreference }) + updatePreferences({ diagnosticsExpansion: next as VisibilityPreference }) return } - actions.setDiagnosticsExpansion(next as ExpansionPreference) + actions.setDiagnosticsExpansion(next as VisibilityPreference) }, options: [ + { value: "hidden", labelKey: "commands.common.hidden" }, { value: "expanded", labelKey: "commands.common.expanded" }, { value: "collapsed", labelKey: "commands.common.collapsed" }, ], @@ -434,16 +454,20 @@ export function getBehaviorCommands(actions: BehaviorRegistryActions): Command[] { id: "tool-output-default-visibility", label: () => { - const mode = actions.preferences().toolOutputExpansion || "expanded" - const state = mode === "expanded" ? tGlobal("commands.common.expanded") : tGlobal("commands.common.collapsed") + const mode = getToolOutputVisibility(actions.preferences()) + const state = mode === "expanded" + ? tGlobal("commands.common.expanded") + : mode === "collapsed" + ? tGlobal("commands.common.collapsed") + : tGlobal("commands.common.hidden") return tGlobal("commands.toolOutputsDefault.label", { state }) }, description: () => tGlobal("commands.toolOutputsDefault.description"), category: "System", keywords: () => splitKeywords("commands.toolOutputsDefault.keywords"), action: () => { - const mode = actions.preferences().toolOutputExpansion || "expanded" - const next: ExpansionPreference = mode === "expanded" ? "collapsed" : "expanded" + const mode = getToolOutputVisibility(actions.preferences()) + const next: VisibilityPreference = mode === "hidden" ? "collapsed" : mode === "collapsed" ? "expanded" : "hidden" actions.setToolOutputExpansion(next) }, }, @@ -451,7 +475,11 @@ export function getBehaviorCommands(actions: BehaviorRegistryActions): Command[] id: "diagnostics-default-visibility", label: () => { const mode = actions.preferences().diagnosticsExpansion || "expanded" - const state = mode === "expanded" ? tGlobal("commands.common.expanded") : tGlobal("commands.common.collapsed") + const state = mode === "expanded" + ? tGlobal("commands.common.expanded") + : mode === "collapsed" + ? tGlobal("commands.common.collapsed") + : tGlobal("commands.common.hidden") return tGlobal("commands.diagnosticsDefault.label", { state }) }, description: () => tGlobal("commands.diagnosticsDefault.description"), @@ -459,7 +487,7 @@ export function getBehaviorCommands(actions: BehaviorRegistryActions): Command[] keywords: () => splitKeywords("commands.diagnosticsDefault.keywords"), action: () => { const mode = actions.preferences().diagnosticsExpansion || "expanded" - const next: ExpansionPreference = mode === "expanded" ? "collapsed" : "expanded" + const next: VisibilityPreference = mode === "hidden" ? "collapsed" : mode === "collapsed" ? "expanded" : "hidden" actions.setDiagnosticsExpansion(next) }, }, diff --git a/packages/ui/src/stores/preferences.tsx b/packages/ui/src/stores/preferences.tsx index ef75092b4..2f39824b2 100644 --- a/packages/ui/src/stores/preferences.tsx +++ b/packages/ui/src/stores/preferences.tsx @@ -32,9 +32,10 @@ export interface ModelPreference { export type DiffViewMode = "split" | "unified" export type ExpansionPreference = "expanded" | "collapsed" +export type VisibilityPreference = "hidden" | ExpansionPreference export type ToolCallExpansionPreset = "minimal" | "balanced" | "detailed" | "everything" export type ToolCallExpansionPresetSelection = ToolCallExpansionPreset | "custom" -export type ToolInputsVisibilityPreference = "hidden" | "collapsed" | "expanded" +export type ToolInputsVisibilityPreference = VisibilityPreference export type ListeningMode = "local" | "all" export type ServerLogLevel = "DEBUG" | "INFO" | "WARN" | "ERROR" export type SpeechProviderPreference = "openai-compatible" @@ -44,7 +45,7 @@ export type SpeechTtsFormat = "mp3" | "wav" | "opus" | "aac" export interface ToolCallExpansionDefaults { preset: ToolCallExpansionPresetSelection thinking: ExpansionPreference - tools: Record + tools: Record } export interface SpeechSettings { @@ -96,9 +97,10 @@ export interface UiSettings { diffViewMode: DiffViewMode toolCallExpansionDefaults: ToolCallExpansionDefaults toolOutputExpansion: ExpansionPreference - diagnosticsExpansion: ExpansionPreference + diagnosticsExpansion: VisibilityPreference toolInputsVisibility: ToolInputsVisibilityPreference showUsageMetrics: boolean + usageMetricsExpansion: ExpansionPreference autoCleanupBlankSessions: boolean keepUnseenSubagentIdleStatus: boolean @@ -188,6 +190,7 @@ const defaultUiSettings: UiSettings = { diagnosticsExpansion: "expanded", toolInputsVisibility: "collapsed", showUsageMetrics: true, + usageMetricsExpansion: "collapsed", autoCleanupBlankSessions: true, keepUnseenSubagentIdleStatus: false, @@ -201,6 +204,10 @@ function normalizeExpansionPreference(value: unknown, fallback: ExpansionPrefere return value === "expanded" || value === "collapsed" ? value : fallback } +function normalizeVisibilityPreference(value: unknown, fallback: VisibilityPreference): VisibilityPreference { + return value === "hidden" || value === "expanded" || value === "collapsed" ? value : fallback +} + function normalizeToolCallExpansionPreset(value: unknown): ToolCallExpansionPresetSelection { if (value === "minimal" || value === "balanced" || value === "detailed" || value === "everything" || value === "custom") { return value @@ -208,12 +215,12 @@ function normalizeToolCallExpansionPreset(value: unknown): ToolCallExpansionPres return defaultToolCallExpansionDefaults.preset } -function normalizeToolCallExpansionTools(value: unknown): Record { +function normalizeToolCallExpansionTools(value: unknown): Record { if (!value || typeof value !== "object" || Array.isArray(value)) return {} - const next: Record = {} + const next: Record = {} for (const [tool, mode] of Object.entries(value as Record)) { if (!tool) continue - if (mode === "expanded" || mode === "collapsed") { + if (mode === "hidden" || mode === "expanded" || mode === "collapsed") { next[tool] = mode } } @@ -226,7 +233,7 @@ function normalizeToolCallExpansionDefaults(input: unknown, legacySettings: Part : undefined const legacyThinking = normalizeExpansionPreference( legacySettings.thinkingBlocksExpansion, - defaultToolCallExpansionDefaults.thinking, + "collapsed", ) return { @@ -257,6 +264,11 @@ const defaultSpeechSettings: SpeechSettings = { function normalizeUiSettings(input?: Partial | null): UiSettings { const sanitized = input ?? {} + const toolCallExpansionDefaults = normalizeToolCallExpansionDefaults(sanitized.toolCallExpansionDefaults, sanitized) + const usageMetricsFallback = + toolCallExpansionDefaults.preset === "minimal" || toolCallExpansionDefaults.preset === "balanced" + ? "collapsed" + : "expanded" return { showThinkingBlocks: sanitized.showThinkingBlocks ?? defaultUiSettings.showThinkingBlocks, showKeyboardShortcutHints: @@ -269,14 +281,21 @@ function normalizeUiSettings(input?: Partial | null): UiSettings { showPromptVoiceInput: sanitized.showPromptVoiceInput ?? defaultUiSettings.showPromptVoiceInput, locale: sanitized.locale ?? defaultUiSettings.locale, diffViewMode: sanitized.diffViewMode ?? defaultUiSettings.diffViewMode, - toolCallExpansionDefaults: normalizeToolCallExpansionDefaults(sanitized.toolCallExpansionDefaults, sanitized), + toolCallExpansionDefaults, toolOutputExpansion: sanitized.toolOutputExpansion ?? defaultUiSettings.toolOutputExpansion, - diagnosticsExpansion: sanitized.diagnosticsExpansion ?? defaultUiSettings.diagnosticsExpansion, + diagnosticsExpansion: normalizeVisibilityPreference( + sanitized.diagnosticsExpansion, + defaultUiSettings.diagnosticsExpansion, + ), toolInputsVisibility: sanitized.toolInputsVisibility === "hidden" || sanitized.toolInputsVisibility === "collapsed" || sanitized.toolInputsVisibility === "expanded" ? sanitized.toolInputsVisibility : defaultUiSettings.toolInputsVisibility, showUsageMetrics: sanitized.showUsageMetrics ?? defaultUiSettings.showUsageMetrics, + usageMetricsExpansion: normalizeExpansionPreference( + sanitized.usageMetricsExpansion, + usageMetricsFallback, + ), autoCleanupBlankSessions: sanitized.autoCleanupBlankSessions ?? defaultUiSettings.autoCleanupBlankSessions, keepUnseenSubagentIdleStatus: sanitized.keepUnseenSubagentIdleStatus ?? defaultUiSettings.keepUnseenSubagentIdleStatus, @@ -822,11 +841,11 @@ function setDiffViewMode(mode: DiffViewMode): void { updateUiSettings({ diffViewMode: mode }) } -function setToolOutputExpansion(mode: ExpansionPreference): void { +function setToolOutputExpansion(mode: VisibilityPreference): void { const current = preferences() - if (current.toolOutputExpansion === mode && current.toolCallExpansionDefaults.tools.other === mode) return + if (current.toolCallExpansionDefaults.tools.other === mode) return updateUiSettings({ - toolOutputExpansion: mode, + toolOutputExpansion: mode === "hidden" ? current.toolOutputExpansion : mode, toolCallExpansionDefaults: { ...current.toolCallExpansionDefaults, preset: "custom", @@ -838,7 +857,7 @@ function setToolOutputExpansion(mode: ExpansionPreference): void { }) } -function setDiagnosticsExpansion(mode: ExpansionPreference): void { +function setDiagnosticsExpansion(mode: VisibilityPreference): void { if (preferences().diagnosticsExpansion === mode) return updateUiSettings({ diagnosticsExpansion: mode }) } diff --git a/packages/ui/src/stores/settings-screen.ts b/packages/ui/src/stores/settings-screen.ts index 2a79e61f3..f83f009a6 100644 --- a/packages/ui/src/stores/settings-screen.ts +++ b/packages/ui/src/stores/settings-screen.ts @@ -1,19 +1,22 @@ import { createSignal } from "solid-js" export type SettingsSectionId = - | "appearance" + | "general" + | "chat" | "notifications" - | "remote" | "speech" + | "remote" | "opencode" - | "config-files" + | "providers" | "sidecars" + | "config-files" + | "advanced" | "info" const [settingsOpen, setSettingsOpen] = createSignal(false) -const [activeSettingsSection, setActiveSettingsSection] = createSignal("appearance") +const [activeSettingsSection, setActiveSettingsSection] = createSignal("general") -export function openSettings(section: SettingsSectionId = "appearance") { +export function openSettings(section: SettingsSectionId = "general") { setActiveSettingsSection(section) setSettingsOpen(true) } diff --git a/packages/ui/src/styles/components/config-files-settings.css b/packages/ui/src/styles/components/config-files-settings.css index c8190d4cd..929cd46a1 100644 --- a/packages/ui/src/styles/components/config-files-settings.css +++ b/packages/ui/src/styles/components/config-files-settings.css @@ -5,6 +5,11 @@ flex-direction: column; } +.advanced-config-files { + height: 36rem; + min-height: 28rem; +} + .config-files-card { height: 100%; min-height: 0; diff --git a/packages/ui/src/styles/components/provider-auth.css b/packages/ui/src/styles/components/provider-auth.css index 1acc258ca..3132b842a 100644 --- a/packages/ui/src/styles/components/provider-auth.css +++ b/packages/ui/src/styles/components/provider-auth.css @@ -12,6 +12,19 @@ border-radius: 0; } +.providers-manager-embedded { + position: static; + transform: none; + width: 100%; + max-height: none; + border: 1px solid var(--border-base); + background: var(--surface-base); +} + +.providers-manager-embedded .providers-manager-body { + overflow: visible; +} + .providers-manager-header { display: flex; align-items: flex-start; @@ -338,6 +351,12 @@ height: 100dvh; } + .providers-manager-embedded { + width: 100%; + max-height: none; + height: auto; + } + .providers-connect-bar, .providers-panel-header { grid-template-columns: 1fr; diff --git a/packages/ui/src/styles/components/settings-screen.css b/packages/ui/src/styles/components/settings-screen.css index 022efdba0..466fd32fc 100644 --- a/packages/ui/src/styles/components/settings-screen.css +++ b/packages/ui/src/styles/components/settings-screen.css @@ -41,6 +41,8 @@ display: flex; flex-direction: column; gap: 1rem; + min-height: 0; + overflow: hidden; padding: 1.25rem; background: linear-gradient(180deg, color-mix(in oklab, var(--surface-secondary) 92%, var(--accent-primary) 8%), var(--surface-secondary)); @@ -94,6 +96,9 @@ display: flex; flex-direction: column; gap: 0.35rem; + min-height: 0; + overflow-x: hidden; + overflow-y: auto; } .settings-nav-button { @@ -112,7 +117,7 @@ .settings-nav-button:focus-visible { outline: 2px solid var(--accent-primary); - outline-offset: 2px; + outline-offset: -2px; } .settings-nav-button:hover { @@ -551,9 +556,6 @@ .settings-expansion-table { margin-bottom: 1.25rem; border: 1px solid var(--border-base); - border-radius: 0; - background: var(--surface-base); - overflow: hidden; } .settings-expansion-table-header, diff --git a/packages/ui/src/styles/messaging/message-base.css b/packages/ui/src/styles/messaging/message-base.css index 74611cc81..986df61cc 100644 --- a/packages/ui/src/styles/messaging/message-base.css +++ b/packages/ui/src/styles/messaging/message-base.css @@ -214,6 +214,15 @@ @apply flex flex-wrap items-center gap-1 text-[10px] text-[var(--text-muted)]; } +.message-step-usage-toggle { + @apply flex min-w-0 items-center gap-1 text-left; +} + +.message-step-usage-disclosure { + @apply shrink-0 text-[9px]; + color: var(--text-muted); +} + .message-step-usage-chip { @apply inline-flex items-center rounded-full border border-[var(--border-base)] px-2 py-0.5 text-[10px]; color: var(--text-primary);