codexmate 0.0.44 → 0.0.47

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.
Files changed (41) hide show
  1. package/README.md +20 -7
  2. package/README.zh.md +20 -7
  3. package/cli.js +473 -3
  4. package/package.json +2 -1
  5. package/web-ui/app.js +42 -5
  6. package/web-ui/index.html +2 -0
  7. package/web-ui/modules/app.computed.index.mjs +3 -1
  8. package/web-ui/modules/app.computed.main-tabs.mjs +3 -0
  9. package/web-ui/modules/app.computed.prompts.mjs +28 -0
  10. package/web-ui/modules/app.computed.session.mjs +23 -1
  11. package/web-ui/modules/app.constants.mjs +13 -0
  12. package/web-ui/modules/app.methods.agents.mjs +50 -4
  13. package/web-ui/modules/app.methods.index.mjs +6 -0
  14. package/web-ui/modules/app.methods.navigation.mjs +2 -1
  15. package/web-ui/modules/app.methods.opencode-config.mjs +228 -0
  16. package/web-ui/modules/app.methods.session-actions.mjs +10 -0
  17. package/web-ui/modules/app.methods.startup-claude.mjs +3 -1
  18. package/web-ui/modules/app.methods.tool-config-permissions.mjs +3 -2
  19. package/web-ui/modules/config-mode.computed.mjs +17 -1
  20. package/web-ui/modules/i18n/locales/en.mjs +66 -5
  21. package/web-ui/modules/i18n/locales/ja.mjs +66 -5
  22. package/web-ui/modules/i18n/locales/vi.mjs +74 -0
  23. package/web-ui/modules/i18n/locales/zh-tw.mjs +1269 -0
  24. package/web-ui/modules/i18n/locales/zh.mjs +66 -5
  25. package/web-ui/modules/i18n.dict.mjs +2 -0
  26. package/web-ui/modules/i18n.mjs +3 -2
  27. package/web-ui/partials/index/layout-footer.html +1 -1
  28. package/web-ui/partials/index/layout-header.html +70 -2
  29. package/web-ui/partials/index/modal-config-template-agents.html +12 -13
  30. package/web-ui/partials/index/panel-config-claude.html +6 -12
  31. package/web-ui/partials/index/panel-config-codex.html +6 -11
  32. package/web-ui/partials/index/panel-config-opencode.html +166 -0
  33. package/web-ui/partials/index/panel-prompts.html +100 -0
  34. package/web-ui/partials/index/panel-sessions.html +30 -10
  35. package/web-ui/partials/index/panel-usage.html +34 -18
  36. package/web-ui/res/web-ui-render.precompiled.js +932 -183
  37. package/web-ui/styles/controls-forms.css +62 -4
  38. package/web-ui/styles/modals-core.css +162 -0
  39. package/web-ui/styles/responsive.css +65 -5
  40. package/web-ui/styles/sessions-toolbar-trash.css +45 -10
  41. package/web-ui/styles/sessions-usage.css +31 -2
@@ -151,6 +151,7 @@ export function createMainTabsComputed() {
151
151
  if (this.mainTab === 'plugins') return this.t('kicker.plugins');
152
152
  if (this.mainTab === 'docs') return this.t('kicker.docs');
153
153
  if (this.mainTab === 'trash') return this.t('kicker.trash');
154
+ if (this.mainTab === 'prompts') return this.t('kicker.prompts');
154
155
  return this.t('kicker.settings');
155
156
  },
156
157
  mainTabTitle() {
@@ -163,6 +164,7 @@ export function createMainTabsComputed() {
163
164
  if (this.mainTab === 'plugins') return this.t('title.plugins');
164
165
  if (this.mainTab === 'docs') return this.t('title.docs');
165
166
  if (this.mainTab === 'trash') return this.t('settings.trash.title');
167
+ if (this.mainTab === 'prompts') return this.t('title.prompts');
166
168
  return this.t('title.settings');
167
169
  },
168
170
  mainTabSubtitle() {
@@ -175,6 +177,7 @@ export function createMainTabsComputed() {
175
177
  if (this.mainTab === 'plugins') return this.t('subtitle.plugins');
176
178
  if (this.mainTab === 'docs') return this.t('subtitle.docs');
177
179
  if (this.mainTab === 'trash') return this.t('settings.trash.meta');
180
+ if (this.mainTab === 'prompts') return this.t('subtitle.prompts');
178
181
  return this.t('subtitle.settings');
179
182
  },
180
183
  taskOrchestrationSelectedRun() {
@@ -0,0 +1,28 @@
1
+ export function createPromptsComputed() {
2
+ return {
3
+ promptsContextHint() {
4
+ if (!this.agentsLoading && !this.agentsDiffVisible && this.hasAgentsContentChanged()) {
5
+ return { text: this.t('modal.agents.unsaved.detectedHint'), warn: true };
6
+ }
7
+ if (this.agentsDiffVisible && (this.agentsDiffLoading || this.agentsSaving)) {
8
+ return { text: this.t('diff.hint.busy'), warn: false };
9
+ }
10
+ if (this.agentsDiffVisible && this.agentsDiffError) {
11
+ return { text: this.t('diff.hint.failedBack'), warn: false };
12
+ }
13
+ if (this.agentsDiffVisible && !this.agentsDiffHasChanges) {
14
+ return { text: this.t('diff.hint.noChangesBack'), warn: false };
15
+ }
16
+ if (this.agentsDiffVisible && this.agentsDiffTruncated) {
17
+ return { text: this.t('diff.viewHint.truncated'), warn: false };
18
+ }
19
+ if (this.agentsDiffVisible) {
20
+ return { text: this.t('diff.viewHint.preview'), warn: false };
21
+ }
22
+ if (!this.agentsLoading) {
23
+ return { text: this.t('modal.agents.hint.twoStepSave'), warn: false };
24
+ }
25
+ return null;
26
+ }
27
+ };
28
+ }
@@ -777,7 +777,7 @@ export function createSessionComputed() {
777
777
  ? this.sessionUsageDaily
778
778
  : null;
779
779
  if (!daily || !Array.isArray(daily.rows) || daily.rows.length === 0) {
780
- return { points: [], labels: [], linePath: '', areaPath: '', width: 800, maxTokens: 0 };
780
+ return { points: [], labels: [], linePath: '', areaPath: '', width: 800, maxTokens: 0, yTicks: [] };
781
781
  }
782
782
 
783
783
  const rows = daily.rows;
@@ -806,6 +806,27 @@ export function createSessionComputed() {
806
806
  const selectedKey = this.sessionsUsageSelectedDayKey;
807
807
  const selectedPoint = points.find(p => p.key === selectedKey) || points[points.length - 1] || null;
808
808
 
809
+ const yTicks = [];
810
+ if (maxTokens > 0) {
811
+ const rawStep = maxTokens / 4;
812
+ const magnitude = Math.pow(10, Math.floor(Math.log10(rawStep)));
813
+ const residual = rawStep / magnitude;
814
+ const niceStep = residual <= 1.5 ? magnitude : (residual <= 3.5 ? 2 * magnitude : 5 * magnitude);
815
+ const tickCount = Math.min(Math.ceil(maxTokens / niceStep), 8);
816
+ for (let i = 0; i <= tickCount; i++) {
817
+ const value = i * niceStep;
818
+ if (value > maxTokens + niceStep * 0.5) break;
819
+ const normalized = value / maxTokens;
820
+ const y = padding.top + chartHeight - (normalized * chartHeight);
821
+ yTicks.push({
822
+ value,
823
+ label: formatCompactUsageSummaryNumber(value),
824
+ y,
825
+ percent: (((height - y) / height) * 100).toFixed(1)
826
+ });
827
+ }
828
+ }
829
+
809
830
  return {
810
831
  points,
811
832
  labels: rows.map((row, index) => ({
@@ -817,6 +838,7 @@ export function createSessionComputed() {
817
838
  width,
818
839
  height,
819
840
  maxTokens,
841
+ yTicks,
820
842
  hoverX: selectedPoint ? selectedPoint.x : 0,
821
843
  hoverY: selectedPoint ? selectedPoint.y : 0
822
844
  };
@@ -2,6 +2,19 @@ export const SESSION_TRASH_LIST_LIMIT = 500;
2
2
  export const SESSION_TRASH_PAGE_SIZE = 200;
3
3
  export const DEFAULT_MODEL_CONTEXT_WINDOW = 190000;
4
4
  export const DEFAULT_MODEL_AUTO_COMPACT_TOKEN_LIMIT = 185000;
5
+ export const OPENCODE_MODEL_CATALOG = Object.freeze({
6
+ anthropic: Object.freeze(['claude-4-sonnet', 'claude-4-opus', 'claude-3.7-sonnet', 'claude-3.5-sonnet']),
7
+ openai: Object.freeze(['gpt-4.1', 'gpt-4o', 'o4-mini', 'o3-mini']),
8
+ gemini: Object.freeze(['gemini-2.5-pro', 'gemini-2.5-flash', 'gemini-1.5-pro']),
9
+ groq: Object.freeze(['llama-3.3-70b-versatile', 'llama-3.1-8b-instant']),
10
+ openrouter: Object.freeze(['anthropic/claude-3.7-sonnet', 'openai/gpt-4.1', 'google/gemini-2.5-pro']),
11
+ copilot: Object.freeze(['gpt-4o', 'claude-3.7-sonnet']),
12
+ azure: Object.freeze(['gpt-4.1', 'gpt-4o']),
13
+ bedrock: Object.freeze(['anthropic.claude-3-7-sonnet-20250219-v1:0']),
14
+ vertexai: Object.freeze(['gemini-2.5-pro', 'gemini-2.5-flash']),
15
+ xai: Object.freeze(['grok-3', 'grok-3-mini']),
16
+ local: Object.freeze(['local.model'])
17
+ });
5
18
  export const DEFAULT_OPENCLAW_TEMPLATE = `{
6
19
  // OpenClaw config (JSON5)
7
20
  agent: {
@@ -635,17 +635,63 @@ export function createAgentsMethods(options = {}) {
635
635
  return;
636
636
  }
637
637
  const successLabel = this.agentsContext === 'openclaw-workspace'
638
- ? `工作区文件已保存${this.agentsWorkspaceFileName ? `: ${this.agentsWorkspaceFileName}` : ''}`
638
+ ? this.t('toast.agents.saved.workspace', { name: this.agentsWorkspaceFileName || '' }).replace(/:\s*$/, '')
639
639
  : (this.agentsContext === 'claude-md'
640
- ? 'CLAUDE.md 已保存'
641
- : (this.agentsContext === 'openclaw' ? 'OpenClaw AGENTS.md 已保存' : 'AGENTS.md 已保存'));
640
+ ? this.t('toast.agents.saved.claudeMd')
641
+ : (this.agentsContext === 'openclaw' ? this.t('toast.agents.saved.openclaw') : this.t('toast.agents.saved.agents')));
642
642
  this.showMessage(successLabel, 'success');
643
- this.closeAgentsModal({ force: true });
643
+ if (this.mainTab === 'prompts') {
644
+ this.loadPromptsContent();
645
+ } else {
646
+ this.closeAgentsModal({ force: true });
647
+ }
644
648
  } catch (e) {
645
649
  this.showMessage(this.t('toast.save.fail'), 'error');
646
650
  } finally {
647
651
  this.agentsSaving = false;
648
652
  }
653
+ },
654
+
655
+ switchPromptsSubTab(subTab) {
656
+ const normalized = subTab === 'claude-md' ? 'claude-md' : 'codex';
657
+ if (this.promptsSubTab === normalized) {
658
+ this.loadPromptsContent();
659
+ return;
660
+ }
661
+ this.promptsSubTab = normalized;
662
+ },
663
+
664
+ async loadPromptsContent() {
665
+ const requestToken = issueLatestRequestToken(this, '_agentsOpenRequestToken');
666
+ this.agentsLoading = true;
667
+ this.resetAgentsDiffState();
668
+ try {
669
+ const isClaude = this.promptsSubTab === 'claude-md';
670
+ const action = isClaude ? 'get-claude-md-file' : 'get-agents-file';
671
+ const res = await api(action);
672
+ if (!isLatestRequestToken(this, '_agentsOpenRequestToken', requestToken)) {
673
+ return;
674
+ }
675
+ if (res.error) {
676
+ this.showMessage(res.error, 'error');
677
+ return;
678
+ }
679
+ this.agentsContent = res.content || '';
680
+ this.agentsOriginalContent = this.agentsContent;
681
+ this.agentsPath = res.path || '';
682
+ this.agentsExists = !!res.exists;
683
+ this.agentsLineEnding = res.lineEnding === '\r\n' ? '\r\n' : '\n';
684
+ this.agentsContext = isClaude ? 'claude-md' : 'codex';
685
+ } catch (e) {
686
+ if (!isLatestRequestToken(this, '_agentsOpenRequestToken', requestToken)) {
687
+ return;
688
+ }
689
+ this.showMessage(this.t('toast.load.fail'), 'error');
690
+ } finally {
691
+ if (isLatestRequestToken(this, '_agentsOpenRequestToken', requestToken)) {
692
+ this.agentsLoading = false;
693
+ }
694
+ }
649
695
  }
650
696
  };
651
697
  }
@@ -7,6 +7,7 @@ import {
7
7
  DEFAULT_MODEL_AUTO_COMPACT_TOKEN_LIMIT,
8
8
  DEFAULT_MODEL_CONTEXT_WINDOW,
9
9
  DEFAULT_OPENCLAW_TEMPLATE,
10
+ OPENCODE_MODEL_CATALOG,
10
11
  SESSION_TRASH_LIST_LIMIT,
11
12
  SESSION_TRASH_PAGE_SIZE
12
13
  } from './app.constants.mjs';
@@ -18,6 +19,7 @@ import { createNavigationMethods } from './app.methods.navigation.mjs';
18
19
  import { createOpenclawCoreMethods } from './app.methods.openclaw-core.mjs';
19
20
  import { createOpenclawEditingMethods } from './app.methods.openclaw-editing.mjs';
20
21
  import { createOpenclawPersistMethods } from './app.methods.openclaw-persist.mjs';
22
+ import { createOpencodeConfigMethods } from './app.methods.opencode-config.mjs';
21
23
  import { createProvidersMethods } from './app.methods.providers.mjs';
22
24
  import { createRuntimeMethods } from './app.methods.runtime.mjs';
23
25
  import { createToolConfigPermissionMethods } from './app.methods.tool-config-permissions.mjs';
@@ -89,6 +91,10 @@ export function createAppMethods() {
89
91
  api,
90
92
  defaultOpenclawTemplate: DEFAULT_OPENCLAW_TEMPLATE
91
93
  }),
94
+ ...createOpencodeConfigMethods({
95
+ api,
96
+ modelCatalog: OPENCODE_MODEL_CATALOG
97
+ }),
92
98
  ...createInstallMethods({ api }),
93
99
  ...createRuntimeMethods({ api }),
94
100
  ...createTaskOrchestrationMethods({ api })
@@ -15,7 +15,8 @@
15
15
  'plugins',
16
16
  'docs',
17
17
  'settings',
18
- 'trash'
18
+ 'trash',
19
+ 'prompts'
19
20
  ]);
20
21
  const loadDoctorOverview = async (vm, options = {}) => {
21
22
  if (!vm || typeof vm !== 'object') return false;
@@ -0,0 +1,228 @@
1
+ function normalizeOpencodeProviderName(value) {
2
+ const name = typeof value === 'string' ? value.trim().toLowerCase() : '';
3
+ return /^[a-z0-9_.-]+$/.test(name) ? name : '';
4
+ }
5
+
6
+ function normalizeOpencodeAgentName(value) {
7
+ const name = typeof value === 'string' ? value.trim() : '';
8
+ return /^[a-zA-Z0-9_.-]+$/.test(name) ? name : '';
9
+ }
10
+
11
+ function getOpencodeJsonParser() {
12
+ const root = typeof globalThis !== 'undefined' ? globalThis : {};
13
+ const json5 = root.JSON5 || (root.window && root.window.JSON5);
14
+ if (json5 && typeof json5.parse === 'function') {
15
+ return json5;
16
+ }
17
+ return JSON;
18
+ }
19
+
20
+ function summarizeOpencodeDraft(content) {
21
+ const parsed = getOpencodeJsonParser().parse(content || '{}');
22
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
23
+ throw new Error('OpenCode config must be a JSON/JSONC object');
24
+ }
25
+ return parsed;
26
+ }
27
+
28
+ export function createOpencodeConfigMethods(options = {}) {
29
+ const { api, modelCatalog = {} } = options;
30
+
31
+ return {
32
+ opencodeProviderCatalog() {
33
+ const configured = Array.isArray(this.opencodeProviders)
34
+ ? this.opencodeProviders.map(item => item && item.name).filter(Boolean)
35
+ : [];
36
+ const builtin = Object.keys(modelCatalog || {});
37
+ return [...new Set([...configured, ...builtin])].sort((a, b) => a.localeCompare(b));
38
+ },
39
+
40
+ opencodeModelCatalogForProvider(providerName = this.opencodeProvider) {
41
+ const provider = normalizeOpencodeProviderName(providerName);
42
+ const list = provider && Array.isArray(modelCatalog[provider]) ? modelCatalog[provider] : [];
43
+ const configured = Array.isArray(this.opencodeAgents)
44
+ ? this.opencodeAgents.map(item => item && item.model).filter(Boolean)
45
+ : [];
46
+ return [...new Set([...list, ...configured])];
47
+ },
48
+
49
+ fillOpencodeProvider(provider) {
50
+ const name = normalizeOpencodeProviderName(provider);
51
+ if (!name) return;
52
+ this.opencodeProvider = name;
53
+ const models = this.opencodeModelCatalogForProvider(name);
54
+ if (!this.opencodeModel || !models.includes(this.opencodeModel)) {
55
+ this.opencodeModel = models[0] || '';
56
+ }
57
+ const selectedProvider = Array.isArray(this.opencodeProviders)
58
+ ? this.opencodeProviders.find(item => normalizeOpencodeProviderName(item && item.name) === name)
59
+ : null;
60
+ this.opencodeProviderDisabled = !!(selectedProvider && selectedProvider.disabled);
61
+ },
62
+
63
+ refreshOpencodeSelectionFromSummary(res = {}) {
64
+ const providers = Array.isArray(res.providers) ? res.providers : [];
65
+ const agents = Array.isArray(res.agents) ? res.agents : [];
66
+ this.opencodeProviders = providers;
67
+ this.opencodeAgents = agents;
68
+ const currentAgent = normalizeOpencodeAgentName(res.currentAgent) || 'build';
69
+ this.opencodeAgent = currentAgent;
70
+ const currentModel = typeof res.currentModel === 'string' ? res.currentModel.trim() : '';
71
+ this.opencodeModel = currentModel;
72
+ if (typeof res.autoCompact === 'boolean') {
73
+ this.opencodeAutoCompact = res.autoCompact;
74
+ }
75
+ const enabledProvider = providers.find(item => item && item.disabled !== true && item.hasKey);
76
+ const firstProvider = providers.find(item => item && item.name);
77
+ this.opencodeProvider = normalizeOpencodeProviderName(res.currentProvider)
78
+ || normalizeOpencodeProviderName(enabledProvider && enabledProvider.name)
79
+ || normalizeOpencodeProviderName(firstProvider && firstProvider.name)
80
+ || normalizeOpencodeProviderName(this.opencodeProvider)
81
+ || 'anthropic';
82
+ const selectedProvider = providers.find(item => normalizeOpencodeProviderName(item && item.name) === this.opencodeProvider);
83
+ this.opencodeProviderDisabled = !!(selectedProvider && selectedProvider.disabled);
84
+ const models = this.opencodeModelCatalogForProvider(this.opencodeProvider);
85
+ if (!this.opencodeModel || (models.length && !models.includes(this.opencodeModel))) {
86
+ this.opencodeModel = models[0] || '';
87
+ }
88
+ },
89
+
90
+ async loadOpencodeConfig(options = {}) {
91
+ if (this.opencodeLoading) return;
92
+ this.opencodeLoading = true;
93
+ this.opencodeError = '';
94
+ try {
95
+ const res = await api('get-opencode-config');
96
+ if (res && res.error) {
97
+ this.opencodeError = res.error;
98
+ return;
99
+ }
100
+ this.opencodeContent = typeof res.content === 'string' ? res.content : '{}';
101
+ this.opencodeConfigPath = typeof res.targetPath === 'string' ? res.targetPath : '';
102
+ this.opencodeProviderStorePath = typeof res.providerStorePath === 'string' ? res.providerStorePath : '';
103
+ this.opencodeConfigExists = res.exists === true;
104
+ this.refreshOpencodeSelectionFromSummary(res || {});
105
+ if (options.toast === true) {
106
+ this.showMessage('OpenCode 配置已刷新', 'success');
107
+ }
108
+ } catch (e) {
109
+ this.opencodeError = e && e.message ? e.message : '读取 OpenCode 配置失败';
110
+ } finally {
111
+ this.opencodeLoading = false;
112
+ }
113
+ },
114
+
115
+ parseOpencodeImportContent(content, fileName = '') {
116
+ const raw = typeof content === 'string' ? content : '';
117
+ if (!raw.trim()) {
118
+ return { error: '导入文件为空' };
119
+ }
120
+ try {
121
+ const parsed = summarizeOpencodeDraft(raw);
122
+ const pretty = JSON.stringify(parsed, null, 2) + '\n';
123
+ return { content: pretty, fileName };
124
+ } catch (e) {
125
+ return { error: `OpenCode JSON/JSONC 解析失败: ${e.message}` };
126
+ }
127
+ },
128
+
129
+ async handleOpencodeImportChange(event) {
130
+ const input = event && event.target ? event.target : null;
131
+ const file = input && input.files && input.files[0] ? input.files[0] : null;
132
+ if (!file) return;
133
+ this.opencodeImportError = '';
134
+ try {
135
+ const content = await file.text();
136
+ const parsed = this.parseOpencodeImportContent(content, file.name || '');
137
+ if (parsed.error) {
138
+ this.opencodeImportError = parsed.error;
139
+ this.showMessage(parsed.error, 'error');
140
+ return;
141
+ }
142
+ this.opencodeContent = parsed.content;
143
+ this.opencodeImportFileName = parsed.fileName;
144
+ this.showMessage('已解析 OpenCode 配置,确认后可保存', 'success');
145
+ } catch (e) {
146
+ this.opencodeImportError = e && e.message ? e.message : '读取导入文件失败';
147
+ this.showMessage(this.opencodeImportError, 'error');
148
+ } finally {
149
+ if (input) input.value = '';
150
+ }
151
+ },
152
+
153
+ async saveOpencodeConfig() {
154
+ if (this.opencodeSaving) return;
155
+ if (!this.isToolConfigWriteAllowed('opencode')) {
156
+ this.showMessage('请先打开 OpenCode 写入开关', 'error');
157
+ return;
158
+ }
159
+ this.opencodeSaving = true;
160
+ this.opencodeError = '';
161
+ try {
162
+ const res = await api('apply-opencode-config', { content: this.opencodeContent });
163
+ if (res && res.error) {
164
+ this.opencodeError = res.error;
165
+ this.showMessage(res.error, 'error');
166
+ return;
167
+ }
168
+ this.opencodeConfigExists = true;
169
+ if (res && typeof res.targetPath === 'string') this.opencodeConfigPath = res.targetPath;
170
+ if (res && typeof res.providerStorePath === 'string') this.opencodeProviderStorePath = res.providerStorePath;
171
+ this.refreshOpencodeSelectionFromSummary(res || {});
172
+ this.showMessage('OpenCode 配置已保存', 'success');
173
+ await this.loadOpencodeConfig();
174
+ } catch (e) {
175
+ this.opencodeError = e && e.message ? e.message : '保存 OpenCode 配置失败';
176
+ this.showMessage(this.opencodeError, 'error');
177
+ } finally {
178
+ this.opencodeSaving = false;
179
+ }
180
+ },
181
+
182
+ async applyOpencodeSelection() {
183
+ if (this.opencodeApplying) return;
184
+ if (!this.isToolConfigWriteAllowed('opencode')) {
185
+ this.showMessage('请先打开 OpenCode 写入开关', 'error');
186
+ return;
187
+ }
188
+ const provider = normalizeOpencodeProviderName(this.opencodeProvider);
189
+ const model = typeof this.opencodeModel === 'string' ? this.opencodeModel.trim() : '';
190
+ if (!provider || !model) {
191
+ this.showMessage('请选择 OpenCode provider 和 model', 'error');
192
+ return;
193
+ }
194
+ this.opencodeApplying = true;
195
+ this.opencodeError = '';
196
+ try {
197
+ const res = await api('update-opencode-selection', {
198
+ provider,
199
+ model,
200
+ apiKey: this.opencodeApiKey,
201
+ agent: this.opencodeAgent || 'build',
202
+ applyToCoreAgents: this.opencodeApplyToCoreAgents === true,
203
+ disabled: this.opencodeProviderDisabled === true,
204
+ autoCompact: this.opencodeAutoCompact !== false,
205
+ maxTokens: this.opencodeMaxTokens,
206
+ reasoningEffort: this.opencodeReasoningEffort
207
+ });
208
+ if (res && res.error) {
209
+ this.opencodeError = res.error;
210
+ this.showMessage(res.error, 'error');
211
+ return;
212
+ }
213
+ if (res && typeof res.content === 'string') this.opencodeContent = res.content;
214
+ if (res && typeof res.targetPath === 'string') this.opencodeConfigPath = res.targetPath;
215
+ if (res && typeof res.providerStorePath === 'string') this.opencodeProviderStorePath = res.providerStorePath;
216
+ this.opencodeConfigExists = true;
217
+ this.opencodeApiKey = '';
218
+ this.refreshOpencodeSelectionFromSummary(res || {});
219
+ this.showMessage('OpenCode provider/model 已应用', 'success');
220
+ } catch (e) {
221
+ this.opencodeError = e && e.message ? e.message : '应用 OpenCode 配置失败';
222
+ this.showMessage(this.opencodeError, 'error');
223
+ } finally {
224
+ this.opencodeApplying = false;
225
+ }
226
+ }
227
+ };
228
+ }
@@ -90,6 +90,10 @@ export function createSessionActionMethods(options = {}) {
90
90
  this.loadSessionStandalonePlain();
91
91
  },
92
92
 
93
+ canBuildStandaloneUrl(session) {
94
+ return !!this.buildSessionStandaloneUrl(session);
95
+ },
96
+
93
97
  buildSessionStandaloneUrl(session) {
94
98
  if (!session) return '';
95
99
  const source = typeof session.source === 'string' ? session.source.trim().toLowerCase() : '';
@@ -129,6 +133,12 @@ export function createSessionActionMethods(options = {}) {
129
133
  this.showMessage(this.t('toast.copy.fail'), 'error');
130
134
  },
131
135
 
136
+ openSessionLink(session) {
137
+ const url = this.buildSessionStandaloneUrl(session);
138
+ if (!url) { this.showMessage(this.t('toast.link.fail'), 'error'); return; }
139
+ window.open(url, '_blank', 'noopener,noreferrer');
140
+ },
141
+
132
142
  getSessionFilePath(session) {
133
143
  const filePath = typeof session?.filePath === 'string' ? session.filePath.trim() : '';
134
144
  return filePath;
@@ -125,7 +125,8 @@ export function createStartupClaudeMethods(options = {}) {
125
125
  if (statusRes.toolConfigPermissions && typeof statusRes.toolConfigPermissions === 'object') {
126
126
  this.toolConfigPermissions = {
127
127
  codex: statusRes.toolConfigPermissions.codex === true,
128
- claude: statusRes.toolConfigPermissions.claude === true
128
+ claude: statusRes.toolConfigPermissions.claude === true,
129
+ opencode: statusRes.toolConfigPermissions.opencode === true
129
130
  };
130
131
  try {
131
132
  localStorage.setItem('toolConfigPermissions', JSON.stringify(this.toolConfigPermissions));
@@ -134,6 +135,7 @@ export function createStartupClaudeMethods(options = {}) {
134
135
  this.providersList = listRes.providers;
135
136
  if (typeof this.loadLocalBridgeExcluded === 'function') { this.loadLocalBridgeExcluded(); }
136
137
  if (typeof this.loadClaudeLocalBridgeStatus === 'function') { this.loadClaudeLocalBridgeStatus(); }
138
+ if (typeof this.loadOpencodeConfig === 'function') { this.loadOpencodeConfig(); }
137
139
  if (statusRes.configReady === false) {
138
140
  this.showMessage('配置已加载', 'info');
139
141
  }
@@ -3,14 +3,15 @@ export function createToolConfigPermissionMethods(options = {}) {
3
3
 
4
4
  function normalizeTarget(value) {
5
5
  const target = typeof value === 'string' ? value.trim().toLowerCase() : '';
6
- return target === 'codex' || target === 'claude' ? target : '';
6
+ return target === 'codex' || target === 'claude' || target === 'opencode' ? target : '';
7
7
  }
8
8
 
9
9
  function normalizePermissions(value) {
10
10
  const source = value && typeof value === 'object' && !Array.isArray(value) ? value : {};
11
11
  return {
12
12
  codex: source.codex === true,
13
- claude: source.claude === true
13
+ claude: source.claude === true,
14
+ opencode: source.opencode === true
14
15
  };
15
16
  }
16
17
 
@@ -10,7 +10,8 @@
10
10
  export const CONFIG_MODE_SET = new Set([
11
11
  ...Object.keys(PROVIDER_CONFIG_MODE_META),
12
12
  'claude',
13
- 'openclaw'
13
+ 'openclaw',
14
+ 'opencode'
14
15
  ]);
15
16
 
16
17
  export function getProviderConfigModeMeta(mode) {
@@ -61,6 +62,7 @@ export function createConfigModeComputed() {
61
62
  if (providerMeta) return providerMeta.label;
62
63
  if (this.configMode === 'claude') return 'Claude Code';
63
64
  if (this.configMode === 'openclaw') return 'OpenClaw';
65
+ if (this.configMode === 'opencode') return 'OpenCode';
64
66
  return '未选择';
65
67
  },
66
68
  inspectorCurrentConfigLabel() {
@@ -76,6 +78,10 @@ export function createConfigModeComputed() {
76
78
  const openclaw = typeof this.currentOpenclawConfig === 'string' ? this.currentOpenclawConfig.trim() : '';
77
79
  return openclaw || '未选择';
78
80
  }
81
+ if (this.configMode === 'opencode') {
82
+ const provider = typeof this.opencodeProvider === 'string' ? this.opencodeProvider.trim() : '';
83
+ return provider || '未选择';
84
+ }
79
85
  return '未选择';
80
86
  },
81
87
  inspectorCurrentModelLabel() {
@@ -93,6 +99,10 @@ export function createConfigModeComputed() {
93
99
  : '';
94
100
  return model || '按配置文件';
95
101
  }
102
+ if (this.configMode === 'opencode') {
103
+ const model = typeof this.opencodeModel === 'string' ? this.opencodeModel.trim() : '';
104
+ return model || '未选择';
105
+ }
96
106
  return '未选择';
97
107
  },
98
108
  inspectorTemplateStatus() {
@@ -118,6 +128,12 @@ export function createConfigModeComputed() {
118
128
  }
119
129
  return 'JSON5 可保存并应用';
120
130
  }
131
+ if (this.configMode === 'opencode') {
132
+ if (this.opencodeSaving || this.opencodeApplying) {
133
+ return 'OpenCode 保存/应用中';
134
+ }
135
+ return 'JSON 可编辑并应用';
136
+ }
121
137
  return '未选择';
122
138
  }
123
139
  };