codexmate 0.0.31 → 0.0.33

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 (37) hide show
  1. package/README.md +92 -308
  2. package/README.zh.md +94 -318
  3. package/cli/local-bridge.js +227 -0
  4. package/cli/update.js +162 -0
  5. package/cli.js +357 -112
  6. package/lib/cli-sessions.js +16 -6
  7. package/lib/win-tray.js +119 -0
  8. package/package.json +2 -2
  9. package/web-ui/app.js +4 -0
  10. package/web-ui/logic.sessions.mjs +17 -1
  11. package/web-ui/modules/app.computed.session.mjs +51 -315
  12. package/web-ui/modules/app.methods.agents.mjs +19 -0
  13. package/web-ui/modules/app.methods.claude-config.mjs +71 -2
  14. package/web-ui/modules/app.methods.codex-config.mjs +20 -0
  15. package/web-ui/modules/app.methods.providers.mjs +53 -7
  16. package/web-ui/modules/app.methods.session-actions.mjs +1 -1
  17. package/web-ui/modules/app.methods.session-browser.mjs +29 -1
  18. package/web-ui/modules/app.methods.startup-claude.mjs +4 -0
  19. package/web-ui/modules/i18n.dict.mjs +21 -3
  20. package/web-ui/partials/index/layout-header.html +1 -2
  21. package/web-ui/partials/index/modal-config-template-agents.html +12 -1
  22. package/web-ui/partials/index/modals-basic.html +14 -3
  23. package/web-ui/partials/index/panel-config-claude.html +57 -85
  24. package/web-ui/partials/index/panel-config-codex.html +60 -226
  25. package/web-ui/partials/index/panel-dashboard.html +0 -33
  26. package/web-ui/partials/index/panel-docs.html +21 -53
  27. package/web-ui/partials/index/panel-sessions.html +37 -20
  28. package/web-ui/partials/index/panel-trash.html +33 -38
  29. package/web-ui/partials/index/panel-usage.html +71 -304
  30. package/web-ui/styles/controls-forms.css +11 -0
  31. package/web-ui/styles/docs-panel.css +57 -83
  32. package/web-ui/styles/layout-shell.css +26 -24
  33. package/web-ui/styles/modals-core.css +33 -0
  34. package/web-ui/styles/responsive.css +5 -67
  35. package/web-ui/styles/sessions-list.css +274 -8
  36. package/web-ui/styles/sessions-toolbar-trash.css +185 -15
  37. package/web-ui/styles/sessions-usage.css +336 -788
@@ -27,53 +27,16 @@ function formatCompactUsageSummaryNumber(value) {
27
27
  }).format(Math.floor(numeric));
28
28
  }
29
29
 
30
- function readUsageCostNumber(value) {
31
- const numeric = Number(value);
32
- if (!Number.isFinite(numeric) || numeric < 0) {
33
- return null;
34
- }
35
- return numeric;
36
- }
37
-
38
- function formatUsageEstimatedCost(value, options = {}) {
39
- const numeric = Number(value);
40
- if (!Number.isFinite(numeric) || numeric <= 0) {
41
- return '$0.00';
42
- }
43
- const precise = options && options.precise === true;
44
- if (numeric < 0.0001) {
45
- return '<$0.0001';
46
- }
47
- let fractionDigits = 2;
48
- if (numeric < 1) {
49
- fractionDigits = precise ? 6 : 4;
50
- } else if (numeric >= 100) {
51
- fractionDigits = 0;
52
- }
53
- return new Intl.NumberFormat('en-US', {
54
- style: 'currency',
55
- currency: 'USD',
56
- minimumFractionDigits: fractionDigits,
57
- maximumFractionDigits: fractionDigits
58
- }).format(numeric);
59
- }
60
-
61
30
  function formatSignedUsageSummaryNumber(value) {
62
31
  const numeric = Number(value);
63
- if (!Number.isFinite(numeric) || numeric === 0) {
32
+ if (!Number.isFinite(numeric)) {
64
33
  return '0';
65
34
  }
66
- const sign = numeric > 0 ? '+' : '-';
67
- return `${sign}${formatUsageSummaryNumber(Math.abs(numeric))}`;
68
- }
69
-
70
- function formatSignedUsageEstimatedCost(value, options = {}) {
71
- const numeric = Number(value);
72
- if (!Number.isFinite(numeric) || numeric === 0) {
73
- return '$0.00';
74
- }
75
- const sign = numeric > 0 ? '+' : '-';
76
- return `${sign}${formatUsageEstimatedCost(Math.abs(numeric), options)}`;
35
+ const abs = Math.floor(Math.abs(numeric));
36
+ const formatted = abs.toLocaleString('en-US');
37
+ if (numeric > 0) return `+${formatted}`;
38
+ if (numeric < 0) return `-${formatted}`;
39
+ return '0';
77
40
  }
78
41
 
79
42
  function formatUsageRangeLabel(range, t) {
@@ -149,208 +112,20 @@ function formatUsageDuration(value, options = {}) {
149
112
  return parts.length ? parts.join(compact ? '' : ' ') : (isEn ? '0m' : '0分');
150
113
  }
151
114
 
152
- const KNOWN_USAGE_MODEL_PRICING = Object.freeze({
153
- 'gpt-5.4': Object.freeze({ input: 2.5, output: 15, cacheRead: 0.25, cacheWrite: 0 }),
154
- 'gpt-5.4-mini': Object.freeze({ input: 0.75, output: 4.5, cacheRead: 0.075, cacheWrite: 0 }),
155
- 'gpt-5.3-codex': Object.freeze({ input: 1.75, output: 14, cacheRead: 0.175, cacheWrite: 0 }),
156
- 'gpt-5.2-codex': Object.freeze({ input: 1.75, output: 14, cacheRead: 0.175, cacheWrite: 0 }),
157
- 'claude-opus-4-6': Object.freeze({ input: 15, output: 75, cacheRead: 1.5, cacheWrite: 18.75, reasoningOutput: 75 }),
158
- 'claude-opus-4-7': Object.freeze({ input: 15, output: 75, cacheRead: 1.5, cacheWrite: 18.75, reasoningOutput: 75 }),
159
- 'claude-sonnet-4-6': Object.freeze({ input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75, reasoningOutput: 15 }),
160
- 'claude-haiku-4-5': Object.freeze({ input: 0.8, output: 4, cacheRead: 0.08, cacheWrite: 1, reasoningOutput: 4 }),
161
- 'claude-3-5-sonnet': Object.freeze({ input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75, reasoningOutput: 15 }),
162
- 'claude-3-5-haiku': Object.freeze({ input: 0.8, output: 4, cacheRead: 0.08, cacheWrite: 1, reasoningOutput: 4 }),
163
- 'claude-3-opus': Object.freeze({ input: 15, output: 75, cacheRead: 1.5, cacheWrite: 18.75, reasoningOutput: 75 })
164
- });
165
-
166
- function createUsagePricingEntry(pricing, source) {
167
- const resolvedSource = typeof source === 'string' && source.trim() ? source.trim() : 'provider-config';
168
- return {
169
- input: readUsageCostNumber(pricing && pricing.input),
170
- output: readUsageCostNumber(pricing && pricing.output),
171
- reasoningOutput: readUsageCostNumber(
172
- pricing && (pricing.reasoningOutput != null ? pricing.reasoningOutput : pricing.reasoning)
173
- ),
174
- cacheRead: readUsageCostNumber(pricing && pricing.cacheRead),
175
- cacheWrite: readUsageCostNumber(pricing && pricing.cacheWrite),
176
- source: resolvedSource
177
- };
178
- }
179
-
180
- function buildUsagePricingIndex(providersList = []) {
181
- const byProvider = new Map();
182
- const byModel = new Map();
183
- const knownByModel = new Map();
184
- const list = Array.isArray(providersList) ? providersList : [];
185
- for (const provider of list) {
186
- if (!provider || typeof provider !== 'object') continue;
187
- const providerName = typeof provider.name === 'string' ? provider.name.trim() : '';
188
- const models = Array.isArray(provider.models) ? provider.models : [];
189
- const providerMap = new Map();
190
- for (const model of models) {
191
- if (!model || typeof model !== 'object') continue;
192
- const modelId = typeof model.id === 'string' ? model.id.trim() : '';
193
- if (!modelId) continue;
194
- const pricing = createUsagePricingEntry(
195
- model.cost && typeof model.cost === 'object' && !Array.isArray(model.cost)
196
- ? model.cost
197
- : null,
198
- 'provider-config'
199
- );
200
- const hasKnownRate = [pricing.input, pricing.output, pricing.reasoningOutput, pricing.cacheRead, pricing.cacheWrite]
201
- .some((value) => value !== null);
202
- if (!hasKnownRate) continue;
203
- providerMap.set(modelId, pricing);
204
- const modelMatches = byModel.get(modelId) || [];
205
- modelMatches.push({ provider: providerName, pricing });
206
- byModel.set(modelId, modelMatches);
207
- }
208
- if (providerName && providerMap.size) {
209
- byProvider.set(providerName, providerMap);
210
- }
211
- }
212
- for (const [modelId, pricing] of Object.entries(KNOWN_USAGE_MODEL_PRICING)) {
213
- const normalizedModelId = typeof modelId === 'string' ? modelId.trim() : '';
214
- if (!normalizedModelId || byModel.has(normalizedModelId)) {
215
- continue;
216
- }
217
- knownByModel.set(normalizedModelId, createUsagePricingEntry(pricing, 'public-catalog'));
218
- }
219
- return { byProvider, byModel, knownByModel };
220
- }
221
-
222
- function resolveUsagePricingForSession(session, pricingIndex, fallbackProvider = '') {
223
- if (!session || typeof session !== 'object' || !pricingIndex || typeof pricingIndex !== 'object') {
224
- return null;
225
- }
226
- const model = typeof session.model === 'string' ? session.model.trim() : '';
227
- if (!model) return null;
228
- const provider = typeof session.provider === 'string' ? session.provider.trim() : '';
229
- const effectiveProvider = provider || fallbackProvider;
230
- if (effectiveProvider) {
231
- const providerMap = pricingIndex.byProvider instanceof Map ? pricingIndex.byProvider.get(effectiveProvider) : null;
232
- if (providerMap instanceof Map && providerMap.has(model)) {
233
- return providerMap.get(model);
234
- }
235
- }
236
- const modelMatches = pricingIndex.byModel instanceof Map ? pricingIndex.byModel.get(model) : null;
237
- if (Array.isArray(modelMatches) && modelMatches.length === 1) {
238
- return modelMatches[0].pricing;
239
- }
240
- const knownPricing = pricingIndex.knownByModel instanceof Map ? pricingIndex.knownByModel.get(model) : null;
241
- if (knownPricing) {
242
- return knownPricing;
243
- }
244
- const strippedModel = model.replace(/-\d{8}(?=[-_]|$)/, '');
245
- if (strippedModel !== model) {
246
- const strippedKnown = pricingIndex.knownByModel instanceof Map ? pricingIndex.knownByModel.get(strippedModel) : null;
247
- if (strippedKnown) {
248
- return strippedKnown;
249
- }
250
- }
251
- return null;
252
- }
253
-
254
- function shouldEstimateUsageCostForSession() {
255
- return true;
256
- }
257
-
258
- function estimateUsageCostSummary(sessions, providersList, currentProvider) {
259
- const list = Array.isArray(sessions) ? sessions : [];
260
- const pricingIndex = buildUsagePricingIndex(providersList);
261
- let totalCostUsd = 0;
262
- let estimatedSessions = 0;
263
- let totalTokens = 0;
264
- let estimatedTokens = 0;
265
- let configuredSessions = 0;
266
- let catalogSessions = 0;
267
- let missingPricingSessions = 0;
268
- let missingTokenSessions = 0;
269
- let supportedSessions = 0;
270
- let skippedUnsupportedSessions = 0;
271
-
272
- for (const session of list) {
273
- if (!session || typeof session !== 'object') continue;
274
- if (!shouldEstimateUsageCostForSession(session)) {
275
- skippedUnsupportedSessions += 1;
276
- continue;
277
- }
278
- supportedSessions += 1;
279
- const cost = estimateUsageCostForSession(session, pricingIndex, currentProvider);
280
- totalTokens += cost.totalSessionTokens;
281
- if (!cost.pricing) {
282
- missingPricingSessions += 1;
283
- continue;
284
- }
285
- if (!cost.hasTokenBreakdown) {
286
- missingTokenSessions += 1;
287
- continue;
288
- }
289
- totalCostUsd += cost.estimatedUsd;
290
- estimatedSessions += 1;
291
- estimatedTokens += cost.totalSessionTokens;
292
- if (cost.pricing.source === 'public-catalog') catalogSessions += 1;
293
- else configuredSessions += 1;
294
- }
295
-
296
- const coveragePercent = totalTokens > 0
297
- ? Math.round((estimatedTokens / totalTokens) * 100)
298
- : (estimatedSessions > 0 ? 100 : 0);
299
- return {
300
- totalCostUsd,
301
- estimatedSessions,
302
- totalSessions: supportedSessions,
303
- estimatedTokens,
304
- totalTokens,
305
- coveragePercent,
306
- hasEstimate: estimatedSessions > 0,
307
- configuredSessions,
308
- catalogSessions,
309
- missingPricingSessions,
310
- missingTokenSessions,
311
- skippedUnsupportedSessions
312
- };
313
- }
314
-
315
- function estimateUsageCostForSession(session, pricingIndex, currentProvider) {
316
- const inputTokens = Number.isFinite(Number(session.inputTokens)) ? Math.max(0, Math.floor(Number(session.inputTokens))) : null;
317
- const cachedInputTokens = Number.isFinite(Number(session.cachedInputTokens)) ? Math.max(0, Math.floor(Number(session.cachedInputTokens))) : 0;
318
- const cacheCreationInputTokens = Number.isFinite(Number(session.cacheCreationInputTokens)) ? Math.max(0, Math.floor(Number(session.cacheCreationInputTokens))) : 0;
319
- const outputTokens = Number.isFinite(Number(session.outputTokens)) ? Math.max(0, Math.floor(Number(session.outputTokens))) : null;
320
- const reasoningOutputTokens = Number.isFinite(Number(session.reasoningOutputTokens)) ? Math.max(0, Math.floor(Number(session.reasoningOutputTokens))) : 0;
321
- const billableInputTokens = Math.max(0, (inputTokens || 0) - cachedInputTokens - cacheCreationInputTokens);
322
- const fallbackSessionTokens = billableInputTokens + cachedInputTokens + cacheCreationInputTokens + (outputTokens || 0) + reasoningOutputTokens;
323
- const totalSessionTokens = Number.isFinite(Number(session.totalTokens))
324
- ? Math.max(0, Math.floor(Number(session.totalTokens)))
325
- : fallbackSessionTokens;
326
- const pricing = resolveUsagePricingForSession(session, pricingIndex, currentProvider);
327
- const hasTokenBreakdown = !(inputTokens === null && outputTokens === null && reasoningOutputTokens === 0);
328
- const reasoningRate = pricing
329
- ? ((pricing.reasoningOutput != null ? pricing.reasoningOutput : pricing.output) || 0)
330
- : 0;
331
- const visibleOutputTokens = Math.max(0, (outputTokens || 0) - reasoningOutputTokens);
332
- const estimatedUsd = pricing && hasTokenBreakdown
333
- ? (
334
- ((pricing.input || 0) * billableInputTokens)
335
- + ((pricing.cacheRead || 0) * cachedInputTokens)
336
- + ((pricing.cacheWrite || 0) * cacheCreationInputTokens)
337
- + (reasoningRate * reasoningOutputTokens)
338
- + ((pricing.output || 0) * visibleOutputTokens)
339
- ) / 1000000
340
- : 0;
341
- return {
342
- pricing,
343
- hasTokenBreakdown,
344
- totalSessionTokens,
345
- estimatedUsd
346
- };
347
- }
348
-
349
115
  export function createSessionComputed() {
350
116
  return {
351
117
  isSessionQueryEnabled() {
352
118
  return isSessionQueryEnabled(this.sessionFilterSource);
353
119
  },
120
+ sessionSourceOptions() {
121
+ return [
122
+ { value: "all", label: this.t("common.all") },
123
+ { value: "codex", label: this.t("sessions.source.codex") },
124
+ { value: "claude", label: this.t("sessions.source.claudeCode") },
125
+ { value: "gemini", label: this.t("sessions.source.gemini") },
126
+ { value: "codebuddy", label: this.t("sessions.source.codebuddy") }
127
+ ];
128
+ },
354
129
  activeSessionExportKey() {
355
130
  return this.activeSession ? this.getSessionExportKey(this.activeSession) : '';
356
131
  },
@@ -642,30 +417,8 @@ export function createSessionComputed() {
642
417
  : this.sessionsUsageList;
643
418
  const t = typeof this.t === 'function' ? this.t : null;
644
419
  const usageRangeLabel = formatUsageRangeLabel(this.sessionsUsageTimeRange, t);
645
- const estimatedCost = estimateUsageCostSummary(
646
- filteredUsageSessions,
647
- this.providersList,
648
- this.currentProvider
649
- );
650
420
  const noneLabel = t ? t('common.none') : '暂无';
651
- const estimatedCostPrefix = estimatedCost.skippedUnsupportedSessions > 0
652
- ? (t ? t('usage.estimatedCost.note.excludesClaudePrefix') : '暂不含 Claude,')
653
- : '';
654
- const estimatedCostMethod = estimatedCost.catalogSessions > 0
655
- ? (estimatedCost.configuredSessions > 0
656
- ? (t ? t('usage.estimatedCost.method.configuredAndCatalog') : '按已配置单价 + 公开模型目录估算')
657
- : (t ? t('usage.estimatedCost.method.catalog') : '按公开模型目录估算'))
658
- : (t ? t('usage.estimatedCost.method.configured') : '按已配置单价估算');
659
- const estimatedCostTitle = estimatedCost.hasEstimate
660
- ? (t ? t('usage.estimatedCost.detail.estimate', {
661
- prefix: estimatedCostPrefix,
662
- method: estimatedCostMethod,
663
- estimate: formatUsageEstimatedCost(estimatedCost.totalCostUsd, { precise: true }),
664
- covered: estimatedCost.estimatedSessions,
665
- total: estimatedCost.totalSessions,
666
- percent: estimatedCost.coveragePercent
667
- }) : `${estimatedCostPrefix}${estimatedCostMethod},估算 ${formatUsageEstimatedCost(estimatedCost.totalCostUsd, { precise: true })},覆盖 ${estimatedCost.estimatedSessions}/${estimatedCost.totalSessions} 个会话,约 ${estimatedCost.coveragePercent}% token`)
668
- : (t ? t('usage.estimatedCost.detail.missing', { prefix: estimatedCostPrefix }) : `${estimatedCostPrefix}缺少可匹配的模型单价或 token 拆分。请先补 models.cost,或确认会话已记录 input/output token。`);
421
+
669
422
  return [
670
423
  { key: 'sessions', label: t ? t('usage.summary.sessions') : '总会话数', value: formatUsageSummaryNumber(summary.totalSessions || 0) },
671
424
  { key: 'messages', label: t ? t('usage.summary.messages') : '总消息数', value: formatUsageSummaryNumber(summary.totalMessages || 0) },
@@ -681,12 +434,6 @@ export function createSessionComputed() {
681
434
  value: formatCompactUsageSummaryNumber(summary.totalContextWindow || 0),
682
435
  title: formatUsageSummaryNumber(summary.totalContextWindow || 0)
683
436
  },
684
- {
685
- key: 'estimated-cost',
686
- label: t ? t('usage.summary.estimatedCost', { range: usageRangeLabel }) : `预估费用 · ${usageRangeLabel}`,
687
- value: estimatedCost.hasEstimate ? formatUsageEstimatedCost(estimatedCost.totalCostUsd) : '0',
688
- title: estimatedCostTitle
689
- },
690
437
  {
691
438
  key: 'active-duration',
692
439
  label: t ? t('usage.summary.activeDuration') : '活跃时长',
@@ -747,7 +494,6 @@ export function createSessionComputed() {
747
494
  const baseBuckets = this.sessionUsageCharts && Array.isArray(this.sessionUsageCharts.buckets)
748
495
  ? this.sessionUsageCharts.buckets
749
496
  : [];
750
- const pricingIndex = buildUsagePricingIndex(this.providersList);
751
497
  const compareEnabled = this.sessionsUsageCompareEnabled === true && this.sessionsUsageTimeRange !== 'all';
752
498
  const sessions = compareEnabled
753
499
  ? this.sessionsUsageList
@@ -766,9 +512,7 @@ export function createSessionComputed() {
766
512
  sessionCount: 0,
767
513
  messageCount: 0,
768
514
  tokenTotal: 0,
769
- estimatedCostUsd: 0,
770
- estimatedSessions: 0,
771
- hasCostEstimate: false
515
+
772
516
  });
773
517
  if (compareEnabled) {
774
518
  const baseMs = Date.parse(`${bucket.key}T00:00:00.000Z`);
@@ -781,9 +525,7 @@ export function createSessionComputed() {
781
525
  sessionCount: 0,
782
526
  messageCount: 0,
783
527
  tokenTotal: 0,
784
- estimatedCostUsd: 0,
785
- estimatedSessions: 0,
786
- hasCostEstimate: false
528
+
787
529
  });
788
530
  }
789
531
  }
@@ -808,15 +550,7 @@ export function createSessionComputed() {
808
550
  row.messageCount += messageCount;
809
551
  row.tokenTotal += tokenTotal;
810
552
 
811
- if (shouldEstimateUsageCostForSession(session)) {
812
- const cost = estimateUsageCostForSession(session, pricingIndex, this.currentProvider);
813
- if (cost.pricing && cost.hasTokenBreakdown) {
814
- row.estimatedCostUsd += cost.estimatedUsd;
815
- row.estimatedSessions += 1;
816
- row.hasCostEstimate = true;
817
- }
818
- }
819
- }
553
+ }
820
554
 
821
555
  const currentKeys = baseBuckets.map((bucket) => bucket && bucket.key).filter(Boolean);
822
556
  const rows = currentKeys
@@ -825,7 +559,7 @@ export function createSessionComputed() {
825
559
  .sort((a, b) => b.key.localeCompare(a.key, 'en-US'));
826
560
  const rowsWithCompare = rows.map((row) => {
827
561
  if (!compareEnabled) {
828
- return { ...row, compareEnabled: false, prevKey: '', prevTokenTotal: 0, prevCostUsd: 0 };
562
+ return { ...row, compareEnabled: false, prevKey: '', prevTokenTotal: 0 };
829
563
  }
830
564
  const baseMs = Date.parse(`${row.key}T00:00:00.000Z`);
831
565
  const prevKey = Number.isFinite(baseMs)
@@ -837,11 +571,9 @@ export function createSessionComputed() {
837
571
  compareEnabled: true,
838
572
  prevKey,
839
573
  prevTokenTotal: prevRow ? prevRow.tokenTotal : 0,
840
- prevCostUsd: prevRow ? prevRow.estimatedCostUsd : 0
841
574
  };
842
575
  });
843
576
  const maxTokens = rowsWithCompare.reduce((max, item) => Math.max(max, item.tokenTotal, item.prevTokenTotal || 0), 0);
844
- const maxCost = rowsWithCompare.reduce((max, item) => Math.max(max, item.estimatedCostUsd, item.prevCostUsd || 0), 0);
845
577
 
846
578
  return {
847
579
  rows: rowsWithCompare.map((row) => ({
@@ -851,14 +583,9 @@ export function createSessionComputed() {
851
583
  tokenPercent: maxTokens > 0 ? Math.round((row.tokenTotal / maxTokens) * 1000) / 10 : 0,
852
584
  prevTokenPercent: row.compareEnabled && maxTokens > 0 ? Math.round(((row.prevTokenTotal || 0) / maxTokens) * 1000) / 10 : 0,
853
585
  prevTokenTitle: row.compareEnabled ? formatUsageSummaryNumber(row.prevTokenTotal || 0) : '',
854
- costLabel: row.hasCostEstimate ? formatUsageEstimatedCost(row.estimatedCostUsd) : '0',
855
- costTitle: row.hasCostEstimate ? formatUsageEstimatedCost(row.estimatedCostUsd, { precise: true }) : '0',
856
- costPercent: maxCost > 0 ? Math.round((row.estimatedCostUsd / maxCost) * 1000) / 10 : 0,
857
- prevCostPercent: row.compareEnabled && maxCost > 0 ? Math.round(((row.prevCostUsd || 0) / maxCost) * 1000) / 10 : 0,
858
- prevCostTitle: row.compareEnabled ? formatUsageEstimatedCost(row.prevCostUsd || 0, { precise: true }) : ''
586
+ prevCostTitle: row.compareEnabled ? formatUsageSummaryNumber(row.prevTokenTotal || 0) : '',
859
587
  })),
860
588
  maxTokens,
861
- maxCost
862
589
  };
863
590
  },
864
591
 
@@ -875,7 +602,6 @@ export function createSessionComputed() {
875
602
  const sessions = this.sessionUsageCharts && Array.isArray(this.sessionUsageCharts.filteredSessions)
876
603
  ? this.sessionUsageCharts.filteredSessions
877
604
  : this.sessionsUsageList;
878
- const pricingIndex = buildUsagePricingIndex(this.providersList);
879
605
  const compareEnabled = this.sessionsUsageCompareEnabled === true && this.sessionsUsageTimeRange !== 'all';
880
606
  const rangeDays = this.sessionsUsageTimeRange === '30d' ? 30 : 7;
881
607
  const dayMs = 24 * 60 * 60 * 1000;
@@ -886,11 +612,7 @@ export function createSessionComputed() {
886
612
  let sessionCount = 0;
887
613
  let messageCount = 0;
888
614
  let tokenTotal = 0;
889
- let estimatedCostUsd = 0;
890
- let hasCostEstimate = false;
891
615
  let prevTokenTotal = 0;
892
- let prevEstimatedCostUsd = 0;
893
- let prevHasCostEstimate = false;
894
616
  const modelMap = new Map();
895
617
  const sessionRows = [];
896
618
  for (const session of (Array.isArray(sessions) ? sessions : [])) {
@@ -914,19 +636,7 @@ export function createSessionComputed() {
914
636
  } else if (isPrev) {
915
637
  prevTokenTotal += sessionTokens;
916
638
  }
917
- if (shouldEstimateUsageCostForSession(session)) {
918
- const cost = estimateUsageCostForSession(session, pricingIndex, this.currentProvider);
919
- if (cost.pricing && cost.hasTokenBreakdown) {
920
- if (isCurrent) {
921
- estimatedCostUsd += cost.estimatedUsd;
922
- hasCostEstimate = true;
923
- } else if (isPrev) {
924
- prevEstimatedCostUsd += cost.estimatedUsd;
925
- prevHasCostEstimate = true;
926
- }
927
- }
928
- }
929
- const model = typeof session.model === 'string' ? session.model.trim() : '';
639
+ const model = typeof session.model === 'string' ? session.model.trim() : '';
930
640
  if (isCurrent && model) {
931
641
  modelMap.set(model, (modelMap.get(model) || 0) + 1);
932
642
  }
@@ -963,12 +673,9 @@ export function createSessionComputed() {
963
673
  messageCount,
964
674
  tokenTotal,
965
675
  tokenLabel: formatUsageSummaryNumber(tokenTotal),
966
- costLabel: hasCostEstimate ? formatUsageEstimatedCost(estimatedCostUsd) : '0',
967
676
  prevTokenTotal,
968
677
  prevTokenLabel: compareEnabled ? formatUsageSummaryNumber(prevTokenTotal) : '0',
969
678
  deltaTokenLabel: compareEnabled ? formatSignedUsageSummaryNumber(tokenTotal - prevTokenTotal) : '0',
970
- prevCostLabel: compareEnabled ? (prevHasCostEstimate ? formatUsageEstimatedCost(prevEstimatedCostUsd) : '0') : '0',
971
- deltaCostLabel: compareEnabled ? formatSignedUsageEstimatedCost(estimatedCostUsd - prevEstimatedCostUsd, { precise: true }) : '0',
972
679
  topSessions,
973
680
  topModels
974
681
  };
@@ -994,6 +701,35 @@ export function createSessionComputed() {
994
701
  return Math.max(0, Math.floor(totalCount));
995
702
  }
996
703
  return Array.isArray(this.sessionTrashItems) ? this.sessionTrashItems.length : 0;
704
+ },
705
+
706
+ sessionContextUtilization() {
707
+ const list = Array.isArray(this.sessionsList) ? this.sessionsList : [];
708
+ const utilizationMap = {};
709
+ for (const session of list) {
710
+ if (!session || typeof session !== 'object') continue;
711
+ const key = this.getSessionExportKey(session);
712
+ if (!key) continue;
713
+ const totalTokens = Number.isFinite(Number(session.totalTokens))
714
+ ? Math.max(0, Math.floor(Number(session.totalTokens)))
715
+ : 0;
716
+ const contextWindow = Number.isFinite(Number(session.contextWindow))
717
+ ? Math.max(0, Math.floor(Number(session.contextWindow)))
718
+ : 0;
719
+ if (contextWindow <= 0) {
720
+ utilizationMap[key] = { percent: 0, level: 'normal' };
721
+ continue;
722
+ }
723
+ const percent = Math.min(100, Math.round((totalTokens / contextWindow) * 100));
724
+ let level = 'normal';
725
+ if (percent >= 95) {
726
+ level = 'critical';
727
+ } else if (percent >= 80) {
728
+ level = 'warning';
729
+ }
730
+ utilizationMap[key] = { percent, level };
731
+ }
732
+ return utilizationMap;
997
733
  }
998
734
  };
999
735
  }
@@ -420,6 +420,25 @@ export function createAgentsMethods(options = {}) {
420
420
  this.resetAgentsDiffState();
421
421
  }
422
422
  },
423
+ async pasteAgentsContent() {
424
+ if (this.agentsLoading || this.agentsSaving || this.agentsDiffVisible) {
425
+ return;
426
+ }
427
+ let text = '';
428
+ try {
429
+ text = await navigator.clipboard.readText();
430
+ } catch (_) {
431
+ this.showMessage('无法读取剪贴板', 'error');
432
+ return;
433
+ }
434
+ if (typeof text !== 'string' || !text) {
435
+ this.showMessage('剪贴板为空', 'info');
436
+ return;
437
+ }
438
+ this.agentsContent = text;
439
+ this.onAgentsContentInput();
440
+ this.showMessage('已粘贴', 'success');
441
+ },
423
442
  buildAgentsDiffFingerprint() {
424
443
  const context = this.agentsContext || 'codex';
425
444
  const fileName = context === 'openclaw-workspace'
@@ -10,8 +10,7 @@ export function createClaudeConfigMethods(options = {}) {
10
10
 
11
11
  onClaudeModelChange() {
12
12
  const name = this.currentClaudeConfig;
13
- if (!name) {
14
- this.showMessage('请先选择配置', 'error');
13
+ if (!name || name === 'claude-local') {
15
14
  return;
16
15
  }
17
16
  const model = (this.currentClaudeModel || '').trim();
@@ -41,6 +40,11 @@ export function createClaudeConfigMethods(options = {}) {
41
40
  if (this.currentClaudeConfig) {
42
41
  try { localStorage.setItem('currentClaudeConfig', this.currentClaudeConfig); } catch (_) {}
43
42
  }
43
+ this.syncClaudeBridgeProviders();
44
+ },
45
+
46
+ async syncClaudeBridgeProviders() {
47
+ try { await api('claude-local-bridge-sync-providers', { providers: this.claudeConfigs || {} }); } catch (_) {}
44
48
  },
45
49
 
46
50
  openCloneClaudeConfigModal(name, config) {
@@ -61,6 +65,7 @@ export function createClaudeConfigMethods(options = {}) {
61
65
  baseUrl: config.baseUrl || '',
62
66
  model: config.model || ''
63
67
  };
68
+ this.showEditClaudeConfigKey = false;
64
69
  this.showEditConfigModal = true;
65
70
  },
66
71
 
@@ -77,9 +82,14 @@ export function createClaudeConfigMethods(options = {}) {
77
82
 
78
83
  closeEditConfigModal() {
79
84
  this.showEditConfigModal = false;
85
+ this.showEditClaudeConfigKey = false;
80
86
  this.editingConfig = { name: '', apiKey: '', baseUrl: '', model: '' };
81
87
  },
82
88
 
89
+ toggleEditClaudeConfigKey() {
90
+ this.showEditClaudeConfigKey = !this.showEditClaudeConfigKey;
91
+ },
92
+
83
93
  async saveAndApplyConfig() {
84
94
  const name = this.editingConfig.name;
85
95
  this.claudeConfigs[name] = this.mergeClaudeConfig(this.claudeConfigs[name], this.editingConfig);
@@ -195,6 +205,65 @@ export function createClaudeConfigMethods(options = {}) {
195
205
  baseUrl: '',
196
206
  model: ''
197
207
  };
208
+ },
209
+
210
+ async loadClaudeLocalBridgeStatus() {
211
+ try {
212
+ const res = await api('claude-local-bridge-status');
213
+ if (res && !res.error) {
214
+ if (Array.isArray(res.excludedProviders)) {
215
+ this.claudeLocalBridgeExcluded = res.excludedProviders;
216
+ }
217
+ }
218
+ } catch (e) { /* ignore */ }
219
+ },
220
+
221
+ async toggleClaudeLocalBridge(enable) {
222
+ try {
223
+ const res = await api('claude-local-bridge-toggle', { enable });
224
+ if (res.error) {
225
+ this.showMessage(res.error, 'error');
226
+ return;
227
+ }
228
+ if (enable) {
229
+ this.showMessage('Claude 本地负载均衡已启用', 'success');
230
+ } else {
231
+ this.showMessage('Claude 本地负载均衡已关闭', 'success');
232
+ }
233
+ } catch (e) {
234
+ this.showMessage('操作失败', 'error');
235
+ }
236
+ },
237
+
238
+ async toggleClaudeLocalBridgeExcluded(providerName) {
239
+ const name = String(providerName || '').trim();
240
+ if (!name) return;
241
+ const idx = this.claudeLocalBridgeExcluded.indexOf(name);
242
+ const next = [...this.claudeLocalBridgeExcluded];
243
+ if (idx >= 0) {
244
+ next.splice(idx, 1);
245
+ } else {
246
+ next.push(name);
247
+ }
248
+ try {
249
+ const res = await api('claude-local-bridge-set-excluded', { names: next });
250
+ if (res && !res.error) {
251
+ this.claudeLocalBridgeExcluded = next;
252
+ }
253
+ } catch (e) { /* ignore */ }
254
+ },
255
+
256
+ isClaudeLocalBridgeExcluded(providerName) {
257
+ return this.claudeLocalBridgeExcluded.indexOf(String(providerName || '').trim()) >= 0;
258
+ },
259
+
260
+ claudeLocalBridgeCandidateProviders() {
261
+ return Object.keys(this.claudeConfigs || {}).filter(name => name && !this.isClaudeLocalBridgeExcluded(name))
262
+ .map(name => ({ name, ...this.claudeConfigs[name] }));
263
+ },
264
+
265
+ claudeLocalBridgeConfigured() {
266
+ return this.claudeLocalBridgeCandidateProviders().some(p => p.hasKey);
198
267
  }
199
268
  };
200
269
  }
@@ -696,6 +696,26 @@ export function createCodexConfigMethods(options = {}) {
696
696
  }
697
697
  },
698
698
 
699
+ async pasteConfigTemplateContent() {
700
+ if (this.configTemplateApplying || this.configTemplateDiffLoading || this.configTemplateDiffVisible) {
701
+ return;
702
+ }
703
+ let text = '';
704
+ try {
705
+ text = await navigator.clipboard.readText();
706
+ } catch (_) {
707
+ this.showMessage('无法读取剪贴板', 'error');
708
+ return;
709
+ }
710
+ if (typeof text !== 'string' || !text) {
711
+ this.showMessage('剪贴板为空', 'info');
712
+ return;
713
+ }
714
+ this.configTemplateContent = text;
715
+ this.onConfigTemplateContentInput();
716
+ this.showMessage('已粘贴', 'success');
717
+ },
718
+
699
719
  buildConfigTemplateDiffFingerprint() {
700
720
  const content = typeof this.configTemplateContent === 'string' ? this.configTemplateContent : '';
701
721
  return `${content.length}::${content}`;