diff --git a/packages/app/src/components/settings-v2/data-storage-controller.ts b/packages/app/src/components/settings-v2/data-storage-controller.ts new file mode 100644 index 000000000..f23ff8327 --- /dev/null +++ b/packages/app/src/components/settings-v2/data-storage-controller.ts @@ -0,0 +1,100 @@ +import { createSignal, onCleanup, onMount } from "solid-js" +import { useSettings } from "@/context/settings" +import { inAmicode } from "@/utils/amicode-bridge" + +export interface DataStorageStatus { + databaseValid: boolean + databaseError?: string + configValid: boolean + configError?: string + serverRestarted: boolean +} + +export interface DataStorageDefaults { + databasePath: string + configDir: string +} + +export function createDataStorageController() { + const settings = useSettings() + const [status, setStatus] = createSignal(undefined) + const [defaults, setDefaults] = createSignal(undefined) + const [pending, setPending] = createSignal(false) + + // On mount, query the extension host for the resolved default paths + onMount(() => { + if (!inAmicode()) return + window.parent.postMessage( + { + source: "amicode", + kind: "data-storage-query", + }, + "*", + ) + }) + + // Listen for the extension host's replies + const handleMessage = (event: MessageEvent) => { + const d = event.data + if (!d || d.source !== "amicode") return + + // Defaults reply (on mount) + if (d.kind === "data-storage-defaults") { + setDefaults({ + databasePath: typeof d.databasePath === "string" ? d.databasePath : "", + configDir: typeof d.configDir === "string" ? d.configDir : "", + }) + } + + // Status reply (after update) + if (d.kind === "data-storage-status") { + setStatus({ + databaseValid: d.databaseValid ?? true, + databaseError: d.databaseError, + configValid: d.configValid ?? true, + configError: d.configError, + serverRestarted: d.serverRestarted ?? false, + }) + setPending(false) + } + } + + if (typeof window !== "undefined") { + window.addEventListener("message", handleMessage) + onCleanup(() => window.removeEventListener("message", handleMessage)) + } + + const sendUpdate = () => { + if (!inAmicode()) return + setPending(true) + setStatus(undefined) + window.parent.postMessage( + { + source: "amicode", + kind: "data-storage-update", + databasePath: settings.storage.databasePath(), + configDir: settings.storage.configDir(), + }, + "*", + ) + } + + return { + databasePath: settings.storage.databasePath, + setDatabasePath: (value: string) => { + settings.storage.setDatabasePath(value) + }, + configDir: settings.storage.configDir, + setConfigDir: (value: string) => { + settings.storage.setConfigDir(value) + }, + /** Trigger validation + apply on blur */ + commitDatabasePath: () => sendUpdate(), + commitConfigDir: () => sendUpdate(), + status, + defaults, + pending, + } +} + +export type DataStorageController = ReturnType diff --git a/packages/app/src/components/settings-v2/data-storage.tsx b/packages/app/src/components/settings-v2/data-storage.tsx new file mode 100644 index 000000000..430e3a3df --- /dev/null +++ b/packages/app/src/components/settings-v2/data-storage.tsx @@ -0,0 +1,98 @@ +import { Component, Show } from "solid-js" +import { TextInputV2 } from "@opencode-ai/ui/v2/text-input-v2" +import { useLanguage } from "@/context/language" +import { SettingsListV2 } from "./parts/list" +import { SettingsRowV2 } from "./parts/row" +import { createDataStorageController, type DataStorageController } from "./data-storage-controller" + +const DataStorageContent: Component<{ controller: DataStorageController }> = (props) => { + const language = useLanguage() + + const databaseError = () => { + const s = props.controller.status() + if (!s || s.databaseValid) return undefined + return s.databaseError ?? language.t("settings.general.row.dataStorage.database.error.invalidPath") + } + + const configError = () => { + const s = props.controller.status() + if (!s || s.configValid) return undefined + return s.configError ?? language.t("settings.general.row.dataStorage.configDir.error.invalidPath") + } + + return ( + + + {language.t("settings.general.row.dataStorage.database.description")} + + {databaseError()} + + + } + > +
+ props.controller.setDatabasePath(event.currentTarget.value)} + onBlur={() => props.controller.commitDatabasePath()} + placeholder={props.controller.defaults()?.databasePath ?? ""} + spellcheck={false} + autocorrect="off" + autocomplete="off" + autocapitalize="off" + aria-label={language.t("settings.general.row.dataStorage.database.title")} + /> +
+
+ + + {language.t("settings.general.row.dataStorage.configDir.description")} + + {configError()} + + + } + > +
+ props.controller.setConfigDir(event.currentTarget.value)} + onBlur={() => props.controller.commitConfigDir()} + placeholder={props.controller.defaults()?.configDir ?? ""} + spellcheck={false} + autocorrect="off" + autocomplete="off" + autocapitalize="off" + aria-label={language.t("settings.general.row.dataStorage.configDir.title")} + /> +
+
+
+ ) +} + +export const DataStorageSection: Component = () => { + const language = useLanguage() + const controller = createDataStorageController() + + return ( +
+

+ {language.t("settings.general.section.dataStorage")} +

+ +
+ ) +} diff --git a/packages/app/src/components/settings-v2/general.tsx b/packages/app/src/components/settings-v2/general.tsx index c56fe699a..8a72701fd 100644 --- a/packages/app/src/components/settings-v2/general.tsx +++ b/packages/app/src/components/settings-v2/general.tsx @@ -25,6 +25,7 @@ import { type ShellSettingsController, type SoundSettingsController, } from "./general-controllers" +import { DataStorageSection } from "./data-storage" import { DeveloperToolsSection } from "./developer-tools" import "./settings-v2.css" @@ -568,6 +569,8 @@ export const SettingsGeneralV2: Component<{ + + diff --git a/packages/app/src/context/settings.tsx b/packages/app/src/context/settings.tsx index 9cf9b6f63..25fb8be84 100644 --- a/packages/app/src/context/settings.tsx +++ b/packages/app/src/context/settings.tsx @@ -46,6 +46,10 @@ export interface Settings { sans: string terminal: string } + storage: { + databasePath: string + configDir: string + } developer: { enabled: boolean opencodePath: string @@ -207,6 +211,10 @@ const defaultSettings: Settings = { sans: "", terminal: "", }, + storage: { + databasePath: "", + configDir: "", + }, developer: { enabled: true, opencodePath: "", @@ -543,6 +551,16 @@ export const { use: useSettings, provider: SettingsProvider } = createSimpleCont setStore("sounds", "errors", value) }, }, + storage: { + databasePath: withFallback(() => store.storage?.databasePath, defaultSettings.storage.databasePath), + setDatabasePath(value: string) { + setStore("storage", "databasePath", value) + }, + configDir: withFallback(() => store.storage?.configDir, defaultSettings.storage.configDir), + setConfigDir(value: string) { + setStore("storage", "configDir", value) + }, + }, developer: { enabled: withFallback(() => store.developer?.enabled, defaultSettings.developer.enabled), setEnabled(value: boolean) { diff --git a/packages/app/src/i18n/en.ts b/packages/app/src/i18n/en.ts index bf76a48c8..57ab09cc6 100644 --- a/packages/app/src/i18n/en.ts +++ b/packages/app/src/i18n/en.ts @@ -1032,6 +1032,16 @@ export const dict = { "settings.general.row.devTools.rebuilding": "Rebuilding…", "settings.general.row.devTools.rebuilt": "Rebuilt!", + "settings.general.section.dataStorage": "Data & Storage", + "settings.general.row.dataStorage.database.title": "Session database", + "settings.general.row.dataStorage.database.description": + "Override the path to the opencode session database. Leave empty to use the default location.", + "settings.general.row.dataStorage.database.error.invalidPath": "Parent directory does not exist", + "settings.general.row.dataStorage.configDir.title": "Configuration directory", + "settings.general.row.dataStorage.configDir.description": + "Override the opencode configuration directory. Leave empty to use the default location.", + "settings.general.row.dataStorage.configDir.error.invalidPath": "Directory does not exist", + "settings.general.row.releaseNotes.title": "Release notes", "settings.general.row.releaseNotes.description": "Show What's New popups after updates",