@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.
- package/README.md +14 -5
- package/lib/client.js +143 -16
- 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).
|
|
34
|
-
|
|
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
|
|
71
|
-
(`dsh-plugin-model-picker:recent:v1`
|
|
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 =
|
|
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 =
|
|
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("
|
|
840
|
+
document.addEventListener("pointerdown", onPointer);
|
|
734
841
|
return () => {
|
|
735
|
-
document.removeEventListener("
|
|
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
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
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
|
|
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
|
|
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