@klarkxy/dsh-session-manager 0.3.4 → 0.4.1

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.
@@ -66,6 +66,9 @@ const text = {
66
66
  creatingContinuation: "正在创建新会话",
67
67
  continueNewHint: "在来源会话的工作区新建会话,将摘要和原会话 ID 填入草稿,不自动发送。",
68
68
  compactNothing: "没有值得压缩的内容。",
69
+ settingsTitle: "会话管理",
70
+ settingsGroupTitle: "会话行为",
71
+ settingsGroupHint: "继续灰影、自动继续、交接与会话摘要",
69
72
  settingsMode: "继续灰影",
70
73
  modeFixed: "固定文本",
71
74
  modeAuto: "模型生成",
@@ -74,7 +77,12 @@ const text = {
74
77
  continueTextLabel: "固定文本内容",
75
78
  continueTextPlaceholder: "留空使用默认「继续」",
76
79
  ghostModelLabel: "生成模型",
77
- ghostModelHint: "每轮结束后生成一次并缓存;不选择时使用当前会话或宿主的默认模型。",
80
+ ghostModelHint: "每轮结束后生成一次并缓存;不选择时使用默认辅助模型。",
81
+ judgeModelLabel: "判定模型",
82
+ judgeModelHint: "判断回复是否提前结束;不选择时使用默认辅助模型。",
83
+ handoffGroup: "交接摘要",
84
+ handoffModelLabel: "交接模型",
85
+ handoffModelHint: "工具请求会话交接摘要时使用;不选择时使用默认辅助模型。",
78
86
  welcomeBackGroup: "回到会话时",
79
87
  recapEnabledLabel: "显示会话摘要",
80
88
  recapEnabledHint: "在对话中回顾上次进展。",
@@ -98,8 +106,8 @@ const text = {
98
106
  autoContinueMaxInvalid: "请填写 1–10 的整数。",
99
107
  minutesUnit: "分钟",
100
108
  modelLabel: "摘要模型",
101
- modelFollowOption: "当前会话 / 宿主默认模型",
102
- modelGroupHint: "只用于欢迎回来摘要;留空跟随会话或宿主默认。",
109
+ modelFollowOption: "默认辅助模型",
110
+ modelGroupHint: "只用于欢迎回来摘要;留空使用默认辅助模型。",
103
111
  saveSettings: "保存",
104
112
  settingsSaving: "正在保存",
105
113
  settingsSaved: "已保存",
@@ -142,6 +150,9 @@ const text = {
142
150
  creatingContinuation: "Creating new session",
143
151
  continueNewHint: "Create a session in the source session's workspace and fill its draft with the recap and source session ID, without sending.",
144
152
  compactNothing: "There is nothing useful to compact.",
153
+ settingsTitle: "Session Management",
154
+ settingsGroupTitle: "Session behavior",
155
+ settingsGroupHint: "Continue ghost, auto-continue, handoff and session recap",
145
156
  settingsMode: "Continue ghost",
146
157
  modeFixed: "Fixed text",
147
158
  modeAuto: "Model-generated",
@@ -150,7 +161,12 @@ const text = {
150
161
  continueTextLabel: "Fixed text",
151
162
  continueTextPlaceholder: "Empty uses the default \"Continue\"",
152
163
  ghostModelLabel: "Generation model",
153
- ghostModelHint: "Generated once after each turn and cached. The default follows the current conversation or host.",
164
+ ghostModelHint: "Generated once after each turn and cached. The default follows the shared auxiliary model.",
165
+ judgeModelLabel: "Judge model",
166
+ judgeModelHint: "Decides whether a reply stopped early. The default follows the shared auxiliary model.",
167
+ handoffGroup: "Session handoff",
168
+ handoffModelLabel: "Handoff model",
169
+ handoffModelHint: "Used when an agent asks for a session handoff summary. The default follows the shared auxiliary model.",
154
170
  welcomeBackGroup: "When you return",
155
171
  recapEnabledLabel: "Show session recap",
156
172
  recapEnabledHint: "Review your previous progress in the conversation.",
@@ -174,8 +190,8 @@ const text = {
174
190
  autoContinueMaxInvalid: "Enter a whole number from 1 to 10.",
175
191
  minutesUnit: "minutes",
176
192
  modelLabel: "Recap model",
177
- modelFollowOption: "Current conversation / host default",
178
- modelGroupHint: "Used only for the welcome-back recap; empty follows the conversation or host default.",
193
+ modelFollowOption: "Default auxiliary model",
194
+ modelGroupHint: "Used only for the welcome-back recap; empty follows the default auxiliary model.",
179
195
  saveSettings: "Save",
180
196
  settingsSaving: "Saving",
181
197
  settingsSaved: "Saved",
@@ -235,6 +251,8 @@ const CONFIG_READ_ENDPOINT = "config.read";
235
251
  const CONFIG_WRITE_ENDPOINT = "config.write";
236
252
  const AUTO_CONTINUE_STATUS_ENDPOINT = "autocontinue.status";
237
253
  const AUTO_CONTINUE_UPDATE_ENDPOINT = "autocontinue.update";
254
+ /** This plugin's own row inside the shared Session Management settings list. */
255
+ const SESSION_SETTINGS_ITEM_ID = "session-manager";
238
256
  /** Card entry id in the chat flow; the recap renders at the latest completed turn's tail. */
239
257
  const WELCOME_BACK_CARD_ID = "@klarkxy/dsh-session-manager.welcome-back";
240
258
  const TURN_TAIL_SLOT = "conversation.chat.turnTail";
@@ -824,6 +842,15 @@ function parseModelMenuChoices(value, bound) {
824
842
  });
825
843
  return choices;
826
844
  }
845
+ /** Reasoning-effort options for one choice; the saved value stays selectable. */
846
+ function modelMenuEffortOptions(choice, current) {
847
+ const listed = (choice?.efforts ?? []).map((item) => ({ ...item }));
848
+ if (current && !listed.some((item) => item.id === current)) listed.push({
849
+ id: current,
850
+ name: current
851
+ });
852
+ return listed;
853
+ }
827
854
  //#endregion
828
855
  //#region ../dsh-model-route/src/catalog-ui.tsx
829
856
  /**
@@ -1815,8 +1842,48 @@ ${modelMenuCss}
1815
1842
  font-size: 13px;
1816
1843
  line-height: 18px;
1817
1844
  }
1818
- /* Settings page: one quiet reading surface. Rules, not nested wells, separate
1819
- * sections; dependent controls align with the feature they belong to. */
1845
+ /* General-section item: a named details group mirroring the host's native
1846
+ * item rows (16px vertical padding, a 0.5px l2 rule the shell drops on the
1847
+ * last item, 24px label-control gutter). The body mounts on first expand and
1848
+ * stays mounted while collapsed, so a fold never drops an unsaved draft. */
1849
+ .${FREE_CHAT_ROOT}.dsm-item {
1850
+ padding: 0;
1851
+ border-bottom: 0.5px solid var(--dsw-alias-border-l2);
1852
+ }
1853
+ .${FREE_CHAT_ROOT} .dsm-item-summary {
1854
+ display: flex;
1855
+ align-items: center;
1856
+ justify-content: space-between;
1857
+ gap: 24px;
1858
+ padding: 16px 0;
1859
+ list-style: none;
1860
+ cursor: pointer;
1861
+ }
1862
+ .${FREE_CHAT_ROOT} .dsm-item-summary::-webkit-details-marker { display: none; }
1863
+ .${FREE_CHAT_ROOT} .dsm-item-summary > span:first-child { min-width: 0; }
1864
+ .${FREE_CHAT_ROOT} .dsm-item-title {
1865
+ display: block;
1866
+ font-size: 14px;
1867
+ line-height: 20px;
1868
+ color: var(--dsw-alias-label-primary);
1869
+ }
1870
+ .${FREE_CHAT_ROOT} .dsm-item-desc {
1871
+ display: block;
1872
+ margin-top: 4px;
1873
+ font-size: 12px;
1874
+ line-height: 18px;
1875
+ color: var(--dsw-alias-label-secondary);
1876
+ }
1877
+ .${FREE_CHAT_ROOT} .dsm-item-chevron {
1878
+ width: 14px;
1879
+ height: 14px;
1880
+ color: var(--dsw-alias-label-secondary);
1881
+ transition: transform 0.15s ease;
1882
+ }
1883
+ .${FREE_CHAT_ROOT}.dsm-item[open] > .dsm-item-summary .dsm-item-chevron { transform: rotate(180deg); }
1884
+ .${FREE_CHAT_ROOT} .dsm-item-body { padding: 0 0 16px; }
1885
+ /* Settings group body: one quiet reading surface. Rules, not nested wells,
1886
+ * separate sections; dependent controls align with the feature they belong to. */
1820
1887
  .${FREE_CHAT_ROOT} .dsm-settings {
1821
1888
  display: flex;
1822
1889
  flex-direction: column;
@@ -3183,7 +3250,7 @@ function createPresenceConfigModel(host, delay = (ms) => new Promise((resolve) =
3183
3250
  function same(view, patch) {
3184
3251
  if (view === void 0) return false;
3185
3252
  return Object.entries(patch).every(([key, value]) => {
3186
- if (key === "model" || key === "ghostModel") return sameRoute(view[key], value);
3253
+ if (key === "model" || key === "ghostModel" || key === "judgeModel" || key === "handoffModel") return sameRoute(view[key], value);
3187
3254
  return view[key] === value;
3188
3255
  });
3189
3256
  }
@@ -3591,6 +3658,153 @@ function modelSettingsChangesIdentity(packageName) {
3591
3658
  namespace: segment
3592
3659
  };
3593
3660
  }
3661
+ /**
3662
+ * Shared "Session Management" settings section id. The section is provided by
3663
+ * shared infrastructure (see `registerSessionSettingsItem` in `client-utils`),
3664
+ * not by any one business plugin: every participant registers only a row into
3665
+ * {@link SESSION_SETTINGS_ITEM_SLOT}, so any enabled subset yields the same page.
3666
+ */
3667
+ const SESSION_SETTINGS_SECTION_ID = "session-manager";
3668
+ /**
3669
+ * Child list slot declared by the shared Session Management page. Neutral
3670
+ * naming on purpose: participants never import each other, they only
3671
+ * contribute `{id, order}` rows into this slot while the page is alive.
3672
+ */
3673
+ const SESSION_SETTINGS_ITEM_SLOT = "dsh.session-settings.item";
3674
+ /**
3675
+ * Host-owned nav icon seat: a root-scoped list where each entry's `id` must
3676
+ * equal the `settings.section` id it decorates. Owner props are
3677
+ * `{size, active}`; icons stay `aria-hidden` so the section label remains the
3678
+ * accessible name. Contributors into a shared section may register the same
3679
+ * icon `id` with different priorities — the ledger keeps every entry while the
3680
+ * host renders one icon per section.
3681
+ */
3682
+ const SETTINGS_SECTION_ICON_SLOT = "settings.section.icon";
3683
+ /** Authenticated host-RPC channel for get/set of the shared default. */
3684
+ const AUXILIARY_MODEL_CHANNEL = "/dsh-auxiliary-model";
3685
+ /** Model-settings namespace under which the shared default is published. */
3686
+ const AUXILIARY_MODEL_NS = "@klarkxy/dsh-plugin-kit/auxiliary-model";
3687
+ /**
3688
+ * Typert identity of the shared change stream. Mounted on the root context
3689
+ * with ownerNs filtering, so frames only carry {@link AUXILIARY_MODEL_NS}.
3690
+ */
3691
+ const AUXILIARY_MODEL_CHANGES_IDENTITY = modelSettingsChangesIdentity("@klarkxy/dsh-plugin-kit");
3692
+ //#endregion
3693
+ //#region ../dsh-plugin-kit/src/client-utils.ts
3694
+ /** One `<svg>` with the shared icon chrome; children are plain elements. */
3695
+ function lineIcon(children) {
3696
+ return function SettingsSectionIcon(props) {
3697
+ return (0, react.createElement)("svg", {
3698
+ "aria-hidden": "true",
3699
+ width: props.size ?? 16,
3700
+ height: props.size ?? 16,
3701
+ viewBox: "0 0 16 16",
3702
+ fill: "none",
3703
+ stroke: "currentColor",
3704
+ strokeWidth: 1,
3705
+ strokeLinecap: "round",
3706
+ strokeLinejoin: "round"
3707
+ }, ...children.map(([tag, attrs], index) => (0, react.createElement)(tag, {
3708
+ key: index,
3709
+ ...attrs
3710
+ })));
3711
+ };
3712
+ }
3713
+ /** Chat bubble for the shared Session Management section. */
3714
+ 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" }]]);
3715
+ /** Private Cordis rendezvous; every bundled copy of this helper resolves the same root service. */
3716
+ const SESSION_SETTINGS_PAGE_SERVICE = "dshSessionSettingsPage";
3717
+ /** The page component renders only its declared child list: items paint their own titles and styles. */
3718
+ function SessionSettingsPageView(props) {
3719
+ return props.renderSlot(SESSION_SETTINGS_ITEM_SLOT, {});
3720
+ }
3721
+ /**
3722
+ * Acquire the root-owned page service, creating and providing it on first use.
3723
+ * One counter covers participants, the section wait and the provider: the last
3724
+ * release removes the wait and then the provider synchronously (Cordis deletes
3725
+ * the impl before its async disposer awaits), so a later acquire sees a clean
3726
+ * get() miss and can re-establish the page.
3727
+ */
3728
+ function acquireSessionSettingsPage(root) {
3729
+ const existing = root.get(SESSION_SETTINGS_PAGE_SERVICE, false);
3730
+ if (existing) return existing;
3731
+ const slots = root.slots;
3732
+ let users = 0;
3733
+ let stopPage;
3734
+ let unprovide;
3735
+ const page = {
3736
+ retain() {
3737
+ users += 1;
3738
+ if (!stopPage) stopPage = slots.inject("settings.section", () => slots.register({
3739
+ name: "settings.section",
3740
+ id: SESSION_SETTINGS_SECTION_ID,
3741
+ order: 30,
3742
+ label: () => {
3743
+ const locale = root.get("locale", false);
3744
+ return String(locale?.getSnapshot?.().active ?? "").startsWith("zh") ? "会话管理" : "Session Management";
3745
+ },
3746
+ children: { [SESSION_SETTINGS_ITEM_SLOT]: {
3747
+ kind: "list",
3748
+ scope: "root"
3749
+ } }
3750
+ }, SessionSettingsPageView));
3751
+ },
3752
+ release() {
3753
+ if (users <= 0) return;
3754
+ users -= 1;
3755
+ if (users > 0) return;
3756
+ stopPage?.();
3757
+ stopPage = void 0;
3758
+ unprovide?.();
3759
+ }
3760
+ };
3761
+ unprovide = root.provide(SESSION_SETTINGS_PAGE_SERVICE, page);
3762
+ return page;
3763
+ }
3764
+ /**
3765
+ * Contribute one row to the shared Session Management settings page.
3766
+ *
3767
+ * `ctx` is the plugin's own context: the row's slot wait unloads with the
3768
+ * plugin, while the page itself is retained on the shared root context for as
3769
+ * long as any contributor lives. There is no fallback seat — when every
3770
+ * contributor is gone the section disappears with the page. Returns a disposer.
3771
+ * Shared root-owned infrastructure (the auxiliary-model card) calls this on
3772
+ * `ctx.root` so its row, icon and page share are owned by the root lifetime
3773
+ * and released by the returned disposer, not by any plugin fiber.
3774
+ */
3775
+ function registerSessionSettingsItem(ctx, item, component) {
3776
+ const slots = ctx.slots;
3777
+ const root = ctx.root;
3778
+ const page = acquireSessionSettingsPage(root);
3779
+ let stopItem;
3780
+ let stopIcon;
3781
+ try {
3782
+ page.retain();
3783
+ stopItem = slots.inject(SESSION_SETTINGS_ITEM_SLOT, () => slots.register({
3784
+ name: SESSION_SETTINGS_ITEM_SLOT,
3785
+ ...item
3786
+ }, component));
3787
+ stopIcon = slots.inject(SETTINGS_SECTION_ICON_SLOT, () => slots.register({
3788
+ name: SETTINGS_SECTION_ICON_SLOT,
3789
+ id: SESSION_SETTINGS_SECTION_ID,
3790
+ order: item.order,
3791
+ priority: item.order
3792
+ }, IconSessionSettings));
3793
+ } catch (error) {
3794
+ stopIcon?.();
3795
+ stopItem?.();
3796
+ page.release();
3797
+ throw error;
3798
+ }
3799
+ let disposed = false;
3800
+ return () => {
3801
+ if (disposed) return;
3802
+ disposed = true;
3803
+ stopIcon?.();
3804
+ stopItem?.();
3805
+ page.release();
3806
+ };
3807
+ }
3594
3808
  //#endregion
3595
3809
  //#region ../dsh-plugin-kit/src/model-settings-client.ts
3596
3810
  /** This stream carries invalidations only; ready is never a settings snapshot. */
@@ -3760,6 +3974,488 @@ function createModelSettingsSubscriber(ctx, packageName, identity = modelSetting
3760
3974
  } });
3761
3975
  }
3762
3976
  //#endregion
3977
+ //#region ../dsh-plugin-kit/src/auxiliary-model-client.tsx
3978
+ const AUXILIARY_MODEL_PACKAGE = "@klarkxy/dsh-plugin-kit";
3979
+ const AUXILIARY_MODEL_SEAT_ID = "auxiliary-model";
3980
+ /** Private Cordis rendezvous; every bundled copy resolves the same root service. */
3981
+ const AUXILIARY_MODEL_CARD_SERVICE = "dshAuxiliaryModelSettingsCard";
3982
+ /** Theme-token-only stylesheet: officialUiCss scope plus the shared ModelMenu recipe. */
3983
+ const auxiliaryModelCss = `${officialUiCss("ams-root")}${modelMenuCss}
3984
+ .ams-root .ams-row { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; min-width: 0; }
3985
+ `;
3986
+ const auxiliaryCopy = {
3987
+ zh: {
3988
+ title: "默认辅助模型",
3989
+ help: "各辅助功能(如标题生成、摘要)没有单独指定模型时使用的模型;仅影响声明使用它的功能。",
3990
+ model: "辅助模型",
3991
+ modelDefault: "跟随会话或宿主默认",
3992
+ modelHint: "不选择模型时,各功能按自己的规则决定对话模型。",
3993
+ effortDefault: "跟随模型默认",
3994
+ loading: "正在读取设置…",
3995
+ failed: "无法读取模型设置。",
3996
+ stale: "设置已被其他窗口更新,请刷新后重试。",
3997
+ saveFailed: "保存失败,原设置未更改。请重试。",
3998
+ saved: "已保存。",
3999
+ retry: "重试",
4000
+ refresh: "刷新设置",
4001
+ syncUnavailable: "自动刷新不可用,请手动刷新设置。"
4002
+ },
4003
+ en: {
4004
+ title: "Default Auxiliary Model",
4005
+ 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.",
4006
+ model: "Auxiliary model",
4007
+ modelDefault: "Follow session or host default",
4008
+ modelHint: "With no model selected, each feature follows its own rules for the conversation model.",
4009
+ effortDefault: "Follow model default",
4010
+ loading: "Loading settings…",
4011
+ failed: "Could not load model settings.",
4012
+ stale: "Settings were changed elsewhere; refresh and retry.",
4013
+ saveFailed: "Save failed; the previous setting was not changed. Try again.",
4014
+ saved: "Saved.",
4015
+ retry: "Retry",
4016
+ refresh: "Refresh settings",
4017
+ syncUnavailable: "Automatic refresh unavailable; refresh settings manually."
4018
+ }
4019
+ };
4020
+ /** A failed RPC keeps its code so the card can translate known rejections. */
4021
+ var AuxiliaryRpcError = class extends Error {
4022
+ code;
4023
+ constructor(code, message) {
4024
+ super(message);
4025
+ this.code = code;
4026
+ this.name = "AuxiliaryRpcError";
4027
+ }
4028
+ };
4029
+ /** Host UI language, following live switches with the host's English fallback. */
4030
+ function useAuxiliaryLocale(client) {
4031
+ const subscribe = (0, react.useCallback)((listener) => client.locale?.subscribe(listener) ?? (() => {}), [client]);
4032
+ const snapshot = (0, react.useCallback)(() => client.locale?.getSnapshot().active ?? "en", [client]);
4033
+ return String((0, react.useSyncExternalStore)(subscribe, snapshot, snapshot)).startsWith("zh") ? "zh" : "en";
4034
+ }
4035
+ function isAbort(error) {
4036
+ return error instanceof Error && (error.name === "AbortError" || /abort|cancel/i.test(error.message));
4037
+ }
4038
+ function isStaleRejection(error) {
4039
+ if (!(error instanceof AuxiliaryRpcError)) return false;
4040
+ return error.code === "stale" || error.code === "auxiliary-model/conflict" || /changed|revision|reload|stale|conflict/i.test(error.message);
4041
+ }
4042
+ async function callAuxiliaryRpc(client, endpoint, payload = {}) {
4043
+ const result = await client.connection.rpc.call(AUXILIARY_MODEL_CHANNEL, endpoint, payload);
4044
+ if (!result || result.ok !== true) {
4045
+ const error = result && "error" in result ? result.error : void 0;
4046
+ throw new AuxiliaryRpcError(error?.code ?? "unknown", error?.message ?? `RPC ${endpoint} failed`);
4047
+ }
4048
+ return result.value;
4049
+ }
4050
+ /**
4051
+ * The shared card. All state comes from the shared host service over the
4052
+ * plugin-kit RPC channel; the card only renders the stored row.
4053
+ */
4054
+ function AuxiliaryModelCard({ client, subscribe }) {
4055
+ const language = useAuxiliaryLocale(client);
4056
+ const text = auxiliaryCopy[language];
4057
+ const [settings, setSettings] = (0, react.useState)();
4058
+ const catalogState = useModelCatalog((0, react.useCallback)(() => readModelCatalog(client), [client]), { subscribe: (0, react.useCallback)((change) => client.connection.generation?.subscribe(change), [client]) });
4059
+ const { refresh: refreshCatalog } = catalogState;
4060
+ const choices = parseModelMenuChoices(catalogState.catalog, settings?.model ?? void 0);
4061
+ const [menuOpen, setMenuOpen] = (0, react.useState)(false);
4062
+ const [error, setError] = (0, react.useState)("");
4063
+ const [note, setNote] = (0, react.useState)(false);
4064
+ const [busy, setBusy] = (0, react.useState)(false);
4065
+ const [syncError, setSyncError] = (0, react.useState)("");
4066
+ const operation = (0, react.useRef)({
4067
+ epoch: 0,
4068
+ active: false,
4069
+ reading: false,
4070
+ writing: false,
4071
+ pending: false
4072
+ });
4073
+ const refreshRef = (0, react.useRef)(() => {});
4074
+ const refresh = () => {
4075
+ const state = operation.current;
4076
+ if (!state.active) return;
4077
+ if (state.reading || state.writing) {
4078
+ state.pending = true;
4079
+ return;
4080
+ }
4081
+ state.pending = false;
4082
+ state.reading = true;
4083
+ setBusy(true);
4084
+ const epoch = state.epoch;
4085
+ callAuxiliaryRpc(client, "defaults.get").then((next) => {
4086
+ if (state.active && state.epoch === epoch && !state.pending) {
4087
+ setSettings(next);
4088
+ setError("");
4089
+ }
4090
+ }).catch((reason) => {
4091
+ if (state.active && state.epoch === epoch && !state.pending && !isAbort(reason)) setError("failed");
4092
+ }).finally(() => {
4093
+ if (!state.active || state.epoch !== epoch) return;
4094
+ state.reading = false;
4095
+ setBusy(false);
4096
+ if (state.pending) refreshRef.current();
4097
+ });
4098
+ };
4099
+ refreshRef.current = refresh;
4100
+ (0, react.useEffect)(() => {
4101
+ const state = operation.current;
4102
+ state.epoch++;
4103
+ state.active = true;
4104
+ state.reading = false;
4105
+ state.writing = false;
4106
+ state.pending = false;
4107
+ setBusy(false);
4108
+ setSettings(void 0);
4109
+ setError("");
4110
+ setNote(false);
4111
+ refreshRef.current();
4112
+ return () => {
4113
+ state.active = false;
4114
+ state.epoch++;
4115
+ };
4116
+ }, [client]);
4117
+ (0, react.useEffect)(() => {
4118
+ setSyncError("");
4119
+ let live = true;
4120
+ let stop;
4121
+ try {
4122
+ stop = subscribe?.(() => {
4123
+ if (live) {
4124
+ setSyncError("");
4125
+ refreshCatalog();
4126
+ refreshRef.current();
4127
+ }
4128
+ }, (message) => {
4129
+ if (live) setSyncError(message);
4130
+ });
4131
+ } catch {
4132
+ if (live) setSyncError("Model settings sync service unavailable");
4133
+ }
4134
+ return () => {
4135
+ live = false;
4136
+ try {
4137
+ stop?.();
4138
+ } catch {}
4139
+ };
4140
+ }, [subscribe, refreshCatalog]);
4141
+ const save = (0, react.useCallback)((model) => {
4142
+ const state = operation.current;
4143
+ if (!settings || !state.active || state.writing || state.reading) return;
4144
+ state.writing = true;
4145
+ const epoch = state.epoch;
4146
+ setBusy(true);
4147
+ setNote(false);
4148
+ setError("");
4149
+ callAuxiliaryRpc(client, "defaults.set", {
4150
+ model: model ?? {
4151
+ provider: "",
4152
+ model: ""
4153
+ },
4154
+ expectedRevision: settings.revision
4155
+ }).then((next) => {
4156
+ if (state.active && state.epoch === epoch) {
4157
+ setSettings(next);
4158
+ setNote(true);
4159
+ refreshCatalog();
4160
+ }
4161
+ }).catch((reason) => {
4162
+ if (state.active && state.epoch === epoch) setError(isStaleRejection(reason) ? "stale" : "saveFailed");
4163
+ }).finally(() => {
4164
+ if (!state.active || state.epoch !== epoch) return;
4165
+ state.writing = false;
4166
+ setBusy(false);
4167
+ if (state.pending) refreshRef.current();
4168
+ });
4169
+ }, [
4170
+ client,
4171
+ settings,
4172
+ refreshCatalog
4173
+ ]);
4174
+ const locked = busy;
4175
+ if (!settings) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
4176
+ className: "ams-root dsh-ui-card",
4177
+ "data-testid": "auxiliary-model-settings",
4178
+ children: [
4179
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4180
+ className: "dsh-ui-heading",
4181
+ children: text.title
4182
+ }),
4183
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
4184
+ className: "dsh-ui-help",
4185
+ children: text.help
4186
+ }),
4187
+ error ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
4188
+ role: "alert",
4189
+ className: "dsh-ui-error",
4190
+ children: text[error]
4191
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
4192
+ role: "status",
4193
+ className: "dsh-ui-hint",
4194
+ children: text.loading
4195
+ }),
4196
+ syncError && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
4197
+ role: "status",
4198
+ className: "dsh-ui-hint",
4199
+ children: text.syncUnavailable
4200
+ }),
4201
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4202
+ className: "dsh-ui-actions",
4203
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
4204
+ variant: "ghost",
4205
+ size: "sm",
4206
+ disabled: locked,
4207
+ onClick: () => refreshRef.current(),
4208
+ children: text.retry
4209
+ })
4210
+ })
4211
+ ]
4212
+ });
4213
+ const current = settings.model ?? {
4214
+ provider: "",
4215
+ model: ""
4216
+ };
4217
+ const follow = !current.provider || !current.model;
4218
+ const selected = follow ? void 0 : choices.find((item) => item.provider === current.provider && item.model === current.model);
4219
+ const efforts = follow ? [] : modelMenuEffortOptions(selected, current.reasoningEffort);
4220
+ const currentLabel = follow ? text.modelDefault : selected?.label ?? `${current.provider} / ${current.model}`;
4221
+ const selectedEffort = efforts.find((item) => item.id === (current.reasoningEffort || ""));
4222
+ const effortName = selectedEffort ? modelMenuEffortLabel(selectedEffort, language) : void 0;
4223
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
4224
+ className: "ams-root dsh-ui-card",
4225
+ "data-testid": "auxiliary-model-settings",
4226
+ children: [
4227
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4228
+ className: "dsh-ui-heading",
4229
+ children: text.title
4230
+ }),
4231
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
4232
+ className: "dsh-ui-help",
4233
+ children: text.help
4234
+ }),
4235
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4236
+ className: "dsh-ui-field",
4237
+ children: [
4238
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4239
+ className: "dsh-ui-label",
4240
+ children: text.model
4241
+ }),
4242
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelCatalogStatus, {
4243
+ state: catalogState,
4244
+ locale: language
4245
+ }),
4246
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelMenu, {
4247
+ locale: language,
4248
+ open: menuOpen && !locked,
4249
+ onOpenChange: (next) => setMenuOpen(next && !locked),
4250
+ listClassName: "ams-root ams-menu-portal",
4251
+ anchor: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
4252
+ type: "button",
4253
+ className: "dsh-model-menu-trigger",
4254
+ disabled: locked,
4255
+ "aria-label": `${text.model}: ${currentLabel}`,
4256
+ children: [
4257
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4258
+ className: "dsh-model-menu-triggerLabel",
4259
+ children: currentLabel
4260
+ }),
4261
+ !follow && efforts.length ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4262
+ className: "dsh-model-menu-triggerEffort",
4263
+ children: effortName ?? text.effortDefault
4264
+ }) : null,
4265
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutlineRegular, { className: `dsh-model-menu-chevron${menuOpen && !locked ? " dsh-model-menu-chevronOpen" : ""}` })
4266
+ ]
4267
+ }),
4268
+ choices,
4269
+ selected: follow ? void 0 : {
4270
+ provider: current.provider,
4271
+ model: current.model
4272
+ },
4273
+ onPick: (picked) => save({
4274
+ provider: picked.provider,
4275
+ model: picked.model
4276
+ }),
4277
+ leading: [{
4278
+ id: "default",
4279
+ label: text.modelDefault,
4280
+ selected: follow
4281
+ }],
4282
+ onPickLeading: () => save(null),
4283
+ efforts,
4284
+ selectedEffort: current.reasoningEffort || "",
4285
+ onPickEffort: (effort) => save(follow ? null : {
4286
+ provider: current.provider,
4287
+ model: current.model,
4288
+ ...effort ? { reasoningEffort: effort } : {}
4289
+ }),
4290
+ modelLabel: text.model,
4291
+ modelValue: currentLabel,
4292
+ defaultEffortLabel: text.effortDefault
4293
+ }),
4294
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4295
+ className: "dsh-ui-hint",
4296
+ children: text.modelHint
4297
+ })
4298
+ ]
4299
+ }),
4300
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4301
+ className: "ams-row",
4302
+ children: [
4303
+ syncError && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
4304
+ role: "status",
4305
+ className: "dsh-ui-hint",
4306
+ children: text.syncUnavailable
4307
+ }),
4308
+ error && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
4309
+ role: "alert",
4310
+ className: "dsh-ui-error",
4311
+ children: text[error]
4312
+ }),
4313
+ note && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
4314
+ role: "status",
4315
+ className: "dsh-ui-notice",
4316
+ children: text.saved
4317
+ }),
4318
+ error || syncError ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
4319
+ variant: "ghost",
4320
+ size: "sm",
4321
+ disabled: locked,
4322
+ onClick: () => refreshRef.current(),
4323
+ children: text.refresh
4324
+ }) : null
4325
+ ]
4326
+ })
4327
+ ]
4328
+ });
4329
+ }
4330
+ const AUXILIARY_MODEL_WATCH_COORDINATOR = "dshAuxiliaryModelWatchCoordinator";
4331
+ function acquireWatchCoordinator(root) {
4332
+ const existing = root.get(AUXILIARY_MODEL_WATCH_COORDINATOR, false);
4333
+ if (existing) return existing;
4334
+ let unprovide;
4335
+ const coordinator = {
4336
+ tail: Promise.resolve(),
4337
+ active: 0,
4338
+ mount() {
4339
+ coordinator.active += 1;
4340
+ const previousTail = coordinator.tail;
4341
+ const lease = {
4342
+ closed: false,
4343
+ done() {
4344
+ if (lease.closed) return;
4345
+ lease.closed = true;
4346
+ coordinator.active -= 1;
4347
+ const owned = lease.fiber;
4348
+ lease.fiber = void 0;
4349
+ coordinator.tail = coordinator.tail.then(() => Promise.resolve(owned?.dispose()).catch(() => {}));
4350
+ coordinator.tail.then(() => {
4351
+ if (coordinator.active > 0) return;
4352
+ try {
4353
+ if (root.get(AUXILIARY_MODEL_WATCH_COORDINATOR, false) === coordinator) Promise.resolve(unprovide?.()).catch(() => {});
4354
+ } catch {}
4355
+ });
4356
+ },
4357
+ fiber: void 0
4358
+ };
4359
+ previousTail.then(() => {
4360
+ if (lease.closed) return;
4361
+ try {
4362
+ lease.fiber = root.inject(["remote"], (injected) => {
4363
+ const scope = injected;
4364
+ const mounting = scope.remote.$mount(modelSettingsChangesRemote(AUXILIARY_MODEL_PACKAGE, AUXILIARY_MODEL_CHANGES_IDENTITY));
4365
+ mounting.catch(() => {});
4366
+ scope.effect(() => async () => {
4367
+ await (await mounting.catch(() => void 0))?.();
4368
+ });
4369
+ });
4370
+ } catch {}
4371
+ });
4372
+ return lease;
4373
+ }
4374
+ };
4375
+ unprovide = root.provide(AUXILIARY_MODEL_WATCH_COORDINATOR, coordinator);
4376
+ return coordinator;
4377
+ }
4378
+ /**
4379
+ * Acquire the root-owned card service, creating it on first use. One counter
4380
+ * covers participants, the seat wait and the change-stream mount: the last
4381
+ * release removes the wait, then the watch contribution, then the provider
4382
+ * itself synchronously (Cordis deletes the impl before its async disposer
4383
+ * awaits), so a later acquire sees a clean get() miss and can re-establish.
4384
+ */
4385
+ function acquireAuxiliaryModelCard(root, ctx) {
4386
+ const existing = root.get(AUXILIARY_MODEL_CARD_SERVICE, false);
4387
+ if (existing) return existing;
4388
+ let users = 0;
4389
+ let styleElement;
4390
+ let stopItem;
4391
+ let unprovide;
4392
+ const watchLease = acquireWatchCoordinator(root).mount();
4393
+ const subscriber = createModelSettingsSubscriber(ctx.root, AUXILIARY_MODEL_PACKAGE, AUXILIARY_MODEL_CHANGES_IDENTITY, (ns) => ns === AUXILIARY_MODEL_NS);
4394
+ const render = () => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AuxiliaryModelCard, {
4395
+ client: root,
4396
+ subscribe: subscriber.subscribe
4397
+ });
4398
+ const service = {
4399
+ retain() {
4400
+ users += 1;
4401
+ if (styleElement === void 0 && typeof document !== "undefined") {
4402
+ styleElement = document.createElement("style");
4403
+ styleElement.setAttribute("data-plugin", "dsh-auxiliary-model-shared");
4404
+ styleElement.textContent = auxiliaryModelCss;
4405
+ document.head.appendChild(styleElement);
4406
+ }
4407
+ if (!stopItem) stopItem = registerSessionSettingsItem(ctx.root, {
4408
+ id: AUXILIARY_MODEL_SEAT_ID,
4409
+ order: 55
4410
+ }, render);
4411
+ },
4412
+ release() {
4413
+ if (users <= 0) return;
4414
+ users -= 1;
4415
+ if (users > 0) return;
4416
+ stopItem?.();
4417
+ stopItem = void 0;
4418
+ styleElement?.remove();
4419
+ styleElement = void 0;
4420
+ watchLease.done();
4421
+ try {
4422
+ Promise.resolve(unprovide?.()).catch(() => {});
4423
+ } catch {}
4424
+ unprovide = void 0;
4425
+ }
4426
+ };
4427
+ unprovide = root.provide(AUXILIARY_MODEL_CARD_SERVICE, service);
4428
+ return service;
4429
+ }
4430
+ /**
4431
+ * Show the shared "default auxiliary model" card while the calling plugin is
4432
+ * enabled.
4433
+ *
4434
+ * `ctx` is the plugin's own client context: the card itself is retained on the
4435
+ * shared root context for as long as any participant lives, so an arbitrary
4436
+ * subset of enabled plugins yields the same single card. The mount's slot wait
4437
+ * and the change-stream contribution are owned by the root service — the first
4438
+ * participant's unload cannot take the card down. Returns a disposer; it may
4439
+ * be invoked by both ctx.effect and an explicit caller and releases at most
4440
+ * once, so a double call cannot unbalance the shared refcount.
4441
+ */
4442
+ function registerAuxiliaryModelSettings(ctx) {
4443
+ const root = ctx.root;
4444
+ const service = acquireAuxiliaryModelCard(root, ctx);
4445
+ try {
4446
+ service.retain();
4447
+ } catch (error) {
4448
+ service.release();
4449
+ throw error;
4450
+ }
4451
+ let disposed = false;
4452
+ return () => {
4453
+ if (disposed) return;
4454
+ disposed = true;
4455
+ service.release();
4456
+ };
4457
+ }
4458
+ //#endregion
3763
4459
  //#region src/presence-panel.tsx
3764
4460
  /**
3765
4461
  * Grey inline-completion for an empty composer, IDE style: the shipped
@@ -4007,7 +4703,9 @@ function toDraft(values) {
4007
4703
  autoContinueMode: values.autoContinueMode,
4008
4704
  autoContinueMax: String(values.autoContinueMax),
4009
4705
  model: values.model === null ? "" : modelRouteKey(values.model.provider, values.model.model),
4010
- ghostModel: values.ghostModel === null ? "" : modelRouteKey(values.ghostModel.provider, values.ghostModel.model)
4706
+ ghostModel: values.ghostModel === null ? "" : modelRouteKey(values.ghostModel.provider, values.ghostModel.model),
4707
+ judgeModel: values.judgeModel === null ? "" : modelRouteKey(values.judgeModel.provider, values.judgeModel.model),
4708
+ handoffModel: values.handoffModel === null ? "" : modelRouteKey(values.handoffModel.provider, values.handoffModel.model)
4011
4709
  };
4012
4710
  }
4013
4711
  /** Enabled thresholds are whole minutes of at least 1. 0 stays the stored "off" value and is not a valid enabled draft. */
@@ -4023,7 +4721,7 @@ function autoMaxOf(text) {
4023
4721
  return Number.isSafeInteger(value) && value >= 1 && value <= 10 ? value : void 0;
4024
4722
  }
4025
4723
  function sameDraft(left, right) {
4026
- return left.recapEnabled === right.recapEnabled && left.recapMinutes === right.recapMinutes && left.compactEnabled === right.compactEnabled && left.compactMinutes === right.compactMinutes && left.continueText === right.continueText && left.continueMode === right.continueMode && left.autoContinueMode === right.autoContinueMode && left.autoContinueMax === right.autoContinueMax && left.model === right.model && left.ghostModel === right.ghostModel;
4724
+ return left.recapEnabled === right.recapEnabled && left.recapMinutes === right.recapMinutes && left.compactEnabled === right.compactEnabled && left.compactMinutes === right.compactMinutes && left.continueText === right.continueText && left.continueMode === right.continueMode && left.autoContinueMode === right.autoContinueMode && left.autoContinueMax === right.autoContinueMax && left.model === right.model && left.ghostModel === right.ghostModel && left.judgeModel === right.judgeModel && left.handoffModel === right.handoffModel;
4027
4725
  }
4028
4726
  const missingCatalog = async () => {
4029
4727
  throw new Error("Model catalog service unavailable");
@@ -4035,6 +4733,8 @@ function SessionManagerSettings(props) {
4035
4733
  const catalog = useModelCatalog(props.modelCatalog ?? missingCatalog, { subscribe: props.subscribeCatalog });
4036
4734
  const [menuOpen, setMenuOpen] = (0, react.useState)(false);
4037
4735
  const [ghostMenuOpen, setGhostMenuOpen] = (0, react.useState)(false);
4736
+ const [judgeMenuOpen, setJudgeMenuOpen] = (0, react.useState)(false);
4737
+ const [handoffMenuOpen, setHandoffMenuOpen] = (0, react.useState)(false);
4038
4738
  const [externalChange, setExternalChange] = (0, react.useState)(false);
4039
4739
  const dirty = (0, react.useRef)(false);
4040
4740
  const baseline = (0, react.useRef)();
@@ -4077,9 +4777,25 @@ function SessionManagerSettings(props) {
4077
4777
  });
4078
4778
  const selectedRoute = parseModelRouteKey(draft.model);
4079
4779
  const ghostRoute = parseModelRouteKey(draft.ghostModel);
4080
- const selectableChoices = parseModelMenuChoices(catalog.catalog, selectedRoute ?? ghostRoute);
4081
- const selectedLabel = selectableChoices.find((choice) => modelRouteKey(choice.provider, choice.model) === draft.model)?.label ?? copy("modelFollowOption");
4082
- const ghostLabel = selectableChoices.find((choice) => modelRouteKey(choice.provider, choice.model) === draft.ghostModel)?.label ?? copy("modelFollowOption");
4780
+ const judgeRoute = parseModelRouteKey(draft.judgeModel);
4781
+ const handoffRoute = parseModelRouteKey(draft.handoffModel);
4782
+ const selectableChoices = parseModelMenuChoices(catalog.catalog);
4783
+ for (const bound of [
4784
+ selectedRoute,
4785
+ ghostRoute,
4786
+ judgeRoute,
4787
+ handoffRoute
4788
+ ]) if (bound && !selectableChoices.some((choice) => choice.provider === bound.provider && choice.model === bound.model)) selectableChoices.push({
4789
+ provider: bound.provider,
4790
+ model: bound.model,
4791
+ label: `${bound.provider} / ${bound.model}`,
4792
+ efforts: []
4793
+ });
4794
+ const routeLabel = (key) => selectableChoices.find((choice) => modelRouteKey(choice.provider, choice.model) === key)?.label ?? copy("modelFollowOption");
4795
+ const selectedLabel = routeLabel(draft.model);
4796
+ const ghostLabel = routeLabel(draft.ghostModel);
4797
+ const judgeLabel = routeLabel(draft.judgeModel);
4798
+ const handoffLabel = routeLabel(draft.handoffModel);
4083
4799
  const recapMinutes = draft.recapEnabled ? minutesOf(draft.recapMinutes) : 0;
4084
4800
  const compactMinutes = draft.compactEnabled ? minutesOf(draft.compactMinutes) : 0;
4085
4801
  const autoContinueMax = autoMaxOf(draft.autoContinueMax);
@@ -4091,8 +4807,14 @@ function SessionManagerSettings(props) {
4091
4807
  const autoPanelId = `${tabsId}-auto-panel`;
4092
4808
  const save = () => {
4093
4809
  if (invalid) return;
4094
- const route = parseModelRouteKey(draft.model);
4095
- const ghost = parseModelRouteKey(draft.ghostModel);
4810
+ const routePatch = (key) => {
4811
+ if (draft[key] === baseline.current?.[key]) return {};
4812
+ const parsed = parseModelRouteKey(draft[key]);
4813
+ return { [key]: parsed === void 0 ? null : {
4814
+ provider: parsed.provider,
4815
+ model: parsed.model
4816
+ } };
4817
+ };
4096
4818
  setExternalChange(false);
4097
4819
  props.model.save({
4098
4820
  recapMinutes,
@@ -4101,14 +4823,10 @@ function SessionManagerSettings(props) {
4101
4823
  continueMode: draft.continueMode,
4102
4824
  autoContinueMode: draft.autoContinueMode,
4103
4825
  autoContinueMax,
4104
- ...draft.model === baseline.current?.model ? {} : { model: route === void 0 ? null : {
4105
- provider: route.provider,
4106
- model: route.model
4107
- } },
4108
- ...draft.ghostModel === baseline.current?.ghostModel ? {} : { ghostModel: ghost === void 0 ? null : {
4109
- provider: ghost.provider,
4110
- model: ghost.model
4111
- } }
4826
+ ...routePatch("model"),
4827
+ ...routePatch("ghostModel"),
4828
+ ...routePatch("judgeModel"),
4829
+ ...routePatch("handoffModel")
4112
4830
  });
4113
4831
  };
4114
4832
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -4245,72 +4963,181 @@ function SessionManagerSettings(props) {
4245
4963
  })
4246
4964
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4247
4965
  className: "dsm-feature-options",
4248
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
4249
- className: "dsh-ui-field",
4966
+ children: [
4967
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
4968
+ className: "dsh-ui-field",
4969
+ children: [
4970
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4971
+ className: "dsh-ui-label",
4972
+ children: copy("autoContinueDefaultLabel")
4973
+ }),
4974
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
4975
+ className: "dsh-ui-select",
4976
+ value: draft.autoContinueMode,
4977
+ onChange: (event) => edit({
4978
+ ...draft,
4979
+ autoContinueMode: event.target.value
4980
+ }),
4981
+ children: [
4982
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
4983
+ value: "off",
4984
+ children: copy("autoContinueOff")
4985
+ }),
4986
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
4987
+ value: "cautious",
4988
+ children: copy("autoContinueCautious")
4989
+ }),
4990
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
4991
+ value: "bold",
4992
+ children: copy("autoContinueBold")
4993
+ })
4994
+ ]
4995
+ }),
4996
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4997
+ className: "dsh-ui-hint",
4998
+ children: copy("autoContinueDefaultHint")
4999
+ })
5000
+ ]
5001
+ }),
5002
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [
5003
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
5004
+ className: "dsh-ui-field",
5005
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5006
+ className: "dsh-ui-label",
5007
+ children: copy("autoContinueMaxLabel")
5008
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5009
+ className: "dsm-minutes-control",
5010
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Input, {
5011
+ className: "dsm-minutes",
5012
+ value: draft.autoContinueMax,
5013
+ inputMode: "numeric",
5014
+ "aria-invalid": autoMaxInvalid || void 0,
5015
+ "aria-describedby": autoMaxInvalid ? `${tabsId}-auto-max-error` : void 0,
5016
+ onChange: (event) => edit({
5017
+ ...draft,
5018
+ autoContinueMax: event.target.value
5019
+ })
5020
+ })
5021
+ })]
5022
+ }),
5023
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
5024
+ className: "dsh-ui-hint",
5025
+ children: copy("autoContinueMaxHint")
5026
+ }),
5027
+ autoMaxInvalid ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
5028
+ id: `${tabsId}-auto-max-error`,
5029
+ className: "dsh-ui-error",
5030
+ role: "alert",
5031
+ children: copy("autoContinueMaxInvalid")
5032
+ }) : null
5033
+ ] }),
5034
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5035
+ className: "dsh-ui-field dsm-model-field",
5036
+ children: [
5037
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5038
+ className: "dsh-ui-label",
5039
+ children: copy("judgeModelLabel")
5040
+ }),
5041
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelMenu, {
5042
+ locale: props.locale,
5043
+ open: judgeMenuOpen,
5044
+ onOpenChange: setJudgeMenuOpen,
5045
+ listClassName: FREE_CHAT_ROOT,
5046
+ anchor: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
5047
+ type: "button",
5048
+ className: "dsh-model-menu-trigger",
5049
+ "aria-label": `${copy("judgeModelLabel")}: ${judgeLabel}`,
5050
+ "aria-haspopup": "menu",
5051
+ "aria-expanded": judgeMenuOpen,
5052
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5053
+ className: "dsh-model-menu-triggerLabel",
5054
+ children: judgeLabel
5055
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutlineRegular, { className: `dsh-model-menu-chevron${judgeMenuOpen ? " dsh-model-menu-chevronOpen" : ""}` })]
5056
+ }),
5057
+ choices: selectableChoices,
5058
+ selected: judgeRoute,
5059
+ onPick: (route) => edit({
5060
+ ...draft,
5061
+ judgeModel: modelRouteKey(route.provider, route.model)
5062
+ }),
5063
+ leading: [{
5064
+ id: "follow",
5065
+ label: copy("modelFollowOption"),
5066
+ selected: !judgeRoute
5067
+ }],
5068
+ onPickLeading: () => edit({
5069
+ ...draft,
5070
+ judgeModel: ""
5071
+ }),
5072
+ modelValue: judgeLabel
5073
+ }),
5074
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5075
+ className: "dsh-ui-hint",
5076
+ children: copy("judgeModelHint")
5077
+ })
5078
+ ]
5079
+ })
5080
+ ]
5081
+ })]
5082
+ }),
5083
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
5084
+ className: "dsm-setting",
5085
+ "aria-labelledby": `${tabsId}-handoff-heading`,
5086
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("header", {
5087
+ className: "dsm-setting-header",
5088
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
5089
+ id: `${tabsId}-handoff-heading`,
5090
+ className: "dsh-ui-title",
5091
+ children: copy("handoffGroup")
5092
+ })
5093
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5094
+ className: "dsm-feature-options",
5095
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5096
+ className: "dsh-ui-field dsm-model-field",
4250
5097
  children: [
4251
5098
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4252
5099
  className: "dsh-ui-label",
4253
- children: copy("autoContinueDefaultLabel")
5100
+ children: copy("handoffModelLabel")
4254
5101
  }),
4255
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
4256
- className: "dsh-ui-select",
4257
- value: draft.autoContinueMode,
4258
- onChange: (event) => edit({
5102
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelMenu, {
5103
+ locale: props.locale,
5104
+ open: handoffMenuOpen,
5105
+ onOpenChange: setHandoffMenuOpen,
5106
+ listClassName: FREE_CHAT_ROOT,
5107
+ anchor: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
5108
+ type: "button",
5109
+ className: "dsh-model-menu-trigger",
5110
+ "aria-label": `${copy("handoffModelLabel")}: ${handoffLabel}`,
5111
+ "aria-haspopup": "menu",
5112
+ "aria-expanded": handoffMenuOpen,
5113
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5114
+ className: "dsh-model-menu-triggerLabel",
5115
+ children: handoffLabel
5116
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutlineRegular, { className: `dsh-model-menu-chevron${handoffMenuOpen ? " dsh-model-menu-chevronOpen" : ""}` })]
5117
+ }),
5118
+ choices: selectableChoices,
5119
+ selected: handoffRoute,
5120
+ onPick: (route) => edit({
4259
5121
  ...draft,
4260
- autoContinueMode: event.target.value
5122
+ handoffModel: modelRouteKey(route.provider, route.model)
4261
5123
  }),
4262
- children: [
4263
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
4264
- value: "off",
4265
- children: copy("autoContinueOff")
4266
- }),
4267
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
4268
- value: "cautious",
4269
- children: copy("autoContinueCautious")
4270
- }),
4271
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
4272
- value: "bold",
4273
- children: copy("autoContinueBold")
4274
- })
4275
- ]
5124
+ leading: [{
5125
+ id: "follow",
5126
+ label: copy("modelFollowOption"),
5127
+ selected: !handoffRoute
5128
+ }],
5129
+ onPickLeading: () => edit({
5130
+ ...draft,
5131
+ handoffModel: ""
5132
+ }),
5133
+ modelValue: handoffLabel
4276
5134
  }),
4277
5135
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4278
5136
  className: "dsh-ui-hint",
4279
- children: copy("autoContinueDefaultHint")
5137
+ children: copy("handoffModelHint")
4280
5138
  })
4281
5139
  ]
4282
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [
4283
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
4284
- className: "dsh-ui-field",
4285
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4286
- className: "dsh-ui-label",
4287
- children: copy("autoContinueMaxLabel")
4288
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4289
- className: "dsm-minutes-control",
4290
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Input, {
4291
- className: "dsm-minutes",
4292
- value: draft.autoContinueMax,
4293
- inputMode: "numeric",
4294
- "aria-invalid": autoMaxInvalid || void 0,
4295
- "aria-describedby": autoMaxInvalid ? `${tabsId}-auto-max-error` : void 0,
4296
- onChange: (event) => edit({
4297
- ...draft,
4298
- autoContinueMax: event.target.value
4299
- })
4300
- })
4301
- })]
4302
- }),
4303
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
4304
- className: "dsh-ui-hint",
4305
- children: copy("autoContinueMaxHint")
4306
- }),
4307
- autoMaxInvalid ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
4308
- id: `${tabsId}-auto-max-error`,
4309
- className: "dsh-ui-error",
4310
- role: "alert",
4311
- children: copy("autoContinueMaxInvalid")
4312
- }) : null
4313
- ] })]
5140
+ })
4314
5141
  })]
4315
5142
  }),
4316
5143
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
@@ -5448,15 +6275,16 @@ function apply(ctx) {
5448
6275
  locale: useLiveLocale(client.locale),
5449
6276
  model: welcomeBack
5450
6277
  }))));
5451
- stops.push(client.slots.inject("plugins.bundle.config", () => client.slots.register({
5452
- name: "plugins.bundle.config",
5453
- key: "@klarkxy/dsh-session-manager"
5454
- }, () => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SessionManagerSettings, {
6278
+ stops.push(registerSessionSettingsItem(ctx, {
6279
+ id: SESSION_SETTINGS_ITEM_ID,
6280
+ order: 65
6281
+ }, () => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SessionManagerSettingsItem, {
5455
6282
  model: presenceConfig,
5456
6283
  locale: useLiveLocale(client.locale),
5457
6284
  modelCatalog,
5458
- subscribeCatalog
5459
- }))));
6285
+ subscribeCatalog,
6286
+ subscribeSettings
6287
+ })));
5460
6288
  stops.push(client.slots.inject("shell.overlay", () => client.slots.register({
5461
6289
  name: "shell.overlay",
5462
6290
  id: DELETE_DIALOG_ID,
@@ -5479,6 +6307,7 @@ function apply(ctx) {
5479
6307
  })
5480
6308
  ] });
5481
6309
  })));
6310
+ stops.push(registerAuxiliaryModelSettings(ctx));
5482
6311
  const findCurrent = () => Object.values(client.sessions?.list.getSnapshot().byId ?? {}).find((row) => (row?.retainedBy?.mainView ?? 0) > 0)?.id ?? "";
5483
6312
  let current = findCurrent();
5484
6313
  const onReturn = () => {
@@ -5573,10 +6402,49 @@ function DeleteMenuItem(props) {
5573
6402
  children: uiText(locale, "deleteChat")
5574
6403
  });
5575
6404
  }
6405
+ /**
6406
+ * Session Management item: a named native details group. The body mounts on
6407
+ * the first expand only — the settings page must not fire every plugin's
6408
+ * config and model reads — and stays mounted afterwards so collapsing never
6409
+ * drops an unsaved draft or the settings-change subscription.
6410
+ */
6411
+ function SessionManagerSettingsItem(props) {
6412
+ const [visited, setVisited] = (0, react.useState)(false);
6413
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("details", {
6414
+ className: `${FREE_CHAT_ROOT} dsm-item`,
6415
+ "data-testid": "session-manager-settings-item",
6416
+ onToggle: (event) => {
6417
+ if (event.currentTarget.open) setVisited(true);
6418
+ },
6419
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("summary", {
6420
+ className: "dsm-item-summary",
6421
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6422
+ className: "dsm-item-title",
6423
+ children: uiText(props.locale, "settingsGroupTitle")
6424
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6425
+ className: "dsm-item-desc",
6426
+ children: uiText(props.locale, "settingsGroupHint")
6427
+ })] }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutlineRegular, {
6428
+ className: "dsm-item-chevron",
6429
+ "aria-hidden": "true"
6430
+ })]
6431
+ }), visited ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
6432
+ className: "dsm-item-body",
6433
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SessionManagerSettings, {
6434
+ model: props.model,
6435
+ locale: props.locale,
6436
+ modelCatalog: props.modelCatalog,
6437
+ subscribeCatalog: props.subscribeCatalog,
6438
+ subscribeSettings: props.subscribeSettings
6439
+ })
6440
+ }) : null]
6441
+ });
6442
+ }
5576
6443
  function useLiveLocale(locale) {
5577
6444
  return copyLocale((0, react.useSyncExternalStore)((listener) => locale?.subscribe(listener) ?? (() => {}), () => locale?.getSnapshot().active ?? "en", () => locale?.getSnapshot().active ?? "en"));
5578
6445
  }
5579
6446
  //#endregion
6447
+ exports.SessionManagerSettingsItem = SessionManagerSettingsItem;
5580
6448
  exports.apply = apply;
5581
6449
  exports.inject = inject;
5582
6450
  exports.name = name;