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