@dungsil/dsh-plugin-ui-model-picker 0.2.1 → 0.2.3

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/dist/client.js +91 -86
  2. package/package.json +4 -5
package/dist/client.js CHANGED
@@ -4,29 +4,79 @@ let react = require("react");
4
4
  let react_dom = require("react-dom");
5
5
  let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
6
6
  let react_jsx_runtime = require("react/jsx-runtime");
7
+ // ../../tools/ui/src/client.ts
8
+ function useLocaleSnapshot(locale) {
9
+ const subscribe = (0, react.useCallback)((listener) => locale.subscribe(listener), [locale]);
10
+ const getSnapshot = (0, react.useCallback)(() => locale.getSnapshot(), [locale]);
11
+ return (0, react.useSyncExternalStore)(subscribe, getSnapshot);
12
+ }
13
+ /** Each effect owns its sheet, even when another plugin installs the same defaults. */
14
+ function installStyles(css) {
15
+ const style = document.createElement("style");
16
+ style.textContent = css;
17
+ document.head.append(style);
18
+ return () => style.remove();
19
+ }
20
+ // ../../tools/ui/src/icons/rotate-ccw.svg
21
+ var rotate_ccw_default = "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\">\n <path d=\"M5 7a9 9 0 1 1-1 9M5 3v4h4\"/>\n</svg>\n";
22
+ // ../../tools/ui/src/icons.ts
23
+ function IconResetOutlineRegular() {
24
+ return (0, react.createElement)("span", {
25
+ style: { display: "inline-flex" },
26
+ "aria-hidden": "true",
27
+ dangerouslySetInnerHTML: { __html: rotate_ccw_default }
28
+ });
29
+ }
30
+ // ../../tools/ui/src/composer.tsx
31
+ const composerStyles = `
32
+ :where(.dsh-plugin-composer-trigger) { display: flex; align-items: center; gap: 4px; min-width: 0; height: 28px; padding: 0 4px 0 8px; border: 0; border-radius: var(--dsw-radius-sm); background: transparent; color: var(--dsw-alias-label-secondary); font: inherit; font-size: 13px; line-height: 20px; cursor: pointer; }
33
+ :where(.dsh-plugin-composer-trigger):disabled { color: var(--dsw-alias-label-dimmed); cursor: default; }
34
+ .dsh-plugin-composer-icon, .dsh-plugin-composer-chevron { display: inline-flex; flex: none; }
35
+ .dsh-plugin-composer-label, .dsh-plugin-composer-detail { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
36
+ .dsh-plugin-composer-chevron, .dsh-plugin-composer-detail { color: var(--dsw-alias-label-caption); }
37
+ .dsh-plugin-composer-trigger[aria-expanded=true] .dsh-plugin-composer-chevron { transform: rotate(180deg); }
38
+ `;
39
+ const ComposerTrigger = /* @__PURE__ */ (0, react.forwardRef)(function ComposerTrigger({ className = "", icon, label, detail, ...props }, ref) {
40
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
41
+ ...props,
42
+ ref,
43
+ type: "button",
44
+ className: `dsh-plugin-composer-trigger ${className}`,
45
+ children: [
46
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
47
+ className: "dsh-plugin-composer-icon",
48
+ "aria-hidden": "true",
49
+ children: icon
50
+ }),
51
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
52
+ className: "dsh-plugin-composer-label",
53
+ children: label
54
+ }),
55
+ detail && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
56
+ className: "dsh-plugin-composer-detail",
57
+ children: detail
58
+ }),
59
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
60
+ className: "dsh-plugin-composer-chevron",
61
+ "aria-hidden": "true",
62
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutlineRegular, {})
63
+ })
64
+ ]
65
+ });
66
+ });
7
67
  // src/styles.ts
8
- const styles = `
68
+ const styles = `${composerStyles}
9
69
  .dsh-codex-picker { display: flex; align-items: center; min-width: 0; }
10
- .dsh-codex-trigger { display: flex; align-items: center; gap: 4px; min-width: 0; max-width: min(360px, 45cqw); height: 28px; padding: 0 4px 0 8px; border: none; border-radius: var(--dsw-radius-sm); background: transparent; color: var(--dsw-alias-label-secondary); font-family: inherit; font-size: 13px; font-weight: 400; line-height: 20px; cursor: pointer; outline: none; }
70
+ .dsh-codex-trigger { max-width: min(360px, 45cqw); font-weight: 400; outline: none; }
11
71
  .dsh-codex-trigger:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover); }
12
72
  .dsh-codex-trigger:focus-visible:not([data-dsh-automatic-focus]) { box-shadow: 0 0 0 2px var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary)); }
13
- .dsh-codex-trigger:disabled { color: var(--dsw-alias-label-dimmed); cursor: default; }
14
- .dsh-codex-trigger-icon { display: var(--dsh-composer-model-icon-display, none); flex: none; }
15
- .dsh-codex-chevron { color: var(--dsw-alias-label-caption); flex: none; transition: transform .12s; }
16
- .dsh-codex-trigger[aria-expanded=true] .dsh-codex-chevron { transform: rotate(180deg); }
17
- .dsh-codex-name, .dsh-codex-effort { display: var(--dsh-composer-model-text-display, block); min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
18
- .dsh-codex-effort { color: var(--dsw-alias-label-caption); flex-shrink: 1000; }
73
+ .dsh-codex-trigger .dsh-plugin-composer-icon { display: var(--dsh-composer-model-icon-display, none); }
74
+ .dsh-codex-trigger .dsh-plugin-composer-chevron { transition: transform .12s; }
75
+ .dsh-codex-trigger .dsh-plugin-composer-label, .dsh-codex-trigger .dsh-plugin-composer-detail { display: var(--dsh-composer-model-text-display, block); }
76
+ .dsh-codex-trigger .dsh-plugin-composer-detail { flex-shrink: 1000; }
19
77
  .dsh-codex-popover { position: fixed; z-index: 1100; display: flex; flex-direction: column; inset: auto; margin: 0; box-sizing: border-box; width: 256px; max-width: calc(100vw - 32px); max-height: min(360px, calc(100dvh - 96px)); overflow: hidden; padding: 4px; border: 0; --dsw-elevation-stroke-color: var(--dsw-alias-border-l1); --dsh-scrollbar-thumb: var(--dsw-alias-scrollbar-bg-l2); --dsh-scrollbar-thumb-hover: var(--dsw-alias-scrollbar-hover-l2); background: transparent; color: var(--dsw-alias-label-primary); box-shadow: var(--dsw-elevation-prominent); font: 13px/20px var(--dsw-font-family, system-ui); }
20
- /* rc.5 has no MenuSurface phase API. Match the newer shared menu fades without
21
- animating the container: container opacity would change its backdrop blur.
22
- Old hosts forward phase to the div; newer hosts consume it for their own CSS. */
23
- .dsh-codex-popover[phase=enter] > * { animation: dsh-codex-menu-in 40ms ease-out; }
24
- .dsh-codex-popover[phase=dismiss] > * { animation: dsh-codex-menu-out 80ms ease-in both; }
25
- @keyframes dsh-codex-menu-in { from { opacity: 0; } to { opacity: 1; } }
26
- @keyframes dsh-codex-menu-out { from { opacity: 1; } to { opacity: 0; } }
27
78
  @media (prefers-reduced-motion: reduce) {
28
- .dsh-codex-popover[data-picker-phase] > * { animation-duration: 0ms; }
29
- .dsh-codex-chevron { transition: none; }
79
+ .dsh-codex-trigger .dsh-plugin-composer-chevron { transition: none; }
30
80
  }
31
81
  .dsh-codex-popover > :not([role=radiogroup]) { flex-shrink: 0; }
32
82
  .dsh-codex-popover [role=radiogroup] { min-height: 0; overflow-y: auto; }
@@ -38,6 +88,7 @@ const styles = `
38
88
  .dsh-codex-model > span { display: block; color: var(--dsw-alias-label-tertiary, #777); font-size: 12px; line-height: 17px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
39
89
  .dsh-codex-model:hover, .dsh-codex-reset:hover, .dsh-codex-back:hover { background: var(--dsw-alias-interactive-bg-hover, #f2f2f2); }
40
90
  .dsh-codex-reset { display: grid; place-items: center; width: 24px; height: 24px; color: var(--dsw-alias-label-tertiary, #999); }
91
+ .dsh-codex-reset svg { width: 16px; height: 16px; }
41
92
  .dsh-codex-back { padding: 4px 8px; color: var(--dsw-alias-label-tertiary, #777); }
42
93
  .dsh-codex-popover h2, .dsh-codex-provider { margin: 0; padding: 6px 10px; font-size: 12px; font-weight: 400; color: var(--dsw-alias-label-tertiary, #777); }
43
94
  .dsh-codex-provider { font-size: 11px; padding-top: 10px; }
@@ -117,11 +168,12 @@ const copy = {
117
168
  };
118
169
  function apply(ctx) {
119
170
  function Picker({ locked, directory }) {
120
- const state = (0, react.useSyncExternalStore)(directory.store.subscribe, directory.store.getSnapshot);
121
- const locale = (0, react.useSyncExternalStore)((fn) => ctx.locale.subscribe(fn), () => ctx.locale.getSnapshot().active);
171
+ const subscribe = (0, react.useCallback)((listener) => directory.store.subscribe(listener), [directory]);
172
+ const getSnapshot = (0, react.useCallback)(() => directory.store.getSnapshot(), [directory]);
173
+ const state = (0, react.useSyncExternalStore)(subscribe, getSnapshot);
174
+ const { active: locale } = useLocaleSnapshot(ctx.locale);
122
175
  const text = copy[locale.startsWith("ko") ? "ko" : locale.startsWith("zh") ? "zh" : "en"];
123
- const [panel, setPanel] = (0, react.useState)(null);
124
- const [exiting, setExiting] = (0, react.useState)(false);
176
+ const [requestedPanel, setPanel] = (0, react.useState)(null);
125
177
  const [draft, setDraft] = (0, react.useState)(0);
126
178
  const [failed, setFailed] = (0, react.useState)(false);
127
179
  const trigger = (0, react.useRef)(null);
@@ -137,9 +189,10 @@ function apply(ctx) {
137
189
  id: void 0,
138
190
  name: text.default
139
191
  }] : [], ...reasoning.efforts] : [];
192
+ const panel = requestedPanel === "effort" && !efforts.length ? "model" : requestedPanel;
140
193
  const effective = current?.reasoningEffort ?? reasoning?.defaultEffort;
141
194
  const effortIndex = efforts.findIndex((item) => item.id === effective);
142
- const effortLabel = efforts[effortIndex]?.name ?? state.retainedEffort;
195
+ const effortLabel = efforts.length ? efforts[effortIndex]?.name ?? state.retainedEffort : void 0;
143
196
  const busy = state.status === "selecting";
144
197
  const disabled = locked || busy;
145
198
  (0, react.useEffect)(() => {
@@ -150,27 +203,10 @@ function apply(ctx) {
150
203
  };
151
204
  }, [directory]);
152
205
  const close = () => {
153
- setExiting(true);
206
+ setPanel(null);
154
207
  if (trigger.current) (0, _deepseek_ai_dsh_client_ui_primitives.focusWithoutRing)(trigger.current);
155
208
  };
156
- (0, _deepseek_ai_dsh_client_ui_primitives.useDismissOnOutsidePointer)(anchor, panel !== null && !exiting, () => setExiting(true), popup);
157
- (0, react.useLayoutEffect)(() => {
158
- if (!exiting || !popup.current) return;
159
- const element = popup.current;
160
- element.inert = true;
161
- let cancelled = false;
162
- const animations = element.getAnimations({ subtree: true }).filter((animation) => {
163
- const name = animation.animationName;
164
- return name?.includes("dsh-context-menu-out") || name === "dsh-codex-menu-out";
165
- });
166
- Promise.all(animations.map((animation) => animation.finished.catch(() => {}))).then(() => {
167
- if (!cancelled) setPanel(null);
168
- });
169
- return () => {
170
- cancelled = true;
171
- element.inert = false;
172
- };
173
- }, [exiting]);
209
+ (0, _deepseek_ai_dsh_client_ui_primitives.useDismissOnOutsidePointer)(anchor, panel !== null, () => setPanel(null), popup);
174
210
  (0, react.useLayoutEffect)(() => {
175
211
  if (!panel || !popup.current || !anchor.current) return;
176
212
  const element = popup.current;
@@ -216,10 +252,10 @@ function apply(ctx) {
216
252
  };
217
253
  }, [panel]);
218
254
  (0, react.useEffect)(() => {
219
- if (locked) setExiting(true);
255
+ if (locked) setPanel(null);
220
256
  }, [locked]);
221
257
  (0, react.useEffect)(() => {
222
- setExiting(true);
258
+ setPanel(null);
223
259
  }, [
224
260
  current?.provider,
225
261
  current?.model,
@@ -242,11 +278,10 @@ function apply(ctx) {
242
278
  };
243
279
  const show = (button) => {
244
280
  trigger.current = button;
245
- if (panel && !exiting) {
281
+ if (panel) {
246
282
  close();
247
283
  return;
248
284
  }
249
- setExiting(false);
250
285
  setDraft(Math.max(0, effortIndex));
251
286
  setFailed(false);
252
287
  setPanel("effort");
@@ -265,43 +300,27 @@ function apply(ctx) {
265
300
  className: "dsh-codex-picker",
266
301
  ref: anchor,
267
302
  "aria-busy": busy,
268
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
269
- type: "button",
303
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ComposerTrigger, {
270
304
  className: "dsh-codex-trigger",
305
+ icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconDataOutlineRegular, { size: 16 }),
306
+ label: model?.name ?? current?.model ?? text.model,
307
+ detail: effortLabel,
271
308
  disabled,
272
- "aria-label": `${text.choose}: ${model?.name ?? current?.model ?? text.model}${effortLabel ? `, ${effortLabel}` : ""}`,
309
+ "aria-label": `${efforts.length ? text.choose : text.model}: ${model?.name ?? current?.model ?? text.model}${effortLabel ? `, ${effortLabel}` : ""}`,
273
310
  title: `${model?.name ?? current?.model ?? text.model}${effortLabel ? ` ${effortLabel}` : ""}`,
274
311
  "aria-haspopup": "dialog",
275
- "aria-expanded": panel !== null && !exiting,
276
- "aria-controls": panel && !exiting ? id : void 0,
277
- onClick: (event) => show(event.currentTarget),
278
- children: [
279
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconDataOutlineRegular, {
280
- className: "dsh-codex-trigger-icon",
281
- size: 16
282
- }),
283
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
284
- className: "dsh-codex-name",
285
- children: model?.name ?? current?.model ?? text.model
286
- }),
287
- effortLabel && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
288
- className: "dsh-codex-effort",
289
- children: effortLabel
290
- }),
291
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutlineRegular, { className: "dsh-codex-chevron" })
292
- ]
312
+ "aria-expanded": panel !== null,
313
+ "aria-controls": panel ? id : void 0,
314
+ onClick: (event) => show(event.currentTarget)
293
315
  })
294
316
  }), panel && (0, react_dom.createPortal)(/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(_deepseek_ai_dsh_client_ui_primitives.MenuSurface, {
295
317
  id,
296
318
  ref: popup,
297
319
  role: "dialog",
298
- "aria-hidden": exiting || void 0,
299
- "data-picker-phase": exiting ? "dismiss" : "enter",
300
- phase: exiting ? "dismiss" : "enter",
301
320
  "aria-label": panel === "model" ? text.model : text.effort,
302
321
  className: "dsh-codex-popover",
303
322
  children: [
304
- panel === "model" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
323
+ panel === "model" ? efforts.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
305
324
  type: "button",
306
325
  className: "dsh-codex-back",
307
326
  onClick: () => setPanel("effort"),
@@ -335,16 +354,7 @@ function apply(ctx) {
335
354
  model: current.model
336
355
  });
337
356
  },
338
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
339
- width: "16",
340
- height: "16",
341
- viewBox: "0 0 16 16",
342
- fill: "none",
343
- stroke: "currentColor",
344
- strokeWidth: "1.2",
345
- "aria-hidden": "true",
346
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M3 4a5.5 5.5 0 1 1-1 6M3 1v3h3" })
347
- })
357
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconResetOutlineRegular, {})
348
358
  })]
349
359
  }),
350
360
  state.status === "loading" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
@@ -462,12 +472,7 @@ function apply(ctx) {
462
472
  locked: props.locked,
463
473
  directory: ctx.modelDirectories.directoryFor(props.sessionId)
464
474
  }, props.sessionId) : null));
465
- ctx.effect(() => {
466
- const style = document.createElement("style");
467
- style.textContent = styles;
468
- document.head.append(style);
469
- return () => style.remove();
470
- }, "ui-model-picker: styles");
475
+ ctx.effect(() => installStyles(styles), "ui-model-picker: styles");
471
476
  }
472
477
  exports.apply = apply;
473
478
  exports.inject = inject;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dungsil/dsh-plugin-ui-model-picker",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "description": "Model and reasoning effort picker for the DeepSeek Harness composer.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -32,14 +32,14 @@
32
32
  "@deepseek-ai/dsh-client-store": "0.2.1-rc.5",
33
33
  "@deepseek-ai/dsh-client-ui-conversation": "0.2.1-rc.5",
34
34
  "@deepseek-ai/dsh-client-ui-model-selection": "0.2.1-rc.5",
35
- "@deepseek-ai/dsh-client-ui-primitives": "0.2.1-rc.5",
36
35
  "@deepseek-ai/dsh-client-ui-renderer": "0.2.1-rc.5",
37
36
  "@deepseek-ai/dsh-client-ui-session": "0.2.1-rc.5",
38
37
  "@deepseek-ai/dsh-client-ui-slots": "0.2.1-rc.5",
39
38
  "@dungsil/dsh-tsconfig": "0.0.0",
39
+ "@dungsil/dsh-tsdown": "0.0.0",
40
+ "@dungsil/dsh-ui": "0.0.0",
40
41
  "@types/react": "18.3.31",
41
42
  "@types/react-dom": "18.3.7",
42
- "happy-dom": "21.0.4",
43
43
  "react": "18.3.1",
44
44
  "react-dom": "18.3.1",
45
45
  "tsdown": "0.23.4"
@@ -88,7 +88,6 @@
88
88
  },
89
89
  "scripts": {
90
90
  "build": "tsdown",
91
- "typecheck": "tsc --noEmit",
92
- "test": "node --test test/*.test.mjs"
91
+ "typecheck": "tsc --noEmit"
93
92
  }
94
93
  }