dsh-openai-subscription 0.1.6 → 0.1.8

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 CHANGED
@@ -137,7 +137,7 @@ After installing dependencies, run `npm run check` (or `pnpm run check`) for typ
137
137
 
138
138
  ### Settings UI and navigation icons
139
139
 
140
- Buttons use theme-aware, high-contrast `1px` borders. The header and navigation reuse one local OpenAI SVG, with no additional runtime dependency or remote image requests; see [third-party notices](THIRD_PARTY_NOTICES.md).
140
+ Every button, picker, and code field shares one control spec: a theme-aware `1px` border taken from the host's own control border step, a `12px` radius, and the host's type scale. Variants change color only, never size or shape. The header and navigation reuse one local OpenAI SVG, with no additional runtime dependency or remote image requests; see [third-party notices](THIRD_PARTY_NOTICES.md).
141
141
 
142
142
  The currently tested upstream DSH build does not expose custom settings navigation icons. Unextended hosts keep the default gear without affecting other plugin features. This project includes an **opt-in, build-guarded, reversible** local DSH extension; installing the plugin does not patch the host automatically. It preserves string labels and passes icons through `icon({ size, className })`, not DOM/CSS substitution. See [DSH settings icon extension](docs/DSH_SETTINGS_ICONS.md) for usage and upgrade caveats.
143
143
 
package/README.zh.md CHANGED
@@ -137,7 +137,7 @@ dsh plugin --profile web remove dsh-openai-subscription
137
137
 
138
138
  ### 设置界面与导航图标
139
139
 
140
- 按钮使用主题自适应的 `1px` 高对比度边框。顶部和导航复用本地 OpenAI SVG,无额外运行时依赖或远程图片请求;来源及许可证见 [第三方声明](THIRD_PARTY_NOTICES.md)。
140
+ 所有按钮、选择器与验证码输入框共用同一套控件规范:主题自适应的 `1px` 边框(取宿主自身的控件边框层级)、`12px` 圆角与宿主字号;变体只改变颜色,不改变尺寸或形状。顶部和导航复用本地 OpenAI SVG,无额外运行时依赖或远程图片请求;来源及许可证见 [第三方声明](THIRD_PARTY_NOTICES.md)。
141
141
 
142
142
  当前官方 DSH 设置导航尚未提供自定义图标接口,未扩展的宿主仍显示默认齿轮,插件其他功能不受影响。本项目提供**显式执行、限定构建版本、可还原**的本机 DSH 扩展,不在安装时自动修改宿主。它保留字符串标签,通过 `icon({ size, className })` 传递图标,不使用 DOM/CSS 替换技巧。用法和升级注意事项见 [DSH 图标接口扩展](docs/DSH_SETTINGS_ICONS.md)。
143
143
 
package/dist/client.js CHANGED
@@ -24,7 +24,12 @@ const ZH = {
24
24
  'context.model': '模型',
25
25
  'context.window': '上下文窗口',
26
26
  'context.default': '跟随模型默认',
27
- 'context.million': '1M · 1,000,000 tokens',
27
+ 'context.million': '1M 大上下文',
28
+ 'context.default.detail': '使用同步的模型配置',
29
+ 'context.custom.detail': '手动设置 tokens 数',
30
+ 'context.pending': '有未保存的更改',
31
+ 'context.applied': '已保存',
32
+ 'manage.help': '授权维护与本机连接',
28
33
  'context.custom': '自定义',
29
34
  'context.tokens': '自定义 tokens 数',
30
35
  'context.current': '当前:{count} tokens',
@@ -155,7 +160,12 @@ const EN = {
155
160
  'context.model': 'Model',
156
161
  'context.window': 'Context window',
157
162
  'context.default': 'Use model default',
158
- 'context.million': '1M · 1,000,000 tokens',
163
+ 'context.million': '1M context',
164
+ 'context.default.detail': 'Use the synced model setting',
165
+ 'context.custom.detail': 'Set a token count manually',
166
+ 'context.pending': 'Unsaved changes',
167
+ 'context.applied': 'Saved',
168
+ 'manage.help': 'Authorization and local connection',
159
169
  'context.custom': 'Custom',
160
170
  'context.tokens': 'Custom token count',
161
171
  'context.current': 'Current: {count} tokens',
@@ -295,9 +305,10 @@ window.__ModuleLoader__.load({
295
305
  .oasub-wrap {
296
306
  --oasub-section-inset: 20px;
297
307
  --oasub-card-border-width: 1px;
298
- /* Control boundaries need more contrast than the host's decorative hairlines. */
299
- --oasub-control-border: color-mix(in srgb, var(--dsw-alias-label-primary, #0f1115) 48%, transparent);
300
- --oasub-control-border-hover: color-mix(in srgb, var(--dsw-alias-label-primary, #0f1115) 68%, transparent);
308
+ /* One control boundary for every button and picker: the host's own control
309
+ border step, so plugin controls carry the same weight as DSH's. */
310
+ --oasub-control-border: var(--dsw-alias-border-l3, rgba(15, 17, 21, .16));
311
+ --oasub-control-border-hover: var(--dsw-alias-border-l4, rgba(15, 17, 21, .2));
301
312
  box-sizing: border-box;
302
313
  display: flex;
303
314
  flex-direction: column;
@@ -363,41 +374,96 @@ window.__ModuleLoader__.load({
363
374
  .oasub-model-title { font-size: 13px; font-weight: 500; line-height: 19px; }
364
375
  .oasub-model-detail { color: var(--dsw-alias-label-tertiary, #81858c); font-size: 11px; line-height: 17px; }
365
376
  .oasub-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
366
- .oasub-management { display: flex; flex-direction: column; gap: 16px; border-top: 1px solid var(--dsw-alias-border-l2, rgba(15, 17, 21, .14)); padding-top: 16px; }
367
- .oasub-manage-row { display: flex; align-items: center; justify-content: space-between; gap: 16px; }
368
- .oasub-manage-row > .oasub-button { flex: 0 0 auto; }
369
- .oasub-button.disclosure { border-color: transparent; border-radius: 8px; color: var(--dsw-alias-label-secondary, #61666b); padding: 4px 0; min-height: 32px; align-self: flex-start; }
370
- .oasub-context { display: flex; flex-direction: column; gap: 10px; }
371
- .oasub-field { display: flex; flex-direction: column; gap: 6px; flex: 1 1 180px; min-width: 0; font-size: 12px; }
377
+ .oasub-wrap { container-type: inline-size; }
378
+ .oasub-ui-icon { display: block; flex: none; }
379
+ .oasub-section-heading { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; }
380
+ .oasub-section-copy { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
381
+ .oasub-context { display: flex; flex-direction: column; gap: 16px; }
382
+ .oasub-context-grid { display: grid; grid-template-columns: 1.1fr 1fr; align-items: start; gap: 12px; }
383
+ .oasub-field { display: flex; flex-direction: column; gap: 8px; min-width: 0; font-size: 12px; font-weight: 500; }
372
384
  .oasub-context > .oasub-field { flex: none; }
373
- .oasub-select { width: 100%; min-height: 36px; border: 1px solid var(--oasub-control-border); border-radius: 8px; padding: 7px 10px; font: inherit; color: inherit; background: var(--dsw-alias-bg-layer-1, #fff); }
374
- .oasub-select:focus-visible { outline: 2px solid var(--dsw-alias-brand-primary, #4176e6); outline-offset: 2px; }
375
- .oasub-select:disabled { opacity: .55; }
385
+ .oasub-picker { position: relative; min-width: 0; }
386
+ .oasub-picker-trigger, .oasub-input-shell {
387
+ width: 100%; min-height: 56px; padding: 10px 12px;
388
+ border: 1px solid var(--oasub-control-border); border-radius: 12px;
389
+ color: var(--dsw-alias-label-primary, #0f1115); background: var(--dsw-alias-bg-layer-2, #f5f6f7);
390
+ }
391
+ .oasub-picker-trigger { appearance: none; display: flex; align-items: center; justify-content: space-between; gap: 12px; font: inherit; text-align: start; cursor: pointer; transition: border-color .16s, background .16s; }
392
+ .oasub-picker-trigger:hover:not(:disabled), .oasub-picker-trigger[aria-expanded="true"] { border-color: var(--oasub-control-border-hover); background: var(--dsw-alias-bg-layer-1, #fff); }
393
+ .oasub-picker-trigger:focus-visible, .oasub-input-shell:focus-within, .oasub-management-toggle:focus-visible { outline: 2px solid var(--dsw-alias-brand-primary, #4176e6); outline-offset: 3px; }
394
+ .oasub-picker-trigger:disabled { opacity: .5; cursor: default; }
395
+ .oasub-picker-copy { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
396
+ .oasub-picker-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 13px; font-weight: 500; line-height: 19px; }
397
+ .oasub-picker-detail { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--dsw-alias-label-secondary, #61666b); font-size: 11px; font-weight: 400; line-height: 16px; font-variant-numeric: tabular-nums; }
398
+ .oasub-chevron { color: var(--dsw-alias-label-secondary, #61666b); transition: transform .16s; }
399
+ [aria-expanded="true"] > .oasub-chevron { transform: rotate(180deg); }
400
+ .oasub-picker-menu { position: absolute; inset: calc(100% + 6px) 0 auto; z-index: 20; margin: 0; padding: 5px; max-height: 248px; overflow-y: auto; overscroll-behavior: contain; list-style: none; border: 1px solid var(--dsw-alias-border-l1, rgba(15, 17, 21, .2)); border-radius: 14px; background: var(--dsw-alias-bg-layer-1, #fff); box-shadow: 0 8px 28px rgba(0, 0, 0, .14); }
401
+ .oasub-picker-option { display: flex; align-items: center; justify-content: space-between; gap: 8px; min-height: 48px; padding: 8px 10px; border-radius: 9px; cursor: pointer; }
402
+ .oasub-picker-option[data-active="true"] { background: var(--dsw-alias-interactive-bg-hover, rgba(15, 17, 21, .06)); }
403
+ .oasub-picker-option[aria-selected="true"] .oasub-picker-label { font-weight: 600; }
404
+ .oasub-picker-option[aria-disabled="true"] { opacity: .4; cursor: not-allowed; }
405
+ .oasub-input-shell { display: flex; align-items: center; gap: 12px; min-height: 44px; }
406
+ .oasub-input-shell:has(input:disabled) { opacity: .5; }
407
+ .oasub-input-shell:has([aria-invalid="true"]) { border-color: var(--dsw-alias-state-error-primary, #dc2626); }
408
+ .oasub-token-input { appearance: none; width: 100%; min-width: 0; padding: 0; border: 0; outline: none; background: transparent; color: inherit; font: inherit; font-size: 14px; line-height: 22px; font-variant-numeric: tabular-nums; }
409
+ .oasub-token-unit { color: var(--dsw-alias-label-tertiary, #81858c); font-size: 12px; font-weight: 400; }
410
+ .oasub-context-meta { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; color: var(--dsw-alias-label-secondary, #61666b); font-size: 12px; line-height: 18px; font-variant-numeric: tabular-nums; }
411
+ .oasub-context-badge { display: inline-flex; align-items: center; gap: 4px; padding: 2px 7px; border-radius: 6px; background: var(--dsw-alias-bg-layer-2, #f5f6f7); font-size: 10px; }
412
+ .oasub-context-note { display: flex; align-items: flex-start; gap: 7px; color: var(--dsw-alias-label-tertiary, #81858c); font-size: 11px; line-height: 17px; }
413
+ .oasub-context-note > svg { margin-top: 2px; }
414
+ .oasub-context-error { color: var(--dsw-alias-state-error-primary, #b91c1c); font-size: 12px; line-height: 18px; }
415
+ .oasub-context-footer { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 12px; padding-top: 14px; border-top: 1px solid var(--dsw-alias-border-l2, rgba(15, 17, 21, .14)); }
416
+ .oasub-management-card { padding: 0; gap: 0; }
417
+ .oasub-management-heading { margin: 0; font: inherit; }
418
+ .oasub-management-toggle { appearance: none; display: flex; align-items: center; gap: 12px; width: 100%; padding: var(--oasub-section-inset); border: 0; border-radius: 16px; color: inherit; background: transparent; font: inherit; text-align: start; cursor: pointer; transition: background .16s; }
419
+ .oasub-management-toggle:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(15, 17, 21, .04)); }
420
+ .oasub-management-toggle > .oasub-section-copy { flex: 1; }
421
+ .oasub-management-symbol { display: grid; place-items: center; width: 34px; height: 34px; flex: none; border-radius: 10px; color: var(--dsw-alias-label-secondary, #61666b); background: var(--dsw-alias-bg-layer-2, #f5f6f7); }
422
+ .oasub-management { display: flex; flex-direction: column; margin-inline: var(--oasub-section-inset); }
423
+ .oasub-management[hidden] { display: none; }
424
+ .oasub-manage-row { display: flex; align-items: center; justify-content: space-between; gap: 24px; padding-block: 16px; border-top: 1px solid var(--dsw-alias-border-l2, rgba(15, 17, 21, .14)); }
425
+ .oasub-manage-row .oasub-status-detail { font-size: 11px; line-height: 17px; }
426
+ .oasub-manage-title { font-size: 13px; font-weight: 500; line-height: 20px; }
427
+ /* Management rows reuse the one button spec; only the row layout differs. */
428
+ .oasub-manage-row > .oasub-button { flex: none; }
429
+ @container (max-width: 460px) {
430
+ .oasub-context-grid { grid-template-columns: minmax(0, 1fr); }
431
+ .oasub-manage-row { align-items: flex-start; flex-direction: column; gap: 10px; }
432
+ .oasub-manage-row > .oasub-button { align-self: flex-start; }
433
+ }
434
+ /* One button spec for the whole panel: DSH's control radius, control border
435
+ step, and type scale. Variants change color only, never size or shape. */
376
436
  .oasub-button {
377
437
  appearance: none;
438
+ display: inline-flex;
439
+ align-items: center;
440
+ justify-content: center;
441
+ gap: 6px;
378
442
  min-height: 36px;
379
- padding: 7px 15px;
443
+ padding: 0 16px;
380
444
  border: 1px solid var(--oasub-control-border);
381
- border-radius: 999px;
445
+ border-radius: 12px;
382
446
  color: var(--dsw-alias-label-primary, #0f1115);
383
447
  background: transparent;
384
448
  font: inherit;
385
- font-size: 13px;
449
+ font-size: 14px;
386
450
  font-weight: 500;
387
- line-height: 20px;
451
+ line-height: 22px;
388
452
  cursor: pointer;
389
453
  text-decoration: none;
390
454
  text-align: center;
391
455
  transition: background .16s ease, border-color .16s ease, opacity .16s ease, transform .16s ease;
392
456
  }
393
- .oasub-button:not(.primary):not(.danger):hover:not(:disabled) { border-color: var(--oasub-control-border-hover); background: var(--dsw-alias-interactive-bg-hover, rgba(15, 17, 21, .06)); }
457
+ .oasub-button:not(.primary):not(.danger):not(.danger-quiet):hover:not(:disabled) { border-color: var(--oasub-control-border-hover); background: var(--dsw-alias-interactive-bg-hover, rgba(15, 17, 21, .06)); }
394
458
  .oasub-button:active:not(:disabled) { transform: translateY(1px); }
395
459
  .oasub-button:focus-visible { outline: 2px solid var(--dsw-alias-brand-primary, #4176e6); outline-offset: 2px; }
396
460
  .oasub-button:disabled { cursor: default; opacity: .45; }
397
- .oasub-button.primary { border-color: var(--dsw-alias-brand-primary, #4176e6); color: var(--dsw-alias-label-primary-inverted, #fff); background: var(--dsw-alias-brand-primary, #4176e6); }
398
- .oasub-button.primary:hover:not(:disabled) { filter: brightness(.96); }
399
- .oasub-button.danger { border-color: var(--dsw-alias-state-error-primary, #dc2626); color: var(--dsw-alias-label-primary-inverted, #fff); background: var(--dsw-alias-state-error-primary, #dc2626); }
461
+ .oasub-button.primary { border-color: var(--dsw-alias-button-primary-fill, var(--dsw-alias-brand-primary, #4176e6)); color: var(--dsw-alias-label-primary-foreground, #fff); background: var(--dsw-alias-button-primary-fill, var(--dsw-alias-brand-primary, #4176e6)); }
462
+ .oasub-button.primary:hover:not(:disabled) { border-color: var(--dsw-alias-button-primary-hover, var(--dsw-alias-brand-primary, #4176e6)); background: var(--dsw-alias-button-primary-hover, var(--dsw-alias-brand-primary, #4176e6)); }
463
+ .oasub-button.danger { border-color: var(--dsw-alias-state-error-primary, #dc2626); color: var(--dsw-alias-label-primary-foreground, #fff); background: var(--dsw-alias-state-error-primary, #dc2626); }
400
464
  .oasub-button.danger:hover:not(:disabled) { filter: brightness(.94); }
465
+ .oasub-button.danger-quiet { color: var(--dsw-alias-state-error-primary, #b91c1c); border-color: color-mix(in srgb, var(--dsw-alias-state-error-primary, #dc2626) 25%, transparent); }
466
+ .oasub-button.danger-quiet:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover-danger, color-mix(in srgb, var(--dsw-alias-state-error-primary, #dc2626) 7%, transparent)); border-color: var(--dsw-alias-state-error-primary, #dc2626); }
401
467
  .oasub-button.quiet { margin-left: auto; }
402
468
  .oasub-notice {
403
469
  display: flex;
@@ -420,10 +486,10 @@ window.__ModuleLoader__.load({
420
486
  align-self: flex-start;
421
487
  width: min(100%, 24ch);
422
488
  min-width: 0;
423
- border: 1px solid var(--dsw-alias-border-l1, rgba(15, 17, 21, .2));
489
+ border: 1px solid var(--oasub-control-border);
424
490
  margin: 0;
425
491
  padding: 10px 14px;
426
- border-radius: 10px;
492
+ border-radius: 12px;
427
493
  color: var(--dsw-alias-label-primary, #0f1115);
428
494
  background: var(--dsw-alias-bg-layer-2, #f5f6f7);
429
495
  font-family: var(--ds-font-family-code, ui-monospace, SFMono-Regular, Menlo, Consolas, monospace);
@@ -448,15 +514,19 @@ window.__ModuleLoader__.load({
448
514
  .oasub-manage-row { align-items: flex-start; flex-direction: column; gap: 8px; }
449
515
  }
450
516
  @media (forced-colors: active) {
517
+ .oasub-picker-trigger, .oasub-input-shell, .oasub-picker-menu { border-color: ButtonText; }
518
+ .oasub-picker-option[data-active="true"] { outline: 1px solid Highlight; outline-offset: -1px; }
519
+ .oasub-picker-option[aria-disabled="true"], .oasub-picker-trigger:disabled { color: GrayText; opacity: 1; }
451
520
  .oasub-button { border-color: ButtonText; }
452
- .oasub-button.primary, .oasub-button.danger { border-color: ButtonText; color: ButtonText; background: ButtonFace; }
453
- .oasub-button:not(.primary):not(.danger):hover:not(:disabled),
454
- .oasub-button.primary:hover:not(:disabled), .oasub-button.danger:hover:not(:disabled) { border-color: Highlight; filter: none; }
521
+ .oasub-button.primary, .oasub-button.danger, .oasub-button.danger-quiet { border-color: ButtonText; color: ButtonText; background: ButtonFace; }
522
+ .oasub-button:not(.primary):not(.danger):not(.danger-quiet):hover:not(:disabled),
523
+ .oasub-button.primary:hover:not(:disabled), .oasub-button.danger:hover:not(:disabled),
524
+ .oasub-button.danger-quiet:hover:not(:disabled) { border-color: Highlight; filter: none; }
455
525
  .oasub-button:focus-visible { outline-color: Highlight; }
456
526
  .oasub-button:disabled { border-color: GrayText; color: GrayText; opacity: 1; }
457
527
  }
458
528
  @media (prefers-reduced-motion: reduce) {
459
- .oasub-button { transition: none; }
529
+ .oasub-button, .oasub-picker-trigger, .oasub-chevron, .oasub-management-toggle { transition: none; }
460
530
  .oasub-skeleton::after { animation: none; display: none; }
461
531
  }
462
532
  `;
@@ -661,6 +731,17 @@ window.__ModuleLoader__.load({
661
731
  return null;
662
732
  }
663
733
  }
734
+ function uiIcon(name, size = 16) {
735
+ const paths = {
736
+ chevron: 'm6 9 6 6 6-6', check: 'm5 12 4 4L19 6',
737
+ info: 'M12 11v6m0-10v.01M22 12a10 10 0 1 1-20 0 10 10 0 0 1 20 0',
738
+ settings: 'M4 7h9m4 0h3M4 17h3m4 0h9M13 4v6M7 14v6',
739
+ };
740
+ return React.createElement('svg', { width: size, height: size, viewBox: '0 0 24 24', fill: 'none',
741
+ stroke: 'currentColor', strokeWidth: 1.6, strokeLinecap: 'round', strokeLinejoin: 'round',
742
+ className: 'oasub-ui-icon' + (name === 'chevron' ? ' oasub-chevron' : ''), 'aria-hidden': true, focusable: false,
743
+ }, React.createElement('path', { d: paths[name] }));
744
+ }
664
745
  function Section(props) {
665
746
  const el = React.createElement;
666
747
  const { connection, timer, t } = props;
@@ -686,6 +767,10 @@ window.__ModuleLoader__.load({
686
767
  // null means no pending edit; '' selects the synchronized model default.
687
768
  const [contextDraft, setContextDraft] = React.useState(null);
688
769
  const [contextChoice, setContextChoice] = React.useState(null);
770
+ const [picker, setPicker] = React.useState(null);
771
+ const pickerRoot = React.useRef(null);
772
+ const activeOption = React.useRef(null);
773
+ const pickerSearch = React.useRef({ text: '', at: 0 });
689
774
  const actionLock = React.useRef(false);
690
775
  const cancelAcknowledged = React.useRef(false);
691
776
  const flowObserved = React.useRef(false);
@@ -770,6 +855,28 @@ window.__ModuleLoader__.load({
770
855
  setContextLoading(false); });
771
856
  return () => controller.abort();
772
857
  }, [connection, configured, statusRevision]);
858
+ const contextDisabled = busy || contextLoading || statusLoading;
859
+ React.useEffect(() => {
860
+ if (contextDisabled || !configured)
861
+ setPicker(null);
862
+ }, [contextDisabled, configured, contexts]);
863
+ React.useEffect(() => {
864
+ if (!picker || typeof document === 'undefined')
865
+ return;
866
+ const dismissOutside = (event) => {
867
+ if (!pickerRoot.current?.contains(event.target))
868
+ setPicker(null);
869
+ };
870
+ document.addEventListener('pointerdown', dismissOutside);
871
+ document.addEventListener('focusin', dismissOutside);
872
+ return () => {
873
+ document.removeEventListener('pointerdown', dismissOutside);
874
+ document.removeEventListener('focusin', dismissOutside);
875
+ };
876
+ }, [picker?.id]);
877
+ React.useEffect(() => {
878
+ activeOption.current?.scrollIntoView?.({ block: 'nearest' });
879
+ }, [picker?.id, picker?.active]);
773
880
  React.useEffect(() => {
774
881
  if (props.subscribeReset === undefined)
775
882
  return;
@@ -1123,6 +1230,81 @@ window.__ModuleLoader__.load({
1123
1230
  }
1124
1231
  });
1125
1232
  };
1233
+ // Select-only combobox: focus stays on the trigger, and navigation never saves a value.
1234
+ const renderPicker = (id, label, value, options, choose, describedBy) => {
1235
+ const open = picker?.id === id && !contextDisabled;
1236
+ const selected = options.find((option) => option.value === value);
1237
+ const enabled = options.map((option, index) => option.disabled ? -1 : index).filter((index) => index >= 0);
1238
+ const selectedIndex = Math.max(0, options.findIndex((option) => option.value === value && !option.disabled));
1239
+ const active = open ? picker.active : selectedIndex;
1240
+ const optionId = (index) => id + '-option-' + index;
1241
+ const commit = (index) => {
1242
+ const option = options[index];
1243
+ if (!option || option.disabled || contextDisabled)
1244
+ return;
1245
+ choose(option.value);
1246
+ setPicker(null);
1247
+ };
1248
+ const onKeyDown = (event) => {
1249
+ if (contextDisabled)
1250
+ return;
1251
+ if (event.key === 'Escape' && open) {
1252
+ event.preventDefault();
1253
+ event.stopPropagation();
1254
+ setPicker(null);
1255
+ return;
1256
+ }
1257
+ if (event.key === 'Tab') {
1258
+ setPicker(null);
1259
+ return;
1260
+ }
1261
+ if (event.key === 'Enter' || event.key === ' ') {
1262
+ event.preventDefault();
1263
+ if (open)
1264
+ commit(active);
1265
+ else {
1266
+ pickerSearch.current.text = '';
1267
+ setPicker({ id, active: selectedIndex });
1268
+ }
1269
+ return;
1270
+ }
1271
+ const direction = event.key === 'ArrowDown' ? 1 : event.key === 'ArrowUp' ? -1 : 0;
1272
+ if (direction || event.key === 'Home' || event.key === 'End') {
1273
+ event.preventDefault();
1274
+ const position = enabled.indexOf(active);
1275
+ const next = event.key === 'Home' ? enabled[0] : event.key === 'End' ? enabled.at(-1)
1276
+ : !open ? selectedIndex : enabled[(position + direction + enabled.length) % enabled.length];
1277
+ setPicker({ id, active: next ?? 0 });
1278
+ return;
1279
+ }
1280
+ if (event.key.length === 1 && !event.altKey && !event.ctrlKey && !event.metaKey) {
1281
+ event.preventDefault();
1282
+ const search = pickerSearch.current;
1283
+ search.text = (Date.now() - search.at > 700 ? '' : search.text) + event.key.toLocaleLowerCase();
1284
+ search.at = Date.now();
1285
+ const text = [...search.text].every((char) => char === search.text[0]) ? search.text[0] : search.text;
1286
+ const order = [...enabled.filter((index) => index > active), ...enabled.filter((index) => index <= active)];
1287
+ const match = order.find((index) => options[index].label.toLocaleLowerCase().startsWith(text) || options[index].value.toLocaleLowerCase().startsWith(text));
1288
+ if (match !== undefined)
1289
+ setPicker({ id, active: match });
1290
+ }
1291
+ };
1292
+ const copy = (option) => el('span', { className: 'oasub-picker-copy' }, el('span', { className: 'oasub-picker-label' }, option?.label ?? value), option?.detail ? el('span', { className: 'oasub-picker-detail' }, option.detail) : null);
1293
+ return el('div', { className: 'oasub-field' }, el('label', { id: id + '-label', htmlFor: id }, label), el('div', { className: 'oasub-picker', ref: open ? pickerRoot : undefined }, el('button', { id, type: 'button', className: 'oasub-picker-trigger', role: 'combobox',
1294
+ 'aria-label': label, 'aria-expanded': open, 'aria-haspopup': 'listbox',
1295
+ 'aria-controls': open ? id + '-list' : undefined, 'aria-activedescendant': open ? optionId(active) : undefined,
1296
+ 'aria-describedby': describedBy, disabled: contextDisabled, 'data-value': value,
1297
+ onKeyDown, onClick: () => { pickerSearch.current.text = ''; setPicker(open ? null : { id, active: selectedIndex }); },
1298
+ }, copy(selected), uiIcon('chevron')), open ? el('ul', { id: id + '-list', className: 'oasub-picker-menu', role: 'listbox', 'aria-labelledby': id + '-label',
1299
+ onMouseDown: (event) => event.preventDefault(),
1300
+ }, options.map((option, index) => el('li', { key: option.value, id: optionId(index), role: 'option',
1301
+ className: 'oasub-picker-option', 'aria-selected': option.value === value, 'aria-disabled': !!option.disabled,
1302
+ 'data-value': option.value, 'data-active': index === active, ref: index === active ? activeOption : undefined,
1303
+ onMouseMove: () => { if (!option.disabled && index !== active)
1304
+ setPicker({ id, active: index }); },
1305
+ onClick: () => commit(index),
1306
+ }, copy(option), option.value === value ? uiIcon('check', 15) : null))) : null));
1307
+ };
1126
1308
  const selectCode = () => {
1127
1309
  codeRef.current?.focus();
1128
1310
  codeRef.current?.select();
@@ -1168,7 +1350,9 @@ window.__ModuleLoader__.load({
1168
1350
  : null, el('div', { className: 'oasub-actions' }, configured
1169
1351
  ? el('button', {
1170
1352
  type: 'button',
1171
- className: 'oasub-button ' + (info.modelsSynced ? '' : 'primary'),
1353
+ // Exactly one primary action per card: a pending credential
1354
+ // repair outranks the model sync action.
1355
+ className: 'oasub-button' + (info.modelsSynced || info.credentialState !== 'valid' ? '' : ' primary'),
1172
1356
  disabled: busy,
1173
1357
  'aria-describedby': 'oasub-sync-help',
1174
1358
  onClick: requestModelSync,
@@ -1185,25 +1369,35 @@ window.__ModuleLoader__.load({
1185
1369
  onClick: () => startAuthorization(info.refreshable ? 'refresh' : 'device_code'),
1186
1370
  }, phase === 'starting' || phase === 'authorizing' ? t('action.refreshing') : t(info.refreshable ? 'action.refresh' : 'action.reconnect'))
1187
1371
  : null), configured ? el('div', { id: 'oasub-sync-help', className: 'oasub-status-detail' }, t('model.sync.help')) : null);
1188
- return el('div', { className: 'oasub-wrap' }, el('header', { className: 'oasub-header' }, el('div', { className: 'oasub-mark', 'aria-hidden': true }, el(OpenAIIcon, { size: 24 })), el('div', { className: 'oasub-heading' }, el('h2', { className: 'oasub-title' }, t('title')), el('p', { className: 'oasub-subtitle' }, t('subtitle')))), statusCard, configured ? el('section', { className: 'oasub-card oasub-context', 'aria-labelledby': 'oasub-context-title' }, el('div', { id: 'oasub-context-title', className: 'oasub-status-title' }, t('context.title')), el('div', { className: 'oasub-status-detail' }, t('context.help')), contextLoading ? el('div', { role: 'status', className: 'oasub-status-detail' }, t('context.loading')) : null, contextError ? el('div', { role: 'alert', className: 'oasub-notice error' }, t(contextError), el('button', { type: 'button', className: 'oasub-button quiet', disabled: contextLoading, onClick: reloadStatus }, t('action.retry'))) : null, !contextLoading && contexts?.models.length === 0 ? el('div', { className: 'oasub-status-detail' }, t('context.empty')) : null, selectedModel ? el('div', { className: 'oasub-context' }, el('div', { className: 'oasub-actions' }, el('label', { className: 'oasub-field' }, t('context.model'), el('select', { className: 'oasub-select', 'aria-label': t('context.model'), value: modelId, disabled: busy || contextLoading || statusLoading,
1189
- onChange: (event) => { setModelId(event.currentTarget.value); setContextDraft(null); setContextChoice(null); } }, contexts?.models.map((model) => el('option', { key: model.id, value: model.id }, model.name && model.name !== model.id ? model.name + ' (' + model.id + ')' : model.id)))), el('label', { className: 'oasub-field' }, t('context.window'), el('select', { className: 'oasub-select', 'aria-label': t('context.window'), value: contextMode, disabled: busy || contextLoading || statusLoading,
1190
- 'aria-describedby': 'oasub-context-limit',
1191
- onChange: (event) => {
1192
- const choice = event.currentTarget.value === 'default' ? 'default' : event.currentTarget.value === 'million' ? 'million' : 'custom';
1193
- setContextChoice(choice);
1194
- setContextDraft(choice === 'default' ? '' : choice === 'million' ? '1000000' : '-');
1195
- } }, el('option', { value: 'default' }, t('context.default') + (selectedModel.defaultContextWindow ? ' · ' + selectedModel.defaultContextWindow.toLocaleString() : '')), el('option', { value: 'million', disabled: selectedModel.maxContextWindow !== undefined && selectedModel.maxContextWindow < 1_000_000 }, t('context.million')), el('option', { value: 'custom' }, t('context.custom'))))), contextMode === 'custom' ? el('label', { className: 'oasub-field' }, t('context.tokens'), el('input', { className: 'oasub-select', type: 'text', inputMode: 'numeric', 'aria-label': t('context.tokens'),
1196
- value: draft === '-' ? '' : draft, disabled: busy || contextLoading || statusLoading,
1197
- 'aria-invalid': contextValidation !== null, 'aria-describedby': 'oasub-context-limit',
1198
- onChange: (event) => setContextDraft(event.currentTarget.value || '-') })) : null, selectedModel.contextWindow ? el('div', { className: 'oasub-status-detail' }, t('context.current', { count: selectedModel.contextWindow.toLocaleString() })) : null, el('div', { id: 'oasub-context-limit', className: 'oasub-status-detail' }, selectedModel.maxContextWindow ? t('context.maximum', { count: selectedModel.maxContextWindow.toLocaleString() }) + ' · ' + t('context.limit.help') : t('context.unknown')), contextDirty && contextValidation ? el('div', { role: 'alert', className: 'oasub-status-detail' }, t(contextValidation)) : null, contextDirty ? el('div', { className: 'oasub-actions' }, el('button', { type: 'button', className: 'oasub-button primary', disabled: busy || contextLoading || statusLoading || contextValidation !== null,
1199
- onClick: saveContext }, t(phase === 'saving-context' ? 'context.saving' : 'context.save'))) : null) : null) : null, statusError !== null
1200
- ? el('div', { className: 'oasub-notice error', role: 'alert' }, t(statusError), info !== null ? ' ' + t('status.stale') : null, el('button', { type: 'button', className: 'oasub-button quiet', disabled: statusLoading, onClick: reloadStatus }, t('action.retry'))) : null, el('button', { ref: reloadRef, type: 'button', className: 'oasub-button disclosure',
1201
- 'aria-expanded': managementOpen, 'aria-controls': managementOpen ? 'oasub-management' : undefined,
1202
- onClick: () => setManagementOpen((open) => !open) }, t(managementOpen ? 'action.less' : 'action.more')), managementOpen ? el('section', { id: 'oasub-management', className: 'oasub-management', 'aria-label': t('action.more') }, configured && info?.credentialState === 'valid' ? el('div', { className: 'oasub-manage-row' }, el('div', { className: 'oasub-status-detail', id: 'oasub-refresh-help' }, t(info.refreshable ? 'manage.refresh.help' : 'manage.reconnect.help')), el('button', { type: 'button', className: 'oasub-button', disabled: busy || !ready,
1203
- 'aria-describedby': 'oasub-refresh-help',
1204
- onClick: () => startAuthorization(info.refreshable ? 'refresh' : 'device_code') }, phase === 'starting' || phase === 'authorizing' ? t('action.refreshing') : t(info.refreshable ? 'action.refresh' : 'action.reconnect'))) : null, el('div', { className: 'oasub-manage-row' }, el('div', { className: 'oasub-status-detail', id: 'oasub-reload-help' }, t('manage.reload.help')), el('button', { type: 'button', className: 'oasub-button', disabled: statusLoading,
1205
- 'aria-describedby': 'oasub-reload-help', onClick: reloadStatus }, t(statusLoading ? 'status.reloading' : 'action.reload'))), info?.cleanupAvailable ? el('div', { className: 'oasub-manage-row' }, el('div', { className: 'oasub-status-detail', id: 'oasub-disconnect-help' }, t('manage.disconnect.help')), el('button', { type: 'button', className: 'oasub-button', disabled: busy,
1206
- 'aria-describedby': 'oasub-disconnect-help', onClick: () => openConfirm('disconnect') }, t(phase === 'disconnecting' ? 'action.disconnecting' : 'action.disconnect'))) : null) : null, device !== null
1372
+ return el('div', { className: 'oasub-wrap' }, el('header', { className: 'oasub-header' }, el('div', { className: 'oasub-mark', 'aria-hidden': true }, el(OpenAIIcon, { size: 24 })), el('div', { className: 'oasub-heading' }, el('h2', { className: 'oasub-title' }, t('title')), el('p', { className: 'oasub-subtitle' }, t('subtitle')))), statusCard, configured ? el('section', { className: 'oasub-card oasub-context', 'aria-labelledby': 'oasub-context-title' }, el('div', { className: 'oasub-section-heading' }, el('div', { className: 'oasub-section-copy' }, el('div', { id: 'oasub-context-title', className: 'oasub-status-title' }, t('context.title')), el('div', { className: 'oasub-status-detail' }, t('context.help')))), contextLoading ? el('div', { role: 'status', className: 'oasub-status-detail' }, t('context.loading')) : null, contextError ? el('div', { role: 'alert', className: 'oasub-notice error' }, t(contextError), el('button', { type: 'button', className: 'oasub-button quiet', disabled: contextLoading, onClick: reloadStatus }, t('action.retry'))) : null, !contextLoading && contexts?.models.length === 0 ? el('div', { className: 'oasub-status-detail' }, t('context.empty')) : null, selectedModel ? el('div', { className: 'oasub-context' }, el('div', { className: 'oasub-context-grid' }, renderPicker('oasub-model', t('context.model'), modelId, (contexts?.models ?? []).map((model) => ({
1373
+ value: model.id, label: model.name || model.id,
1374
+ detail: model.name && model.name !== model.id ? model.id : undefined,
1375
+ })), (value) => { setModelId(value); setContextDraft(null); setContextChoice(null); }), renderPicker('oasub-window', t('context.window'), contextMode, [
1376
+ { value: 'default', label: t('context.default'), detail: selectedModel.defaultContextWindow ? selectedModel.defaultContextWindow.toLocaleString() + ' tokens' : t('context.default.detail') },
1377
+ { value: 'million', label: t('context.million'), detail: '1,000,000 tokens', disabled: selectedModel.maxContextWindow !== undefined && selectedModel.maxContextWindow < 1_000_000 },
1378
+ { value: 'custom', label: t('context.custom'), detail: t('context.custom.detail') },
1379
+ ], (value) => {
1380
+ const choice = value === 'default' ? 'default' : value === 'million' ? 'million' : 'custom';
1381
+ setContextChoice(choice);
1382
+ setContextDraft(choice === 'default' ? '' : choice === 'million' ? '1000000' : '-');
1383
+ }, 'oasub-context-limit')), contextMode === 'custom' ? el('label', { className: 'oasub-field' }, t('context.tokens'), el('span', { className: 'oasub-input-shell' }, el('input', { className: 'oasub-token-input', type: 'text', inputMode: 'numeric', 'aria-label': t('context.tokens'),
1384
+ value: draft === '-' ? '' : draft, disabled: contextDisabled, placeholder: '1000000',
1385
+ 'aria-invalid': contextValidation !== null, 'aria-describedby': 'oasub-context-limit' + (contextDirty && contextValidation ? ' oasub-context-error' : ''),
1386
+ onChange: (event) => setContextDraft(event.currentTarget.value || '-') }), el('span', { className: 'oasub-token-unit', 'aria-hidden': true }, 'tokens'))) : null, el('div', { id: 'oasub-context-limit', className: 'oasub-context-note' }, uiIcon('info', 13), el('span', null, selectedModel.maxContextWindow ? t('context.maximum', { count: selectedModel.maxContextWindow.toLocaleString() }) + ' · ' + t('context.limit.help') : t('context.unknown'))), contextDirty && contextValidation ? el('div', { id: 'oasub-context-error', role: 'alert', className: 'oasub-context-error' }, t(contextValidation)) : null, el('div', { className: 'oasub-context-footer' }, el('div', { className: 'oasub-context-meta' }, selectedModel.contextWindow ? el('span', null, t('context.current', { count: selectedModel.contextWindow.toLocaleString() })) : null, contextDirty ? el('span', { className: 'oasub-context-badge' }, t('context.pending'))
1387
+ : selectedModel.customized ? el('span', { className: 'oasub-context-badge' }, uiIcon('check', 12), t('context.applied')) : null), contextDirty ? el('button', { type: 'button', className: 'oasub-button primary', disabled: contextDisabled || contextValidation !== null,
1388
+ onClick: saveContext }, t(phase === 'saving-context' ? 'context.saving' : 'context.save')) : null)) : null) : null, statusError !== null
1389
+ ? el('div', { className: 'oasub-notice error', role: 'alert' }, t(statusError), info !== null ? ' ' + t('status.stale') : null, el('button', { type: 'button', className: 'oasub-button quiet', disabled: statusLoading, onClick: reloadStatus }, t('action.retry'))) : null, el('section', { className: 'oasub-card oasub-management-card', 'aria-label': t('action.more') }, el('h3', { className: 'oasub-management-heading' }, el('button', { ref: reloadRef, type: 'button', className: 'oasub-management-toggle',
1390
+ 'aria-label': t(managementOpen ? 'action.less' : 'action.more'),
1391
+ 'aria-expanded': managementOpen, 'aria-controls': 'oasub-management',
1392
+ onClick: () => setManagementOpen((open) => !open) }, el('span', { className: 'oasub-management-symbol' }, uiIcon('settings', 18)), el('span', { className: 'oasub-section-copy' }, el('span', { className: 'oasub-status-title' }, t('action.more')), el('span', { className: 'oasub-status-detail' }, t('manage.help'))), uiIcon('chevron'))), el('div', { id: 'oasub-management', className: 'oasub-management', hidden: !managementOpen }, managementOpen ? [
1393
+ configured && info?.credentialState === 'valid' ? el('div', { key: 'refresh', className: 'oasub-manage-row' }, el('div', { className: 'oasub-section-copy' }, el('div', { className: 'oasub-manage-title' }, t(info.refreshable ? 'action.refresh' : 'action.reconnect')), el('div', { className: 'oasub-status-detail', id: 'oasub-refresh-help' }, t(info.refreshable ? 'manage.refresh.help' : 'manage.reconnect.help'))), el('button', { type: 'button', className: 'oasub-button', disabled: busy || !ready,
1394
+ 'aria-describedby': 'oasub-refresh-help',
1395
+ onClick: () => startAuthorization(info.refreshable ? 'refresh' : 'device_code') }, phase === 'starting' || phase === 'authorizing' ? t('action.refreshing') : t(info.refreshable ? 'action.refresh' : 'action.reconnect'))) : null,
1396
+ el('div', { key: 'reload', className: 'oasub-manage-row' }, el('div', { className: 'oasub-section-copy' }, el('div', { className: 'oasub-manage-title' }, t('action.reload')), el('div', { className: 'oasub-status-detail', id: 'oasub-reload-help' }, t('manage.reload.help'))), el('button', { type: 'button', className: 'oasub-button', disabled: statusLoading,
1397
+ 'aria-describedby': 'oasub-reload-help', onClick: reloadStatus }, t(statusLoading ? 'status.reloading' : 'action.reload'))),
1398
+ info?.cleanupAvailable ? el('div', { key: 'disconnect', className: 'oasub-manage-row' }, el('div', { className: 'oasub-section-copy' }, el('div', { className: 'oasub-manage-title' }, t('action.disconnect')), el('div', { className: 'oasub-status-detail', id: 'oasub-disconnect-help' }, t('manage.disconnect.help'))), el('button', { type: 'button', className: 'oasub-button danger-quiet', disabled: busy,
1399
+ 'aria-describedby': 'oasub-disconnect-help', onClick: () => openConfirm('disconnect') }, t(phase === 'disconnecting' ? 'action.disconnecting' : 'action.disconnect'))) : null,
1400
+ ] : null)), device !== null
1207
1401
  ? el('section', { className: 'oasub-card oasub-device', 'aria-label': t('device.title') }, el('div', { className: 'oasub-device-head' }, el('div', { className: 'oasub-status-title' }, t('device.title')), el('div', { className: 'oasub-status-detail' }, t('device.detail'))), el('input', { ref: codeRef, className: 'oasub-code', type: 'text', readOnly: true, value: device.code,
1208
1402
  'aria-label': t('device.code'), autoComplete: 'off', spellCheck: false, onFocus: (event) => event.currentTarget.select() }), el('div', { className: 'oasub-actions' }, device.url !== null
1209
1403
  ? el('a', { className: 'oasub-button primary', href: device.url, target: '_blank', rel: 'noopener noreferrer',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-openai-subscription",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
4
4
  "description": "ChatGPT subscription sign-in for DeepSeek Harness via OpenAI device authorization.",
5
5
  "type": "module",
6
6
  "packageManager": "pnpm@10.6.3",