dsh-connect-trae 4.14.2 → 4.16.0

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
@@ -7684,13 +7684,22 @@ ${u} ${p}-pool .sw input:checked+.sw-track::after{transform:translateX(16px)}
7684
7684
  return value.toLocaleString("en-US", { maximumFractionDigits: 0 });
7685
7685
  }
7686
7686
  /**
7687
- * The soonest expiry the chosen tier names, or undefined when none carries one.
7688
- *
7689
- * Read from the tier the shared ladder picked, so the time shown always belongs
7690
- * to the horizon the headline claims — the two cannot disagree.
7687
+ * The notice this panel would render, or undefined when there is nothing to say.
7688
+ *
7689
+ * THE ONE GUARD THE RENDER READS. An earlier version asked
7690
+ * `signedIn?.credits?.expiry !== undefined`, which is true whenever upstream sent
7691
+ * the four buckets — even with every `packs` count at 0. The box then rendered
7692
+ * with an empty `<span>` inside and the `lapsed` border (the tone fallback),
7693
+ * i.e. a warning-shaped rectangle containing nothing. Every empty-bucket
7694
+ * document produced one, which is the blank box in the reported screenshot.
7695
+ *
7696
+ * Putting the whole lookup here means the render cannot ask a weaker question
7697
+ * than the copy does: "is there a box" and "is there a sentence" are now the
7698
+ * same boolean.
7691
7699
  */
7692
- function soonestExpiryOf(expiry) {
7693
- return expiryNotice(expiry)?.soonestExpiresAtMs;
7700
+ function expiryNoticeOf(signedIn) {
7701
+ const expiry = signedIn?.credits?.expiry;
7702
+ return expiry === void 0 ? void 0 : expiryNotice(expiry);
7694
7703
  }
7695
7704
  /**
7696
7705
  * The notice's headline sentence, naming the amount and the pack count.
@@ -8255,12 +8264,12 @@ ${u} ${p}-pool .sw input:checked+.sw-track::after{transform:translateX(16px)}
8255
8264
  }, row.id);
8256
8265
  }) })]
8257
8266
  }),
8258
- activeRegion === "cn" && signedIn?.credits?.expiry !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
8259
- className: `dsm-trae-expiry ${expiryToneClass(expiryNotice(signedIn.credits.expiry)?.tier ?? "lapsed")}`,
8267
+ activeRegion === "cn" && expiryNoticeOf(signedIn) !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
8268
+ className: `dsm-trae-expiry ${expiryToneClass(expiryNoticeOf(signedIn).tier)}`,
8260
8269
  role: "status",
8261
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: expiryMessageOf(signedIn.credits.expiry, t) }), soonestExpiryOf(signedIn.credits.expiry) === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
8270
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: expiryMessageOf(signedIn.credits.expiry, t) }), expiryNoticeOf(signedIn).soonestExpiresAtMs === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
8262
8271
  className: "dsm-trae-expiry-detail",
8263
- children: t("row.expiryEarliest", { time: formatDateTime(soonestExpiryOf(signedIn.credits.expiry)) })
8272
+ children: t("row.expiryEarliest", { time: formatDateTime(expiryNoticeOf(signedIn).soonestExpiresAtMs) })
8264
8273
  })]
8265
8274
  }) : null,
8266
8275
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
@@ -9545,6 +9554,17 @@ ${u} ${p}-pool .sw input:checked+.sw-track::after{transform:translateX(16px)}
9545
9554
  *
9546
9555
  * @module dsh-connect-trae/client/TraeModelLabel
9547
9556
  */
9557
+ /**
9558
+ * Every provider id this plugin owns, in panel order.
9559
+ *
9560
+ * Exported because on a `keyed` host the label seat is one registration PER
9561
+ * provider id — the registration side needs the same list the component uses to
9562
+ * decide "is this row mine", and a second literal would drift from
9563
+ * `providerOfPanel`. Derived rather than typed out: `trae-enterprise` is the id
9564
+ * most easily forgotten, and forgetting it means that panel's rows silently fall
9565
+ * through to the host's plain fallback.
9566
+ */
9567
+ const TRAE_MODEL_LABEL_PROVIDERS = TRAE_PANELS.map(providerOfPanel);
9548
9568
  /** How long a fetched snapshot is trusted, in milliseconds. */
9549
9569
  const CACHE_MS = 6e4;
9550
9570
  /** How often a mounted label re-reads the clock, in milliseconds. */
@@ -9816,6 +9836,17 @@ ${u} ${p}-pool .sw input:checked+.sw-track::after{transform:translateX(16px)}
9816
9836
  * host's own presentation — catalog name plus that model's optional badge — for
9817
9837
  * any provider that is not ours.
9818
9838
  *
9839
+ * WHAT CHANGES WHEN THE HOST KEYS THE SLOT — and why the number stays. On a host
9840
+ * that declares this slot `keyed`, the seat is registered once per provider id
9841
+ * (`./model-label-seat.ts`), each `key` is its own cell, and priority stops
9842
+ * deciding WHO IS VISIBLE: qoder's `-101` and workbuddy's `-105` no longer share
9843
+ * a cell with this `-102`, so none of the three can shadow another and the
9844
+ * armistice below stops being a race. Priority keeps exactly one meaning inside
9845
+ * a cell — the tie-breaker between two registrations claiming the SAME provider
9846
+ * id, where an equal value throws and the lowest renders. The value is therefore
9847
+ * kept as this plugin's rank for its own rows, not as a bid in an auction that
9848
+ * no longer happens.
9849
+ *
9819
9850
  * WHY A DISTINCT PRIORITY IS NOT ENOUGH ON ITS OWN. It removes the cross-plugin
9820
9851
  * collision, but issue #74's error was a SECOND registration of THIS plugin
9821
9852
  * against its own first one (the report's stack shows
@@ -9824,90 +9855,155 @@ ${u} ${p}-pool .sw input:checked+.sw-track::after{transform:translateX(16px)}
9824
9855
  * slot's declaration lifetime collapses and re-declares, so a callback that
9825
9856
  * registers unconditionally double-registers. `index.tsx` therefore also
9826
9857
  * disposes its previous entry before every attempt. Priority alone would have
9827
- * made the throw rarer and harder to read, not absent.
9858
+ * made the throw rarer and harder to read, not absent. (On a `keyed` host the
9859
+ * same rule applies per key: re-registering one provider's seat twice at one
9860
+ * priority throws just as loudly.)
9828
9861
  */
9829
9862
  const MODEL_LABEL_PRIORITY = -102;
9830
- /** The tone colours, mapped onto the same theme tokens the host's Tag paints. */
9863
+ /** The four tones the host understands, as a lookup for narrowing. */
9864
+ const HOST_BADGE_TONE_NAMES = [
9865
+ "neutral",
9866
+ "info",
9867
+ "success",
9868
+ "warning"
9869
+ ];
9870
+ /**
9871
+ * The tone palette, transcribed from the host's `Tag.module.css` (0.2.2-rc.1).
9872
+ *
9873
+ * TWO TOKENS ARE EASY TO MISCOPY, AND BOTH WERE: `info` paints
9874
+ * `state-business-primary` (not `brand-primary`) and `warning` paints
9875
+ * `state-warn-primary` (not `state-warn-secondary`). A near-miss token still
9876
+ * "looks blue" or "looks amber", which is why this survived a review — but two
9877
+ * capsules on one row are compared directly, so the drift is visible.
9878
+ *
9879
+ * The fill is the tone's own colour mixed into transparent (10%, or 12% for
9880
+ * `warning`), and there is NO border: the host's capsule is a fill only.
9881
+ */
9831
9882
  const HOST_BADGE_TONES = {
9832
9883
  neutral: {
9833
9884
  color: "var(--dsw-alias-label-secondary)",
9834
- background: "var(--dsw-alias-bg-layer-2)",
9835
- border: "var(--dsw-alias-border-l2)"
9885
+ background: "var(--dsw-alias-bg-module-platform)"
9836
9886
  },
9837
9887
  info: {
9838
- color: "var(--dsw-alias-brand-primary)",
9839
- background: "var(--dsw-alias-bg-layer-2)",
9840
- border: "var(--dsw-alias-border-l2)"
9888
+ color: "var(--dsw-alias-state-business-primary)",
9889
+ background: "color-mix(in srgb, var(--dsw-alias-state-business-primary) 10%, transparent)"
9841
9890
  },
9842
9891
  success: {
9843
9892
  color: "var(--dsw-alias-state-success-primary)",
9844
- background: "var(--dsw-alias-bg-layer-2)",
9845
- border: "var(--dsw-alias-border-l2)"
9893
+ background: "color-mix(in srgb, var(--dsw-alias-state-success-primary) 10%, transparent)"
9846
9894
  },
9847
9895
  warning: {
9848
- color: "var(--dsw-alias-state-warn-secondary)",
9849
- background: "var(--dsw-alias-bg-layer-2)",
9850
- border: "var(--dsw-alias-border-l2)"
9896
+ color: "var(--dsw-alias-state-warn-primary)",
9897
+ background: "color-mix(in srgb, var(--dsw-alias-state-warn-primary) 12%, transparent)"
9851
9898
  }
9852
9899
  };
9900
+ /** Whether a value is one of the host's four tone literals. */
9901
+ function isHostBadgeTone(value) {
9902
+ return typeof value === "string" && HOST_BADGE_TONE_NAMES.includes(value);
9903
+ }
9853
9904
  /**
9854
9905
  * The one line a badge shows, in the reader's language.
9855
9906
  *
9856
- * The host contract requires an English entry, so that is the last resort; the
9857
- * active language and its base tag (`de-DE` → `de`) come first, matching what
9858
- * the host's own resolver does. An empty string is treated as absent — a blank
9859
- * chip would be a decoration that says nothing.
9907
+ * `text` is `unknown` because it comes off the wire. A literal is used as-is; a
9908
+ * table is looked up exactly the way the host's `resolveText` does it — active
9909
+ * language, then its base tag (`pt-BR` → `pt-br` → `pt`), then the required
9910
+ * `en`. Anything else (a number, an array, a nested object) is not text, so it
9911
+ * reads as absent rather than being stringified into `[object Object]`.
9912
+ *
9913
+ * A BLANK string is treated as absent, which is deliberately STRICTER than the
9914
+ * host: `resolveText` returns `''` untouched (it only fills in missing keys), so
9915
+ * `{ en: '' }` paints a real, empty capsule. Refusing that here means a
9916
+ * decoration never says nothing — a rule this component adds, not the host's.
9860
9917
  */
9861
9918
  function resolveHostBadgeText(text) {
9862
9919
  if (typeof text === "string") return text.trim() === "" ? void 0 : text;
9920
+ if (typeof text !== "object" || text === null || Array.isArray(text)) return void 0;
9921
+ const table = text;
9863
9922
  const language = uiLanguage().toLowerCase();
9864
9923
  const base = language.split("-")[0] ?? "";
9865
- const picked = text[language] ?? (base === "" ? void 0 : text[base]) ?? text.en;
9866
- return picked === void 0 || picked.trim() === "" ? void 0 : picked;
9924
+ const candidates = [
9925
+ table[language],
9926
+ base === "" ? void 0 : table[base],
9927
+ table["en"]
9928
+ ];
9929
+ for (const candidate of candidates) {
9930
+ if (typeof candidate !== "string") continue;
9931
+ if (candidate.trim() !== "") return candidate;
9932
+ }
9867
9933
  }
9868
- /** One provider-authored selector badge, drawn where the host's own label draws it. */
9934
+ /**
9935
+ * Read one provider-authored badge, or `undefined` when there is nothing to say.
9936
+ *
9937
+ * THE WHOLE POINT IS THAT THIS DOES NOT THROW. `badge` arrives from another
9938
+ * plugin's catalog, so its shape is that plugin's business: a `null`, an unknown
9939
+ * tone, a missing `text` and a numeric `text` are all things a third party can
9940
+ * ship. A throw here does not lose the capsule — it retires THIS COMPONENT from
9941
+ * the `single` cell, which strips the name off every row in the picker. The
9942
+ * degradation is therefore total: anything unreadable becomes the plain name.
9943
+ */
9944
+ function readHostBadge(badge) {
9945
+ if (typeof badge !== "object" || badge === null || Array.isArray(badge)) return void 0;
9946
+ const label = resolveHostBadgeText(badge.text);
9947
+ if (label === void 0) return void 0;
9948
+ const tone = badge.tone;
9949
+ return {
9950
+ tone: isHostBadgeTone(tone) ? tone : "neutral",
9951
+ label
9952
+ };
9953
+ }
9954
+ /**
9955
+ * One provider-authored selector badge, drawn where the host's own label draws it.
9956
+ *
9957
+ * Styled INLINE rather than by class, because a slot component renders OUTSIDE
9958
+ * the card: the card's stylesheet is only mounted while the card is, so a picker
9959
+ * opened on a page that never mounted it would otherwise get an uncoloured
9960
+ * capsule. The geometry is the host's `Tag` verbatim — `padding: 1px 8px`,
9961
+ * `font-size: 11px`, `line-height: 17px`, `border-radius: 999px`,
9962
+ * `corner-shape: round`.
9963
+ *
9964
+ * NO `max-width` / `text-overflow`, deliberately: the host does not truncate, so
9965
+ * truncating here would show "限时免…" at exactly the row width where the host
9966
+ * showed the whole thing — less consistent, not more.
9967
+ */
9869
9968
  function HostBadge(props) {
9870
- const tone = HOST_BADGE_TONES[props.badge.tone] ?? HOST_BADGE_TONES.neutral;
9969
+ const tone = HOST_BADGE_TONES[props.badge.tone];
9970
+ const style = {
9971
+ display: "inline-flex",
9972
+ alignItems: "center",
9973
+ flex: "none",
9974
+ borderRadius: "999px",
9975
+ padding: "1px 8px",
9976
+ fontSize: "11px",
9977
+ lineHeight: "17px",
9978
+ fontWeight: 500,
9979
+ whiteSpace: "nowrap",
9980
+ color: tone.color,
9981
+ background: tone.background,
9982
+ cornerShape: "round"
9983
+ };
9871
9984
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
9872
- className: "dsm-trae-model-label-host-badge",
9873
- style: {
9874
- flex: "none",
9875
- borderRadius: "999px",
9876
- padding: "0 6px",
9877
- fontSize: "10px",
9878
- fontWeight: 500,
9879
- lineHeight: "15px",
9880
- whiteSpace: "nowrap",
9881
- border: `1px solid ${tone.border}`,
9882
- color: tone.color,
9883
- background: tone.background,
9884
- maxWidth: "8em",
9885
- overflow: "hidden",
9886
- textOverflow: "ellipsis"
9887
- },
9888
- children: props.label
9985
+ className: `dsm-trae-model-label-host-badge dsm-trae-model-label-host-badge-${props.badge.tone}`,
9986
+ style,
9987
+ children: props.badge.label
9889
9988
  });
9890
9989
  }
9891
9990
  function TraeModelLabel(props) {
9892
9991
  const { provider, model } = props;
9893
9992
  const panel = panelOfProvider(provider.id);
9894
9993
  if (panel === void 0) {
9895
- const badgeInfo = model.badge;
9896
- const label = badgeInfo === void 0 ? void 0 : resolveHostBadgeText(badgeInfo.text);
9897
- if (badgeInfo === void 0 || label === void 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: model.name });
9898
- const badge = /* @__PURE__ */ (0, react_jsx_runtime.jsx)(HostBadge, {
9899
- badge: badgeInfo,
9900
- label
9994
+ const badge = readHostBadge(model.badge);
9995
+ if (badge === void 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: model.name });
9996
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
9997
+ className: "dsm-trae-model-label",
9998
+ style: {
9999
+ display: "inline-flex",
10000
+ alignItems: "center",
10001
+ gap: "5px",
10002
+ whiteSpace: "nowrap",
10003
+ verticalAlign: "baseline"
10004
+ },
10005
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: model.name }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(HostBadge, { badge })]
9901
10006
  });
9902
- return props.placement === "trigger" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [
9903
- badge,
9904
- " ",
9905
- model.name
9906
- ] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [
9907
- model.name,
9908
- " ",
9909
- badge
9910
- ] });
9911
10007
  }
9912
10008
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TraeModelLabelInner, {
9913
10009
  modelId: model.id,
@@ -9958,6 +10054,114 @@ ${u} ${p}-pool .sw input:checked+.sw-track::after{transform:translateX(16px)}
9958
10054
  });
9959
10055
  }
9960
10056
  //#endregion
10057
+ //#region src/client/model-label-seat.ts
10058
+ /**
10059
+ * Read the declared kind of a slot, or `undefined` when it cannot be read.
10060
+ *
10061
+ * `spec()` is the documented read-only accessor (`SlotRegistry.spec`), present
10062
+ * on 0.1.7 and on 0.2.2. It is also the only way to learn the kind: the
10063
+ * declaration lives in the host package, and a plugin's own `SlotMap`
10064
+ * augmentation says what this plugin HOPES the slot is, not what the running
10065
+ * host declared.
10066
+ *
10067
+ * Anything unexpected — no `spec` method on an older line, a throw from a
10068
+ * service that is not ready, a kind this build does not know — returns
10069
+ * `undefined`, and the caller falls back to the single-cell shape. That is the
10070
+ * conservative direction: it is what every released version of this plugin
10071
+ * already does.
10072
+ *
10073
+ * @param slots - The `slots` service (typed loosely: the accessor is optional).
10074
+ * @param key - The slot key to inspect.
10075
+ * @returns The declared kind, or `undefined`.
10076
+ */
10077
+ function declaredSlotKind(slots, key) {
10078
+ const face = slots;
10079
+ if (face === void 0 || typeof face.spec !== "function") return void 0;
10080
+ let spec;
10081
+ try {
10082
+ spec = face.spec(key);
10083
+ } catch {
10084
+ return;
10085
+ }
10086
+ const kind = spec?.kind;
10087
+ if (kind === "single" || kind === "list" || kind === "keyed" || kind === "chain") return kind;
10088
+ }
10089
+ /**
10090
+ * The registrations this plugin should make for the model-label seat.
10091
+ *
10092
+ * `keyed` yields ONE seat per provider id: each is its own cell, each renders
10093
+ * only the rows whose owner props carry that provider id, and a provider this
10094
+ * plugin does not own falls through to the owner's own fallback (the catalog
10095
+ * name, plus that model's `badge` on hosts that have one).
10096
+ *
10097
+ * Every other kind — including `undefined`, i.e. a host that cannot be
10098
+ * inspected — yields the single historical seat. That is not a degraded mode:
10099
+ * it is what `single` requires, and on a not-yet-declared slot it is what the
10100
+ * inject callback is about to find.
10101
+ *
10102
+ * An empty provider list under `keyed` would silently disable the label, so it
10103
+ * falls back to the single seat instead: a wrong-shaped registration that still
10104
+ * draws something beats a correct-shaped registration that draws nothing.
10105
+ *
10106
+ * @param params - Slot key, declared kind, this plugin's provider ids, priority.
10107
+ * @returns One or more `register()` option objects, in registration order.
10108
+ */
10109
+ function modelLabelSeats(params) {
10110
+ const { name, kind, providerIds, priority } = params;
10111
+ if (kind === "list" || kind === "chain") return [];
10112
+ if (kind === "keyed" && providerIds.length > 0) return providerIds.map((id) => ({
10113
+ name,
10114
+ key: id,
10115
+ priority
10116
+ }));
10117
+ return [{
10118
+ name,
10119
+ priority
10120
+ }];
10121
+ }
10122
+ /**
10123
+ * Claim every planned seat, atomically, and hand back ONE disposer.
10124
+ *
10125
+ * WHY ATOMIC. The seat list is 2-3 entries on a `keyed` host, and `register()`
10126
+ * can throw on any ONE of them — a rival plugin claiming the same provider id at
10127
+ * the same priority, or the declaration collapsing mid-loop. A plain `.map()`
10128
+ * would then leave the earlier seats landed on the ledger with their disposers
10129
+ * unreachable (they were only ever held by the array that the throw discarded),
10130
+ * which is worse than failing outright: the component stays mounted, the caller
10131
+ * believes registration failed, and nothing will ever release those cells. On a
10132
+ * `single` host it also means the identity keeps a live entry that the next
10133
+ * `inject` re-run cannot displace without a priority clash.
10134
+ *
10135
+ * So a partial failure is rolled back in reverse order before the error is
10136
+ * rethrown, exactly as if it had never been attempted. The rethrow is important:
10137
+ * each caller already has a catch that logs, and swallowing here would hide the
10138
+ * refusal from the diagnostics that exist to explain it.
10139
+ *
10140
+ * A disposal that itself throws must not mask the original error, hence the
10141
+ * inner catch — the registry treats a second dispose as a no-op, and a broken
10142
+ * one must not turn one failure into a crash.
10143
+ *
10144
+ * @param seats - The plan from {@link modelLabelSeats}, in registration order.
10145
+ * @param register - Claims one seat and returns its disposer.
10146
+ * @returns One disposer that releases every seat, in reverse registration order.
10147
+ */
10148
+ function registerModelLabelSeats(seats, register) {
10149
+ const claimed = [];
10150
+ try {
10151
+ for (const seat of seats) claimed.push(register(seat));
10152
+ } catch (error) {
10153
+ for (const dispose of [...claimed].reverse()) try {
10154
+ dispose();
10155
+ } catch {}
10156
+ throw error;
10157
+ }
10158
+ return () => {
10159
+ for (const dispose of [...claimed].reverse()) try {
10160
+ dispose();
10161
+ } catch {}
10162
+ };
10163
+ }
10164
+ //#endregion
9961
10165
  //#region src/client/index.tsx
9962
10166
  /** Stable browser-plugin name. */
9963
10167
  const name = "dsh-connect-trae-client";
@@ -10009,10 +10213,19 @@ ${u} ${p}-pool .sw input:checked+.sw-track::after{transform:translateX(16px)}
10009
10213
  * the same priority. That refusal is OUR fault and we can fix it rather than
10010
10214
  * report it, so each run releases whatever this identity already holds.
10011
10215
  *
10216
+ * ONE DISPOSER PER CALL, HOWEVER MANY ENTRIES IT OWNS. A `keyed` host makes this
10217
+ * plugin claim one cell per provider id (see `./model-label-seat.ts`), and those
10218
+ * cells must live and die together — a partial teardown would leave a cell
10219
+ * occupied by a component whose siblings are gone, visible as some rows
10220
+ * decorated and others not. Folding them into one disposer is
10221
+ * `registerModelLabelSeats`'s job, so the contract here stays the simple one:
10222
+ * whatever `register` returns is the whole ownership of this identity, and
10223
+ * `SlotHold` keeps exactly one.
10224
+ *
10012
10225
  * @param ctx - The applying plugin's context (module scope has none).
10013
10226
  * @param identity - Stable key for this seat.
10014
10227
  * @param slotKey - The host slot to register into.
10015
- * @param register - Performs the registration, returning its disposer.
10228
+ * @param register - Performs the registration(s), returning ONE disposer for all.
10016
10229
  * @param describe - Names the seat in the failure log.
10017
10230
  */
10018
10231
  function registerSlotEntry(ctx, identity, slotKey, register, describe) {
@@ -10277,16 +10490,31 @@ ${u} ${p}-pool .sw input:checked+.sw-track::after{transform:translateX(16px)}
10277
10490
  * non-interactive inline content such as a coloured name or badge" while the
10278
10491
  * picker keeps owning clicks, focus and check marks.
10279
10492
  *
10493
+ * HOW MANY SEATS, AND WHY THAT IS READ AT MOUNT. On a `single` host this slot
10494
+ * holds ONE cell: a second registration at the same priority throws, a second
10495
+ * registration at any other priority is silently discarded, and whoever holds
10496
+ * the cell must draw EVERY provider's rows. On a `keyed` host each
10497
+ * `options.key` is its own cell, so one seat per provider id gives this
10498
+ * plugin exactly its own rows and leaves every other provider to the owner's
10499
+ * own fallback. Which shape is legal is the HOST's declaration, so the kind
10500
+ * is read from `ctx.slots.spec(...)` at mount and the seat list is derived
10501
+ * from it (`./model-label-seat.ts`). One build serves both lines.
10502
+ *
10280
10503
  * Registered unconditionally: the component renders the plain name for a
10281
10504
  * provider that is not ours, so there is nothing to gate on. Its own
10282
10505
  * try/catch keeps a host that does not serve this slot from taking the card
10283
10506
  * down with it.
10284
10507
  */
10285
10508
  const registerModelLabel = () => {
10286
- registerSlotEntry(ctx, "conversation.input.model.label", "conversation.input.model.label", () => ctx.slots.register({
10287
- name: "conversation.input.model.label",
10288
- priority: MODEL_LABEL_PRIORITY
10289
- }, TraeModelLabel), "model label slot");
10509
+ registerSlotEntry(ctx, "conversation.input.model.label", "conversation.input.model.label", () => {
10510
+ const registry = ctx.slots;
10511
+ return registerModelLabelSeats(modelLabelSeats({
10512
+ name: "conversation.input.model.label",
10513
+ kind: declaredSlotKind(ctx.slots, "conversation.input.model.label"),
10514
+ providerIds: TRAE_MODEL_LABEL_PROVIDERS,
10515
+ priority: MODEL_LABEL_PRIORITY
10516
+ }), (seat) => registry.register(seat, TraeModelLabel));
10517
+ }, "model label slot");
10290
10518
  };
10291
10519
  registerModelLabel();
10292
10520
  /**
package/lib/index.d.ts CHANGED
@@ -3408,6 +3408,69 @@ declare class TraeGatedUpstreamClient implements TraeUpstreamClient {
3408
3408
  chatStream(bodyJson: string, signal?: AbortSignal): Promise<TraeChatResult>;
3409
3409
  }
3410
3410
  //#endregion
3411
+ //#region src/model-badge.d.ts
3412
+ /**
3413
+ * Emphasis of a selector badge, spelled out locally.
3414
+ *
3415
+ * Deliberately NOT imported from `@deepseek-ai/dsh-llm`: `LlmModelBadge` does
3416
+ * not exist on 0.1.x, so importing it would make this file uncompilable on the
3417
+ * host range the plugin still declares. The four literals below are the host's
3418
+ * complete vocabulary, and {@link TRAE_BADGE_TONES} is what the test checks
3419
+ * them against.
3420
+ */
3421
+ type TraeBadgeTone = 'neutral' | 'info' | 'success' | 'warning';
3422
+ /** Every tone the host understands; the test asserts this list is complete. */
3423
+ declare const TRAE_BADGE_TONES: readonly TraeBadgeTone[];
3424
+ /**
3425
+ * One selector badge.
3426
+ *
3427
+ * `text` is a translation table rather than a literal so the capsule reads in
3428
+ * the reader's language. `en` is REQUIRED (the host falls back to it for any
3429
+ * language it cannot find) and both keys are lowercase language ids, matching
3430
+ * the host's `localeKey()` lookup.
3431
+ */
3432
+ interface TraeModelBadge {
3433
+ text: {
3434
+ readonly en: string;
3435
+ readonly zh: string;
3436
+ };
3437
+ tone: TraeBadgeTone;
3438
+ }
3439
+ /**
3440
+ * The badge one catalog row should carry, or `undefined` for none.
3441
+ *
3442
+ * WHY THE PROMOTION, AND NOT THE PRICE. A badge is the ONE thing a sibling can
3443
+ * draw on this plugin's behalf, so it has to be the thing a user would act on.
3444
+ * "This model is 50% off right now" changes the choice; a price that has been
3445
+ * identical for months does not.
3446
+ *
3447
+ * WHY THE CLOCK IS READ HERE. `activityDiscount` rides along UNRESOLVED on the
3448
+ * model entry precisely so the judgement happens where the row is drawn
3449
+ * (`src/promo.ts` explains why freezing it at parse time would leave a night-time
3450
+ * discount hanging there the next morning). `listModels()` IS that moment on the
3451
+ * host side — it runs when the host builds the catalog — and the plugin asks the
3452
+ * host to re-read the catalog via `llm/models-updated` whenever the card's own
3453
+ * clock crosses a window boundary. So this is not "catalog data that ignores the
3454
+ * clock"; it is a clock judgement made at each catalog rebuild.
3455
+ *
3456
+ * A NOTE ON WHAT WAS TRIED FIRST AND REJECTED. An earlier version advertised
3457
+ * `creditMultiplier === 0` as "免费"/"Free". It never fired: every path that
3458
+ * parses upstream's `consumption_rate` runs it through `finitePositive`
3459
+ * (`src/solo.ts`, `src/model-metadata.ts`), which requires `> 0`, so a rate of
3460
+ * `0` arrives as `undefined`. A multiplier of exactly `0` can only come from a
3461
+ * saved `lastCatalog`, never from the live directory. Do not reintroduce a
3462
+ * price-derived badge from that field.
3463
+ *
3464
+ * @param model - the catalog row about to be advertised.
3465
+ * @param now - the clock the promotion is judged against. Passed in rather than
3466
+ * read from `Date.now()` so the caller owns the "one clock for the whole
3467
+ * catalog" decision and the tests can move time.
3468
+ * @returns the badge, or `undefined` — never `null`. A `null` makes the host
3469
+ * registry throw inside `listModels`, which drops this provider's
3470
+ * ENTIRE catalog into `failures`, not just the decorated row.
3471
+ */
3472
+ declare function traeModelBadge(model: Pick<TraeModelInfo, 'activityDiscount'>, now: Date): TraeModelBadge | undefined;
3473
+ //#endregion
3411
3474
  //#region src/refresh.d.ts
3412
3475
  /**
3413
3476
  * Stable device identity for the DeviceInfo body, sourced from the app's own
@@ -4867,4 +4930,4 @@ declare function asVolatile<S>(schema: S): S;
4867
4930
  declare const Config: z<Config>;
4868
4931
  declare function apply(ctx: Context$1, config: Config): void;
4869
4932
  //#endregion
4870
- export { Config, FALLBACK_CONTEXT_WINDOW, FALLBACK_TRAE_MODELS, FALLBACK_TRAE_MODELS_AI, MAX_PROBE_INPUT_TOKENS, type PiAiGeneration, type PiAiRuntimeInfo, REGION_GATEWAYS, type RawChatDelta, type RawChatMessage, type RawChatTool, SseDecoder, type SseEvent, TRAE_ACCOUNT_STATUS_PATH, TRAE_AI_PROVIDER, TRAE_CHANNEL_PRIORITY, TRAE_CHECKIN_PATH, TRAE_CN_AGENT_TASK_PATH, TRAE_CN_TITLE_PATH, TRAE_COMMON_FUNCTIONS, TRAE_ENTERPRISE_PROVIDER, TRAE_MODELS_TEST_PATH, TRAE_MODEL_DETAIL_FUNCTIONS, TRAE_MODEL_DETAIL_PATH, TRAE_OFFICIAL_ROSTER_PATH, TRAE_PANELS, TRAE_PAY_BASE, TRAE_PROBE_TIMEOUT_MS, TRAE_PROVIDER, TRAE_PROVIDERS, TRAE_PROVIDER_DISPLAY_NAMES, TRAE_PROVIDER_IDS, TRAE_PROVIDER_ID_DISPLAY_NAMES, TRAE_PROVIDER_ROUTES, TRAE_RAW_CHAT_V1_PATH, TRAE_RAW_CHAT_V2_PATH, TRAE_REASONING_EFFORTS, TRAE_REGIONS, TRAE_REGION_PARAM, TRAE_SETTINGS_NS, TRAE_SOLO_CHAT_PATH, TRAE_SOLO_FUNCTION, TRAE_SOLO_MODELS_PATH, TRAE_SOLO_REMOTE_BASE, TRAE_STATE_DB_FILENAME, TRAE_STREAM_IDLE_TIMEOUT_MS, TRAE_USAGE_PATH, TRAE_VERSION_CODE_FALLBACK, type TraeActivityRule, type TraeAdapter, type TraeCachedModelConfig, TraeCatalog, type TraeChatResult, type TraeCheckinClaim, type TraeCheckinStatus, type TraeContextBudget, type TraeCredential, TraeCredentialStore, TraeDelegatingUpstreamClient, type TraeDiscoveredModel, type TraeDiscoveredReasoning, type TraeEdition, type TraeFailure, TraeFallbackUpstreamClient, type TraeFallbackUpstreamOptions, type TraeFusionRawChatEnvelope, TraeGatedUpstreamClient, type TraeGatedUpstreamOptions, type TraeIdentity, type TraeInputModality, type TraeModelDetailRequest, type TraeModelGroup, type TraeModelInfo, type TraeModelProbeVerdict, type TraeModelView, type TraeModelsTestResponse, type TraeObservedModelConfig, type TraeOfficialChannel, type TraeOfficialFilterResult, type TraeOfficialRoster, TraeOfficialRosterClient, type TraeOfficialVerdict, type TraeOrderableModel, type TraePanel, type TraeProbeResult, type TraeProbeVerdict, type TraeProviderRoute, TraeRawCapabilityController, type TraeRawCapabilityControllerOptions, type TraeRawCapabilitySnapshot, TraeRawCapabilityState, type TraeRawChatBehaviorConfig, type TraeRawChatCapability, type TraeRawChatClientOptions, type TraeRawChatConfig, type TraeRawChatFailureReason, type TraeRawChatRuntimeConfig, TraeRawChatUpstreamClient, type TraeRawDiagnostic, type TraeRawDiagnosticState, type TraeRawGateway, type TraeRawGatewayOptions, type TraeRawResolverResult, type TraeReasoningCapability, type TraeReasoningEffort, type TraeRefreshDevice, type TraeRegion, type TraeRegionGateways, TraeRegionState, type TraeShim, TraeSoloBridge, type TraeSoloClientOptions, TraeSoloRemoteCatalogClient, type TraeSoloRemoteCatalogOptions, TraeSoloUpstreamClient, type TraeStorageCandidate, type TraeStreamEvent, type TraeUpstreamClient, TraeUsageClient, type TraeUsageOptions, type TraeUsagePack, type TraeUsageRouteOptions, type TraeUsageSnapshot, type TraeUsageSummary, type TraeUsageView, type TraeWebAccountState, type TraeWebActivity, type TraeWebCheckin, type TraeWebCheckinClaim, type TraeWebCredits, type TraeWebUsage, type TraeWireModel, type TraeWireTarget, UnconfiguredTraeUpstreamClient, adaptLegacyPiAiContext, adaptModernPiAiContext, apply, applyContextBudgets, applyImageSelection, applyOfficialRoster, applyReasoningEffort, asVolatile, bridgeTraeSoloStream, buildTraeAgentTaskBody, buildTraeCnHeaders, buildTraeFusionRawChatEnvelope, buildTraeModelDetailRequest, buildTraeRawChatDraft, buildTraeRawChatRuntimeConfig, classifyProbeError, classifyProbeResult, classifyTraeRawChatFailure, configuredAccountsOf, createTraeAdapter, createTraeRawGateway, createTraeShim, creditPosition, decidePiAiGeneration, decodeRawChatChunk, decodeTraeEvent, decryptTraeStorageValue, deriveCatalog, describeIdShape, describeNameShape, describeTraeFailure, discoveredCatalog, displayNameOfTraeProvider, fallbackModelsFor, filterTraeModels, generationOf, groupTraeModels, hashTraeRawChatArg, identityHeaders, inject, isEnterpriseAccount, isPromotableWireModel, legacyTraeOwnAuthPath, maskUserPath, mergeTraeModelSources, name, nextRegionCreditPosition, nextRegionEnabled, nextRegionSlots, normalizeTraeCredential, normalizeTraeToolSchema, normalizeTraeToolSchemas, normalizeTraeVersionCode, officialVerdictOf, orderTraeModels, panelOfKey, panelOfProvider, parseObservedModelConfig, parseReasoningCapability, parseTraeAuthValue, parseTraeCachedModel, parseTraeModelExtraConfigLogLine, parseTraeOfficialRoster, parseTraeRawChatBehaviorConfig, parseTraeRemoteModel, parseTraeStorageDocument, piAiRuntimeInfo, pickTraeStorageIdentity, prepareSoloBody, probeBody, probeModelsSequentially, probeOneModel, probeTraeRawChatCapability, providerOfPanel, rawCapabilityDiagnostic, rawCapabilityFingerprint, readTraeCachedModel, readTraeCliIdentity, readTraeIdentity, refreshTraeCredential, regionCreditPositionOf, regionEnabled, regionEnabledOf, regionOfCredential, regionOfEdition, regionOfHost, regionOfPanel, regionOfTraeProvider, regionOfTraeStatusUrl, regionOfUserRegion, regionStateOf, registerTraeUsageRoute, resolveTraeIdentity, resolveTraeRawRuntime, routeOfTraeProvider, sanitizeCatalog, scopeTraeModels, servedContextWindow, settingsNamespaceOf, sidebarPanelEnabled, thinkingEffortsOf, traeEndpoint, traeInputModalities, traeModelCallChannel, traeModelDisplayName, traeModelInView, traeModelMatchesQuery, traeOwnAuthPath, traeRawChatExtraInfo, traeStateDatabaseCandidates, traeStorageCandidates, traeWebUsage, traeWindowsAppNames, unwrapVolatile, unwrapVolatileDeep, wireTargetsOf, withChatCallableFunctions, withContextAdaptation, withTraeRegion };
4933
+ export { Config, FALLBACK_CONTEXT_WINDOW, FALLBACK_TRAE_MODELS, FALLBACK_TRAE_MODELS_AI, MAX_PROBE_INPUT_TOKENS, type PiAiGeneration, type PiAiRuntimeInfo, REGION_GATEWAYS, type RawChatDelta, type RawChatMessage, type RawChatTool, SseDecoder, type SseEvent, TRAE_ACCOUNT_STATUS_PATH, TRAE_AI_PROVIDER, TRAE_BADGE_TONES, TRAE_CHANNEL_PRIORITY, TRAE_CHECKIN_PATH, TRAE_CN_AGENT_TASK_PATH, TRAE_CN_TITLE_PATH, TRAE_COMMON_FUNCTIONS, TRAE_ENTERPRISE_PROVIDER, TRAE_MODELS_TEST_PATH, TRAE_MODEL_DETAIL_FUNCTIONS, TRAE_MODEL_DETAIL_PATH, TRAE_OFFICIAL_ROSTER_PATH, TRAE_PANELS, TRAE_PAY_BASE, TRAE_PROBE_TIMEOUT_MS, TRAE_PROVIDER, TRAE_PROVIDERS, TRAE_PROVIDER_DISPLAY_NAMES, TRAE_PROVIDER_IDS, TRAE_PROVIDER_ID_DISPLAY_NAMES, TRAE_PROVIDER_ROUTES, TRAE_RAW_CHAT_V1_PATH, TRAE_RAW_CHAT_V2_PATH, TRAE_REASONING_EFFORTS, TRAE_REGIONS, TRAE_REGION_PARAM, TRAE_SETTINGS_NS, TRAE_SOLO_CHAT_PATH, TRAE_SOLO_FUNCTION, TRAE_SOLO_MODELS_PATH, TRAE_SOLO_REMOTE_BASE, TRAE_STATE_DB_FILENAME, TRAE_STREAM_IDLE_TIMEOUT_MS, TRAE_USAGE_PATH, TRAE_VERSION_CODE_FALLBACK, type TraeActivityRule, type TraeAdapter, type TraeBadgeTone, type TraeCachedModelConfig, TraeCatalog, type TraeChatResult, type TraeCheckinClaim, type TraeCheckinStatus, type TraeContextBudget, type TraeCredential, TraeCredentialStore, TraeDelegatingUpstreamClient, type TraeDiscoveredModel, type TraeDiscoveredReasoning, type TraeEdition, type TraeFailure, TraeFallbackUpstreamClient, type TraeFallbackUpstreamOptions, type TraeFusionRawChatEnvelope, TraeGatedUpstreamClient, type TraeGatedUpstreamOptions, type TraeIdentity, type TraeInputModality, type TraeModelBadge, type TraeModelDetailRequest, type TraeModelGroup, type TraeModelInfo, type TraeModelProbeVerdict, type TraeModelView, type TraeModelsTestResponse, type TraeObservedModelConfig, type TraeOfficialChannel, type TraeOfficialFilterResult, type TraeOfficialRoster, TraeOfficialRosterClient, type TraeOfficialVerdict, type TraeOrderableModel, type TraePanel, type TraeProbeResult, type TraeProbeVerdict, type TraeProviderRoute, TraeRawCapabilityController, type TraeRawCapabilityControllerOptions, type TraeRawCapabilitySnapshot, TraeRawCapabilityState, type TraeRawChatBehaviorConfig, type TraeRawChatCapability, type TraeRawChatClientOptions, type TraeRawChatConfig, type TraeRawChatFailureReason, type TraeRawChatRuntimeConfig, TraeRawChatUpstreamClient, type TraeRawDiagnostic, type TraeRawDiagnosticState, type TraeRawGateway, type TraeRawGatewayOptions, type TraeRawResolverResult, type TraeReasoningCapability, type TraeReasoningEffort, type TraeRefreshDevice, type TraeRegion, type TraeRegionGateways, TraeRegionState, type TraeShim, TraeSoloBridge, type TraeSoloClientOptions, TraeSoloRemoteCatalogClient, type TraeSoloRemoteCatalogOptions, TraeSoloUpstreamClient, type TraeStorageCandidate, type TraeStreamEvent, type TraeUpstreamClient, TraeUsageClient, type TraeUsageOptions, type TraeUsagePack, type TraeUsageRouteOptions, type TraeUsageSnapshot, type TraeUsageSummary, type TraeUsageView, type TraeWebAccountState, type TraeWebActivity, type TraeWebCheckin, type TraeWebCheckinClaim, type TraeWebCredits, type TraeWebUsage, type TraeWireModel, type TraeWireTarget, UnconfiguredTraeUpstreamClient, adaptLegacyPiAiContext, adaptModernPiAiContext, apply, applyContextBudgets, applyImageSelection, applyOfficialRoster, applyReasoningEffort, asVolatile, bridgeTraeSoloStream, buildTraeAgentTaskBody, buildTraeCnHeaders, buildTraeFusionRawChatEnvelope, buildTraeModelDetailRequest, buildTraeRawChatDraft, buildTraeRawChatRuntimeConfig, classifyProbeError, classifyProbeResult, classifyTraeRawChatFailure, configuredAccountsOf, createTraeAdapter, createTraeRawGateway, createTraeShim, creditPosition, decidePiAiGeneration, decodeRawChatChunk, decodeTraeEvent, decryptTraeStorageValue, deriveCatalog, describeIdShape, describeNameShape, describeTraeFailure, discoveredCatalog, displayNameOfTraeProvider, fallbackModelsFor, filterTraeModels, generationOf, groupTraeModels, hashTraeRawChatArg, identityHeaders, inject, isEnterpriseAccount, isPromotableWireModel, legacyTraeOwnAuthPath, maskUserPath, mergeTraeModelSources, name, nextRegionCreditPosition, nextRegionEnabled, nextRegionSlots, normalizeTraeCredential, normalizeTraeToolSchema, normalizeTraeToolSchemas, normalizeTraeVersionCode, officialVerdictOf, orderTraeModels, panelOfKey, panelOfProvider, parseObservedModelConfig, parseReasoningCapability, parseTraeAuthValue, parseTraeCachedModel, parseTraeModelExtraConfigLogLine, parseTraeOfficialRoster, parseTraeRawChatBehaviorConfig, parseTraeRemoteModel, parseTraeStorageDocument, piAiRuntimeInfo, pickTraeStorageIdentity, prepareSoloBody, probeBody, probeModelsSequentially, probeOneModel, probeTraeRawChatCapability, providerOfPanel, rawCapabilityDiagnostic, rawCapabilityFingerprint, readTraeCachedModel, readTraeCliIdentity, readTraeIdentity, refreshTraeCredential, regionCreditPositionOf, regionEnabled, regionEnabledOf, regionOfCredential, regionOfEdition, regionOfHost, regionOfPanel, regionOfTraeProvider, regionOfTraeStatusUrl, regionOfUserRegion, regionStateOf, registerTraeUsageRoute, resolveTraeIdentity, resolveTraeRawRuntime, routeOfTraeProvider, sanitizeCatalog, scopeTraeModels, servedContextWindow, settingsNamespaceOf, sidebarPanelEnabled, thinkingEffortsOf, traeEndpoint, traeInputModalities, traeModelBadge, traeModelCallChannel, traeModelDisplayName, traeModelInView, traeModelMatchesQuery, traeOwnAuthPath, traeRawChatExtraInfo, traeStateDatabaseCandidates, traeStorageCandidates, traeWebUsage, traeWindowsAppNames, unwrapVolatile, unwrapVolatileDeep, wireTargetsOf, withChatCallableFunctions, withContextAdaptation, withTraeRegion };