@klarkxy/dsh-git-commit 0.3.1 → 0.3.2

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
@@ -1412,6 +1412,37 @@ function modelSettingsChangesIdentity(packageName) {
1412
1412
  namespace: segment
1413
1413
  };
1414
1414
  }
1415
+ /**
1416
+ * Shared "Session Management" settings section id. The section is provided by
1417
+ * shared infrastructure (see `registerSessionSettingsItem` in `client-utils`),
1418
+ * not by any one business plugin: every participant registers only a row into
1419
+ * {@link SESSION_SETTINGS_ITEM_SLOT}, so any enabled subset yields the same page.
1420
+ */
1421
+ const SESSION_SETTINGS_SECTION_ID = "session-manager";
1422
+ /**
1423
+ * Child list slot declared by the shared Session Management page. Neutral
1424
+ * naming on purpose: participants never import each other, they only
1425
+ * contribute `{id, order}` rows into this slot while the page is alive.
1426
+ */
1427
+ const SESSION_SETTINGS_ITEM_SLOT = "dsh.session-settings.item";
1428
+ /**
1429
+ * Host-owned nav icon seat: a root-scoped list where each entry's `id` must
1430
+ * equal the `settings.section` id it decorates. Owner props are
1431
+ * `{size, active}`; icons stay `aria-hidden` so the section label remains the
1432
+ * accessible name. Contributors into a shared section may register the same
1433
+ * icon `id` with different priorities — the ledger keeps every entry while the
1434
+ * host renders one icon per section.
1435
+ */
1436
+ const SETTINGS_SECTION_ICON_SLOT = "settings.section.icon";
1437
+ /** Authenticated host-RPC channel for get/set of the shared default. */
1438
+ const AUXILIARY_MODEL_CHANNEL = "/dsh-auxiliary-model";
1439
+ /** Model-settings namespace under which the shared default is published. */
1440
+ const AUXILIARY_MODEL_NS = "@klarkxy/dsh-plugin-kit/auxiliary-model";
1441
+ /**
1442
+ * Typert identity of the shared change stream. Mounted on the root context
1443
+ * with ownerNs filtering, so frames only carry {@link AUXILIARY_MODEL_NS}.
1444
+ */
1445
+ const AUXILIARY_MODEL_CHANGES_IDENTITY = modelSettingsChangesIdentity("@klarkxy/dsh-plugin-kit");
1415
1446
  //#endregion
1416
1447
  //#region ../dsh-plugin-kit/src/model-settings-client.ts
1417
1448
  /** This stream carries invalidations only; ready is never a settings snapshot. */
@@ -1581,6 +1612,604 @@ function createModelSettingsSubscriber(ctx, packageName, identity = modelSetting
1581
1612
  } });
1582
1613
  }
1583
1614
  //#endregion
1615
+ //#region ../dsh-plugin-kit/src/client-utils.ts
1616
+ /** One `<svg>` with the shared icon chrome; children are plain elements. */
1617
+ function lineIcon(children) {
1618
+ return function SettingsSectionIcon(props) {
1619
+ return (0, react.createElement)("svg", {
1620
+ "aria-hidden": "true",
1621
+ width: props.size ?? 16,
1622
+ height: props.size ?? 16,
1623
+ viewBox: "0 0 16 16",
1624
+ fill: "none",
1625
+ stroke: "currentColor",
1626
+ strokeWidth: 1,
1627
+ strokeLinecap: "round",
1628
+ strokeLinejoin: "round"
1629
+ }, ...children.map(([tag, attrs], index) => (0, react.createElement)(tag, {
1630
+ key: index,
1631
+ ...attrs
1632
+ })));
1633
+ };
1634
+ }
1635
+ /** Chat bubble for the shared Session Management section. */
1636
+ 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" }]]);
1637
+ /** Private Cordis rendezvous; every bundled copy of this helper resolves the same root service. */
1638
+ const SESSION_SETTINGS_PAGE_SERVICE = "dshSessionSettingsPage";
1639
+ /** The page component renders only its declared child list: items paint their own titles and styles. */
1640
+ function SessionSettingsPageView(props) {
1641
+ return props.renderSlot(SESSION_SETTINGS_ITEM_SLOT, {});
1642
+ }
1643
+ /**
1644
+ * Acquire the root-owned page service, creating and providing it on first use.
1645
+ * One counter covers participants, the section wait and the provider: the last
1646
+ * release removes the wait and then the provider synchronously (Cordis deletes
1647
+ * the impl before its async disposer awaits), so a later acquire sees a clean
1648
+ * get() miss and can re-establish the page.
1649
+ */
1650
+ function acquireSessionSettingsPage(root) {
1651
+ const existing = root.get(SESSION_SETTINGS_PAGE_SERVICE, false);
1652
+ if (existing) return existing;
1653
+ const slots = root.slots;
1654
+ let users = 0;
1655
+ let stopPage;
1656
+ let unprovide;
1657
+ const page = {
1658
+ retain() {
1659
+ users += 1;
1660
+ if (!stopPage) stopPage = slots.inject("settings.section", () => slots.register({
1661
+ name: "settings.section",
1662
+ id: SESSION_SETTINGS_SECTION_ID,
1663
+ order: 30,
1664
+ label: () => {
1665
+ const locale = root.get("locale", false);
1666
+ return String(locale?.getSnapshot?.().active ?? "").startsWith("zh") ? "会话管理" : "Session Management";
1667
+ },
1668
+ children: { [SESSION_SETTINGS_ITEM_SLOT]: {
1669
+ kind: "list",
1670
+ scope: "root"
1671
+ } }
1672
+ }, SessionSettingsPageView));
1673
+ },
1674
+ release() {
1675
+ if (users <= 0) return;
1676
+ users -= 1;
1677
+ if (users > 0) return;
1678
+ stopPage?.();
1679
+ stopPage = void 0;
1680
+ unprovide?.();
1681
+ }
1682
+ };
1683
+ unprovide = root.provide(SESSION_SETTINGS_PAGE_SERVICE, page);
1684
+ return page;
1685
+ }
1686
+ /**
1687
+ * Contribute one row to the shared Session Management settings page.
1688
+ *
1689
+ * `ctx` is the plugin's own context: the row's slot wait unloads with the
1690
+ * plugin, while the page itself is retained on the shared root context for as
1691
+ * long as any contributor lives. There is no fallback seat — when every
1692
+ * contributor is gone the section disappears with the page. Returns a disposer.
1693
+ * Shared root-owned infrastructure (the auxiliary-model card) calls this on
1694
+ * `ctx.root` so its row, icon and page share are owned by the root lifetime
1695
+ * and released by the returned disposer, not by any plugin fiber.
1696
+ */
1697
+ function registerSessionSettingsItem(ctx, item, component) {
1698
+ const slots = ctx.slots;
1699
+ const root = ctx.root;
1700
+ const page = acquireSessionSettingsPage(root);
1701
+ let stopItem;
1702
+ let stopIcon;
1703
+ try {
1704
+ page.retain();
1705
+ stopItem = slots.inject(SESSION_SETTINGS_ITEM_SLOT, () => slots.register({
1706
+ name: SESSION_SETTINGS_ITEM_SLOT,
1707
+ ...item
1708
+ }, component));
1709
+ stopIcon = slots.inject(SETTINGS_SECTION_ICON_SLOT, () => slots.register({
1710
+ name: SETTINGS_SECTION_ICON_SLOT,
1711
+ id: SESSION_SETTINGS_SECTION_ID,
1712
+ order: item.order,
1713
+ priority: item.order
1714
+ }, IconSessionSettings));
1715
+ } catch (error) {
1716
+ stopIcon?.();
1717
+ stopItem?.();
1718
+ page.release();
1719
+ throw error;
1720
+ }
1721
+ let disposed = false;
1722
+ return () => {
1723
+ if (disposed) return;
1724
+ disposed = true;
1725
+ stopIcon?.();
1726
+ stopItem?.();
1727
+ page.release();
1728
+ };
1729
+ }
1730
+ //#endregion
1731
+ //#region ../dsh-plugin-kit/src/auxiliary-model-client.tsx
1732
+ const AUXILIARY_MODEL_PACKAGE = "@klarkxy/dsh-plugin-kit";
1733
+ const AUXILIARY_MODEL_SEAT_ID = "auxiliary-model";
1734
+ /** Private Cordis rendezvous; every bundled copy resolves the same root service. */
1735
+ const AUXILIARY_MODEL_CARD_SERVICE = "dshAuxiliaryModelSettingsCard";
1736
+ /** Theme-token-only stylesheet: officialUiCss scope plus the shared ModelMenu recipe. */
1737
+ const auxiliaryModelCss = `${officialUiCss("ams-root")}${modelMenuCss}
1738
+ .ams-root .ams-row { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; min-width: 0; }
1739
+ `;
1740
+ const auxiliaryCopy = {
1741
+ zh: {
1742
+ title: "默认辅助模型",
1743
+ help: "各辅助功能(如标题生成、摘要)没有单独指定模型时使用的模型;仅影响声明使用它的功能。",
1744
+ model: "辅助模型",
1745
+ modelDefault: "跟随会话或宿主默认",
1746
+ modelHint: "不选择模型时,各功能按自己的规则决定对话模型。",
1747
+ effortDefault: "跟随模型默认",
1748
+ loading: "正在读取设置…",
1749
+ failed: "无法读取模型设置。",
1750
+ stale: "设置已被其他窗口更新,请刷新后重试。",
1751
+ saveFailed: "保存失败,原设置未更改。请重试。",
1752
+ saved: "已保存。",
1753
+ retry: "重试",
1754
+ refresh: "刷新设置",
1755
+ syncUnavailable: "自动刷新不可用,请手动刷新设置。"
1756
+ },
1757
+ en: {
1758
+ title: "Default Auxiliary Model",
1759
+ 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.",
1760
+ model: "Auxiliary model",
1761
+ modelDefault: "Follow session or host default",
1762
+ modelHint: "With no model selected, each feature follows its own rules for the conversation model.",
1763
+ effortDefault: "Follow model default",
1764
+ loading: "Loading settings…",
1765
+ failed: "Could not load model settings.",
1766
+ stale: "Settings were changed elsewhere; refresh and retry.",
1767
+ saveFailed: "Save failed; the previous setting was not changed. Try again.",
1768
+ saved: "Saved.",
1769
+ retry: "Retry",
1770
+ refresh: "Refresh settings",
1771
+ syncUnavailable: "Automatic refresh unavailable; refresh settings manually."
1772
+ }
1773
+ };
1774
+ /** A failed RPC keeps its code so the card can translate known rejections. */
1775
+ var AuxiliaryRpcError = class extends Error {
1776
+ code;
1777
+ constructor(code, message) {
1778
+ super(message);
1779
+ this.code = code;
1780
+ this.name = "AuxiliaryRpcError";
1781
+ }
1782
+ };
1783
+ /** Host UI language, following live switches with the host's English fallback. */
1784
+ function useAuxiliaryLocale(client) {
1785
+ const subscribe = (0, react.useCallback)((listener) => client.locale?.subscribe(listener) ?? (() => {}), [client]);
1786
+ const snapshot = (0, react.useCallback)(() => client.locale?.getSnapshot().active ?? "en", [client]);
1787
+ return String((0, react.useSyncExternalStore)(subscribe, snapshot, snapshot)).startsWith("zh") ? "zh" : "en";
1788
+ }
1789
+ function isAbort(error) {
1790
+ return error instanceof Error && (error.name === "AbortError" || /abort|cancel/i.test(error.message));
1791
+ }
1792
+ function isStaleRejection(error) {
1793
+ if (!(error instanceof AuxiliaryRpcError)) return false;
1794
+ return error.code === "stale" || error.code === "auxiliary-model/conflict" || /changed|revision|reload|stale|conflict/i.test(error.message);
1795
+ }
1796
+ async function callAuxiliaryRpc(client, endpoint, payload = {}) {
1797
+ const result = await client.connection.rpc.call(AUXILIARY_MODEL_CHANNEL, endpoint, payload);
1798
+ if (!result || result.ok !== true) {
1799
+ const error = result && "error" in result ? result.error : void 0;
1800
+ throw new AuxiliaryRpcError(error?.code ?? "unknown", error?.message ?? `RPC ${endpoint} failed`);
1801
+ }
1802
+ return result.value;
1803
+ }
1804
+ /**
1805
+ * The shared card. All state comes from the shared host service over the
1806
+ * plugin-kit RPC channel; the card only renders the stored row.
1807
+ */
1808
+ function AuxiliaryModelCard({ client, subscribe }) {
1809
+ const language = useAuxiliaryLocale(client);
1810
+ const text = auxiliaryCopy[language];
1811
+ const [settings, setSettings] = (0, react.useState)();
1812
+ const catalogState = useModelCatalog((0, react.useCallback)(() => readModelCatalog(client), [client]), { subscribe: (0, react.useCallback)((change) => client.connection.generation?.subscribe(change), [client]) });
1813
+ const { refresh: refreshCatalog } = catalogState;
1814
+ const choices = parseModelMenuChoices(catalogState.catalog, settings?.model ?? void 0);
1815
+ const [menuOpen, setMenuOpen] = (0, react.useState)(false);
1816
+ const [error, setError] = (0, react.useState)("");
1817
+ const [note, setNote] = (0, react.useState)(false);
1818
+ const [busy, setBusy] = (0, react.useState)(false);
1819
+ const [syncError, setSyncError] = (0, react.useState)("");
1820
+ const operation = (0, react.useRef)({
1821
+ epoch: 0,
1822
+ active: false,
1823
+ reading: false,
1824
+ writing: false,
1825
+ pending: false
1826
+ });
1827
+ const refreshRef = (0, react.useRef)(() => {});
1828
+ const refresh = () => {
1829
+ const state = operation.current;
1830
+ if (!state.active) return;
1831
+ if (state.reading || state.writing) {
1832
+ state.pending = true;
1833
+ return;
1834
+ }
1835
+ state.pending = false;
1836
+ state.reading = true;
1837
+ setBusy(true);
1838
+ const epoch = state.epoch;
1839
+ callAuxiliaryRpc(client, "defaults.get").then((next) => {
1840
+ if (state.active && state.epoch === epoch && !state.pending) {
1841
+ setSettings(next);
1842
+ setError("");
1843
+ }
1844
+ }).catch((reason) => {
1845
+ if (state.active && state.epoch === epoch && !state.pending && !isAbort(reason)) setError("failed");
1846
+ }).finally(() => {
1847
+ if (!state.active || state.epoch !== epoch) return;
1848
+ state.reading = false;
1849
+ setBusy(false);
1850
+ if (state.pending) refreshRef.current();
1851
+ });
1852
+ };
1853
+ refreshRef.current = refresh;
1854
+ (0, react.useEffect)(() => {
1855
+ const state = operation.current;
1856
+ state.epoch++;
1857
+ state.active = true;
1858
+ state.reading = false;
1859
+ state.writing = false;
1860
+ state.pending = false;
1861
+ setBusy(false);
1862
+ setSettings(void 0);
1863
+ setError("");
1864
+ setNote(false);
1865
+ refreshRef.current();
1866
+ return () => {
1867
+ state.active = false;
1868
+ state.epoch++;
1869
+ };
1870
+ }, [client]);
1871
+ (0, react.useEffect)(() => {
1872
+ setSyncError("");
1873
+ let live = true;
1874
+ let stop;
1875
+ try {
1876
+ stop = subscribe?.(() => {
1877
+ if (live) {
1878
+ setSyncError("");
1879
+ refreshCatalog();
1880
+ refreshRef.current();
1881
+ }
1882
+ }, (message) => {
1883
+ if (live) setSyncError(message);
1884
+ });
1885
+ } catch {
1886
+ if (live) setSyncError("Model settings sync service unavailable");
1887
+ }
1888
+ return () => {
1889
+ live = false;
1890
+ try {
1891
+ stop?.();
1892
+ } catch {}
1893
+ };
1894
+ }, [subscribe, refreshCatalog]);
1895
+ const save = (0, react.useCallback)((model) => {
1896
+ const state = operation.current;
1897
+ if (!settings || !state.active || state.writing || state.reading) return;
1898
+ state.writing = true;
1899
+ const epoch = state.epoch;
1900
+ setBusy(true);
1901
+ setNote(false);
1902
+ setError("");
1903
+ callAuxiliaryRpc(client, "defaults.set", {
1904
+ model: model ?? {
1905
+ provider: "",
1906
+ model: ""
1907
+ },
1908
+ expectedRevision: settings.revision
1909
+ }).then((next) => {
1910
+ if (state.active && state.epoch === epoch) {
1911
+ setSettings(next);
1912
+ setNote(true);
1913
+ refreshCatalog();
1914
+ }
1915
+ }).catch((reason) => {
1916
+ if (state.active && state.epoch === epoch) setError(isStaleRejection(reason) ? "stale" : "saveFailed");
1917
+ }).finally(() => {
1918
+ if (!state.active || state.epoch !== epoch) return;
1919
+ state.writing = false;
1920
+ setBusy(false);
1921
+ if (state.pending) refreshRef.current();
1922
+ });
1923
+ }, [
1924
+ client,
1925
+ settings,
1926
+ refreshCatalog
1927
+ ]);
1928
+ const locked = busy;
1929
+ if (!settings) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
1930
+ className: "ams-root dsh-ui-card",
1931
+ "data-testid": "auxiliary-model-settings",
1932
+ children: [
1933
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1934
+ className: "dsh-ui-heading",
1935
+ children: text.title
1936
+ }),
1937
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1938
+ className: "dsh-ui-help",
1939
+ children: text.help
1940
+ }),
1941
+ error ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1942
+ role: "alert",
1943
+ className: "dsh-ui-error",
1944
+ children: text[error]
1945
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1946
+ role: "status",
1947
+ className: "dsh-ui-hint",
1948
+ children: text.loading
1949
+ }),
1950
+ syncError && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1951
+ role: "status",
1952
+ className: "dsh-ui-hint",
1953
+ children: text.syncUnavailable
1954
+ }),
1955
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1956
+ className: "dsh-ui-actions",
1957
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
1958
+ variant: "ghost",
1959
+ size: "sm",
1960
+ disabled: locked,
1961
+ onClick: () => refreshRef.current(),
1962
+ children: text.retry
1963
+ })
1964
+ })
1965
+ ]
1966
+ });
1967
+ const current = settings.model ?? {
1968
+ provider: "",
1969
+ model: ""
1970
+ };
1971
+ const follow = !current.provider || !current.model;
1972
+ const selected = follow ? void 0 : choices.find((item) => item.provider === current.provider && item.model === current.model);
1973
+ const efforts = follow ? [] : modelMenuEffortOptions(selected, current.reasoningEffort);
1974
+ const currentLabel = follow ? text.modelDefault : selected?.label ?? `${current.provider} / ${current.model}`;
1975
+ const selectedEffort = efforts.find((item) => item.id === (current.reasoningEffort || ""));
1976
+ const effortName = selectedEffort ? modelMenuEffortLabel(selectedEffort, language) : void 0;
1977
+ 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
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1990
+ className: "dsh-ui-field",
1991
+ children: [
1992
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1993
+ className: "dsh-ui-label",
1994
+ children: text.model
1995
+ }),
1996
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelCatalogStatus, {
1997
+ state: catalogState,
1998
+ locale: language
1999
+ }),
2000
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelMenu, {
2001
+ locale: language,
2002
+ open: menuOpen && !locked,
2003
+ onOpenChange: (next) => setMenuOpen(next && !locked),
2004
+ listClassName: "ams-root ams-menu-portal",
2005
+ anchor: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2006
+ type: "button",
2007
+ className: "dsh-model-menu-trigger",
2008
+ disabled: locked,
2009
+ "aria-label": `${text.model}: ${currentLabel}`,
2010
+ children: [
2011
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2012
+ className: "dsh-model-menu-triggerLabel",
2013
+ children: currentLabel
2014
+ }),
2015
+ !follow && efforts.length ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2016
+ className: "dsh-model-menu-triggerEffort",
2017
+ children: effortName ?? text.effortDefault
2018
+ }) : null,
2019
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutlineRegular, { className: `dsh-model-menu-chevron${menuOpen && !locked ? " dsh-model-menu-chevronOpen" : ""}` })
2020
+ ]
2021
+ }),
2022
+ choices,
2023
+ selected: follow ? void 0 : {
2024
+ provider: current.provider,
2025
+ model: current.model
2026
+ },
2027
+ onPick: (picked) => save({
2028
+ provider: picked.provider,
2029
+ model: picked.model
2030
+ }),
2031
+ leading: [{
2032
+ id: "default",
2033
+ label: text.modelDefault,
2034
+ selected: follow
2035
+ }],
2036
+ onPickLeading: () => save(null),
2037
+ efforts,
2038
+ selectedEffort: current.reasoningEffort || "",
2039
+ onPickEffort: (effort) => save(follow ? null : {
2040
+ provider: current.provider,
2041
+ model: current.model,
2042
+ ...effort ? { reasoningEffort: effort } : {}
2043
+ }),
2044
+ modelLabel: text.model,
2045
+ modelValue: currentLabel,
2046
+ defaultEffortLabel: text.effortDefault
2047
+ }),
2048
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2049
+ className: "dsh-ui-hint",
2050
+ children: text.modelHint
2051
+ })
2052
+ ]
2053
+ }),
2054
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2055
+ className: "ams-row",
2056
+ children: [
2057
+ syncError && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2058
+ role: "status",
2059
+ className: "dsh-ui-hint",
2060
+ children: text.syncUnavailable
2061
+ }),
2062
+ error && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2063
+ role: "alert",
2064
+ className: "dsh-ui-error",
2065
+ children: text[error]
2066
+ }),
2067
+ note && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2068
+ role: "status",
2069
+ className: "dsh-ui-notice",
2070
+ children: text.saved
2071
+ }),
2072
+ error || syncError ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
2073
+ variant: "ghost",
2074
+ size: "sm",
2075
+ disabled: locked,
2076
+ onClick: () => refreshRef.current(),
2077
+ children: text.refresh
2078
+ }) : null
2079
+ ]
2080
+ })
2081
+ ]
2082
+ });
2083
+ }
2084
+ const AUXILIARY_MODEL_WATCH_COORDINATOR = "dshAuxiliaryModelWatchCoordinator";
2085
+ function acquireWatchCoordinator(root) {
2086
+ const existing = root.get(AUXILIARY_MODEL_WATCH_COORDINATOR, false);
2087
+ if (existing) return existing;
2088
+ let unprovide;
2089
+ const coordinator = {
2090
+ tail: Promise.resolve(),
2091
+ active: 0,
2092
+ mount() {
2093
+ coordinator.active += 1;
2094
+ const previousTail = coordinator.tail;
2095
+ const lease = {
2096
+ closed: false,
2097
+ done() {
2098
+ if (lease.closed) return;
2099
+ lease.closed = true;
2100
+ coordinator.active -= 1;
2101
+ const owned = lease.fiber;
2102
+ lease.fiber = void 0;
2103
+ coordinator.tail = coordinator.tail.then(() => Promise.resolve(owned?.dispose()).catch(() => {}));
2104
+ coordinator.tail.then(() => {
2105
+ if (coordinator.active > 0) return;
2106
+ try {
2107
+ if (root.get(AUXILIARY_MODEL_WATCH_COORDINATOR, false) === coordinator) Promise.resolve(unprovide?.()).catch(() => {});
2108
+ } catch {}
2109
+ });
2110
+ },
2111
+ fiber: void 0
2112
+ };
2113
+ previousTail.then(() => {
2114
+ if (lease.closed) return;
2115
+ try {
2116
+ lease.fiber = root.inject(["remote"], (injected) => {
2117
+ const scope = injected;
2118
+ const mounting = scope.remote.$mount(modelSettingsChangesRemote(AUXILIARY_MODEL_PACKAGE, AUXILIARY_MODEL_CHANGES_IDENTITY));
2119
+ mounting.catch(() => {});
2120
+ scope.effect(() => async () => {
2121
+ await (await mounting.catch(() => void 0))?.();
2122
+ });
2123
+ });
2124
+ } catch {}
2125
+ });
2126
+ return lease;
2127
+ }
2128
+ };
2129
+ unprovide = root.provide(AUXILIARY_MODEL_WATCH_COORDINATOR, coordinator);
2130
+ return coordinator;
2131
+ }
2132
+ /**
2133
+ * Acquire the root-owned card service, creating it on first use. One counter
2134
+ * covers participants, the seat wait and the change-stream mount: the last
2135
+ * release removes the wait, then the watch contribution, then the provider
2136
+ * itself synchronously (Cordis deletes the impl before its async disposer
2137
+ * awaits), so a later acquire sees a clean get() miss and can re-establish.
2138
+ */
2139
+ function acquireAuxiliaryModelCard(root, ctx) {
2140
+ const existing = root.get(AUXILIARY_MODEL_CARD_SERVICE, false);
2141
+ if (existing) return existing;
2142
+ let users = 0;
2143
+ let styleElement;
2144
+ let stopItem;
2145
+ let unprovide;
2146
+ const watchLease = acquireWatchCoordinator(root).mount();
2147
+ const subscriber = createModelSettingsSubscriber(ctx.root, AUXILIARY_MODEL_PACKAGE, AUXILIARY_MODEL_CHANGES_IDENTITY, (ns) => ns === AUXILIARY_MODEL_NS);
2148
+ const render = () => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AuxiliaryModelCard, {
2149
+ client: root,
2150
+ subscribe: subscriber.subscribe
2151
+ });
2152
+ const service = {
2153
+ retain() {
2154
+ users += 1;
2155
+ if (styleElement === void 0 && typeof document !== "undefined") {
2156
+ styleElement = document.createElement("style");
2157
+ styleElement.setAttribute("data-plugin", "dsh-auxiliary-model-shared");
2158
+ styleElement.textContent = auxiliaryModelCss;
2159
+ document.head.appendChild(styleElement);
2160
+ }
2161
+ if (!stopItem) stopItem = registerSessionSettingsItem(ctx.root, {
2162
+ id: AUXILIARY_MODEL_SEAT_ID,
2163
+ order: 55
2164
+ }, render);
2165
+ },
2166
+ release() {
2167
+ if (users <= 0) return;
2168
+ users -= 1;
2169
+ if (users > 0) return;
2170
+ stopItem?.();
2171
+ stopItem = void 0;
2172
+ styleElement?.remove();
2173
+ styleElement = void 0;
2174
+ watchLease.done();
2175
+ try {
2176
+ Promise.resolve(unprovide?.()).catch(() => {});
2177
+ } catch {}
2178
+ unprovide = void 0;
2179
+ }
2180
+ };
2181
+ unprovide = root.provide(AUXILIARY_MODEL_CARD_SERVICE, service);
2182
+ return service;
2183
+ }
2184
+ /**
2185
+ * Show the shared "default auxiliary model" card while the calling plugin is
2186
+ * enabled.
2187
+ *
2188
+ * `ctx` is the plugin's own client context: the card itself is retained on the
2189
+ * shared root context for as long as any participant lives, so an arbitrary
2190
+ * subset of enabled plugins yields the same single card. The mount's slot wait
2191
+ * and the change-stream contribution are owned by the root service — the first
2192
+ * participant's unload cannot take the card down. Returns a disposer; it may
2193
+ * be invoked by both ctx.effect and an explicit caller and releases at most
2194
+ * once, so a double call cannot unbalance the shared refcount.
2195
+ */
2196
+ function registerAuxiliaryModelSettings(ctx) {
2197
+ const root = ctx.root;
2198
+ const service = acquireAuxiliaryModelCard(root, ctx);
2199
+ try {
2200
+ service.retain();
2201
+ } catch (error) {
2202
+ service.release();
2203
+ throw error;
2204
+ }
2205
+ let disposed = false;
2206
+ return () => {
2207
+ if (disposed) return;
2208
+ disposed = true;
2209
+ service.release();
2210
+ };
2211
+ }
2212
+ //#endregion
1584
2213
  //#region src/contracts.ts
1585
2214
  const PLUGIN_NAME = "@klarkxy/dsh-git-commit";
1586
2215
  const RPC_CHANNEL = "/dsh-git-commit";
@@ -1601,12 +2230,11 @@ const zh = {
1601
2230
  commitAction: "分次提交全部工作区修改",
1602
2231
  files: "个文件",
1603
2232
  model: "模型",
1604
- modelDefault: "默认模型",
1605
- modelCustom: "插件设置",
1606
2233
  filesCount: "({n} 个文件)",
1607
2234
  confirmPrompt: "将把 {n} 个文件的全部修改分次提交到当前分支。",
1608
2235
  confirm: "确认提交",
1609
2236
  cancel: "取消",
2237
+ commitBlocked: "正在保存模型设置或上次保存未生效,恢复后才可提交",
1610
2238
  loading: "正在读取仓库状态…",
1611
2239
  running: "正在整理修改并分次提交…",
1612
2240
  done: "已创建提交",
@@ -1632,7 +2260,7 @@ const zh = {
1632
2260
  workspaceChanged: "工作区或分支已变更,请刷新后重试提交",
1633
2261
  settingsUnavailable: "无法读取提交设置,请重新加载插件后重试",
1634
2262
  invalidRequest: "提交请求无效,请刷新后重试",
1635
- planUnavailable: "模型没有给出可用的提交方案,已取消本次提交,工作区没有被改动。换一个模型重试,或在插件设置里开启兜底提交。",
2263
+ planUnavailable: "模型没有给出可用的提交方案,已取消本次提交,工作区没有被改动。换一个模型重试,或开启下方「模型不可用时仍然提交」。",
1636
2264
  close: "关闭"
1637
2265
  };
1638
2266
  const en = {
@@ -1641,12 +2269,11 @@ const en = {
1641
2269
  commitAction: "Commit all workspace changes",
1642
2270
  files: "files",
1643
2271
  model: "Model",
1644
- modelDefault: "default model",
1645
- modelCustom: "Plugin settings",
1646
2272
  filesCount: " ({n} files)",
1647
2273
  confirmPrompt: "All changes in {n} files will be committed to the current branch in one or more commits.",
1648
2274
  confirm: "Commit now",
1649
2275
  cancel: "Cancel",
2276
+ commitBlocked: "A settings write is pending or unrecovered; commits unlock once it settles",
1650
2277
  loading: "Reading repository status…",
1651
2278
  running: "Organizing changes into commits…",
1652
2279
  done: "Commits created",
@@ -1672,7 +2299,7 @@ const en = {
1672
2299
  workspaceChanged: "The workspace or branch changed; refresh and retry the commit",
1673
2300
  settingsUnavailable: "Could not read commit settings; reload the plugin and retry",
1674
2301
  invalidRequest: "Invalid commit request; refresh and retry",
1675
- planUnavailable: "The model returned no usable commit plan, so the run was cancelled and your workspace is untouched. Try another model, or turn on the fallback in plugin settings.",
2302
+ planUnavailable: "The model returned no usable commit plan, so the run was cancelled and your workspace is untouched. Try another model, or enable “Commit even without a model plan” below.",
1676
2303
  close: "Close"
1677
2304
  };
1678
2305
  /** A failed RPC keeps its code so the panel can explain a rejection in its own words. */
@@ -1776,7 +2403,7 @@ function GitBranchIcon() {
1776
2403
  ]
1777
2404
  });
1778
2405
  }
1779
- function GitCommitAction({ sessionId, useSession, call, t }) {
2406
+ function GitCommitAction({ sessionId, useSession, call, t, client, subscribeSettings }) {
1780
2407
  const [open, setOpen] = (0, react.useState)(false);
1781
2408
  const [status, setStatus] = (0, react.useState)();
1782
2409
  const [loading, setLoading] = (0, react.useState)(false);
@@ -1784,6 +2411,29 @@ function GitCommitAction({ sessionId, useSession, call, t }) {
1784
2411
  const [result, setResult] = (0, react.useState)(null);
1785
2412
  const [error, setError] = (0, react.useState)(null);
1786
2413
  const [confirming, setConfirming] = (0, react.useState)(false);
2414
+ const pendingWrites = (0, react.useRef)(0);
2415
+ const writeFailed = (0, react.useRef)(false);
2416
+ const settingsDirty = (0, react.useRef)(false);
2417
+ const [settingsPending, setSettingsPending] = (0, react.useState)(false);
2418
+ const [settingsSeen, setSettingsSeen] = (0, react.useState)(false);
2419
+ const syncLock = (0, react.useCallback)(() => {
2420
+ const locked = pendingWrites.current > 0 || writeFailed.current;
2421
+ settingsDirty.current = locked;
2422
+ setSettingsPending(locked);
2423
+ }, []);
2424
+ const markWrite = (0, react.useCallback)((active) => {
2425
+ pendingWrites.current = Math.max(0, pendingWrites.current + (active ? 1 : -1));
2426
+ syncLock();
2427
+ }, [syncLock]);
2428
+ const markWriteFailed = (0, react.useCallback)(() => {
2429
+ writeFailed.current = true;
2430
+ syncLock();
2431
+ }, [syncLock]);
2432
+ const markSeen = (0, react.useCallback)(() => {
2433
+ writeFailed.current = false;
2434
+ setSettingsSeen(true);
2435
+ syncLock();
2436
+ }, [syncLock]);
1787
2437
  const rootRef = (0, react.useRef)(null);
1788
2438
  const triggerRef = (0, react.useRef)(null);
1789
2439
  const panelRef = (0, react.useRef)(null);
@@ -1796,19 +2446,26 @@ function GitCommitAction({ sessionId, useSession, call, t }) {
1796
2446
  margin: 16
1797
2447
  });
1798
2448
  const sessionRunning = useSession((snapshot) => snapshot.running === true);
2449
+ const panelPortalRef = (0, react.useMemo)(() => ({ get current() {
2450
+ const panel = panelRef.current;
2451
+ if (!panel) return null;
2452
+ return { contains: (node) => panel.contains(node) || [...document.querySelectorAll(".gcm-menu-portal")].some((menu) => menu.contains(node)) };
2453
+ } }), []);
1799
2454
  const changeOpen = (0, react.useCallback)((next) => {
2455
+ if (!next && pendingWrites.current > 0) return;
1800
2456
  setOpen(next);
1801
2457
  setConfirming(false);
1802
2458
  if (next) {
1803
2459
  setResult(null);
1804
2460
  setError(null);
2461
+ setSettingsSeen(false);
1805
2462
  }
1806
2463
  }, []);
1807
2464
  const positioned = Boolean(position);
1808
2465
  (0, react.useEffect)(() => {
1809
2466
  if (open && positioned) panelRef.current?.focus();
1810
2467
  }, [open, positioned]);
1811
- (0, _deepseek_ai_dsh_client_ui_primitives.useDismissOnOutsidePointer)(rootRef, open, changeOpen, panelRef);
2468
+ (0, _deepseek_ai_dsh_client_ui_primitives.useDismissOnOutsidePointer)(rootRef, open, changeOpen, panelPortalRef);
1812
2469
  (0, react.useEffect)(() => {
1813
2470
  generationRef.current += 1;
1814
2471
  setOpen(false);
@@ -1816,6 +2473,11 @@ function GitCommitAction({ sessionId, useSession, call, t }) {
1816
2473
  setResult(null);
1817
2474
  setError(null);
1818
2475
  setRunning(false);
2476
+ pendingWrites.current = 0;
2477
+ writeFailed.current = false;
2478
+ settingsDirty.current = false;
2479
+ setSettingsPending(false);
2480
+ setSettingsSeen(false);
1819
2481
  }, [sessionId]);
1820
2482
  const loadStatus = (0, react.useCallback)(async () => {
1821
2483
  const generation = generationRef.current;
@@ -1850,6 +2512,10 @@ function GitCommitAction({ sessionId, useSession, call, t }) {
1850
2512
  }, [open, changeOpen]);
1851
2513
  const runCommit = (0, react.useCallback)(async () => {
1852
2514
  const generation = generationRef.current;
2515
+ if (settingsDirty.current || client && !settingsSeen) {
2516
+ setConfirming(false);
2517
+ return;
2518
+ }
1853
2519
  setConfirming(false);
1854
2520
  setRunning(true);
1855
2521
  setResult(null);
@@ -1871,10 +2537,15 @@ function GitCommitAction({ sessionId, useSession, call, t }) {
1871
2537
  }
1872
2538
  }
1873
2539
  }
1874
- }, [call, sessionId]);
2540
+ }, [
2541
+ call,
2542
+ sessionId,
2543
+ client,
2544
+ settingsSeen
2545
+ ]);
1875
2546
  const available = status?.available === true;
1876
2547
  const workspaceCount = status?.workspace.files ?? 0;
1877
- const canCommit = available && !running && !sessionRunning && workspaceCount > 0;
2548
+ const canCommit = available && !running && !sessionRunning && workspaceCount > 0 && !settingsPending && (!client || settingsSeen);
1878
2549
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1879
2550
  ref: rootRef,
1880
2551
  className: "gcm-root",
@@ -1944,33 +2615,21 @@ function GitCommitAction({ sessionId, useSession, call, t }) {
1944
2615
  role: "alert",
1945
2616
  children: errorText(t, error)
1946
2617
  }),
2618
+ client ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(GitCommitSettingsPanel, {
2619
+ client,
2620
+ subscribeSettings,
2621
+ disabled: running,
2622
+ onSaving: markWrite,
2623
+ onWriteFailed: markWriteFailed,
2624
+ onReady: markSeen,
2625
+ onSaved: () => void loadStatus()
2626
+ }) : null,
1947
2627
  status && !available && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1948
2628
  className: "dsh-ui-meta",
1949
2629
  role: "status",
1950
2630
  children: reasonText(t, status)
1951
2631
  }),
1952
2632
  available && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
1953
- status?.model ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
1954
- className: "gcm-model",
1955
- children: [
1956
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1957
- className: "dsh-ui-hint",
1958
- children: t("model")
1959
- }),
1960
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1961
- className: "dsh-ui-truncate",
1962
- children: [
1963
- status.model.provider,
1964
- "/",
1965
- status.model.model
1966
- ]
1967
- }),
1968
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tag, {
1969
- tone: status.model.source === "page" ? "info" : "neutral",
1970
- children: status.model.source === "page" ? t("modelCustom") : t("modelDefault")
1971
- })
1972
- ]
1973
- }) : null,
1974
2633
  confirming && canCommit ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1975
2634
  className: "dsh-ui-stack",
1976
2635
  role: "group",
@@ -2006,6 +2665,11 @@ function GitCommitAction({ sessionId, useSession, call, t }) {
2006
2665
  sessionRunning && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2007
2666
  className: "dsh-ui-hint",
2008
2667
  children: t("sessionRunning")
2668
+ }),
2669
+ settingsPending && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2670
+ className: "dsh-ui-hint",
2671
+ role: "status",
2672
+ children: t("commitBlocked")
2009
2673
  })
2010
2674
  ] }),
2011
2675
  running && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
@@ -2081,7 +2745,6 @@ const clientCss = `${officialUiCss("gcm-root")}${modelMenuCss}
2081
2745
  * of at the trigger. Naming both classes puts this rule at the same weight,
2082
2746
  * later in the sheet, so it wins. */
2083
2747
  .gcm-root.gcm-panel { position: fixed; width: min(360px, calc(100vw - 32px)); max-height: min(560px, calc(100vh - 32px)); overflow: auto; }
2084
- .gcm-root .gcm-model { display: flex; align-items: center; gap: 6px; min-width: 0; margin: 0; font-size: 12px; line-height: 18px; color: var(--dsw-alias-label-primary); }
2085
2748
  .gcm-root .gcm-commits { margin: 0; padding-inline-start: 18px; display: grid; gap: 2px; font-size: 12px; line-height: 18px; overflow-wrap: anywhere; }
2086
2749
  .gcm-root .gcm-commits code { font-size: 12px; background: var(--dsw-alias-bg-layer-2); padding: 0 4px; border-radius: var(--dsw-radius-sm); }
2087
2750
  .gcm-root .gcm-settings-status { display: flex; flex-direction: column; gap: 4px; }
@@ -2093,31 +2756,31 @@ function useHostLocale(client) {
2093
2756
  return (0, react.useSyncExternalStore)(subscribe, snapshot, snapshot);
2094
2757
  }
2095
2758
  const settingsZh = {
2096
- pageTitle: "Git 提交",
2097
2759
  model: "提交规划模型",
2098
- modelDefault: "默认模型",
2099
- modelHint: "留空则使用当前会话模型,再回落到宿主默认对话模型。",
2760
+ modelDefault: "默认辅助模型",
2761
+ modelHint: "留空使用全局默认辅助模型;未设置时跟随当前会话模型,再回落到宿主默认对话模型。",
2100
2762
  fallback: "模型不可用时仍然提交",
2101
2763
  fallbackHint: "默认关闭:模型规划失败会取消本次提交,工作区不变。开启后全部改动会合并成一个提交,由插件代写提交信息。",
2102
2764
  loading: "正在读取设置…",
2103
2765
  saved: "已保存。",
2104
2766
  stale: "设置已更新,请刷新后重试。",
2105
2767
  failed: "无法读取提交设置。",
2768
+ saveFailed: "设置未能保存,已显示的选择未生效。请重试。",
2106
2769
  retry: "重试",
2107
2770
  refresh: "刷新设置",
2108
2771
  syncUnavailable: "自动刷新不可用,请手动刷新设置。"
2109
2772
  };
2110
2773
  const settingsEn = {
2111
- pageTitle: "Git Commit",
2112
2774
  model: "Commit planning model",
2113
- modelDefault: "Default model",
2114
- modelHint: "Leave empty to use the current session model, then the host default chat model.",
2775
+ modelDefault: "Default auxiliary model",
2776
+ modelHint: "Leave empty to use the global default auxiliary model; when none is set, the current conversation model, then the host default chat model.",
2115
2777
  fallback: "Commit even without a model plan",
2116
2778
  fallbackHint: "Off by default: a failed plan cancels the run and leaves the workspace untouched. When on, every change lands in one commit with a plugin-written message.",
2117
2779
  loading: "Loading settings…",
2118
2780
  saved: "Saved.",
2119
2781
  stale: "Settings changed; refresh and retry.",
2120
2782
  failed: "Could not load commit settings.",
2783
+ saveFailed: "The change could not be saved, so the shown selection did not take effect. Try again.",
2121
2784
  retry: "Retry",
2122
2785
  refresh: "Refresh settings",
2123
2786
  syncUnavailable: "Automatic refresh unavailable; refresh settings manually."
@@ -2125,7 +2788,8 @@ const settingsEn = {
2125
2788
  function settingsCopy(locale) {
2126
2789
  return String(locale).startsWith("zh") ? settingsZh : settingsEn;
2127
2790
  }
2128
- /** Plugin-page settings row: pick the model this plugin's planning call uses. */
2791
+ /** Model/fallback block embedded in the commit popover; picks persist as the
2792
+ * plugin's saved defaults and sync through the model-settings change channel. */
2129
2793
  function GitCommitSettingsPanel(props) {
2130
2794
  const hostLocale = useHostLocale(props.client);
2131
2795
  const text = settingsCopy(props.locale ?? hostLocale);
@@ -2163,6 +2827,7 @@ function GitCommitSettingsPanel(props) {
2163
2827
  if (state.active && state.epoch === epoch && !state.pending) {
2164
2828
  setSettings(next);
2165
2829
  setError("");
2830
+ props.onReady?.();
2166
2831
  }
2167
2832
  }).catch(() => {
2168
2833
  if (state.active && state.epoch === epoch && !state.pending) setError("failed");
@@ -2212,14 +2877,16 @@ function GitCommitSettingsPanel(props) {
2212
2877
  props.client,
2213
2878
  refreshCatalog
2214
2879
  ]);
2880
+ const controlsLocked = props.disabled === true;
2215
2881
  const save = (0, react.useCallback)((patch) => {
2216
2882
  const state = operation.current;
2217
- if (!settings || !state.active || state.writing || state.reading) return;
2883
+ if (!settings || !state.active || state.writing || state.reading || props.disabled) return;
2218
2884
  state.writing = true;
2219
2885
  const epoch = state.epoch;
2220
2886
  setBusy(true);
2221
2887
  setNote(false);
2222
2888
  setError("");
2889
+ props.onSaving?.(true);
2223
2890
  rpc(call, "settings.update", {
2224
2891
  settings: {
2225
2892
  model: patch.model ?? settings.model,
@@ -2230,10 +2897,16 @@ function GitCommitSettingsPanel(props) {
2230
2897
  if (state.active && state.epoch === epoch) {
2231
2898
  setSettings(next);
2232
2899
  setNote(true);
2900
+ props.onReady?.();
2901
+ props.onSaved?.(next);
2902
+ }
2903
+ }).catch((reason) => {
2904
+ if (state.active && state.epoch === epoch) {
2905
+ props.onWriteFailed?.();
2906
+ setError(reason instanceof RpcError && reason.code === "stale" ? "stale" : "saveFailed");
2233
2907
  }
2234
- }).catch(() => {
2235
- if (state.active && state.epoch === epoch) setError("stale");
2236
2908
  }).finally(() => {
2909
+ props.onSaving?.(false);
2237
2910
  if (!state.active || state.epoch !== epoch) return;
2238
2911
  state.writing = false;
2239
2912
  setBusy(false);
@@ -2242,11 +2915,12 @@ function GitCommitSettingsPanel(props) {
2242
2915
  }, [
2243
2916
  call,
2244
2917
  settings,
2245
- text.saved,
2246
- text.stale
2918
+ text.stale,
2919
+ props.disabled
2247
2920
  ]);
2921
+ const locked = busy || controlsLocked;
2248
2922
  if (!settings) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
2249
- className: "gcm-root gcm-settings dsh-ui-page",
2923
+ className: "gcm-root gcm-settings dsh-ui-stack",
2250
2924
  children: [
2251
2925
  error ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2252
2926
  role: "alert",
@@ -2265,7 +2939,7 @@ function GitCommitSettingsPanel(props) {
2265
2939
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
2266
2940
  variant: "ghost",
2267
2941
  size: "sm",
2268
- disabled: busy,
2942
+ disabled: locked,
2269
2943
  onClick: () => refreshRef.current(),
2270
2944
  children: text.retry
2271
2945
  })
@@ -2279,36 +2953,9 @@ function GitCommitSettingsPanel(props) {
2279
2953
  const selectedEffort = efforts.find((item) => item.id === (current.reasoningEffort || ""));
2280
2954
  const effortName = selectedEffort ? modelMenuEffortLabel(selectedEffort, language) : void 0;
2281
2955
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
2282
- className: "gcm-root gcm-settings dsh-ui-page",
2956
+ className: "gcm-root gcm-settings dsh-ui-stack",
2283
2957
  "data-testid": "git-commit-settings",
2284
2958
  children: [
2285
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2286
- className: "gcm-settings-status",
2287
- children: [
2288
- syncError && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2289
- role: "status",
2290
- className: "dsh-ui-hint",
2291
- children: text.syncUnavailable
2292
- }),
2293
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
2294
- variant: "ghost",
2295
- size: "sm",
2296
- disabled: busy,
2297
- onClick: () => refreshRef.current(),
2298
- children: text.refresh
2299
- }),
2300
- error && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2301
- role: "alert",
2302
- className: "dsh-ui-error",
2303
- children: text[error]
2304
- }),
2305
- note && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2306
- role: "status",
2307
- className: "dsh-ui-notice",
2308
- children: text.saved
2309
- })
2310
- ]
2311
- }),
2312
2959
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2313
2960
  className: "dsh-ui-field",
2314
2961
  children: [
@@ -2322,13 +2969,13 @@ function GitCommitSettingsPanel(props) {
2322
2969
  }),
2323
2970
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelMenu, {
2324
2971
  locale: language,
2325
- open: menuOpen && !busy,
2326
- onOpenChange: (next) => setMenuOpen(next && !busy),
2327
- listClassName: "gcm-root",
2972
+ open: menuOpen && !locked,
2973
+ onOpenChange: (next) => setMenuOpen(next && !locked),
2974
+ listClassName: "gcm-root gcm-menu-portal",
2328
2975
  anchor: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2329
2976
  type: "button",
2330
2977
  className: "dsh-model-menu-trigger",
2331
- disabled: busy,
2978
+ disabled: locked,
2332
2979
  "aria-label": `${text.model}: ${currentLabel}`,
2333
2980
  children: [
2334
2981
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
@@ -2339,7 +2986,7 @@ function GitCommitSettingsPanel(props) {
2339
2986
  className: "dsh-model-menu-triggerEffort",
2340
2987
  children: effortName ?? text.modelDefault
2341
2988
  }) : null,
2342
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutlineRegular, { className: `dsh-model-menu-chevron${menuOpen && !busy ? " dsh-model-menu-chevronOpen" : ""}` })
2989
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutlineRegular, { className: `dsh-model-menu-chevron${menuOpen && !locked ? " dsh-model-menu-chevronOpen" : ""}` })
2343
2990
  ]
2344
2991
  }),
2345
2992
  choices,
@@ -2380,6 +3027,33 @@ function GitCommitSettingsPanel(props) {
2380
3027
  className: "dsh-ui-meta",
2381
3028
  children: text.modelHint
2382
3029
  }),
3030
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3031
+ className: "gcm-settings-status",
3032
+ children: [
3033
+ syncError && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
3034
+ role: "status",
3035
+ className: "dsh-ui-hint",
3036
+ children: text.syncUnavailable
3037
+ }),
3038
+ error && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
3039
+ role: "alert",
3040
+ className: "dsh-ui-error",
3041
+ children: text[error]
3042
+ }),
3043
+ note && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
3044
+ role: "status",
3045
+ className: "dsh-ui-notice",
3046
+ children: text.saved
3047
+ }),
3048
+ error || syncError ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
3049
+ variant: "ghost",
3050
+ size: "sm",
3051
+ disabled: locked,
3052
+ onClick: () => refreshRef.current(),
3053
+ children: text.refresh
3054
+ }) : null
3055
+ ]
3056
+ }),
2383
3057
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("article", {
2384
3058
  className: "gcm-root dsh-ui-card",
2385
3059
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", {
@@ -2396,7 +3070,7 @@ function GitCommitSettingsPanel(props) {
2396
3070
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Switch, {
2397
3071
  checked: settings.allowFallback,
2398
3072
  label: text.fallback,
2399
- disabled: busy,
3073
+ disabled: locked,
2400
3074
  onChange: (next) => save({ allowFallback: next })
2401
3075
  })]
2402
3076
  })
@@ -2410,6 +3084,7 @@ function apply(ctx) {
2410
3084
  const settingsSubscriber = createModelSettingsSubscriber(ctx, PLUGIN_NAME);
2411
3085
  const subscribeSettings = (change, error) => settingsSubscriber.subscribe(change, error);
2412
3086
  ctx.provide(GIT_COMMIT_CLIENT_SERVICE, { active: true });
3087
+ ctx.effect(() => registerAuxiliaryModelSettings(ctx), "git-commit.auxiliary-model");
2413
3088
  ctx.effect(() => client.locale.register(NS, {
2414
3089
  zh: { ...zh },
2415
3090
  en: { ...en }
@@ -2422,19 +3097,16 @@ function apply(ctx) {
2422
3097
  document.head.appendChild(style);
2423
3098
  return () => style.remove();
2424
3099
  }, "git-commit.styles");
2425
- ctx.effect(() => client.slots.inject("plugins.bundle.config", () => client.slots.register({
2426
- name: "plugins.bundle.config",
2427
- key: PLUGIN_NAME
2428
- }, () => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(GitCommitSettingsPanel, {
2429
- client,
2430
- subscribeSettings
2431
- }))), "git-commit.settings");
2432
3100
  ctx.effect(() => client.slots.inject("conversation.session.header.actions", () => client.slots.register({
2433
3101
  name: "conversation.session.header.actions",
2434
3102
  id: "git-commit",
2435
3103
  order: 10,
2436
3104
  locale: NS,
2437
- inject: () => ({ call: (endpoint, payload) => client.connection.rpc.call(RPC_CHANNEL, endpoint, payload) })
3105
+ inject: () => ({
3106
+ call: (endpoint, payload) => client.connection.rpc.call(RPC_CHANNEL, endpoint, payload),
3107
+ client,
3108
+ subscribeSettings
3109
+ })
2438
3110
  }, GitCommitAction)), "git-commit.slot");
2439
3111
  }
2440
3112
  //#endregion