From 815b33002031a80de07a42bccfbf575108fe235f Mon Sep 17 00:00:00 2001 From: JJ Lee Date: Fri, 14 Aug 2026 17:27:36 +0200 Subject: [PATCH 1/2] feat: Data & Storage bridge handlers + env var injection (#378) Extension-side support for the Data & Storage settings section: - chat_bridge.ts: add data-storage-query (returns resolved XDG defaults) and data-storage-update (validates paths, writes VS Code settings, restarts server) handlers - chat_panel.ts: add both message kinds to Lane 1 (up) and their reply kinds (data-storage-defaults, data-storage-status) to Lane 2 (down) - deck/shell.ts: same allowlist additions for the deck surface - package.json: add amicode.sessionDatabase and amicode.configDir configuration properties - extension.ts: inject OPENCODE_DB / OPENCODE_CONFIG_DIR into the server spawn env when the corresponding VS Code settings are non-empty --- packages/extension/package.json | 10 +++ packages/extension/src/chat_bridge.ts | 101 ++++++++++++++++++++++++++ packages/extension/src/chat_panel.ts | 4 +- packages/extension/src/deck/shell.ts | 4 +- packages/extension/src/extension.ts | 16 +++- 5 files changed, 128 insertions(+), 7 deletions(-) diff --git a/packages/extension/package.json b/packages/extension/package.json index 04a7d018..5600ef48 100644 --- a/packages/extension/package.json +++ b/packages/extension/package.json @@ -182,6 +182,16 @@ "default": 43117, "description": "Fixed port for the spawned opencode server. Set to 0 to pick a free ephemeral port on each start instead." }, + "amicode.sessionDatabase": { + "type": "string", + "default": "", + "description": "Override the session database path. The value is injected as OPENCODE_DB into the spawned server process. Empty = opencode uses its XDG default (~/.local/share/opencode/opencode.db)." + }, + "amicode.configDir": { + "type": "string", + "default": "", + "description": "Override the configuration directory. The value is injected as OPENCODE_CONFIG_DIR into the spawned server process. Empty = opencode uses its XDG default (~/.config/opencode)." + }, "amicode.juliaProject": { "type": "string", "default": "", diff --git a/packages/extension/src/chat_bridge.ts b/packages/extension/src/chat_bridge.ts index 55f466d2..13d9e4a3 100644 --- a/packages/extension/src/chat_bridge.ts +++ b/packages/extension/src/chat_bridge.ts @@ -590,5 +590,106 @@ export function handleAmicodeBridgeMessage(msg: unknown, io: BridgeIo): boolean return true; } + // Data & Storage settings (#378): query resolved defaults on mount, and + // update overrides (validate, write VS Code settings, restart server). + if (msg.kind === "data-storage-query") { + // Resolve the XDG defaults that opencode would use if no override is set. + const xdgData = process.env.XDG_DATA_HOME || path.join(os.homedir(), ".local", "share"); + const xdgConfig = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config"); + const defaultDbPath = path.join(xdgData, "opencode", "opencode.db"); + const defaultConfigDir = path.join(xdgConfig, "opencode"); + io.postToWebview({ + source: "amicode", + kind: "data-storage-defaults", + databasePath: defaultDbPath, + configDir: defaultConfigDir, + tab: msg.tab, + }); + return true; + } + + if (msg.kind === "data-storage-update") { + const databasePath = typeof (msg as { databasePath?: unknown }).databasePath === "string" + ? (msg as unknown as { databasePath: string }).databasePath.trim() + : ""; + const configDir = typeof (msg as { configDir?: unknown }).configDir === "string" + ? (msg as unknown as { configDir: string }).configDir.trim() + : ""; + + const reply: { + source: "amicode"; kind: "data-storage-status"; tab?: string; + databaseValid: boolean; databaseError?: string; + configValid: boolean; configError?: string; + serverRestarted: boolean; + } = { + source: "amicode", + kind: "data-storage-status", + tab: msg.tab, + databaseValid: true, + configValid: true, + serverRestarted: false, + }; + + // Validate database path: must be absolute, parent directory must exist. + if (databasePath) { + if (!path.isAbsolute(databasePath)) { + reply.databaseValid = false; + reply.databaseError = "Path must be absolute"; + } else { + const parentDir = path.dirname(databasePath); + try { + const stat = fs.statSync(parentDir); + if (!stat.isDirectory()) { + reply.databaseValid = false; + reply.databaseError = "Parent path exists but is not a directory"; + } + } catch { + reply.databaseValid = false; + reply.databaseError = "Parent directory does not exist"; + } + } + } + + // Validate config directory: must be absolute and must exist as a directory. + if (configDir) { + if (!path.isAbsolute(configDir)) { + reply.configValid = false; + reply.configError = "Path must be absolute"; + } else { + try { + const stat = fs.statSync(configDir); + if (!stat.isDirectory()) { + reply.configValid = false; + reply.configError = "Path exists but is not a directory"; + } + } catch { + reply.configValid = false; + reply.configError = "Directory does not exist"; + } + } + } + + // Write valid settings and restart server + if (reply.databaseValid) { + void vscode.workspace.getConfiguration("amicode").update( + "sessionDatabase", databasePath, vscode.ConfigurationTarget.Global, + ); + } + if (reply.configValid) { + void vscode.workspace.getConfiguration("amicode").update( + "configDir", configDir, vscode.ConfigurationTarget.Global, + ); + } + + // Restart the server so it picks up the new env vars + if (reply.databaseValid && reply.configValid) { + void vscode.commands.executeCommand("amicode.restartServer"); + reply.serverRestarted = true; + } + + io.postToWebview(reply); + return true; + } + return false; } diff --git a/packages/extension/src/chat_panel.ts b/packages/extension/src/chat_panel.ts index 72c37e67..ed6bf236 100644 --- a/packages/extension/src/chat_panel.ts +++ b/packages/extension/src/chat_panel.ts @@ -264,7 +264,7 @@ export class ChatPanel { replyClipboardImage(d.nonce); return; } - if (d && d.source === "amicode" && (d.kind === "command" || d.kind === "clipboard-request" || d.kind === "clipboard-write" || d.kind === "open-external" || d.kind === "open-file" || d.kind === "save-file" || d.kind === "set-default-model" || d.kind === "bug-filed" || d.kind === "bug-report-closed" || d.kind === "bug-report-poke" || d.kind === "dev-tools-update" || d.kind === "dev-tools-rebuild" || d.kind === "device:refresh")) { + if (d && d.source === "amicode" && (d.kind === "command" || d.kind === "clipboard-request" || d.kind === "clipboard-write" || d.kind === "open-external" || d.kind === "open-file" || d.kind === "save-file" || d.kind === "set-default-model" || d.kind === "bug-filed" || d.kind === "bug-report-closed" || d.kind === "bug-report-poke" || d.kind === "dev-tools-update" || d.kind === "dev-tools-rebuild" || d.kind === "data-storage-query" || d.kind === "data-storage-update" || d.kind === "device:refresh")) { vscode.postMessage(d); } return; @@ -273,7 +273,7 @@ export class ChatPanel { // (webview-internal origin, never the opencode origin). Forward only // our own envelopes, pinned to the opencode origin. #351 adds // run:*/device:* envelopes for the Work Column inspector tabs. - if (d && d.source === "amicode" && (d.kind === "theme" || d.kind === "clipboard" || d.kind === "open-compute-connect" || d.kind === "open-bug-report" || d.kind === "close-bug-report" || d.kind === "dev-tools-status" || d.kind === "dev-tools-rebuild-status" || (typeof d.kind === "string" && (d.kind.indexOf("run:") === 0 || d.kind.indexOf("device:") === 0)) || d.kind === "clipboard-image")) { + if (d && d.source === "amicode" && (d.kind === "theme" || d.kind === "clipboard" || d.kind === "open-compute-connect" || d.kind === "open-bug-report" || d.kind === "close-bug-report" || d.kind === "dev-tools-status" || d.kind === "dev-tools-rebuild-status" || d.kind === "data-storage-defaults" || d.kind === "data-storage-status" || (typeof d.kind === "string" && (d.kind.indexOf("run:") === 0 || d.kind.indexOf("device:") === 0)) || d.kind === "clipboard-image")) { var f = document.querySelector("iframe"); if (f && f.contentWindow) f.contentWindow.postMessage(d, ${origin}); } diff --git a/packages/extension/src/deck/shell.ts b/packages/extension/src/deck/shell.ts index b2bdb054..5b05e1e8 100644 --- a/packages/extension/src/deck/shell.ts +++ b/packages/extension/src/deck/shell.ts @@ -404,7 +404,7 @@ window.addEventListener("message", (e) => { if (d.kind === "clipboard" && typeof d.tab === "string") { frameByTab.get(d.tab)?.contentWindow?.postMessage(d, boot.origin); } - if ((d.kind === "dev-tools-status" || d.kind === "dev-tools-rebuild-status") && typeof d.tab === "string") { + if ((d.kind === "dev-tools-status" || d.kind === "dev-tools-rebuild-status" || d.kind === "data-storage-defaults" || d.kind === "data-storage-status") && typeof d.tab === "string") { frameByTab.get(d.tab)?.contentWindow?.postMessage(d, boot.origin); } // #351: inspector fan-out — broadcast to every live pane (no tab routing; @@ -478,7 +478,7 @@ window.addEventListener("message", (e) => { // Everything else rides up to the extension, tagged with the asking pane so // replies (clipboard text) route back correctly. - if (d.kind === "command" || d.kind === "clipboard-request" || d.kind === "clipboard-write" || d.kind === "open-external" || d.kind === "open-file" || d.kind === "save-file" || d.kind === "set-default-model" || d.kind === "dev-tools-update" || d.kind === "dev-tools-rebuild" || d.kind === "device:refresh") { + if (d.kind === "command" || d.kind === "clipboard-request" || d.kind === "clipboard-write" || d.kind === "open-external" || d.kind === "open-file" || d.kind === "save-file" || d.kind === "set-default-model" || d.kind === "dev-tools-update" || d.kind === "dev-tools-rebuild" || d.kind === "data-storage-query" || d.kind === "data-storage-update" || d.kind === "device:refresh") { vscode.postMessage({ ...d, tab: tabId }); } }); diff --git a/packages/extension/src/extension.ts b/packages/extension/src/extension.ts index 7a146373..b2f0fe77 100644 --- a/packages/extension/src/extension.ts +++ b/packages/extension/src/extension.ts @@ -279,12 +279,22 @@ export async function activate(ctx: vscode.ExtensionContext): Promise { * otherwise). */ const spawnEnv = ( o: Omit[0], "amicoPython" | "telemetry">, - ): Record => - (currentSpawnEnv = buildServerSpawnEnv({ + ): Record => { + const env = buildServerSpawnEnv({ ...o, amicoPython, telemetry: resolveTelemetryContext(ctx, { sessionId: telemetrySessionId }), - })); + }); + // Data & Storage overrides (#378): inject OPENCODE_DB / OPENCODE_CONFIG_DIR + // from the user's VS Code settings when non-empty. On cold start + on + // restartServer, the new env reaches the fresh server process. + const cfg = vscode.workspace.getConfiguration("amicode"); + const sessionDb = cfg.get("sessionDatabase", ""); + const configDirOverride = cfg.get("configDir", ""); + if (sessionDb) env.OPENCODE_DB = sessionDb; + if (configDirOverride) env.OPENCODE_CONFIG_DIR = configDirOverride; + return (currentSpawnEnv = env); + }; /** Is the telemetry gate open RIGHT NOW? Threaded into buildOpencodeConfigContent * at each spawn site so the config's experimental.openTelemetry (span generation) From 11e95a37105aed3d08c7509dc1fb63a8f5497e4c Mon Sep 17 00:00:00 2001 From: JJ Lee Date: Fri, 14 Aug 2026 17:32:43 +0200 Subject: [PATCH 2/2] fix: use ~/ prefix in Data & Storage defaults for consistency with Developer Tools The placeholder paths now display as ~/... instead of /Users/x/..., matching the Developer Tools convention. The update handler expands ~ before validating and writing the VS Code setting (same pattern as dev-tools-rebuild). --- packages/extension/src/chat_bridge.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/extension/src/chat_bridge.ts b/packages/extension/src/chat_bridge.ts index 13d9e4a3..d30f6edd 100644 --- a/packages/extension/src/chat_bridge.ts +++ b/packages/extension/src/chat_bridge.ts @@ -594,15 +594,18 @@ export function handleAmicodeBridgeMessage(msg: unknown, io: BridgeIo): boolean // update overrides (validate, write VS Code settings, restart server). if (msg.kind === "data-storage-query") { // Resolve the XDG defaults that opencode would use if no override is set. + // Display with ~/ prefix for readability (consistent with Developer Tools). const xdgData = process.env.XDG_DATA_HOME || path.join(os.homedir(), ".local", "share"); const xdgConfig = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config"); const defaultDbPath = path.join(xdgData, "opencode", "opencode.db"); const defaultConfigDir = path.join(xdgConfig, "opencode"); + const home = os.homedir(); + const shorten = (p: string) => p.startsWith(home) ? "~" + p.slice(home.length) : p; io.postToWebview({ source: "amicode", kind: "data-storage-defaults", - databasePath: defaultDbPath, - configDir: defaultConfigDir, + databasePath: shorten(defaultDbPath), + configDir: shorten(defaultConfigDir), tab: msg.tab, }); return true; @@ -610,10 +613,10 @@ export function handleAmicodeBridgeMessage(msg: unknown, io: BridgeIo): boolean if (msg.kind === "data-storage-update") { const databasePath = typeof (msg as { databasePath?: unknown }).databasePath === "string" - ? (msg as unknown as { databasePath: string }).databasePath.trim() + ? (msg as unknown as { databasePath: string }).databasePath.trim().replace(/^~/, os.homedir()) : ""; const configDir = typeof (msg as { configDir?: unknown }).configDir === "string" - ? (msg as unknown as { configDir: string }).configDir.trim() + ? (msg as unknown as { configDir: string }).configDir.trim().replace(/^~/, os.homedir()) : ""; const reply: {