@klarkxy/dsh-safe-auto 0.5.2 → 0.5.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -1423,6 +1423,51 @@ ${scope} {
1423
1423
  `.replace(/^& (\.dsh-ui-[a-z0-9-]+)(?=\s*\{)/gm, (_match, className) => `&${className}, & ${className}`);
1424
1424
  }
1425
1425
  //#endregion
1426
+ //#region ../dsh-plugin-kit/lib/contracts.js
1427
+ /** `@klarkxy/dsh-git-commit` becomes `KlarkxyDshGitCommitModelSettingsChanges`. */
1428
+ function modelSettingsChangesIdentity(packageName) {
1429
+ const scoped = /^@([^/]+)\/(.+)$/.exec(packageName);
1430
+ const parts = (scoped ? `${scoped[1]}/${scoped[2]}` : packageName).split(/[^A-Za-z0-9]+/).filter(Boolean);
1431
+ if (!parts.length) throw new Error("Model settings package name has no RPC segment");
1432
+ const segment = `${parts.map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("")}ModelSettingsChanges`;
1433
+ if (!/^[A-Z][A-Za-z0-9]*$/.test(segment)) throw new Error(`Model settings sync name is not an RPC segment: ${segment}`);
1434
+ return {
1435
+ service: segment,
1436
+ namespace: segment
1437
+ };
1438
+ }
1439
+ /**
1440
+ * Shared "Session Management" settings section id. The section is provided by
1441
+ * shared infrastructure (see `registerSessionSettingsItem` in `client-utils`),
1442
+ * not by any one business plugin: every participant registers only a row into
1443
+ * {@link SESSION_SETTINGS_ITEM_SLOT}, so any enabled subset yields the same page.
1444
+ */
1445
+ const SESSION_SETTINGS_SECTION_ID = "session-manager";
1446
+ /**
1447
+ * Child list slot declared by the shared Session Management page. Neutral
1448
+ * naming on purpose: participants never import each other, they only
1449
+ * contribute `{id, order}` rows into this slot while the page is alive.
1450
+ */
1451
+ const SESSION_SETTINGS_ITEM_SLOT = "dsh.session-settings.item";
1452
+ /**
1453
+ * Host-owned nav icon seat: a root-scoped list where each entry's `id` must
1454
+ * equal the `settings.section` id it decorates. Owner props are
1455
+ * `{size, active}`; icons stay `aria-hidden` so the section label remains the
1456
+ * accessible name. Contributors into a shared section may register the same
1457
+ * icon `id` with different priorities — the ledger keeps every entry while the
1458
+ * host renders one icon per section.
1459
+ */
1460
+ const SETTINGS_SECTION_ICON_SLOT = "settings.section.icon";
1461
+ /** Authenticated host-RPC channel for get/set of the shared default. */
1462
+ const AUXILIARY_MODEL_CHANNEL = "/dsh-auxiliary-model";
1463
+ /** Model-settings namespace under which the shared default is published. */
1464
+ const AUXILIARY_MODEL_NS = "@klarkxy/dsh-plugin-kit/auxiliary-model";
1465
+ /**
1466
+ * Typert identity of the shared change stream. Mounted on the root context
1467
+ * with ownerNs filtering, so frames only carry {@link AUXILIARY_MODEL_NS}.
1468
+ */
1469
+ const AUXILIARY_MODEL_CHANGES_IDENTITY = modelSettingsChangesIdentity("@klarkxy/dsh-plugin-kit");
1470
+ //#endregion
1426
1471
  //#region ../dsh-plugin-kit/lib/client-utils.js
1427
1472
  /** Fixed pause for failing background reads; manual work is never replayed. */
1428
1473
  function createPluginReadGate(now = Date.now) {
@@ -1448,18 +1493,118 @@ function createPluginReadGate(now = Date.now) {
1448
1493
  }
1449
1494
  };
1450
1495
  }
1451
- //#endregion
1452
- //#region ../dsh-plugin-kit/lib/contracts.js
1453
- /** `@klarkxy/dsh-git-commit` becomes `KlarkxyDshGitCommitModelSettingsChanges`. */
1454
- function modelSettingsChangesIdentity(packageName) {
1455
- const scoped = /^@([^/]+)\/(.+)$/.exec(packageName);
1456
- const parts = (scoped ? `${scoped[1]}/${scoped[2]}` : packageName).split(/[^A-Za-z0-9]+/).filter(Boolean);
1457
- if (!parts.length) throw new Error("Model settings package name has no RPC segment");
1458
- const segment = `${parts.map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("")}ModelSettingsChanges`;
1459
- if (!/^[A-Z][A-Za-z0-9]*$/.test(segment)) throw new Error(`Model settings sync name is not an RPC segment: ${segment}`);
1460
- return {
1461
- service: segment,
1462
- namespace: segment
1496
+ /** One `<svg>` with the shared icon chrome; children are plain elements. */
1497
+ function lineIcon(children) {
1498
+ return function SettingsSectionIcon(props) {
1499
+ return (0, react.createElement)("svg", {
1500
+ "aria-hidden": "true",
1501
+ width: props.size ?? 16,
1502
+ height: props.size ?? 16,
1503
+ viewBox: "0 0 16 16",
1504
+ fill: "none",
1505
+ stroke: "currentColor",
1506
+ strokeWidth: 1,
1507
+ strokeLinecap: "round",
1508
+ strokeLinejoin: "round"
1509
+ }, ...children.map(([tag, attrs], index) => (0, react.createElement)(tag, {
1510
+ key: index,
1511
+ ...attrs
1512
+ })));
1513
+ };
1514
+ }
1515
+ /** Chat bubble for the shared Session Management section. */
1516
+ const IconSessionSettings = lineIcon([["path", { d: "M8.5 13a5.5 5.5 0 0 0 5.5-5.5A5.5 5.5 0 0 0 8.5 2 5.5 5.5 0 0 0 3 7.5a5.48 5.48 0 0 0 .9 3L2.8 12.6a.5.5 0 0 0 .62.62l2.9-.98a5.48 5.48 0 0 0 2.18.76Z" }]]);
1517
+ /** Private Cordis rendezvous; every bundled copy of this helper resolves the same root service. */
1518
+ const SESSION_SETTINGS_PAGE_SERVICE = "dshSessionSettingsPage";
1519
+ /** The page component renders only its declared child list: items paint their own titles and styles. */
1520
+ function SessionSettingsPageView(props) {
1521
+ return props.renderSlot(SESSION_SETTINGS_ITEM_SLOT, {});
1522
+ }
1523
+ /**
1524
+ * Acquire the root-owned page service, creating and providing it on first use.
1525
+ * One counter covers participants, the section wait and the provider: the last
1526
+ * release removes the wait and then the provider synchronously (Cordis deletes
1527
+ * the impl before its async disposer awaits), so a later acquire sees a clean
1528
+ * get() miss and can re-establish the page.
1529
+ */
1530
+ function acquireSessionSettingsPage(root) {
1531
+ const existing = root.get(SESSION_SETTINGS_PAGE_SERVICE, false);
1532
+ if (existing) return existing;
1533
+ const slots = root.slots;
1534
+ let users = 0;
1535
+ let stopPage;
1536
+ let unprovide;
1537
+ const page = {
1538
+ retain() {
1539
+ users += 1;
1540
+ if (!stopPage) stopPage = slots.inject("settings.section", () => slots.register({
1541
+ name: "settings.section",
1542
+ id: SESSION_SETTINGS_SECTION_ID,
1543
+ order: 30,
1544
+ label: () => {
1545
+ const locale = root.get("locale", false);
1546
+ return String(locale?.getSnapshot?.().active ?? "").startsWith("zh") ? "会话管理" : "Session Management";
1547
+ },
1548
+ children: { [SESSION_SETTINGS_ITEM_SLOT]: {
1549
+ kind: "list",
1550
+ scope: "root"
1551
+ } }
1552
+ }, SessionSettingsPageView));
1553
+ },
1554
+ release() {
1555
+ if (users <= 0) return;
1556
+ users -= 1;
1557
+ if (users > 0) return;
1558
+ stopPage?.();
1559
+ stopPage = void 0;
1560
+ unprovide?.();
1561
+ }
1562
+ };
1563
+ unprovide = root.provide(SESSION_SETTINGS_PAGE_SERVICE, page);
1564
+ return page;
1565
+ }
1566
+ /**
1567
+ * Contribute one row to the shared Session Management settings page.
1568
+ *
1569
+ * `ctx` is the plugin's own context: the row's slot wait unloads with the
1570
+ * plugin, while the page itself is retained on the shared root context for as
1571
+ * long as any contributor lives. There is no fallback seat — when every
1572
+ * contributor is gone the section disappears with the page. Returns a disposer.
1573
+ * Shared root-owned infrastructure (the auxiliary-model card) calls this on
1574
+ * `ctx.root` so its row, icon and page share are owned by the root lifetime
1575
+ * and released by the returned disposer, not by any plugin fiber.
1576
+ */
1577
+ function registerSessionSettingsItem(ctx, item, component) {
1578
+ const slots = ctx.slots;
1579
+ const root = ctx.root;
1580
+ const page = acquireSessionSettingsPage(root);
1581
+ let stopItem;
1582
+ let stopIcon;
1583
+ try {
1584
+ page.retain();
1585
+ stopItem = slots.inject(SESSION_SETTINGS_ITEM_SLOT, () => slots.register({
1586
+ name: SESSION_SETTINGS_ITEM_SLOT,
1587
+ ...item
1588
+ }, component));
1589
+ stopIcon = slots.inject(SETTINGS_SECTION_ICON_SLOT, () => slots.register({
1590
+ name: SETTINGS_SECTION_ICON_SLOT,
1591
+ id: SESSION_SETTINGS_SECTION_ID,
1592
+ order: item.order,
1593
+ priority: item.order
1594
+ }, IconSessionSettings));
1595
+ } catch (error) {
1596
+ stopIcon?.();
1597
+ stopItem?.();
1598
+ page.release();
1599
+ throw error;
1600
+ }
1601
+ let disposed = false;
1602
+ return () => {
1603
+ if (disposed) return;
1604
+ disposed = true;
1605
+ stopIcon?.();
1606
+ stopItem?.();
1607
+ page.release();
1463
1608
  };
1464
1609
  }
1465
1610
  //#endregion
@@ -1631,6 +1776,488 @@ function createModelSettingsSubscriber(ctx, packageName, identity = modelSetting
1631
1776
  } });
1632
1777
  }
1633
1778
  //#endregion
1779
+ //#region ../dsh-plugin-kit/lib/auxiliary-model-client.js
1780
+ const AUXILIARY_MODEL_PACKAGE = "@klarkxy/dsh-plugin-kit";
1781
+ const AUXILIARY_MODEL_SEAT_ID = "auxiliary-model";
1782
+ /** Private Cordis rendezvous; every bundled copy resolves the same root service. */
1783
+ const AUXILIARY_MODEL_CARD_SERVICE = "dshAuxiliaryModelSettingsCard";
1784
+ /** Theme-token-only stylesheet: officialUiCss scope plus the shared ModelMenu recipe. */
1785
+ const auxiliaryModelCss = `${officialUiCss("ams-root")}${modelMenuCss}
1786
+ .ams-root .ams-row { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; min-width: 0; }
1787
+ `;
1788
+ const auxiliaryCopy = {
1789
+ zh: {
1790
+ title: "默认辅助模型",
1791
+ help: "各辅助功能(如标题生成、摘要)没有单独指定模型时使用的模型;仅影响声明使用它的功能。",
1792
+ model: "辅助模型",
1793
+ modelDefault: "跟随会话或宿主默认",
1794
+ modelHint: "不选择模型时,各功能按自己的规则决定对话模型。",
1795
+ effortDefault: "跟随模型默认",
1796
+ loading: "正在读取设置…",
1797
+ failed: "无法读取模型设置。",
1798
+ stale: "设置已被其他窗口更新,请刷新后重试。",
1799
+ saveFailed: "保存失败,原设置未更改。请重试。",
1800
+ saved: "已保存。",
1801
+ retry: "重试",
1802
+ refresh: "刷新设置",
1803
+ syncUnavailable: "自动刷新不可用,请手动刷新设置。"
1804
+ },
1805
+ en: {
1806
+ title: "Default Auxiliary Model",
1807
+ help: "The model used by auxiliary features (such as title generation or summarizing) that do not name their own model; only features that opted in are affected.",
1808
+ model: "Auxiliary model",
1809
+ modelDefault: "Follow session or host default",
1810
+ modelHint: "With no model selected, each feature follows its own rules for the conversation model.",
1811
+ effortDefault: "Follow model default",
1812
+ loading: "Loading settings…",
1813
+ failed: "Could not load model settings.",
1814
+ stale: "Settings were changed elsewhere; refresh and retry.",
1815
+ saveFailed: "Save failed; the previous setting was not changed. Try again.",
1816
+ saved: "Saved.",
1817
+ retry: "Retry",
1818
+ refresh: "Refresh settings",
1819
+ syncUnavailable: "Automatic refresh unavailable; refresh settings manually."
1820
+ }
1821
+ };
1822
+ /** A failed RPC keeps its code so the card can translate known rejections. */
1823
+ var AuxiliaryRpcError = class extends Error {
1824
+ code;
1825
+ constructor(code, message) {
1826
+ super(message);
1827
+ this.code = code;
1828
+ this.name = "AuxiliaryRpcError";
1829
+ }
1830
+ };
1831
+ /** Host UI language, following live switches with the host's English fallback. */
1832
+ function useAuxiliaryLocale(client) {
1833
+ const subscribe = (0, react.useCallback)((listener) => client.locale?.subscribe(listener) ?? (() => {}), [client]);
1834
+ const snapshot = (0, react.useCallback)(() => client.locale?.getSnapshot().active ?? "en", [client]);
1835
+ return String((0, react.useSyncExternalStore)(subscribe, snapshot, snapshot)).startsWith("zh") ? "zh" : "en";
1836
+ }
1837
+ function isAbort(error) {
1838
+ return error instanceof Error && (error.name === "AbortError" || /abort|cancel/i.test(error.message));
1839
+ }
1840
+ function isStaleRejection(error) {
1841
+ if (!(error instanceof AuxiliaryRpcError)) return false;
1842
+ return error.code === "stale" || error.code === "auxiliary-model/conflict" || /changed|revision|reload|stale|conflict/i.test(error.message);
1843
+ }
1844
+ async function callAuxiliaryRpc(client, endpoint, payload = {}) {
1845
+ const result = await client.connection.rpc.call(AUXILIARY_MODEL_CHANNEL, endpoint, payload);
1846
+ if (!result || result.ok !== true) {
1847
+ const error = result && "error" in result ? result.error : void 0;
1848
+ throw new AuxiliaryRpcError(error?.code ?? "unknown", error?.message ?? `RPC ${endpoint} failed`);
1849
+ }
1850
+ return result.value;
1851
+ }
1852
+ /**
1853
+ * The shared card. All state comes from the shared host service over the
1854
+ * plugin-kit RPC channel; the card only renders the stored row.
1855
+ */
1856
+ function AuxiliaryModelCard({ client, subscribe }) {
1857
+ const language = useAuxiliaryLocale(client);
1858
+ const text = auxiliaryCopy[language];
1859
+ const [settings, setSettings] = (0, react.useState)();
1860
+ const catalogState = useModelCatalog((0, react.useCallback)(() => readModelCatalog(client), [client]), { subscribe: (0, react.useCallback)((change) => client.connection.generation?.subscribe(change), [client]) });
1861
+ const { refresh: refreshCatalog } = catalogState;
1862
+ const choices = parseModelMenuChoices(catalogState.catalog, settings?.model ?? void 0);
1863
+ const [menuOpen, setMenuOpen] = (0, react.useState)(false);
1864
+ const [error, setError] = (0, react.useState)("");
1865
+ const [note, setNote] = (0, react.useState)(false);
1866
+ const [busy, setBusy] = (0, react.useState)(false);
1867
+ const [syncError, setSyncError] = (0, react.useState)("");
1868
+ const operation = (0, react.useRef)({
1869
+ epoch: 0,
1870
+ active: false,
1871
+ reading: false,
1872
+ writing: false,
1873
+ pending: false
1874
+ });
1875
+ const refreshRef = (0, react.useRef)(() => {});
1876
+ const refresh = () => {
1877
+ const state = operation.current;
1878
+ if (!state.active) return;
1879
+ if (state.reading || state.writing) {
1880
+ state.pending = true;
1881
+ return;
1882
+ }
1883
+ state.pending = false;
1884
+ state.reading = true;
1885
+ setBusy(true);
1886
+ const epoch = state.epoch;
1887
+ callAuxiliaryRpc(client, "defaults.get").then((next) => {
1888
+ if (state.active && state.epoch === epoch && !state.pending) {
1889
+ setSettings(next);
1890
+ setError("");
1891
+ }
1892
+ }).catch((reason) => {
1893
+ if (state.active && state.epoch === epoch && !state.pending && !isAbort(reason)) setError("failed");
1894
+ }).finally(() => {
1895
+ if (!state.active || state.epoch !== epoch) return;
1896
+ state.reading = false;
1897
+ setBusy(false);
1898
+ if (state.pending) refreshRef.current();
1899
+ });
1900
+ };
1901
+ refreshRef.current = refresh;
1902
+ (0, react.useEffect)(() => {
1903
+ const state = operation.current;
1904
+ state.epoch++;
1905
+ state.active = true;
1906
+ state.reading = false;
1907
+ state.writing = false;
1908
+ state.pending = false;
1909
+ setBusy(false);
1910
+ setSettings(void 0);
1911
+ setError("");
1912
+ setNote(false);
1913
+ refreshRef.current();
1914
+ return () => {
1915
+ state.active = false;
1916
+ state.epoch++;
1917
+ };
1918
+ }, [client]);
1919
+ (0, react.useEffect)(() => {
1920
+ setSyncError("");
1921
+ let live = true;
1922
+ let stop;
1923
+ try {
1924
+ stop = subscribe?.(() => {
1925
+ if (live) {
1926
+ setSyncError("");
1927
+ refreshCatalog();
1928
+ refreshRef.current();
1929
+ }
1930
+ }, (message) => {
1931
+ if (live) setSyncError(message);
1932
+ });
1933
+ } catch {
1934
+ if (live) setSyncError("Model settings sync service unavailable");
1935
+ }
1936
+ return () => {
1937
+ live = false;
1938
+ try {
1939
+ stop?.();
1940
+ } catch {}
1941
+ };
1942
+ }, [subscribe, refreshCatalog]);
1943
+ const save = (0, react.useCallback)((model) => {
1944
+ const state = operation.current;
1945
+ if (!settings || !state.active || state.writing || state.reading) return;
1946
+ state.writing = true;
1947
+ const epoch = state.epoch;
1948
+ setBusy(true);
1949
+ setNote(false);
1950
+ setError("");
1951
+ callAuxiliaryRpc(client, "defaults.set", {
1952
+ model: model ?? {
1953
+ provider: "",
1954
+ model: ""
1955
+ },
1956
+ expectedRevision: settings.revision
1957
+ }).then((next) => {
1958
+ if (state.active && state.epoch === epoch) {
1959
+ setSettings(next);
1960
+ setNote(true);
1961
+ refreshCatalog();
1962
+ }
1963
+ }).catch((reason) => {
1964
+ if (state.active && state.epoch === epoch) setError(isStaleRejection(reason) ? "stale" : "saveFailed");
1965
+ }).finally(() => {
1966
+ if (!state.active || state.epoch !== epoch) return;
1967
+ state.writing = false;
1968
+ setBusy(false);
1969
+ if (state.pending) refreshRef.current();
1970
+ });
1971
+ }, [
1972
+ client,
1973
+ settings,
1974
+ refreshCatalog
1975
+ ]);
1976
+ const locked = busy;
1977
+ if (!settings) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
1978
+ className: "ams-root dsh-ui-card",
1979
+ "data-testid": "auxiliary-model-settings",
1980
+ children: [
1981
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1982
+ className: "dsh-ui-heading",
1983
+ children: text.title
1984
+ }),
1985
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1986
+ className: "dsh-ui-help",
1987
+ children: text.help
1988
+ }),
1989
+ error ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1990
+ role: "alert",
1991
+ className: "dsh-ui-error",
1992
+ children: text[error]
1993
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1994
+ role: "status",
1995
+ className: "dsh-ui-hint",
1996
+ children: text.loading
1997
+ }),
1998
+ syncError && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1999
+ role: "status",
2000
+ className: "dsh-ui-hint",
2001
+ children: text.syncUnavailable
2002
+ }),
2003
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2004
+ className: "dsh-ui-actions",
2005
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
2006
+ variant: "ghost",
2007
+ size: "sm",
2008
+ disabled: locked,
2009
+ onClick: () => refreshRef.current(),
2010
+ children: text.retry
2011
+ })
2012
+ })
2013
+ ]
2014
+ });
2015
+ const current = settings.model ?? {
2016
+ provider: "",
2017
+ model: ""
2018
+ };
2019
+ const follow = !current.provider || !current.model;
2020
+ const selected = follow ? void 0 : choices.find((item) => item.provider === current.provider && item.model === current.model);
2021
+ const efforts = follow ? [] : modelMenuEffortOptions(selected, current.reasoningEffort);
2022
+ const currentLabel = follow ? text.modelDefault : selected?.label ?? `${current.provider} / ${current.model}`;
2023
+ const selectedEffort = efforts.find((item) => item.id === (current.reasoningEffort || ""));
2024
+ const effortName = selectedEffort ? modelMenuEffortLabel(selectedEffort, language) : void 0;
2025
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
2026
+ className: "ams-root dsh-ui-card",
2027
+ "data-testid": "auxiliary-model-settings",
2028
+ children: [
2029
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2030
+ className: "dsh-ui-heading",
2031
+ children: text.title
2032
+ }),
2033
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2034
+ className: "dsh-ui-help",
2035
+ children: text.help
2036
+ }),
2037
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2038
+ className: "dsh-ui-field",
2039
+ children: [
2040
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2041
+ className: "dsh-ui-label",
2042
+ children: text.model
2043
+ }),
2044
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelCatalogStatus, {
2045
+ state: catalogState,
2046
+ locale: language
2047
+ }),
2048
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelMenu, {
2049
+ locale: language,
2050
+ open: menuOpen && !locked,
2051
+ onOpenChange: (next) => setMenuOpen(next && !locked),
2052
+ listClassName: "ams-root ams-menu-portal",
2053
+ anchor: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2054
+ type: "button",
2055
+ className: "dsh-model-menu-trigger",
2056
+ disabled: locked,
2057
+ "aria-label": `${text.model}: ${currentLabel}`,
2058
+ children: [
2059
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2060
+ className: "dsh-model-menu-triggerLabel",
2061
+ children: currentLabel
2062
+ }),
2063
+ !follow && efforts.length ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2064
+ className: "dsh-model-menu-triggerEffort",
2065
+ children: effortName ?? text.effortDefault
2066
+ }) : null,
2067
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutlineRegular, { className: `dsh-model-menu-chevron${menuOpen && !locked ? " dsh-model-menu-chevronOpen" : ""}` })
2068
+ ]
2069
+ }),
2070
+ choices,
2071
+ selected: follow ? void 0 : {
2072
+ provider: current.provider,
2073
+ model: current.model
2074
+ },
2075
+ onPick: (picked) => save({
2076
+ provider: picked.provider,
2077
+ model: picked.model
2078
+ }),
2079
+ leading: [{
2080
+ id: "default",
2081
+ label: text.modelDefault,
2082
+ selected: follow
2083
+ }],
2084
+ onPickLeading: () => save(null),
2085
+ efforts,
2086
+ selectedEffort: current.reasoningEffort || "",
2087
+ onPickEffort: (effort) => save(follow ? null : {
2088
+ provider: current.provider,
2089
+ model: current.model,
2090
+ ...effort ? { reasoningEffort: effort } : {}
2091
+ }),
2092
+ modelLabel: text.model,
2093
+ modelValue: currentLabel,
2094
+ defaultEffortLabel: text.effortDefault
2095
+ }),
2096
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2097
+ className: "dsh-ui-hint",
2098
+ children: text.modelHint
2099
+ })
2100
+ ]
2101
+ }),
2102
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2103
+ className: "ams-row",
2104
+ children: [
2105
+ syncError && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2106
+ role: "status",
2107
+ className: "dsh-ui-hint",
2108
+ children: text.syncUnavailable
2109
+ }),
2110
+ error && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2111
+ role: "alert",
2112
+ className: "dsh-ui-error",
2113
+ children: text[error]
2114
+ }),
2115
+ note && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2116
+ role: "status",
2117
+ className: "dsh-ui-notice",
2118
+ children: text.saved
2119
+ }),
2120
+ error || syncError ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
2121
+ variant: "ghost",
2122
+ size: "sm",
2123
+ disabled: locked,
2124
+ onClick: () => refreshRef.current(),
2125
+ children: text.refresh
2126
+ }) : null
2127
+ ]
2128
+ })
2129
+ ]
2130
+ });
2131
+ }
2132
+ const AUXILIARY_MODEL_WATCH_COORDINATOR = "dshAuxiliaryModelWatchCoordinator";
2133
+ function acquireWatchCoordinator(root) {
2134
+ const existing = root.get(AUXILIARY_MODEL_WATCH_COORDINATOR, false);
2135
+ if (existing) return existing;
2136
+ let unprovide;
2137
+ const coordinator = {
2138
+ tail: Promise.resolve(),
2139
+ active: 0,
2140
+ mount() {
2141
+ coordinator.active += 1;
2142
+ const previousTail = coordinator.tail;
2143
+ const lease = {
2144
+ closed: false,
2145
+ done() {
2146
+ if (lease.closed) return;
2147
+ lease.closed = true;
2148
+ coordinator.active -= 1;
2149
+ const owned = lease.fiber;
2150
+ lease.fiber = void 0;
2151
+ coordinator.tail = coordinator.tail.then(() => Promise.resolve(owned?.dispose()).catch(() => {}));
2152
+ coordinator.tail.then(() => {
2153
+ if (coordinator.active > 0) return;
2154
+ try {
2155
+ if (root.get(AUXILIARY_MODEL_WATCH_COORDINATOR, false) === coordinator) Promise.resolve(unprovide?.()).catch(() => {});
2156
+ } catch {}
2157
+ });
2158
+ },
2159
+ fiber: void 0
2160
+ };
2161
+ previousTail.then(() => {
2162
+ if (lease.closed) return;
2163
+ try {
2164
+ lease.fiber = root.inject(["remote"], (injected) => {
2165
+ const scope = injected;
2166
+ const mounting = scope.remote.$mount(modelSettingsChangesRemote(AUXILIARY_MODEL_PACKAGE, AUXILIARY_MODEL_CHANGES_IDENTITY));
2167
+ mounting.catch(() => {});
2168
+ scope.effect(() => async () => {
2169
+ await (await mounting.catch(() => void 0))?.();
2170
+ });
2171
+ });
2172
+ } catch {}
2173
+ });
2174
+ return lease;
2175
+ }
2176
+ };
2177
+ unprovide = root.provide(AUXILIARY_MODEL_WATCH_COORDINATOR, coordinator);
2178
+ return coordinator;
2179
+ }
2180
+ /**
2181
+ * Acquire the root-owned card service, creating it on first use. One counter
2182
+ * covers participants, the seat wait and the change-stream mount: the last
2183
+ * release removes the wait, then the watch contribution, then the provider
2184
+ * itself synchronously (Cordis deletes the impl before its async disposer
2185
+ * awaits), so a later acquire sees a clean get() miss and can re-establish.
2186
+ */
2187
+ function acquireAuxiliaryModelCard(root, ctx) {
2188
+ const existing = root.get(AUXILIARY_MODEL_CARD_SERVICE, false);
2189
+ if (existing) return existing;
2190
+ let users = 0;
2191
+ let styleElement;
2192
+ let stopItem;
2193
+ let unprovide;
2194
+ const watchLease = acquireWatchCoordinator(root).mount();
2195
+ const subscriber = createModelSettingsSubscriber(ctx.root, AUXILIARY_MODEL_PACKAGE, AUXILIARY_MODEL_CHANGES_IDENTITY, (ns) => ns === AUXILIARY_MODEL_NS);
2196
+ const render = () => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AuxiliaryModelCard, {
2197
+ client: root,
2198
+ subscribe: subscriber.subscribe
2199
+ });
2200
+ const service = {
2201
+ retain() {
2202
+ users += 1;
2203
+ if (styleElement === void 0 && typeof document !== "undefined") {
2204
+ styleElement = document.createElement("style");
2205
+ styleElement.setAttribute("data-plugin", "dsh-auxiliary-model-shared");
2206
+ styleElement.textContent = auxiliaryModelCss;
2207
+ document.head.appendChild(styleElement);
2208
+ }
2209
+ if (!stopItem) stopItem = registerSessionSettingsItem(ctx.root, {
2210
+ id: AUXILIARY_MODEL_SEAT_ID,
2211
+ order: 55
2212
+ }, render);
2213
+ },
2214
+ release() {
2215
+ if (users <= 0) return;
2216
+ users -= 1;
2217
+ if (users > 0) return;
2218
+ stopItem?.();
2219
+ stopItem = void 0;
2220
+ styleElement?.remove();
2221
+ styleElement = void 0;
2222
+ watchLease.done();
2223
+ try {
2224
+ Promise.resolve(unprovide?.()).catch(() => {});
2225
+ } catch {}
2226
+ unprovide = void 0;
2227
+ }
2228
+ };
2229
+ unprovide = root.provide(AUXILIARY_MODEL_CARD_SERVICE, service);
2230
+ return service;
2231
+ }
2232
+ /**
2233
+ * Show the shared "default auxiliary model" card while the calling plugin is
2234
+ * enabled.
2235
+ *
2236
+ * `ctx` is the plugin's own client context: the card itself is retained on the
2237
+ * shared root context for as long as any participant lives, so an arbitrary
2238
+ * subset of enabled plugins yields the same single card. The mount's slot wait
2239
+ * and the change-stream contribution are owned by the root service — the first
2240
+ * participant's unload cannot take the card down. Returns a disposer; it may
2241
+ * be invoked by both ctx.effect and an explicit caller and releases at most
2242
+ * once, so a double call cannot unbalance the shared refcount.
2243
+ */
2244
+ function registerAuxiliaryModelSettings(ctx) {
2245
+ const root = ctx.root;
2246
+ const service = acquireAuxiliaryModelCard(root, ctx);
2247
+ try {
2248
+ service.retain();
2249
+ } catch (error) {
2250
+ service.release();
2251
+ throw error;
2252
+ }
2253
+ let disposed = false;
2254
+ return () => {
2255
+ if (disposed) return;
2256
+ disposed = true;
2257
+ service.release();
2258
+ };
2259
+ }
2260
+ //#endregion
1634
2261
  //#region src/client.jsx
1635
2262
  const h = react.default.createElement;
1636
2263
  const name = "dsh-safe-auto-client";
@@ -1664,7 +2291,7 @@ const TEXT = {
1664
2291
  unknownConfigField: (field) => `未知配置项:${field}`,
1665
2292
  externalChange: "配置已在别处修改。草稿已保留,请刷新并丢弃草稿后重新确认。",
1666
2293
  syncUnavailable: "自动刷新不可用,请手动刷新配置。",
1667
- follow: "跟随当前对话",
2294
+ follow: "默认辅助模型",
1668
2295
  searchModels: "搜索模型…",
1669
2296
  noModels: "没有匹配的模型",
1670
2297
  clearSearch: "清除搜索",
@@ -1672,7 +2299,7 @@ const TEXT = {
1672
2299
  model: "模型",
1673
2300
  effort: "思考强度",
1674
2301
  effortDefault: "默认",
1675
- followHelp: "跟随对话:使用当前会话的模型与默认思考强度。",
2302
+ followHelp: "默认辅助模型:优先使用全局辅助模型,未设置时使用当前会话的模型与默认思考强度。",
1676
2303
  savedEffortRetained: (effort) => `已保存的强度「${effort}」保留,需在 profile 中检查。`,
1677
2304
  modelAbsent: "当前模型不在目录中,保留已保存配置。",
1678
2305
  effortAbsent: "已保存的思考强度不在目录中,不会自动替换。",
@@ -1682,6 +2309,7 @@ const TEXT = {
1682
2309
  workspaceWrite: "工作区内修改",
1683
2310
  fullAccess: "完全权限",
1684
2311
  safeAuto: "安全自动",
2312
+ groupHint: "审核模型、审核说明与审核历史",
1685
2313
  unavailableShort: "当前不可用",
1686
2314
  refreshRetry: (message) => `${message}。重新打开菜单后重试。`,
1687
2315
  panelLabel: "安全自动设置",
@@ -1701,8 +2329,7 @@ const TEXT = {
1701
2329
  save: "保存",
1702
2330
  refreshDiscard: "刷新(丢弃草稿)",
1703
2331
  saveFailed: (message) => `${message}。草稿已保留,刷新后重新编辑,不会自动重试。`,
1704
- saved: "已保存",
1705
- summary: "安全自动:需要审批的请求交给审核模型判断,通过才放行这一次。"
2332
+ saved: "已保存"
1706
2333
  },
1707
2334
  en: {
1708
2335
  requestFailed: "Request failed",
@@ -1725,7 +2352,7 @@ const TEXT = {
1725
2352
  unknownConfigField: (field) => `Unknown configuration field: ${field}`,
1726
2353
  externalChange: "Settings changed elsewhere. Draft preserved; refresh and discard it before reviewing changes.",
1727
2354
  syncUnavailable: "Automatic refresh unavailable; refresh settings manually.",
1728
- follow: "Follow conversation",
2355
+ follow: "Default auxiliary model",
1729
2356
  searchModels: "Search models…",
1730
2357
  noModels: "No matching models",
1731
2358
  clearSearch: "Clear search",
@@ -1733,7 +2360,7 @@ const TEXT = {
1733
2360
  model: "Model",
1734
2361
  effort: "Reasoning effort",
1735
2362
  effortDefault: "Default",
1736
- followHelp: "Follows the conversation model with the default reasoning effort.",
2363
+ followHelp: "The global auxiliary model when it is set; otherwise the current conversation model with the default reasoning effort.",
1737
2364
  savedEffortRetained: (effort) => `Saved effort "${effort}" retained; check the profile.`,
1738
2365
  modelAbsent: "This model is not in the catalog; the saved configuration is kept.",
1739
2366
  effortAbsent: "The saved effort is not advertised; it will not be replaced automatically.",
@@ -1743,6 +2370,7 @@ const TEXT = {
1743
2370
  workspaceWrite: "Workspace Write",
1744
2371
  fullAccess: "Full Access",
1745
2372
  safeAuto: "Safe Auto",
2373
+ groupHint: "Reviewer model, safety notes and review history",
1746
2374
  unavailableShort: "unavailable",
1747
2375
  refreshRetry: (message) => `${message}. Reopen the menu and retry.`,
1748
2376
  panelLabel: "Safe Auto settings",
@@ -1762,8 +2390,7 @@ const TEXT = {
1762
2390
  save: "Save",
1763
2391
  refreshDiscard: "Refresh (discard draft)",
1764
2392
  saveFailed: (message) => `${message}. Draft retained; refresh before editing again. No automatic retry.`,
1765
- saved: "Saved",
1766
- summary: "Safe Auto: a reviewer model answers approval requests and approves only the calls that pass."
2393
+ saved: "Saved"
1767
2394
  }
1768
2395
  };
1769
2396
  /** The host locale may arrive as the injected service or as a plain tag. */
@@ -1905,6 +2532,17 @@ const css = `${officialUiCss("dsh-safe-auto")}${modelMenuCss}
1905
2532
  /* A fieldset is the settings group and takes the contract's card surface; its
1906
2533
  * legend keeps the UA inline padding from indenting the group title. */
1907
2534
  .dsh-safe-auto legend { padding: 0; }
2535
+ /* The shared column row mirrors the other items' geometry: 16px padding, a
2536
+ * 0.5px l2 rule the shell drops on the last item, 24px label-control gutter. */
2537
+ .dsh-safe-auto.sa-item { padding: 0; border-bottom: 0.5px solid var(--dsw-alias-border-l2); }
2538
+ .dsh-safe-auto .sa-item-summary { display: flex; align-items: center; justify-content: space-between; gap: 24px; padding: 16px 0; list-style: none; cursor: pointer; }
2539
+ .dsh-safe-auto .sa-item-summary::-webkit-details-marker { display: none; }
2540
+ .dsh-safe-auto .sa-item-summary > span:first-child { min-width: 0; }
2541
+ .dsh-safe-auto .sa-item-title { display: block; font-size: 14px; line-height: 20px; color: var(--dsw-alias-label-primary); }
2542
+ .dsh-safe-auto .sa-item-desc { display: block; margin-top: 4px; font-size: 12px; line-height: 18px; color: var(--dsw-alias-label-secondary); }
2543
+ .dsh-safe-auto .sa-item-chevron { width: 14px; height: 14px; color: var(--dsw-alias-label-secondary); transition: transform 0.15s ease; }
2544
+ .dsh-safe-auto.sa-item[open] > .sa-item-summary .sa-item-chevron { transform: rotate(180deg); }
2545
+ .dsh-safe-auto .sa-item-body { padding: 0 0 16px; }
1908
2546
  `;
1909
2547
  function ErrorText({ error, t }) {
1910
2548
  return error ? h("p", {
@@ -2413,7 +3051,7 @@ function HistoryPanel({ api, locale, initialPage = null, download = downloadHist
2413
3051
  }
2414
3052
  }, JSON.stringify(detail, null, 2))));
2415
3053
  }
2416
- function SettingsPanel({ view, api, locale, initialSettings = null, initialCatalog = null, initialTab = "reviewer" }) {
3054
+ function SettingsPanel({ api, locale, initialSettings = null, initialCatalog = null, initialTab = "reviewer" }) {
2417
3055
  const t = useText(locale);
2418
3056
  const [settings, setSettings] = (0, react.useState)(initialSettings);
2419
3057
  const [values, setValues] = (0, react.useState)(initialSettings?.values || null);
@@ -2563,10 +3201,6 @@ function SettingsPanel({ view, api, locale, initialSettings = null, initialCatal
2563
3201
  tabIndex: 0,
2564
3202
  className
2565
3203
  }, children);
2566
- if (view === "summary") return h("div", { className: "dsh-safe-auto" }, h("p", { className: "dsh-ui-compact dsh-ui-wrap" }, t("summary")), h(ErrorText, {
2567
- error,
2568
- t
2569
- }));
2570
3204
  return h("section", {
2571
3205
  className: "dsh-safe-auto dsh-ui-page",
2572
3206
  "aria-label": t("panelLabel"),
@@ -2644,6 +3278,29 @@ function SettingsPanel({ view, api, locale, initialSettings = null, initialCatal
2644
3278
  className: "dsh-ui-notice"
2645
3279
  }, t("saved")));
2646
3280
  }
3281
+ /**
3282
+ * The shared-column row: a named native details group. The panel mounts on the
3283
+ * first expand only — the shared page must not fire every plugin's settings
3284
+ * and model-catalog reads — and stays mounted afterwards so a collapsed group
3285
+ * never drops an unsaved reviewer draft or an in-flight save.
3286
+ */
3287
+ function SafeAutoSettingsItem({ api, locale }) {
3288
+ const t = useText(locale);
3289
+ const [visited, setVisited] = (0, react.useState)(false);
3290
+ return h("details", {
3291
+ className: "dsh-safe-auto sa-item",
3292
+ "data-testid": "safe-auto-settings-item",
3293
+ onToggle: (event) => {
3294
+ if (event.currentTarget.open) setVisited(true);
3295
+ }
3296
+ }, h("summary", { className: "sa-item-summary" }, h("span", null, h("span", { className: "sa-item-title" }, t("safeAuto")), h("span", { className: "sa-item-desc" }, t("groupHint"))), h(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutlineRegular, {
3297
+ className: "sa-item-chevron",
3298
+ "aria-hidden": "true"
3299
+ })), visited ? h("div", { className: "sa-item-body" }, h(SettingsPanel, {
3300
+ api,
3301
+ locale
3302
+ })) : null);
3303
+ }
2647
3304
  function apply(ctx) {
2648
3305
  ctx.effect(() => {
2649
3306
  if (typeof document === "undefined") return void 0;
@@ -2675,14 +3332,14 @@ function apply(ctx) {
2675
3332
  locale: ctx.locale
2676
3333
  })
2677
3334
  }, PermissionMenu));
2678
- ctx.slots.inject("plugins.bundle.config", () => ctx.slots.register({
2679
- name: "plugins.bundle.config",
2680
- key: BUNDLE,
2681
- inject: () => ({
2682
- api,
2683
- locale: ctx.locale
2684
- })
2685
- }, SettingsPanel));
3335
+ ctx.effect(() => registerSessionSettingsItem(ctx, {
3336
+ id: "safe-auto",
3337
+ order: 80
3338
+ }, () => h(SafeAutoSettingsItem, {
3339
+ api,
3340
+ locale: ctx.locale
3341
+ })));
3342
+ ctx.effect(() => registerAuxiliaryModelSettings(ctx));
2686
3343
  }
2687
3344
  //#endregion
2688
3345
  exports.BUNDLE = BUNDLE;
@@ -2690,6 +3347,7 @@ exports.CHANNEL = CHANNEL;
2690
3347
  exports.HistoryPanel = HistoryPanel;
2691
3348
  exports.PermissionMenu = PermissionMenu;
2692
3349
  exports.ReviewerFields = ReviewerFields;
3350
+ exports.SafeAutoSettingsItem = SafeAutoSettingsItem;
2693
3351
  exports.SettingsPanel = SettingsPanel;
2694
3352
  exports.apply = apply;
2695
3353
  exports.createRequestScope = createRequestScope;