diff --git a/patches/@deepseek-ai+dsh-client-ui-model-selection+0.1.0-rc.8.patch b/patches/@deepseek-ai+dsh-client-ui-model-selection+0.1.0-rc.8.patch new file mode 100644 index 00000000..cf129467 --- /dev/null +++ b/patches/@deepseek-ai+dsh-client-ui-model-selection+0.1.0-rc.8.patch @@ -0,0 +1,186 @@ +diff --git a/node_modules/@deepseek-ai/dsh-client-ui-model-selection/lib/client.js b/node_modules/@deepseek-ai/dsh-client-ui-model-selection/lib/client.js +index d01b916..f979023 100644 +--- a/node_modules/@deepseek-ai/dsh-client-ui-model-selection/lib/client.js ++++ b/node_modules/@deepseek-ai/dsh-client-ui-model-selection/lib/client.js +@@ -231,7 +231,7 @@ window.__ModuleLoader__.load({ + } + //#endregion + //#region \0dsh-css:/home/runner/work/deepseek-harness/deepseek-harness/packages/client/ui-model-selection/src/client/ModelSelect.module.css.mjs +- const css = "._7KE1Ra_root{min-width:0;position:relative}._7KE1Ra_trigger{min-width:0;max-width:min(360px,45cqw);height:28px;color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;border-radius:24px;outline:none;align-items:center;gap:4px;padding:0 4px 0 8px;font-size:13px;font-weight:500;line-height:20px;display:flex}._7KE1Ra_trigger:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}._7KE1Ra_trigger:focus-visible{box-shadow:0 0 0 2px var(--dsw-alias-border-l3)}._7KE1Ra_trigger:disabled{color:var(--dsw-alias-label-dimmed);cursor:default}._7KE1Ra_triggerLabel{text-overflow:ellipsis;white-space:nowrap;min-width:0;overflow:hidden}._7KE1Ra_triggerEffort{color:var(--dsw-alias-label-caption);flex:none}._7KE1Ra_chevron{color:var(--dsw-alias-label-caption);flex:none;transition:transform .12s}._7KE1Ra_chevronOpen{transform:rotate(180deg)}._7KE1Ra_menu{z-index:20;border:1px solid var(--dsw-alias-border-inverted);background:var(--dsw-specific-menu);width:max-content;min-width:min(240px,100vw - 32px);max-width:min(420px,100vw - 32px);max-height:min(360px,100vh - 96px);box-shadow:var(--dsw-shadow-lv3);color:var(--dsw-alias-label-primary);--dsh-scrollbar-thumb:var(--dsw-alias-scrollbar-bg-l2);--dsh-scrollbar-thumb-hover:var(--dsw-alias-scrollbar-hover-l2);border-radius:12px;flex-direction:column;padding:4px;display:flex;position:absolute;bottom:calc(100% + 8px);right:0;overflow:hidden}._7KE1Ra_status,._7KE1Ra_empty{color:var(--dsw-alias-label-tertiary);padding:10px;font-size:13px;line-height:20px}._7KE1Ra_error,._7KE1Ra_warning{background:var(--dsw-alias-interactive-bg-hover-danger);color:var(--dsw-alias-state-error-primary);border-radius:8px;justify-content:space-between;align-items:flex-start;gap:8px;margin-bottom:4px;padding:7px 8px;font-size:12px;line-height:18px;display:flex}._7KE1Ra_warning{background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-state-warn-label)}._7KE1Ra_retry{color:inherit;font:inherit;cursor:pointer;background:0 0;border:none;flex:none;padding:0;font-weight:600}._7KE1Ra_groups{min-height:0;overflow-y:auto}._7KE1Ra_group+._7KE1Ra_group{margin-top:4px}._7KE1Ra_groupTitle{z-index:1;background:var(--dsw-specific-menu);color:var(--dsw-alias-label-tertiary);padding:5px 8px 3px;font-size:12px;font-weight:500;line-height:18px;position:sticky;top:0}._7KE1Ra_option{box-sizing:border-box;width:auto;min-width:100%;min-height:38px;color:inherit;text-align:left;cursor:pointer;background:0 0;border:none;border-radius:10px;outline:none;align-items:center;gap:8px;padding:6px 8px;display:flex}._7KE1Ra_option:hover:not(:disabled),._7KE1Ra_option:focus-visible{background:var(--dsw-alias-interactive-bg-hover)}._7KE1Ra_selected{background:0 0}._7KE1Ra_option:disabled{color:var(--dsw-alias-label-dimmed);cursor:default}._7KE1Ra_optionCopy{flex-direction:column;flex:1;min-width:0;display:flex}._7KE1Ra_modelName{color:inherit;text-overflow:ellipsis;white-space:nowrap;font-size:14px;font-weight:500;line-height:20px;overflow:hidden}._7KE1Ra_description{color:var(--dsw-alias-label-tertiary);text-overflow:ellipsis;white-space:nowrap;font-size:12px;line-height:18px;overflow:hidden}._7KE1Ra_check{color:var(--dsw-alias-label-primary);flex:0 0 18px;place-items:center;display:grid}._7KE1Ra_cell{box-sizing:border-box;width:auto;min-width:100%;height:40px;color:var(--dsw-alias-label-primary);cursor:pointer;text-align:left;background:0 0;border:none;border-radius:10px;align-items:center;gap:8px;padding:0 10px;font-size:14px;line-height:22px;display:flex}._7KE1Ra_cell:hover{background:var(--dsw-alias-interactive-bg-hover)}._7KE1Ra_cellLabel{white-space:nowrap;flex:none}._7KE1Ra_cellValue{text-overflow:ellipsis;white-space:nowrap;text-align:right;min-width:0;color:var(--dsw-alias-label-tertiary);flex:auto;overflow:hidden}._7KE1Ra_cellChevron{color:var(--dsw-alias-label-tertiary);flex:none}"; ++ const css = "._7KE1Ra_root{min-width:0;position:relative}._7KE1Ra_trigger{min-width:0;max-width:min(360px,45cqw);height:28px;color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;border-radius:24px;outline:none;align-items:center;gap:4px;padding:0 4px 0 8px;font-size:13px;font-weight:500;line-height:20px;display:flex}._7KE1Ra_trigger:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}._7KE1Ra_trigger:focus-visible{box-shadow:0 0 0 2px var(--dsw-alias-border-l3)}._7KE1Ra_trigger:disabled{color:var(--dsw-alias-label-dimmed);cursor:default}._7KE1Ra_triggerLabel{text-overflow:ellipsis;white-space:nowrap;min-width:0;overflow:hidden}._7KE1Ra_triggerEffort{color:var(--dsw-alias-label-caption);flex:none}._7KE1Ra_chevron{color:var(--dsw-alias-label-caption);flex:none;transition:transform .12s}._7KE1Ra_chevronOpen{transform:rotate(180deg)}._7KE1Ra_menu{z-index:20;border:1px solid var(--dsw-alias-border-inverted);background:var(--dsw-specific-menu);width:max-content;min-width:min(240px,100vw - 32px);max-width:min(420px,100vw - 32px);max-height:min(360px,100vh - 96px);box-shadow:var(--dsw-shadow-lv3);color:var(--dsw-alias-label-primary);--dsh-scrollbar-thumb:var(--dsw-alias-scrollbar-bg-l2);--dsh-scrollbar-thumb-hover:var(--dsw-alias-scrollbar-hover-l2);border-radius:12px;flex-direction:column;padding:4px;display:flex;position:absolute;bottom:calc(100% + 8px);right:0;overflow:hidden}._7KE1Ra_search{box-sizing:border-box;height:36px;color:var(--dsw-alias-label-tertiary);background:var(--dsw-alias-bg-module-platform);border:1px solid transparent;border-radius:9px;align-items:center;gap:7px;margin:2px 4px 4px;padding:0 9px;display:flex;flex:none}._7KE1Ra_search:focus-within{border-color:var(--dsw-alias-border-l2);background:var(--dsw-specific-menu)}._7KE1Ra_searchIcon{flex:none}._7KE1Ra_searchInput{width:100%;min-width:0;color:var(--dsw-alias-label-primary);font:inherit;background:0 0;border:none;outline:none;padding:0;font-size:13px;line-height:20px}._7KE1Ra_searchInput::placeholder{color:var(--dsw-alias-label-caption)}._7KE1Ra_status,._7KE1Ra_empty{color:var(--dsw-alias-label-tertiary);padding:10px;font-size:13px;line-height:20px}._7KE1Ra_error,._7KE1Ra_warning{background:var(--dsw-alias-interactive-bg-hover-danger);color:var(--dsw-alias-state-error-primary);border-radius:8px;justify-content:space-between;align-items:flex-start;gap:8px;margin-bottom:4px;padding:7px 8px;font-size:12px;line-height:18px;display:flex}._7KE1Ra_warning{background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-state-warn-label)}._7KE1Ra_retry{color:inherit;font:inherit;cursor:pointer;background:0 0;border:none;flex:none;padding:0;font-weight:600}._7KE1Ra_groups{min-height:0;overflow-y:auto}._7KE1Ra_group+._7KE1Ra_group{margin-top:4px}._7KE1Ra_groupTitle{z-index:1;background:var(--dsw-specific-menu);color:var(--dsw-alias-label-tertiary);padding:5px 8px 3px;font-size:12px;font-weight:500;line-height:18px;position:sticky;top:0}._7KE1Ra_option{box-sizing:border-box;width:auto;min-width:100%;min-height:38px;color:inherit;text-align:left;cursor:pointer;background:0 0;border:none;border-radius:10px;outline:none;align-items:center;gap:8px;padding:6px 8px;display:flex}._7KE1Ra_option:hover:not(:disabled),._7KE1Ra_option:focus-visible{background:var(--dsw-alias-interactive-bg-hover)}._7KE1Ra_selected{background:0 0}._7KE1Ra_option:disabled{color:var(--dsw-alias-label-dimmed);cursor:default}._7KE1Ra_optionCopy{flex-direction:column;flex:1;min-width:0;display:flex}._7KE1Ra_modelName{color:inherit;text-overflow:ellipsis;white-space:nowrap;font-size:14px;font-weight:500;line-height:20px;overflow:hidden}._7KE1Ra_description{color:var(--dsw-alias-label-tertiary);text-overflow:ellipsis;white-space:nowrap;font-size:12px;line-height:18px;overflow:hidden}._7KE1Ra_check{color:var(--dsw-alias-label-primary);flex:0 0 18px;place-items:center;display:grid}._7KE1Ra_cell{box-sizing:border-box;width:auto;min-width:100%;height:40px;color:var(--dsw-alias-label-primary);cursor:pointer;text-align:left;background:0 0;border:none;border-radius:10px;align-items:center;gap:8px;padding:0 10px;font-size:14px;line-height:22px;display:flex}._7KE1Ra_cell:hover{background:var(--dsw-alias-interactive-bg-hover)}._7KE1Ra_cellLabel{white-space:nowrap;flex:none}._7KE1Ra_cellValue{text-overflow:ellipsis;white-space:nowrap;text-align:right;min-width:0;color:var(--dsw-alias-label-tertiary);flex:auto;overflow:hidden}._7KE1Ra_cellChevron{color:var(--dsw-alias-label-tertiary);flex:none}"; + const tagId = "@deepseek-ai/dsh-client-ui-model-selection/ModelSelect.module.css"; + if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) { + const tag = document.createElement("style"); +@@ -260,6 +260,9 @@ window.__ModuleLoader__.load({ + "optionCopy": "_7KE1Ra_optionCopy", + "retry": "_7KE1Ra_retry", + "root": "_7KE1Ra_root", ++ "search": "_7KE1Ra_search", ++ "searchIcon": "_7KE1Ra_searchIcon", ++ "searchInput": "_7KE1Ra_searchInput", + "selected": "_7KE1Ra_selected", + "status": "_7KE1Ra_status", + "trigger": "_7KE1Ra_trigger", +@@ -269,6 +272,16 @@ window.__ModuleLoader__.load({ + }; + //#endregion + //#region lib/types/client/ModelSelect.js ++ /** Keep provider grouping while narrowing a large model catalog. */ ++ function filterModelGroups(groups, query) { ++ const needle = query.trim().toLowerCase(); ++ if (needle.length === 0) return groups; ++ return groups.flatMap((group) => { ++ const providerMatches = [group.id, group.name].some((value) => value.toLowerCase().includes(needle)); ++ const models = providerMatches ? group.models : group.models.filter((model) => [model.id, model.name, model.description ?? ""].some((value) => value.toLowerCase().includes(needle))); ++ return models.length === 0 ? [] : [{ ...group, models }]; ++ }); ++ } + /** + * ModelSelect: the composer's named model seat (`conversation.input.model`). + * Two-level selection per figma 496:26454's MenuDropdown: the root menu is +@@ -292,11 +305,13 @@ window.__ModuleLoader__.load({ + const state = (0, react.useSyncExternalStore)((fn) => directory.subscribe(fn), () => directory.getSnapshot()); + const [open, setOpen] = (0, react.useState)(false); + const [pane, setPane] = (0, react.useState)("root"); ++ const [modelQuery, setModelQuery] = (0, react.useState)(""); + const lastActionRef = (0, react.useRef)("load"); + const [toast, setToast] = (0, react.useState)(null); + const toastSeq = (0, react.useRef)(0); + const rootRef = (0, react.useRef)(null); + const triggerRef = (0, react.useRef)(null); ++ const searchRef = (0, react.useRef)(null); + const itemRefs = (0, react.useRef)([]); + const id = (0, react.useId)(); + const choices = (0, react.useMemo)(() => state.groups.flatMap((group) => group.models.map((model) => ({ +@@ -308,6 +323,7 @@ window.__ModuleLoader__.load({ + ...model.reasoning?.defaultEffort === void 0 ? {} : { reasoningEffort: model.reasoning.defaultEffort } + } + }))), [state.groups]); ++ const filteredGroups = (0, react.useMemo)(() => filterModelGroups(state.groups, modelQuery), [state.groups, modelQuery]); + const currentChoice = choices[state.current === null ? -1 : choices.findIndex((c) => c.selection.provider === state.current?.provider && c.selection.model === state.current.model)]; + const reasoning = currentChoice?.model.reasoning; + const effectiveEffort = state.current?.reasoningEffort ?? reasoning?.defaultEffort; +@@ -343,15 +359,20 @@ window.__ModuleLoader__.load({ + document.removeEventListener("mousedown", closeOutside); + }; + }, [open]); ++ (0, react.useEffect)(() => { ++ if (open && pane === "model") searchRef.current?.focus(); ++ }, [open, pane]); + if (!available) return null; + const show = () => { + setPane("root"); ++ setModelQuery(""); + setOpen(true); + reload(); + }; + const close = (restoreFocus = false) => { + setOpen(false); + setPane("root"); ++ setModelQuery(""); + if (restoreFocus) queueMicrotask(() => { + triggerRef.current?.focus(); + }); +@@ -360,12 +381,16 @@ window.__ModuleLoader__.load({ + const items = itemRefs.current.filter((item) => item !== null); + if (items.length === 0) return; + const active = items.findIndex((item) => item === document.activeElement); +- items[(Math.max(active, 0) + offset + items.length) % items.length]?.focus(); ++ const next = active < 0 ? offset > 0 ? 0 : items.length - 1 : (active + offset + items.length) % items.length; ++ items[next]?.focus(); + }; + const onRootKeyDown = (event) => { + if (event.key === "Escape" && open) { + event.preventDefault(); +- if (pane !== "root") setPane("root"); ++ if (pane !== "root") { ++ setPane("root"); ++ setModelQuery(""); ++ } + else close(true); + return; + } +@@ -474,6 +499,7 @@ window.__ModuleLoader__.load({ + role: "menuitem", + className: ModelSelect_module_css_default.cell, + onClick: () => { ++ setModelQuery(""); + setPane("model"); + }, + children: [ +@@ -508,6 +534,23 @@ window.__ModuleLoader__.load({ + ] + })] }), + pane === "model" && (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [ ++ (0, react_jsx_runtime.jsxs)("label", { ++ className: ModelSelect_module_css_default.search, ++ children: [(0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconSearchOutline16, { className: ModelSelect_module_css_default.searchIcon }), (0, react_jsx_runtime.jsx)("input", { ++ ref: searchRef, ++ type: "search", ++ role: "searchbox", ++ className: ModelSelect_module_css_default.searchInput, ++ value: modelQuery, ++ placeholder: t("search.placeholder"), ++ "aria-label": t("search.aria"), ++ autoComplete: "off", ++ spellCheck: false, ++ onChange: (event) => { ++ setModelQuery(event.target.value); ++ } ++ })] ++ }), + state.status === "loading" && (0, react_jsx_runtime.jsx)("div", { + className: ModelSelect_module_css_default.status, + children: t("status.loading") +@@ -535,7 +578,7 @@ window.__ModuleLoader__.load({ + }, failure.id)), + (0, react_jsx_runtime.jsx)("div", { + className: clsx(ModelSelect_module_css_default.groups, "scrollable"), +- children: state.groups.map((group) => { ++ children: filteredGroups.map((group) => { + const headingId = `${id}-${group.id}`; + return (0, react_jsx_runtime.jsxs)("section", { + role: "group", +@@ -582,6 +625,10 @@ window.__ModuleLoader__.load({ + state.status === "ready" && choices.length === 0 && (0, react_jsx_runtime.jsx)("div", { + className: ModelSelect_module_css_default.empty, + children: t("empty.models") ++ }), ++ state.status === "ready" && choices.length > 0 && filteredGroups.length === 0 && (0, react_jsx_runtime.jsx)("div", { ++ className: ModelSelect_module_css_default.empty, ++ children: t("empty.search") + }) + ] }), + pane === "effort" && (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [state.error !== null && lastActionRef.current === "load" && (0, react_jsx_runtime.jsxs)("div", { +@@ -654,12 +701,15 @@ window.__ModuleLoader__.load({ + "menu.aria": "模型与推理等级", + "menu.model": "模型", + "menu.effort": "推理等级", ++ "search.placeholder": "搜索模型或服务商", ++ "search.aria": "搜索模型或服务商", + "effort.providerDefault": "Default", + "status.loading": "正在刷新模型列表…", + "error.action": "模型操作失败:{message}", + "action.reload": "重新加载", + "warning.groupLoad": "{name} 加载失败:{message}", + "empty.models": "没有可用的模型。", ++ "empty.search": "没有找到匹配的模型。", + "blocked.composer": "当前模型不可用,请先选择模型", + "empty.efforts": "当前模型未提供推理等级。" + }; +@@ -674,12 +724,15 @@ window.__ModuleLoader__.load({ + "menu.aria": "Model and reasoning effort", + "menu.model": "Model", + "menu.effort": "Effort", ++ "search.placeholder": "Search models or providers", ++ "search.aria": "Search models or providers", + "effort.providerDefault": "Default", + "status.loading": "Refreshing model list…", + "error.action": "Model operation failed: {message}", + "action.reload": "Reload", + "warning.groupLoad": "{name} failed to load: {message}", + "empty.models": "No models available.", ++ "empty.search": "No matching models.", + "blocked.composer": "This model is unavailable — select one to continue", + "empty.efforts": "This model provides no reasoning effort levels." + }; diff --git a/patches/@deepseek-ai+dsh-client-ui-settings-models+0.1.0-rc.8.patch b/patches/@deepseek-ai+dsh-client-ui-settings-models+0.1.0-rc.8.patch index d2f8469b..e60bb8d8 100644 --- a/patches/@deepseek-ai+dsh-client-ui-settings-models+0.1.0-rc.8.patch +++ b/patches/@deepseek-ai+dsh-client-ui-settings-models+0.1.0-rc.8.patch @@ -1,8 +1,8 @@ diff --git a/node_modules/@deepseek-ai/dsh-client-ui-settings-models/lib/client.js b/node_modules/@deepseek-ai/dsh-client-ui-settings-models/lib/client.js -index 12ebd55..76d09ba 100644 +index 12ebd55..ec97bfb 100644 --- a/node_modules/@deepseek-ai/dsh-client-ui-settings-models/lib/client.js +++ b/node_modules/@deepseek-ai/dsh-client-ui-settings-models/lib/client.js -@@ -64,6 +64,14 @@ window.__ModuleLoader__.load({ +@@ -64,6 +64,30 @@ window.__ModuleLoader__.load({ tag.textContent = css$3; document.head.appendChild(tag); } @@ -13,11 +13,309 @@ index 12ebd55..76d09ba 100644 + tag.dataset.pluginCss = providerPickerTagId; + tag.textContent = ".dshProviderPicker{flex-direction:column;gap:10px;display:flex}.dshProviderSearch{box-sizing:border-box;border:1px solid var(--dsw-alias-border-l2);width:100%;height:36px;font:inherit;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);border-radius:8px;padding:0 11px;font-size:13px}.dshProviderSearch:focus{border-color:var(--dsw-alias-border-l1);outline:none}.dshProviderSearch::placeholder{color:var(--dsw-alias-label-dimmed)}.dshProviderGrid{grid-template-columns:repeat(2,minmax(0,1fr));gap:8px;display:grid}.dshProviderCard{box-sizing:border-box;color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l2);border-radius:8px;flex-direction:column;align-items:flex-start;min-height:58px;padding:10px 12px;display:flex;cursor:pointer;text-align:left;font:inherit}.dshProviderCard:hover{background:var(--dsw-alias-interactive-bg-hover)}.dshProviderCard[aria-pressed=true]{border-color:var(--dsw-alias-border-l1);background:var(--dsw-alias-interactive-bg-hover)}.dshProviderCard:focus-visible{outline:none;box-shadow:0 0 0 2px var(--dsw-alias-border-l3)}.dshProviderName{font-size:14px;font-weight:500;line-height:20px}.dshProviderRoute{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:16px}.dshProviderSummary{box-sizing:border-box;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);border-radius:8px;align-items:center;gap:12px;min-height:52px;padding:8px 10px 8px 12px;display:flex}.dshProviderSummaryText{flex-direction:column;min-width:0;display:flex}.dshProviderChange{color:var(--dsw-alias-label-secondary);background:transparent;border:1px solid var(--dsw-alias-border-l2);border-radius:14px;margin-left:auto;padding:4px 10px;font:inherit;font-size:12px;cursor:pointer}.dshProviderChange:hover{background:var(--dsw-alias-interactive-bg-hover)}@media (width<=620px){.dshProviderGrid{grid-template-columns:1fr}}"; + document.head.appendChild(tag); ++ } ++ const modelModalityTagId = "@deepseek-ai/dsh-client-ui-settings-models/ModelModalityToggle"; ++ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(modelModalityTagId) + "]") === null) { ++ const tag = document.createElement("style"); ++ tag.dataset.plugin = "@deepseek-ai/dsh-client-ui-settings-models"; ++ tag.dataset.pluginCss = modelModalityTagId; ++ tag.textContent = ".dshModelModalityToggle{grid-column:1/-1;align-items:flex-start;gap:9px;min-height:36px;padding:4px;display:flex;cursor:pointer}.dshModelModalityToggle[aria-disabled=true]{cursor:default;opacity:.6}.dshModelModalityToggle input{accent-color:var(--dsw-alias-brand-primary);flex:none;width:16px;height:16px;margin:2px 0 0}.dshModelModalityCopy{flex-direction:column;gap:1px;display:flex}.dshModelModalityTitle{color:var(--dsw-alias-label-secondary);font-size:12px;font-weight:500;line-height:18px}.dshModelModalityHint{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:16px}"; ++ document.head.appendChild(tag); ++ } ++ const modelCatalogUxTagId = "@deepseek-ai/dsh-client-ui-settings-models/ModelCatalogUx"; ++ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(modelCatalogUxTagId) + "]") === null) { ++ const tag = document.createElement("style"); ++ tag.dataset.plugin = "@deepseek-ai/dsh-client-ui-settings-models"; ++ tag.dataset.pluginCss = modelCatalogUxTagId; ++ tag.textContent = ".dshModelCatalogSearch{box-sizing:border-box;position:relative;align-items:center;display:flex}.dshModelCatalogSearchIcon{position:absolute;left:10px;color:var(--dsw-alias-label-tertiary);pointer-events:none}.dshModelCatalogSearch input{box-sizing:border-box;border:1px solid var(--dsw-alias-border-l2);width:100%;height:34px;font:inherit;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);border-radius:8px;padding:0 11px 0 34px;font-size:13px;line-height:20px}.dshModelCatalogSearch input:focus{border-color:var(--dsw-alias-brand-primary);outline:none}.dshModelCatalogSearch input::placeholder{color:var(--dsw-alias-label-dimmed)}.dshProviderEditorExpanded .zGbnIq_customizedBody{padding-bottom:72px}.dshProviderEditorStickyFooter{z-index:4;position:sticky;bottom:-24px;background:var(--dsw-alias-bg-module-platform);border-top:1px solid var(--dsw-alias-border-l2);box-shadow:0 -10px 18px -18px #0009;align-items:center;justify-content:space-between;gap:12px;margin:0 -16px -14px;padding:12px 16px 14px;display:flex}.dshProviderEditorStickyFooter .zGbnIq_editorActions{margin-left:auto}@media (width<=620px){.dshProviderEditorStickyFooter{align-items:stretch;flex-wrap:wrap}.dshProviderEditorStickyFooter .zGbnIq_editorActions{margin-left:auto}}"; ++ document.head.appendChild(tag); + } var ModelsSection_module_css_default = { "addActions": "zGbnIq_addActions", "addBlock": "zGbnIq_addBlock", -@@ -1784,6 +1792,26 @@ window.__ModuleLoader__.load({ +@@ -155,6 +179,67 @@ window.__ModuleLoader__.load({ + }); + } + //#endregion ++ //#region lib/types/client/ModelImageInputToggle.js ++ /** Whether one model explicitly declares image input in the adapter-owned field. */ ++ function imageInputEnabled(model, field) { ++ const modalities = model[field]; ++ return Array.isArray(modalities) && modalities.includes("image"); ++ } ++ /** The explicit modality list written by the binary image-input control. */ ++ function imageInputModalities(enabled) { ++ return enabled ? ["text", "image"] : ["text"]; ++ } ++ /** Render the shared per-model image-input declaration control. */ ++ function ModelImageInputToggle(props) { ++ return (0, react_jsx_runtime.jsxs)("label", { ++ className: "dshModelModalityToggle", ++ "aria-disabled": props.disabled, ++ children: [(0, react_jsx_runtime.jsx)("input", { ++ type: "checkbox", ++ checked: imageInputEnabled(props.model, props.field), ++ "aria-label": `${props.t("modelImageInput")} ${String(props.index + 1)}`, ++ disabled: props.disabled, ++ onChange: (event) => { ++ props.onChange(event.target.checked); ++ } ++ }), (0, react_jsx_runtime.jsxs)("span", { ++ className: "dshModelModalityCopy", ++ children: [(0, react_jsx_runtime.jsx)("span", { ++ className: "dshModelModalityTitle", ++ children: props.t("modelImageInput") ++ }), (0, react_jsx_runtime.jsx)("span", { ++ className: "dshModelModalityHint", ++ children: props.t("modelImageInputHint") ++ })] ++ })] ++ }); ++ } ++ //#endregion ++ //#region lib/types/client/ModelCatalogSearch.js ++ /** Filter a model catalog without losing the source index used by edit actions. */ ++ function searchableModelEntries(models, query) { ++ const needle = query.trim().toLowerCase(); ++ return models.map((model, index) => ({ model, index })).filter(({ model }) => needle.length === 0 || [model.id, model.name].some((value) => typeof value === "string" && value.toLowerCase().includes(needle))); ++ } ++ /** Shared search field for both adapter-owned model catalog editors. */ ++ function ModelCatalogSearch(props) { ++ return (0, react_jsx_runtime.jsxs)("label", { ++ className: "dshModelCatalogSearch", ++ children: [(0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconSearchOutline16, { className: "dshModelCatalogSearchIcon" }), (0, react_jsx_runtime.jsx)("input", { ++ type: "search", ++ role: "searchbox", ++ value: props.value, ++ placeholder: props.t("modelSearch"), ++ "aria-label": props.t("modelSearch"), ++ autoComplete: "off", ++ spellCheck: false, ++ onChange: (event) => { ++ props.onChange(event.target.value); ++ } ++ })] ++ }); ++ } ++ //#endregion + //#region lib/types/client/DeepSeekModelsEditor.js + /** + * Curated editor for the direct DeepSeek adapter's advisory model catalog. +@@ -256,6 +341,7 @@ window.__ModuleLoader__.load({ + function DeepSeekModelsEditor(props) { + const [editing, setEditing] = (0, react.useState)(() => /* @__PURE__ */ new Map()); + const [expanded, setExpanded] = (0, react.useState)(() => /* @__PURE__ */ new Set()); ++ const visibleModels = (0, react.useMemo)(() => searchableModelEntries(props.models, props.modelQuery), [props.models, props.modelQuery]); + const update = (index, key, value) => { + const next = props.models.map((model, at) => { + const copy = { ...model }; +@@ -364,12 +450,20 @@ window.__ModuleLoader__.load({ + children: props.t("resetModels") + }) : null] + }), ++ (0, react_jsx_runtime.jsx)(ModelCatalogSearch, { ++ value: props.modelQuery, ++ onChange: props.onModelQueryChange, ++ t: props.t ++ }), + props.models.length === 0 ? (0, react_jsx_runtime.jsx)("p", { + className: ModelsSection_module_css_default["modelEmpty"], + children: props.t("modelsEmpty") ++ }) : visibleModels.length === 0 ? (0, react_jsx_runtime.jsx)("p", { ++ className: ModelsSection_module_css_default["modelEmpty"], ++ children: props.t("modelSearchEmpty") + }) : (0, react_jsx_runtime.jsx)("div", { + className: ModelsSection_module_css_default["modelList"], +- children: props.models.map((model, index) => (0, react_jsx_runtime.jsxs)("div", { ++ children: visibleModels.map(({ model, index }) => (0, react_jsx_runtime.jsxs)("div", { + className: ModelsSection_module_css_default["modelEntry"], + children: [(0, react_jsx_runtime.jsxs)("div", { + className: ModelsSection_module_css_default["modelRow"], +@@ -425,18 +519,18 @@ window.__ModuleLoader__.load({ + ] + }), expanded.has(index) ? (0, react_jsx_runtime.jsxs)("div", { + className: ModelsSection_module_css_default["modelAdvanced"], +- children: [capacityField(model, index, "contextWindow", props.defaultContextWindow), capacityField(model, index, "maxTokens", props.defaultMaxTokens)] ++ children: [capacityField(model, index, "contextWindow", props.defaultContextWindow), capacityField(model, index, "maxTokens", props.defaultMaxTokens), (0, react_jsx_runtime.jsx)(ModelImageInputToggle, { ++ model, ++ field: "inputModalities", ++ index, ++ t: props.t, ++ disabled: props.disabled, ++ onChange: (enabled) => { ++ update(index, "inputModalities", imageInputModalities(enabled)); ++ } ++ })] + }) : null] + }, index)) +- }), +- (0, react_jsx_runtime.jsxs)("button", { +- type: "button", +- className: ModelsSection_module_css_default["addModelButton"], +- disabled: props.disabled, +- onClick: () => { +- props.onChange([...props.models.map((model) => ({ ...model })), { id: "" }]); +- }, +- children: [(0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconPlusOutline16, { size: 14 }), props.t("addModel")] + }) + ] + }); +@@ -759,6 +853,7 @@ window.__ModuleLoader__.load({ + const [picked, setPicked] = (0, react.useState)(/* @__PURE__ */ new Set()); + const [expanded, setExpanded] = (0, react.useState)(/* @__PURE__ */ new Set()); + const [editing, setEditing] = (0, react.useState)(/* @__PURE__ */ new Map()); ++ const visibleModels = (0, react.useMemo)(() => searchableModelEntries(models, props.modelQuery), [models, props.modelQuery]); + /** Buffer key for one capacity field; the row half moves when rows do. */ + const bufferKey = (index, field) => `${String(index)}:${field}`; + const editCapacity = (index, field, text) => { +@@ -889,11 +984,19 @@ window.__ModuleLoader__.load({ + }) + ] + }), ++ (0, react_jsx_runtime.jsx)(ModelCatalogSearch, { ++ value: props.modelQuery, ++ onChange: props.onModelQueryChange, ++ t ++ }), + models.length === 0 ? (0, react_jsx_runtime.jsx)("p", { + className: ModelsSection_module_css_default["modelEmpty"], + children: t("modelsEmpty") ++ }) : visibleModels.length === 0 ? (0, react_jsx_runtime.jsx)("p", { ++ className: ModelsSection_module_css_default["modelEmpty"], ++ children: t("modelSearchEmpty") + }) : null, +- models.map((model, index) => (0, react_jsx_runtime.jsxs)("div", { ++ visibleModels.map(({ model, index }) => (0, react_jsx_runtime.jsxs)("div", { + className: ModelsSection_module_css_default["modelEntry"], + children: [(0, react_jsx_runtime.jsxs)("div", { + className: ModelsSection_module_css_default["modelRow"], +@@ -986,18 +1089,18 @@ window.__ModuleLoader__.load({ + editCapacity(index, "maxTokens", event.target.value); + } + })] ++ }), (0, react_jsx_runtime.jsx)(ModelImageInputToggle, { ++ model, ++ field: "input", ++ index, ++ t, ++ disabled, ++ onChange: (enabled) => { ++ patch(index, { input: imageInputModalities(enabled) }); ++ } + })] + }) : null] + }, index)), +- (0, react_jsx_runtime.jsx)("button", { +- type: "button", +- className: ModelsSection_module_css_default["addModelButton"], +- disabled, +- onClick: () => { +- onChange([...models, { id: "" }]); +- }, +- children: t("addModel") +- }), + failure !== void 0 ? (0, react_jsx_runtime.jsx)("p", { + className: ModelsSection_module_css_default["error"], + children: failure +@@ -1397,6 +1500,8 @@ window.__ModuleLoader__.load({ + const [keyState, setKeyState] = (0, react.useState)(void 0); + const [busy, setBusy] = (0, react.useState)(false); + const [failure, setFailure] = (0, react.useState)(void 0); ++ const [modelQuery, setModelQuery] = (0, react.useState)(""); ++ const [customizedOpen, setCustomizedOpen] = (0, react.useState)(false); + const [committedOriginal, setCommittedOriginal] = (0, react.useState)(() => schema.getPath(namespace.user, settingsPath)); + const [expectedRevision, setExpectedRevision] = (0, react.useState)(() => namespace.revision); + const root = (0, react.useMemo)(() => schema.rehydrate(namespace.schema), [namespace.schema, schema]); +@@ -1522,6 +1627,15 @@ window.__ModuleLoader__.load({ + const inheritedModels = () => { + return schema.getPath(namespace.base, [...settingsPath, "models"]) ?? schema.nodeAtPath(root, [...settingsPath, "models"])?.meta.default; + }; ++ /** Materialize the visible catalog override and reveal the newly added row. */ ++ const addModel = () => { ++ const customModels = schema.getPath(draft, ["models"]); ++ const modelsOverridden = schema.hasPath(draft, ["models"]); ++ const models = modelDrafts(modelsOverridden ? customModels : inheritedModels()); ++ setDraft((current) => schema.setPath(current, ["models"], [...models.map((model) => ({ ...model })), { id: "" }])); ++ setModelQuery(""); ++ setCustomizedOpen(true); ++ }; + /** + * The curated fields of one known adapter family. The family arrives + * narrowed so the per-family branches below are total: an unknown namespace +@@ -1539,6 +1653,8 @@ window.__ModuleLoader__.load({ + const catalogProps = { + models, + overridden: modelsOverridden, ++ modelQuery, ++ onModelQueryChange: setModelQuery, + t, + disabled, + onChange: (next) => { +@@ -1577,6 +1693,10 @@ window.__ModuleLoader__.load({ + ] + }), props.credentialOnly === true ? null : (0, react_jsx_runtime.jsxs)("details", { + className: ModelsSection_module_css_default["customized"], ++ open: customizedOpen, ++ onToggle: (event) => { ++ setCustomizedOpen(event.currentTarget.open); ++ }, + children: [(0, react_jsx_runtime.jsx)("summary", { + className: ModelsSection_module_css_default["customizedSummary"], + children: t("customized") +@@ -1653,8 +1773,22 @@ window.__ModuleLoader__.load({ + })] + })] }); + }; ++ const footerProps = { ++ t, ++ busy, ++ submitDisabled: disabled || layout === "unknown" || props.credentialOnly !== true && modelFailure !== void 0 || shownKeyFailure !== void 0 || props.credentialRequired === true && keyValue.length === 0, ++ submitLabel: props.submitLabel ?? "apply", ++ submitBusyLabel: props.submitBusyLabel ?? "applying", ++ ...props.cancelLabel === void 0 ? {} : { cancelLabel: props.cancelLabel }, ++ onCancel: () => { ++ props.onClose(false); ++ }, ++ onSubmit: () => { ++ apply(); ++ } ++ }; + return (0, react_jsx_runtime.jsxs)("div", { +- className: props.credentialOnly === true ? ModelsSection_module_css_default["addBlock"] : ModelsSection_module_css_default["editor"], ++ className: props.credentialOnly === true ? ModelsSection_module_css_default["addBlock"] : `${ModelsSection_module_css_default["editor"]}${customizedOpen ? " dshProviderEditorExpanded" : ""}`, + children: [ + props.hideTitle === true ? null : (0, react_jsx_runtime.jsxs)("div", { + className: ModelsSection_module_css_default["editorHeader"], +@@ -1678,19 +1812,15 @@ window.__ModuleLoader__.load({ + className: ModelsSection_module_css_default["advancedHint"], + children: `${t("model")} ${String(modelFailure.index + 1)}: ${t(modelFailure.key)}` + }), +- (0, react_jsx_runtime.jsx)(EditorFooter, { +- t, +- busy, +- submitDisabled: disabled || layout === "unknown" || props.credentialOnly !== true && modelFailure !== void 0 || shownKeyFailure !== void 0 || props.credentialRequired === true && keyValue.length === 0, +- submitLabel: props.submitLabel ?? "apply", +- submitBusyLabel: props.submitBusyLabel ?? "applying", +- ...props.cancelLabel === void 0 ? {} : { cancelLabel: props.cancelLabel }, +- onCancel: () => { +- props.onClose(false); +- }, +- onSubmit: () => { +- apply(); +- } ++ props.credentialOnly === true || !customizedOpen || layout === "unknown" ? (0, react_jsx_runtime.jsx)(EditorFooter, { ...footerProps }) : (0, react_jsx_runtime.jsxs)("div", { ++ className: "dshProviderEditorStickyFooter", ++ children: [(0, react_jsx_runtime.jsxs)("button", { ++ type: "button", ++ className: ModelsSection_module_css_default["addModelButton"], ++ disabled, ++ onClick: addModel, ++ children: [(0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconPlusOutline16, { size: 14 }), t("addModel")] ++ }), (0, react_jsx_runtime.jsx)(EditorFooter, { ...footerProps })] + }) + ] + }); +@@ -1784,6 +1914,26 @@ window.__ModuleLoader__.load({ function providerCopy(template, target) { return template.replace("{provider}", () => providerTargetLabel(target)); } @@ -44,7 +342,7 @@ index 12ebd55..76d09ba 100644 /** * Render the Models section content column. * @param props - slot-delivered injected dependencies. -@@ -1810,6 +1838,8 @@ window.__ModuleLoader__.load({ +@@ -1810,6 +1960,8 @@ window.__ModuleLoader__.load({ const [deleteFailure, setDeleteFailure] = (0, react.useState)(void 0); const [savedTarget, setSavedTarget] = (0, react.useState)(void 0); const [declaring, setDeclaring] = (0, react.useState)(false); @@ -53,7 +351,7 @@ index 12ebd55..76d09ba 100644 const [dismissedSetup, setDismissedSetup] = (0, react.useState)(() => /* @__PURE__ */ new Set()); const announceSaved = (target) => { controller.load().then(() => { -@@ -1879,10 +1909,16 @@ window.__ModuleLoader__.load({ +@@ -1879,10 +2031,16 @@ window.__ModuleLoader__.load({ }; const anyUsable = state.rows.some(providerUsable); const configured = state.rows.filter((row) => row.configured); @@ -71,7 +369,7 @@ index 12ebd55..76d09ba 100644 return (0, react_jsx_runtime.jsxs)("div", { className: ModelsSection_module_css_default["section"], children: [ -@@ -2001,24 +2037,66 @@ window.__ModuleLoader__.load({ +@@ -2001,24 +2159,66 @@ window.__ModuleLoader__.load({ className: ModelsSection_module_css_default["addCard"], children: [(0, react_jsx_runtime.jsxs)("div", { className: ModelsSection_module_css_default["field"], @@ -153,7 +451,7 @@ index 12ebd55..76d09ba 100644 }), (0, react_jsx_runtime.jsx)(ProviderEditor, { provider: addTarget.provider, displayName: addTarget.displayName, -@@ -2062,6 +2140,8 @@ window.__ModuleLoader__.load({ +@@ -2062,6 +2262,8 @@ window.__ModuleLoader__.load({ setDeclaring(false); setAdding(true); setEditing(targetOf(first)); @@ -162,7 +460,7 @@ index 12ebd55..76d09ba 100644 }, children: [(0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconPlusOutline16, { size: 14 }), t("add")] }), (0, react_jsx_runtime.jsxs)("button", { -@@ -2170,7 +2250,7 @@ window.__ModuleLoader__.load({ +@@ -2170,7 +2372,7 @@ window.__ModuleLoader__.load({ } //#endregion //#region \0dsh-css:/home/runner/work/deepseek-harness/deepseek-harness/packages/client/ui-settings-models/src/client/DeepSeekOnboardingDialog.module.css.mjs @@ -171,7 +469,7 @@ index 12ebd55..76d09ba 100644 const tagId$1 = "@deepseek-ai/dsh-client-ui-settings-models/DeepSeekOnboardingDialog.module.css"; if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$1) + "]") === null) { const tag = document.createElement("style"); -@@ -2181,6 +2261,12 @@ window.__ModuleLoader__.load({ +@@ -2181,6 +2383,12 @@ window.__ModuleLoader__.load({ } var DeepSeekOnboardingDialog_module_css_default = { "description": "GL8Viq_description", @@ -184,7 +482,7 @@ index 12ebd55..76d09ba 100644 "editor": "GL8Viq_editor" }; //#endregion -@@ -2196,35 +2282,50 @@ window.__ModuleLoader__.load({ +@@ -2196,35 +2404,50 @@ window.__ModuleLoader__.load({ function assertNever$1(_value) { throw new Error("unexpected DeepSeek onboarding state"); } @@ -253,7 +551,7 @@ index 12ebd55..76d09ba 100644 const finishCredential = (changed) => { if (!changed) { complete(); -@@ -2237,26 +2338,48 @@ window.__ModuleLoader__.load({ +@@ -2237,26 +2460,48 @@ window.__ModuleLoader__.load({ children: [(0, react_jsx_runtime.jsx)("p", { className: DeepSeekOnboardingDialog_module_css_default.description, children: t("onboardingDescription") @@ -305,7 +603,7 @@ index 12ebd55..76d09ba 100644 })] }); } -@@ -2520,6 +2643,8 @@ window.__ModuleLoader__.load({ +@@ -2520,6 +2765,8 @@ window.__ModuleLoader__.load({ deleting: "Deleting {provider}…", add: "Add provider", provider: "Provider", @@ -314,7 +612,20 @@ index 12ebd55..76d09ba 100644 close: "Close", cancel: "Cancel", apply: "Apply", -@@ -2595,10 +2720,14 @@ window.__ModuleLoader__.load({ +@@ -2551,7 +2798,11 @@ window.__ModuleLoader__.load({ + contextWindowPlaceholder: "Uses the provider default", + maxTokens: "Max output tokens", + maxTokensPlaceholder: "Uses the provider default", +- modelAdvanced: "Capacities", ++ modelAdvanced: "Model settings", ++ modelImageInput: "Image input", ++ modelImageInputHint: "Declare that this model accepts images; the endpoint must support them.", ++ modelSearch: "Search models", ++ modelSearchEmpty: "No matching models.", + addModel: "Add model", + removeModel: "Delete model", + modelsEmpty: "No models will be shown in the selector. Unlisted IDs can still be sent directly.", +@@ -2595,10 +2846,14 @@ window.__ModuleLoader__.load({ welcomeBody: WELCOME_NOTICE_COPY.en.body, welcomeContinue: WELCOME_NOTICE_COPY.en.continueLabel, welcomeError: "The acknowledgement could not be saved. Please try again.", @@ -332,7 +643,7 @@ index 12ebd55..76d09ba 100644 onboardingSaving: "Saving…", keyRequired: "Enter an API key to continue." }; -@@ -2618,6 +2747,8 @@ window.__ModuleLoader__.load({ +@@ -2618,6 +2873,8 @@ window.__ModuleLoader__.load({ deleting: "正在删除 {provider}…", add: "添加提供方", provider: "提供方", @@ -341,7 +652,20 @@ index 12ebd55..76d09ba 100644 close: "关闭", cancel: "取消", apply: "保存", -@@ -2693,10 +2824,14 @@ window.__ModuleLoader__.load({ +@@ -2649,7 +2906,11 @@ window.__ModuleLoader__.load({ + contextWindowPlaceholder: "使用提供方默认值", + maxTokens: "最大输出 token 数", + maxTokensPlaceholder: "使用提供方默认值", +- modelAdvanced: "容量", ++ modelAdvanced: "模型设置", ++ modelImageInput: "支持图片输入", ++ modelImageInputHint: "声明该模型可接收图片;请确认接口实际支持。", ++ modelSearch: "搜索模型", ++ modelSearchEmpty: "没有找到匹配的模型。", + addModel: "添加模型", + removeModel: "删除模型", + modelsEmpty: "模型选择器中将不显示任何模型;目录外 ID 仍可直接发送。", +@@ -2693,10 +2954,14 @@ window.__ModuleLoader__.load({ welcomeBody: WELCOME_NOTICE_COPY.zh.body, welcomeContinue: WELCOME_NOTICE_COPY.zh.continueLabel, welcomeError: "暂时无法保存确认状态,请重试。", diff --git a/test/model-picker-patch.test.ts b/test/model-picker-patch.test.ts index 8f627356..d838ea03 100644 --- a/test/model-picker-patch.test.ts +++ b/test/model-picker-patch.test.ts @@ -49,3 +49,34 @@ describe('DSH Desktop available-model picker', () => { expect(patch).not.toContain('fetchSelectAll: "Select all"') }) }) + +describe('DSH Desktop model image-input declarations', () => { + it('renders one shared per-model control for both adapter field names', async () => { + const client = await readFile(settingsModelsClient, 'utf8') + + expect(client).toContain('function ModelImageInputToggle(props)') + expect(client).toContain('field: "inputModalities"') + expect(client).toContain('field: "input"') + expect(client).toContain('return enabled ? ["text", "image"] : ["text"]') + }) + + it('ships localized capability copy and an endpoint warning', async () => { + const client = await readFile(settingsModelsClient, 'utf8') + + expect(client).toContain('modelImageInput: "Image input"') + expect(client).toContain('the endpoint must support them') + expect(client).toContain('modelImageInput: "支持图片输入"') + expect(client).toContain('请确认接口实际支持') + }) + + it('captures the image-input control in the reproducible dependency patch', async () => { + const patch = await readFile( + patchPath('@deepseek-ai/dsh-client-ui-settings-models'), + 'utf8' + ) + + expect(patch).toContain('ModelImageInputToggle') + expect(patch).toContain('field: "inputModalities"') + expect(patch).toContain('field: "input"') + }) +}) diff --git a/test/model-selection-search-patch.test.ts b/test/model-selection-search-patch.test.ts new file mode 100644 index 00000000..5bad6b5d --- /dev/null +++ b/test/model-selection-search-patch.test.ts @@ -0,0 +1,97 @@ +import { readFile } from 'node:fs/promises' +import path from 'node:path' +import { describe, expect, it } from 'vitest' +import { patchPath, projectRoot } from './patch-path' + +const modelSelectionClient = path.join( + projectRoot, + 'node_modules', + '@deepseek-ai', + 'dsh-client-ui-model-selection', + 'lib', + 'client.js' +) + +interface ModelRow { + id: string + name: string + description?: string +} + +interface ModelGroup { + id: string + name: string + models: ModelRow[] +} + +async function loadFilter(): Promise< + (groups: ModelGroup[], query: string) => ModelGroup[] +> { + const client = await readFile(modelSelectionClient, 'utf8') + const source = client.match( + /function filterModelGroups\(groups, query\) \{[\s\S]*?\n\t\t\}/ + )?.[0] + + expect(source).toBeDefined() + return Function(`${source}; return filterModelGroups`)() as ( + groups: ModelGroup[], + query: string + ) => ModelGroup[] +} + +describe('composer model search', () => { + const groups: ModelGroup[] = [ + { + id: 'tokenrouter', + name: 'Token Router', + models: [ + { id: 'openai/gpt-5.6-luna', name: 'GPT-5.6 Luna' }, + { id: 'qwen/qwen3.7-max', name: 'Qwen 3.7 Max' } + ] + }, + { + id: 'deepseek', + name: 'DeepSeek', + models: [{ id: 'deepseek-v4-pro', name: 'DeepSeek V4 Pro' }] + } + ] + + it('filters model names and ids without losing provider grouping', async () => { + const filter = await loadFilter() + + expect(filter(groups, 'luna')).toEqual([ + { ...groups[0]!, models: [groups[0]!.models[0]!] } + ]) + expect(filter(groups, 'deepseek-v4')).toEqual([groups[1]!]) + expect(filter(groups, 'missing')).toEqual([]) + }) + + it('matches a provider by display name or id and keeps all its models', async () => { + const filter = await loadFilter() + + expect(filter(groups, 'token router')).toEqual([groups[0]!]) + expect(filter(groups, 'DEEPSEEK')).toEqual([groups[1]!]) + }) + + it('renders an accessible localized search field and empty state', async () => { + const client = await readFile(modelSelectionClient, 'utf8') + + expect(client).toContain('type: "search"') + expect(client).toContain('role: "searchbox"') + expect(client).toContain('search.placeholder": "搜索模型或服务商"') + expect(client).toContain('empty.search": "没有找到匹配的模型。"') + expect(client).toContain('search.placeholder": "Search models or providers"') + expect(client).toContain('empty.search": "No matching models."') + }) + + it('captures the search behavior in the reproducible dependency patch', async () => { + const patch = await readFile( + patchPath('@deepseek-ai/dsh-client-ui-model-selection'), + 'utf8' + ) + + expect(patch).toContain('function filterModelGroups(groups, query)') + expect(patch).toContain('IconSearchOutline16') + expect(patch).toContain('children: filteredGroups.map((group)') + }) +}) diff --git a/test/model-settings-catalog-ux-patch.test.ts b/test/model-settings-catalog-ux-patch.test.ts new file mode 100644 index 00000000..b2fdd33b --- /dev/null +++ b/test/model-settings-catalog-ux-patch.test.ts @@ -0,0 +1,101 @@ +import { readFile } from 'node:fs/promises' +import path from 'node:path' +import { describe, expect, it } from 'vitest' +import { patchPath, projectRoot } from './patch-path' + +const settingsModelsClient = path.join( + projectRoot, + 'node_modules', + '@deepseek-ai', + 'dsh-client-ui-settings-models', + 'lib', + 'client.js' +) + +interface ModelRow { + id?: string + name?: string +} + +async function loadSearch(): Promise< + (models: ModelRow[], query: string) => Array<{ model: ModelRow; index: number }> +> { + const client = await readFile(settingsModelsClient, 'utf8') + const source = client.match( + /function searchableModelEntries\(models, query\) \{[\s\S]*?\n\t\t\}/ + )?.[0] + + expect(source).toBeDefined() + return Function(`${source}; return searchableModelEntries`)() as ( + models: ModelRow[], + query: string + ) => Array<{ model: ModelRow; index: number }> +} + +describe('settings model catalog search', () => { + const models: ModelRow[] = [ + { id: 'qwen3.8-max', name: 'Qwen Max' }, + { id: 'deepseek-v4-pro', name: 'DeepSeek V4 Pro' }, + { id: 'openai/gpt-5.6-luna', name: 'Luna' } + ] + + it('matches model ids and display names while preserving source indexes', async () => { + const search = await loadSearch() + + expect(search(models, 'MAX')).toEqual([{ model: models[0]!, index: 0 }]) + expect(search(models, 'deepseek-v4')).toEqual([ + { model: models[1]!, index: 1 } + ]) + expect(search(models, 'luna')).toEqual([{ model: models[2]!, index: 2 }]) + expect(search(models, 'missing')).toEqual([]) + }) + + it('keeps every row and index when the query is blank', async () => { + const search = await loadSearch() + + expect(search(models, ' ')).toEqual( + models.map((model, index) => ({ model, index })) + ) + }) + + it('uses the shared search in both adapter catalog editors', async () => { + const client = await readFile(settingsModelsClient, 'utf8') + + expect(client.match(/jsx\)\(ModelCatalogSearch/g)).toHaveLength(2) + expect(client.match(/visibleModels\.map\(\(\{ model, index \}\)/g)).toHaveLength(2) + expect(client).toContain('modelSearch: "Search models"') + expect(client).toContain('modelSearch: "搜索模型"') + expect(client).toContain('modelSearchEmpty: "没有找到匹配的模型。"') + }) +}) + +describe('settings provider editor sticky actions', () => { + it('only freezes add, cancel, and submit while custom settings are expanded', async () => { + const client = await readFile(settingsModelsClient, 'utf8') + + expect(client).toContain('.dshProviderEditorStickyFooter{') + expect(client).toContain('position:sticky;bottom:-24px') + expect(client).toContain( + '.dshProviderEditorExpanded .zGbnIq_customizedBody{padding-bottom:72px}' + ) + expect(client).toContain('" dshProviderEditorExpanded"') + expect(client).toContain( + 'props.credentialOnly === true || !customizedOpen || layout === "unknown"' + ) + expect(client).toContain('className: "dshProviderEditorStickyFooter"') + expect(client).toContain('onClick: addModel') + expect(client).toContain('jsx)(EditorFooter, { ...footerProps })') + }) + + it('captures search and sticky actions in the reproducible package patch', async () => { + const patch = await readFile( + patchPath('@deepseek-ai/dsh-client-ui-settings-models'), + 'utf8' + ) + + expect(patch).toContain('function searchableModelEntries(models, query)') + expect(patch).toContain('ModelCatalogSearch') + expect(patch).toContain('dshProviderEditorStickyFooter') + expect(patch).toContain('onClick: addModel') + }) +})