@mars-sea/dsh-commandcode-provider 0.13.3 → 0.13.4

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.
package/lib/client.js CHANGED
@@ -482,6 +482,28 @@ window.__ModuleLoader__.load({
482
482
  if (raw === null || typeof raw !== "object" || Array.isArray(raw)) return {};
483
483
  return Object.fromEntries(Object.entries(raw).filter(([id, value]) => id !== "" && typeof value === "boolean"));
484
484
  }
485
+ /** 自定义上下文长度接受的上界(token);覆盖任何已知模型窗口仍留有余量。 */
486
+ const CONTEXT_WINDOW_MAX = 1e7;
487
+ /**
488
+ * 判定一个值能否作为自定义上下文长度:边界内的安全整数,符合宿主模型元数据约束。
489
+ *
490
+ * 程序化构造的连接可能绕过 schemastery 归一化,所以 `resolveAdapterOptions`
491
+ * 也要重新判定——越界或非数值直接丢弃,回落到目录公布值,而不是让请求带着
492
+ * 一个网关会拒绝的窗口上线。
493
+ */
494
+ function isValidContextWindow(value) {
495
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 1 && value <= 1e7;
496
+ }
497
+ /**
498
+ * 只接收合法条目的按模型覆盖表;空表、非对象与非法数值一律视为未设置。
499
+ * 配置校验会拒绝非整数或越界值;本函数额外保护绕过配置校验的程序化调用,
500
+ * 丢弃坏条目,不把非法窗口交给宿主。返回新对象;未设置时为 `undefined`。
501
+ */
502
+ function readContextWindowOverrides(raw) {
503
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) return void 0;
504
+ const entries = Object.entries(raw).filter((entry) => entry[0] !== "" && isValidContextWindow(entry[1]));
505
+ return entries.length === 0 ? void 0 : Object.fromEntries(entries);
506
+ }
485
507
  //#endregion
486
508
  //#region src/client/settings-write.ts
487
509
  /** 设置写入过程:同一控制器的意图按顺序执行,配置确认与凭据确认分别保留。
@@ -579,6 +601,11 @@ window.__ModuleLoader__.load({
579
601
  value: Object.fromEntries([...keys].map((id) => [id, list.length === 0 || list.includes(id)]))
580
602
  });
581
603
  }
604
+ if (owned.kind === "save" && owned.contextWindowOverrides !== void 0) ops.push({
605
+ op: "set",
606
+ path: ["contextWindowOverrides"],
607
+ value: owned.contextWindowOverrides
608
+ });
582
609
  const baseline = new Map(ops.map((op) => [op.path[0], structuredClone(fieldBaseline(snapshot, op.path[0]))]));
583
610
  const defaultRef = owned.kind === "save" && owned.key !== void 0 || owned.kind === "key" && owned.target === "default" ? this.defaultRef() : void 0;
584
611
  return new Promise((resolve) => {
@@ -929,6 +956,50 @@ window.__ModuleLoader__.load({
929
956
  };
930
957
  }
931
958
  /**
959
+ * The compact token-count shorthand (`32K`, `1M`) in BINARY units, the way
960
+ * context windows are quoted: 32K → 32 768 (32 × 1024), 128K → 131 072,
961
+ * 1M → 1 048 576 (1024 × 1024), so the common power-of-two windows land on
962
+ * their exact values instead of 1000-based rounding.
963
+ *
964
+ * Plain text uses the {@link numberField} parse plus an integer check;
965
+ * shorthand converts to whole tokens first. Empty-clears and bounds
966
+ * errors stay identical, and the converted value is bounds-checked against the same
967
+ * 1 – 10,000,000 range (the Host schema would reject an overshoot at save
968
+ * time with only a generic failure; naming `tooLarge` here says which).
969
+ * `Math.round` keeps fractional shorthand like `1.5K` on whole tokens.
970
+ */
971
+ const COMPACT_TOKENS = /^(\d+(?:\.\d+)?)\s*([kKmM])$/;
972
+ function contextWindowField(field, bounds) {
973
+ const plain = numberField$1(field, bounds);
974
+ return {
975
+ field,
976
+ format: plain.format,
977
+ parse: (text) => {
978
+ const compact = COMPACT_TOKENS.exec(text.trim());
979
+ if (compact === null) {
980
+ const parsed = plain.parse(text);
981
+ return parsed.kind === "set" && !isValidContextWindow(parsed.value) ? {
982
+ kind: "invalid",
983
+ reason: "format"
984
+ } : parsed;
985
+ }
986
+ const value = Math.round(Number(compact[1]) * (compact[2].toLowerCase() === "k" ? 1024 : 1048576));
987
+ if (value < bounds.min) return {
988
+ kind: "invalid",
989
+ reason: "tooSmall"
990
+ };
991
+ if (value > bounds.max) return {
992
+ kind: "invalid",
993
+ reason: "tooLarge"
994
+ };
995
+ return {
996
+ kind: "set",
997
+ value
998
+ };
999
+ }
1000
+ };
1001
+ }
1002
+ /**
932
1003
  * A boolean field, staged as the strings `'true'`/`'false'` (an empty draft
933
1004
  * clears it). The component renders a toggle and only ever stages these two
934
1005
  * strings; anything else blocks save.
@@ -1007,6 +1078,10 @@ window.__ModuleLoader__.load({
1007
1078
  min: 0,
1008
1079
  max: 50
1009
1080
  }),
1081
+ contextWindowField("contextWindow", {
1082
+ min: 1,
1083
+ max: CONTEXT_WINDOW_MAX
1084
+ }),
1010
1085
  booleanField$1("offloadSeenImagesForCache"),
1011
1086
  booleanField$1("filterModelsByPlan"),
1012
1087
  booleanField$1("webSearch"),
@@ -1034,6 +1109,8 @@ window.__ModuleLoader__.load({
1034
1109
  accountResult;
1035
1110
  /** Staged visible-model allowlist (undefined = no draft). */
1036
1111
  visibleModelsDraft = void 0;
1112
+ /** Staged per-model context-length overrides as raw text (undefined = no draft, issue #86). */
1113
+ contextWindowOverridesDraft = void 0;
1037
1114
  catalogModels = [];
1038
1115
  catalogFailed = false;
1039
1116
  saving = false;
@@ -1061,6 +1138,7 @@ window.__ModuleLoader__.load({
1061
1138
  this.writer.dispose();
1062
1139
  this.staged.clear();
1063
1140
  this.visibleModelsDraft = void 0;
1141
+ this.contextWindowOverridesDraft = void 0;
1064
1142
  for (const dispose of this.disposers) dispose();
1065
1143
  this.disposers.length = 0;
1066
1144
  this.listeners.clear();
@@ -1122,10 +1200,12 @@ window.__ModuleLoader__.load({
1122
1200
  accountEnrollmentRefs: this.enrollmentRefs(false),
1123
1201
  visibleModels: this.effectiveVisibleModels(),
1124
1202
  visibleModelsAll: this.visibleModelsDraft === void 0 ? this.storedShowsAll() : this.visibleModelsDraft.length === 0,
1203
+ contextWindow: this.field("contextWindow"),
1204
+ contextWindowOverrides: this.effectiveContextWindowOverrides(),
1125
1205
  catalogModels: this.catalogModels,
1126
1206
  catalogFailed: this.catalogFailed,
1127
- dirty: plan.length > 0 || this.visibleModelsDirty(),
1128
- invalid: plan.some((item) => item.change === void 0),
1207
+ dirty: plan.length > 0 || this.visibleModelsDirty() || this.contextWindowOverridesDirty(),
1208
+ invalid: plan.some((item) => item.change === void 0) || this.effectiveContextWindowOverrides().some((row) => row.invalid),
1129
1209
  saving: this.saving,
1130
1210
  failed: this.failed,
1131
1211
  savedCount: this.savedCount,
@@ -1173,9 +1253,10 @@ window.__ModuleLoader__.load({
1173
1253
  }
1174
1254
  /** Discard every staged edit. */
1175
1255
  discard() {
1176
- if (this.staged.size === 0 && this.visibleModelsDraft === void 0 && !this.failed) return;
1256
+ if (this.staged.size === 0 && this.visibleModelsDraft === void 0 && this.contextWindowOverridesDraft === void 0 && !this.failed) return;
1177
1257
  this.staged.clear();
1178
1258
  this.visibleModelsDraft = void 0;
1259
+ this.contextWindowOverridesDraft = void 0;
1179
1260
  this.failed = false;
1180
1261
  this.saveResult = void 0;
1181
1262
  this.publish();
@@ -1202,9 +1283,12 @@ window.__ModuleLoader__.load({
1202
1283
  if (this.disposed || this.saving) return;
1203
1284
  const submitted = new Map(this.staged);
1204
1285
  const submittedVisible = this.visibleModelsDraft;
1286
+ const submittedContextOverrides = this.contextWindowOverridesDraft;
1205
1287
  const plan = this.plan();
1206
1288
  const visibleDirty = this.visibleModelsDirty();
1207
- if (plan.length === 0 && !visibleDirty || plan.some((item) => item.change === void 0)) return;
1289
+ const contextOverrides = this.parsedContextWindowOverrides();
1290
+ const contextOverridesDirty = this.contextWindowOverridesDirty();
1291
+ if (plan.length === 0 && !visibleDirty && !contextOverridesDirty || plan.some((item) => item.change === void 0) || contextOverrides.invalid) return;
1208
1292
  const ops = plan.flatMap((item) => typeof item.change === "object" ? [item.change] : []);
1209
1293
  const key = plan.find((item) => item.field === "apiKey")?.change;
1210
1294
  this.saving = true;
@@ -1214,6 +1298,7 @@ window.__ModuleLoader__.load({
1214
1298
  kind: "save",
1215
1299
  ops,
1216
1300
  ...visibleDirty ? { visibleModels: submittedVisible ?? [] } : {},
1301
+ ...contextOverridesDirty ? { contextWindowOverrides: contextOverrides.values } : {},
1217
1302
  ...typeof key === "string" ? { key } : {}
1218
1303
  });
1219
1304
  this.publish();
@@ -1225,6 +1310,7 @@ window.__ModuleLoader__.load({
1225
1310
  if (!this.failed) this.savedCount += 1;
1226
1311
  for (const [field, draft] of submitted) if ((field === "apiKey" ? outcome.credential === "confirmed" && outcome.issue === void 0 : outcome.config === "confirmed" || writeCompleted(outcome)) && this.staged.get(field) === draft) this.staged.delete(field);
1227
1312
  if ((outcome.config === "confirmed" || writeCompleted(outcome)) && this.visibleModelsDraft === submittedVisible) this.visibleModelsDraft = void 0;
1313
+ if ((outcome.config === "confirmed" || writeCompleted(outcome)) && this.contextWindowOverridesDraft === submittedContextOverrides) this.contextWindowOverridesDraft = void 0;
1228
1314
  this.publish();
1229
1315
  return outcome;
1230
1316
  }
@@ -1242,6 +1328,63 @@ window.__ModuleLoader__.load({
1242
1328
  this.saveResult = void 0;
1243
1329
  this.publish();
1244
1330
  }
1331
+ /**
1332
+ * Stage the whole per-model context-length map (issue #86): one entry per
1333
+ * row, the value the row's input shows verbatim (blank = drop the override
1334
+ * on save). The component rebuilds the map from its projected rows, so a
1335
+ * model reselect moves the value to its new key in the same edit.
1336
+ */
1337
+ editContextWindowOverrides(next) {
1338
+ this.contextWindowOverridesDraft = { ...next };
1339
+ this.failed = false;
1340
+ this.saveResult = void 0;
1341
+ this.publish();
1342
+ }
1343
+ /** The stored per-model overrides, already cleaned by the shared validity rule. */
1344
+ storedContextWindowOverrides() {
1345
+ return { ...readContextWindowOverrides(this.sectionValue("contextWindowOverrides")) };
1346
+ }
1347
+ /** Effective rows: the staged draft verbatim, or the stored values formatted. */
1348
+ effectiveContextWindowOverrides() {
1349
+ const draft = this.contextWindowOverridesDraft;
1350
+ const source = draft !== void 0 ? draft : Object.fromEntries(Object.entries(this.storedContextWindowOverrides()).map(([model, value]) => [model, String(value)]));
1351
+ return Object.entries(source).filter(([model]) => model !== "").map(([model, text]) => {
1352
+ const parsed = this.spec("contextWindow").parse(text);
1353
+ return {
1354
+ model,
1355
+ text,
1356
+ invalid: parsed.kind === "invalid",
1357
+ invalidReason: parsed.kind === "invalid" ? parsed.reason : void 0
1358
+ };
1359
+ });
1360
+ }
1361
+ /**
1362
+ * The draft parsed for the write: blank rows and unparseable rows drop out
1363
+ * (an invalid row blocks save on its own), everything else is the value the
1364
+ * writer puts at `contextWindowOverrides`.
1365
+ */
1366
+ parsedContextWindowOverrides() {
1367
+ const values = {};
1368
+ let invalid = false;
1369
+ for (const row of this.effectiveContextWindowOverrides()) {
1370
+ if (row.invalid) {
1371
+ invalid = true;
1372
+ continue;
1373
+ }
1374
+ const parsed = this.spec("contextWindow").parse(row.text);
1375
+ if (parsed.kind === "set" && typeof parsed.value === "number") values[row.model] = parsed.value;
1376
+ }
1377
+ return {
1378
+ values,
1379
+ invalid
1380
+ };
1381
+ }
1382
+ contextWindowOverridesDirty() {
1383
+ if (this.contextWindowOverridesDraft === void 0) return false;
1384
+ const values = this.parsedContextWindowOverrides().values;
1385
+ const stored = this.storedContextWindowOverrides();
1386
+ return [.../* @__PURE__ */ new Set([...Object.keys(values), ...Object.keys(stored)])].some((model) => values[model] !== stored[model]);
1387
+ }
1245
1388
  renameAccount(ref, label) {
1246
1389
  return this.runAccountOp("rename", {
1247
1390
  kind: "rename",
@@ -1519,12 +1662,51 @@ window.__ModuleLoader__.load({
1519
1662
  function formatMoneyExact(value) {
1520
1663
  return `$${value.toFixed(4)}`;
1521
1664
  }
1522
- function formatTokensCompact(value) {
1523
- if (value >= 1e9) return `${(value / 1e9).toFixed(1)}B`;
1524
- if (value >= 1e6) return `${(value / 1e6).toFixed(1)}M`;
1525
- if (value >= 1e3) return `${(value / 1e3).toFixed(1)}K`;
1665
+ /**
1666
+ * 紧凑数字:K/M/B 三级,一切档位决策的唯一实现。
1667
+ *
1668
+ * 档位必须**回头判断**:先除以档位再四舍五入会算出 `999_999 → "1000.0K"`、
1669
+ * `999_999_999 → "1000.0M"` 这类既超长、又落在并不存在的档位上的结果,
1670
+ * 因此每个档位算完后若已经够到下一档,就改用下一档重算一次。
1671
+ *
1672
+ * @param value - 非负计数或词元数。
1673
+ * @param keepZeroDecimal - 整数值是否保留一位小数。词元显示沿用 `3.0B` 的旧样式;
1674
+ * 计数显示去掉多余的 `.0`,见 {@link formatCompactCount}。
1675
+ * @returns 紧凑文本;不足 1000 时原样返回该整数。
1676
+ */
1677
+ function compact(value, keepZeroDecimal) {
1678
+ const rounded = (scaled) => Math.round(scaled * 10) / 10;
1679
+ const text = (scaled) => {
1680
+ const fixed = rounded(scaled);
1681
+ return keepZeroDecimal ? fixed.toFixed(1) : String(fixed);
1682
+ };
1683
+ if (value >= 1e9) return `${text(value / 1e9)}B`;
1684
+ if (value >= 1e6) return rounded(value / 1e6) >= 1e3 ? `${text(value / 1e9)}B` : `${text(value / 1e6)}M`;
1685
+ if (value >= 1e3) return rounded(value / 1e3) >= 1e3 ? `${text(value / 1e6)}M` : `${text(value / 1e3)}K`;
1526
1686
  return String(value);
1527
1687
  }
1688
+ /**
1689
+ * 紧凑计数:请求数、成功/失败数这类会被窄卡片截断的整数。
1690
+ * `1234` → `1.2K`、`12000` → `12K`、`1500000` → `1.5M`,整数不带多余的 `.0`。
1691
+ */
1692
+ function formatCompactCount(value) {
1693
+ return compact(value, false);
1694
+ }
1695
+ /** 紧凑词元数:与 {@link formatCompactCount} 共用档位,但整数值保留一位小数(`3.0B`)。 */
1696
+ function formatTokensCompact(value) {
1697
+ return compact(value, true);
1698
+ }
1699
+ /**
1700
+ * 精确值悬停提示:只在紧凑显示确实丢了信息时才返回文本。
1701
+ * 未换算时悬停弹出的文字与卡片上写的一模一样,没有意义,因此返回 `undefined` 让调用方
1702
+ * 整个省略 `title`,而不是挂一个内容重复的提示。
1703
+ *
1704
+ * @param parts - 标签与精确数值,按卡片上的显示顺序。
1705
+ * @returns 形如 `请求数 1234567 · 成功 1234567 · 失败 123` 的提示,或 `undefined`。
1706
+ */
1707
+ function compactCountTitle(parts) {
1708
+ return parts.some(([, value]) => formatCompactCount(value) !== String(value)) ? parts.map(([label, value]) => `${label} ${value}`).join(" · ") : void 0;
1709
+ }
1528
1710
  /** The input is already in percent units; callers add the percent sign. */
1529
1711
  function formatSuccessRate(value) {
1530
1712
  return String(Number(value.toFixed(2)));
@@ -2824,6 +3006,18 @@ window.__ModuleLoader__.load({
2824
3006
  visibleModelsShowAll: "显示全部",
2825
3007
  visibleModelsStaleHint: "有 {count} 个已选模型在目录中找不到了(可能已下架),不影响其他模型;可清理或保留。",
2826
3008
  visibleModelsCleanStale: "清理失效({count})",
3009
+ contextWindow: "自定义上下文长度",
3010
+ contextWindowHint: "覆盖模型目录公布的上下文窗口(token):上下文计量、自动压缩时机与模型选择器的上下文标注都按此值计算,对未单独设置的模型生效。留空则跟随目录;支持 32K、1M 这样的简写(二进制换算:32K = 32 768,1M = 1 048 576)。",
3011
+ contextOverridesTitle: "按模型覆盖",
3012
+ contextOverridesHint: "为单个模型单独设置上下文长度,优先于上面的全局值;模型下拉只列出模型白名单中的可见模型(显示全部时即整个目录)。",
3013
+ contextOverrideAdd: "添加模型",
3014
+ contextOverrideRemove: "移除该模型的上下文覆盖",
3015
+ contextOverrideModel: "模型",
3016
+ contextOverrideValue: "上下文长度",
3017
+ contextOverrideNoCatalog: "模型目录尚未加载,暂时无法添加按模型覆盖。",
3018
+ contextWindowTooSmall: "上下文长度不能小于 1",
3019
+ contextWindowTooLarge: "上下文长度不能大于 10,000,000",
3020
+ contextWindowInvalid: "请输入有效的整数 token 数",
2827
3021
  overridden: "已覆盖",
2828
3022
  reset: "重置",
2829
3023
  invalidNumber: "无效数字",
@@ -2949,7 +3143,7 @@ window.__ModuleLoader__.load({
2949
3143
  modelPick: "选择模型…",
2950
3144
  modelCount: "已选 {count} 个模型",
2951
3145
  modelCatalogFailed: "模型目录获取失败,暂时无法选择模型;已保存的设置仍会生效。",
2952
- modelsTitle: "模型",
3146
+ modelsTitle: "模型与上下文",
2953
3147
  privacyTitle: "隐私与安全",
2954
3148
  integrationsTitle: "集成与显示"
2955
3149
  };
@@ -3002,6 +3196,18 @@ window.__ModuleLoader__.load({
3002
3196
  visibleModelsShowAll: "Show all",
3003
3197
  visibleModelsStaleHint: "{count} selected model(s) are no longer in the catalog (possibly retired); other models are unaffected. Clean them up or keep them.",
3004
3198
  visibleModelsCleanStale: "Clean stale ({count})",
3199
+ contextWindow: "Custom context length",
3200
+ contextWindowHint: "Overrides the context window published by the model catalog (in tokens): the context meter, auto-compaction timing and the model-picker annotation all follow it, for every model without a per-model override below. Empty follows the catalog; shorthand like 32K or 1M is accepted (binary units: 32K is 32,768, 1M is 1,048,576).",
3201
+ contextOverridesTitle: "Per-model overrides",
3202
+ contextOverridesHint: "Set a context length for one model; it wins over the global value above. The picker lists only the models kept by the visible-models allowlist (the whole catalog while showing all).",
3203
+ contextOverrideAdd: "Add model",
3204
+ contextOverrideRemove: "Remove this model’s context override",
3205
+ contextOverrideModel: "Model",
3206
+ contextOverrideValue: "Context length",
3207
+ contextOverrideNoCatalog: "The model catalog has not loaded yet; per-model overrides cannot be added right now.",
3208
+ contextWindowTooSmall: "Context length must be at least 1",
3209
+ contextWindowTooLarge: "Context length must not exceed 10,000,000",
3210
+ contextWindowInvalid: "Enter a valid whole-token count",
3005
3211
  overridden: "Overridden",
3006
3212
  reset: "Reset",
3007
3213
  invalidNumber: "Invalid number",
@@ -3127,7 +3333,7 @@ window.__ModuleLoader__.load({
3127
3333
  modelPick: "Choose models…",
3128
3334
  modelCount: "{count} models selected",
3129
3335
  modelCatalogFailed: "The model catalog could not be loaded; saved settings still apply.",
3130
- modelsTitle: "Models",
3336
+ modelsTitle: "Models & context",
3131
3337
  privacyTitle: "Privacy & security",
3132
3338
  integrationsTitle: "Integrations & display"
3133
3339
  };
@@ -3270,6 +3476,19 @@ window.__ModuleLoader__.load({
3270
3476
  return selected.includes(modelId) ? selected.filter((value) => value !== modelId) : [...selected, modelId];
3271
3477
  }
3272
3478
  /**
3479
+ * The catalog rows a picker may offer under the visible-models allowlist, in
3480
+ * catalog order: the whole catalog while "show all" is active (the empty
3481
+ * allowlist means exactly that), otherwise only the allowlisted ids. The
3482
+ * per-model context override editor uses this so its dropdown agrees with the
3483
+ * whitelist on the same card stack — a staged whitelist edit narrows it
3484
+ * immediately, since both read the projected page state.
3485
+ */
3486
+ function allowlistedModels(catalog, allowlist, showAll) {
3487
+ if (showAll) return [...catalog];
3488
+ const keep = new Set(allowlist);
3489
+ return catalog.filter((model) => keep.has(model.id));
3490
+ }
3491
+ /**
3273
3492
  * Whether the catalog is trustworthy enough to call an unlisted selection
3274
3493
  * "retired". FALSE while the first fetch is in flight, after a failure, and
3275
3494
  * for a loaded-but-empty catalog — an empty list makes every selected id look
@@ -3291,7 +3510,7 @@ window.__ModuleLoader__.load({
3291
3510
  }
3292
3511
  //#endregion
3293
3512
  //#region package.json
3294
- var version = "0.13.3";
3513
+ var version = "0.13.4";
3295
3514
  var repository = {
3296
3515
  "type": "git",
3297
3516
  "url": "git+https://github.com/Mars-Sea/dsh-commandcode-provider.git"
@@ -3474,11 +3693,12 @@ window.__ModuleLoader__.load({
3474
3693
  *
3475
3694
  * Layout, top to bottom: Accounts (one card per account, status, quota,
3476
3695
  * actions and dedicated models — every operation commits immediately), Models
3477
- * (plan filter + visible-model allowlist), Privacy & security (ZDR),
3478
- * Integrations & display (web search, sidebar quota card), and a collapsed
3479
- * Advanced card (API base + network limits). Everything below Accounts is a
3480
- * flat group of rows (title/description left, control right) inside the staged
3481
- * form the floating save bar writes.
3696
+ * & context (plan filter + visible-model allowlist, then the context-length
3697
+ * rows: the global custom window and the per-model overrides, issue #86),
3698
+ * Privacy & security (ZDR), Integrations & display (web search, sidebar quota
3699
+ * card), and a collapsed Advanced card (API base + network limits). Everything
3700
+ * below Accounts is a flat group of rows (title/description left, control
3701
+ * right) inside the staged form the floating save bar writes.
3482
3702
  *
3483
3703
  * Styles are injected once by the client entry (see src/client/index.ts) and
3484
3704
  * class-prefixed `cc-` to stay local.
@@ -3549,7 +3769,7 @@ window.__ModuleLoader__.load({
3549
3769
  }) : null;
3550
3770
  }
3551
3771
  /** One text/number field row. */
3552
- function Field({ id, label, hint, state, disabled, numeric, decimal, wide, placeholder, onEdit, onReset, t }) {
3772
+ function Field({ id, label, hint, state, disabled, numeric, decimal, context, wide, placeholder, onEdit, onReset, t }) {
3553
3773
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SettingRow, {
3554
3774
  title: label,
3555
3775
  titleFor: id,
@@ -3558,7 +3778,7 @@ window.__ModuleLoader__.load({
3558
3778
  t
3559
3779
  }),
3560
3780
  description: hint,
3561
- error: state.invalid ? invalidCopy(state.invalidReason, t, decimal) : void 0,
3781
+ error: state.invalid ? invalidCopy(state.invalidReason, t, context === true ? "context" : decimal === true ? "duration" : "number") : void 0,
3562
3782
  control: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(FieldOverride, {
3563
3783
  label,
3564
3784
  state,
@@ -3579,12 +3799,17 @@ window.__ModuleLoader__.load({
3579
3799
  });
3580
3800
  }
3581
3801
  /** The per-field error copy for a staged draft's failure reason. */
3582
- function invalidCopy(reason, t, duration = false) {
3583
- if (duration) {
3802
+ function invalidCopy(reason, t, mode = "number") {
3803
+ if (mode === "duration") {
3584
3804
  if (reason === "tooSmall") return t("durationTooSmall");
3585
3805
  if (reason === "tooLarge") return t("durationTooLarge");
3586
3806
  return t("durationInvalid");
3587
3807
  }
3808
+ if (mode === "context") {
3809
+ if (reason === "tooSmall") return t("contextWindowTooSmall");
3810
+ if (reason === "tooLarge") return t("contextWindowTooLarge");
3811
+ return t("contextWindowInvalid");
3812
+ }
3588
3813
  if (reason === "tooSmall") return t("numberTooSmall");
3589
3814
  if (reason === "tooLarge") return t("numberTooLarge");
3590
3815
  return t("invalidNumber");
@@ -3622,9 +3847,10 @@ window.__ModuleLoader__.load({
3622
3847
  });
3623
3848
  }
3624
3849
  /** One stat tile in the account card's summary grid. */
3625
- function UsageStat({ label, value, sub }) {
3850
+ function UsageStat({ label, value, sub, title }) {
3626
3851
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3627
3852
  className: "cc-usageStat",
3853
+ title,
3628
3854
  children: [
3629
3855
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3630
3856
  className: "cc-usageStatLabel",
@@ -3757,8 +3983,13 @@ window.__ModuleLoader__.load({
3757
3983
  children: [
3758
3984
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(UsageStat, {
3759
3985
  label: t("usageRequests"),
3760
- value: String(report.usage.totalCount),
3761
- sub: `${t("usageCompleted")} ${report.usage.completedCount} / ${t("usageFailed")} ${report.usage.failedCount}`
3986
+ value: formatCompactCount(report.usage.totalCount),
3987
+ sub: `${t("usageCompleted")} ${formatCompactCount(report.usage.completedCount)} / ${t("usageFailed")} ${formatCompactCount(report.usage.failedCount)}`,
3988
+ title: compactCountTitle([
3989
+ [t("usageRequests"), report.usage.totalCount],
3990
+ [t("usageCompleted"), report.usage.completedCount],
3991
+ [t("usageFailed"), report.usage.failedCount]
3992
+ ])
3762
3993
  }),
3763
3994
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(UsageStat, {
3764
3995
  label: t("usageSuccessRate"),
@@ -4025,31 +4256,189 @@ window.__ModuleLoader__.load({
4025
4256
  });
4026
4257
  }
4027
4258
  /**
4028
- * Models: the plan filter and the allowlist sit together because both decide
4029
- * what the model picker lists (the allowlist applies after the plan filter).
4259
+ * Models & context: the plan filter and the allowlist sit together because both
4260
+ * decide what the model picker lists (the allowlist applies after the plan
4261
+ * filter), and the context-length fields follow them because they are the same
4262
+ * subject seen from the other side — the window each listed model advertises.
4263
+ * Those fields carry no heading or divider of their own: they are rows of this
4264
+ * group, not a section inside it.
4030
4265
  */
4031
- function ModelsCard({ state, disabled, t, onEdit, onReset, onSelect, onClear }) {
4266
+ function ModelsCard({ state, disabled, t, onEdit, onReset, onSelect, onClear, onOverrides }) {
4032
4267
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(SettingsGroup, {
4033
4268
  title: t("modelsTitle"),
4034
4269
  divided: true,
4035
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToggleField, {
4036
- id: "cc-filter-models-by-plan",
4037
- label: t("filterModelsByPlan"),
4038
- hint: t("filterModelsByPlanHint"),
4039
- state: state.filterModelsByPlan,
4270
+ children: [
4271
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToggleField, {
4272
+ id: "cc-filter-models-by-plan",
4273
+ label: t("filterModelsByPlan"),
4274
+ hint: t("filterModelsByPlanHint"),
4275
+ state: state.filterModelsByPlan,
4276
+ disabled,
4277
+ defaultChecked: true,
4278
+ onEdit: (text) => onEdit("filterModelsByPlan", text),
4279
+ onReset: () => onReset("filterModelsByPlan"),
4280
+ t
4281
+ }),
4282
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(VisibleModelsRow, {
4283
+ t,
4284
+ state,
4285
+ disabled,
4286
+ onSelect,
4287
+ onClear
4288
+ }),
4289
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ContextCard, {
4290
+ state,
4291
+ disabled,
4292
+ t,
4293
+ onEdit,
4294
+ onReset,
4295
+ onOverrides
4296
+ })
4297
+ ]
4298
+ });
4299
+ }
4300
+ /**
4301
+ * Context length (issue #86): the global custom window plus one row per model
4302
+ * override, both staged into the same save as the other form fields. Rendered
4303
+ * as plain rows of the Models & context group — no heading, no divider of its
4304
+ * own: the setting belongs to the models that group already governs, and on the
4305
+ * real page a section inside the group read as visual noise. The advertised
4306
+ * window is what the harness meters context against and when it compacts, so a
4307
+ * lowered value compacts earlier and a raised one follows the model's real
4308
+ * capacity when the catalog under-reports it. Precedence: a per-model row, then
4309
+ * the global value, then the catalog.
4310
+ *
4311
+ * The rows live as raw text drafts: blank drops the override on save, an
4312
+ * unparseable one blocks the save with its own bounds copy, and the value
4313
+ * accepts the compact shorthand the picker prints (`256K`, `1M`). The model
4314
+ * select offers only allowlisted models the list does not already carry —
4315
+ * one override per model, matching what the Models card lets through — and
4316
+ * keeps a stored id the catalog no longer carries (or the whitelist just
4317
+ * dropped) as itself so an existing override never silently disappears.
4318
+ */
4319
+ function ContextCard({ state, disabled, t, onEdit, onReset, onOverrides }) {
4320
+ const rows = state.contextWindowOverrides;
4321
+ const used = new Set(rows.map((row) => row.model));
4322
+ const available = allowlistedModels(state.catalogModels, state.visibleModels, state.visibleModelsAll).filter((model) => !used.has(model.id));
4323
+ const asMap = () => Object.fromEntries(rows.map((row) => [row.model, row.text]));
4324
+ const setText = (model, text) => onOverrides({
4325
+ ...asMap(),
4326
+ [model]: text
4327
+ });
4328
+ const setModel = (from, to) => {
4329
+ if (to === "" || to === from || used.has(to)) return;
4330
+ const next = {};
4331
+ for (const row of rows) next[row.model === from ? to : row.model] = row.text;
4332
+ onOverrides(next);
4333
+ };
4334
+ const removeRow = (model) => {
4335
+ const next = asMap();
4336
+ delete next[model];
4337
+ onOverrides(next);
4338
+ };
4339
+ const addRow = () => {
4340
+ const model = available[0];
4341
+ if (model === void 0) return;
4342
+ onOverrides({
4343
+ ...asMap(),
4344
+ [model.id]: ""
4345
+ });
4346
+ };
4347
+ const labelOf = (model) => {
4348
+ const entry = state.catalogModels.find((candidate) => candidate.id === model);
4349
+ return entry === void 0 ? `${model} · ${t("modelStale")}` : `${entry.name} (${model})`;
4350
+ };
4351
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
4352
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
4353
+ id: "cc-context-window",
4354
+ label: t("contextWindow"),
4355
+ hint: t("contextWindowHint"),
4356
+ state: state.contextWindow,
4040
4357
  disabled,
4041
- defaultChecked: true,
4042
- onEdit: (text) => onEdit("filterModelsByPlan", text),
4043
- onReset: () => onReset("filterModelsByPlan"),
4358
+ context: true,
4359
+ onEdit: (text) => onEdit("contextWindow", text),
4360
+ onReset: () => onReset("contextWindow"),
4044
4361
  t
4045
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(VisibleModelsRow, {
4046
- t,
4047
- state,
4048
- disabled,
4049
- onSelect,
4050
- onClear
4051
- })]
4052
- });
4362
+ }),
4363
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SettingRow, {
4364
+ title: t("contextOverridesTitle"),
4365
+ description: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: t("contextOverridesHint") }),
4366
+ control: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4367
+ type: "button",
4368
+ className: "cc-linkButton",
4369
+ disabled: disabled || available.length === 0,
4370
+ onClick: addRow,
4371
+ children: t("contextOverrideAdd")
4372
+ })
4373
+ }),
4374
+ state.catalogFailed ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
4375
+ className: "cc-groupDesc",
4376
+ children: t("contextOverrideNoCatalog")
4377
+ }) : null,
4378
+ rows.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4379
+ className: "cc-contextHead",
4380
+ children: [
4381
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4382
+ className: "cc-contextHeadModel",
4383
+ children: t("contextOverrideModel")
4384
+ }),
4385
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4386
+ className: "cc-contextHeadValue",
4387
+ children: t("contextOverrideValue")
4388
+ }),
4389
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4390
+ className: "cc-contextHeadGap",
4391
+ "aria-hidden": "true"
4392
+ })
4393
+ ]
4394
+ }) : null,
4395
+ rows.map((row) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4396
+ className: "cc-contextItem",
4397
+ children: [
4398
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
4399
+ className: "cc-contextSelect",
4400
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
4401
+ className: "cc-input cc-contextModel",
4402
+ value: row.model,
4403
+ "aria-label": t("contextOverrideModel"),
4404
+ disabled,
4405
+ onChange: (event) => setModel(row.model, event.target.value),
4406
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
4407
+ value: row.model,
4408
+ children: labelOf(row.model)
4409
+ }), available.map((model) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
4410
+ value: model.id,
4411
+ children: labelOf(model.id)
4412
+ }, model.id))]
4413
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4414
+ className: "cc-contextCaret",
4415
+ "aria-hidden": "true"
4416
+ })]
4417
+ }),
4418
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
4419
+ className: `cc-input cc-contextValue${row.invalid ? " cc-inputInvalid" : ""}`,
4420
+ type: "text",
4421
+ "aria-label": t("contextOverrideValue"),
4422
+ "aria-invalid": row.invalid || void 0,
4423
+ value: row.text,
4424
+ disabled,
4425
+ onChange: (event) => setText(row.model, event.target.value)
4426
+ }),
4427
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4428
+ type: "button",
4429
+ className: "cc-iconButton",
4430
+ "aria-label": t("contextOverrideRemove"),
4431
+ disabled,
4432
+ onClick: () => removeRow(row.model),
4433
+ children: "×"
4434
+ }),
4435
+ row.invalid ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
4436
+ className: "cc-rowError cc-contextError",
4437
+ children: invalidCopy(row.invalidReason, t, "context")
4438
+ }) : null
4439
+ ]
4440
+ }, row.model))
4441
+ ] });
4053
4442
  }
4054
4443
  /**
4055
4444
  * Privacy & security: the opt-in that changes where data goes. ZDR restricts
@@ -5279,7 +5668,8 @@ window.__ModuleLoader__.load({
5279
5668
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelsCard, {
5280
5669
  ...form,
5281
5670
  onSelect: props.editVisibleModels,
5282
- onClear: props.clearVisibleModels
5671
+ onClear: props.clearVisibleModels,
5672
+ onOverrides: props.editContextWindowOverrides
5283
5673
  }),
5284
5674
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PrivacyCard, { ...form }),
5285
5675
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IntegrationsCard, { ...form }),
@@ -5608,11 +5998,22 @@ window.__ModuleLoader__.load({
5608
5998
  analysisNoShare: "无占比",
5609
5999
  analysisShareHint: "占全部已知成本小计",
5610
6000
  analysisSimulationHeading: "相同工作量价格试算",
5611
- analysisSimulationHint: "计价覆盖不同的金额不能直接比较。",
6001
+ analysisSimulationHint: "差额以所选历史全部请求的当前价估算为基准;负数表示成本更低,仅比较完整覆盖的相同工作量。",
5612
6002
  analysisSimulationCost: "试算成本",
5613
6003
  analysisCoverage: "计价覆盖",
5614
6004
  analysisUsd: "美元",
5615
6005
  analysisTokenUnit: "词元",
6006
+ analysisScenarioMonthly: "折算 30 天",
6007
+ analysisDifference: "较历史基准",
6008
+ analysisBaseline: "历史成本基准",
6009
+ analysisZeroBaseline: "基准为零,不计算比例",
6010
+ analysisNeedRange: "请选择日期范围",
6011
+ analysisShortRange: "观察范围不足一天",
6012
+ analysisIncomplete: "历史读取不完整",
6013
+ analysisNoRequests: "没有请求记录",
6014
+ analysisScenarioExcluded: "存在超上下文请求",
6015
+ analysisScenarioUnpriced: "存在未知用量或价格",
6016
+ analysisBaselineUnpriced: "历史基准未全部计价",
5616
6017
  analysisCost: "已知成本 · 美元",
5617
6018
  analysisRequests: "模型请求",
5618
6019
  analysisSessionCount: "会话数",
@@ -5650,6 +6051,7 @@ window.__ModuleLoader__.load({
5650
6051
  analysisAssumption: "沿用原请求的词元与时段,对比不同模型的价格。换模型后的生成长度、缓存命中、重试和质量可能变化;不会调用模型或自动推荐。",
5651
6052
  analysisContext: "上下文词元",
5652
6053
  analysisCapabilities: "模型能力",
6054
+ analysisInputType: "输入类型",
5653
6055
  analysisEfforts: "推理力度",
5654
6056
  analysisExcluded: "超出上下文",
5655
6057
  analysisUnknownPrice: "未知用量或价格",
@@ -5701,11 +6103,22 @@ window.__ModuleLoader__.load({
5701
6103
  analysisNoShare: ANALYTICS_COPY_ZH.analysisNoShare,
5702
6104
  analysisShareHint: "Share of the known cost subtotal",
5703
6105
  analysisSimulationHeading: "Price simulation for the same workload",
5704
- analysisSimulationHint: "Amounts with different priced coverage are not directly comparable.",
6106
+ analysisSimulationHint: "Differences use all selected recorded requests repriced at current rates as the baseline. Negative means lower cost; only complete coverage is compared.",
5705
6107
  analysisSimulationCost: "Simulated cost",
5706
6108
  analysisCoverage: "Priced coverage",
5707
6109
  analysisUsd: "USD",
5708
6110
  analysisTokenUnit: "tokens",
6111
+ analysisScenarioMonthly: "30-day estimate",
6112
+ analysisDifference: "vs. recorded baseline",
6113
+ analysisBaseline: "Recorded cost baseline",
6114
+ analysisZeroBaseline: ANALYTICS_COPY_ZH.analysisZeroBaseline,
6115
+ analysisNeedRange: ANALYTICS_COPY_ZH.analysisNeedRange,
6116
+ analysisShortRange: ANALYTICS_COPY_ZH.analysisShortRange,
6117
+ analysisIncomplete: ANALYTICS_COPY_ZH.analysisIncomplete,
6118
+ analysisNoRequests: ANALYTICS_COPY_ZH.analysisNoRequests,
6119
+ analysisScenarioExcluded: ANALYTICS_COPY_ZH.analysisScenarioExcluded,
6120
+ analysisScenarioUnpriced: ANALYTICS_COPY_ZH.analysisScenarioUnpriced,
6121
+ analysisBaselineUnpriced: ANALYTICS_COPY_ZH.analysisBaselineUnpriced,
5709
6122
  analysisCost: "Known cost · USD",
5710
6123
  analysisRequests: "Model requests",
5711
6124
  analysisSessionCount: "Sessions",
@@ -5743,6 +6156,7 @@ window.__ModuleLoader__.load({
5743
6156
  analysisAssumption: "Compares model prices using original tokens and request times. Output length, cache hits, retries and quality may change with another model. Makes no model calls or automatic recommendations.",
5744
6157
  analysisContext: "Context tokens",
5745
6158
  analysisCapabilities: "Model capabilities",
6159
+ analysisInputType: "Input type",
5746
6160
  analysisEfforts: "Reasoning efforts",
5747
6161
  analysisExcluded: "Over context limit",
5748
6162
  analysisUnknownPrice: "Unknown usage or price",
@@ -6010,8 +6424,13 @@ window.__ModuleLoader__.load({
6010
6424
  if (usage !== void 0) {
6011
6425
  stats.push({
6012
6426
  label: "requests",
6013
- value: String(usage.totalCount),
6014
- sub: `${usage.completedCount} ${t("completed")} / ${usage.failedCount} ${t("failed")}`
6427
+ value: formatCompactCount(usage.totalCount),
6428
+ sub: `${formatCompactCount(usage.completedCount)} ${t("completed")} / ${formatCompactCount(usage.failedCount)} ${t("failed")}`,
6429
+ title: compactCountTitle([
6430
+ [t("requests"), usage.totalCount],
6431
+ [t("completed"), usage.completedCount],
6432
+ [t("failed"), usage.failedCount]
6433
+ ])
6015
6434
  });
6016
6435
  stats.push({
6017
6436
  label: "successRate",
@@ -6337,6 +6756,44 @@ window.__ModuleLoader__.load({
6337
6756
  }]
6338
6757
  };
6339
6758
  //#endregion
6759
+ //#region src/client/analytics-comparison.ts
6760
+ function coverageReason(report, scenario) {
6761
+ if (report.status !== "ready") return "unavailable";
6762
+ if (report.truncated || report.unreadableSessions > 0 || report.skippedSessions > 0) return "partial";
6763
+ if (report.totals.requests === 0) return "empty";
6764
+ if (scenario.excludedRequests > 0) return "excluded";
6765
+ if (scenario.unknownRequests > 0 || scenario.pricedRequests !== report.totals.requests) return "coverage";
6766
+ }
6767
+ function compareScenario(report, scenario) {
6768
+ const reason = coverageReason(report, scenario);
6769
+ let monthly;
6770
+ if (reason) monthly = {
6771
+ value: null,
6772
+ reason
6773
+ };
6774
+ else if (report.query.mode !== "range") monthly = {
6775
+ value: null,
6776
+ reason: "recent"
6777
+ };
6778
+ else {
6779
+ const elapsed = Math.min(report.query.to, report.generatedAt) - report.query.from;
6780
+ monthly = elapsed >= 864e5 ? { value: scenario.cost * (2592e6 / elapsed) } : {
6781
+ value: null,
6782
+ reason: "short-range"
6783
+ };
6784
+ }
6785
+ const comparisonReason = reason ?? (report.totals.pricedRequests !== report.totals.requests ? "baseline" : void 0);
6786
+ const difference = comparisonReason ? {
6787
+ value: null,
6788
+ reason: comparisonReason
6789
+ } : { value: scenario.cost - report.totals.cost };
6790
+ return {
6791
+ monthly,
6792
+ difference,
6793
+ differencePercent: difference.value !== null && report.totals.cost > 0 ? difference.value / report.totals.cost * 100 : null
6794
+ };
6795
+ }
6796
+ //#endregion
6340
6797
  //#region src/client/analytics-view.tsx
6341
6798
  /** 只展示数字快照;筛选提交后才查询,页签、展开与模型筛选不触发历史读取。 */
6342
6799
  function localDate(time) {
@@ -6587,7 +7044,19 @@ window.__ModuleLoader__.load({
6587
7044
  })]
6588
7045
  });
6589
7046
  }
6590
- function ScenarioRow({ m, text }) {
7047
+ const estimateReasonKeys = {
7048
+ unavailable: "analysisUnavailable",
7049
+ empty: "analysisNoRequests",
7050
+ partial: "analysisIncomplete",
7051
+ excluded: "analysisScenarioExcluded",
7052
+ coverage: "analysisScenarioUnpriced",
7053
+ baseline: "analysisBaselineUnpriced",
7054
+ recent: "analysisNeedRange",
7055
+ "short-range": "analysisShortRange"
7056
+ };
7057
+ const signedMoney = (n) => `${n > 0 ? "+" : n < 0 ? "-" : ""}${money(Math.abs(n))}`;
7058
+ function ScenarioRow({ m, report, text }) {
7059
+ const { monthly, difference, differencePercent } = compareScenario(report, m);
6591
7060
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("details", {
6592
7061
  className: "ccp-analysisModelRow",
6593
7062
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("summary", {
@@ -6606,17 +7075,17 @@ window.__ModuleLoader__.load({
6606
7075
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Metric, {
6607
7076
  label: text("analysisSimulationCost"),
6608
7077
  value: m.pricedRequests ? money(m.cost) : text("analysisMissing"),
6609
- sub: text("analysisUsd")
7078
+ sub: `${text("analysisCoverage")} ${m.pricedRequests}/${report.totals.requests}`
6610
7079
  }),
6611
7080
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Metric, {
6612
- label: text("analysisCoverage"),
6613
- value: `${m.pricedRequests}/${m.pricedRequests + m.excludedRequests + m.unknownRequests}`,
6614
- sub: `${text("analysisExcluded")} ${digits(m.excludedRequests)} · ${text("analysisUnknownPrice")} ${digits(m.unknownRequests)}`
7081
+ label: text("analysisScenarioMonthly"),
7082
+ value: monthly.value === null ? text("analysisMissing") : money(monthly.value),
7083
+ sub: monthly.value === null ? text(estimateReasonKeys[monthly.reason]) : text("analysisUsd")
6615
7084
  }),
6616
7085
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Metric, {
6617
- label: text("analysisCapabilities"),
6618
- value: m.contextWindow === null ? text("analysisMissing") : `${digits(m.contextWindow)} ${text("analysisTokenUnit")}`,
6619
- sub: text(m.images ? "analysisImages" : "analysisTextOnly")
7086
+ label: text("analysisDifference"),
7087
+ value: difference.value === null ? text("analysisMissing") : signedMoney(difference.value),
7088
+ sub: difference.value === null ? text(estimateReasonKeys[difference.reason]) : differencePercent === null ? text("analysisZeroBaseline") : `${differencePercent > 0 ? "+" : ""}${differencePercent.toFixed(1)}%`
6620
7089
  }),
6621
7090
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Chevron, {})
6622
7091
  ]
@@ -6633,7 +7102,11 @@ window.__ModuleLoader__.load({
6633
7102
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DetailGroup, {
6634
7103
  title: "analysisCapabilities",
6635
7104
  text,
6636
- values: [["analysisEfforts", m.efforts.length ? m.efforts.map((e) => effortName(e, text)).join(", ") : text("analysisNoEfforts")]]
7105
+ values: [
7106
+ ["analysisContext", m.contextWindow === null ? text("analysisMissing") : `${digits(m.contextWindow)} ${text("analysisTokenUnit")}`],
7107
+ ["analysisInputType", text(m.images ? "analysisImages" : "analysisTextOnly")],
7108
+ ["analysisEfforts", m.efforts.length ? m.efforts.map((e) => effortName(e, text)).join(", ") : text("analysisNoEfforts")]
7109
+ ]
6637
7110
  })]
6638
7111
  })]
6639
7112
  });
@@ -6935,6 +7408,16 @@ window.__ModuleLoader__.load({
6935
7408
  className: "ccp-analysisAssumption",
6936
7409
  children: text("analysisAssumption")
6937
7410
  }),
7411
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
7412
+ className: "ccp-analysisNote ccp-analysisBaseline",
7413
+ children: [
7414
+ text("analysisBaseline"),
7415
+ ": ",
7416
+ m.pricedRequests === m.requests && m.requests > 0 && !report.truncated && !report.unreadableSessions && !report.skippedSessions ? money(m.cost) : text("analysisMissing"),
7417
+ " · ",
7418
+ text("analysisMonthlyHint")
7419
+ ]
7420
+ }),
6938
7421
  report.catalogUnavailable ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
6939
7422
  className: "ccp-analysisMessage ccp-analysisWarning",
6940
7423
  role: "status",
@@ -6976,12 +7459,13 @@ window.__ModuleLoader__.load({
6976
7459
  children: [
6977
7460
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: text("analysisModel") }),
6978
7461
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: text("analysisSimulationCost") }),
6979
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: text("analysisCoverage") }),
6980
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: text("analysisCapabilities") }),
7462
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: text("analysisScenarioMonthly") }),
7463
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: text("analysisDifference") }),
6981
7464
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {})
6982
7465
  ]
6983
7466
  }), scenarios.map((model) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ScenarioRow, {
6984
7467
  m: model,
7468
+ report,
6985
7469
  text
6986
7470
  }, model.id))]
6987
7471
  }) : !report.catalogUnavailable ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
@@ -7125,9 +7609,10 @@ window.__ModuleLoader__.load({
7125
7609
  });
7126
7610
  }
7127
7611
  /** A labelled figure. */
7128
- function Tile({ label, value, sub }) {
7612
+ function Tile({ label, value, sub, title }) {
7129
7613
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7130
7614
  className: "ccp-tile",
7615
+ title,
7131
7616
  children: [
7132
7617
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
7133
7618
  className: "ccp-tileLabel",
@@ -7299,7 +7784,8 @@ window.__ModuleLoader__.load({
7299
7784
  children: account.stats.map((stat) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Tile, {
7300
7785
  label: text(stat.label),
7301
7786
  value: stat.value,
7302
- sub: stat.sub
7787
+ sub: stat.sub,
7788
+ title: stat.title
7303
7789
  }, stat.label))
7304
7790
  })
7305
7791
  ]
@@ -8430,7 +8916,8 @@ window.__ModuleLoader__.load({
8430
8916
  * 12/18 label-tertiary, the control on the right. The hairline rhythm is
8431
8917
  * section-level: a 0.5px border-l2 rule separates the GROUPS (see
8432
8918
  * `cc-groupDivided`), never the rows inside one, so a group's rows read as one
8433
- * list. Selectors and buttons are capsules (36px / 28px compact), inputs are
8919
+ * list. Selectors and buttons are
8920
+ * capsules (36px / 28px compact), inputs are
8434
8921
  * 32px with an 8px radius and a 0.5px border-l4 edge, a card is a 0.5px
8435
8922
  * border-l4 outline with a 16px radius, and a nested panel is a
8436
8923
  * bg-module-platform fill with a 12px radius. Errors use state-error-primary.
@@ -8537,6 +9024,25 @@ window.__ModuleLoader__.load({
8537
9024
  .cc-iconButton{flex-shrink:0;align-items:center;justify-content:center;display:inline-flex;width:28px;height:28px;padding:0;background:0 0;border:0;border-radius:6px;color:var(--dsw-alias-label-tertiary);cursor:pointer}
8538
9025
  .cc-iconButton:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}
8539
9026
  .cc-iconButton:focus-visible{outline:none;box-shadow:0 0 0 2px var(--dsw-alias-border-l3)}
9027
+ /* Per-model context-length override rows (issue #86): the error wraps onto
9028
+ its own line beneath the select/input/remove trio. The three cells form a
9029
+ fixed column contract — flexible model, 112px value, 28px remove — and the
9030
+ cc-contextHead labels repeat the first two (plus the remove cell's width
9031
+ as a spacer) so they sit exactly over their columns. */
9032
+ .cc-contextItem{align-items:center;gap:8px;display:flex;flex-wrap:wrap;padding:8px 0}
9033
+ .cc-contextHead{align-items:center;gap:8px;display:flex;flex-wrap:wrap;padding:4px 0 0;color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px}
9034
+ .cc-contextHeadModel{flex:1;min-width:200px}
9035
+ .cc-contextHeadValue{flex:none;width:112px}
9036
+ .cc-contextHeadGap{flex:none;width:28px}
9037
+ .cc-contextSelect{position:relative;display:block;flex:1;min-width:200px}
9038
+ /* appearance:none swaps the platform arrow for the cc-contextCaret, shaped
9039
+ like cc-selectorCaret so every dropdown in the plugin opens with the same
9040
+ hand-drawn chevron. */
9041
+ .cc-contextModel{box-sizing:border-box;appearance:none;-webkit-appearance:none;width:100%;padding-inline-end:26px;text-overflow:ellipsis;cursor:pointer}
9042
+ .cc-contextCaret{position:absolute;inset-inline-end:12px;top:50%;width:6px;height:6px;border-right:1.5px solid var(--dsw-alias-label-tertiary);border-bottom:1.5px solid var(--dsw-alias-label-tertiary);transform:translateY(-50%) rotate(45deg);pointer-events:none}
9043
+ .cc-contextSelect:has(.cc-contextModel:disabled) .cc-contextCaret{opacity:.6}
9044
+ .cc-contextValue{flex:none;width:112px}
9045
+ .cc-contextError{flex-basis:100%}
8540
9046
  /* Three dots from one element: the dot plus two box-shadow copies. */
8541
9047
  .cc-kebab{width:3px;height:3px;border-radius:50%;corner-shape:round;background:currentColor;box-shadow:0 -5px 0 currentColor,0 5px 0 currentColor}
8542
9048
  .cc-tabDot{flex-shrink:0;width:8px;height:8px;border-radius:50%;corner-shape:round}
@@ -8848,7 +9354,6 @@ window.__ModuleLoader__.load({
8848
9354
  .ccp-analysisTableHead{padding-top:11px;padding-bottom:11px;background:var(--dsw-alias-interactive-bg-hover);border-bottom:.5px solid var(--dsw-alias-border-l2);font-size:12px;line-height:18px;color:var(--dsw-alias-label-tertiary)}
8849
9355
  .ccp-analysisTableHead > span:not(:first-child){text-align:right}
8850
9356
  .ccp-analysisScenarioHead,.ccp-analysisScenarioSummary{grid-template-columns:minmax(190px,2fr) minmax(118px,1.1fr) minmax(155px,1.35fr) minmax(142px,1.15fr) 16px}
8851
- .ccp-analysisScenarioHead > span:nth-child(3),.ccp-analysisScenarioHead > span:nth-child(4){text-align:left}
8852
9357
  .ccp-analysisModelSummary,.ccp-analysisScenarioSummary{cursor:pointer;list-style:none;min-height:60px}
8853
9358
  .ccp-analysisModelSummary::-webkit-details-marker,.ccp-analysisScenarioSummary::-webkit-details-marker,.ccp-analysisDisclosure::-webkit-details-marker{display:none}
8854
9359
  .ccp-analysisModelSummary:hover,.ccp-analysisScenarioSummary:hover{background:var(--dsw-alias-interactive-bg-hover)}
@@ -8856,7 +9361,6 @@ window.__ModuleLoader__.load({
8856
9361
  .ccp-analysisModelName{font-size:14px;line-height:22px;font-weight:500;overflow-wrap:anywhere}
8857
9362
  .ccp-analysisModelCount{margin-top:3px}
8858
9363
  .ccp-analysisModelSummary > .ccp-analysisMetric,.ccp-analysisScenarioSummary > .ccp-analysisMetric{align-items:flex-end}
8859
- .ccp-analysisScenarioSummary > .ccp-analysisMetric:nth-child(3),.ccp-analysisScenarioSummary > .ccp-analysisMetric:nth-child(4){align-items:flex-start}
8860
9364
  /* 宽表使用集中表头,但保留每个指标的无障碍标签;窄面板恢复可见标签。 */
8861
9365
  .ccp-analysisModelSummary .ccp-analysisMetricLabel,.ccp-analysisScenarioSummary .ccp-analysisMetricLabel,.ccp-analysisSearchLabel{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}
8862
9366
  .ccp-analysisShare{display:flex;align-items:center;gap:8px;width:100%;max-width:148px;margin-top:3px}
@@ -8880,6 +9384,7 @@ window.__ModuleLoader__.load({
8880
9384
  .ccp-analysisMonthlyValue{font-size:14px;line-height:22px;font-weight:500;color:var(--dsw-alias-label-primary);font-variant-numeric:tabular-nums}
8881
9385
  .ccp-analysisMonthly .ccp-analysisNote{width:100%;margin:0}
8882
9386
  .ccp-analysisAssumption{font-size:14px;line-height:22px;color:var(--dsw-alias-label-tertiary);margin:0 0 18px;max-width:76ch}
9387
+ .ccp-analysisBaseline{margin:0 0 16px}
8883
9388
  .ccp-analysisSearch{flex:0 1 270px}
8884
9389
  .ccp-analysisSearchShell{position:relative;display:block}
8885
9390
  .ccp-analysisSearchShell > .ccp-analysisIcon{position:absolute;top:10px;left:11px;color:var(--dsw-alias-label-tertiary);pointer-events:none}
@@ -9265,7 +9770,8 @@ window.__ModuleLoader__.load({
9265
9770
  setActiveAccount: (id) => controller.setActiveAccount(id).then(refreshUsageOn),
9266
9771
  setAccountModels: (target, ids) => controller.setAccountModels(target, ids),
9267
9772
  editVisibleModels: (ids) => controller.editVisibleModels(ids),
9268
- clearVisibleModels: () => controller.clearVisibleModels()
9773
+ clearVisibleModels: () => controller.clearVisibleModels(),
9774
+ editContextWindowOverrides: (next) => controller.editContextWindowOverrides(next)
9269
9775
  });
9270
9776
  ctx.slots.inject("settings.section", () => ctx.slots.register({
9271
9777
  name: "settings.section",