Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 100 additions & 0 deletions packages/app/src/components/settings-v2/data-storage-controller.ts
Original file line number Diff line number Diff line change
@@ -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<DataStorageStatus | undefined>(undefined)
const [defaults, setDefaults] = createSignal<DataStorageDefaults | undefined>(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<typeof createDataStorageController>
98 changes: 98 additions & 0 deletions packages/app/src/components/settings-v2/data-storage.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<SettingsListV2>
<SettingsRowV2
title={language.t("settings.general.row.dataStorage.database.title")}
description={
<>
{language.t("settings.general.row.dataStorage.database.description")}
<Show when={databaseError()}>
<span class="settings-v2-field-error">{databaseError()}</span>
</Show>
</>
}
>
<div class="w-full sm:w-[280px]">
<TextInputV2
data-action="settings-data-storage-database"
type="text"
appearance="base"
value={props.controller.databasePath()}
onInput={(event) => 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")}
/>
</div>
</SettingsRowV2>

<SettingsRowV2
title={language.t("settings.general.row.dataStorage.configDir.title")}
description={
<>
{language.t("settings.general.row.dataStorage.configDir.description")}
<Show when={configError()}>
<span class="settings-v2-field-error">{configError()}</span>
</Show>
</>
}
>
<div class="w-full sm:w-[280px]">
<TextInputV2
data-action="settings-data-storage-config-dir"
type="text"
appearance="base"
value={props.controller.configDir()}
onInput={(event) => 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")}
/>
</div>
</SettingsRowV2>
</SettingsListV2>
)
}

export const DataStorageSection: Component = () => {
const language = useLanguage()
const controller = createDataStorageController()

return (
<div class="settings-v2-section">
<h3 class="settings-v2-section-title">
{language.t("settings.general.section.dataStorage")}
</h3>
<DataStorageContent controller={controller} />
</div>
)
}
3 changes: 3 additions & 0 deletions packages/app/src/components/settings-v2/general.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -568,6 +569,8 @@ export const SettingsGeneralV2: Component<{

<DisplaySection />

<DataStorageSection />

<AdvancedSection />

<DeveloperToolsSection />
Expand Down
18 changes: 18 additions & 0 deletions packages/app/src/context/settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ export interface Settings {
sans: string
terminal: string
}
storage: {
databasePath: string
configDir: string
}
developer: {
enabled: boolean
opencodePath: string
Expand Down Expand Up @@ -207,6 +211,10 @@ const defaultSettings: Settings = {
sans: "",
terminal: "",
},
storage: {
databasePath: "",
configDir: "",
},
developer: {
enabled: true,
opencodePath: "",
Expand Down Expand Up @@ -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) {
Expand Down
10 changes: 10 additions & 0 deletions packages/app/src/i18n/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",

Expand Down
Loading