minecodex 2.0.22 → 2.0.24

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.
@@ -7,8 +7,8 @@
7
7
  "zh-CN": "模型窗口"
8
8
  },
9
9
  "description": {
10
- "en": "Switch models and thinking levels, and keep favorites close at hand.",
11
- "zh-CN": "快速切换模型和思考强度,收藏常用模型。"
10
+ "en": "Search and switch models and thinking levels, and keep favorites close at hand.",
11
+ "zh-CN": "搜索与切换模型和思考强度,收藏常用模型。"
12
12
  },
13
13
  "entry": {
14
14
  "kind": "composer-model-selector"
@@ -28,6 +28,14 @@
28
28
  "openai": "OpenAI"
29
29
  },
30
30
  "strings": {
31
+ "searchPlaceholder": {
32
+ "en": "Search model",
33
+ "zh-CN": "搜索模型"
34
+ },
35
+ "noResults": {
36
+ "en": "No models found",
37
+ "zh-CN": "未找到模型"
38
+ },
31
39
  "star": {
32
40
  "en": "Star {model}",
33
41
  "zh-CN": "收藏 {model}"
@@ -31,6 +31,18 @@ export function installModelSlider(signal, { feature, ui, createIcon, requestRef
31
31
  z-index: 10;
32
32
  padding: 6px;
33
33
  }
34
+ [data-codex-model-slider-search-header] {
35
+ position: sticky; top: 0; z-index: 1; flex-shrink: 0; padding: 2px 8px 8px;
36
+ background: var(--mcx-color-surface-elevated-secondary);
37
+ }
38
+ [data-codex-model-slider-search] {
39
+ width: 100%; height: 28px; border: 0; outline: none; padding: 0;
40
+ background: transparent; color: var(--mcx-color-text-primary);
41
+ font: inherit; font-size: var(--mcx-text-sm, 13px);
42
+ }
43
+ [data-codex-model-slider-search]::placeholder { color: var(--mcx-color-text-tertiary); }
44
+ [data-codex-model-slider-empty] { padding: 12px 8px; color: var(--mcx-color-text-secondary); font-size: 13px; }
45
+ [data-codex-model-slider-item][data-codex-model-slider-keyboard] { background: var(--mcx-color-background-primary-ghost-hover); }
34
46
  [data-codex-model-slider-menu][data-codex-model-slider-overflow] {
35
47
  max-height: var(--codex-model-slider-menu-max-height) !important;
36
48
  overflow-y: auto;
@@ -647,7 +659,12 @@ export function installModelSlider(signal, { feature, ui, createIcon, requestRef
647
659
  const items = modelMenuItems(menu).filter((item) => item.dataset.codexModelSliderRaw);
648
660
  if (!items.length) return;
649
661
  const favorites = loadModelFavorites(selector);
650
- items.forEach((item) => { item.style.display = ""; });
662
+ const query = menu.querySelector('[data-codex-model-slider-search]')?.value.trim().toLocaleLowerCase() ?? "";
663
+ const terms = query.split(/\s+/).filter(Boolean);
664
+ items.forEach((item) => {
665
+ const text = `${item.dataset.codexModelSliderRaw} ${item.querySelector('[data-codex-model-slider-copy]')?.textContent ?? ""}`.toLocaleLowerCase();
666
+ item.style.display = terms.every((term) => text.includes(term)) ? "" : "none";
667
+ });
651
668
  if (!menu.hasAttribute("data-codex-model-slider-controller-menu")) {
652
669
  if (!modelSelectorMenuSizeFrames.has(menu)) {
653
670
  const frame = requestAnimationFrame(() => {
@@ -658,8 +675,9 @@ export function installModelSlider(signal, { feature, ui, createIcon, requestRef
658
675
  }
659
676
  return;
660
677
  }
661
- const starred = items.filter((item) => favorites.has(item.dataset.codexModelSliderRaw));
662
- const ordinary = items.filter((item) => !favorites.has(item.dataset.codexModelSliderRaw));
678
+ const visible = items.filter((item) => item.style.display !== "none");
679
+ const starred = visible.filter((item) => favorites.has(item.dataset.codexModelSliderRaw));
680
+ const ordinary = visible.filter((item) => !favorites.has(item.dataset.codexModelSliderRaw));
663
681
  const desired = items.slice().sort((a, b) => compareEnhancedModelItems(a, b, favorites));
664
682
  const parent = items[0].parentElement;
665
683
  if (!parent || !desired.every((item) => item.parentElement === parent)) return;
@@ -673,13 +691,15 @@ export function installModelSlider(signal, { feature, ui, createIcon, requestRef
673
691
  divider.setAttribute("data-codex-model-slider-divider", "");
674
692
  divider.setAttribute("role", "separator");
675
693
  }
676
- const firstOrdinary = desired.find((item) => !favorites.has(item.dataset.codexModelSliderRaw));
694
+ const firstOrdinary = desired.find((item) => item.style.display !== "none" && !favorites.has(item.dataset.codexModelSliderRaw));
677
695
  if (firstOrdinary && divider.nextElementSibling !== firstOrdinary) {
678
696
  parent.insertBefore(divider, firstOrdinary);
679
697
  }
680
698
  } else {
681
699
  divider?.remove();
682
700
  }
701
+ const empty = menu.querySelector('[data-codex-model-slider-empty]');
702
+ if (empty) empty.hidden = visible.length > 0;
683
703
 
684
704
  if (!modelSelectorMenuSizeFrames.has(menu)) {
685
705
  const frame = requestAnimationFrame(() => {
@@ -693,7 +713,20 @@ export function installModelSlider(signal, { feature, ui, createIcon, requestRef
693
713
  function sizeModelMenuViewport(menu, maxVisibleItems) {
694
714
  if (!menu.isConnected) return;
695
715
  if (modelSelectorControllerMenu?.menu === menu && modelSelectorControllerMenu.animations?.length) return;
696
- const items = modelMenuItems(menu).filter((item) => item.dataset.codexModelSliderRaw);
716
+ const items = modelMenuItems(menu).filter((item) => item.dataset.codexModelSliderRaw && item.style.display !== "none");
717
+ if (menu.hasAttribute("data-codex-model-slider-controller-menu")) {
718
+ const content = menu.querySelector('[data-codex-model-slider-results]');
719
+ const header = menu.querySelector('[data-codex-model-slider-search-header]');
720
+ const style = getComputedStyle(menu);
721
+ const chrome = [style.paddingTop, style.paddingBottom, style.borderTopWidth, style.borderBottomWidth]
722
+ .reduce((sum, value) => sum + (Number.parseFloat(value) || 0), 0);
723
+ const last = items[Math.max(1, maxVisibleItems) - 1];
724
+ const resultsHeight = last ? last.offsetTop + last.offsetHeight - content.offsetTop : content.offsetHeight;
725
+ const available = modelSelectorControllerMenu.parentMenu.getBoundingClientRect().bottom - 8;
726
+ menu.style.height = `${Math.ceil(Math.min(chrome + header.offsetHeight + resultsHeight, available))}px`;
727
+ menu.style.overflowY = "auto";
728
+ return;
729
+ }
697
730
  if (items.length <= maxVisibleItems) {
698
731
  menu.removeAttribute("data-codex-model-slider-overflow");
699
732
  menu.style.removeProperty("--codex-model-slider-menu-max-height");
@@ -1055,7 +1088,20 @@ export function installModelSlider(signal, { feature, ui, createIcon, requestRef
1055
1088
  featureUI.surface(menu, "dropdown", { variant: "opaque" });
1056
1089
  // 切换同一个 picker 视图,不重复播放整个 Dropdown 的入场缩放。
1057
1090
  menu.style.animation = "none";
1091
+ menu.style.boxSizing = "border-box";
1092
+ const header = document.createElement("div");
1093
+ header.setAttribute("data-codex-model-slider-search-header", "");
1094
+ const search = document.createElement("input");
1095
+ search.type = "text";
1096
+ search.setAttribute("data-codex-model-slider-search", "");
1097
+ search.placeholder = selector.strings.searchPlaceholder;
1098
+ search.setAttribute("aria-label", search.placeholder);
1099
+ search.autocomplete = "off";
1100
+ search.spellcheck = false;
1101
+ header.append(search);
1102
+ menu.append(header);
1058
1103
  const content = document.createElement("div");
1104
+ content.setAttribute("data-codex-model-slider-results", "");
1059
1105
  content.style.flexShrink = "0";
1060
1106
  menu.append(content);
1061
1107
  models.forEach((model, index) => {
@@ -1063,6 +1109,7 @@ export function installModelSlider(signal, { feature, ui, createIcon, requestRef
1063
1109
  item.className = "no-drag outline-hidden rounded-lg px-[var(--padding-row-x)] py-[var(--padding-row-y)] text-sm text-default group hover:bg-primary-ghost-hover focus:bg-primary-ghost-hover cursor-interaction flex flex-col";
1064
1110
  item.setAttribute("role", "menuitem");
1065
1111
  item.setAttribute("tabindex", "-1");
1112
+ item.id = `mcx-model-result-${index}`;
1066
1113
  enhanceModelItem(item, menu, selector, index, {
1067
1114
  rawValue: String(model.model),
1068
1115
  selected: String(model.model) === String(controller.model),
@@ -1071,6 +1118,71 @@ export function installModelSlider(signal, { feature, ui, createIcon, requestRef
1071
1118
  });
1072
1119
  content.append(item);
1073
1120
  });
1121
+ const empty = document.createElement("div");
1122
+ empty.setAttribute("data-codex-model-slider-empty", "");
1123
+ empty.setAttribute("role", "status");
1124
+ empty.textContent = selector.strings.noResults;
1125
+ empty.hidden = true;
1126
+ content.append(empty);
1127
+ let keyboardItem = null;
1128
+ const resetKeyboard = () => {
1129
+ keyboardItem?.removeAttribute("data-codex-model-slider-keyboard");
1130
+ keyboardItem = null;
1131
+ search.removeAttribute("aria-activedescendant");
1132
+ };
1133
+ search.addEventListener("input", () => {
1134
+ resetKeyboard();
1135
+ const record = modelSelectorControllerMenu;
1136
+ if (!record || record.menu !== menu || record.closing) return;
1137
+ const previousHeight = menu.getBoundingClientRect().height;
1138
+ clearTimeout(record.motionTimer);
1139
+ record.animations?.forEach((animation) => animation.cancel());
1140
+ record.animations = [];
1141
+ menu.scrollTop = 0;
1142
+ renderEnhancedModelMenu(menu, selector);
1143
+ sizeModelMenuViewport(menu, selector.maxVisibleItems);
1144
+ record.listHeight = menu.getBoundingClientRect().height;
1145
+ if (matchMedia("(prefers-reduced-motion: reduce)").matches) return;
1146
+ const motion = menu.animate([{ height: `${previousHeight}px` }, { height: `${record.listHeight}px` }],
1147
+ { duration: 320, easing: "cubic-bezier(.19,1,.22,1)" });
1148
+ record.animations.push(motion);
1149
+ record.motionTimer = setTimeout(() => {
1150
+ if (modelSelectorControllerMenu !== record || record.closing) return;
1151
+ motion.cancel();
1152
+ record.animations = [];
1153
+ }, 320);
1154
+ });
1155
+ // 原生菜单的 typeahead 不应吞掉搜索输入;鼠标操作保留输入焦点。
1156
+ menu.addEventListener("pointerdown", (event) => {
1157
+ if (event.target.closest?.('[data-codex-model-slider-item]')) event.preventDefault();
1158
+ });
1159
+ search.addEventListener("keydown", (event) => {
1160
+ event.stopPropagation();
1161
+ if (event.isComposing) return;
1162
+ if (event.key === "Escape") {
1163
+ event.preventDefault();
1164
+ closeControllerModelMenu();
1165
+ modelButton.focus();
1166
+ } else if (event.key === "ArrowDown" || event.key === "ArrowUp") {
1167
+ event.preventDefault();
1168
+ const visible = modelMenuItems(menu).filter((item) => item.style.display !== "none");
1169
+ if (!visible.length) return;
1170
+ const current = visible.indexOf(keyboardItem);
1171
+ const index = current < 0 ? (event.key === "ArrowDown" ? 0 : visible.length - 1)
1172
+ : (current + (event.key === "ArrowDown" ? 1 : -1) + visible.length) % visible.length;
1173
+ resetKeyboard();
1174
+ keyboardItem = visible[index];
1175
+ keyboardItem.setAttribute("data-codex-model-slider-keyboard", "");
1176
+ search.setAttribute("aria-activedescendant", keyboardItem.id);
1177
+ const top = keyboardItem.offsetTop - header.offsetHeight;
1178
+ const bottom = keyboardItem.offsetTop + keyboardItem.offsetHeight - menu.clientHeight;
1179
+ if (top < menu.scrollTop) menu.scrollTop = top;
1180
+ else if (bottom > menu.scrollTop) menu.scrollTop = bottom;
1181
+ } else if (event.key === "Enter") {
1182
+ event.preventDefault();
1183
+ (keyboardItem ?? modelMenuItems(menu).find((item) => item.style.display !== "none"))?.click();
1184
+ }
1185
+ });
1074
1186
  menu.addEventListener("click", (event) => {
1075
1187
  const star = event.target.closest?.("[data-codex-model-slider-star]");
1076
1188
  const item = event.target.closest?.("[data-codex-model-slider-item]");
@@ -1132,8 +1244,7 @@ export function installModelSlider(signal, { feature, ui, createIcon, requestRef
1132
1244
  parentMenu.append(menu);
1133
1245
  sizeModelMenuViewport(menu, selector.maxVisibleItems);
1134
1246
  modelButton.setAttribute("aria-expanded", "true");
1135
- (menu.querySelector('[data-codex-model-slider-selected]')
1136
- ?? menu.querySelector('[role="menuitem"]'))?.focus();
1247
+ search.focus({ preventScroll: true });
1137
1248
  const record = modelSelectorControllerMenu;
1138
1249
  record.listHeight = menu.getBoundingClientRect().height;
1139
1250
  animateControllerModelMenu(record, true, () => {
@@ -109,15 +109,15 @@ export function sessionStatusSnapshot(state, threadId) {
109
109
  const turnSpeeds = new Map();
110
110
  for (const sample of state.speedMeasurements) {
111
111
  const elapsed = sample.end - sample.start - boundedToolWaitMs(sample.tools, sample.start, sample.end, sample.outputs);
112
- if (!sample.turnId || elapsed <= 0 || elapsed >= 600_000) continue;
112
+ if (!sample.turnId || !Number.isFinite(elapsed) || elapsed <= 0) continue;
113
113
  const turn = turnSpeeds.get(sample.turnId) ?? { tokens:0, ms:0, samples:0 };
114
114
  turn.tokens += sample.output;
115
115
  turn.ms += elapsed;
116
116
  turn.samples += 1;
117
117
  turnSpeeds.set(sample.turnId, turn);
118
118
  }
119
- // 按回合顺序取最近三个有效回合;同轮多步合并,无数据的回合不占名额。
120
- const recentTurns = [...state.turns].filter(id => turnSpeeds.has(id)).slice(-3);
119
+ // 按回合顺序取最近五个有效回合;同轮多步合并,无数据的回合不占名额。
120
+ const recentTurns = [...state.turns].filter(id => turnSpeeds.has(id)).slice(-5);
121
121
  for (const id of recentTurns) {
122
122
  const turn = turnSpeeds.get(id);
123
123
  decodeTokens += turn.tokens;
@@ -7,14 +7,14 @@ const copyByLocale = { "zh-CN": {
7
7
  firstToken: "平均首字延迟", input: "输入", cached: "缓存输入", output: "输出",
8
8
  sum: "合计", capacity: "窗口容量", contextShare: "上下文占用", compactions: "压缩次数",
9
9
  instructions: "指令", messages: "消息", toolHistory: "工具交互", other: "未分类",
10
- speedHelp: "关于 TPS", speedInfo: "GPT 等模型不公开完整思考过程及逐 Token 生成时间。TPS 取最近 3 个有有效数据的回合,不足 3 轮时按已有数据计算。它是基于可观测事件估算的体感响应速度,仅供参考,不代表模型真实的 Token 生成速度。",
10
+ speedHelp: "关于 TPS", speedInfo: "GPT 等模型不公开完整思考过程及逐 Token 生成时间。TPS 取最近 5 个有有效数据的回合,不足 5 轮时按已有数据计算。它是基于可观测事件估算的体感响应速度,仅供参考,不代表模型真实的 Token 生成速度。",
11
11
  }, en: {
12
12
  turns: "turns", steps: "steps", speed: "TPS", cache: "Cache", cacheDetail: "Cache hit",
13
13
  statistics: "Session stats", usage: "Token usage", context: "Context", total: "Total",
14
14
  firstToken: "Avg first token", input: "Input", cached: "Cached input", output: "Output",
15
15
  sum: "Sum", capacity: "Window size", contextShare: "Context used", compactions: "Compactions",
16
16
  instructions: "Instructions", messages: "Messages", toolHistory: "Tool activity", other: "Unclassified",
17
- speedHelp: "About TPS", speedInfo: "Models such as GPT do not expose their full reasoning process or per-token generation timing. TPS uses the latest 3 turns with valid timing data, or fewer if unavailable. It estimates perceived response speed from observable events, rather than the model’s actual token generation speed.",
17
+ speedHelp: "About TPS", speedInfo: "Models such as GPT do not expose their full reasoning process or per-token generation timing. TPS uses the latest 5 turns with valid timing data, or fewer if unavailable. It estimates perceived response speed from observable events, rather than the model’s actual token generation speed.",
18
18
  } };
19
19
  function selectedCopy() {
20
20
  const language = document.documentElement.lang || navigator.languages?.[0] || navigator.language || "en";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "minecodex",
3
- "version": "2.0.22",
3
+ "version": "2.0.24",
4
4
  "description": "Lightweight, local-first plugins for the Codex desktop app.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -152,7 +152,7 @@ async function normalizeModelSelector(modelSelector, projectDir) {
152
152
  throw new Error("modelSelector.icons requires fallback, star, and unstar");
153
153
  }
154
154
  const strings = {};
155
- for (const key of ["star", "unstar"]) {
155
+ for (const key of new Set(["star", "unstar", ...Object.keys(modelSelector.strings ?? {})])) {
156
156
  const localized = normalizeLocalizedText(modelSelector.strings?.[key], `modelSelector.strings.${key}`);
157
157
  strings[key] = { value: localized.value, locales: localized.locales };
158
158
  }