@1outres/dsh-plugin-model-picker 0.5.0 → 0.7.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.
Files changed (3) hide show
  1. package/README.md +14 -5
  2. package/lib/client.js +143 -16
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -18,7 +18,13 @@ insert the same `model-picker` row.
18
18
  ## Using the picker
19
19
 
20
20
  Click the model name in the composer. The menu opens above it with the
21
- search field focused.
21
+ search field focused. The menu never grows past the room above the button in
22
+ the visible part of the page, so the Effort row stays on screen.
23
+
24
+ On a touch screen the search field is not focused on open, since the
25
+ on-screen keyboard would cover the list and the Effort row; tap the field to
26
+ search. Dismissing the keyboard keeps the menu open, and a tap outside closes
27
+ it.
22
28
 
23
29
  - **Empty search**: ★ Favorites, then the five most recent models you picked
24
30
  here (favorites excluded), then every provider with its heading pinned
@@ -30,8 +36,10 @@ search field focused.
30
36
  matches come first, then prefixes, word starts, and other substrings; ties
31
37
  go to favorites, then recent models, then catalog order.
32
38
  - **Effort**: the row at the bottom lists the current model's reasoning
33
- levels (plus Default when the model has no default level). Switching to
34
- another model keeps the current level when that model offers it.
39
+ levels (plus Default when the model has no default level). Each model
40
+ remembers the level you last used with it, so switching back restores it.
41
+ A model without a remembered level keeps the current one when it offers
42
+ it, and otherwise falls back to its own default.
35
43
 
36
44
  Each row shows a check for the current model, the model name, and the model
37
45
  id when it differs from the name.
@@ -67,8 +75,9 @@ profile patch:
67
75
  model: claude-opus-5-5
68
76
  ```
69
77
 
70
- Recent models are kept in this browser's local storage
71
- (`dsh-plugin-model-picker:recent:v1`), so every browser has its own list.
78
+ Recent models and the effort remembered for each model are kept in this
79
+ browser's local storage (`dsh-plugin-model-picker:recent:v1` and
80
+ `dsh-plugin-model-picker:efforts:v1`), so every browser has its own lists.
72
81
  Only selections made in this picker count.
73
82
 
74
83
  A favorite or recent model whose provider is gone disappears from the menu but
package/lib/client.js CHANGED
@@ -64,6 +64,76 @@ window.__ModuleLoader__.load({
64
64
  );
65
65
  }
66
66
 
67
+ // ../dsh-plugin-model-picker/src/client/efforts.ts
68
+ var EFFORTS_STORAGE_KEY = "dsh-plugin-model-picker:efforts:v1";
69
+ var EFFORTS_STORED = 200;
70
+ function normalizeEfforts(value) {
71
+ if (!Array.isArray(value)) return [];
72
+ const seen = /* @__PURE__ */ new Set();
73
+ const efforts = [];
74
+ for (const item of value) {
75
+ if (typeof item !== "object" || item === null) continue;
76
+ const { provider, model, effort } = item;
77
+ if (typeof provider !== "string" || typeof model !== "string") continue;
78
+ if (typeof effort !== "string") continue;
79
+ if (provider.length === 0 || model.length === 0 || effort.length === 0) continue;
80
+ const key = refKey(provider, model);
81
+ if (seen.has(key)) continue;
82
+ seen.add(key);
83
+ efforts.push({ provider, model, effort });
84
+ }
85
+ return efforts;
86
+ }
87
+ function sameEfforts(left, right) {
88
+ if (left.length !== right.length) return false;
89
+ return left.every(
90
+ (item, index) => item.provider === right[index]?.provider && item.model === right[index]?.model && item.effort === right[index]?.effort
91
+ );
92
+ }
93
+ function createEffortStore(storage, events) {
94
+ const read = () => {
95
+ if (storage === void 0) return [];
96
+ try {
97
+ const raw = storage.getItem(EFFORTS_STORAGE_KEY);
98
+ return raw === null ? [] : normalizeEfforts(JSON.parse(raw)).slice(0, EFFORTS_STORED);
99
+ } catch {
100
+ return [];
101
+ }
102
+ };
103
+ let snapshot = read();
104
+ const replace = (next) => {
105
+ if (sameEfforts(next, snapshot)) return;
106
+ snapshot = next;
107
+ };
108
+ return {
109
+ connect() {
110
+ if (events === void 0) return () => {
111
+ };
112
+ const onStorage = (event) => {
113
+ if (event.key === null || event.key === EFFORTS_STORAGE_KEY) replace(read());
114
+ };
115
+ events.addEventListener("storage", onStorage);
116
+ return () => {
117
+ events.removeEventListener("storage", onStorage);
118
+ };
119
+ },
120
+ get(ref) {
121
+ const key = refKey(ref.provider, ref.model);
122
+ return snapshot.find((item) => refKey(item.provider, item.model) === key)?.effort;
123
+ },
124
+ remember(ref, effort) {
125
+ const key = refKey(ref.provider, ref.model);
126
+ const kept = snapshot.filter((item) => refKey(item.provider, item.model) !== key);
127
+ const next = effort === void 0 ? kept : [{ provider: ref.provider, model: ref.model, effort }, ...kept].slice(0, EFFORTS_STORED);
128
+ try {
129
+ storage?.setItem(EFFORTS_STORAGE_KEY, JSON.stringify(next));
130
+ } catch {
131
+ }
132
+ replace(next);
133
+ }
134
+ };
135
+ }
136
+
67
137
  // ../dsh-plugin-model-picker/src/client/model.ts
68
138
  var ACCOUNT_PROVIDER = "deepseek-account";
69
139
  var OFFICIAL_PROVIDER = "deepseek-official";
@@ -199,6 +269,9 @@ window.__ModuleLoader__.load({
199
269
  if (effort === void 0 || target === void 0) return void 0;
200
270
  return target.efforts.some((level) => level.id === effort) ? effort : void 0;
201
271
  }
272
+ function preferredEffort(remembered, current, target) {
273
+ return carriedEffort(remembered, target) ?? carriedEffort(current, target);
274
+ }
202
275
  function effortChoices(reasoning, defaultLabel) {
203
276
  if (reasoning === void 0) return [];
204
277
  return [
@@ -487,11 +560,41 @@ window.__ModuleLoader__.load({
487
560
  );
488
561
  }
489
562
 
563
+ // ../dsh-plugin-model-picker/src/client/placement.ts
564
+ var DEFAULT_PLACEMENT = { margin: 12, gap: 8, minHeight: 160 };
565
+ function clamp(value, low, high) {
566
+ return Math.min(Math.max(value, low), high);
567
+ }
568
+ function placeMenu(trigger, size, view, options = DEFAULT_PLACEMENT) {
569
+ const { margin, gap, minHeight } = options;
570
+ const room = Math.floor(trigger.top - gap - (view.top + margin));
571
+ const maxHeight = room >= minHeight ? room : Math.max(minHeight, view.height - 2 * margin);
572
+ const height = Math.min(size.height, maxHeight);
573
+ let left = trigger.right - size.width;
574
+ let top = trigger.top - gap - height;
575
+ if (size.width > 0)
576
+ left = clamp(left, view.left + margin, view.left + view.width - size.width - margin);
577
+ if (height > 0) top = clamp(top, view.top + margin, view.top + view.height - height - margin);
578
+ return { left, top, maxHeight };
579
+ }
580
+ function viewBoxOf(win) {
581
+ const view = win.visualViewport;
582
+ if (view === void 0 || view === null) {
583
+ return { left: 0, top: 0, width: win.innerWidth, height: win.innerHeight };
584
+ }
585
+ return { left: view.offsetLeft, top: view.offsetTop, width: view.width, height: view.height };
586
+ }
587
+ function isCoarsePointer(win) {
588
+ try {
589
+ return win?.matchMedia?.("(hover: none) and (pointer: coarse)").matches === true;
590
+ } catch {
591
+ return false;
592
+ }
593
+ }
594
+
490
595
  // ../dsh-plugin-model-picker/src/client/Picker.tsx
491
596
  var import_jsx_runtime2 = require("react/jsx-runtime");
492
597
  var MEASURE_STYLE = { visibility: "hidden", left: 0, top: 0 };
493
- var MARGIN = 12;
494
- var GAP = 8;
495
598
  var ROW_HEIGHT = 34;
496
599
  var TOAST_HOLD_MS = 3e3;
497
600
  var TOAST_FADE_MS = 1e3;
@@ -548,7 +651,7 @@ window.__ModuleLoader__.load({
548
651
  );
549
652
  }
550
653
  function ModelPicker(props) {
551
- const { locked, available, directory, load, select, favorites, recent, t } = props;
654
+ const { locked, available, directory, load, select, favorites, recent, efforts, t } = props;
552
655
  const state = useObservable(directory);
553
656
  const favoriteRefs = useObservable(favorites);
554
657
  const recentRefs = useObservable(recent);
@@ -644,7 +747,7 @@ window.__ModuleLoader__.load({
644
747
  setQuery("");
645
748
  setHighlightId(null);
646
749
  setPreferFirst(false);
647
- focusSearchOnPlace.current = true;
750
+ focusSearchOnPlace.current = !isCoarsePointer(window);
648
751
  setOpen(true);
649
752
  reload();
650
753
  };
@@ -665,6 +768,10 @@ window.__ModuleLoader__.load({
665
768
  (result) => {
666
769
  if (result === void 0) return;
667
770
  if (result.ok) {
771
+ efforts.remember(
772
+ { provider: selection.provider, model: selection.model },
773
+ selection.reasoningEffort
774
+ );
668
775
  if (record !== void 0) recent.push(record);
669
776
  if (rootRef.current !== null) closeAfterSelection();
670
777
  return;
@@ -684,7 +791,7 @@ window.__ModuleLoader__.load({
684
791
  closeAfterSelection();
685
792
  return;
686
793
  }
687
- const effort = carriedEffort(effective, entry.model.reasoning);
794
+ const effort = preferredEffort(efforts.get(refOf(entry)), effective, entry.model.reasoning);
688
795
  submit(
689
796
  {
690
797
  provider: entry.provider.id,
@@ -730,14 +837,22 @@ window.__ModuleLoader__.load({
730
837
  if (menuRef.current?.contains(target) === true) return;
731
838
  setOpen(false);
732
839
  };
733
- document.addEventListener("mousedown", onPointer);
840
+ document.addEventListener("pointerdown", onPointer);
734
841
  return () => {
735
- document.removeEventListener("mousedown", onPointer);
842
+ document.removeEventListener("pointerdown", onPointer);
736
843
  };
737
844
  }, [open]);
738
845
  (0, import_react.useEffect)(() => {
739
846
  if (locked && open) setOpen(false);
740
847
  }, [locked, open]);
848
+ const selectedProvider = current?.provider;
849
+ const selectedModel = current?.model;
850
+ const selectedEffort = current?.reasoningEffort;
851
+ (0, import_react.useEffect)(() => {
852
+ if (selectedProvider === void 0 || selectedModel === void 0) return;
853
+ if (selectedEffort === void 0) return;
854
+ efforts.remember({ provider: selectedProvider, model: selectedModel }, selectedEffort);
855
+ }, [selectedProvider, selectedModel, selectedEffort, efforts]);
741
856
  const placed = menuPos !== null;
742
857
  (0, import_react.useLayoutEffect)(() => {
743
858
  if (!open || !placed || !focusSearchOnPlace.current) return;
@@ -761,19 +876,21 @@ window.__ModuleLoader__.load({
761
876
  const place = () => {
762
877
  const rect = triggerRef.current?.getBoundingClientRect();
763
878
  if (rect === void 0) return;
764
- const width = menuRef.current?.offsetWidth ?? 0;
765
- const height = menuRef.current?.offsetHeight ?? 0;
766
- let left = rect.right - width;
767
- let top = rect.top - GAP - height;
768
- if (width > 0) left = Math.min(Math.max(left, MARGIN), window.innerWidth - width - MARGIN);
769
- if (height > 0) top = Math.min(Math.max(top, MARGIN), window.innerHeight - height - MARGIN);
879
+ const next = placeMenu(
880
+ rect,
881
+ { width: menuRef.current?.offsetWidth ?? 0, height: menuRef.current?.offsetHeight ?? 0 },
882
+ viewBoxOf(window)
883
+ );
770
884
  setMenuPos(
771
- (previous) => previous !== null && previous.left === left && previous.top === top ? previous : { left, top }
885
+ (previous) => previous !== null && previous.left === next.left && previous.top === next.top && previous.maxHeight === next.maxHeight ? previous : next
772
886
  );
773
887
  };
774
888
  place();
775
889
  window.addEventListener("scroll", place, true);
776
890
  window.addEventListener("resize", place);
891
+ const view = window.visualViewport;
892
+ view?.addEventListener("resize", place);
893
+ view?.addEventListener("scroll", place);
777
894
  const menu = menuRef.current;
778
895
  const observer = typeof ResizeObserver !== "undefined" && menu !== null ? new ResizeObserver(place) : null;
779
896
  if (menu !== null) observer?.observe(menu);
@@ -781,6 +898,8 @@ window.__ModuleLoader__.load({
781
898
  observer?.disconnect();
782
899
  window.removeEventListener("scroll", place, true);
783
900
  window.removeEventListener("resize", place);
901
+ view?.removeEventListener("resize", place);
902
+ view?.removeEventListener("scroll", place);
784
903
  };
785
904
  }, [available, open, state, sections, choices]);
786
905
  (0, import_react.useLayoutEffect)(() => {
@@ -909,6 +1028,7 @@ window.__ModuleLoader__.load({
909
1028
  if (next instanceof Node && (rootRef.current?.contains(next) === true || menuRef.current?.contains(next) === true)) {
910
1029
  return;
911
1030
  }
1031
+ if (next === null && isCoarsePointer(window)) return;
912
1032
  close();
913
1033
  };
914
1034
  const keepFocus = (event) => {
@@ -976,7 +1096,11 @@ window.__ModuleLoader__.load({
976
1096
  id: menuId,
977
1097
  className: "dsh-mp-menu",
978
1098
  "data-menu-material": "translucent",
979
- style: menuPos ?? MEASURE_STYLE,
1099
+ style: menuPos === null ? MEASURE_STYLE : {
1100
+ left: menuPos.left,
1101
+ top: menuPos.top,
1102
+ maxHeight: `min(560px, 100vh - 96px, ${menuPos.maxHeight}px)`
1103
+ },
980
1104
  role: "group",
981
1105
  "aria-label": t("menu.aria"),
982
1106
  "aria-busy": loading || busy,
@@ -1341,8 +1465,10 @@ window.__ModuleLoader__.load({
1341
1465
  }, "model-picker: stylesheet");
1342
1466
  const favorites = createFavoritesStore(ctx.configForms.get(CONFIG_NAMESPACE));
1343
1467
  const recent = createRecentStore(browserStorage(), storageEvents());
1468
+ const efforts = createEffortStore(browserStorage(), storageEvents());
1344
1469
  ctx.effect(() => favorites.connect(), "model-picker: favorites");
1345
1470
  ctx.effect(() => recent.connect(), "model-picker: recent models");
1471
+ ctx.effect(() => efforts.connect(), "model-picker: per-model efforts");
1346
1472
  ctx.effect(
1347
1473
  () => ctx.slots.inject(
1348
1474
  SEAT,
@@ -1363,7 +1489,8 @@ window.__ModuleLoader__.load({
1363
1489
  },
1364
1490
  select: (selection) => available ? directory.select(selection) : Promise.resolve(void 0),
1365
1491
  favorites,
1366
- recent
1492
+ recent,
1493
+ efforts
1367
1494
  };
1368
1495
  }
1369
1496
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@1outres/dsh-plugin-model-picker",
3
- "version": "0.5.0",
3
+ "version": "0.7.0",
4
4
  "description": "A searchable composer model picker with favorites and recent models for DeepSeek Harness profiles with many providers.",
5
5
  "repository": {
6
6
  "type": "git",