From 260bfe666b49edb9d5cdea2b2b8742859961a7db Mon Sep 17 00:00:00 2001
From: Bohdan Triapitsyn
- {t('settings.behavior.page.warning.description', { path: AGENTS_MD_PATH })} + {t('settings.behavior.page.warning.description', { path: agentsMdPath })}
)} diff --git a/packages/ui/src/lib/opencode/client.ts b/packages/ui/src/lib/opencode/client.ts index 608077a4..1a5f27e6 100644 --- a/packages/ui/src/lib/opencode/client.ts +++ b/packages/ui/src/lib/opencode/client.ts @@ -1301,12 +1301,10 @@ class OpencodeService { * endpoint introduced in OpenCode SDK v1.17.12. Wraps * `session.permission.get`. * - * Returns a tagged `FetchPermissionResult` so the caller can distinguish - * a confirmed-resolved permission (HTTP 404) from a fetch failure - * (network error, malformed response, or pre-v1.17.12 server without - * the V2 endpoint). The auto-accept flow uses this distinction to drop - * resolved permissions from the resync output, preventing stale - * `permission.list` entries from sticking around in the UI. + * Returns the state of the V2 permission authority. Its HTTP 404 result + * does not prove that a request from `permission.list` has settled: + * list-derived reconciliation must use that list's own reply path. + * Fetch failures remain distinct from the V2 resolved result. */ async fetchPermission( sessionID: string, diff --git a/packages/vscode/src/DOCUMENTATION.md b/packages/vscode/src/DOCUMENTATION.md index 7aefa3ab..212c58fe 100644 --- a/packages/vscode/src/DOCUMENTATION.md +++ b/packages/vscode/src/DOCUMENTATION.md @@ -172,3 +172,14 @@ Reachable filesystem routes: `api:fs:read` (attachments, config), `api:fs:search Maintenance: reviews, changelog entries, and parity claims consult this map; whoever mounts or unmounts a surface updates it in the same change. + +## Global OpenCode paths + +`opencodeConfigPaths.ts` owns the global config directory for config CRUD, +skill discovery/install, global AGENTS.md, and quota config-file lookup. It +resolves `$XDG_CONFIG_HOME/opencode` at extension startup, falling back to +`~/.config/opencode` when unset or blank. Project paths, the explicit +`OPENCODE_CONFIG` file layer, and the auth data directory stay separate. +No files are migrated. The behavior GET bridge response includes the effective +`path` for both existing and missing AGENTS.md files; shared Settings uses it +in the warning. diff --git a/packages/vscode/src/bridge-config-runtime.ts b/packages/vscode/src/bridge-config-runtime.ts index 463353b3..44f1cb5c 100644 --- a/packages/vscode/src/bridge-config-runtime.ts +++ b/packages/vscode/src/bridge-config-runtime.ts @@ -1,6 +1,6 @@ +import { OPENCODE_CONFIG_DIR } from './opencodeConfigPaths'; import * as vscode from 'vscode'; import * as fs from 'fs'; -import * as os from 'os'; import * as path from 'path'; import { createAgent, @@ -78,7 +78,7 @@ type ConfigRuntimeDeps = { clientReloadDelayMs: number; }; -const AGENTS_MD_PATH = path.join(os.homedir(), '.config', 'opencode', 'AGENTS.md'); +const AGENTS_MD_PATH = path.join(OPENCODE_CONFIG_DIR, 'AGENTS.md'); const MAX_BEHAVIOR_PROMPT_SIZE = 1024 * 1024; const resolveWorkingDirectory = (ctx: BridgeContext | undefined, directory?: string): string | undefined => ( @@ -186,10 +186,10 @@ export async function handleConfigBridgeMessage( case 'api:behavior/agents-md:get': { try { const content = await fs.promises.readFile(AGENTS_MD_PATH, 'utf8'); - return { id, type, success: true, data: { content, exists: true } }; + return { id, type, success: true, data: { content, exists: true, path: AGENTS_MD_PATH } }; } catch (error) { if ((error as NodeJS.ErrnoException)?.code === 'ENOENT') { - return { id, type, success: true, data: { content: '', exists: false } }; + return { id, type, success: true, data: { content: '', exists: false, path: AGENTS_MD_PATH } }; } throw error; } diff --git a/packages/vscode/src/bridge-settings-runtime.ts b/packages/vscode/src/bridge-settings-runtime.ts index ee48b7e8..d2b01669 100644 --- a/packages/vscode/src/bridge-settings-runtime.ts +++ b/packages/vscode/src/bridge-settings-runtime.ts @@ -1,3 +1,4 @@ +import { OPENCODE_CONFIG_DIR } from './opencodeConfigPaths'; import * as fs from 'fs'; import * as os from 'os'; import * as path from 'path'; @@ -76,7 +77,7 @@ const inferSkillScopeAndSourceFromLocation = (location: string, workingDirectory const home = os.homedir(); const userRoots = [ - path.join(home, '.config', 'opencode'), + OPENCODE_CONFIG_DIR, path.join(home, '.opencode'), path.join(home, '.claude', 'skills'), path.join(home, '.agents', 'skills'), diff --git a/packages/vscode/src/opencodeConfig.ts b/packages/vscode/src/opencodeConfig.ts index 629aecaa..dbf8e28f 100644 --- a/packages/vscode/src/opencodeConfig.ts +++ b/packages/vscode/src/opencodeConfig.ts @@ -1,10 +1,10 @@ +import { OPENCODE_CONFIG_DIR } from './opencodeConfigPaths'; import fs from 'node:fs'; import path from 'node:path'; import os from 'node:os'; import yaml from 'yaml'; import { parse as parseJsonc, printParseErrorCode, type ParseError } from 'jsonc-parser'; -const OPENCODE_CONFIG_DIR = path.join(os.homedir(), '.config', 'opencode'); const AGENT_DIR = path.join(OPENCODE_CONFIG_DIR, 'agents'); const COMMAND_DIR = path.join(OPENCODE_CONFIG_DIR, 'commands'); const GLOBAL_SNIPPET_DIR = path.join(OPENCODE_CONFIG_DIR, 'snippet'); diff --git a/packages/vscode/src/opencodeConfigPaths.test.ts b/packages/vscode/src/opencodeConfigPaths.test.ts new file mode 100644 index 00000000..f422c154 --- /dev/null +++ b/packages/vscode/src/opencodeConfigPaths.test.ts @@ -0,0 +1,51 @@ +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import { spawnSync } from 'node:child_process'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; + +const moduleUrl = new URL('./opencodeConfigPaths.ts', import.meta.url).href; +const configUrl = new URL('./opencodeConfig.ts', import.meta.url).href; + +test('unset and blank XDG keep the existing global directory', () => { + for (const value of [undefined, '', ' ']) { + const env = { ...process.env }; + if (value === undefined) delete env.XDG_CONFIG_HOME; + else env.XDG_CONFIG_HOME = value; + const result = spawnSync(process.execPath, ['--eval', ` + const { OPENCODE_CONFIG_DIR } = await import(${JSON.stringify(moduleUrl)}); + process.stdout.write(OPENCODE_CONFIG_DIR); + `], { env, encoding: 'utf8' }); + assert.equal(result.status, 0, result.stderr); + assert.equal(result.stdout, path.join(os.homedir(), '.config', 'opencode')); + } +}); + +test('global CRUD uses XDG while project writes stay in the project', () => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'openchamber-vscode-xdg-')); + const configHome = path.join(root, 'config'); + const project = path.join(root, 'project'); + try { + const result = spawnSync(process.execPath, ['--eval', ` + const { createAgent, createCommand, createSkill } = await import(${JSON.stringify(configUrl)}); + const project = ${JSON.stringify(project)}; + for (const scope of ['user', 'project']) { + createAgent(scope + '-agent', { description: 'Test', prompt: 'Test prompt' }, project, scope); + createCommand(scope + '-command', { description: 'Test', template: 'Test command' }, project, scope); + createSkill(scope + '-skill', { description: 'Test', instructions: 'Test skill' }, project, scope); + } + `], { + env: { ...process.env, XDG_CONFIG_HOME: configHome, OPENCODE_CONFIG: '', OPENCODE_CONFIG_DIR: '' }, + encoding: 'utf8', + }); + assert.equal(result.status, 0, result.stderr); + for (const [scope, directory] of [['user', path.join(configHome, 'opencode')], ['project', path.join(project, '.opencode')]]) { + assert.match(fs.readFileSync(path.join(directory, 'agents', `${scope}-agent.md`), 'utf8'), /Test prompt/); + assert.match(fs.readFileSync(path.join(directory, 'commands', `${scope}-command.md`), 'utf8'), /Test command/); + assert.match(fs.readFileSync(path.join(directory, 'skills', `${scope}-skill`, 'SKILL.md'), 'utf8'), /Test skill/); + } + } finally { + fs.rmSync(root, { recursive: true, force: true }); + } +}); diff --git a/packages/vscode/src/opencodeConfigPaths.ts b/packages/vscode/src/opencodeConfigPaths.ts new file mode 100644 index 00000000..bd8c19b7 --- /dev/null +++ b/packages/vscode/src/opencodeConfigPaths.ts @@ -0,0 +1,8 @@ +import os from 'node:os'; +import path from 'node:path'; + +// Resolve once at extension startup, matching the web backend. +export const OPENCODE_CONFIG_DIR = path.join( + process.env.XDG_CONFIG_HOME?.trim() || path.join(os.homedir(), '.config'), + 'opencode', +); diff --git a/packages/vscode/src/quotaProviders.ts b/packages/vscode/src/quotaProviders.ts index c275a07d..74be878f 100644 --- a/packages/vscode/src/quotaProviders.ts +++ b/packages/vscode/src/quotaProviders.ts @@ -1,3 +1,4 @@ +import { OPENCODE_CONFIG_DIR } from './opencodeConfigPaths'; import fs from 'node:fs'; import path from 'node:path'; import os from 'node:os'; @@ -191,7 +192,6 @@ export type ProviderResult = { planLabel?: string | null; }; -const OPENCODE_CONFIG_DIR = path.join(os.homedir(), '.config', 'opencode'); const OPENCODE_DATA_DIR = path.join(os.homedir(), '.local', 'share', 'opencode'); const AUTH_FILE = path.join(OPENCODE_DATA_DIR, 'auth.json'); diff --git a/packages/vscode/src/skillsCatalog.ts b/packages/vscode/src/skillsCatalog.ts index f9a171f6..7f1ac586 100644 --- a/packages/vscode/src/skillsCatalog.ts +++ b/packages/vscode/src/skillsCatalog.ts @@ -1,3 +1,4 @@ +import { OPENCODE_CONFIG_DIR } from './opencodeConfigPaths'; import fs from 'fs'; import os from 'os'; import path from 'path'; @@ -418,8 +419,8 @@ async function copyDirectoryNoSymlinks(srcDir: string, dstDir: string) { } function getUserSkillBaseDir() { - const pluralPath = path.join(os.homedir(), '.config', 'opencode', 'skills'); - const legacyPath = path.join(os.homedir(), '.config', 'opencode', 'skill'); + const pluralPath = path.join(OPENCODE_CONFIG_DIR, 'skills'); + const legacyPath = path.join(OPENCODE_CONFIG_DIR, 'skill'); if (fs.existsSync(legacyPath) && !fs.existsSync(pluralPath)) return legacyPath; return pluralPath; } diff --git a/packages/vscode/src/webviewHtml.ts b/packages/vscode/src/webviewHtml.ts index 21894ea9..8ea45e22 100644 --- a/packages/vscode/src/webviewHtml.ts +++ b/packages/vscode/src/webviewHtml.ts @@ -228,11 +228,11 @@ export function getWebviewHtml(options: WebviewHtmlOptions): string { return { startingApi: 'OpenCode API başlatılıyor…', initializing: 'Başlatılıyor…', - connecting: 'Bağlanılıyor…', + connecting: 'Bağlanıyor…', connected: 'Bağlandı!', connectionError: 'Bağlantı hatası', - reconnecting: 'Yeniden bağlanılıyor…', - cliNotFound: 'OpenCode CLI bulunamadı. Lütfen önce yükleyin.', + reconnecting: 'Yeniden bağlanıyor…', + cliNotFound: 'OpenCode CLI bulunamadı. Lütfen önce kurun.', }; } return { @@ -310,8 +310,8 @@ export function getWebviewHtml(options: WebviewHtmlOptions): string { } if (detected.indexOf('tr') === 0) { return { - startingDevServer: (host) => 'Webview geliştirme sunucusu başlatılıyor (' + host + ')...', - waitingDevServer: (host, attempt) => 'Webview geliştirme sunucusu bekleniyor (' + host + ')... deneme ' + attempt, + startingDevServer: (host) => 'Webview dev sunucusu başlatılıyor (' + host + ')...', + waitingDevServer: (host, attempt) => 'Webview dev sunucusu bekleniyor (' + host + ')... deneme ' + attempt, }; } } diff --git a/packages/web/server/lib/opencode/DOCUMENTATION.md b/packages/web/server/lib/opencode/DOCUMENTATION.md index 937113d0..814d1a76 100644 --- a/packages/web/server/lib/opencode/DOCUMENTATION.md +++ b/packages/web/server/lib/opencode/DOCUMENTATION.md @@ -421,7 +421,7 @@ an authoritative loopback callback URL even when OpenChamber binds port `0`. ## Storage and configuration - Provider auth: `~/.local/share/opencode/auth.json`. -- User config: `~/.config/opencode/opencode.json`. +- User config: `$XDG_CONFIG_HOME/opencode/opencode.json`, falling back to `~/.config/opencode/opencode.json` when unset or blank. - Project config: `