@nimoxie/dsh-model-organizer 0.5.11 → 0.5.19

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/lib/client.js CHANGED
@@ -100,7 +100,7 @@ window.__ModuleLoader__.load({
100
100
  "page.capacityInvalid": "容量要写正数,可以用 K / M 后缀(如 256K、1M)。",
101
101
  "page.fetchModels": "获取可用模型",
102
102
  "page.fetching": "获取中…",
103
- "page.fetchNone": "这个路由的模型都已经在列表里了。",
103
+ "page.fetchNone": "这个路由的模型已经都在列表里了。",
104
104
  "page.fetchTitle": "选择要添加的模型",
105
105
  "page.fetchDescription": "以下是模型提供商的可用模型,勾选要添加的模型。",
106
106
  "page.fetchSearch": "搜索模型",
@@ -126,26 +126,34 @@ window.__ModuleLoader__.load({
126
126
  "page.keyOptional": "API 密钥(可留空,稍后在右栏填写)",
127
127
  "page.modelsField": "模型 ID(至少一个)",
128
128
  "page.modelsFieldPlaceholder": "例如 glm-4.6, qwen-max(多个用逗号或空格分隔)",
129
- "page.modelsFieldHint": "这个路由不在 DSH 的目录里,配置里必须先列出模型,否则它会解析不出模型。填好 API 地址与密钥后可点「从网关获取」自动填。",
129
+ "page.modelsFieldHint": "这个路由不在 DSH 的目录里,配置里必须先列出模型,否则它会解析不出模型。填好 API 地址与密钥后可以点「获取可用模型」从网关取一份清单勾选,也可以直接按 ID 添加。",
130
130
  "page.modelsRequired": "至少要填一个模型 ID:这个路由不在 DSH 的目录里,没有模型它无法解析。",
131
- "page.fetchFromGateway": "从网关获取",
132
- "page.baseUrlRequired": "「从网关获取」需要先填 API 地址。",
131
+ "page.fetchAvailable": "获取可用模型",
132
+ "page.modelAddById": "添加",
133
+ "page.modelListEmpty": "还没有模型:按 ID 输入后点「添加」,或点「获取可用模型」从网关取一份清单勾选。",
134
+ "page.addModeLabel": "添加方式",
135
+ "page.addModeCatalog": "第三方模型提供商",
136
+ "page.addModeCustom": "自定义模型 API",
137
+ "page.providerPreset": "提供商",
138
+ "page.presetPlaceholder": "选择内置目录里的提供商…",
139
+ "page.presetHint": "地址、协议和模型都由 DSH 内置目录提供,这里只需要密钥;添加后模型列表会显示目录里的模型(只读)。",
140
+ "page.presetRequired": "先选择一个提供商。",
141
+ "page.noPresets": "内置目录里的提供商都已经添加过了。",
142
+ "page.inheritedModels": "这些模型来自 DSH 内置目录(这条路由没有在配置里列出模型),只能看和测试,不能在这里改。",
143
+ "page.baseUrlRequired": "「获取可用模型」需要先填 API 地址。",
133
144
  "page.deleteProvider": "删除供应商",
134
145
  "page.deleteProviderHint": "删除这条自建路由:它的配置和本页派生的密钥引用会一起移除,无法撤销。官方路由不能在这里删除。",
135
146
  "page.deleteProviderBody": "删除后无法撤销:这条供应商的配置会被移除,本页为它派生的密钥引用也会一并清理(别处配置的密钥不会被删)。",
136
147
  "page.deleteFailed": "删除失败。",
137
148
  "page.test": "测试",
138
149
  "page.testing": "测试中…",
139
- "page.testOkShort": "✓ 可用",
140
- "page.testMissingShort": "⚠ 不在列表",
141
- "page.testFailShort": "✗ 失败",
142
- "page.testHint": "让宿主向该供应商询问模型列表,并检查这个模型 ID 是否在其中。自建路由会真的访问网关(用已保存的密钥),目录已描述的路由直接读目录。",
143
- "page.testOk": "网关可达,且它返回的模型列表里有这个 ID。",
144
- "page.testMissing": "网关可达,但它返回的模型列表里没有这个 ID(可能拼错,或网关没暴露该模型)。",
150
+ "page.testHint": "测一下这个模型现在能不能用",
151
+ "page.testOk": "可用",
152
+ "page.testMissing": "网关可达,但列表里没有这个 ID",
145
153
  "page.testFailed": "测试失败。",
146
- "page.testNoCatalog": "这条路由没有目录、也没有填 API 地址,无法询问网关:先在「API 地址」里填上它的地址再测。",
154
+ "page.testNoCatalog": "没有 API 地址,无法测试",
147
155
  "page.deleteModel": "删除模型",
148
- "page.testNeedsId": "先填模型 ID 再测试。",
156
+ "page.testNeedsId": "先填模型 ID",
149
157
  "page.deleteModelHint": "从配置里删掉这个模型(每条路由至少要留一个)",
150
158
  "page.modelDeleted": "模型已删除",
151
159
  "page.openOfficial": "官方模型页",
@@ -286,26 +294,34 @@ window.__ModuleLoader__.load({
286
294
  "page.keyOptional": "API key (leave empty, fill it on the right later)",
287
295
  "page.modelsField": "Model IDs (at least one)",
288
296
  "page.modelsFieldPlaceholder": "e.g. glm-4.6, qwen-max (comma or space separated)",
289
- "page.modelsFieldHint": "DSH's catalog does not describe this route, so the config must list its models — otherwise the route resolves no models. Fill the API address and key, then use Fetch from gateway.",
297
+ "page.modelsFieldHint": "DSH's catalog does not describe this route, so the config must list its models — otherwise the route resolves no models. Fill the API address and key, then use Fetch available models, or add ids by hand.",
290
298
  "page.modelsRequired": "List at least one model ID: the catalog does not describe this route, so it cannot resolve without models.",
291
- "page.fetchFromGateway": "Fetch from gateway",
292
- "page.baseUrlRequired": "Fetch from gateway needs the API address first.",
299
+ "page.fetchAvailable": "Fetch available models",
300
+ "page.modelAddById": "Add",
301
+ "page.modelListEmpty": "No models yet: type an ID and press Add, or use Fetch available models to pick from the gateway.",
302
+ "page.addModeLabel": "Add mode",
303
+ "page.addModeCatalog": "Third-party provider",
304
+ "page.addModeCustom": "Custom model API",
305
+ "page.providerPreset": "Provider",
306
+ "page.presetPlaceholder": "Pick a provider from the built-in catalog…",
307
+ "page.presetHint": "The address, protocol and models come from DSH's built-in catalog — only the key is needed. After adding, its models appear in the list (read-only).",
308
+ "page.presetRequired": "Pick a provider first.",
309
+ "page.noPresets": "Every provider in the built-in catalog is already added.",
310
+ "page.inheritedModels": "These models come from DSH's built-in catalog (this route lists none in the configuration); they can be viewed and tested here, not edited.",
311
+ "page.baseUrlRequired": "Fetch available models needs the API address first.",
293
312
  "page.deleteProvider": "Delete provider",
294
313
  "page.deleteProviderHint": "Delete this hand-declared route: its profile and the credential reference this page derived go with it, and it cannot be undone. Built-in routes cannot be deleted here.",
295
314
  "page.deleteProviderBody": "This cannot be undone: the provider's profile is removed and the credential reference this page derived is cleaned up (a key configured elsewhere is left alone).",
296
315
  "page.deleteFailed": "Deleting failed.",
297
316
  "page.test": "Test",
298
317
  "page.testing": "Testing…",
299
- "page.testOkShort": "✓ reachable",
300
- "page.testMissingShort": "⚠ not listed",
301
- "page.testFailShort": "✗ failed",
302
- "page.testHint": "Asks the host which models this provider serves and checks whether this model ID is among them. A hand-declared route really calls the gateway (with the stored key); a catalog route answers from the catalog.",
303
- "page.testOk": "The gateway answered and lists this ID.",
304
- "page.testMissing": "The gateway answered, but its model list does not contain this ID (typo, or the gateway does not expose it).",
318
+ "page.testHint": "Check whether this model works right now",
319
+ "page.testOk": "Works",
320
+ "page.testMissing": "Gateway reachable, but this ID is not listed",
305
321
  "page.testFailed": "The test failed.",
306
- "page.testNoCatalog": "This route has no catalog and no API address, so the gateway cannot be asked: fill in the API address first.",
322
+ "page.testNoCatalog": "No API address — cannot test",
307
323
  "page.deleteModel": "Delete model",
308
- "page.testNeedsId": "Give the model an ID before testing it.",
324
+ "page.testNeedsId": "Enter a model ID first",
309
325
  "page.deleteModelHint": "Remove this model from the configuration (every route keeps at least one)",
310
326
  "page.modelDeleted": "Model deleted",
311
327
  "page.openOfficial": "Official Models page",
@@ -377,7 +393,7 @@ window.__ModuleLoader__.load({
377
393
  code changes, so a browser can keep running an old build until a hard reload.
378
394
  Kept OFF the UI (it is not user-facing): read it as
379
395
  `window.__dshModelOrganizerBuild` when a stale build is suspected. */
380
- const BUILD = "b65";
396
+ const BUILD = "b82";
381
397
 
382
398
  let officialClasses = null;
383
399
  let officialClassesSheets = -1;
@@ -470,8 +486,11 @@ window.__ModuleLoader__.load({
470
486
  ".dsh-mo-head:focus-visible{outline:none;background:var(--dsw-alias-interactive-bg-hover);}",
471
487
  ".dsh-mo-list .dsh-mo-head.dsh-mo-head:focus-visible,.dsh-mo-list .dsh-mo-model.dsh-mo-model:focus-visible,.dsh-mo-list .dsh-mo-row.dsh-mo-row:focus-visible{background:transparent;}",
472
488
  ".dsh-mo-panelhead.dsh-mo-panelhead:focus-visible{background:var(--dsw-alias-interactive-bg-hover);}",
473
- /* Ghost icon actions (row delete / model test): the shipped kit's icons inherit
474
- currentColor, so the state lives entirely in these three rules. */
489
+ /* Icon actions (row delete / model test): the kit's icons inherit
490
+ currentColor, so their colour state lives in these rules. */
491
+ /* A native select paints its own popup; colour-scheme keeps that popup dark. */
492
+ ".dsh-mo-select{background:var(--dsw-alias-bg-module, rgba(127,127,127,.08));color:var(--dsw-alias-label-primary);border:1px solid var(--dsw-alias-border-l1);border-radius:7px;padding:4px 8px;font-size:12px;color-scheme:dark;}",
493
+ ".dsh-mo-select option{background:var(--dsw-alias-bg-module, #1f1f1f);color:var(--dsw-alias-label-primary);}",
475
494
  ".dsh-mo-act{color:var(--dsw-alias-label-secondary);transition:background .12s ease,color .12s ease;}",
476
495
  ".dsh-mo-act:hover{background:var(--dsw-alias-interactive-bg-hover);}",
477
496
  ".dsh-mo-act:disabled{opacity:.45;cursor:not-allowed;background:transparent;}",
@@ -529,6 +548,35 @@ window.__ModuleLoader__.load({
529
548
  ".dsh-mo-model::before{content:'';position:absolute;left:15px;top:50%;width:5px;height:5px;margin-top:-2.5px;border-radius:50%;background:var(--dsw-alias-label-tertiary);opacity:.5;}",
530
549
  ".dsh-mo-model[aria-checked='true']::before{background:var(--dsw-alias-label-primary);opacity:.9;}"
531
550
  ];
551
+
552
+ /* Every button on this page, in its dialogs, and in the footer panel shares one
553
+ pointer state. The resting paint is inline (the S.* objects), so the hover
554
+ paint has to outrank it: `!important` is confined to hover/disabled, and
555
+ :where() keeps the scope selector at zero specificity so the variant rules
556
+ below still win. */
557
+ const BTN_SURFACES = [
558
+ '[data-mo="page"] button', '[data-mo="panel"] button',
559
+ '[data-mo="fetch-dialog"] button', '[data-mo="add-dialog"] button',
560
+ '[data-mo="model-dialog"] button', '[data-mo="provider-delete-dialog"] button'
561
+ ];
562
+ const BTN = ":where(" + BTN_SURFACES.join(",") + ")";
563
+ const byMarker = function (markers) {
564
+ return ":where(" + markers.map(function (marker) { return "[data-mo='" + marker + "']"; }).join(",") + ")";
565
+ };
566
+ rules.push(BTN + "{transition:background .12s ease,border-color .12s ease,color .12s ease,filter .12s ease,box-shadow .12s ease;}");
567
+ rules.push(BTN + ":not([role='switch']):not(:disabled):hover{background:var(--dsw-alias-interactive-bg-hover, rgba(127,127,127,.20))!important;}");
568
+ rules.push(BTN + ":not(:disabled):active{filter:brightness(.96);}");
569
+ rules.push(BTN + ":disabled{cursor:not-allowed;opacity:.55;}");
570
+ /* A switch paints its own on/off fill: repainting it on hover would read as a
571
+ state change, so it gets a ring instead. */
572
+ rules.push(BTN + ":is([role='switch']):not(:disabled):hover{box-shadow:0 0 0 2px color-mix(in srgb, var(--dsw-alias-state-accent, #4f8cff) 40%, transparent);}");
573
+ /* Primary actions keep their accent fill, destructive ones tint the danger
574
+ colour, and the two quiet cross-links accent their frame. */
575
+ rules.push(byMarker(["page-key-save", "fetch-adopt", "add-submit", "model-dialog-save"]) + ":not(:disabled):hover{background:var(--dsw-alias-state-accent, #4f8cff)!important;filter:brightness(1.12);}");
576
+ rules.push(byMarker(["page-model-delete", "page-provider-delete", "model-dialog-delete", "provider-delete-confirm"]) + ":not(:disabled):hover{background:color-mix(in srgb, var(--dsw-alias-state-error, #f85149) 18%, transparent)!important;border-color:var(--dsw-alias-state-error, #f85149)!important;}");
577
+ rules.push(byMarker(["page-open-official", "page-provider-add"]) + ":not(:disabled):hover{color:var(--dsw-alias-label-primary)!important;border-color:var(--dsw-alias-state-accent, #4f8cff)!important;}");
578
+ rules.push(BTN + "[role='radio'][aria-checked='true']:not(:disabled):hover{filter:brightness(1.12);}");
579
+ rules.push(BTN + "[data-mo$='-close']:not(:disabled):hover{color:var(--dsw-alias-label-primary)!important;}");
532
580
  /* The shipped check alone is easy to miss: bold the selected row's name
533
581
  wherever the official sheet is in use. Settings rows carry no
534
582
  aria-checked, so the drag lists are untouched. */
@@ -564,6 +612,30 @@ window.__ModuleLoader__.load({
564
612
  return h("span", { "aria-hidden": "true", style: { fontSize: size - 3, lineHeight: 1 } }, glyph);
565
613
  }
566
614
 
615
+ /* One box for the three icon actions: the provider head's delete and each model
616
+ row's test / delete. Height is the switch's (17) on purpose — a taller icon
617
+ button beside the switch is what the page screenshot called out. Width is a
618
+ nudge above the proportional shrink (the same screenshot then called 20 a hair
619
+ too narrow); the height stays equal to the switch so the row never looks taller. */
620
+ const ACTION_BOX = {
621
+ boxSizing: "border-box", flex: "none", width: 19, height: 17, borderRadius: 6,
622
+ /* The detail head aligns its TEXT on the baseline; an icon-only box has no real
623
+ text baseline, so it must opt into center alignment or it floats a pixel or two
624
+ above the switch (the cropped screenshot that showed them out of line). */
625
+ alignSelf: "center",
626
+ display: "inline-flex", alignItems: "center", justifyContent: "center", padding: 0,
627
+ borderWidth: 1, borderStyle: "solid", borderColor: "var(--dsw-alias-border-l1)",
628
+ background: "transparent", cursor: "pointer"
629
+ };
630
+ /* The destructive sibling: identical geometry, danger frame. */
631
+ const DELETE_ACTION = Object.assign({}, ACTION_BOX, { borderColor: "var(--dsw-alias-state-error, #f85149)" });
632
+ /* The diagnostic button is a NORMAL button to the eye: same box as the two above,
633
+ but it wears the page buttons' tinted fill (S.pageButton) instead of a transparent
634
+ frame. Its verdict stays in the icon colour (.dsh-mo-act-ok/-miss/-danger). */
635
+ const TEST_ACTION = Object.assign({}, ACTION_BOX, {
636
+ background: "var(--dsw-alias-interactive-bg, rgba(127,127,127,.10))"
637
+ });
638
+
567
639
  /* --------------------------------- styles --------------------------------- */
568
640
  const S = {
569
641
  chip: {
@@ -676,15 +748,6 @@ window.__ModuleLoader__.load({
676
748
  background: "var(--dsw-alias-interactive-bg, rgba(127,127,127,.10))",
677
749
  borderWidth: 1, borderStyle: "solid", borderColor: "var(--dsw-alias-border-l1)"
678
750
  },
679
- switchLabel: {
680
- display: "inline-flex", alignItems: "center", gap: 6, fontSize: 11,
681
- color: "var(--dsw-alias-label-secondary)", cursor: "pointer", userSelect: "none"
682
- },
683
- linkPillOn: {
684
- color: "var(--dsw-alias-label-primary)",
685
- background: "var(--dsw-alias-interactive-bg-hover, rgba(127,127,127,.20))",
686
- borderColor: "var(--dsw-alias-state-accent, #4f8cff)"
687
- },
688
751
  pageTitle: { margin: 0, fontSize: 16, fontWeight: 600, color: "var(--dsw-alias-label-primary)" },
689
752
  pageSubtitle: { margin: 0, fontSize: 12, lineHeight: "18px", color: "var(--dsw-alias-label-tertiary)" },
690
753
  pageGrid: { display: "grid", gridTemplateColumns: "minmax(170px, 210px) minmax(0, 1fr)", gap: 14, alignItems: "start" },
@@ -706,21 +769,10 @@ window.__ModuleLoader__.load({
706
769
  pageDetailHead: { display: "flex", alignItems: "baseline", gap: 8, flexWrap: "wrap" },
707
770
  pageDetailTitle: { margin: 0, fontSize: 15, fontWeight: 600 },
708
771
  pageDetailRoute: { fontSize: 11, color: "var(--dsw-alias-label-tertiary)" },
709
- pageFacts: { display: "flex", flexDirection: "column", gap: 6 },
710
- pageFact: { display: "grid", gridTemplateColumns: "84px minmax(0, 1fr)", gap: 10, alignItems: "baseline", fontSize: 12 },
711
- pageFactLabel: { color: "var(--dsw-alias-label-tertiary)" },
712
- pageFactValue: { color: "var(--dsw-alias-label-secondary)", wordBreak: "break-all" },
713
772
  pageSectionHead: { display: "flex", alignItems: "baseline", justifyContent: "space-between", gap: 8 },
714
773
  pageSectionLabel: { fontSize: 12, fontWeight: 600 },
715
774
  pageHint: { fontSize: 10, color: "var(--dsw-alias-label-tertiary)" },
716
775
  pageEmpty: { margin: 0, fontSize: 12, lineHeight: "18px", color: "var(--dsw-alias-label-tertiary)" },
717
- pageModels: { display: "flex", flexDirection: "column", gap: 4 },
718
- pageModelRow: {
719
- display: "flex", alignItems: "center", gap: 8, minWidth: 0, padding: "5px 8px", borderRadius: 8,
720
- fontSize: 12, background: "var(--dsw-alias-interactive-bg, rgba(127,127,127,.06))"
721
- },
722
- pageModelId: { minWidth: 0, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", fontFamily: "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" },
723
- pageModelName: { minWidth: 0, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", color: "var(--dsw-alias-label-tertiary)" },
724
776
  pageSpacer: { flex: "1 1 auto", minWidth: 0 },
725
777
  pageChip: { flex: "none", padding: "1px 6px", borderRadius: 6, fontSize: 10, border: "1px solid var(--dsw-alias-border-l1)", color: "var(--dsw-alias-label-tertiary)" },
726
778
  pageLoading: { margin: 0, fontSize: 12, color: "var(--dsw-alias-label-tertiary)" },
@@ -773,8 +825,6 @@ window.__ModuleLoader__.load({
773
825
  outline: "none"
774
826
  },
775
827
  pageInputMono: { flex: "1 1 auto", minWidth: 0, fontFamily: "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" },
776
- pageModelTop: { display: "flex", alignItems: "center", gap: 6, minWidth: 0 },
777
- pageModelFields: { display: "grid", gridTemplateColumns: "92px 92px auto", gap: 6, alignItems: "center", paddingLeft: 18 },
778
828
  /* A native <select> popup is painted by the platform: in this theme its list came
779
829
  out white-on-white. Three radio-style segments are painted by us, so the choice
780
830
  is legible whatever the platform does. */
@@ -782,6 +832,9 @@ window.__ModuleLoader__.load({
782
832
  "vision", but a switch reads as an on/off state at a glance. */
783
833
  switchTrack: {
784
834
  boxSizing: "border-box", flex: "none", width: 30, height: 17, padding: 2, borderRadius: 9,
835
+ /* Same reason as ACTION_BOX: the knob gives this flex box a baseline that is
836
+ NOT its box bottom, so baseline rows would offset it from the icon button. */
837
+ alignSelf: "center",
785
838
  display: "inline-flex", alignItems: "center", justifyContent: "flex-start", cursor: "pointer",
786
839
  background: "var(--dsw-alias-interactive-bg, rgba(127,127,127,.25))",
787
840
  borderWidth: 1, borderStyle: "solid", borderColor: "var(--dsw-alias-border-l1)",
@@ -817,12 +870,6 @@ window.__ModuleLoader__.load({
817
870
  background: "var(--dsw-alias-interactive-bg-hover, rgba(127,127,127,.20))",
818
871
  borderColor: "var(--dsw-alias-state-accent, #4f8cff)"
819
872
  },
820
- pageSelect: {
821
- boxSizing: "border-box", padding: "4px 8px", borderRadius: 7, fontSize: 12,
822
- color: "var(--dsw-alias-label-primary)",
823
- background: "var(--dsw-alias-bg-module, rgba(127,127,127,.08))",
824
- borderWidth: 1, borderStyle: "solid", borderColor: "var(--dsw-alias-border-l1)"
825
- },
826
873
  pageButton: {
827
874
  boxSizing: "border-box", flex: "none", padding: "4px 10px", borderRadius: 7, fontSize: 12, cursor: "pointer",
828
875
  color: "var(--dsw-alias-label-secondary)",
@@ -837,36 +884,13 @@ window.__ModuleLoader__.load({
837
884
  },
838
885
  /* Row-level actions and the one destructive action on this page. */
839
886
  pageModelActions: { flex: "none", display: "flex", alignItems: "center", gap: 4 },
840
- /* Row actions are the shipped ghost icon buttons: no border, one height, colour comes
841
- from the icon's own state (see .dsh-mo-act-* in the injected sheet). */
842
- pageIconAction: {
843
- boxSizing: "border-box", flex: "none", width: 26, height: 22, borderRadius: 7,
844
- display: "inline-flex", alignItems: "center", justifyContent: "center", padding: 0,
845
- borderWidth: 1, borderStyle: "solid", borderColor: "var(--dsw-alias-border-l1)",
846
- background: "transparent", cursor: "pointer"
847
- },
848
- pageIconActionHead: {
849
- boxSizing: "border-box", flex: "none", width: 22, height: 17, borderRadius: 6,
850
- display: "inline-flex", alignItems: "center", justifyContent: "center", padding: 0,
851
- borderWidth: 1, borderStyle: "solid", borderColor: "var(--dsw-alias-border-l1)",
852
- background: "transparent", cursor: "pointer"
853
- },
887
+ /* The diagnostic action: same box as the trash, but it wears the normal page
888
+ button's tinted fill; its verdict colour rides the icon (.dsh-mo-act-*), never
889
+ the frame. */
890
+ pageIconAction: TEST_ACTION,
891
+ /* The one trash box; the provider head and the model rows share this object. */
892
+ pageIconDelete: DELETE_ACTION,
854
893
 
855
- pageButtonCompact: {
856
- boxSizing: "border-box", flex: "none", padding: "1px 7px", borderRadius: 6, fontSize: 10, cursor: "pointer",
857
- borderWidth: 1, borderStyle: "solid", borderColor: "var(--dsw-alias-border-l1)",
858
- background: "transparent", color: "var(--dsw-alias-label-secondary)"
859
- },
860
- pageButtonOk: {
861
- boxSizing: "border-box", flex: "none", padding: "1px 7px", borderRadius: 6, fontSize: 10, cursor: "pointer",
862
- borderWidth: 1, borderStyle: "solid", borderColor: "var(--dsw-alias-state-success, #3fb950)",
863
- background: "transparent", color: "var(--dsw-alias-state-success, #3fb950)"
864
- },
865
- pageButtonWarn: {
866
- boxSizing: "border-box", flex: "none", padding: "1px 7px", borderRadius: 6, fontSize: 10, cursor: "pointer",
867
- borderWidth: 1, borderStyle: "solid", borderColor: "var(--dsw-alias-state-warn-label, #d29922)",
868
- background: "transparent", color: "var(--dsw-alias-state-warn-label, #d29922)"
869
- },
870
894
  pageButtonDanger: {
871
895
  boxSizing: "border-box", flex: "none", padding: "4px 10px", borderRadius: 7, fontSize: 12, cursor: "pointer",
872
896
  borderWidth: 1, borderStyle: "solid", borderColor: "var(--dsw-alias-state-error, #f85149)",
@@ -877,9 +901,17 @@ window.__ModuleLoader__.load({
877
901
  cursor: "pointer", color: "var(--dsw-alias-label-tertiary)", background: "transparent",
878
902
  borderWidth: 1, borderStyle: "solid", borderColor: "transparent"
879
903
  },
904
+ /* One line, never wrapped: the control on the right must stay beside its input. */
905
+ pageInlineRow: { display: "flex", alignItems: "center", gap: 6, flexWrap: "nowrap", minWidth: 0 },
906
+ pageInputGrow: {
907
+ boxSizing: "border-box", flex: "1 1 auto", width: "auto", minWidth: 0, padding: "4px 8px", borderRadius: 7, fontSize: 12,
908
+ color: "var(--dsw-alias-label-primary)",
909
+ background: "var(--dsw-alias-bg-module, rgba(127,127,127,.08))",
910
+ borderWidth: 1, borderStyle: "solid", borderColor: "var(--dsw-alias-border-l1)",
911
+ outline: "none"
912
+ },
880
913
  pageKeyRow: { display: "flex", alignItems: "center", gap: 6, flexWrap: "wrap" },
881
914
  pageActions: { display: "flex", alignItems: "center", gap: 8, flexWrap: "wrap" },
882
- pageHandle: { flex: "none", color: "var(--dsw-alias-label-tertiary)", fontSize: 12, userSelect: "none" },
883
915
  pageCheckLabel: { flex: "none", display: "inline-flex", alignItems: "center", gap: 4, fontSize: 11, color: "var(--dsw-alias-label-tertiary)", cursor: "pointer" },
884
916
  /* Built-in rail rows: readable, obviously not ours to edit, and never draggable
885
917
  (they sit outside the drag list for exactly that reason). */
@@ -2784,6 +2816,11 @@ window.__ModuleLoader__.load({
2784
2816
  const cardRef = useRef(null);
2785
2817
  const restoreRef = useRef(null);
2786
2818
  const closeRef = useRef(props.onClose);
2819
+ /* Every dialog is movable by its header: these panels cover the page, and you oft
2820
+ need to look at what is underneath while they are open. */
2821
+ const offsetState = useState({ x: 0, y: 0 });
2822
+ const offset = offsetState[0];
2823
+ const setOffset = offsetState[1];
2787
2824
  closeRef.current = props.onClose;
2788
2825
 
2789
2826
  useEffect(function () {
@@ -2878,6 +2915,31 @@ window.__ModuleLoader__.load({
2878
2915
  trapTab(event, event.currentTarget);
2879
2916
  }
2880
2917
 
2918
+ function onCardMouseDown(event) {
2919
+ if (event === null || event === undefined) return;
2920
+ if (event.button !== undefined && event.button !== 0) return;
2921
+ const node = event.target;
2922
+ if (node === null || node === undefined || typeof node.closest !== "function") return;
2923
+ /* Only the header strip drags, and never a control inside it. */
2924
+ if (node.closest("button, a, input, select, textarea, [data-mo-no-drag]") !== null) return;
2925
+ if (node.closest("[data-mo-drag]") === null) return;
2926
+ if (typeof event.preventDefault === "function") event.preventDefault();
2927
+ const from = { x: offset.x, y: offset.y };
2928
+ const startX = event.clientX;
2929
+ const startY = event.clientY;
2930
+ const move = function (moveEvent) {
2931
+ setOffset({ x: from.x + (moveEvent.clientX - startX), y: from.y + (moveEvent.clientY - startY) });
2932
+ };
2933
+ const stop = function () {
2934
+ try { document.removeEventListener("mousemove", move, true); } catch (e) {}
2935
+ try { document.removeEventListener("mouseup", stop, true); } catch (e) {}
2936
+ };
2937
+ try {
2938
+ document.addEventListener("mousemove", move, true);
2939
+ document.addEventListener("mouseup", stop, true);
2940
+ } catch (e) {}
2941
+ }
2942
+
2881
2943
  return h("div", {
2882
2944
  ref: cardRef,
2883
2945
  role: "dialog",
@@ -2886,7 +2948,8 @@ window.__ModuleLoader__.load({
2886
2948
  "data-mo": props.marker,
2887
2949
  tabIndex: -1,
2888
2950
  onKeyDown: onKeyDown,
2889
- style: S.pickCard
2951
+ onMouseDown: onCardMouseDown,
2952
+ style: Object.assign({}, S.pickCard, { transform: "translate(" + offset.x + "px, " + offset.y + "px)" })
2890
2953
  }, props.children);
2891
2954
  }
2892
2955
 
@@ -3049,6 +3112,52 @@ window.__ModuleLoader__.load({
3049
3112
  return function () { live = false; };
3050
3113
  }, [bridge.llm]);
3051
3114
 
3115
+ /* A route whose profile lists no models may still serve some: an adapter that
3116
+ ships a catalog (every built-in preset) answers discovery without a network call.
3117
+ Those rows are shown read-only — they live in the catalog, not in this document. */
3118
+ const catalogState = useState({});
3119
+ const catalogRows = catalogState[0];
3120
+ const setCatalogRows = catalogState[1];
3121
+
3122
+ useEffect(function () {
3123
+ const llm = bridge.llm;
3124
+ if (llm === undefined || llm === null || current === null) return undefined;
3125
+ /* An explicit models list always wins: never show catalog rows beside it. */
3126
+ if (storedModels.length > 0) return undefined;
3127
+ if (catalogRows[current] !== undefined) return undefined;
3128
+ let live = true;
3129
+ const mark = function (status, models) {
3130
+ if (!live) return;
3131
+ const next = Object.assign({}, catalogRows);
3132
+ next[current] = { status: status, models: models };
3133
+ setCatalogRows(next);
3134
+ };
3135
+ mark("loading", []);
3136
+ try {
3137
+ Promise.resolve(llm.discoverModels(namespace, { provider: current })).then(function (response) {
3138
+ if (!response || !response.ok) { mark("error", []); return; }
3139
+ const found = Array.isArray(response.value) ? response.value : [];
3140
+ const rows = [];
3141
+ for (let i = 0; i < found.length; i++) {
3142
+ const model = found[i];
3143
+ if (model === null || typeof model !== "object") continue;
3144
+ const id = typeof model.id === "string" ? model.id.trim() : "";
3145
+ if (id === "") continue;
3146
+ rows.push({
3147
+ id: id,
3148
+ name: typeof model.name === "string" ? model.name : "",
3149
+ context: model.contextWindow === undefined ? "" : formatCapacity(model.contextWindow),
3150
+ maxTokens: model.maxTokens === undefined ? "" : formatCapacity(model.maxTokens),
3151
+ vision: Array.isArray(model.inputModalities) && model.inputModalities.indexOf("image") !== -1,
3152
+ reasoning: { mode: "inherit", levels: [] }
3153
+ });
3154
+ }
3155
+ mark("ready", rows);
3156
+ }, function () { mark("error", []); });
3157
+ } catch (e) { mark("error", []); }
3158
+ return function () { live = false; };
3159
+ }, [current, storedModels.length]);
3160
+
3052
3161
  if (view === undefined || view === null) {
3053
3162
  return h("section", { "data-mo": "page", "data-mo-build": BUILD, style: S.pageRoot },
3054
3163
  h("p", { style: S.pageLoading }, t("page.loading")));
@@ -3310,6 +3419,36 @@ window.__ModuleLoader__.load({
3310
3419
  endpoint, protocol and one-shot credential, which is exactly what the discovery
3311
3420
  seam accepts (it never reads stored settings). Filling the ids here is what makes
3312
3421
  the create button able to satisfy "a declared route must list its models". */
3422
+ /* Draft models use the same "pick, then adopt" flow as the saved list: the picker
3423
+ only fills this dialog's list and nothing is written before Create. */
3424
+ function addModelsToDraft(rows, clearInput) {
3425
+ const next = addDialog.models.slice();
3426
+ const known = {};
3427
+ for (let i = 0; i < next.length; i++) known[String(next[i].id)] = true;
3428
+ for (let i = 0; i < rows.length; i++) {
3429
+ const id = String(rows[i] !== null && rows[i] !== undefined && rows[i].id !== undefined ? rows[i].id : "").trim();
3430
+ if (id === "" || known[id] === true) continue;
3431
+ known[id] = true;
3432
+ next.push(Object.assign({}, rows[i], { id: id }));
3433
+ }
3434
+ const patch = { models: next, error: "" };
3435
+ if (clearInput === true) patch.modelInput = "";
3436
+ setAdd(Object.assign({}, addDialog, patch));
3437
+ }
3438
+
3439
+ function addManualModels() {
3440
+ const ids = parseModelIds(addDialog.modelInput);
3441
+ if (ids.length === 0) return;
3442
+ addModelsToDraft(ids.map(function (id) { return { id: id, context: "", maxTokens: "", vision: false }; }), true);
3443
+ }
3444
+
3445
+ function removeDraftModel(id) {
3446
+ setAdd(Object.assign({}, addDialog, {
3447
+ models: addDialog.models.filter(function (row) { return String(row.id) !== String(id); }),
3448
+ error: ""
3449
+ }));
3450
+ }
3451
+
3313
3452
  function fetchAddModels() {
3314
3453
  const llm = bridge.llm;
3315
3454
  if (llm === undefined || llm === null) { setAdd(Object.assign({}, addDialog, { error: t("page.fetchUnavailable") })); return; }
@@ -3328,19 +3467,82 @@ window.__ModuleLoader__.load({
3328
3467
  return;
3329
3468
  }
3330
3469
  const found = Array.isArray(response.value) ? response.value : [];
3331
- const ids = parseModelIds(found.map(function (row) { return row !== null && row !== undefined && typeof row.id === "string" ? row.id : ""; }).join(","));
3332
- if (ids.length === 0) { setAdd(Object.assign({}, addDialog, { error: t("page.fetchEmpty") })); return; }
3333
- setAdd(Object.assign({}, addDialog, { models: ids.join(", "), error: "" }));
3470
+ const known = {};
3471
+ for (let k = 0; k < addDialog.models.length; k++) known[String(addDialog.models[k].id)] = true;
3472
+ const candidates = [];
3473
+ const picked = {};
3474
+ for (let k = 0; k < found.length; k++) {
3475
+ const model = found[k];
3476
+ if (model === null || typeof model !== "object") continue;
3477
+ const id = typeof model.id === "string" ? model.id.trim() : "";
3478
+ if (id === "" || known[id] === true) continue;
3479
+ known[id] = true;
3480
+ candidates.push({
3481
+ id: id,
3482
+ context: model.contextWindow === undefined ? "" : formatCapacity(model.contextWindow),
3483
+ maxTokens: model.maxTokens === undefined ? "" : formatCapacity(model.maxTokens),
3484
+ vision: Array.isArray(model.inputModalities) && model.inputModalities.indexOf("image") !== -1
3485
+ });
3486
+ picked[id] = true;
3487
+ }
3488
+ if (candidates.length === 0) { setAdd(Object.assign({}, addDialog, { error: t("page.fetchNone") })); return; }
3489
+ setPicker({ rows: candidates, picked: picked, query: "", target: "draft" });
3334
3490
  }, function (error) {
3335
3491
  setBusy("");
3336
3492
  setAdd(Object.assign({}, addDialog, { error: String((error && error.message) || error) }));
3337
3493
  });
3338
3494
  }
3339
3495
 
3496
+ /* Third-party presets: routes the shipped catalog already describes and this
3497
+ document has not configured yet. Creating one writes an EMPTY profile — its
3498
+ address, protocol and models come from the catalog, so no models list is needed. */
3499
+ const addablePresets = [];
3500
+ if (directory !== null) {
3501
+ for (let i = 0; i < directory.length; i++) {
3502
+ const entry = directory[i];
3503
+ if (entry === null || typeof entry !== "object") continue;
3504
+ if (typeof entry.provider !== "string" || entry.provider === "") continue;
3505
+ /* `declared` means hand-declared by the user; a catalog provider has false. */
3506
+ if (entry.declared === true) continue;
3507
+ if (!Array.isArray(entry.settingsPath) || entry.settingsPath.length === 0) continue;
3508
+ if (entry.settingsNs !== namespace) continue;
3509
+ if (providers[entry.provider] !== undefined) continue;
3510
+ addablePresets.push(entry);
3511
+ }
3512
+ }
3513
+
3514
+ function createCatalogProvider() {
3515
+ const preset = String(addDialog.preset || "");
3516
+ if (preset === "") { setAdd(Object.assign({}, addDialog, { error: t("page.presetRequired") })); return; }
3517
+ const known = addablePresets.some(function (entry) { return entry.provider === preset; });
3518
+ if (!known) { setAdd(Object.assign({}, addDialog, { error: t("page.routeTaken") })); return; }
3519
+ const displayName = addDialog.displayName.trim();
3520
+ const secret = addDialog.key.trim();
3521
+ const ref = deriveKeyRef(preset);
3522
+ const profile = {};
3523
+ if (displayName !== "") profile.displayName = displayName;
3524
+ if (secret !== "") profile.apiKeyEnv = ref;
3525
+ setBusy("create");
3526
+ const failed = function (error) {
3527
+ setBusy("");
3528
+ setAdd(Object.assign({}, addDialog, { error: String((error && error.message) || error || t("page.createFailed")) }));
3529
+ };
3530
+ Promise.resolve(mutate([{ op: "set", path: ["providers", preset], value: profile }], revision)).then(function (result) {
3531
+ if (!result || !result.ok) { failed((result && result.error && result.error.message) || t("page.createFailed")); return; }
3532
+ const done = function () { setBusy(""); setAdd(null); setPicked(preset); setField(null); };
3533
+ if (secret === "" || bridge.credentials === null || bridge.credentials === undefined) { done(); return; }
3534
+ Promise.resolve(bridge.credentials.set(ref, secret)).then(done, function (error) {
3535
+ setBusy("");
3536
+ setAdd(Object.assign({}, addDialog, { error: String((error && error.message) || error) }));
3537
+ });
3538
+ }, failed);
3539
+ }
3540
+
3340
3541
  /* A brand-new route: the profile and the key reference go in ONE mutate (one
3341
3542
  revision, so a concurrent edit cannot land between them), the secret goes to the
3342
3543
  credentials service, and the page then selects what it just created. */
3343
3544
  function createProvider() {
3545
+ if (addDialog.mode === "catalog") { createCatalogProvider(); return; }
3344
3546
  const route = addDialog.route.trim();
3345
3547
  if (route === "") { setAdd(Object.assign({}, addDialog, { error: t("page.routeRequired") })); return; }
3346
3548
  if (!/^[A-Za-z0-9._-]+$/.test(route)) { setAdd(Object.assign({}, addDialog, { error: t("page.routeInvalid") })); return; }
@@ -3350,9 +3552,19 @@ window.__ModuleLoader__.load({
3350
3552
  /* A hand-declared route has no catalog entry, so the host refuses a profile whose
3351
3553
  models list is empty ("resolves no models"). The dialog therefore requires the
3352
3554
  ids up front instead of creating a route that cannot resolve. */
3353
- const modelIds = parseModelIds(addDialog.models);
3354
- if (modelIds.length === 0) { setAdd(Object.assign({}, addDialog, { error: t("page.modelsRequired") })); return; }
3355
- const profile = { api: addDialog.api, models: modelIds.map(function (id) { return { id: id }; }) };
3555
+ if (addDialog.models.length === 0) { setAdd(Object.assign({}, addDialog, { error: t("page.modelsRequired") })); return; }
3556
+ const built = modelsFromDraft(addDialog.models.map(function (row) {
3557
+ return {
3558
+ id: row.id,
3559
+ name: row.name === undefined ? "" : row.name,
3560
+ context: row.context === undefined ? "" : row.context,
3561
+ maxTokens: row.maxTokens === undefined ? "" : row.maxTokens,
3562
+ vision: row.vision === true,
3563
+ reasoning: { mode: "inherit", levels: [] }
3564
+ };
3565
+ }), t);
3566
+ if (built.error !== undefined) { setAdd(Object.assign({}, addDialog, { error: built.error })); return; }
3567
+ const profile = { api: addDialog.api, models: built.models };
3356
3568
  if (displayName !== "") profile.displayName = displayName;
3357
3569
  if (baseURL !== "") profile.baseURL = baseURL;
3358
3570
  const secret = addDialog.key.trim();
@@ -3522,7 +3734,7 @@ window.__ModuleLoader__.load({
3522
3734
  disabled: !writable,
3523
3735
  title: t("page.addProvider"),
3524
3736
  onClick: function () {
3525
- setAdd({ route: "", displayName: "", baseURL: "", api: apiChoices[0], key: "", models: "", error: "" });
3737
+ setAdd({ route: "", displayName: "", baseURL: "", api: apiChoices[0], key: "", models: [], modelInput: "", mode: "custom", preset: "", error: "" });
3526
3738
  },
3527
3739
  style: S.pageAddButton
3528
3740
  }, "+ " + t("page.addProvider"))
@@ -3543,6 +3755,37 @@ window.__ModuleLoader__.load({
3543
3755
  return chips.join(" · ");
3544
3756
  }
3545
3757
 
3758
+ /* Catalog-served rows: visible, testable, but not ours to edit or reorder. */
3759
+ const inheritedFacts = current === null ? undefined : catalogRows[current];
3760
+ const inheritedModels = draftRows.length === 0 && inheritedFacts !== undefined && inheritedFacts.status === "ready" ? inheritedFacts.models : [];
3761
+ const inheritedList = inheritedModels.length === 0 ? null : h(React.Fragment, null,
3762
+ h("p", { "data-mo": "page-inherited-hint", style: S.pageHint }, t("page.inheritedModels")),
3763
+ h("div", { "data-mo": "page-inherited-list", style: { display: "flex", flexDirection: "column", gap: 2 } },
3764
+ inheritedModels.map(function (row) {
3765
+ const enabled = hidden.models[hiddenModelKey(current, row.id)] !== true;
3766
+ const mark = testStateOf(row);
3767
+ return h("div", { key: row.id, "data-mo": "page-model-inherited", "data-mo-id": row.id, style: S.row },
3768
+ h("span", {
3769
+ "data-mo": "page-model-dot",
3770
+ "data-mo-on": enabled ? "true" : "false",
3771
+ title: (enabled ? t("page.dotOn") : t("page.dotOff")) + " — " + t("page.dotHint"),
3772
+ style: enabled ? S.pageDot : Object.assign({}, S.pageDot, S.pageDotMuted)
3773
+ }),
3774
+ h("span", { style: S.pageRowName }, row.name !== "" ? row.name : row.id),
3775
+ h("span", { style: S.pageRowCount }, modelMeta(row)),
3776
+ h("span", { style: S.pageModelActions },
3777
+ h("button", {
3778
+ "data-mo": "page-model-test",
3779
+ "data-mo-id": row.id,
3780
+ "data-mo-state": mark,
3781
+ className: cx("dsh-mo-act", mark === "busy" ? "dsh-mo-act-busy" : "", mark === "ok" ? "dsh-mo-act-ok" : mark === "miss" ? "dsh-mo-act-miss" : mark === "fail" ? "dsh-mo-act-danger" : ""),
3782
+ "aria-label": t("page.test"),
3783
+ title: testMark(row) === undefined ? t("page.testHint") : testMark(row).message,
3784
+ onClick: function (event) { stopRowEvent(event); startSpinNow(event); testModel(row); },
3785
+ style: S.pageIconAction
3786
+ }, kitIcon(TEST_ICONS[mark], "▶", 14))))
3787
+ })));
3788
+
3546
3789
  const modelList = h(DragList, {
3547
3790
  ids: draftRows.map(function (row, index) { return "row-" + String(index); }),
3548
3791
  disabled: false,
@@ -3606,7 +3849,7 @@ window.__ModuleLoader__.load({
3606
3849
  onMouseDown: stopRowEvent,
3607
3850
  onClick: function (event) { stopRowEvent(event); startSpinNow(event); testModel(row); },
3608
3851
  style: S.pageIconAction
3609
- }, kitIcon(TEST_ICONS[testStateOf(row)], "▶", 14)),
3852
+ }, kitIcon(TEST_ICONS[testStateOf(row)], "▶", 13)),
3610
3853
  h("button", {
3611
3854
  "data-mo": "page-model-delete",
3612
3855
  type: "button",
@@ -3617,8 +3860,8 @@ window.__ModuleLoader__.load({
3617
3860
  onMouseDown: stopRowEvent,
3618
3861
  className: "dsh-mo-act dsh-mo-act-danger",
3619
3862
  onClick: function (event) { stopRowEvent(event); deleteModelRow(row); },
3620
- style: S.pageIconAction
3621
- }, kitIcon("IconTrashOutlineRegular", "🗑", 14)))
3863
+ style: S.pageIconDelete
3864
+ }, kitIcon("IconTrashOutlineRegular", "🗑", 13)))
3622
3865
  );
3623
3866
  }
3624
3867
  });
@@ -3645,7 +3888,7 @@ window.__ModuleLoader__.load({
3645
3888
  title: t("page.deleteProviderHint"),
3646
3889
  className: "dsh-mo-act dsh-mo-act-danger",
3647
3890
  onClick: function () { setDeleteTarget(current); },
3648
- style: S.pageIconActionHead
3891
+ style: S.pageIconDelete
3649
3892
  }, kitIcon("IconTrashOutlineRegular", "🗑", 13)),
3650
3893
  h(Switch, {
3651
3894
  marker: "page-provider-switch",
@@ -3744,7 +3987,9 @@ window.__ModuleLoader__.load({
3744
3987
  onClick: fetchModels,
3745
3988
  style: S.pageButton
3746
3989
  }, busy === "fetch" ? t("page.fetching") : t("page.fetchModels"))),
3747
- draftRows.length === 0 ? h("p", { style: S.pageEmpty }, t("page.modelsEmptyEdit")) : modelList,
3990
+ draftRows.length === 0
3991
+ ? (inheritedList !== null ? inheritedList : h("p", { style: S.pageEmpty }, t("page.modelsEmptyEdit")))
3992
+ : modelList,
3748
3993
  h("div", { style: S.pageActions },
3749
3994
  h("button", {
3750
3995
  "data-mo": "page-model-add",
@@ -3771,10 +4016,9 @@ window.__ModuleLoader__.load({
3771
4016
  h("div", {
3772
4017
  "data-mo": "fetch-dialog",
3773
4018
  style: S.pickBackdrop,
3774
- onMouseDown: function (event) { if (event.target === event.currentTarget) setPicker(null); }
3775
4019
  },
3776
4020
  h(DialogShell, { label: t("page.fetchTitle"), onClose: function () { setPicker(null); } },
3777
- h("div", { style: S.pickHead },
4021
+ h("div", { style: Object.assign({}, S.pickHead, { cursor: "move" }), "data-mo-drag": "true" },
3778
4022
  h("h3", { style: S.pickTitle }, t("page.fetchTitle")),
3779
4023
  h("button", {
3780
4024
  "data-mo": "fetch-close",
@@ -3791,7 +4035,7 @@ window.__ModuleLoader__.load({
3791
4035
  value: picker.query,
3792
4036
  placeholder: t("page.fetchSearch"),
3793
4037
  spellCheck: false,
3794
- onChange: function (event) { setPicker({ rows: picker.rows, picked: picker.picked, query: event.target.value }); },
4038
+ onChange: function (event) { setPicker(Object.assign({}, picker, { query: event.target.value })); },
3795
4039
  style: S.pageInputWide
3796
4040
  }),
3797
4041
  h("button", {
@@ -3800,7 +4044,7 @@ window.__ModuleLoader__.load({
3800
4044
  onClick: function () {
3801
4045
  const next = Object.assign({}, picker.picked);
3802
4046
  for (let i = 0; i < visible.length; i++) next[visible[i].id] = allPicked ? undefined : true;
3803
- setPicker({ rows: picker.rows, picked: next, query: picker.query });
4047
+ setPicker(Object.assign({}, picker, { picked: next }));
3804
4048
  },
3805
4049
  style: S.pageButton
3806
4050
  }, allPicked ? t("page.deselectAll") : t("page.selectAll"))),
@@ -3817,7 +4061,7 @@ window.__ModuleLoader__.load({
3817
4061
  onChange: function (event) {
3818
4062
  const next = Object.assign({}, picker.picked);
3819
4063
  next[row.id] = event.target.checked ? true : undefined;
3820
- setPicker({ rows: picker.rows, picked: next, query: picker.query });
4064
+ setPicker(Object.assign({}, picker, { picked: next }));
3821
4065
  }
3822
4066
  }),
3823
4067
  h("span", { style: S.pickId }, row.id),
@@ -3838,8 +4082,9 @@ window.__ModuleLoader__.load({
3838
4082
  const chosen = picker.rows.filter(function (row) { return picker.picked[row.id] === true; });
3839
4083
  setPicker(null);
3840
4084
  if (chosen.length === 0) return;
3841
- /* Adopting is a write, not a draft: the picked rows join the config
3842
- immediately, which is also what keeps the list honest. */
4085
+ /* Draft picker: fill the create dialog's list, write nothing yet.
4086
+ Saved picker: adopting IS the write, which keeps that list honest. */
4087
+ if (picker.target === "draft") { addModelsToDraft(chosen, false); return; }
3843
4088
  writeModels(draftRows.concat(chosen));
3844
4089
  },
3845
4090
  style: S.pageButtonPrimary
@@ -3847,6 +4092,32 @@ window.__ModuleLoader__.load({
3847
4092
  document.body);
3848
4093
  }
3849
4094
 
4095
+ /* ------------------------ the third-party preset dialog ------------------------ */
4096
+ /* The shipped catalog already describes these routes, so the only thing this page
4097
+ can add is the profile stub plus a key; models and address come from the catalog. */
4098
+ function addModeTabs() {
4099
+ const on = addDialog.mode === "catalog" ? "catalog" : "custom";
4100
+ const options = [{ id: "catalog", label: t("page.addModeCatalog") }, { id: "custom", label: t("page.addModeCustom") }];
4101
+ return h("div", { style: S.pageField },
4102
+ h("span", { style: S.pageFieldLabel }, t("page.addModeLabel")),
4103
+ h("div", { "data-mo": "add-mode", role: "radiogroup", "aria-label": t("page.addModeLabel"), style: S.pageSegments },
4104
+ options.map(function (option) {
4105
+ const active = option.id === on;
4106
+ return h("button", {
4107
+ key: option.id,
4108
+ "data-mo": "add-mode-option",
4109
+ "data-mo-id": option.id,
4110
+ type: "button",
4111
+ role: "radio",
4112
+ "aria-checked": active ? "true" : "false",
4113
+ disabled: option.id === "catalog" && addablePresets.length === 0,
4114
+ title: option.id === "catalog" && addablePresets.length === 0 ? t("page.noPresets") : "",
4115
+ onClick: function () { setAdd(Object.assign({}, addDialog, { mode: option.id, error: "" })); },
4116
+ style: active ? Object.assign({}, S.pageSegment, S.pageSegmentActive) : S.pageSegment
4117
+ }, option.label);
4118
+ })));
4119
+ }
4120
+
3850
4121
  /* ------------------------ the add-provider dialog ------------------------ */
3851
4122
  let addNode = null;
3852
4123
  if (addDialog !== null) {
@@ -3854,11 +4125,11 @@ window.__ModuleLoader__.load({
3854
4125
  addNode = ReactDOM.createPortal(
3855
4126
  h("div", {
3856
4127
  "data-mo": "add-dialog",
4128
+ "data-mo-mode": "custom",
3857
4129
  style: S.pickBackdrop,
3858
- onMouseDown: function (event) { if (event.target === event.currentTarget) setAdd(null); }
3859
4130
  },
3860
4131
  h(DialogShell, { label: t("page.newTitle"), onClose: function () { setAdd(null); } },
3861
- h("div", { style: S.pickHead },
4132
+ h("div", { style: Object.assign({}, S.pickHead, { cursor: "move" }), "data-mo-drag": "true" },
3862
4133
  h("h3", { style: S.pickTitle }, t("page.newTitle")),
3863
4134
  h("button", {
3864
4135
  "data-mo": "add-close",
@@ -3868,7 +4139,22 @@ window.__ModuleLoader__.load({
3868
4139
  style: S.pageIconButton
3869
4140
  }, "✕")),
3870
4141
  h("p", { style: S.pickDesc }, t("page.newDescription")),
3871
- h("label", { style: S.pageField },
4142
+ addModeTabs(),
4143
+ addDialog.mode === "catalog" ? h(React.Fragment, null,
4144
+ h("label", { style: S.pageField },
4145
+ h("span", { style: S.pageFieldLabel }, t("page.providerPreset")),
4146
+ h("select", {
4147
+ "data-mo": "add-preset",
4148
+ "data-mo-focus": "true",
4149
+ className: "dsh-mo-select",
4150
+ value: String(addDialog.preset || ""),
4151
+ onChange: function (event) { setAdd(Object.assign({}, addDialog, { preset: event.target.value, error: "" })); },
4152
+ style: Object.assign({}, S.pageInputGrow, { colorScheme: "dark" })
4153
+ }, [h("option", { key: "", value: "" }, t("page.presetPlaceholder"))].concat(addablePresets.map(function (entry) {
4154
+ return h("option", { key: entry.provider, value: entry.provider }, entry.displayName || entry.provider);
4155
+ })))),
4156
+ h("span", { style: S.pageHint }, t("page.presetHint"))) : null,
4157
+ addDialog.mode === "catalog" ? null : h("label", { style: S.pageField },
3872
4158
  h("span", { style: S.pageFieldLabel }, t("page.route")),
3873
4159
  h("input", {
3874
4160
  "data-mo": "add-route",
@@ -3889,7 +4175,7 @@ window.__ModuleLoader__.load({
3889
4175
  onChange: function (event) { setAdd(Object.assign({}, addDialog, { displayName: event.target.value, error: "" })); },
3890
4176
  style: S.pageInputWide
3891
4177
  })),
3892
- h("label", { style: S.pageField },
4178
+ addDialog.mode === "catalog" ? null : h("label", { style: S.pageField },
3893
4179
  h("span", { style: S.pageFieldLabel }, t("page.baseUrlOptional")),
3894
4180
  h("input", {
3895
4181
  "data-mo": "add-base-url",
@@ -3899,7 +4185,7 @@ window.__ModuleLoader__.load({
3899
4185
  onChange: function (event) { setAdd(Object.assign({}, addDialog, { baseURL: event.target.value, error: "" })); },
3900
4186
  style: S.pageInputWide
3901
4187
  })),
3902
- h("div", { style: S.pageField },
4188
+ addDialog.mode === "catalog" ? null : h("div", { style: S.pageField },
3903
4189
  h("span", { style: S.pageFieldLabel }, t("page.api")),
3904
4190
  h("div", { "data-mo": "add-api", role: "radiogroup", "aria-label": t("page.api"), style: S.pageSegments },
3905
4191
  apiChoices.map(function (choice) {
@@ -3927,17 +4213,24 @@ window.__ModuleLoader__.load({
3927
4213
  onKeyDown: function (event) { if (event.key === "Enter") createProvider(); },
3928
4214
  style: S.pageInputWide
3929
4215
  })),
3930
- h("label", { style: S.pageField },
4216
+ addDialog.mode === "catalog" ? null : h("div", { style: S.pageField },
3931
4217
  h("span", { style: S.pageFieldLabel }, t("page.modelsField")),
3932
- h("input", {
3933
- "data-mo": "add-models",
3934
- value: addDialog.models,
3935
- placeholder: t("page.modelsFieldPlaceholder"),
3936
- spellCheck: false,
3937
- onChange: function (event) { setAdd(Object.assign({}, addDialog, { models: event.target.value, error: "" })); },
3938
- onKeyDown: function (event) { if (event.key === "Enter") createProvider(); },
3939
- style: S.pageInputWide
3940
- }),
4218
+ h("div", { style: S.pageInlineRow },
4219
+ h("input", {
4220
+ "data-mo": "add-models",
4221
+ value: addDialog.modelInput,
4222
+ placeholder: t("page.modelsFieldPlaceholder"),
4223
+ spellCheck: false,
4224
+ onChange: function (event) { setAdd(Object.assign({}, addDialog, { modelInput: event.target.value, error: "" })); },
4225
+ onKeyDown: function (event) { if (event.key === "Enter") { event.preventDefault(); addManualModels(); } },
4226
+ style: S.pageInputGrow
4227
+ }),
4228
+ h("button", {
4229
+ "data-mo": "add-model-add",
4230
+ type: "button",
4231
+ onClick: addManualModels,
4232
+ style: S.pageButton
4233
+ }, t("page.modelAddById"))),
3941
4234
  h("div", { style: S.pageKeyRow },
3942
4235
  h("button", {
3943
4236
  "data-mo": "add-fetch",
@@ -3946,8 +4239,27 @@ window.__ModuleLoader__.load({
3946
4239
  title: t("page.modelsFieldHint"),
3947
4240
  onClick: fetchAddModels,
3948
4241
  style: S.pageButton
3949
- }, busy === "addfetch" ? t("page.fetching") : t("page.fetchFromGateway")),
3950
- h("span", { style: S.pageHint }, t("page.modelsFieldHint")))),
4242
+ }, busy === "addfetch" ? t("page.fetching") : t("page.fetchAvailable")),
4243
+ h("span", { style: S.pageHint }, t("page.modelsFieldHint"))),
4244
+ addDialog.models.length === 0
4245
+ ? h("p", { "data-mo": "add-models-empty", style: S.pageHint }, t("page.modelListEmpty"))
4246
+ : h("div", { "data-mo": "add-models-list", style: S.pickList }, addDialog.models.map(function (row) {
4247
+ const chips = [];
4248
+ if (row.context !== undefined && row.context !== "") chips.push(h("span", { key: "c", style: S.pageChip }, row.context));
4249
+ if (row.vision === true) chips.push(h("span", { key: "v", style: S.pageChip }, t("page.vision")));
4250
+ return h("div", { key: row.id, style: S.pickRow, "data-mo": "add-model-item", "data-mo-id": row.id },
4251
+ h("span", { style: S.pickId }, row.id),
4252
+ h("span", { style: S.pageSpacer }),
4253
+ chips,
4254
+ h("button", {
4255
+ "data-mo": "add-model-remove",
4256
+ "data-mo-id": row.id,
4257
+ type: "button",
4258
+ title: t("page.deleteModelHint"),
4259
+ onClick: function () { removeDraftModel(row.id); },
4260
+ style: S.pageIconAction
4261
+ }, kitIcon("IconTrashOutlineRegular", "🗑", 13)));
4262
+ }))),
3951
4263
  addDialog.error === "" ? null : h("p", { "data-mo": "add-error", style: S.pageError }, addDialog.error),
3952
4264
  h("div", { style: S.pickFoot },
3953
4265
  h("button", {
@@ -3959,7 +4271,7 @@ window.__ModuleLoader__.load({
3959
4271
  h("button", {
3960
4272
  "data-mo": "add-submit",
3961
4273
  type: "button",
3962
- disabled: busy === "create",
4274
+ disabled: busy === "create" || (addDialog.mode === "catalog" && String(addDialog.preset || "") === ""),
3963
4275
  onClick: createProvider,
3964
4276
  style: S.pageButtonPrimary
3965
4277
  }, busy === "create" ? t("page.working") : t("page.create"))))),
@@ -4052,10 +4364,9 @@ window.__ModuleLoader__.load({
4052
4364
  h("div", {
4053
4365
  "data-mo": "model-dialog",
4054
4366
  style: S.pickBackdrop,
4055
- onMouseDown: function (event) { if (event.target === event.currentTarget) closeModel(); }
4056
4367
  },
4057
4368
  h(DialogShell, { label: t("page.modelDialogTitle"), marker: "model-dialog-card", onClose: closeModel },
4058
- h("div", { style: S.pickHead },
4369
+ h("div", { style: Object.assign({}, S.pickHead, { cursor: "move" }), "data-mo-drag": "true" },
4059
4370
  h("h3", { style: S.pickTitle }, modelDialog.mode === "new" ? t("page.modelDialogNew") : t("page.modelDialogTitle")),
4060
4371
  h("button", {
4061
4372
  "data-mo": "model-dialog-close",
@@ -4208,10 +4519,9 @@ window.__ModuleLoader__.load({
4208
4519
  h("div", {
4209
4520
  "data-mo": "provider-delete-dialog",
4210
4521
  style: S.pickBackdrop,
4211
- onMouseDown: function (event) { if (event.target === event.currentTarget) setDeleteTarget(null); }
4212
4522
  },
4213
4523
  h(DialogShell, { label: t("page.deleteProvider"), onClose: function () { setDeleteTarget(null); } },
4214
- h("div", { style: S.pickHead },
4524
+ h("div", { style: Object.assign({}, S.pickHead, { cursor: "move" }), "data-mo-drag": "true" },
4215
4525
  h("h3", { style: S.pickTitle }, t("page.deleteProvider") + " · " + deleteTarget),
4216
4526
  h("button", {
4217
4527
  "data-mo": "provider-delete-close",