@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
@@ -48,6 +48,7 @@ interface ContextualAskPanel {
48
48
  readonly notify: (event: "copied" | "copy-failed" | "converted" | "source-open-failed") => void;
49
49
  readonly readConsent: () => boolean;
50
50
  readonly readExecutorId: () => string | undefined;
51
+ readonly readModel: () => string | undefined;
51
52
  readonly readQuestion: () => string;
52
53
  readonly renderAnswer: (result: AskAnswerResult, stale: boolean) => void;
53
54
  readonly renderCapability: (capability?: ContextualAskCapability) => void;
@@ -113,6 +114,7 @@ interface ContextualAskMessages {
113
114
  readonly suggestionsLabel: string;
114
115
  readonly suggestions: readonly string[];
115
116
  readonly executorLabel: string;
117
+ readonly modelLabel: string;
116
118
  readonly loadingExecutors: string;
117
119
  readonly noExecutor: string;
118
120
  readonly unavailable: string;
@@ -48,6 +48,7 @@ interface ContextualAskPanel {
48
48
  readonly notify: (event: "copied" | "copy-failed" | "converted" | "source-open-failed") => void;
49
49
  readonly readConsent: () => boolean;
50
50
  readonly readExecutorId: () => string | undefined;
51
+ readonly readModel: () => string | undefined;
51
52
  readonly readQuestion: () => string;
52
53
  readonly renderAnswer: (result: AskAnswerResult, stale: boolean) => void;
53
54
  readonly renderCapability: (capability?: ContextualAskCapability) => void;
@@ -113,6 +114,7 @@ interface ContextualAskMessages {
113
114
  readonly suggestionsLabel: string;
114
115
  readonly suggestions: readonly string[];
115
116
  readonly executorLabel: string;
117
+ readonly modelLabel: string;
116
118
  readonly loadingExecutors: string;
117
119
  readonly noExecutor: string;
118
120
  readonly unavailable: string;
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  getContextualAskExtension,
3
3
  registerContextualAskExtension
4
- } from "./chunk-KIDHJQRG.js";
4
+ } from "./chunk-PXJKFEUJ.js";
5
5
  import {
6
6
  createButton,
7
7
  createMarkedElement
@@ -29,6 +29,7 @@ import {
29
29
  ASK_SOURCE_CONFIDENCES,
30
30
  CONTEXTUAL_ASK_EXECUTOR_KINDS,
31
31
  CONTEXTUAL_ASK_EXECUTOR_STATES,
32
+ CONTEXTUAL_ASK_LIMITS,
32
33
  CONTEXTUAL_ASK_SCHEMA_VERSION,
33
34
  isErrorCode
34
35
  } from "@spotpatch/shared/contextual-ask-browser";
@@ -77,8 +78,10 @@ function isExecutorCapability(value) {
77
78
  "providerDataConsentRequired",
78
79
  "readOnlyProven"
79
80
  ],
80
- ["errorCode"]
81
- ) || !isOpaqueId(value.executorId) || !isEnum(value.kind, CONTEXTUAL_ASK_EXECUTOR_KINDS) || !isNonEmptyString(value.label) || !isNonEmptyString(value.requestedModelLabel) || !isNonEmptyString(value.effectiveModelLabel) || !isEnum(value.state, CONTEXTUAL_ASK_EXECUTOR_STATES) || typeof value.providerDataConsentRequired !== "boolean" || typeof value.readOnlyProven !== "boolean" || value.errorCode !== void 0 && !isAskDomainError(value.errorCode)) {
81
+ ["errorCode", "models"]
82
+ ) || !isOpaqueId(value.executorId) || !isEnum(value.kind, 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 > CONTEXTUAL_ASK_LIMITS.maximumModels || !value.models.every(
83
+ (model) => isNonEmptyString(model) && model.length <= CONTEXTUAL_ASK_LIMITS.maximumLabelCharacters
84
+ ) || !unique(value.models)) || !isEnum(value.state, CONTEXTUAL_ASK_EXECUTOR_STATES) || typeof value.providerDataConsentRequired !== "boolean" || typeof value.readOnlyProven !== "boolean" || value.errorCode !== void 0 && !isAskDomainError(value.errorCode)) {
82
85
  return false;
83
86
  }
84
87
  return value.state === "ready" ? value.readOnlyProven && value.errorCode === void 0 : value.errorCode !== void 0;
@@ -552,6 +555,7 @@ function createContextualAskWorkflow(input) {
552
555
  return;
553
556
  }
554
557
  const requestRevision = ++revision;
558
+ const model = input.panel.readModel();
555
559
  currentQuestion = question;
556
560
  busy = true;
557
561
  input.onBusyChange(true);
@@ -564,6 +568,7 @@ function createContextualAskWorkflow(input) {
564
568
  requestId: input.createId(),
565
569
  envelope,
566
570
  executorId,
571
+ ...model === void 0 ? {} : { model },
567
572
  providerDataConsent: true
568
573
  });
569
574
  if (!isCurrent(requestRevision)) return;
@@ -758,6 +763,158 @@ function createContextualAskWorkflow(input) {
758
763
  });
759
764
  }
760
765
 
766
+ // src/ui/ask-picker.ts
767
+ function createAskPicker(document, onViewChange) {
768
+ const root = createMarkedElement(document, "div");
769
+ root.className = "spotpatch-ask-executor-picker";
770
+ const trigger = createButton(document, "", "spotpatch-ask-executor");
771
+ trigger.id = `spotpatch-ask-picker-${Math.random().toString(36).slice(2)}`;
772
+ trigger.setAttribute("role", "combobox");
773
+ trigger.setAttribute("aria-haspopup", "listbox");
774
+ trigger.setAttribute("aria-expanded", "false");
775
+ const text = createMarkedElement(document, "span");
776
+ trigger.append(text);
777
+ const menu = createMarkedElement(document, "div");
778
+ menu.id = `${trigger.id}-menu`;
779
+ menu.className = "spotpatch-ask-executor-menu";
780
+ menu.setAttribute("role", "listbox");
781
+ menu.hidden = true;
782
+ trigger.setAttribute("aria-controls", menu.id);
783
+ const select = createMarkedElement(document, "select");
784
+ select.className = "spotpatch-ask-executor-native";
785
+ select.tabIndex = -1;
786
+ select.setAttribute("aria-hidden", "true");
787
+ root.append(trigger, menu, select);
788
+ function close(focus = false) {
789
+ if (menu.hidden) return;
790
+ menu.hidden = true;
791
+ trigger.setAttribute("aria-expanded", "false");
792
+ trigger.removeAttribute("aria-activedescendant");
793
+ if (focus) trigger.focus({ preventScroll: true });
794
+ onViewChange();
795
+ }
796
+ function activate(index) {
797
+ const options = [...menu.children];
798
+ const active = options[index];
799
+ if (active === void 0) return;
800
+ for (const option of options) option.dataset.active = String(option === active);
801
+ trigger.setAttribute("aria-activedescendant", active.id);
802
+ const itemRect = active.getBoundingClientRect();
803
+ const menuRect = menu.getBoundingClientRect();
804
+ if (itemRect.top < menuRect.top) menu.scrollTop -= menuRect.top - itemRect.top;
805
+ else if (itemRect.bottom > menuRect.bottom)
806
+ menu.scrollTop += itemRect.bottom - menuRect.bottom;
807
+ }
808
+ function open() {
809
+ if (trigger.disabled || !menu.hidden) return;
810
+ menu.hidden = false;
811
+ trigger.setAttribute("aria-expanded", "true");
812
+ activate(Math.max(0, select.selectedIndex));
813
+ onViewChange();
814
+ }
815
+ function sync() {
816
+ text.textContent = select.selectedOptions[0]?.textContent ?? "";
817
+ trigger.title = text.textContent;
818
+ trigger.dataset.empty = String(select.value.length === 0);
819
+ for (const option of [...menu.children]) {
820
+ option.setAttribute(
821
+ "aria-selected",
822
+ String(option.dataset.value === select.value)
823
+ );
824
+ }
825
+ }
826
+ function choose(value) {
827
+ if (trigger.disabled) return;
828
+ select.value = value;
829
+ sync();
830
+ close(true);
831
+ select.dispatchEvent(new Event("change", { bubbles: true }));
832
+ }
833
+ function rebuild() {
834
+ close();
835
+ menu.replaceChildren();
836
+ for (const [index, option] of [...select.options].entries()) {
837
+ const item = createMarkedElement(document, "div");
838
+ item.id = `${menu.id}-${String(index)}`;
839
+ item.className = "spotpatch-ask-executor-option";
840
+ item.setAttribute("role", "option");
841
+ item.dataset.value = option.value;
842
+ item.textContent = option.textContent;
843
+ item.title = option.textContent;
844
+ item.addEventListener("pointerdown", (event) => {
845
+ event.preventDefault();
846
+ });
847
+ item.addEventListener("click", () => {
848
+ choose(option.value);
849
+ });
850
+ menu.append(item);
851
+ }
852
+ sync();
853
+ }
854
+ trigger.addEventListener("click", () => {
855
+ if (menu.hidden) open();
856
+ else close();
857
+ });
858
+ trigger.addEventListener("keydown", (event) => {
859
+ if (event.key === "Escape" && !menu.hidden) {
860
+ event.preventDefault();
861
+ event.stopPropagation();
862
+ close();
863
+ return;
864
+ }
865
+ if (event.key === "Tab") {
866
+ close();
867
+ return;
868
+ }
869
+ if (["Enter", " "].includes(event.key) && !menu.hidden) {
870
+ event.preventDefault();
871
+ const active = [...menu.children].find(
872
+ (item) => item.id === trigger.getAttribute("aria-activedescendant")
873
+ );
874
+ if (active?.dataset.value !== void 0) choose(active.dataset.value);
875
+ return;
876
+ }
877
+ if (!["ArrowDown", "ArrowUp", "Home", "End"].includes(event.key)) return;
878
+ event.preventDefault();
879
+ if (menu.hidden) {
880
+ open();
881
+ return;
882
+ }
883
+ const options = [...menu.children];
884
+ const index = options.findIndex(
885
+ (item) => item.id === trigger.getAttribute("aria-activedescendant")
886
+ );
887
+ activate(
888
+ event.key === "Home" ? 0 : event.key === "End" ? options.length - 1 : (index + (event.key === "ArrowDown" ? 1 : -1) + options.length) % options.length
889
+ );
890
+ });
891
+ root.addEventListener("focusout", (event) => {
892
+ if (!(event.relatedTarget instanceof Node) || !root.contains(event.relatedTarget))
893
+ close();
894
+ });
895
+ const outside = (event) => {
896
+ if (!event.composedPath().includes(root)) close();
897
+ };
898
+ document.addEventListener("pointerdown", outside);
899
+ select.addEventListener("change", sync);
900
+ return {
901
+ root,
902
+ trigger,
903
+ select,
904
+ rebuild,
905
+ close,
906
+ setDisabled(disabled) {
907
+ select.disabled = disabled;
908
+ trigger.disabled = disabled || select.options.length < 2;
909
+ trigger.dataset.expandable = String(!trigger.disabled);
910
+ if (trigger.disabled) close();
911
+ },
912
+ dispose() {
913
+ document.removeEventListener("pointerdown", outside);
914
+ }
915
+ };
916
+ }
917
+
761
918
  // src/ui/contextual-ask-localization.ts
762
919
  var EN = Object.freeze({
763
920
  mode: Object.freeze({ ask: "Ask", change: "Change", label: "Task mode" }),
@@ -772,6 +929,7 @@ var EN = Object.freeze({
772
929
  "Why is it implemented this way?"
773
930
  ]),
774
931
  executorLabel: "Read-only executor",
932
+ modelLabel: "Model",
775
933
  loadingExecutors: "Checking available executors\u2026",
776
934
  noExecutor: "No verified read-only executor is available.",
777
935
  unavailable: "unavailable",
@@ -841,6 +999,7 @@ var ZH = Object.freeze({
841
999
  "\u4E3A\u4EC0\u4E48\u8981\u8FD9\u6837\u5B9E\u73B0\uFF1F"
842
1000
  ]),
843
1001
  executorLabel: "\u53EA\u8BFB\u6267\u884C\u5668",
1002
+ modelLabel: "\u6A21\u578B",
844
1003
  loadingExecutors: "\u6B63\u5728\u68C0\u67E5\u53EF\u7528\u6267\u884C\u5668\u2026",
845
1004
  noExecutor: "\u76EE\u524D\u6CA1\u6709\u901A\u8FC7\u53EA\u8BFB\u9A8C\u8BC1\u7684\u6267\u884C\u5668\u3002",
846
1005
  unavailable: "\u4E0D\u53EF\u7528",
@@ -909,7 +1068,8 @@ function createStyles(document) {
909
1068
  .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; }
910
1069
  .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%); }
911
1070
  .spotpatch-ask-panel { display: grid; gap: 14px; }
912
- .spotpatch-ask-field { display: grid; gap: 7px; }
1071
+ .spotpatch-ask-field { display: grid; gap: 7px; min-width: 0; }
1072
+ .spotpatch-ask-field[hidden] { display: none; }
913
1073
  .spotpatch-ask-field > label, .spotpatch-ask-label { color: #c9cad2; font-size: 11px; font-weight: 680; }
914
1074
  .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; }
915
1075
  .spotpatch-ask-question:focus { border-color: rgb(139 123 255 / 68%); box-shadow: 0 0 0 3px rgb(139 123 255 / 12%); }
@@ -924,10 +1084,10 @@ function createStyles(document) {
924
1084
  .spotpatch-ask-executor:focus-visible { border-color: rgb(139 123 255 / 68%); box-shadow: 0 0 0 3px rgb(139 123 255 / 12%); }
925
1085
  .spotpatch-ask-executor:disabled { color: var(--spotpatch-text); background: rgb(255 255 255 / 3%); cursor: default; opacity: 1; }
926
1086
  .spotpatch-ask-executor[data-empty="true"]:disabled { color: var(--spotpatch-text-muted); }
927
- .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%); }
1087
+ .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%); }
928
1088
  .spotpatch-ask-executor-menu[hidden] { display: none; }
929
- .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; }
930
- .spotpatch-ask-executor-option:hover, .spotpatch-ask-executor-option:focus-visible { color: #fff; background: rgb(139 123 255 / 13%); outline: none; }
1089
+ .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; }
1090
+ .spotpatch-ask-executor-option[data-active="true"], .spotpatch-ask-executor-option:hover { color: #fff; background: rgb(139 123 255 / 13%); outline: none; }
931
1091
  .spotpatch-ask-executor-option[aria-selected="true"] { color: #f4f1ff; background: rgb(139 123 255 / 20%); }
932
1092
  .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; }
933
1093
  .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; }
@@ -1028,33 +1188,21 @@ function createContextualAskPanel(input) {
1028
1188
  const executorField = createMarkedElement(document, "div");
1029
1189
  executorField.className = "spotpatch-ask-field";
1030
1190
  const executorLabel = createMarkedElement(document, "label");
1031
- const executorPicker = createMarkedElement(document, "div");
1032
- executorPicker.className = "spotpatch-ask-executor-picker";
1033
- const executorTrigger = createButton(document, "", "spotpatch-ask-executor");
1034
- const executorSelect = createMarkedElement(document, "select");
1035
- const executorId = `spotpatch-ask-executor-${Math.random().toString(36).slice(2)}`;
1036
- const executorMenuId = `${executorId}-menu`;
1037
- executorTrigger.id = executorId;
1038
- executorTrigger.setAttribute("role", "combobox");
1039
- executorTrigger.setAttribute("aria-controls", executorMenuId);
1040
- executorTrigger.setAttribute("aria-expanded", "false");
1041
- const executorText = createMarkedElement(document, "span");
1042
- executorTrigger.append(executorText);
1043
- const executorMenu = createMarkedElement(document, "div");
1044
- executorMenu.id = executorMenuId;
1045
- executorMenu.className = "spotpatch-ask-executor-menu";
1046
- executorMenu.setAttribute("role", "listbox");
1047
- executorMenu.hidden = true;
1048
- executorSelect.className = "spotpatch-ask-executor-native";
1049
- executorSelect.tabIndex = -1;
1050
- executorSelect.setAttribute("aria-hidden", "true");
1051
- executorLabel.htmlFor = executorId;
1191
+ const executorPicker = createAskPicker(document, input.onViewChange);
1192
+ const executorSelect = executorPicker.select;
1193
+ executorLabel.htmlFor = executorPicker.trigger.id;
1194
+ const modelField = createMarkedElement(document, "div");
1195
+ modelField.className = "spotpatch-ask-field";
1196
+ const modelLabel = createMarkedElement(document, "label");
1197
+ const modelPicker = createAskPicker(document, input.onViewChange);
1198
+ modelLabel.htmlFor = modelPicker.trigger.id;
1199
+ modelField.append(modelLabel, modelPicker.root);
1200
+ let modelExecutorId;
1052
1201
  const executorStatus = createMarkedElement(document, "p");
1053
1202
  executorStatus.className = "spotpatch-ask-executor-status";
1054
1203
  executorStatus.setAttribute("role", "status");
1055
1204
  executorStatus.hidden = true;
1056
- executorPicker.append(executorTrigger, executorMenu, executorSelect);
1057
- executorField.append(executorLabel, executorPicker, executorStatus);
1205
+ executorField.append(executorLabel, executorPicker.root, executorStatus);
1058
1206
  const safety = createMarkedElement(document, "div");
1059
1207
  safety.className = "spotpatch-ask-safety";
1060
1208
  const dataSummary = createMarkedElement(document, "div");
@@ -1111,7 +1259,16 @@ function createContextualAskPanel(input) {
1111
1259
  const convertButton = createButton(document, "", "spotpatch-primary");
1112
1260
  answerActions.append(copyButton, convertButton);
1113
1261
  answer.append(stale, answerTitle, warnings, blocks, sourcesSection, answerActions);
1114
- askPanel.append(questionField, executorField, safety, status, error, actions, answer);
1262
+ askPanel.append(
1263
+ questionField,
1264
+ executorField,
1265
+ modelField,
1266
+ safety,
1267
+ status,
1268
+ error,
1269
+ actions,
1270
+ answer
1271
+ );
1115
1272
  root.append(modeSwitch, origin, askPanel);
1116
1273
  function hasReadyExecutor() {
1117
1274
  const selected = currentCapability?.executors.find(
@@ -1124,57 +1281,31 @@ function createContextualAskPanel(input) {
1124
1281
  (candidate) => candidate.state === "ready" && candidate.readOnlyProven
1125
1282
  ) === true;
1126
1283
  }
1127
- function readyExecutorCount() {
1128
- return currentCapability?.executors.filter(
1129
- (candidate) => candidate.state === "ready" && candidate.readOnlyProven
1130
- ).length ?? 0;
1131
- }
1132
- function closeExecutorMenu(focusTrigger = false) {
1133
- if (executorMenu.hidden) return;
1134
- executorMenu.hidden = true;
1135
- executorTrigger.setAttribute("aria-expanded", "false");
1136
- if (focusTrigger) executorTrigger.focus({ preventScroll: true });
1137
- input.onViewChange();
1138
- }
1139
- function openExecutorMenu(focusSelected = false) {
1140
- if (executorTrigger.disabled || !executorMenu.hidden) return;
1141
- executorMenu.hidden = false;
1142
- executorTrigger.setAttribute("aria-expanded", "true");
1143
- if (focusSelected) {
1144
- const selected = executorMenu.querySelector(
1145
- '.spotpatch-ask-executor-option[aria-selected="true"]'
1146
- );
1147
- const first = executorMenu.firstElementChild;
1148
- const focusTarget = selected ?? (first instanceof HTMLElement ? first : null);
1149
- focusTarget?.focus({ preventScroll: true });
1150
- }
1151
- input.onViewChange();
1152
- }
1153
- function syncExecutorControl() {
1154
- const selectedOption = [...executorSelect.options].find(
1155
- (option) => option.value === executorSelect.value
1284
+ function renderModels() {
1285
+ const executor = currentCapability?.executors.find(
1286
+ (item) => item.executorId === executorSelect.value
1156
1287
  );
1157
- executorText.textContent = selectedOption?.textContent ?? messages.noExecutor;
1158
- executorTrigger.dataset.empty = String(executorSelect.value.length === 0);
1159
- for (const option of executorMenu.querySelectorAll(
1160
- ".spotpatch-ask-executor-option"
1161
- )) {
1162
- option.setAttribute(
1163
- "aria-selected",
1164
- String(option.dataset.executorId === executorSelect.value)
1165
- );
1288
+ const previous = modelExecutorId === executor?.executorId ? modelPicker.select.value : void 0;
1289
+ modelExecutorId = executor?.executorId;
1290
+ const models = executor?.models;
1291
+ modelField.hidden = models === void 0;
1292
+ modelPicker.select.replaceChildren();
1293
+ for (const model of models ?? []) {
1294
+ const option = document.createElement("option");
1295
+ option.value = model;
1296
+ option.textContent = model;
1297
+ modelPicker.select.append(option);
1166
1298
  }
1299
+ modelPicker.select.value = models?.find((model) => model === previous) ?? models?.find((model) => model === executor?.requestedModelLabel) ?? models?.[0] ?? "";
1300
+ modelPicker.rebuild();
1167
1301
  }
1168
1302
  function refreshSubmitState() {
1169
1303
  submitButton.disabled = busy || questionInput.value.trim().length === 0 || !hasReadyExecutor() || !consentCheckbox.checked || currentPreview.targetCount === 0;
1170
1304
  if (!currentPreview.contextReady) submitButton.disabled = true;
1171
1305
  questionInput.disabled = busy;
1172
- const readyCount = readyExecutorCount();
1173
1306
  const executorUnavailable = busy || currentCapability === void 0 || !hasAnyReadyExecutor();
1174
- executorSelect.disabled = executorUnavailable;
1175
- executorTrigger.disabled = executorUnavailable || readyCount < 2;
1176
- executorTrigger.dataset.expandable = String(!executorUnavailable && readyCount > 1);
1177
- if (executorTrigger.disabled) closeExecutorMenu();
1307
+ executorPicker.setDisabled(executorUnavailable);
1308
+ modelPicker.setDisabled(busy || !hasReadyExecutor());
1178
1309
  consentCheckbox.disabled = busy || !hasReadyExecutor();
1179
1310
  newQuestionButton.hidden = busy || currentResult === void 0;
1180
1311
  cancelButton.hidden = !busy;
@@ -1217,6 +1348,9 @@ function createContextualAskPanel(input) {
1217
1348
  questionInput.placeholder = messages.questionPlaceholder;
1218
1349
  suggestions.setAttribute("aria-label", messages.suggestionsLabel);
1219
1350
  executorLabel.textContent = messages.executorLabel;
1351
+ modelLabel.textContent = messages.modelLabel;
1352
+ executorPicker.root.querySelector("[role=listbox]")?.setAttribute("aria-label", messages.executorLabel);
1353
+ modelPicker.root.querySelector("[role=listbox]")?.setAttribute("aria-label", messages.modelLabel);
1220
1354
  consentText.textContent = messages.consent;
1221
1355
  safetyText.textContent = messages.safety;
1222
1356
  submitButton.textContent = messages.submit;
@@ -1241,8 +1375,7 @@ function createContextualAskPanel(input) {
1241
1375
  currentCapability = capability;
1242
1376
  const previous = executorSelect.value;
1243
1377
  executorSelect.replaceChildren();
1244
- executorMenu.replaceChildren();
1245
- closeExecutorMenu();
1378
+ executorPicker.close();
1246
1379
  executorStatus.hidden = true;
1247
1380
  executorStatus.textContent = "";
1248
1381
  if (capability === void 0) {
@@ -1265,22 +1398,8 @@ function createContextualAskPanel(input) {
1265
1398
  for (const executor of readyExecutors) {
1266
1399
  const option = document.createElement("option");
1267
1400
  option.value = executor.executorId;
1268
- option.textContent = `${executor.label} \xB7 ${executor.effectiveModelLabel}`;
1401
+ option.textContent = executor.models === void 0 ? `${executor.label} \xB7 ${executor.effectiveModelLabel}` : executor.label;
1269
1402
  executorSelect.append(option);
1270
- const menuOption = createButton(
1271
- document,
1272
- option.textContent,
1273
- "spotpatch-ask-executor-option"
1274
- );
1275
- menuOption.dataset.executorId = executor.executorId;
1276
- menuOption.setAttribute("role", "option");
1277
- menuOption.addEventListener("click", () => {
1278
- executorSelect.value = executor.executorId;
1279
- syncExecutorControl();
1280
- closeExecutorMenu(true);
1281
- executorSelect.dispatchEvent(new Event("change", { bubbles: true }));
1282
- });
1283
- executorMenu.append(menuOption);
1284
1403
  }
1285
1404
  if (readyExecutors.length === 0) {
1286
1405
  const option = document.createElement("option");
@@ -1296,7 +1415,8 @@ function createContextualAskPanel(input) {
1296
1415
  executorStatus.hidden = false;
1297
1416
  }
1298
1417
  }
1299
- syncExecutorControl();
1418
+ executorPicker.rebuild();
1419
+ renderModels();
1300
1420
  refreshSubmitState();
1301
1421
  }
1302
1422
  function renderJob(snapshot) {
@@ -1451,42 +1571,11 @@ ${sourceText}`;
1451
1571
  applyMode();
1452
1572
  });
1453
1573
  questionInput.addEventListener("input", refreshSubmitState);
1454
- executorSelect.addEventListener("change", refreshSubmitState);
1455
- executorTrigger.addEventListener("click", () => {
1456
- if (executorMenu.hidden) openExecutorMenu();
1457
- else closeExecutorMenu();
1458
- });
1459
- executorTrigger.addEventListener("keydown", (event) => {
1460
- if (event.key !== "ArrowDown" && event.key !== "ArrowUp") return;
1461
- event.preventDefault();
1462
- openExecutorMenu(true);
1463
- });
1464
- executorMenu.addEventListener("keydown", (event) => {
1465
- const options = [
1466
- ...executorMenu.querySelectorAll(
1467
- ".spotpatch-ask-executor-option"
1468
- )
1469
- ];
1470
- if (event.key === "Escape") {
1471
- event.preventDefault();
1472
- closeExecutorMenu(true);
1473
- return;
1474
- }
1475
- if (!["ArrowDown", "ArrowUp", "Home", "End"].includes(event.key)) return;
1476
- event.preventDefault();
1477
- const currentIndex = options.indexOf(document.activeElement);
1478
- 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;
1479
- options[nextIndex]?.focus({ preventScroll: true });
1480
- });
1481
- executorPicker.addEventListener("focusout", (event) => {
1482
- if (!(event.relatedTarget instanceof Node) || !executorPicker.contains(event.relatedTarget)) {
1483
- closeExecutorMenu();
1484
- }
1574
+ executorSelect.addEventListener("change", () => {
1575
+ renderModels();
1576
+ refreshSubmitState();
1577
+ input.onViewChange();
1485
1578
  });
1486
- const closeExecutorMenuOutside = (event) => {
1487
- if (!event.composedPath().includes(executorPicker)) closeExecutorMenu();
1488
- };
1489
- document.addEventListener("pointerdown", closeExecutorMenuOutside);
1490
1579
  consentCheckbox.addEventListener("change", refreshSubmitState);
1491
1580
  const unsubscribeLocale = input.subscribeLocale(applyMessages);
1492
1581
  applyMessages();
@@ -1505,7 +1594,8 @@ ${sourceText}`;
1505
1594
  clear,
1506
1595
  dispose: () => {
1507
1596
  unsubscribeLocale();
1508
- document.removeEventListener("pointerdown", closeExecutorMenuOutside);
1597
+ executorPicker.dispose();
1598
+ modelPicker.dispose();
1509
1599
  },
1510
1600
  focusQuestion: () => {
1511
1601
  questionInput.focus({ preventScroll: true });
@@ -1517,6 +1607,7 @@ ${sourceText}`;
1517
1607
  },
1518
1608
  readConsent: () => consentCheckbox.checked,
1519
1609
  readExecutorId: () => executorSelect.value || void 0,
1610
+ readModel: () => modelPicker.select.value || void 0,
1520
1611
  readQuestion: () => questionInput.value.trim(),
1521
1612
  renderAnswer,
1522
1613
  renderCapability,