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
10 changes: 10 additions & 0 deletions packages/extension/package.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -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": "",
Expand Down
104 changes: 104 additions & 0 deletions packages/extension/src/chat_bridge.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -590,5 +590,109 @@ 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.
// 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: shorten(defaultDbPath),
configDir: shorten(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().replace(/^~/, os.homedir())
: "";
const configDir = typeof (msg as { configDir?: unknown }).configDir === "string"
? (msg as unknown as { configDir: string }).configDir.trim().replace(/^~/, os.homedir())
: "";

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;
}
4 changes: 2 additions & 2 deletions packages/extension/src/chat_panel.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -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;
Expand All@@ -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});
}
Expand Down
4 changes: 2 additions & 2 deletions packages/extension/src/deck/shell.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -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;
Expand DownExpand Up@@ -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 });
}
});
Expand Down
16 changes: 13 additions & 3 deletions packages/extension/src/extension.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -279,12 +279,22 @@ export async function activate(ctx: vscode.ExtensionContext): Promise<void> {
* otherwise). */
const spawnEnv = (
o: Omit<Parameters<typeof buildServerSpawnEnv>[0], "amicoPython" | "telemetry">,
): Record<string, string> =>
(currentSpawnEnv = buildServerSpawnEnv({
): Record<string, string> => {
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<string>("sessionDatabase", "");
const configDirOverride = cfg.get<string>("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)
Expand Down
Loading