codexmate 0.0.26 → 0.0.28

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 +7 -2
  2. package/README.zh.md +7 -2
  3. package/cli/builtin-proxy.js +636 -95
  4. package/cli/openai-bridge.js +497 -5
  5. package/cli.js +75 -29
  6. package/lib/cli-models-utils.js +71 -10
  7. package/package.json +3 -1
  8. package/plugins/prompt-templates/computed.mjs +1 -1
  9. package/plugins/prompt-templates/methods.mjs +0 -66
  10. package/plugins/prompt-templates/overview.mjs +1 -0
  11. package/web-ui/app.js +16 -16
  12. package/web-ui/logic.codex.mjs +56 -0
  13. package/web-ui/logic.sessions.mjs +56 -0
  14. package/web-ui/modules/app.computed.dashboard.mjs +54 -0
  15. package/web-ui/modules/app.computed.session.mjs +48 -0
  16. package/web-ui/modules/app.methods.claude-config.mjs +18 -7
  17. package/web-ui/modules/app.methods.codex-config.mjs +35 -3
  18. package/web-ui/modules/app.methods.providers.mjs +9 -1
  19. package/web-ui/modules/app.methods.session-actions.mjs +2 -5
  20. package/web-ui/modules/app.methods.session-browser.mjs +4 -5
  21. package/web-ui/modules/app.methods.session-trash.mjs +19 -4
  22. package/web-ui/modules/app.methods.startup-claude.mjs +12 -1
  23. package/web-ui/modules/i18n.dict.mjs +28 -32
  24. package/web-ui/modules/provider-url-display.mjs +17 -0
  25. package/web-ui/partials/index/panel-config-claude.html +5 -1
  26. package/web-ui/partials/index/panel-config-codex.html +33 -4
  27. package/web-ui/partials/index/panel-plugins.html +3 -29
  28. package/web-ui/partials/index/panel-sessions.html +0 -10
  29. package/web-ui/partials/index/panel-settings.html +62 -67
  30. package/web-ui/partials/index/panel-usage.html +31 -2
  31. package/web-ui/session-helpers.mjs +2 -2
  32. package/web-ui/styles/base-theme.css +47 -34
  33. package/web-ui/styles/controls-forms.css +27 -28
  34. package/web-ui/styles/layout-shell.css +37 -34
  35. package/web-ui/styles/modals-core.css +12 -10
  36. package/web-ui/styles/navigation-panels.css +36 -35
  37. package/web-ui/styles/responsive.css +4 -4
  38. package/web-ui/styles/sessions-list.css +10 -6
  39. package/web-ui/styles/sessions-usage.css +95 -0
  40. package/web-ui/styles/settings-panel.css +19 -0
  41. package/web-ui/styles/titles-cards.css +90 -26
@@ -108,8 +108,6 @@ const DICT = Object.freeze({
108
108
  'placeholder.apiKeyExampleClaude': 'sk-ant-...',
109
109
  'placeholder.baseUrlExampleClaude': 'https://open.bigmodel.cn/api/anthropic',
110
110
  'placeholder.selectProvider': '请选择提供商',
111
- 'placeholder.varNameExample': '例如: code',
112
- 'hint.varNameRules': '仅支持字母/数字/下划线/中划线/点,将插入为 {{var}}。',
113
111
 
114
112
  // Roles / labels
115
113
  'role.you': '你',
@@ -332,7 +330,6 @@ const DICT = Object.freeze({
332
330
  'plugins.promptTemplates.editor.templatePlaceholder': '在这里编写模板。使用 {{var}} 占位符。',
333
331
  'plugins.promptTemplates.vars.title': '变量',
334
332
  'plugins.promptTemplates.vars.hint': '从模板中检测。填写后可渲染最终提示词。',
335
- 'plugins.promptTemplates.vars.add': '新增变量',
336
333
  'plugins.promptTemplates.vars.reset': '重置',
337
334
  'plugins.promptTemplates.vars.empty': '未检测到变量。',
338
335
  'plugins.promptTemplates.vars.valuePlaceholder': '变量值:{name}',
@@ -341,10 +338,6 @@ const DICT = Object.freeze({
341
338
  'plugins.promptTemplates.preview.copy': '复制',
342
339
  'plugins.promptTemplates.preview.outputAria': '渲染结果(提示词)',
343
340
  'plugins.promptTemplates.noPluginSelected': '请先从左侧选择一个插件。',
344
- 'plugins.promptTemplates.varModal.title': '新增变量',
345
- 'plugins.promptTemplates.varModal.nameLabel': '变量名',
346
- 'plugins.promptTemplates.varModal.cancel': '取消',
347
- 'plugins.promptTemplates.varModal.add': '添加',
348
341
 
349
342
  'plugins.meta.attribution': '创建者:{createdBy} · 维护者:{maintainers}',
350
343
  'plugins.meta.createdBy': '创建者:{createdBy}',
@@ -378,12 +371,8 @@ const DICT = Object.freeze({
378
371
  'toast.export.notSupported': '当前不支持导出',
379
372
  'toast.plugins.loadFail': '加载 Plugins 失败',
380
373
  'toast.templates.builtinNotEditable': '内置模板不可编辑',
381
- 'toast.templates.varAdded': '已添加变量',
382
374
  'toast.templates.builtinNotModifiable': '内置模板不可修改,请先复制再编辑',
383
375
  'toast.templates.nameRequired': '模板名称不能为空',
384
- 'toast.templates.varNameRequired': '请输入变量名',
385
- 'toast.templates.varNameInvalid': '变量名仅支持字母/数字/下划线/中划线/点',
386
- 'toast.templates.varExists': '变量已存在',
387
376
  'toast.templates.builtinNotDuplicable': '内置模板不可复制',
388
377
  'toast.templates.builtinNotDeletable': '内置模板不可删除',
389
378
  'toast.templates.deleteTitle': '删除模板',
@@ -545,7 +534,6 @@ const DICT = Object.freeze({
545
534
  'sessions.pin': '置顶',
546
535
  'sessions.unpin': '取消置顶',
547
536
  'sessions.copyResume': '复制恢复命令',
548
- 'sessions.resumeYolo': '复制恢复命令附带 --yolo',
549
537
  'sessions.preview.refresh': '刷新内容',
550
538
  'sessions.preview.loading': '加载中...',
551
539
  'sessions.preview.deleteHard': '直接删除',
@@ -622,6 +610,11 @@ const DICT = Object.freeze({
622
610
  'usage.heatmap.legend.more': '多',
623
611
  'usage.heatmap.tooltip': '{date} · {sessions} 会话 · {messages} 消息 · {tokens} token',
624
612
  'usage.heatmap.aria': '{date},{sessions} 会话',
613
+ 'usage.hourlyHeatmap.title': '7×24 活跃热力图',
614
+ 'usage.hourlyHeatmap.subtitle': '按星期 × 小时聚合会话分布,深色 = 高活跃。',
615
+ 'usage.hourlyHeatmap.tooltip': '{weekday} {hour}:00 · {sessions} 会话 · {messages} 消息 · {tokens} token',
616
+ 'usage.hourlyHeatmap.legend.less': '少',
617
+ 'usage.hourlyHeatmap.legend.more': '多',
625
618
  'usage.legend.tokens': 'Token',
626
619
  'usage.legend.cost': '预估费用',
627
620
  'usage.table.date': '日期',
@@ -705,6 +698,7 @@ const DICT = Object.freeze({
705
698
 
706
699
  // Config panel (Codex)
707
700
  'config.addProvider': '新增提供商',
701
+ 'config.providerTemplate.title': '预设供应商',
708
702
  'config.models': '模型',
709
703
  'config.modelLoading': '加载中...',
710
704
  'config.models.unlimited': '当前无模型列表,可手填。',
@@ -740,6 +734,7 @@ const DICT = Object.freeze({
740
734
  'modal.agents.title.openclawWorkspaceFile': 'OpenClaw 工作区文件: {fileName}',
741
735
  'modal.agents.hint.openclawWorkspaceFile': '保存后会写入 OpenClaw Workspace 下的 {fileName}。',
742
736
  'config.url.unset': '未设 URL',
737
+ 'config.model.unset': '未设置模型',
743
738
  'config.badge.system': '系统',
744
739
  'config.availabilityTest': '可用性测试',
745
740
  'config.availabilityTestAria': '测试 {name} 可用性',
@@ -904,10 +899,9 @@ const DICT = Object.freeze({
904
899
  ,
905
900
 
906
901
  // Settings panel
907
- 'settings.tab.backup': '备份与导入',
908
- 'settings.tab.trash': '回收站',
909
- 'settings.tab.device': '设备',
910
- 'settings.tabs.aria': '设置标签页',
902
+ 'settings.tab.general': '通用',
903
+ 'settings.tab.data': '数据',
904
+ 'settings.tabs.aria': '设置分类',
911
905
  'settings.sharePrefix.title': '分享命令前缀',
912
906
  'settings.sharePrefix.meta': '影响 Web UI 里“复制分享命令”的前缀',
913
907
  'settings.sharePrefix.label': '前缀',
@@ -944,6 +938,10 @@ const DICT = Object.freeze({
944
938
  'settings.trash.workspace': '工作区',
945
939
  'settings.trash.originalFile': '原文件',
946
940
  'settings.trash.loadMore': '加载更多(剩余 {count} 项)',
941
+ 'settings.trash.retention': '自动清理',
942
+ 'settings.trash.retentionMeta': '超过保留天数的回收站记录将自动清除',
943
+ 'settings.trash.retentionLabel': '保留天数',
944
+ 'settings.trash.retentionHint': '范围 1-365 天,默认 30 天。每次加载回收站时自动清理过期记录。',
947
945
 
948
946
  'settings.templateConfirm.title': '配置模板二次确认',
949
947
  'settings.templateConfirm.meta': '降低误写入风险',
@@ -1162,8 +1160,6 @@ const DICT = Object.freeze({
1162
1160
  'placeholder.apiKeyExampleClaude': 'sk-ant-...',
1163
1161
  'placeholder.baseUrlExampleClaude': 'https://open.bigmodel.cn/api/anthropic',
1164
1162
  'placeholder.selectProvider': 'Select a provider',
1165
- 'placeholder.varNameExample': 'e.g. code',
1166
- 'hint.varNameRules': 'Allowed: letters/numbers/underscore/dash/dot. Will be inserted as {{var}}.',
1167
1163
 
1168
1164
  // Roles / labels
1169
1165
  'role.you': 'You',
@@ -1386,7 +1382,6 @@ const DICT = Object.freeze({
1386
1382
  'plugins.promptTemplates.editor.templatePlaceholder': 'Write your template here. Use {{var}} placeholders.',
1387
1383
  'plugins.promptTemplates.vars.title': 'Variables',
1388
1384
  'plugins.promptTemplates.vars.hint': 'Detected from the template. Fill them to render the final prompt.',
1389
- 'plugins.promptTemplates.vars.add': 'Add variable',
1390
1385
  'plugins.promptTemplates.vars.reset': 'Reset',
1391
1386
  'plugins.promptTemplates.vars.empty': 'No variables detected.',
1392
1387
  'plugins.promptTemplates.vars.valuePlaceholder': 'Value for {name}',
@@ -1395,10 +1390,6 @@ const DICT = Object.freeze({
1395
1390
  'plugins.promptTemplates.preview.copy': 'Copy',
1396
1391
  'plugins.promptTemplates.preview.outputAria': 'Rendered prompt',
1397
1392
  'plugins.promptTemplates.noPluginSelected': 'Select a plugin from the left panel first.',
1398
- 'plugins.promptTemplates.varModal.title': 'Add variable',
1399
- 'plugins.promptTemplates.varModal.nameLabel': 'Variable name',
1400
- 'plugins.promptTemplates.varModal.cancel': 'Cancel',
1401
- 'plugins.promptTemplates.varModal.add': 'Add',
1402
1393
 
1403
1394
  'plugins.meta.attribution': 'Created by {createdBy} · Maintained by {maintainers}',
1404
1395
  'plugins.meta.createdBy': 'Created by {createdBy}',
@@ -1432,12 +1423,8 @@ const DICT = Object.freeze({
1432
1423
  'toast.export.notSupported': 'Export not supported',
1433
1424
  'toast.plugins.loadFail': 'Failed to load plugins',
1434
1425
  'toast.templates.builtinNotEditable': 'Built-in templates are not editable',
1435
- 'toast.templates.varAdded': 'Variable added',
1436
1426
  'toast.templates.builtinNotModifiable': 'Built-in templates are read-only. Duplicate first.',
1437
1427
  'toast.templates.nameRequired': 'Template name is required',
1438
- 'toast.templates.varNameRequired': 'Variable name is required',
1439
- 'toast.templates.varNameInvalid': 'Variable name may only contain letters, numbers, underscore, dash, dot',
1440
- 'toast.templates.varExists': 'Variable already exists',
1441
1428
  'toast.templates.builtinNotDuplicable': 'Built-in templates cannot be duplicated',
1442
1429
  'toast.templates.builtinNotDeletable': 'Built-in templates cannot be deleted',
1443
1430
  'toast.templates.deleteTitle': 'Delete template',
@@ -1599,7 +1586,6 @@ const DICT = Object.freeze({
1599
1586
  'sessions.pin': 'Pin',
1600
1587
  'sessions.unpin': 'Unpin',
1601
1588
  'sessions.copyResume': 'Copy resume command',
1602
- 'sessions.resumeYolo': 'Append --yolo to resume command',
1603
1589
  'sessions.preview.refresh': 'Refresh content',
1604
1590
  'sessions.preview.loading': 'Loading...',
1605
1591
  'sessions.preview.deleteHard': 'Delete permanently',
@@ -1676,6 +1662,11 @@ const DICT = Object.freeze({
1676
1662
  'usage.heatmap.legend.more': 'More',
1677
1663
  'usage.heatmap.tooltip': '{date} · {sessions} sessions · {messages} messages · {tokens} tokens',
1678
1664
  'usage.heatmap.aria': '{date}, {sessions} sessions',
1665
+ 'usage.hourlyHeatmap.title': '7×24 Activity Heatmap',
1666
+ 'usage.hourlyHeatmap.subtitle': 'Session distribution by weekday × hour; darker = more active.',
1667
+ 'usage.hourlyHeatmap.tooltip': '{weekday} {hour}:00 · {sessions} sessions · {messages} messages · {tokens} tokens',
1668
+ 'usage.hourlyHeatmap.legend.less': 'Less',
1669
+ 'usage.hourlyHeatmap.legend.more': 'More',
1679
1670
  'usage.legend.tokens': 'Tokens',
1680
1671
  'usage.legend.cost': 'Estimated cost',
1681
1672
  'usage.table.date': 'Date',
@@ -1759,6 +1750,7 @@ const DICT = Object.freeze({
1759
1750
 
1760
1751
  // Config panel (Codex)
1761
1752
  'config.addProvider': 'Add provider',
1753
+ 'config.providerTemplate.title': 'Provider presets',
1762
1754
  'config.models': 'Model',
1763
1755
  'config.modelLoading': 'Loading...',
1764
1756
  'config.models.unlimited': 'No model list available. Enter manually.',
@@ -1794,6 +1786,7 @@ const DICT = Object.freeze({
1794
1786
  'modal.agents.title.openclawWorkspaceFile': 'OpenClaw workspace file: {fileName}',
1795
1787
  'modal.agents.hint.openclawWorkspaceFile': 'Saved content will be written to OpenClaw workspace {fileName}.',
1796
1788
  'config.url.unset': 'URL not set',
1789
+ 'config.model.unset': 'Model not set',
1797
1790
  'config.badge.system': 'System',
1798
1791
  'config.availabilityTest': 'Availability test',
1799
1792
  'config.availabilityTestAria': 'Test availability for {name}',
@@ -1958,10 +1951,9 @@ const DICT = Object.freeze({
1958
1951
  ,
1959
1952
 
1960
1953
  // Settings panel
1961
- 'settings.tab.backup': 'Backup & Import',
1962
- 'settings.tab.trash': 'Trash',
1963
- 'settings.tab.device': 'Device',
1964
- 'settings.tabs.aria': 'Settings tabs',
1954
+ 'settings.tab.general': 'General',
1955
+ 'settings.tab.data': 'Data',
1956
+ 'settings.tabs.aria': 'Settings categories',
1965
1957
  'settings.sharePrefix.title': 'Share command prefix',
1966
1958
  'settings.sharePrefix.meta': 'Used as the prefix for “Copy share command” in the Web UI',
1967
1959
  'settings.sharePrefix.label': 'Prefix',
@@ -1998,6 +1990,10 @@ const DICT = Object.freeze({
1998
1990
  'settings.trash.workspace': 'Workspace',
1999
1991
  'settings.trash.originalFile': 'Original file',
2000
1992
  'settings.trash.loadMore': 'Load more (remaining {count})',
1993
+ 'settings.trash.retention': 'Auto-purge',
1994
+ 'settings.trash.retentionMeta': 'Trash entries older than retention days are auto-purged',
1995
+ 'settings.trash.retentionLabel': 'Retention days',
1996
+ 'settings.trash.retentionHint': 'Range 1-365 days, default 30. Expired entries are purged on each trash load.',
2001
1997
 
2002
1998
  'settings.templateConfirm.title': 'Template apply confirmation',
2003
1999
  'settings.templateConfirm.meta': 'Reduce accidental writes',
@@ -0,0 +1,17 @@
1
+ export function getProviderDisplayUrl(provider) {
2
+ if (!provider) return '';
3
+ const bridge = typeof provider.codexmate_bridge === 'string' ? provider.codexmate_bridge.trim() : '';
4
+ if (bridge === 'openai') {
5
+ const upstream = typeof provider.upstreamUrl === 'string' ? provider.upstreamUrl.trim() : '';
6
+ if (upstream) return upstream;
7
+ }
8
+ return provider.url || '';
9
+ }
10
+
11
+ export function checkIsTransformProvider(provider) {
12
+ if (!provider || typeof provider !== 'object') return false;
13
+ const bridge = typeof provider.codexmate_bridge === 'string' ? provider.codexmate_bridge.trim() : '';
14
+ if (bridge === 'openai') return true;
15
+ const url = String(provider.url || '');
16
+ return url.includes('/bridge/openai/');
17
+ }
@@ -64,6 +64,9 @@
64
64
  <button type="button" class="btn-mini" @click="newClaudeConfig.name = 'AiHubMix'; newClaudeConfig.apiKey = ''; newClaudeConfig.baseUrl = 'https://aihubmix.com'; newClaudeConfig.model = 'glm-4.7'; showClaudeConfigModal = true">AiHubMix</button>
65
65
  <button type="button" class="btn-mini" @click="newClaudeConfig.name = 'DMXAPI'; newClaudeConfig.apiKey = ''; newClaudeConfig.baseUrl = 'https://www.dmxapi.cn'; newClaudeConfig.model = 'glm-4.7'; showClaudeConfigModal = true">DMXAPI</button>
66
66
  <button type="button" class="btn-mini" @click="newClaudeConfig.name = 'PackyCode'; newClaudeConfig.apiKey = ''; newClaudeConfig.baseUrl = 'https://www.packyapi.com'; newClaudeConfig.model = 'glm-4.7'; showClaudeConfigModal = true">PackyCode</button>
67
+ <button type="button" class="btn-mini" @click="newClaudeConfig.name = 'AnyRouter'; newClaudeConfig.apiKey = ''; newClaudeConfig.baseUrl = 'https://anyrouter.top'; newClaudeConfig.model = 'claude-opus-4-7[1m]'; showClaudeConfigModal = true">AnyRouter</button>
68
+ <button type="button" class="btn-mini" @click="newClaudeConfig.name = 'Xiaomi MiMo'; newClaudeConfig.apiKey = ''; newClaudeConfig.baseUrl = 'https://api.xiaomimimo.com/anthropic'; newClaudeConfig.model = 'mimo-v2.5-pro'; showClaudeConfigModal = true">Xiaomi MiMo</button>
69
+ <button type="button" class="btn-mini" @click="newClaudeConfig.name = 'Xiaomi Token Plan'; newClaudeConfig.apiKey = ''; newClaudeConfig.baseUrl = 'https://token-plan-cn.xiaomimimo.com/anthropic'; newClaudeConfig.model = 'mimo-v2.5-pro'; showClaudeConfigModal = true">Xiaomi Token Plan</button>
67
70
  </div>
68
71
  </div>
69
72
 
@@ -146,7 +149,8 @@
146
149
  <div class="card-icon">{{ name.charAt(0).toUpperCase() }}</div>
147
150
  <div class="card-content">
148
151
  <div class="card-title">{{ name }}</div>
149
- <div class="card-subtitle">{{ config.model || t('claude.model.unset') }}</div>
152
+ <div class="card-subtitle card-subtitle-model">{{ config.model || t('claude.model.unset') }}</div>
153
+ <div class="card-subtitle card-subtitle-url" v-if="config.baseUrl">{{ config.baseUrl }}</div>
150
154
  </div>
151
155
  </div>
152
156
  <div class="card-trailing">
@@ -38,6 +38,27 @@
38
38
  {{ t('config.addProvider') }}
39
39
  </button>
40
40
 
41
+ <!-- 服务预设 -->
42
+ <div class="selector-section" v-if="isCodexConfigMode && codexProviderTemplates.length">
43
+ <div class="selector-header">
44
+ <span class="selector-title">{{ t('config.providerTemplate.title') }}</span>
45
+ </div>
46
+ <div class="btn-group" style="flex-wrap: wrap; gap: 8px;">
47
+ <button
48
+ v-for="tpl in codexProviderTemplates"
49
+ :key="tpl.name"
50
+ type="button"
51
+ class="btn-mini"
52
+ @click="newProvider.name = tpl.name;
53
+ newProvider.url = tpl.url;
54
+ newProvider._suggestedModel = tpl.model || '';
55
+ newProvider.useTransform = !!tpl.useTransform;
56
+ showAddModal = true">
57
+ {{ tpl.label }}
58
+ </button>
59
+ </div>
60
+ </div>
61
+
41
62
  <!-- 模型选择器 -->
42
63
  <div class="selector-section">
43
64
  <div class="selector-header">
@@ -55,15 +76,20 @@
55
76
  :disabled="codexModelsLoading"
56
77
  >
57
78
  <option v-if="codexModelsLoading" value="">{{ t('config.modelLoading') }}</option>
58
- <option v-else v-for="model in models" :key="model" :value="model">{{ model }}</option>
79
+ <option v-else v-for="model in codexModelOptions" :key="model" :value="model">{{ model }}</option>
59
80
  </select>
60
81
  <input
61
82
  v-if="!codexModelsLoading && (modelsSource !== 'remote' || !modelsHasCurrent)"
62
83
  class="model-input"
63
84
  v-model="currentModel"
64
85
  @blur="onModelChange"
86
+ @keyup.enter="onModelChange"
65
87
  :placeholder="activeProviderModelPlaceholder"
88
+ :list="codexModelHasList ? 'codex-model-options' : null"
66
89
  >
90
+ <datalist v-if="codexModelHasList" id="codex-model-options">
91
+ <option v-for="model in codexModelOptions" :key="model" :value="model"></option>
92
+ </datalist>
67
93
  <div class="config-template-hint" v-if="modelsSource === 'unlimited'">
68
94
  {{ t('config.models.unlimited') }}
69
95
  </div>
@@ -204,14 +230,17 @@
204
230
  role="button"
205
231
  :aria-current="displayCurrentProvider === provider.name ? 'true' : null">
206
232
  <div class="card-leading">
207
- <div class="card-icon">{{ provider.name.charAt(0).toUpperCase() }}</div>
233
+ <div class="card-icon">{{ provider.name.charAt(0).toUpperCase() }}<span v-if="isTransformProvider(provider)" class="card-icon-dot" title="通过内建转换适配"></span></div>
208
234
  <div class="card-content">
209
235
  <div class="card-title">
210
236
  <span>{{ provider.name }}</span>
211
237
  <span v-if="provider.readOnly" class="provider-readonly-badge">{{ t('config.badge.system') }}</span>
212
238
  </div>
213
- <div class="card-subtitle">
214
- {{ provider.url || t('config.url.unset') }}
239
+ <div class="card-subtitle card-subtitle-model">
240
+ {{ activeProviderModel(provider.name) || t('config.model.unset') }}
241
+ </div>
242
+ <div class="card-subtitle card-subtitle-url">
243
+ {{ displayProviderUrl(provider) || t('config.url.unset') }}
215
244
  </div>
216
245
  </div>
217
246
  </div>
@@ -176,7 +176,7 @@
176
176
  <template v-else>
177
177
  <div class="prompt-editor-head">
178
178
  <div class="prompt-editor-title-row">
179
- <input class="form-input prompt-editor-name" type="text" v-model.trim="promptTemplateDraft.name" :disabled="promptTemplateDraft.isBuiltin" :placeholder="t('plugins.promptTemplates.editor.namePlaceholder')" :aria-label="t('plugins.promptTemplates.editor.nameAria')">
179
+ <input class="form-input prompt-editor-name" type="text" v-model.trim="promptTemplateDraftRaw.name" :disabled="promptTemplateDraft.isBuiltin" :placeholder="t('plugins.promptTemplates.editor.namePlaceholder')" :aria-label="t('plugins.promptTemplates.editor.nameAria')">
180
180
  <div v-if="!promptTemplateDraft.isBuiltin" class="prompt-editor-actions">
181
181
  <button type="button" class="btn-mini" @click="duplicatePromptTemplate" :disabled="pluginsLoading">{{ t('plugins.promptTemplates.editor.duplicate') }}</button>
182
182
  <button type="button" class="btn-mini delete" @click="deletePromptTemplate" :disabled="pluginsLoading">{{ t('plugins.promptTemplates.editor.delete') }}</button>
@@ -189,14 +189,14 @@
189
189
  <div v-if="promptTemplateDraft.isBuiltin && (promptTemplateDraft.createdBy || (promptTemplateDraft.maintainers && promptTemplateDraft.maintainers.length))" class="plugins-panel-note">
190
190
  {{ t('plugins.meta.attribution', { createdBy: promptTemplateDraft.createdBy || '', maintainers: (promptTemplateDraft.maintainers || []).join(', ') }) }}
191
191
  </div>
192
- <input class="form-input" type="text" v-model.trim="promptTemplateDraft.description" :disabled="promptTemplateDraft.isBuiltin" :placeholder="t('plugins.promptTemplates.editor.descPlaceholder')" :aria-label="t('plugins.promptTemplates.editor.descAria')">
192
+ <input class="form-input" type="text" v-model.trim="promptTemplateDraftRaw.description" :disabled="promptTemplateDraft.isBuiltin" :placeholder="t('plugins.promptTemplates.editor.descPlaceholder')" :aria-label="t('plugins.promptTemplates.editor.descAria')">
193
193
  </div>
194
194
 
195
195
  <div class="prompt-editor-body">
196
196
  <label class="form-label">{{ t('plugins.promptTemplates.editor.templateLabel') }}</label>
197
197
  <textarea
198
198
  class="form-input prompt-editor-textarea"
199
- v-model="promptTemplateDraft.template"
199
+ v-model="promptTemplateDraftRaw.template"
200
200
  :disabled="promptTemplateDraft.isBuiltin"
201
201
  rows="10"
202
202
  spellcheck="false"
@@ -210,7 +210,6 @@
210
210
  <div class="plugins-panel-note">{{ t('plugins.promptTemplates.vars.hint') }}</div>
211
211
  </div>
212
212
  <div v-if="!promptTemplateDraft.isBuiltin" class="prompt-editor-actions">
213
- <button type="button" class="btn-mini" @click="addPromptTemplateVariable" :disabled="pluginsLoading">{{ t('plugins.promptTemplates.vars.add') }}</button>
214
213
  <button type="button" class="btn-mini" @click="resetPromptVariableValues" :disabled="pluginsLoading">{{ t('plugins.promptTemplates.vars.reset') }}</button>
215
214
  </div>
216
215
  </div>
@@ -252,28 +251,3 @@
252
251
  style="display:none"
253
252
  @change="handlePromptTemplatesImportChange">
254
253
 
255
- <!-- 新增变量(Prompt Templates) -->
256
- <div v-if="showPromptTemplateVarModal" class="modal-overlay" @click.self="closePromptTemplateVarModal">
257
- <div class="modal" role="dialog" aria-modal="true" aria-labelledby="add-prompt-var-modal-title">
258
- <div class="modal-title" id="add-prompt-var-modal-title">{{ t('plugins.promptTemplates.varModal.title') }}</div>
259
-
260
- <div class="form-group">
261
- <label class="form-label">{{ t('plugins.promptTemplates.varModal.nameLabel') }}</label>
262
- <input
263
- ref="promptTemplateVarNameInput"
264
- v-model.trim="promptTemplateVarDraftName"
265
- :class="['form-input', { invalid: !!promptTemplateVarDraftError }]"
266
- :placeholder="t('placeholder.varNameExample')"
267
- autocomplete="off"
268
- spellcheck="false"
269
- @keydown.enter.prevent="confirmAddPromptTemplateVariable">
270
- <div v-if="promptTemplateVarDraftError" class="form-hint form-error">{{ promptTemplateVarDraftError }}</div>
271
- <div v-else class="form-hint">{{ t('hint.varNameRules') }}</div>
272
- </div>
273
-
274
- <div class="btn-group">
275
- <button class="btn btn-cancel" @click="closePromptTemplateVarModal">{{ t('plugins.promptTemplates.varModal.cancel') }}</button>
276
- <button class="btn btn-confirm" @click="confirmAddPromptTemplateVariable">{{ t('plugins.promptTemplates.varModal.add') }}</button>
277
- </div>
278
- </div>
279
- </div>
@@ -97,16 +97,6 @@
97
97
  </button>
98
98
  </div>
99
99
  </div>
100
- <div class="session-toolbar-footer">
101
- <label class="quick-option">
102
- <input
103
- type="checkbox"
104
- v-model="sessionResumeWithYolo"
105
- @change="onSessionResumeYoloChange"
106
- >
107
- {{ t('sessions.resumeYolo') }}
108
- </label>
109
- </div>
110
100
  <div v-if="hasActiveSessionFilters()" class="session-filter-chips" :aria-label="t('sessions.filters.copyLink')">
111
101
  <button
112
102
  v-for="chip in getSessionFilterChips()"
@@ -7,43 +7,32 @@
7
7
  :aria-labelledby="'tab-settings'">
8
8
  <div class="config-subtabs settings-subtabs" role="tablist" :aria-label="t('settings.tabs.aria')">
9
9
  <button
10
- id="settings-tab-backup"
10
+ id="settings-tab-general"
11
11
  role="tab"
12
- aria-controls="settings-panel-backup"
13
- :aria-selected="settingsTab === 'backup'"
14
- :tabindex="settingsTab === 'backup' ? 0 : -1"
15
- :class="['config-subtab', { active: settingsTab === 'backup' }]"
16
- @click="onSettingsTabClick('backup')">
17
- {{ t('settings.tab.backup') }}
12
+ aria-controls="settings-panel-general"
13
+ :aria-selected="settingsTab === 'general'"
14
+ :tabindex="settingsTab === 'general' ? 0 : -1"
15
+ :class="['config-subtab', { active: settingsTab === 'general' }]"
16
+ @click="onSettingsTabClick('general')">
17
+ {{ t('settings.tab.general') }}
18
18
  </button>
19
19
  <button
20
- id="settings-tab-trash"
20
+ id="settings-tab-data"
21
21
  role="tab"
22
- aria-controls="settings-panel-trash"
23
- :aria-selected="settingsTab === 'trash'"
24
- :tabindex="settingsTab === 'trash' ? 0 : -1"
25
- :class="['config-subtab', { active: settingsTab === 'trash' }]"
26
- @click="onSettingsTabClick('trash')">
27
- {{ t('settings.tab.trash') }}
28
- <span class="settings-tab-badge">{{ sessionTrashCount }}</span>
29
- </button>
30
- <button
31
- id="settings-tab-device"
32
- role="tab"
33
- aria-controls="settings-panel-device"
34
- :aria-selected="settingsTab === 'device'"
35
- :tabindex="settingsTab === 'device' ? 0 : -1"
36
- :class="['config-subtab', { active: settingsTab === 'device' }]"
37
- @click="onSettingsTabClick('device')">
38
- {{ t('settings.tab.device') }}
22
+ aria-controls="settings-panel-data"
23
+ :aria-selected="settingsTab === 'data'"
24
+ :tabindex="settingsTab === 'data' ? 0 : -1"
25
+ :class="['config-subtab', { active: settingsTab === 'data' }]"
26
+ @click="onSettingsTabClick('data')">
27
+ {{ t('settings.tab.data') }}
39
28
  </button>
40
29
  </div>
41
30
 
42
31
  <div
43
- v-show="settingsTab === 'backup'"
44
- id="settings-panel-backup"
32
+ v-show="settingsTab === 'general'"
33
+ id="settings-panel-general"
45
34
  role="tabpanel"
46
- aria-labelledby="settings-tab-backup">
35
+ aria-labelledby="settings-tab-general">
47
36
  <div class="settings-layout">
48
37
  <div class="settings-grid">
49
38
  <section class="settings-card settings-card--wide" :aria-label="t('settings.sharePrefix.title')">
@@ -69,6 +58,35 @@
69
58
  </div>
70
59
  </section>
71
60
 
61
+ <section class="settings-card settings-card--wide" :aria-label="t('settings.templateConfirm.title')">
62
+ <div class="settings-card-header">
63
+ <div class="settings-card-title">{{ t('settings.templateConfirm.title') }}</div>
64
+ <div class="settings-card-meta">{{ t('settings.templateConfirm.meta') }}</div>
65
+ </div>
66
+ <div class="settings-card-body">
67
+ <label class="health-remote-toggle settings-toggle">
68
+ <input
69
+ type="checkbox"
70
+ :checked="configTemplateDiffConfirmEnabled"
71
+ @change="setConfigTemplateDiffConfirmEnabled($event.target.checked)">
72
+ <span>{{ t('settings.templateConfirm.toggle') }}</span>
73
+ </label>
74
+ <div class="settings-card-hint">
75
+ {{ t('settings.templateConfirm.hint') }}
76
+ </div>
77
+ </div>
78
+ </section>
79
+ </div>
80
+ </div>
81
+ </div>
82
+
83
+ <div
84
+ v-show="settingsTab === 'data'"
85
+ id="settings-panel-data"
86
+ role="tabpanel"
87
+ aria-labelledby="settings-tab-data">
88
+ <div class="settings-layout">
89
+ <div class="settings-grid">
72
90
  <section class="settings-card" :aria-label="t('settings.claude.title')">
73
91
  <div class="settings-card-header">
74
92
  <div class="settings-card-title">{{ t('settings.claude.title') }}</div>
@@ -114,17 +132,7 @@
114
132
  @change="handleCodexImportChange">
115
133
  </div>
116
134
  </section>
117
- </div>
118
- </div>
119
- </div>
120
135
 
121
- <div
122
- v-show="settingsTab === 'trash'"
123
- id="settings-panel-trash"
124
- role="tabpanel"
125
- aria-labelledby="settings-tab-trash">
126
- <div class="settings-layout">
127
- <div class="settings-grid">
128
136
  <section class="settings-card settings-card--wide" :aria-label="t('settings.deleteBehavior.title')">
129
137
  <div class="settings-card-header">
130
138
  <div class="settings-card-title">{{ t('settings.deleteBehavior.title') }}</div>
@@ -141,6 +149,22 @@
141
149
  </div>
142
150
  </section>
143
151
 
152
+ <section class="settings-card settings-card--wide" :aria-label="t('settings.trash.retention')">
153
+ <div class="settings-card-header">
154
+ <div class="settings-card-title">{{ t('settings.trash.retention') }}</div>
155
+ <div class="settings-card-meta">{{ t('settings.trash.retentionMeta') }}</div>
156
+ </div>
157
+ <div class="settings-card-body">
158
+ <label class="settings-retention-row">
159
+ <span>{{ t('settings.trash.retentionLabel') }}</span>
160
+ <input type="number" min="1" max="365" :value="sessionTrashRetentionDays" @change="setSessionTrashRetentionDays(Number($event.target.value))" class="settings-retention-input" />
161
+ </label>
162
+ <div class="settings-card-hint">
163
+ {{ t('settings.trash.retentionHint') }}
164
+ </div>
165
+ </div>
166
+ </section>
167
+
144
168
  <section class="settings-card settings-card--wide" :aria-label="t('settings.trash.title')">
145
169
  <div class="settings-card-header settings-card-header-row">
146
170
  <div>
@@ -208,35 +232,6 @@
208
232
  </div>
209
233
  </div>
210
234
  </section>
211
- </div>
212
- </div>
213
- </div>
214
-
215
- <div
216
- v-show="settingsTab === 'device'"
217
- id="settings-panel-device"
218
- role="tabpanel"
219
- aria-labelledby="settings-tab-device">
220
- <div class="settings-layout">
221
- <div class="settings-grid">
222
- <section class="settings-card settings-card--wide" :aria-label="t('settings.templateConfirm.title')">
223
- <div class="settings-card-header">
224
- <div class="settings-card-title">{{ t('settings.templateConfirm.title') }}</div>
225
- <div class="settings-card-meta">{{ t('settings.templateConfirm.meta') }}</div>
226
- </div>
227
- <div class="settings-card-body">
228
- <label class="health-remote-toggle settings-toggle">
229
- <input
230
- type="checkbox"
231
- :checked="configTemplateDiffConfirmEnabled"
232
- @change="setConfigTemplateDiffConfirmEnabled($event.target.checked)">
233
- <span>{{ t('settings.templateConfirm.toggle') }}</span>
234
- </label>
235
- <div class="settings-card-hint">
236
- {{ t('settings.templateConfirm.hint') }}
237
- </div>
238
- </div>
239
- </section>
240
235
 
241
236
  <section class="settings-card settings-card--wide settings-card--danger" :aria-label="t('settings.reset.title')">
242
237
  <div class="settings-card-header">
@@ -96,9 +96,8 @@
96
96
  <div class="usage-daydetail-controls">
97
97
  <select
98
98
  class="usage-daydetail-select"
99
- :value="sessionsUsageSelectedDayKey || ''"
99
+ :value="sessionsUsageSelectedDayKey || (sessionUsageDailyTableRows[0] && sessionUsageDailyTableRows[0].key) || ''"
100
100
  @change="selectSessionsUsageDay(($event.target && $event.target.value) ? String($event.target.value) : '')">
101
- <option value="">{{ t('usage.dayDetail.pick') }}</option>
102
101
  <option v-for="day in sessionUsageDailyTableRows" :key="'day-select-' + day.key" :value="day.key">{{ day.key }}</option>
103
102
  </select>
104
103
  <button
@@ -288,6 +287,36 @@
288
287
  </div>
289
288
  </section>
290
289
 
290
+ <section class="usage-card usage-card-hourly-heatmap">
291
+ <div class="usage-card-title">{{ t('usage.hourlyHeatmap.title') }}</div>
292
+ <div class="usage-card-subtitle">{{ t('usage.hourlyHeatmap.subtitle') }}</div>
293
+ <div class="hourly-heatmap-wrapper">
294
+ <div class="hourly-heatmap-header">
295
+ <div class="hourly-heatmap-corner"></div>
296
+ <div v-for="h in sessionUsageHourlyHeatmap.hourLabels" :key="'hh-' + h" class="hourly-heatmap-hour-label">{{ h }}</div>
297
+ </div>
298
+ <div v-for="(row, dayIndex) in sessionUsageHourlyHeatmap.rows" :key="'hd-' + dayIndex" class="hourly-heatmap-row">
299
+ <div class="hourly-heatmap-weekday-label">{{ row.weekday }}</div>
300
+ <div
301
+ v-for="(cell, hourIndex) in row.cells"
302
+ :key="'hc-' + dayIndex + '-' + hourIndex"
303
+ :class="['hourly-heatmap-cell', 'level-' + cell.level]"
304
+ :title="cell.tooltip"
305
+ :aria-label="cell.tooltip">
306
+ </div>
307
+ </div>
308
+ <div class="hourly-heatmap-legend">
309
+ <span class="hourly-heatmap-legend-label">{{ t('usage.hourlyHeatmap.legend.less') }}</span>
310
+ <span class="hourly-heatmap-cell level-0"></span>
311
+ <span class="hourly-heatmap-cell level-1"></span>
312
+ <span class="hourly-heatmap-cell level-2"></span>
313
+ <span class="hourly-heatmap-cell level-3"></span>
314
+ <span class="hourly-heatmap-cell level-4"></span>
315
+ <span class="hourly-heatmap-legend-label">{{ t('usage.hourlyHeatmap.legend.more') }}</span>
316
+ </div>
317
+ </div>
318
+ </section>
319
+
291
320
  <section class="usage-card usage-card-top-paths">
292
321
  <div class="usage-card-title">{{ t('usage.paths.title') }}</div>
293
322
  <div v-if="!sessionUsageCharts.topPaths.length" class="usage-list-value">{{ t('usage.paths.empty') }}</div>