@spotpatch/runtime 1.14.0 → 1.15.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/dist/{chunk-2WA6XVA4.js → chunk-DCN6OMLH.js} +13 -3
  2. package/dist/{chunk-2WA6XVA4.js.map → chunk-DCN6OMLH.js.map} +1 -1
  3. package/dist/{chunk-KIDHJQRG.js → chunk-PXJKFEUJ.js} +1 -1
  4. package/dist/chunk-PXJKFEUJ.js.map +1 -0
  5. package/dist/contextual-ask-panel.cjs +217 -127
  6. package/dist/contextual-ask-panel.cjs.map +1 -1
  7. package/dist/contextual-ask-panel.d.cts +2 -0
  8. package/dist/contextual-ask-panel.d.ts +2 -0
  9. package/dist/contextual-ask-panel.js +219 -128
  10. package/dist/contextual-ask-panel.js.map +1 -1
  11. package/dist/data-flow-panel.cjs +12 -2
  12. package/dist/data-flow-panel.cjs.map +1 -1
  13. package/dist/data-flow-panel.d.cts +1 -1
  14. package/dist/data-flow-panel.d.ts +1 -1
  15. package/dist/data-flow-panel.js +7 -1
  16. package/dist/data-flow-panel.js.map +1 -1
  17. package/dist/{data-flow-runtime-DMmV0d3E.d.cts → data-flow-runtime-CacBbxN0.d.cts} +2 -1
  18. package/dist/{data-flow-runtime-DMmV0d3E.d.ts → data-flow-runtime-CacBbxN0.d.ts} +2 -1
  19. package/dist/data-flow.cjs +13 -2
  20. package/dist/data-flow.cjs.map +1 -1
  21. package/dist/data-flow.d.cts +1 -1
  22. package/dist/data-flow.d.ts +1 -1
  23. package/dist/data-flow.js +3 -1
  24. package/dist/external-handoff-panel.cjs +22 -6
  25. package/dist/external-handoff-panel.cjs.map +1 -1
  26. package/dist/external-handoff-panel.d.cts +5 -2
  27. package/dist/external-handoff-panel.d.ts +5 -2
  28. package/dist/external-handoff-panel.js +21 -6
  29. package/dist/external-handoff-panel.js.map +1 -1
  30. package/dist/index.cjs +15 -6
  31. package/dist/index.cjs.map +1 -1
  32. package/dist/index.d.cts +5 -5
  33. package/dist/index.d.ts +5 -5
  34. package/dist/index.js +11 -6
  35. package/dist/index.js.map +1 -1
  36. package/package.json +2 -2
  37. package/dist/chunk-KIDHJQRG.js.map +0 -1
@@ -81,8 +81,10 @@ function isExecutorCapability(value) {
81
81
  "providerDataConsentRequired",
82
82
  "readOnlyProven"
83
83
  ],
84
- ["errorCode"]
85
- ) || !isOpaqueId(value.executorId) || !isEnum(value.kind, import_contextual_ask_browser.CONTEXTUAL_ASK_EXECUTOR_KINDS) || !isNonEmptyString(value.label) || !isNonEmptyString(value.requestedModelLabel) || !isNonEmptyString(value.effectiveModelLabel) || !isEnum(value.state, import_contextual_ask_browser.CONTEXTUAL_ASK_EXECUTOR_STATES) || typeof value.providerDataConsentRequired !== "boolean" || typeof value.readOnlyProven !== "boolean" || value.errorCode !== void 0 && !isAskDomainError(value.errorCode)) {
84
+ ["errorCode", "models"]
85
+ ) || !isOpaqueId(value.executorId) || !isEnum(value.kind, import_contextual_ask_browser.CONTEXTUAL_ASK_EXECUTOR_KINDS) || !isNonEmptyString(value.label) || !isNonEmptyString(value.requestedModelLabel) || !isNonEmptyString(value.effectiveModelLabel) || value.models !== void 0 && (!Array.isArray(value.models) || value.models.length === 0 || value.models.length > import_contextual_ask_browser.CONTEXTUAL_ASK_LIMITS.maximumModels || !value.models.every(
86
+ (model) => isNonEmptyString(model) && model.length <= import_contextual_ask_browser.CONTEXTUAL_ASK_LIMITS.maximumLabelCharacters
87
+ ) || !unique(value.models)) || !isEnum(value.state, import_contextual_ask_browser.CONTEXTUAL_ASK_EXECUTOR_STATES) || typeof value.providerDataConsentRequired !== "boolean" || typeof value.readOnlyProven !== "boolean" || value.errorCode !== void 0 && !isAskDomainError(value.errorCode)) {
86
88
  return false;
87
89
  }
88
90
  return value.state === "ready" ? value.readOnlyProven && value.errorCode === void 0 : value.errorCode !== void 0;
@@ -556,6 +558,7 @@ function createContextualAskWorkflow(input) {
556
558
  return;
557
559
  }
558
560
  const requestRevision = ++revision;
561
+ const model = input.panel.readModel();
559
562
  currentQuestion = question;
560
563
  busy = true;
561
564
  input.onBusyChange(true);
@@ -568,6 +571,7 @@ function createContextualAskWorkflow(input) {
568
571
  requestId: input.createId(),
569
572
  envelope,
570
573
  executorId,
574
+ ...model === void 0 ? {} : { model },
571
575
  providerDataConsent: true
572
576
  });
573
577
  if (!isCurrent(requestRevision)) return;
@@ -801,6 +805,158 @@ function createButton(document, label, className = "") {
801
805
  return button;
802
806
  }
803
807
 
808
+ // src/ui/ask-picker.ts
809
+ function createAskPicker(document, onViewChange) {
810
+ const root = createMarkedElement(document, "div");
811
+ root.className = "spotpatch-ask-executor-picker";
812
+ const trigger = createButton(document, "", "spotpatch-ask-executor");
813
+ trigger.id = `spotpatch-ask-picker-${Math.random().toString(36).slice(2)}`;
814
+ trigger.setAttribute("role", "combobox");
815
+ trigger.setAttribute("aria-haspopup", "listbox");
816
+ trigger.setAttribute("aria-expanded", "false");
817
+ const text = createMarkedElement(document, "span");
818
+ trigger.append(text);
819
+ const menu = createMarkedElement(document, "div");
820
+ menu.id = `${trigger.id}-menu`;
821
+ menu.className = "spotpatch-ask-executor-menu";
822
+ menu.setAttribute("role", "listbox");
823
+ menu.hidden = true;
824
+ trigger.setAttribute("aria-controls", menu.id);
825
+ const select = createMarkedElement(document, "select");
826
+ select.className = "spotpatch-ask-executor-native";
827
+ select.tabIndex = -1;
828
+ select.setAttribute("aria-hidden", "true");
829
+ root.append(trigger, menu, select);
830
+ function close(focus = false) {
831
+ if (menu.hidden) return;
832
+ menu.hidden = true;
833
+ trigger.setAttribute("aria-expanded", "false");
834
+ trigger.removeAttribute("aria-activedescendant");
835
+ if (focus) trigger.focus({ preventScroll: true });
836
+ onViewChange();
837
+ }
838
+ function activate(index) {
839
+ const options = [...menu.children];
840
+ const active = options[index];
841
+ if (active === void 0) return;
842
+ for (const option of options) option.dataset.active = String(option === active);
843
+ trigger.setAttribute("aria-activedescendant", active.id);
844
+ const itemRect = active.getBoundingClientRect();
845
+ const menuRect = menu.getBoundingClientRect();
846
+ if (itemRect.top < menuRect.top) menu.scrollTop -= menuRect.top - itemRect.top;
847
+ else if (itemRect.bottom > menuRect.bottom)
848
+ menu.scrollTop += itemRect.bottom - menuRect.bottom;
849
+ }
850
+ function open() {
851
+ if (trigger.disabled || !menu.hidden) return;
852
+ menu.hidden = false;
853
+ trigger.setAttribute("aria-expanded", "true");
854
+ activate(Math.max(0, select.selectedIndex));
855
+ onViewChange();
856
+ }
857
+ function sync() {
858
+ text.textContent = select.selectedOptions[0]?.textContent ?? "";
859
+ trigger.title = text.textContent;
860
+ trigger.dataset.empty = String(select.value.length === 0);
861
+ for (const option of [...menu.children]) {
862
+ option.setAttribute(
863
+ "aria-selected",
864
+ String(option.dataset.value === select.value)
865
+ );
866
+ }
867
+ }
868
+ function choose(value) {
869
+ if (trigger.disabled) return;
870
+ select.value = value;
871
+ sync();
872
+ close(true);
873
+ select.dispatchEvent(new Event("change", { bubbles: true }));
874
+ }
875
+ function rebuild() {
876
+ close();
877
+ menu.replaceChildren();
878
+ for (const [index, option] of [...select.options].entries()) {
879
+ const item = createMarkedElement(document, "div");
880
+ item.id = `${menu.id}-${String(index)}`;
881
+ item.className = "spotpatch-ask-executor-option";
882
+ item.setAttribute("role", "option");
883
+ item.dataset.value = option.value;
884
+ item.textContent = option.textContent;
885
+ item.title = option.textContent;
886
+ item.addEventListener("pointerdown", (event) => {
887
+ event.preventDefault();
888
+ });
889
+ item.addEventListener("click", () => {
890
+ choose(option.value);
891
+ });
892
+ menu.append(item);
893
+ }
894
+ sync();
895
+ }
896
+ trigger.addEventListener("click", () => {
897
+ if (menu.hidden) open();
898
+ else close();
899
+ });
900
+ trigger.addEventListener("keydown", (event) => {
901
+ if (event.key === "Escape" && !menu.hidden) {
902
+ event.preventDefault();
903
+ event.stopPropagation();
904
+ close();
905
+ return;
906
+ }
907
+ if (event.key === "Tab") {
908
+ close();
909
+ return;
910
+ }
911
+ if (["Enter", " "].includes(event.key) && !menu.hidden) {
912
+ event.preventDefault();
913
+ const active = [...menu.children].find(
914
+ (item) => item.id === trigger.getAttribute("aria-activedescendant")
915
+ );
916
+ if (active?.dataset.value !== void 0) choose(active.dataset.value);
917
+ return;
918
+ }
919
+ if (!["ArrowDown", "ArrowUp", "Home", "End"].includes(event.key)) return;
920
+ event.preventDefault();
921
+ if (menu.hidden) {
922
+ open();
923
+ return;
924
+ }
925
+ const options = [...menu.children];
926
+ const index = options.findIndex(
927
+ (item) => item.id === trigger.getAttribute("aria-activedescendant")
928
+ );
929
+ activate(
930
+ event.key === "Home" ? 0 : event.key === "End" ? options.length - 1 : (index + (event.key === "ArrowDown" ? 1 : -1) + options.length) % options.length
931
+ );
932
+ });
933
+ root.addEventListener("focusout", (event) => {
934
+ if (!(event.relatedTarget instanceof Node) || !root.contains(event.relatedTarget))
935
+ close();
936
+ });
937
+ const outside = (event) => {
938
+ if (!event.composedPath().includes(root)) close();
939
+ };
940
+ document.addEventListener("pointerdown", outside);
941
+ select.addEventListener("change", sync);
942
+ return {
943
+ root,
944
+ trigger,
945
+ select,
946
+ rebuild,
947
+ close,
948
+ setDisabled(disabled) {
949
+ select.disabled = disabled;
950
+ trigger.disabled = disabled || select.options.length < 2;
951
+ trigger.dataset.expandable = String(!trigger.disabled);
952
+ if (trigger.disabled) close();
953
+ },
954
+ dispose() {
955
+ document.removeEventListener("pointerdown", outside);
956
+ }
957
+ };
958
+ }
959
+
804
960
  // src/ui/contextual-ask-localization.ts
805
961
  var EN = Object.freeze({
806
962
  mode: Object.freeze({ ask: "Ask", change: "Change", label: "Task mode" }),
@@ -815,6 +971,7 @@ var EN = Object.freeze({
815
971
  "Why is it implemented this way?"
816
972
  ]),
817
973
  executorLabel: "Read-only executor",
974
+ modelLabel: "Model",
818
975
  loadingExecutors: "Checking available executors\u2026",
819
976
  noExecutor: "No verified read-only executor is available.",
820
977
  unavailable: "unavailable",
@@ -884,6 +1041,7 @@ var ZH = Object.freeze({
884
1041
  "\u4E3A\u4EC0\u4E48\u8981\u8FD9\u6837\u5B9E\u73B0\uFF1F"
885
1042
  ]),
886
1043
  executorLabel: "\u53EA\u8BFB\u6267\u884C\u5668",
1044
+ modelLabel: "\u6A21\u578B",
887
1045
  loadingExecutors: "\u6B63\u5728\u68C0\u67E5\u53EF\u7528\u6267\u884C\u5668\u2026",
888
1046
  noExecutor: "\u76EE\u524D\u6CA1\u6709\u901A\u8FC7\u53EA\u8BFB\u9A8C\u8BC1\u7684\u6267\u884C\u5668\u3002",
889
1047
  unavailable: "\u4E0D\u53EF\u7528",
@@ -952,7 +1110,8 @@ function createStyles(document) {
952
1110
  .spotpatch-ask-mode button { min-height: 34px; border: 0; border-radius: 7px; color: var(--spotpatch-text-secondary); background: transparent; cursor: pointer; font-size: 12px; font-weight: 700; }
953
1111
  .spotpatch-ask-mode button[aria-selected="true"] { color: #f7f7ff; background: linear-gradient(135deg, rgb(139 123 255 / 25%), rgb(82 168 255 / 14%)); box-shadow: inset 0 0 0 1px rgb(139 123 255 / 24%); }
954
1112
  .spotpatch-ask-panel { display: grid; gap: 14px; }
955
- .spotpatch-ask-field { display: grid; gap: 7px; }
1113
+ .spotpatch-ask-field { display: grid; gap: 7px; min-width: 0; }
1114
+ .spotpatch-ask-field[hidden] { display: none; }
956
1115
  .spotpatch-ask-field > label, .spotpatch-ask-label { color: #c9cad2; font-size: 11px; font-weight: 680; }
957
1116
  .spotpatch-ask-question { box-sizing: border-box; width: 100%; min-height: 92px; resize: vertical; border: 1px solid var(--spotpatch-border); border-radius: 10px; padding: 11px 12px; outline: none; color: var(--spotpatch-text); background: var(--spotpatch-bg-input); line-height: 1.55; }
958
1117
  .spotpatch-ask-question:focus { border-color: rgb(139 123 255 / 68%); box-shadow: 0 0 0 3px rgb(139 123 255 / 12%); }
@@ -967,10 +1126,10 @@ function createStyles(document) {
967
1126
  .spotpatch-ask-executor:focus-visible { border-color: rgb(139 123 255 / 68%); box-shadow: 0 0 0 3px rgb(139 123 255 / 12%); }
968
1127
  .spotpatch-ask-executor:disabled { color: var(--spotpatch-text); background: rgb(255 255 255 / 3%); cursor: default; opacity: 1; }
969
1128
  .spotpatch-ask-executor[data-empty="true"]:disabled { color: var(--spotpatch-text-muted); }
970
- .spotpatch-ask-executor-menu { display: grid; gap: 3px; border: 1px solid var(--spotpatch-border); border-radius: 9px; padding: 4px; background: #101116; box-shadow: 0 10px 28px rgb(0 0 0 / 28%); }
1129
+ .spotpatch-ask-executor-menu { display: grid; gap: 3px; border: 1px solid var(--spotpatch-border); border-radius: 9px; padding: 4px; background: var(--spotpatch-bg-input); max-height: 224px; overflow-y: auto; overscroll-behavior: contain; scrollbar-width: thin; box-shadow: 0 10px 28px rgb(0 0 0 / 28%); }
971
1130
  .spotpatch-ask-executor-menu[hidden] { display: none; }
972
- .spotpatch-ask-executor-option { box-sizing: border-box; min-height: 34px; border: 0; border-radius: 6px; padding: 7px 9px; color: var(--spotpatch-text-secondary); background: transparent; cursor: pointer; font: inherit; text-align: left; }
973
- .spotpatch-ask-executor-option:hover, .spotpatch-ask-executor-option:focus-visible { color: #fff; background: rgb(139 123 255 / 13%); outline: none; }
1131
+ .spotpatch-ask-executor-option { box-sizing: border-box; min-height: 34px; border: 0; border-radius: 6px; padding: 7px 9px; color: var(--spotpatch-text-secondary); background: transparent; cursor: pointer; font: inherit; text-align: left; overflow-wrap: anywhere; }
1132
+ .spotpatch-ask-executor-option[data-active="true"], .spotpatch-ask-executor-option:hover { color: #fff; background: rgb(139 123 255 / 13%); outline: none; }
974
1133
  .spotpatch-ask-executor-option[aria-selected="true"] { color: #f4f1ff; background: rgb(139 123 255 / 20%); }
975
1134
  .spotpatch-ask-executor-native { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden; border: 0; clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap; }
976
1135
  .spotpatch-ask-executor-status { margin: 0; border-left: 2px solid var(--spotpatch-warning); padding-left: 8px; color: #d8b66c; font-size: 10.5px; line-height: 1.45; }
@@ -1071,33 +1230,21 @@ function createContextualAskPanel(input) {
1071
1230
  const executorField = createMarkedElement(document, "div");
1072
1231
  executorField.className = "spotpatch-ask-field";
1073
1232
  const executorLabel = createMarkedElement(document, "label");
1074
- const executorPicker = createMarkedElement(document, "div");
1075
- executorPicker.className = "spotpatch-ask-executor-picker";
1076
- const executorTrigger = createButton(document, "", "spotpatch-ask-executor");
1077
- const executorSelect = createMarkedElement(document, "select");
1078
- const executorId = `spotpatch-ask-executor-${Math.random().toString(36).slice(2)}`;
1079
- const executorMenuId = `${executorId}-menu`;
1080
- executorTrigger.id = executorId;
1081
- executorTrigger.setAttribute("role", "combobox");
1082
- executorTrigger.setAttribute("aria-controls", executorMenuId);
1083
- executorTrigger.setAttribute("aria-expanded", "false");
1084
- const executorText = createMarkedElement(document, "span");
1085
- executorTrigger.append(executorText);
1086
- const executorMenu = createMarkedElement(document, "div");
1087
- executorMenu.id = executorMenuId;
1088
- executorMenu.className = "spotpatch-ask-executor-menu";
1089
- executorMenu.setAttribute("role", "listbox");
1090
- executorMenu.hidden = true;
1091
- executorSelect.className = "spotpatch-ask-executor-native";
1092
- executorSelect.tabIndex = -1;
1093
- executorSelect.setAttribute("aria-hidden", "true");
1094
- executorLabel.htmlFor = executorId;
1233
+ const executorPicker = createAskPicker(document, input.onViewChange);
1234
+ const executorSelect = executorPicker.select;
1235
+ executorLabel.htmlFor = executorPicker.trigger.id;
1236
+ const modelField = createMarkedElement(document, "div");
1237
+ modelField.className = "spotpatch-ask-field";
1238
+ const modelLabel = createMarkedElement(document, "label");
1239
+ const modelPicker = createAskPicker(document, input.onViewChange);
1240
+ modelLabel.htmlFor = modelPicker.trigger.id;
1241
+ modelField.append(modelLabel, modelPicker.root);
1242
+ let modelExecutorId;
1095
1243
  const executorStatus = createMarkedElement(document, "p");
1096
1244
  executorStatus.className = "spotpatch-ask-executor-status";
1097
1245
  executorStatus.setAttribute("role", "status");
1098
1246
  executorStatus.hidden = true;
1099
- executorPicker.append(executorTrigger, executorMenu, executorSelect);
1100
- executorField.append(executorLabel, executorPicker, executorStatus);
1247
+ executorField.append(executorLabel, executorPicker.root, executorStatus);
1101
1248
  const safety = createMarkedElement(document, "div");
1102
1249
  safety.className = "spotpatch-ask-safety";
1103
1250
  const dataSummary = createMarkedElement(document, "div");
@@ -1154,7 +1301,16 @@ function createContextualAskPanel(input) {
1154
1301
  const convertButton = createButton(document, "", "spotpatch-primary");
1155
1302
  answerActions.append(copyButton, convertButton);
1156
1303
  answer.append(stale, answerTitle, warnings, blocks, sourcesSection, answerActions);
1157
- askPanel.append(questionField, executorField, safety, status, error, actions, answer);
1304
+ askPanel.append(
1305
+ questionField,
1306
+ executorField,
1307
+ modelField,
1308
+ safety,
1309
+ status,
1310
+ error,
1311
+ actions,
1312
+ answer
1313
+ );
1158
1314
  root.append(modeSwitch, origin, askPanel);
1159
1315
  function hasReadyExecutor() {
1160
1316
  const selected = currentCapability?.executors.find(
@@ -1167,57 +1323,31 @@ function createContextualAskPanel(input) {
1167
1323
  (candidate) => candidate.state === "ready" && candidate.readOnlyProven
1168
1324
  ) === true;
1169
1325
  }
1170
- function readyExecutorCount() {
1171
- return currentCapability?.executors.filter(
1172
- (candidate) => candidate.state === "ready" && candidate.readOnlyProven
1173
- ).length ?? 0;
1174
- }
1175
- function closeExecutorMenu(focusTrigger = false) {
1176
- if (executorMenu.hidden) return;
1177
- executorMenu.hidden = true;
1178
- executorTrigger.setAttribute("aria-expanded", "false");
1179
- if (focusTrigger) executorTrigger.focus({ preventScroll: true });
1180
- input.onViewChange();
1181
- }
1182
- function openExecutorMenu(focusSelected = false) {
1183
- if (executorTrigger.disabled || !executorMenu.hidden) return;
1184
- executorMenu.hidden = false;
1185
- executorTrigger.setAttribute("aria-expanded", "true");
1186
- if (focusSelected) {
1187
- const selected = executorMenu.querySelector(
1188
- '.spotpatch-ask-executor-option[aria-selected="true"]'
1189
- );
1190
- const first = executorMenu.firstElementChild;
1191
- const focusTarget = selected ?? (first instanceof HTMLElement ? first : null);
1192
- focusTarget?.focus({ preventScroll: true });
1193
- }
1194
- input.onViewChange();
1195
- }
1196
- function syncExecutorControl() {
1197
- const selectedOption = [...executorSelect.options].find(
1198
- (option) => option.value === executorSelect.value
1326
+ function renderModels() {
1327
+ const executor = currentCapability?.executors.find(
1328
+ (item) => item.executorId === executorSelect.value
1199
1329
  );
1200
- executorText.textContent = selectedOption?.textContent ?? messages.noExecutor;
1201
- executorTrigger.dataset.empty = String(executorSelect.value.length === 0);
1202
- for (const option of executorMenu.querySelectorAll(
1203
- ".spotpatch-ask-executor-option"
1204
- )) {
1205
- option.setAttribute(
1206
- "aria-selected",
1207
- String(option.dataset.executorId === executorSelect.value)
1208
- );
1330
+ const previous = modelExecutorId === executor?.executorId ? modelPicker.select.value : void 0;
1331
+ modelExecutorId = executor?.executorId;
1332
+ const models = executor?.models;
1333
+ modelField.hidden = models === void 0;
1334
+ modelPicker.select.replaceChildren();
1335
+ for (const model of models ?? []) {
1336
+ const option = document.createElement("option");
1337
+ option.value = model;
1338
+ option.textContent = model;
1339
+ modelPicker.select.append(option);
1209
1340
  }
1341
+ modelPicker.select.value = models?.find((model) => model === previous) ?? models?.find((model) => model === executor?.requestedModelLabel) ?? models?.[0] ?? "";
1342
+ modelPicker.rebuild();
1210
1343
  }
1211
1344
  function refreshSubmitState() {
1212
1345
  submitButton.disabled = busy || questionInput.value.trim().length === 0 || !hasReadyExecutor() || !consentCheckbox.checked || currentPreview.targetCount === 0;
1213
1346
  if (!currentPreview.contextReady) submitButton.disabled = true;
1214
1347
  questionInput.disabled = busy;
1215
- const readyCount = readyExecutorCount();
1216
1348
  const executorUnavailable = busy || currentCapability === void 0 || !hasAnyReadyExecutor();
1217
- executorSelect.disabled = executorUnavailable;
1218
- executorTrigger.disabled = executorUnavailable || readyCount < 2;
1219
- executorTrigger.dataset.expandable = String(!executorUnavailable && readyCount > 1);
1220
- if (executorTrigger.disabled) closeExecutorMenu();
1349
+ executorPicker.setDisabled(executorUnavailable);
1350
+ modelPicker.setDisabled(busy || !hasReadyExecutor());
1221
1351
  consentCheckbox.disabled = busy || !hasReadyExecutor();
1222
1352
  newQuestionButton.hidden = busy || currentResult === void 0;
1223
1353
  cancelButton.hidden = !busy;
@@ -1260,6 +1390,9 @@ function createContextualAskPanel(input) {
1260
1390
  questionInput.placeholder = messages.questionPlaceholder;
1261
1391
  suggestions.setAttribute("aria-label", messages.suggestionsLabel);
1262
1392
  executorLabel.textContent = messages.executorLabel;
1393
+ modelLabel.textContent = messages.modelLabel;
1394
+ executorPicker.root.querySelector("[role=listbox]")?.setAttribute("aria-label", messages.executorLabel);
1395
+ modelPicker.root.querySelector("[role=listbox]")?.setAttribute("aria-label", messages.modelLabel);
1263
1396
  consentText.textContent = messages.consent;
1264
1397
  safetyText.textContent = messages.safety;
1265
1398
  submitButton.textContent = messages.submit;
@@ -1284,8 +1417,7 @@ function createContextualAskPanel(input) {
1284
1417
  currentCapability = capability;
1285
1418
  const previous = executorSelect.value;
1286
1419
  executorSelect.replaceChildren();
1287
- executorMenu.replaceChildren();
1288
- closeExecutorMenu();
1420
+ executorPicker.close();
1289
1421
  executorStatus.hidden = true;
1290
1422
  executorStatus.textContent = "";
1291
1423
  if (capability === void 0) {
@@ -1308,22 +1440,8 @@ function createContextualAskPanel(input) {
1308
1440
  for (const executor of readyExecutors) {
1309
1441
  const option = document.createElement("option");
1310
1442
  option.value = executor.executorId;
1311
- option.textContent = `${executor.label} \xB7 ${executor.effectiveModelLabel}`;
1443
+ option.textContent = executor.models === void 0 ? `${executor.label} \xB7 ${executor.effectiveModelLabel}` : executor.label;
1312
1444
  executorSelect.append(option);
1313
- const menuOption = createButton(
1314
- document,
1315
- option.textContent,
1316
- "spotpatch-ask-executor-option"
1317
- );
1318
- menuOption.dataset.executorId = executor.executorId;
1319
- menuOption.setAttribute("role", "option");
1320
- menuOption.addEventListener("click", () => {
1321
- executorSelect.value = executor.executorId;
1322
- syncExecutorControl();
1323
- closeExecutorMenu(true);
1324
- executorSelect.dispatchEvent(new Event("change", { bubbles: true }));
1325
- });
1326
- executorMenu.append(menuOption);
1327
1445
  }
1328
1446
  if (readyExecutors.length === 0) {
1329
1447
  const option = document.createElement("option");
@@ -1339,7 +1457,8 @@ function createContextualAskPanel(input) {
1339
1457
  executorStatus.hidden = false;
1340
1458
  }
1341
1459
  }
1342
- syncExecutorControl();
1460
+ executorPicker.rebuild();
1461
+ renderModels();
1343
1462
  refreshSubmitState();
1344
1463
  }
1345
1464
  function renderJob(snapshot) {
@@ -1494,42 +1613,11 @@ ${sourceText}`;
1494
1613
  applyMode();
1495
1614
  });
1496
1615
  questionInput.addEventListener("input", refreshSubmitState);
1497
- executorSelect.addEventListener("change", refreshSubmitState);
1498
- executorTrigger.addEventListener("click", () => {
1499
- if (executorMenu.hidden) openExecutorMenu();
1500
- else closeExecutorMenu();
1501
- });
1502
- executorTrigger.addEventListener("keydown", (event) => {
1503
- if (event.key !== "ArrowDown" && event.key !== "ArrowUp") return;
1504
- event.preventDefault();
1505
- openExecutorMenu(true);
1506
- });
1507
- executorMenu.addEventListener("keydown", (event) => {
1508
- const options = [
1509
- ...executorMenu.querySelectorAll(
1510
- ".spotpatch-ask-executor-option"
1511
- )
1512
- ];
1513
- if (event.key === "Escape") {
1514
- event.preventDefault();
1515
- closeExecutorMenu(true);
1516
- return;
1517
- }
1518
- if (!["ArrowDown", "ArrowUp", "Home", "End"].includes(event.key)) return;
1519
- event.preventDefault();
1520
- const currentIndex = options.indexOf(document.activeElement);
1521
- const nextIndex = event.key === "Home" ? 0 : event.key === "End" ? options.length - 1 : event.key === "ArrowDown" ? (currentIndex + 1 + options.length) % options.length : (currentIndex - 1 + options.length) % options.length;
1522
- options[nextIndex]?.focus({ preventScroll: true });
1523
- });
1524
- executorPicker.addEventListener("focusout", (event) => {
1525
- if (!(event.relatedTarget instanceof Node) || !executorPicker.contains(event.relatedTarget)) {
1526
- closeExecutorMenu();
1527
- }
1616
+ executorSelect.addEventListener("change", () => {
1617
+ renderModels();
1618
+ refreshSubmitState();
1619
+ input.onViewChange();
1528
1620
  });
1529
- const closeExecutorMenuOutside = (event) => {
1530
- if (!event.composedPath().includes(executorPicker)) closeExecutorMenu();
1531
- };
1532
- document.addEventListener("pointerdown", closeExecutorMenuOutside);
1533
1621
  consentCheckbox.addEventListener("change", refreshSubmitState);
1534
1622
  const unsubscribeLocale = input.subscribeLocale(applyMessages);
1535
1623
  applyMessages();
@@ -1548,7 +1636,8 @@ ${sourceText}`;
1548
1636
  clear,
1549
1637
  dispose: () => {
1550
1638
  unsubscribeLocale();
1551
- document.removeEventListener("pointerdown", closeExecutorMenuOutside);
1639
+ executorPicker.dispose();
1640
+ modelPicker.dispose();
1552
1641
  },
1553
1642
  focusQuestion: () => {
1554
1643
  questionInput.focus({ preventScroll: true });
@@ -1560,6 +1649,7 @@ ${sourceText}`;
1560
1649
  },
1561
1650
  readConsent: () => consentCheckbox.checked,
1562
1651
  readExecutorId: () => executorSelect.value || void 0,
1652
+ readModel: () => modelPicker.select.value || void 0,
1563
1653
  readQuestion: () => questionInput.value.trim(),
1564
1654
  renderAnswer,
1565
1655
  renderCapability,