codexmate 0.0.45 → 0.0.48

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,7 @@
1
1
  const zh = Object.freeze({
2
2
  // Global
3
3
  'lang.zh': '中文',
4
+ 'lang.zh-tw': '繁體中文',
4
5
  'lang.en': 'English',
5
6
  'lang.vi': '越南语',
6
7
  'lang.label': '语言',
@@ -65,6 +66,8 @@ const zh = Object.freeze({
65
66
  'common.none': '暂无',
66
67
  'common.configured': '已配置',
67
68
  'common.notConfigured': '未配置',
69
+ 'common.enabled': '已启用',
70
+ 'common.disabled': '已禁用',
68
71
  'cli.missing.title': '{name} CLI 未安装',
69
72
  'cli.missing.subtitle': '请先安装 {name} CLI 后再继续使用此页面。',
70
73
  'cli.missing.openDocs': '打开安装指南',
@@ -125,6 +128,7 @@ const zh = Object.freeze({
125
128
  'tab.config.codex': 'Codex',
126
129
  'tab.config.claude': 'Claude',
127
130
  'tab.config.openclaw': 'OpenClaw',
131
+ 'tab.config.opencode': 'OpenCode',
128
132
  'tab.sessions': '会话',
129
133
  'tab.usage': '用量',
130
134
  'tab.orchestration': '任务',
@@ -166,6 +170,8 @@ const zh = Object.freeze({
166
170
  'side.config.claude.meta': 'Claude Settings',
167
171
  'side.config.openclaw': 'OpenClaw',
168
172
  'side.config.openclaw.meta': 'JSON5 / AGENTS',
173
+ 'side.config.opencode': 'OpenCode',
174
+ 'side.config.opencode.meta': '配置 / Provider',
169
175
  'side.sessions.browser': '会话浏览',
170
176
  'side.sessions.browser.meta': '浏览 / 导出 / 清理',
171
177
  'side.prompts.agents': 'AGENTS.md',
@@ -480,6 +486,7 @@ const zh = Object.freeze({
480
486
  'modal.configTemplate.mode.twoStep': '两步确认:先预览差异,再应用写入。',
481
487
  'modal.configTemplate.mode.oneStep': '一步应用:点击“应用”直接写入。',
482
488
  'diff.title.configTemplate': '差异预览(config.toml)',
489
+ 'diff.title.claudeSettings': '差异预览(settings.json)',
483
490
  'diff.generating': '生成中...',
484
491
  'diff.failed': '生成失败',
485
492
  'diff.noChanges': '未检测到改动',
@@ -804,6 +811,39 @@ const zh = Object.freeze({
804
811
  'toolConfig.claude.lockedTitle': 'Claude 提供商当前为只读',
805
812
  'toolConfig.claude.lockedDesc': '这里不会写入 Claude 配置。要添加、应用、编辑或删除提供商,请先打开本 tab 的写入开关。',
806
813
  'toolConfig.claude.confirmMessage': '打开后,Claude tab 内的应用操作会写入 ~/.claude/settings.json 等 Claude 配置。',
814
+ 'toolConfig.opencode.title': 'OpenCode 配置写入',
815
+ 'toolConfig.opencode.desc': '默认只读;开启后可写入 OpenCode 配置文件。',
816
+ 'toolConfig.opencode.lockedTitle': '只读模式',
817
+ 'toolConfig.opencode.lockedDesc': '开启写入权限后可保存、导入或应用配置。',
818
+ 'toolConfig.opencode.confirmMessage': '打开后,OpenCode tab 内的操作会写入 XDG OpenCode 配置文件(如 ~/.config/opencode/opencode.jsonc)或 OPENCODE_CONFIG 指定文件。',
819
+ 'opencode.providerModel.title': 'OpenCode provider / model',
820
+ 'opencode.writeAria': 'OpenCode 写入权限',
821
+ 'opencode.applySelection': '应用到 OpenCode',
822
+ 'opencode.targetFile': 'OpenCode 生效配置:{path} · {status}',
823
+ 'opencode.providerStoreFile': 'Provider 草稿库:{path}(应用时写入 OpenCode)',
824
+ 'opencode.field.agent': 'Agent',
825
+ 'opencode.field.apiKeyKeep': 'API Key(留空则保留已有 key)',
826
+ 'opencode.field.maxTokens': 'maxTokens(可选)',
827
+ 'opencode.field.reasoningEffort': 'reasoningEffort(可选)',
828
+ 'opencode.option.keepUnchanged': '不改动',
829
+ 'opencode.option.reasoningLow': 'low',
830
+ 'opencode.option.reasoningMedium': 'medium',
831
+ 'opencode.option.reasoningHigh': 'high',
832
+ 'opencode.applyToCoreAgents': '同步应用到所有核心 agent',
833
+ 'opencode.enableAutoCompaction': '启用 compaction.auto',
834
+ 'opencode.disableProvider': '禁用该 provider',
835
+ 'opencode.configFile.title': 'OpenCode 配置文件',
836
+ 'opencode.importParse': '导入/解析文件',
837
+ 'opencode.saveConfig': '保存配置',
838
+ 'opencode.parsedFile': '已解析:{file}',
839
+ 'opencode.textarea.placeholder': '编辑 OpenCode 配置',
840
+ 'opencode.configFile.hint': '生效配置预览;草稿保存在独立 Provider 库。',
841
+ 'opencode.summary.title': '当前解析摘要',
842
+ 'opencode.summary.noApiKey': '未配置 API Key',
843
+ 'opencode.summary.noModel': '未设置 model',
844
+ 'opencode.summary.agentType': 'agent',
845
+ 'opencode.summary.sourceCodexMate': 'CodexMate 库',
846
+ 'opencode.summary.sourceOpenCode': 'OpenCode 生效',
807
847
  'config.providerTemplate.title': '预设供应商',
808
848
  'config.models': '模型',
809
849
  'config.modelLoading': '加载中...',
@@ -1025,6 +1065,9 @@ const zh = Object.freeze({
1025
1065
  'status.claudeConfig': 'Claude 配置',
1026
1066
  'status.claudeModel': 'Claude 模型',
1027
1067
  'status.openclawConfig': 'OpenClaw 配置',
1068
+ 'status.opencodeProvider': 'OpenCode Provider',
1069
+ 'status.opencodeModel': 'OpenCode 模型',
1070
+ 'status.opencodeConfig': 'OpenCode 配置',
1028
1071
  'status.workspaceFile': '工作区文件',
1029
1072
  'status.configMode': '配置模式',
1030
1073
  'status.currentProvider': '当前 Provider',
@@ -1171,6 +1214,10 @@ const zh = Object.freeze({
1171
1214
  'claude.model': '模型',
1172
1215
  'claude.model.placeholder': '例如: claude-3-7-sonnet',
1173
1216
  'claude.model.hint': '模型修改后会自动保存并应用到当前配置。',
1217
+ 'claude.model.haiku': 'Haiku 模型',
1218
+ 'claude.model.sonnet': 'Sonnet 模型',
1219
+ 'claude.model.opus': 'Opus 模型',
1220
+ 'claude.model.sub.placeholder': '留空则跟随主模型',
1174
1221
  'claude.targetApi.label': '目标 API',
1175
1222
  'claude.targetApi.responses': 'Anthropic',
1176
1223
  'claude.targetApi.chatCompletions': 'OpenAI Chat Completions (/v1/chat/completions)',
@@ -1,10 +1,12 @@
1
1
  import { zh } from './i18n/locales/zh.mjs';
2
+ import { zhTw } from './i18n/locales/zh-tw.mjs';
2
3
  import { en } from './i18n/locales/en.mjs';
3
4
  import { ja } from './i18n/locales/ja.mjs';
4
5
  import { vi } from './i18n/locales/vi.mjs';
5
6
 
6
7
  const DICT = Object.freeze({
7
8
  zh,
9
+ 'zh-tw': zhTw,
8
10
  en,
9
11
  ja,
10
12
  vi
@@ -4,6 +4,7 @@ const I18N_STORAGE_KEY = 'codexmateLang';
4
4
 
5
5
  const LANGUAGE_META = Object.freeze([
6
6
  Object.freeze({ code: 'zh', nativeName: '中文', englishName: 'Chinese', htmlLang: 'zh-CN', dir: 'ltr' }),
7
+ Object.freeze({ code: 'zh-tw', nativeName: '繁體中文', englishName: 'Chinese-TW', htmlLang: 'zh-TW', dir: 'ltr' }),
7
8
  Object.freeze({ code: 'en', nativeName: 'English', englishName: 'English', htmlLang: 'en', dir: 'ltr' }),
8
9
  Object.freeze({ code: 'ja', nativeName: '日本語', englishName: 'Japanese', htmlLang: 'ja', dir: 'ltr' }),
9
10
  Object.freeze({ code: 'vi', nativeName: 'Tiếng Việt', englishName: 'Vietnamese', htmlLang: 'vi', dir: 'ltr' })
@@ -102,9 +103,9 @@ export function createI18nMethods() {
102
103
  t(key, params = null) {
103
104
  const lang = normalizeLang(this.lang);
104
105
  const table = DICT[lang] || DICT.zh;
105
- const fallbackEn = DICT.en;
106
106
  const fallbackZh = DICT.zh;
107
- const raw = (table && table[key]) || (fallbackEn && fallbackEn[key]) || (fallbackZh && fallbackZh[key]) || key;
107
+ const fallbackEn = DICT.en;
108
+ const raw = (table && table[key]) || (fallbackZh && fallbackZh[key]) || (fallbackEn && fallbackEn[key]) || key;
108
109
  return interpolate(raw, params);
109
110
  }
110
111
  };
@@ -39,7 +39,7 @@
39
39
  :data-config-mode="configMode"
40
40
  :tabindex="mainTab === 'config' ? 0 : -1"
41
41
  :aria-selected="mainTab === 'config'"
42
- :aria-controls="configMode === 'claude' ? 'panel-config-claude' : (configMode === 'openclaw' ? 'panel-config-openclaw' : 'panel-config-provider')"
42
+ :aria-controls="configMode === 'claude' ? 'panel-config-claude' : (configMode === 'openclaw' ? 'panel-config-openclaw' : (configMode === 'opencode' ? 'panel-config-opencode' : 'panel-config-provider'))"
43
43
  :class="{ active: isMainTabNavActive('config') }"
44
44
  @pointerdown="onMainTabPointerDown('config', $event)"
45
45
  @click="onMainTabClick('config', $event)">{{ t('tab.config') }}</button>
@@ -129,7 +129,7 @@
129
129
  <div class="brand-head">
130
130
  <img class="brand-logo" src="/res/logo-pack.webp" alt="Codex Mate logo">
131
131
  <div class="brand-copy">
132
- <div class="brand-kicker">Codex Mate<span v-if="appVersion" class="brand-version"> v{{ appVersion }}</span></div>
132
+ <div class="brand-kicker">Codex Mate</div>
133
133
  </div>
134
134
  </div>
135
135
  <button
@@ -223,6 +223,20 @@
223
223
  <span v-if="currentOpenclawConfig">{{ t('common.current', { value: currentOpenclawConfig }) }}</span>
224
224
  </div>
225
225
  </button>
226
+ <button
227
+ id="side-tab-config-opencode"
228
+ data-main-tab="config"
229
+ data-config-mode="opencode"
230
+ :aria-current="mainTab === 'config' && configMode === 'opencode' ? 'page' : null"
231
+ :class="['side-item', { active: isConfigModeNavActive('opencode') }]"
232
+ @pointerdown="onConfigTabPointerDown('opencode', $event)"
233
+ @click="onConfigTabClick('opencode', $event)">
234
+ <div class="side-item-title">{{ t('side.config.opencode') }}</div>
235
+ <div class="side-item-meta">
236
+ <span>{{ t('side.config.opencode.meta') }}</span>
237
+ <span v-if="opencodeModel">{{ t('common.current', { value: opencodeModel }) }}</span>
238
+ </div>
239
+ </button>
226
240
  </div>
227
241
 
228
242
  <div class="side-section" role="navigation" :aria-label="t('side.prompts')">
@@ -419,6 +433,20 @@
419
433
  <span class="value">{{ openclawWorkspaceFileName || t('common.notSelected') }}</span>
420
434
  </div>
421
435
  </template>
436
+ <template v-else-if="configMode === 'opencode'">
437
+ <div class="status-chip">
438
+ <span class="label">{{ t('status.opencodeProvider') }}</span>
439
+ <span class="value">{{ opencodeProvider || t('common.notSelected') }}</span>
440
+ </div>
441
+ <div class="status-chip">
442
+ <span class="label">{{ t('status.opencodeModel') }}</span>
443
+ <span class="value">{{ opencodeModel || t('common.notSelected') }}</span>
444
+ </div>
445
+ <div class="status-chip">
446
+ <span class="label">{{ t('status.opencodeConfig') }}</span>
447
+ <span class="value">{{ opencodeConfigPath || t('common.notSelected') }}</span>
448
+ </div>
449
+ </template>
422
450
  <template v-else>
423
451
  <div class="status-chip">
424
452
  <span class="label">{{ t('status.configMode') }}</span>
@@ -12,7 +12,7 @@
12
12
  <div v-if="configTemplateDiffVisible" class="agents-diff-container">
13
13
  <div class="agents-diff-header">
14
14
  <div class="agents-diff-title">
15
- {{ t('diff.title.configTemplate') }}
15
+ {{ configTemplateContext === 'claude' ? t('diff.title.claudeSettings') : t('diff.title.configTemplate') }}
16
16
  <span v-if="configTemplateDiffLoading" class="agents-diff-subtitle">{{ t('diff.generating') }}</span>
17
17
  <span v-else-if="configTemplateDiffError" class="agents-diff-subtitle">{{ t('diff.failed') }}</span>
18
18
  <span v-else-if="!configTemplateDiffHasChanges" class="agents-diff-subtitle">{{ t('diff.noChanges') }}</span>
@@ -170,12 +170,12 @@
170
170
  </div>
171
171
  </div>
172
172
 
173
- </div>
174
- <div v-if="!isToolConfigWriteAllowed('claude')" class="tool-config-write-overlay">
175
- <div class="tool-config-write-overlay-card">
176
- <div class="tool-config-write-overlay-title">{{ t('toolConfig.claude.lockedTitle') }}</div>
177
- <p>{{ t('toolConfig.claude.lockedDesc') }}</p>
178
- <button type="button" class="btn-tool" @click="setToolConfigPermission('claude', true)" :disabled="toolConfigPermissionSaving.claude">{{ t('toolConfig.enableWrite') }}</button>
173
+ <div v-if="!isToolConfigWriteAllowed('claude')" class="tool-config-write-overlay">
174
+ <div class="tool-config-write-overlay-card">
175
+ <div class="tool-config-write-overlay-title">{{ t('toolConfig.claude.lockedTitle') }}</div>
176
+ <p>{{ t('toolConfig.claude.lockedDesc') }}</p>
177
+ <button type="button" class="btn-tool" @click="setToolConfigPermission('claude', true)" :disabled="toolConfigPermissionSaving.claude">{{ t('toolConfig.enableWrite') }}</button>
178
+ </div>
179
179
  </div>
180
180
  </div>
181
181
  </div>
@@ -189,12 +189,12 @@
189
189
  </div>
190
190
  </div>
191
191
  </div>
192
- </div>
193
- <div v-if="!isToolConfigWriteAllowed('codex')" class="tool-config-write-overlay">
194
- <div class="tool-config-write-overlay-card">
195
- <div class="tool-config-write-overlay-title">{{ t('toolConfig.codex.lockedTitle') }}</div>
196
- <p>{{ t('toolConfig.codex.lockedDesc') }}</p>
197
- <button type="button" class="btn-tool" @click="setToolConfigPermission('codex', true)" :disabled="toolConfigPermissionSaving.codex">{{ t('toolConfig.enableWrite') }}</button>
192
+ <div v-if="!isToolConfigWriteAllowed('codex')" class="tool-config-write-overlay">
193
+ <div class="tool-config-write-overlay-card">
194
+ <div class="tool-config-write-overlay-title">{{ t('toolConfig.codex.lockedTitle') }}</div>
195
+ <p>{{ t('toolConfig.codex.lockedDesc') }}</p>
196
+ <button type="button" class="btn-tool" @click="setToolConfigPermission('codex', true)" :disabled="toolConfigPermissionSaving.codex">{{ t('toolConfig.enableWrite') }}</button>
197
+ </div>
198
198
  </div>
199
199
  </div>
200
200
  </div>
@@ -0,0 +1,166 @@
1
+ <!-- OpenCode 配置 -->
2
+ <div
3
+ v-show="mainTab === 'config' && configMode === 'opencode'"
4
+ class="mode-content mode-cards"
5
+ id="panel-config-opencode"
6
+ role="tabpanel"
7
+ :aria-labelledby="forceCompactLayout ? 'tab-config' : 'side-tab-config-opencode'">
8
+ <div v-if="forceCompactLayout && !sessionStandalone" class="segmented-control">
9
+ <button type="button" :class="['segment', { active: configMode === 'codex' }]" @click="switchConfigMode('codex')">{{ t('tab.config.codex') }}</button>
10
+ <button type="button" :class="['segment', { active: configMode === 'claude' }]" @click="switchConfigMode('claude')">{{ t('tab.config.claude') }}</button>
11
+ <button type="button" :class="['segment', { active: configMode === 'openclaw' }]" @click="switchConfigMode('openclaw')">{{ t('tab.config.openclaw') }}</button>
12
+ <button type="button" :class="['segment', { active: configMode === 'opencode' }]" @click="switchConfigMode('opencode')">{{ t('tab.config.opencode') }}</button>
13
+ </div>
14
+
15
+ <section class="tool-config-write-card" :aria-label="t('opencode.writeAria')">
16
+ <div class="tool-config-write-copy">
17
+ <div class="tool-config-write-title">{{ t('toolConfig.opencode.title') }}</div>
18
+ <p class="tool-config-write-desc">{{ t('toolConfig.opencode.desc') }}</p>
19
+ </div>
20
+ <label class="settings-toggle-row tool-config-write-toggle">
21
+ <input
22
+ type="checkbox"
23
+ autocomplete="off"
24
+ :checked="isToolConfigWriteAllowed('opencode')"
25
+ :disabled="toolConfigPermissionSaving.opencode"
26
+ @change="setToolConfigPermission('opencode', $event.target.checked)">
27
+ <span class="toggle-track">
28
+ <span class="toggle-thumb"></span>
29
+ </span>
30
+ <span>{{ toolConfigPermissionStatusLabel('opencode') }}</span>
31
+ </label>
32
+ </section>
33
+
34
+ <div class="tool-config-write-scope" :class="{ locked: !isToolConfigWriteAllowed('opencode') }">
35
+ <div class="tool-config-write-body">
36
+ <section class="selector-section">
37
+ <div class="selector-header">
38
+ <span class="selector-title">{{ t('opencode.providerModel.title') }}</span>
39
+ <div class="selector-actions opencode-provider-actions">
40
+ <button type="button" class="btn-tool btn-tool-compact" @click="loadOpencodeConfig({ toast: true })" :disabled="opencodeLoading">{{ opencodeLoading ? t('common.refreshing') : t('common.refresh') }}</button>
41
+ <button type="button" class="btn-tool btn-tool-compact" @click="applyOpencodeSelection" :disabled="opencodeApplying || opencodeLoading || !isToolConfigWriteAllowed('opencode')">{{ opencodeApplying ? t('common.applying') : t('opencode.applySelection') }}</button>
42
+ </div>
43
+ </div>
44
+ <div class="config-template-hint">{{ t('opencode.targetFile', { path: opencodeConfigPath || '~/.config/opencode/opencode.jsonc', status: opencodeConfigExists ? t('common.exists') : t('common.notExistsWillCreateOnSave') }) }}</div>
45
+ <div class="config-template-hint">{{ t('opencode.providerStoreFile', { path: opencodeProviderStorePath || '~/.codexmate/opencode/providers.json' }) }}</div>
46
+ <div class="codex-config-grid">
47
+ <div class="form-group codex-config-field">
48
+ <label class="form-label" for="opencode-provider">{{ t('field.provider') }}</label>
49
+ <input id="opencode-provider" class="form-input" v-model="opencodeProvider" list="opencode-provider-options" autocomplete="off" spellcheck="false" @change="fillOpencodeProvider(opencodeProvider)" @blur="fillOpencodeProvider(opencodeProvider)" placeholder="anthropic / openai / gemini">
50
+ <datalist id="opencode-provider-options">
51
+ <option v-for="provider in opencodeProviderCatalog()" :key="provider" :value="provider"></option>
52
+ </datalist>
53
+ </div>
54
+ <div class="form-group codex-config-field">
55
+ <label class="form-label" for="opencode-model">{{ t('field.model') }}</label>
56
+ <input id="opencode-model" class="form-input" v-model="opencodeModel" list="opencode-model-options" autocomplete="off" spellcheck="false" placeholder="claude-3.7-sonnet / gpt-4.1">
57
+ <datalist id="opencode-model-options">
58
+ <option v-for="model in opencodeModelCatalogForProvider(opencodeProvider)" :key="model" :value="model"></option>
59
+ </datalist>
60
+ </div>
61
+ <div class="form-group codex-config-field">
62
+ <label class="form-label" for="opencode-agent">{{ t('opencode.field.agent') }}</label>
63
+ <input id="opencode-agent" class="form-input" v-model="opencodeAgent" list="opencode-agent-options" autocomplete="off" spellcheck="false" placeholder="build">
64
+ <datalist id="opencode-agent-options">
65
+ <option value="build"></option>
66
+ <option value="plan"></option>
67
+ <option value="general"></option>
68
+ <option value="summary"></option>
69
+ <option value="compaction"></option>
70
+ <option value="title"></option>
71
+ <option v-for="agent in opencodeAgents" :key="agent.name" :value="agent.name"></option>
72
+ </datalist>
73
+ </div>
74
+ <div class="form-group codex-config-field">
75
+ <label class="form-label" for="opencode-api-key">{{ t('opencode.field.apiKeyKeep') }}</label>
76
+ <div class="input-with-toggle">
77
+ <input id="opencode-api-key" class="form-input" v-model="opencodeApiKey" :type="opencodeShowKey ? 'text' : 'password'" autocomplete="off" spellcheck="false" placeholder="sk-...">
78
+ <button type="button" class="input-toggle-btn" @click="opencodeShowKey = !opencodeShowKey" :title="opencodeShowKey ? t('common.hide') : t('common.show')">{{ opencodeShowKey ? t('common.hide') : t('common.show') }}</button>
79
+ </div>
80
+ </div>
81
+ <div class="form-group codex-config-field">
82
+ <label class="form-label" for="opencode-max-tokens">{{ t('opencode.field.maxTokens') }}</label>
83
+ <input id="opencode-max-tokens" class="form-input" v-model="opencodeMaxTokens" inputmode="numeric" autocomplete="off" placeholder="5000">
84
+ </div>
85
+ <div class="form-group codex-config-field">
86
+ <label class="form-label" for="opencode-reasoning-effort">{{ t('opencode.field.reasoningEffort') }}</label>
87
+ <select id="opencode-reasoning-effort" class="model-select" v-model="opencodeReasoningEffort">
88
+ <option value="">{{ t('opencode.option.keepUnchanged') }}</option>
89
+ <option value="low">{{ t('opencode.option.reasoningLow') }}</option>
90
+ <option value="medium">{{ t('opencode.option.reasoningMedium') }}</option>
91
+ <option value="high">{{ t('opencode.option.reasoningHigh') }}</option>
92
+ </select>
93
+ </div>
94
+ </div>
95
+ <label class="settings-toggle-row" style="margin-top: 12px;">
96
+ <input type="checkbox" v-model="opencodeApplyToCoreAgents">
97
+ <span class="toggle-track"><span class="toggle-thumb"></span></span>
98
+ <span>{{ t('opencode.applyToCoreAgents') }}</span>
99
+ </label>
100
+ <label class="settings-toggle-row" style="margin-top: 8px;">
101
+ <input type="checkbox" v-model="opencodeAutoCompact">
102
+ <span class="toggle-track"><span class="toggle-thumb"></span></span>
103
+ <span>{{ t('opencode.enableAutoCompaction') }}</span>
104
+ </label>
105
+ <label class="settings-toggle-row" style="margin-top: 8px;">
106
+ <input type="checkbox" v-model="opencodeProviderDisabled">
107
+ <span class="toggle-track"><span class="toggle-thumb"></span></span>
108
+ <span>{{ t('opencode.disableProvider') }}</span>
109
+ </label>
110
+ </section>
111
+
112
+ <section class="selector-section">
113
+ <div class="selector-header">
114
+ <span class="selector-title">{{ t('opencode.configFile.title') }}</span>
115
+ <div class="selector-actions">
116
+ <button type="button" class="btn-tool btn-tool-compact" @click="$refs.opencodeImportInput && $refs.opencodeImportInput.click()">{{ t('opencode.importParse') }}</button>
117
+ <button type="button" class="btn-tool btn-tool-compact" @click="saveOpencodeConfig" :disabled="opencodeSaving || opencodeLoading || !isToolConfigWriteAllowed('opencode')">{{ opencodeSaving ? t('common.saving') : t('opencode.saveConfig') }}</button>
118
+ </div>
119
+ </div>
120
+ <input ref="opencodeImportInput" class="sr-only" type="file" accept=".json,.jsonc,.opencode" @change="handleOpencodeImportChange">
121
+ <div v-if="opencodeImportFileName" class="config-template-hint">{{ t('opencode.parsedFile', { file: opencodeImportFileName }) }}</div>
122
+ <div v-if="opencodeError || opencodeImportError" class="config-template-hint error-text">{{ opencodeError || opencodeImportError }}</div>
123
+ <textarea class="template-textarea" v-model="opencodeContent" spellcheck="false" :readonly="opencodeSaving || opencodeLoading" :placeholder="t('opencode.textarea.placeholder')"></textarea>
124
+ <div class="config-template-hint">{{ t('opencode.configFile.hint') }}</div>
125
+ </section>
126
+
127
+ <section class="selector-section" v-if="opencodeProviders.length || opencodeAgents.length">
128
+ <div class="selector-header"><span class="selector-title">{{ t('opencode.summary.title') }}</span></div>
129
+ <div class="card-list">
130
+ <div v-for="provider in opencodeProviders" :key="provider.name" class="card">
131
+ <div class="card-leading">
132
+ <div class="card-icon">{{ provider.name.charAt(0).toUpperCase() }}</div>
133
+ <div class="card-content">
134
+ <div class="card-title">{{ provider.name }}</div>
135
+ <div class="card-subtitle">{{ provider.hasKey ? provider.apiKey : t('opencode.summary.noApiKey') }}</div>
136
+ </div>
137
+ </div>
138
+ <div class="card-trailing">
139
+ <span :class="['pill', provider.disabled ? 'empty' : 'configured']">{{ provider.disabled ? t('common.disabled') : t('common.enabled') }}</span>
140
+ <span class="pill empty">{{ provider.source === 'codexmate' ? t('opencode.summary.sourceCodexMate') : t('opencode.summary.sourceOpenCode') }}</span>
141
+ </div>
142
+ </div>
143
+ <div v-for="agent in opencodeAgents" :key="agent.name" class="card">
144
+ <div class="card-leading">
145
+ <div class="card-icon">A</div>
146
+ <div class="card-content">
147
+ <div class="card-title">{{ agent.name }}</div>
148
+ <div class="card-subtitle">{{ agent.model || t('opencode.summary.noModel') }}</div>
149
+ </div>
150
+ </div>
151
+ <div class="card-trailing">
152
+ <span class="pill configured">{{ t('opencode.summary.agentType') }}</span>
153
+ </div>
154
+ </div>
155
+ </div>
156
+ </section>
157
+ <div v-if="!isToolConfigWriteAllowed('opencode')" class="tool-config-write-overlay">
158
+ <div class="tool-config-write-overlay-card">
159
+ <div class="tool-config-write-overlay-title">{{ t('toolConfig.opencode.lockedTitle') }}</div>
160
+ <p>{{ t('toolConfig.opencode.lockedDesc') }}</p>
161
+ <button type="button" class="btn-tool" @click="setToolConfigPermission('opencode', true)" :disabled="toolConfigPermissionSaving.opencode">{{ t('toolConfig.enableWrite') }}</button>
162
+ </div>
163
+ </div>
164
+ </div>
165
+ </div>
166
+ </div>