codexmate 0.0.33 → 0.0.36

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 (47) hide show
  1. package/cli/agents-files.js +6 -0
  2. package/cli/archive-helpers.js +11 -4
  3. package/cli/local-bridge.js +9 -4
  4. package/cli/openai-bridge.js +1 -1
  5. package/cli/update.js +11 -2
  6. package/cli.js +133 -64
  7. package/lib/cli-webhook.js +29 -1
  8. package/package.json +2 -1
  9. package/web-ui/app.js +37 -2
  10. package/web-ui/index.html +2 -1
  11. package/web-ui/logic.claude.mjs +4 -0
  12. package/web-ui/logic.sessions.mjs +6 -5
  13. package/web-ui/modules/app.computed.dashboard.mjs +4 -0
  14. package/web-ui/modules/app.computed.session.mjs +147 -6
  15. package/web-ui/modules/app.methods.claude-config.mjs +41 -0
  16. package/web-ui/modules/app.methods.codex-config.mjs +11 -3
  17. package/web-ui/modules/app.methods.navigation.mjs +32 -2
  18. package/web-ui/modules/app.methods.session-browser.mjs +12 -6
  19. package/web-ui/modules/app.methods.session-trash.mjs +30 -0
  20. package/web-ui/modules/app.methods.startup-claude.mjs +9 -0
  21. package/web-ui/modules/app.methods.webhook.mjs +8 -0
  22. package/web-ui/modules/i18n.dict.mjs +8 -0
  23. package/web-ui/modules/sessions-filters-url.mjs +65 -12
  24. package/web-ui/modules/skills.methods.mjs +31 -0
  25. package/web-ui/partials/index/layout-header.html +17 -12
  26. package/web-ui/partials/index/modal-webhook.html +42 -0
  27. package/web-ui/partials/index/modals-basic.html +50 -0
  28. package/web-ui/partials/index/panel-config-claude.html +13 -22
  29. package/web-ui/partials/index/panel-config-codex.html +8 -22
  30. package/web-ui/partials/index/panel-market.html +76 -149
  31. package/web-ui/partials/index/panel-sessions.html +2 -2
  32. package/web-ui/partials/index/panel-settings.html +119 -149
  33. package/web-ui/partials/index/panel-usage.html +115 -68
  34. package/web-ui/res/vue.runtime.global.prod.js +7 -0
  35. package/web-ui/res/web-ui-render.precompiled.js +7274 -0
  36. package/web-ui/session-helpers.mjs +15 -4
  37. package/web-ui/source-bundle.cjs +73 -1
  38. package/web-ui/styles/base-theme.css +10 -0
  39. package/web-ui/styles/bridge-pool.css +69 -0
  40. package/web-ui/styles/layout-shell.css +66 -27
  41. package/web-ui/styles/navigation-panels.css +8 -0
  42. package/web-ui/styles/responsive.css +50 -9
  43. package/web-ui/styles/sessions-usage.css +336 -319
  44. package/web-ui/styles/settings-panel.css +300 -234
  45. package/web-ui/styles/skills-market.css +294 -0
  46. package/web-ui/styles/titles-cards.css +14 -0
  47. package/web-ui/styles/webhook.css +38 -4
package/web-ui/index.html CHANGED
@@ -6,7 +6,7 @@
6
6
  <title>Codex Mate</title>
7
7
  <link rel="icon" type="image/webp" href="/res/logo-pack.webp">
8
8
  <link rel="apple-touch-icon" href="/res/logo-pack.webp">
9
- <script src="/res/vue.global.prod.js"></script>
9
+ <script src="/res/vue.runtime.global.prod.js"></script>
10
10
  <script src="/res/json5.min.js"></script>
11
11
  <link rel="stylesheet" href="/web-ui/styles.css">
12
12
  </head>
@@ -26,6 +26,7 @@
26
26
  <!-- @include ./partials/index/panel-plugins.html -->
27
27
  <!-- @include ./partials/index/layout-footer.html -->
28
28
  <!-- @include ./partials/index/modals-basic.html -->
29
+ <!-- @include ./partials/index/modal-webhook.html -->
29
30
  <!-- @include ./partials/index/modal-openclaw-config.html -->
30
31
  <!-- @include ./partials/index/modal-config-template-agents.html -->
31
32
  <!-- @include ./partials/index/modal-skills.html -->
@@ -111,6 +111,10 @@ export function matchClaudeConfigFromSettings(claudeConfigs = {}, env = {}) {
111
111
  if (!normalizedSettings.baseUrl || !normalizedSettings.model || !hasClaudeCredential(normalizedSettings)) {
112
112
  return '';
113
113
  }
114
+ // 检测本地桥接 URL
115
+ if (typeof normalizedSettings.baseUrl === 'string' && normalizedSettings.baseUrl.includes('/bridge/claude-local/')) {
116
+ return 'claude-local';
117
+ }
114
118
  const comparableSettingsUrl = normalizeClaudeComparableUrl(normalizedSettings.baseUrl);
115
119
  const entries = Object.entries(claudeConfigs || {});
116
120
  for (const [name, config] of entries) {
@@ -180,12 +180,9 @@ export function formatSessionTimelineTimestamp(timestamp) {
180
180
  if (!value) return '';
181
181
 
182
182
  const matched = value.match(/^(\d{4})-(\d{2})-(\d{2})[T\s](\d{2}):(\d{2})(?::(\d{2}))?/);
183
- if (matched) {
184
- const second = matched[6] || '00';
185
- return `${matched[2]}-${matched[3]} ${matched[4]}:${matched[5]}:${second}`;
186
- }
183
+ if (!matched) return value;
187
184
 
188
- return value;
185
+ return `${matched[1]}-${matched[2]}-${matched[3]} ${matched[4]}:${matched[5]}`;
189
186
  }
190
187
 
191
188
  function normalizeUsageRange(range) {
@@ -549,11 +546,15 @@ export function buildUsageChartGroups(sessions = [], options = {}) {
549
546
  String(messageCount),
550
547
  String(sessionIndex)
551
548
  ].join(':'),
549
+ sessionId: session.sessionId || '',
550
+ filePath: session.filePath || '',
552
551
  title: normalizedTitle,
553
552
  source,
554
553
  sourceLabel,
555
554
  cwd,
556
555
  messageCount,
556
+ totalTokens: sessionTotalTokens,
557
+ contextWindow: sessionContextWindow,
557
558
  updatedAt: session.updatedAt || '',
558
559
  updatedAtMs,
559
560
  updatedAtLabel: formatSessionTimelineTimestamp(session.updatedAt || ''),
@@ -92,6 +92,10 @@ export function createDashboardComputed() {
92
92
  return list;
93
93
  },
94
94
 
95
+ isLocalProviderDisabled() {
96
+ return this.configMode === 'codex';
97
+ },
98
+
95
99
  displayProviderUrl() {
96
100
  return (provider) => {
97
101
  if (provider && provider.name === 'local') return '';
@@ -602,11 +602,10 @@ export function createSessionComputed() {
602
602
  const sessions = this.sessionUsageCharts && Array.isArray(this.sessionUsageCharts.filteredSessions)
603
603
  ? this.sessionUsageCharts.filteredSessions
604
604
  : this.sessionsUsageList;
605
- const compareEnabled = this.sessionsUsageCompareEnabled === true && this.sessionsUsageTimeRange !== 'all';
606
605
  const rangeDays = this.sessionsUsageTimeRange === '30d' ? 30 : 7;
607
606
  const dayMs = 24 * 60 * 60 * 1000;
608
607
  const baseMs = Date.parse(`${dayKey}T00:00:00.000Z`);
609
- const prevKey = compareEnabled && Number.isFinite(baseMs)
608
+ const prevKey = Number.isFinite(baseMs)
610
609
  ? new Date(baseMs - (rangeDays * dayMs)).toISOString().slice(0, 10)
611
610
  : '';
612
611
  let sessionCount = 0;
@@ -636,7 +635,7 @@ export function createSessionComputed() {
636
635
  } else if (isPrev) {
637
636
  prevTokenTotal += sessionTokens;
638
637
  }
639
- const model = typeof session.model === 'string' ? session.model.trim() : '';
638
+ const model = typeof session.model === 'string' ? session.model.trim() : '';
640
639
  if (isCurrent && model) {
641
640
  modelMap.set(model, (modelMap.get(model) || 0) + 1);
642
641
  }
@@ -667,20 +666,162 @@ export function createSessionComputed() {
667
666
  }));
668
667
  return {
669
668
  dayKey,
670
- compareEnabled,
671
669
  prevKey,
672
670
  sessionCount,
673
671
  messageCount,
674
672
  tokenTotal,
675
673
  tokenLabel: formatUsageSummaryNumber(tokenTotal),
676
674
  prevTokenTotal,
677
- prevTokenLabel: compareEnabled ? formatUsageSummaryNumber(prevTokenTotal) : '0',
678
- deltaTokenLabel: compareEnabled ? formatSignedUsageSummaryNumber(tokenTotal - prevTokenTotal) : '0',
675
+ prevTokenLabel: prevKey ? formatUsageSummaryNumber(prevTokenTotal) : null,
676
+ deltaTokenLabel: prevKey ? formatSignedUsageSummaryNumber(tokenTotal - prevTokenTotal) : null,
679
677
  topSessions,
680
678
  topModels
681
679
  };
682
680
  },
683
681
 
682
+ usageHeroMainValue() {
683
+ const summary = this.sessionUsageCharts && this.sessionUsageCharts.summary
684
+ ? this.sessionUsageCharts.summary
685
+ : null;
686
+ if (!summary) return '0';
687
+ return formatCompactUsageSummaryNumber(summary.totalTokens || 0);
688
+ },
689
+
690
+ usageHeroSubLabel() {
691
+ const summary = this.sessionUsageCharts && this.sessionUsageCharts.summary
692
+ ? this.sessionUsageCharts.summary
693
+ : null;
694
+ if (!summary) return '';
695
+ const t = typeof this.t === 'function' ? this.t : null;
696
+ const sessionCount = summary.totalSessions || 0;
697
+ const rangeLabel = this.sessionsUsageTimeRange === '30d' ? '30天' : (this.sessionsUsageTimeRange === 'all' ? '全部' : '7天');
698
+ const rangeText = t ? t('usage.range.' + this.sessionsUsageTimeRange) : rangeLabel;
699
+ return `${formatUsageSummaryNumber(sessionCount)} sessions · ${rangeText}`;
700
+ },
701
+
702
+ usageHeroDelta() {
703
+ const range = this.sessionsUsageTimeRange;
704
+ if (range === 'all') return null;
705
+ const summary = this.sessionUsageCharts && this.sessionUsageCharts.summary
706
+ ? this.sessionUsageCharts.summary
707
+ : null;
708
+ if (!summary || !summary.totalTokens) return null;
709
+
710
+ const rangeDays = range === '30d' ? 30 : 7;
711
+ const dayMs = 24 * 60 * 60 * 1000;
712
+ const nowMs = Date.now();
713
+ const prevStartMs = nowMs - (rangeDays * 2 * dayMs);
714
+ const prevEndMs = nowMs - (rangeDays * dayMs);
715
+
716
+ let prevTokens = 0;
717
+ for (const session of (Array.isArray(this.sessionsUsageList) ? this.sessionsUsageList : [])) {
718
+ if (!session || typeof session !== 'object') continue;
719
+ const updatedAtMs = Date.parse(session.updatedAt || '');
720
+ if (!Number.isFinite(updatedAtMs)) continue;
721
+ if (updatedAtMs >= prevStartMs && updatedAtMs < prevEndMs) {
722
+ const sessionTokens = Number.isFinite(Number(session.totalTokens))
723
+ ? Math.max(0, Math.floor(Number(session.totalTokens)))
724
+ : 0;
725
+ prevTokens += sessionTokens;
726
+ }
727
+ }
728
+
729
+ if (prevTokens === 0) return null;
730
+ const currentTokens = summary.totalTokens;
731
+ const delta = currentTokens - prevTokens;
732
+ const deltaPercent = prevTokens > 0 ? Math.round((delta / prevTokens) * 100) : 0;
733
+ const arrow = delta > 0 ? '↑' : (delta < 0 ? '↓' : '–');
734
+ const sign = delta >= 0 ? '+' : '';
735
+ return `${arrow} ${sign}${deltaPercent}%`;
736
+ },
737
+
738
+ usageHeroDeltaClass() {
739
+ const summary = this.sessionUsageCharts && this.sessionUsageCharts.summary
740
+ ? this.sessionUsageCharts.summary
741
+ : null;
742
+ if (!summary || !summary.totalTokens) return '';
743
+
744
+ const range = this.sessionsUsageTimeRange;
745
+ if (range === 'all') return '';
746
+
747
+ const rangeDays = range === '30d' ? 30 : 7;
748
+ const dayMs = 24 * 60 * 60 * 1000;
749
+ const nowMs = Date.now();
750
+ const prevStartMs = nowMs - (rangeDays * 2 * dayMs);
751
+ const prevEndMs = nowMs - (rangeDays * dayMs);
752
+
753
+ let prevTokens = 0;
754
+ for (const session of (Array.isArray(this.sessionsUsageList) ? this.sessionsUsageList : [])) {
755
+ if (!session || typeof session !== 'object') continue;
756
+ const updatedAtMs = Date.parse(session.updatedAt || '');
757
+ if (!Number.isFinite(updatedAtMs)) continue;
758
+ if (updatedAtMs >= prevStartMs && updatedAtMs < prevEndMs) {
759
+ const sessionTokens = Number.isFinite(Number(session.totalTokens))
760
+ ? Math.max(0, Math.floor(Number(session.totalTokens)))
761
+ : 0;
762
+ prevTokens += sessionTokens;
763
+ }
764
+ }
765
+
766
+ if (prevTokens === 0) return '';
767
+ const currentTokens = summary.totalTokens;
768
+ return currentTokens >= prevTokens ? 'delta-up' : 'delta-down';
769
+ },
770
+
771
+ sessionsUsageSelectedDay() {
772
+ return this.sessionsUsageSelectedDayKey || '';
773
+ },
774
+
775
+ sessionUsageWave() {
776
+ const daily = this.sessionUsageDaily && typeof this.sessionUsageDaily === 'object'
777
+ ? this.sessionUsageDaily
778
+ : null;
779
+ if (!daily || !Array.isArray(daily.rows) || daily.rows.length === 0) {
780
+ return { points: [], labels: [], linePath: '', areaPath: '', width: 800, maxTokens: 0 };
781
+ }
782
+
783
+ const rows = daily.rows;
784
+ const maxTokens = daily.maxTokens || 1;
785
+ const width = 800;
786
+ const height = 140;
787
+ const padding = { top: 10, bottom: 30, left: 0, right: 0 };
788
+ const chartWidth = width - padding.left - padding.right;
789
+ const chartHeight = height - padding.top - padding.bottom;
790
+
791
+ const points = rows.map((row, index) => {
792
+ const x = padding.left + (index / (rows.length - 1 || 1)) * chartWidth;
793
+ const normalizedValue = maxTokens > 0 ? (row.tokenTotal / maxTokens) : 0;
794
+ const y = padding.top + chartHeight - (normalizedValue * chartHeight);
795
+ return { x, y, key: row.key, value: row.tokenTotal, label: row.label };
796
+ });
797
+
798
+ const linePath = points.length > 1
799
+ ? `M ${points.map(p => `${p.x.toFixed(1)},${p.y.toFixed(1)}`).join(' L ')}`
800
+ : '';
801
+
802
+ const areaPath = points.length > 1
803
+ ? `${linePath} L ${points[points.length - 1].x.toFixed(1)},${(padding.top + chartHeight).toFixed(1)} L ${points[0].x.toFixed(1)},${(padding.top + chartHeight).toFixed(1)} Z`
804
+ : '';
805
+
806
+ const selectedKey = this.sessionsUsageSelectedDayKey;
807
+ const selectedPoint = points.find(p => p.key === selectedKey) || points[points.length - 1] || null;
808
+
809
+ return {
810
+ points,
811
+ labels: rows.map((row, index) => ({
812
+ key: row.key,
813
+ text: row.label
814
+ })),
815
+ linePath,
816
+ areaPath,
817
+ width,
818
+ height,
819
+ maxTokens,
820
+ hoverX: selectedPoint ? selectedPoint.x : 0,
821
+ hoverY: selectedPoint ? selectedPoint.y : 0
822
+ };
823
+ },
824
+
684
825
  visibleSessionTrashItems() {
685
826
  const items = Array.isArray(this.sessionTrashItems) ? this.sessionTrashItems : [];
686
827
  const visibleCount = Number(this.sessionTrashVisibleCount);
@@ -264,6 +264,47 @@ export function createClaudeConfigMethods(options = {}) {
264
264
 
265
265
  claudeLocalBridgeConfigured() {
266
266
  return this.claudeLocalBridgeCandidateProviders().some(p => p.hasKey);
267
+ },
268
+
269
+ isClaudeLocalBridgeDisabled() {
270
+ return this.configMode === 'claude';
271
+ },
272
+
273
+ async applyClaudeLocalBridge() {
274
+ this.currentClaudeConfig = 'claude-local';
275
+ try { localStorage.setItem('currentClaudeConfig', 'claude-local'); } catch (_) {}
276
+ this.refreshClaudeModelContext();
277
+
278
+ const candidates = this.claudeLocalBridgeCandidateProviders();
279
+ if (candidates.length === 0) {
280
+ return this.showMessage('请先添加并配置至少一个 Claude 提供商', 'error');
281
+ }
282
+
283
+ try {
284
+ const res = await api('claude-local-bridge-toggle', { enable: true });
285
+ if (res.error) {
286
+ this.showMessage(res.error || '启用本地负载均衡失败', 'error');
287
+ return;
288
+ }
289
+ this.showMessage('Claude 本地负载均衡已启用', 'success');
290
+ } catch (e) {
291
+ this.showMessage('启用本地负载均衡失败', 'error');
292
+ }
293
+ },
294
+
295
+ async openClaudeConfigTemplateEditor() {
296
+ try {
297
+ const res = await api('get-claude-settings-raw');
298
+ if (res.error) {
299
+ this.showMessage(res.error, 'error');
300
+ return;
301
+ }
302
+ this.configTemplateContent = res.content || '{}';
303
+ this.configTemplateContext = 'claude';
304
+ this.showConfigTemplateModal = true;
305
+ } catch (e) {
306
+ this.showMessage('加载 Claude settings 失败', 'error');
307
+ }
267
308
  }
268
309
  };
269
310
  }
@@ -558,6 +558,7 @@ export function createCodexConfigMethods(options = {}) {
558
558
  template = `${template.trimEnd()}\n\n${appendBlock}\n`;
559
559
  }
560
560
  this.configTemplateContent = template;
561
+ this.configTemplateContext = 'codex';
561
562
  this.showConfigTemplateModal = true;
562
563
  } catch (e) {
563
564
  this.showMessage('加载模板失败', 'error');
@@ -807,9 +808,16 @@ export function createCodexConfigMethods(options = {}) {
807
808
  const performApply = async () => {
808
809
  this.configTemplateApplying = true;
809
810
  try {
810
- const res = await api('apply-config-template', {
811
- template: this.configTemplateContent
812
- });
811
+ let res;
812
+ if (this.configTemplateContext === 'claude') {
813
+ res = await api('apply-claude-settings-raw', {
814
+ content: this.configTemplateContent
815
+ });
816
+ } else {
817
+ res = await api('apply-config-template', {
818
+ template: this.configTemplateContent
819
+ });
820
+ }
813
821
  if (res.error) {
814
822
  this.showMessage(res.error, 'error');
815
823
  return;
@@ -56,6 +56,32 @@
56
56
  return null;
57
57
  }
58
58
  };
59
+
60
+ const canonicalizeWebUiRuntimeUrl = () => {
61
+ if (typeof window === 'undefined' || !window.location) return;
62
+ try {
63
+ const url = new URL(window.location.href);
64
+ if (url.pathname === '/session') return;
65
+ let pathname = url.pathname;
66
+ let previousPathname = '';
67
+ do {
68
+ previousPathname = pathname;
69
+ pathname = pathname.replace(/\/+web-ui\/+web-ui\/+/, '/web-ui/');
70
+ } while (pathname !== previousPathname);
71
+ if (pathname === '/web-ui' || pathname === '/web-ui/' || pathname === '/web-ui/index.html') {
72
+ pathname = '/';
73
+ }
74
+ url.pathname = pathname;
75
+ url.search = '';
76
+ url.hash = '';
77
+ const nextUrl = url.toString();
78
+ if (nextUrl === window.location.href) return;
79
+ if (window.history && typeof window.history.replaceState === 'function') {
80
+ window.history.replaceState(null, '', nextUrl);
81
+ }
82
+ } catch (_) {}
83
+ };
84
+
59
85
  const persistNavState = (vm, overrides = null) => {
60
86
  if (!vm || vm.__navStateRestoring) return;
61
87
  if (typeof localStorage === 'undefined') return;
@@ -138,6 +164,7 @@
138
164
  const normalizedMode = typeof mode === 'string'
139
165
  ? mode.trim().toLowerCase()
140
166
  : '';
167
+ canonicalizeWebUiRuntimeUrl();
141
168
  this.cancelTouchNavIntentReset();
142
169
  if (typeof this.ensureMainTabSwitchState === 'function') {
143
170
  this.ensureMainTabSwitchState().pendingConfigMode = '';
@@ -412,6 +439,7 @@
412
439
  : '';
413
440
  const targetTab = normalizedTab || tab;
414
441
  if (!targetTab) return;
442
+ canonicalizeWebUiRuntimeUrl();
415
443
  if (targetTab === 'orchestration' && this.taskOrchestrationTabEnabled !== true) {
416
444
  return this.switchMainTab('config');
417
445
  }
@@ -419,6 +447,7 @@
419
447
  mainTab: targetTab,
420
448
  configMode: targetTab === 'config' ? this.configMode : this.configMode
421
449
  });
450
+ // URL 保持静态,不写入任何状态
422
451
  this.cancelTouchNavIntentReset();
423
452
  if (targetTab === 'sessions') {
424
453
  this.cancelScheduledSessionTabDeferredTeardown();
@@ -460,9 +489,10 @@
460
489
  return;
461
490
  }
462
491
  const isLeavingSessions = previousTab === 'sessions' && targetTab !== 'sessions';
463
- const shouldDeferApply = isLeavingSessions;
492
+ const shouldPreserveSessionRender = isLeavingSessions && this.preserveSessionRenderOnTabLeave === true;
493
+ const shouldDeferApply = isLeavingSessions && !shouldPreserveSessionRender;
464
494
  if (isLeavingSessions && !this.isSessionPanelFastHidden()) {
465
- this.setSessionPanelFastHidden(true);
495
+ this.setSessionPanelFastHidden(!shouldPreserveSessionRender);
466
496
  }
467
497
  if (shouldDeferApply && typeof this.suspendSessionTabRender === 'function') {
468
498
  this.suspendSessionTabRender();
@@ -264,6 +264,13 @@ export function createSessionBrowserMethods(options = {}) {
264
264
  });
265
265
  if (urlState) {
266
266
  applySessionsFilterUrlState(this, urlState);
267
+ // 清理 URL,保持静态
268
+ try {
269
+ const url = new URL(window.location.href);
270
+ url.search = '';
271
+ url.hash = '';
272
+ window.history.replaceState(null, '', url.toString());
273
+ } catch (_) {}
267
274
  try {
268
275
  const sortCache = localStorage.getItem('codexmateSessionSortMode');
269
276
  this.sessionSortMode = normalizeSortMode(sortCache);
@@ -479,14 +486,13 @@ export function createSessionBrowserMethods(options = {}) {
479
486
  if (typeof text !== 'string' || !text) return text;
480
487
  var tokens = this.queryTokens;
481
488
  if (!tokens || tokens.length === 0) return text;
482
- var result = text;
489
+ var escaped = text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
483
490
  for (var i = 0; i < tokens.length; i++) {
484
- var token = tokens[i];
485
- var escaped = token.replace(/[.*+?^${}()|[\]\\]/g, '\\ async onSessionSourceChange(event) {');
486
- var re = new RegExp('(' + escaped + ')', 'gi');
487
- result = result.replace(re, '<mark>$1</mark>');
491
+ var token = tokens[i].replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
492
+ var re = new RegExp('(' + token + ')', 'gi');
493
+ escaped = escaped.replace(re, '<mark>$1</mark>');
488
494
  }
489
- return result;
495
+ return escaped;
490
496
  },
491
497
 
492
498
  async onSessionSourceChange(event) {
@@ -218,6 +218,36 @@ export function createSessionTrashMethods(options = {}) {
218
218
  await this.switchSettingsTab(tab);
219
219
  },
220
220
 
221
+ async onSettingsTabKeydown(event, tab) {
222
+ if (!event) {
223
+ return;
224
+ }
225
+ const tabs = ['general', 'data'];
226
+ const currentTab = this.normalizeSettingsTab(tab || this.settingsTab);
227
+ const currentIndex = Math.max(0, tabs.indexOf(currentTab));
228
+ let nextIndex = currentIndex;
229
+ if (event.key === 'ArrowRight' || event.key === 'ArrowDown') {
230
+ nextIndex = (currentIndex + 1) % tabs.length;
231
+ } else if (event.key === 'ArrowLeft' || event.key === 'ArrowUp') {
232
+ nextIndex = (currentIndex - 1 + tabs.length) % tabs.length;
233
+ } else if (event.key === 'Home') {
234
+ nextIndex = 0;
235
+ } else if (event.key === 'End') {
236
+ nextIndex = tabs.length - 1;
237
+ } else {
238
+ return;
239
+ }
240
+ event.preventDefault();
241
+ const nextTab = tabs[nextIndex];
242
+ await this.switchSettingsTab(nextTab);
243
+ const target = typeof document !== 'undefined'
244
+ ? document.getElementById(`settings-tab-${nextTab}`)
245
+ : null;
246
+ if (target && typeof target.focus === 'function') {
247
+ target.focus();
248
+ }
249
+ },
250
+
221
251
  async switchSettingsTab(tab, options = {}) {
222
252
  const nextTab = this.normalizeSettingsTab(tab);
223
253
  this.settingsTab = nextTab;
@@ -383,6 +383,15 @@ export function createStartupClaudeMethods(options = {}) {
383
383
  },
384
384
 
385
385
  syncClaudeModelFromConfig() {
386
+ if (this.currentClaudeConfig === 'claude-local') {
387
+ const candidates = this.claudeLocalBridgeCandidateProviders
388
+ ? this.claudeLocalBridgeCandidateProviders()
389
+ : [];
390
+ const active = candidates.find(cp => !this.isClaudeLocalBridgeExcluded(cp.name));
391
+ this.currentClaudeModel = active && active.model ? active.model : '';
392
+ this.claudeCustomModelDraft = this.currentClaudeModel;
393
+ return;
394
+ }
386
395
  const config = this.getCurrentClaudeConfig();
387
396
  this.currentClaudeModel = config && config.model ? config.model : '';
388
397
  this.claudeCustomModelDraft = this.currentClaudeModel;
@@ -2,6 +2,14 @@ import { api } from './api.mjs';
2
2
 
3
3
  export function createWebhookMethods() {
4
4
  return {
5
+ openWebhookModal() {
6
+ this.showWebhookModal = true;
7
+ },
8
+
9
+ closeWebhookModal() {
10
+ this.showWebhookModal = false;
11
+ },
12
+
5
13
  async loadWebhookSettings() {
6
14
  try {
7
15
  const data = await api('get-webhook-config');
@@ -532,6 +532,7 @@ const DICT = Object.freeze({
532
532
  'sessions.loadingList': '会话加载中...',
533
533
  'sessions.empty': '暂无可用会话记录',
534
534
  'sessions.unknownTime': '未知时间',
535
+
535
536
  'sessions.query.placeholder.enabled': '关键词检索(支持 Codex/Claude/Gemini/CodeBuddy,例:claude code)',
536
537
  'sessions.query.placeholder.disabled': '当前来源暂不支持关键词检索',
537
538
  'sessions.pin': '置顶',
@@ -910,6 +911,7 @@ const DICT = Object.freeze({
910
911
  'settings.tab.general': '通用',
911
912
  'settings.tab.data': '数据',
912
913
  'settings.tabs.aria': '设置分类',
914
+ 'settings.quickSettings.title': '快捷设置',
913
915
  'settings.sharePrefix.title': '分享命令前缀',
914
916
  'settings.sharePrefix.meta': '影响 Web UI 里“复制分享命令”的前缀',
915
917
  'settings.sharePrefix.label': '前缀',
@@ -1601,6 +1603,8 @@ const DICT = Object.freeze({
1601
1603
  'sessions.loadingList': 'セッション一覧を読み込み中...',
1602
1604
  'sessions.empty': 'セッションがありません',
1603
1605
  'sessions.unknownTime': '不明な時間',
1606
+
1607
+
1604
1608
  'sessions.query.placeholder.enabled': 'セッションを検索...',
1605
1609
  'sessions.query.placeholder.disabled': '現在のソースでは検索は利用できません',
1606
1610
  'sessions.pin': 'ピン留め',
@@ -1965,6 +1969,7 @@ const DICT = Object.freeze({
1965
1969
  'settings.tab.general': '一般',
1966
1970
  'settings.tab.data': 'データ',
1967
1971
  'settings.tabs.aria': '設定カテゴリ',
1972
+ 'settings.quickSettings.title': 'クイック設定',
1968
1973
  'settings.sharePrefix.title': '共有コマンドプレフィックス',
1969
1974
  'settings.sharePrefix.meta': 'Web UI の「共有コマンドをコピー」のプレフィックスに影響',
1970
1975
  'settings.sharePrefix.label': 'プレフィックス',
@@ -2656,6 +2661,8 @@ const DICT = Object.freeze({
2656
2661
  'sessions.loadingList': 'Loading sessions...',
2657
2662
  'sessions.empty': 'No sessions found',
2658
2663
  'sessions.unknownTime': 'unknown time',
2664
+
2665
+
2659
2666
  'sessions.query.placeholder.enabled': 'Search keywords (Codex/Claude/Gemini/CodeBuddy, e.g. claude code)',
2660
2667
  'sessions.query.placeholder.disabled': 'Keyword search is not available for this source',
2661
2668
  'sessions.pin': 'Pin',
@@ -3034,6 +3041,7 @@ const DICT = Object.freeze({
3034
3041
  'settings.tab.general': 'General',
3035
3042
  'settings.tab.data': 'Data',
3036
3043
  'settings.tabs.aria': 'Settings categories',
3044
+ 'settings.quickSettings.title': 'Quick Settings',
3037
3045
  'settings.sharePrefix.title': 'Share command prefix',
3038
3046
  'settings.sharePrefix.meta': 'Used as the prefix for “Copy share command” in the Web UI',
3039
3047
  'settings.sharePrefix.label': 'Prefix',
@@ -55,20 +55,76 @@ export function applySessionsFilterUrlState(vm, state) {
55
55
  }
56
56
  }
57
57
 
58
+ export function canonicalizeWebUiUrl(url) {
59
+ if (!url || typeof url !== 'object') return url;
60
+ if (url.pathname === '/web-ui/index.html') {
61
+ url.pathname = '/';
62
+ }
63
+ return url;
64
+ }
65
+
66
+ function canonicalizeWebUiHistoryUrl(value) {
67
+ if (typeof window === 'undefined' || !window.location) return value;
68
+ if (typeof value === 'undefined' || value === null) return value;
69
+ try {
70
+ const url = canonicalizeWebUiUrl(new URL(String(value), window.location.href));
71
+ return url && url.pathname === '/' ? url.href : value;
72
+ } catch (_) {
73
+ return value;
74
+ }
75
+ }
76
+
77
+ export function normalizeCurrentWebUiUrl() {
78
+ try {
79
+ const url = canonicalizeWebUiUrl(new URL(window.location.href));
80
+ if (url && url.href !== window.location.href) {
81
+ window.history.replaceState(null, '', url.href);
82
+ }
83
+ return url;
84
+ } catch (_) {
85
+ return null;
86
+ }
87
+ }
88
+
89
+ export function installWebUiUrlCanonicalization() {
90
+ if (typeof window === 'undefined' || !window.history) return false;
91
+ if (window.__codexmateWebUiUrlCanonicalizationInstalled) {
92
+ normalizeCurrentWebUiUrl();
93
+ return true;
94
+ }
95
+ try {
96
+ const originalReplaceState = window.history.replaceState;
97
+ const originalPushState = window.history.pushState;
98
+ if (typeof originalReplaceState === 'function') {
99
+ window.history.replaceState = function replaceState(state, title, url) {
100
+ return originalReplaceState.call(this, state, title, canonicalizeWebUiHistoryUrl(url));
101
+ };
102
+ }
103
+ if (typeof originalPushState === 'function') {
104
+ window.history.pushState = function pushState(state, title, url) {
105
+ return originalPushState.call(this, state, title, canonicalizeWebUiHistoryUrl(url));
106
+ };
107
+ }
108
+ window.__codexmateWebUiUrlCanonicalizationInstalled = true;
109
+ normalizeCurrentWebUiUrl();
110
+ return true;
111
+ } catch (_) {
112
+ normalizeCurrentWebUiUrl();
113
+ return false;
114
+ }
115
+ }
116
+
58
117
  export function buildSessionsFilterShareUrl(vm) {
59
118
  try {
60
- const url = new URL(window.location.href);
61
- if (url.pathname === '/session') return '';
119
+ // 使用干净的根路径作为基础 URL,避免把 /web-ui/index.html 或 /session 带进分享链接。
120
+ const baseUrl = window.location.origin + '/';
121
+ const url = canonicalizeWebUiUrl(new URL(baseUrl));
122
+ url.searchParams.set('tab', 'sessions');
62
123
  url.searchParams.set('s_source', String(vm.sessionFilterSource || 'all'));
63
124
  if (vm.sessionPathFilter) url.searchParams.set('s_path', String(vm.sessionPathFilter || ''));
64
- else url.searchParams.delete('s_path');
65
125
  if (vm.sessionQuery && isSessionQueryEnabled(vm.sessionFilterSource)) url.searchParams.set('s_query', String(vm.sessionQuery || ''));
66
- else url.searchParams.delete('s_query');
67
126
  if (vm.sessionRoleFilter && vm.sessionRoleFilter !== 'all') url.searchParams.set('s_role', String(vm.sessionRoleFilter || 'all'));
68
- else url.searchParams.delete('s_role');
69
127
  if (vm.sessionTimePreset && vm.sessionTimePreset !== 'all') url.searchParams.set('s_time', String(vm.sessionTimePreset || 'all'));
70
- else url.searchParams.delete('s_time');
71
- url.searchParams.set('tab', 'sessions');
72
128
  return url.toString();
73
129
  } catch (_) {
74
130
  return '';
@@ -76,10 +132,7 @@ export function buildSessionsFilterShareUrl(vm) {
76
132
  }
77
133
 
78
134
  export function syncSessionsFilterUrl(vm) {
79
- const url = buildSessionsFilterShareUrl(vm);
80
- if (!url) return;
81
- try {
82
- window.history.replaceState(null, '', url);
83
- } catch (_) {}
135
+ // URL 保持静态,不同步状态到 URL
136
+ // 所有状态通过 localStorage 管理
84
137
  }
85
138