minecodex 2.0.22 → 2.0.23

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, () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "minecodex",
3
- "version": "2.0.22",
3
+ "version": "2.0.23",
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
  }