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 +1 -1
- package/README.zh.md +1 -1
- package/dist/client.js +242 -48
- package/package.json +1 -1
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
/*
|
|
299
|
-
|
|
300
|
-
--oasub-control-border
|
|
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-
|
|
367
|
-
.oasub-
|
|
368
|
-
.oasub-
|
|
369
|
-
.oasub-
|
|
370
|
-
.oasub-context { display: flex; flex-direction: column; gap:
|
|
371
|
-
.oasub-
|
|
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-
|
|
374
|
-
.oasub-
|
|
375
|
-
|
|
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:
|
|
443
|
+
padding: 0 16px;
|
|
380
444
|
border: 1px solid var(--oasub-control-border);
|
|
381
|
-
border-radius:
|
|
445
|
+
border-radius: 12px;
|
|
382
446
|
color: var(--dsw-alias-label-primary, #0f1115);
|
|
383
447
|
background: transparent;
|
|
384
448
|
font: inherit;
|
|
385
|
-
font-size:
|
|
449
|
+
font-size: 14px;
|
|
386
450
|
font-weight: 500;
|
|
387
|
-
line-height:
|
|
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-
|
|
398
|
-
.oasub-button.primary:hover:not(:disabled) {
|
|
399
|
-
.oasub-button.danger { border-color: var(--dsw-alias-state-error-primary, #dc2626); color: var(--dsw-alias-label-primary-
|
|
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(--
|
|
489
|
+
border: 1px solid var(--oasub-control-border);
|
|
424
490
|
margin: 0;
|
|
425
491
|
padding: 10px 14px;
|
|
426
|
-
border-radius:
|
|
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)
|
|
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
|
-
|
|
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-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
value
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
'aria-
|
|
1202
|
-
|
|
1203
|
-
'
|
|
1204
|
-
onClick:
|
|
1205
|
-
'
|
|
1206
|
-
'aria-
|
|
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