dsh-effort-menu 1.0.1 → 1.1.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 (2) hide show
  1. package/client.js +124 -5
  2. package/package.json +2 -1
package/client.js CHANGED
@@ -22,6 +22,7 @@ window.__ModuleLoader__.load({
22
22
  'menu.recent': '最近使用',
23
23
  'menu.allModels': '全部模型',
24
24
  'menu.effort': '思考强度',
25
+ 'menu.manageModels': '管理模型',
25
26
  'provider.account': 'DeepSeek 账号',
26
27
  'trigger.fallback': '请选择模型',
27
28
  'trigger.selectAria': '请选择模型',
@@ -44,6 +45,7 @@ window.__ModuleLoader__.load({
44
45
  'menu.recent': 'Recently used',
45
46
  'menu.allModels': 'All models',
46
47
  'menu.effort': 'Thinking intensity',
48
+ 'menu.manageModels': 'Manage models',
47
49
  'provider.account': 'DeepSeek Account',
48
50
  'trigger.fallback': 'Select model',
49
51
  'trigger.selectAria': 'Select model',
@@ -87,7 +89,7 @@ window.__ModuleLoader__.load({
87
89
  .es-providerChevron{width:14px;height:14px;flex:none;color:var(--dsw-alias-label-caption,var(--dsw-alias-label-secondary));transition:transform .18s ease}
88
90
  .es-flyout{box-sizing:border-box;position:fixed;z-index:1101;width:280px;max-width:calc(100vw - 24px);max-height:min(480px,calc(100vh - 24px));display:flex;flex-direction:column;padding:6px;border-radius:12px;border:1px solid var(--dsw-alias-border-l1);background:var(--dsw-specific-input-major,var(--dsw-alias-bg-overlay));box-shadow:var(--dsw-elevation-prominent,0 18px 48px rgba(0,0,0,.45));color:var(--dsw-alias-label-primary);font-size:var(--dsh-content-font-size,13px);line-height:calc(20px + var(--dsh-content-font-delta,0px));overflow:hidden}
89
91
  .es-flyout:focus{outline:none}
90
- .es-flyoutList{display:flex;flex-direction:column;min-height:0;overflow-x:hidden;overflow-y:auto;overscroll-behavior:contain}
92
+ .es-flyoutList{display:flex;flex-direction:column;min-height:0;flex:1;overflow-x:hidden;overflow-y:auto;overscroll-behavior:contain}
91
93
  .es-sectionLabel{flex:none;padding:7px 8px 3px;color:var(--dsw-alias-label-tertiary,var(--dsw-alias-label-secondary));font-size:calc(11px + var(--dsh-content-font-delta-secondary,0px));line-height:16px;font-weight:500}
92
94
  .es-modelGroup{display:flex;flex-direction:column}
93
95
  .es-groupHeading{flex:none;padding:6px 8px 2px;color:var(--dsw-alias-label-tertiary,var(--dsw-alias-label-secondary));font-size:calc(11px + var(--dsh-content-font-delta-secondary,0px));line-height:16px;font-weight:500}
@@ -102,6 +104,13 @@ window.__ModuleLoader__.load({
102
104
  .es-check{flex:0 0 14px;display:grid;place-items:center;color:var(--dsw-alias-label-primary)}
103
105
  .es-effortDivider{border-top:1px solid var(--dsw-alias-border-l1);margin:8px 0 0;flex:none}
104
106
  .es-effortBlock{padding:8px 2px 2px;border-radius:10px;flex:none}
107
+ .es-footerDivider{border-top:1px solid var(--dsw-alias-border-l1);margin:8px 0 0;flex:none}
108
+ .es-footer{padding:4px 0 0;flex:none}
109
+ .es-manageRow{box-sizing:border-box;width:100%;height:calc(36px + var(--dsh-content-font-delta,0px));display:flex;align-items:center;gap:8px;padding:0 8px;margin:0;border:0;border-radius:8px;background:0 0;color:inherit;font:inherit;text-align:left;cursor:pointer;outline:none;transition:background-color .16s ease}
110
+ @media (hover:hover){.es-manageRow:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}}
111
+ .es-manageRow:focus-visible{background:var(--dsw-alias-interactive-bg-hover);box-shadow:inset 0 0 0 1px var(--dsw-alias-border-l2)}
112
+ .es-manageRow:disabled{color:var(--dsw-alias-label-dimmed);cursor:default}
113
+ .es-manageLabel{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-size:var(--dsh-content-font-size,13px);line-height:calc(20px + var(--dsh-content-font-delta,0px))}
105
114
  .es-effortHeader{display:flex;align-items:center;gap:8px;padding:2px 6px 0}
106
115
  .es-effortLabel{color:var(--dsw-alias-label-primary);flex:none;font-size:var(--dsh-content-font-size,13px);line-height:calc(20px + var(--dsh-content-font-delta,0px))}
107
116
  .es-effortValue{color:var(--dsw-alias-label-primary);font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-size:var(--dsh-content-font-size,13px);line-height:calc(20px + var(--dsh-content-font-delta,0px))}
@@ -212,6 +221,98 @@ window.__ModuleLoader__.load({
212
221
  );
213
222
  }
214
223
 
224
+ /**
225
+ * Click the Models row in an open Settings dialog. The row index comes from the
226
+ * live `settings.section` ledger (the shell sorts its nav by `order`), with a
227
+ * localized label fallback for shells whose registry is not exposed here.
228
+ */
229
+ function selectModelsSection(scope, panel) {
230
+ let index = -1;
231
+ try {
232
+ const sections = scope.slots
233
+ .entries('settings.section')
234
+ .map((entry) => ({
235
+ id: entry.options === undefined || entry.options.id === undefined ? '' : entry.options.id,
236
+ order: entry.options === undefined || entry.options.order === undefined ? 0 : entry.options.order,
237
+ }))
238
+ .sort((left, right) => left.order - right.order);
239
+ index = sections.findIndex((section) => section.id === 'models');
240
+ } catch (_error) {
241
+ index = -1;
242
+ }
243
+ const nav = panel.querySelector('nav');
244
+ const buttons = nav === null ? [] : Array.from(nav.querySelectorAll('button'));
245
+ let target = index >= 0 && index < buttons.length ? buttons[index] : null;
246
+ if (target === null) {
247
+ const label = /^(模型|Models?|モデル|모델|Модели|Modèles|Modelle|Modelos|Modelli)$/i;
248
+ target = buttons.find((button) => label.test((button.textContent || '').trim())) || null;
249
+ }
250
+ if (target !== null) target.click();
251
+ }
252
+
253
+ /**
254
+ * Open Settings on its Models section. The settings shell keeps its open
255
+ * state in the store registered on `sidebar.settings`, so the exact live
256
+ * instance is reachable through the slot ledger. If that route is absent,
257
+ * invoke the shipped `settings.open` command and click the Models row in the
258
+ * rendered dialog instead.
259
+ */
260
+ function openModelsSettings(scope) {
261
+ const existing = document.querySelector('[data-shortcut-modal="settings"]');
262
+ if (existing !== null) {
263
+ selectModelsSection(scope, existing);
264
+ return;
265
+ }
266
+ let opened = false;
267
+ try {
268
+ const entries =
269
+ typeof scope.slots.entriesOfSlot === 'function'
270
+ ? scope.slots.entriesOfSlot('sidebar.settings')
271
+ : scope.slots.entries('sidebar.settings');
272
+ const entry = entries.find(
273
+ (candidate) =>
274
+ candidate !== undefined &&
275
+ candidate !== null &&
276
+ candidate.store !== undefined &&
277
+ typeof candidate.store.create === 'function',
278
+ );
279
+ const instance = entry === undefined ? undefined : entry.store.create();
280
+ if (
281
+ instance !== undefined &&
282
+ instance !== null &&
283
+ instance.actions !== undefined &&
284
+ typeof instance.actions.openSection === 'function'
285
+ ) {
286
+ instance.actions.openSection('models');
287
+ opened = true;
288
+ }
289
+ } catch (_error) {
290
+ opened = false;
291
+ }
292
+ if (!opened) {
293
+ const shortcuts = scope.shortcuts;
294
+ const registry = shortcuts === undefined || shortcuts === null ? undefined : shortcuts.registry;
295
+ if (registry !== undefined && registry !== null && typeof registry.invoke === 'function') {
296
+ registry.invoke('settings.open', { region: 'page', modal: null, target: null });
297
+ }
298
+ }
299
+ // The dialog is portaled on the next React commit; retry briefly when the
300
+ // shell store route missed and the command opened the dialog asynchronously.
301
+ let attempts = 0;
302
+ const settle = () => {
303
+ const panel = document.querySelector('[data-shortcut-modal="settings"]');
304
+ if (panel === null) {
305
+ if (attempts < 40) {
306
+ attempts += 1;
307
+ window.setTimeout(settle, 50);
308
+ }
309
+ return;
310
+ }
311
+ selectModelsSection(scope, panel);
312
+ };
313
+ window.setTimeout(settle, 30);
314
+ }
315
+
215
316
  const RECENT_KEY = 'dsh-effort-menu/recent-models';
216
317
  /** The menu lists at most this many recent models, the current one included. */
217
318
  const RECENT_VISIBLE = 3;
@@ -576,7 +677,7 @@ window.__ModuleLoader__.load({
576
677
  * @param props - owner share (`locked`), the injected directory face, and locale `t`.
577
678
  * @returns the trigger button plus, while open, a portaled menu.
578
679
  */
579
- function EffortSeat({ locked, available, directory, load, select, resolveText, t }) {
680
+ function EffortSeat({ locked, available, directory, load, select, resolveText, t, manageModels }) {
580
681
  const state = React.useSyncExternalStore(
581
682
  (callback) => directory.subscribe(callback),
582
683
  () => directory.getSnapshot(),
@@ -1159,7 +1260,7 @@ window.__ModuleLoader__.load({
1159
1260
  return;
1160
1261
  }
1161
1262
  if (event.key !== 'ArrowDown' && event.key !== 'ArrowUp') return;
1162
- const rows = Array.from(event.currentTarget.querySelectorAll('.es-modelRow')).filter((row) => !row.disabled);
1263
+ const rows = Array.from(event.currentTarget.querySelectorAll('.es-modelRow, .es-manageRow')).filter((row) => !row.disabled);
1163
1264
  if (rows.length === 0) return;
1164
1265
  event.preventDefault();
1165
1266
  const at = rows.indexOf(document.activeElement);
@@ -1169,6 +1270,23 @@ window.__ModuleLoader__.load({
1169
1270
  },
1170
1271
  },
1171
1272
  h('div', { className: 'es-flyoutList' }, allGroups),
1273
+ h('div', { className: 'es-footerDivider' }),
1274
+ h(
1275
+ 'div',
1276
+ { className: 'es-footer' },
1277
+ h(
1278
+ 'button',
1279
+ {
1280
+ type: 'button',
1281
+ className: 'es-manageRow',
1282
+ onClick: () => {
1283
+ close(false);
1284
+ if (typeof manageModels === 'function') manageModels();
1285
+ },
1286
+ },
1287
+ h('span', { className: 'es-manageLabel' }, t('menu.manageModels')),
1288
+ ),
1289
+ ),
1172
1290
  );
1173
1291
 
1174
1292
  return h(
@@ -1203,14 +1321,14 @@ window.__ModuleLoader__.load({
1203
1321
  return currentModel === null && state.current !== null && state.current !== undefined;
1204
1322
  }
1205
1323
 
1206
- const inject = ['locale', 'slots', 'modelDirectories', 'sessions'];
1324
+ const inject = ['locale', 'slots', 'modelDirectories', 'sessions', 'shortcuts'];
1207
1325
 
1208
1326
  /**
1209
1327
  * Mount the replacement seat over the shipped per-session model directory.
1210
1328
  * @param ctx - client root context.
1211
1329
  */
1212
1330
  function apply(ctx) {
1213
- ctx.inject(['locale', 'slots', 'modelDirectories', 'sessions'], (scope) => {
1331
+ ctx.inject(['locale', 'slots', 'modelDirectories', 'sessions', 'shortcuts'], (scope) => {
1214
1332
  scope.effect(() => scope.locale.register(NS, { zh, en }), 'effort-menu: dictionaries');
1215
1333
  const models = scope.modelDirectories;
1216
1334
  const sessions = scope.sessions;
@@ -1231,6 +1349,7 @@ window.__ModuleLoader__.load({
1231
1349
  },
1232
1350
  resolveText: (text) => scope.locale.resolveText(text),
1233
1351
  select: (selection) => (available ? directory.select(selection) : Promise.resolve(undefined)),
1352
+ manageModels: () => openModelsSettings(scope),
1234
1353
  };
1235
1354
  },
1236
1355
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-effort-menu",
3
- "version": "1.0.1",
3
+ "version": "1.1.0",
4
4
  "description": "Replaces the composer's model menu with a recent-models list, an all-providers flyout, and a draggable reasoning-effort slider.",
5
5
  "keywords": [
6
6
  "dsh-plugin",
@@ -56,6 +56,7 @@
56
56
  "immediately": true,
57
57
  "inject": [
58
58
  "@deepseek-ai/dsh-client-locale",
59
+ "@deepseek-ai/dsh-client-shortcuts",
59
60
  "@deepseek-ai/dsh-client-ui-conversation",
60
61
  "@deepseek-ai/dsh-client-ui-model-selection"
61
62
  ]