@klarkxy/dsh-safe-auto 0.5.2 → 0.5.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -4
- package/README.zh-CN.md +6 -4
- package/dsh.plugin.json +1 -1
- package/lib/client.js +691 -33
- package/locale/en.json +1 -1
- package/locale/zh.json +2 -2
- package/package.json +14 -6
- package/src/audit.js +11 -0
- package/src/index.js +113 -11
- package/src/model-route.js +59 -7
- package/src/reviewer.js +32 -13
- package/src/transcript.js +135 -8
- package/src/ui-host.js +1 -1
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
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
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: "
|
|
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: "
|
|
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({
|
|
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.
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
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;
|