@klarkxy/dsh-current-title 0.2.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,4 +1,6 @@
1
- # @klarkxy/dsh-current-title
1
+ # @klarkxy/dsh-current-title
2
+
3
+ <!-- release-state-note -->
2
4
 
3
5
  Keeps a DSH session title on the task you are working on right now, and leaves a name you set by hand alone.
4
6
 
@@ -10,7 +12,7 @@ The bundle insert is enabled after installation. It does not permanently turn of
10
12
 
11
13
  ## Install
12
14
 
13
- Requires Node.js ≥22 and DSH `0.1.7-rc.2`. The **Auto Title** entry starts enabled and can be disabled in plugin settings. Native title storage and scheduling come from the host `sessionTitle`; generation uses the host `llm` service. Install the published package into the target profile:
15
+ Requires Node.js ≥22 (26 verified) and DSH `0.1.7-rc.2` or later. The **Auto Title** entry starts enabled and can be disabled in plugin settings. Native title storage and scheduling come from the host `sessionTitle`; generation uses the host `llm` service. Install the published package into the target profile:
14
16
 
15
17
  ```sh
16
18
  dsh plugin --profile web add @klarkxy/dsh-current-title
@@ -1,4 +1,6 @@
1
- # @klarkxy/dsh-current-title
1
+ # @klarkxy/dsh-current-title
2
+
3
+ <!-- release-state-note -->
2
4
 
3
5
  让会话标题跟着你手头的任务走;你手动改过的名字不会被覆盖。
4
6
 
@@ -10,7 +12,7 @@
10
12
 
11
13
  ## 安装
12
14
 
13
- 需要 Node.js ≥22 与 DSH `0.1.7-rc.2`。「自动标题」默认启用,可在插件设置中关闭。标题写入宿主 `sessionTitle`,由宿主调度;生成使用宿主 `llm` 服务。把已发布包装进目标 profile:
15
+ 需要 Node.js ≥22(已验证 26)与 DSH `0.1.7-rc.2` 或更高版本。「自动标题」默认启用,可在插件设置中关闭。标题写入宿主 `sessionTitle`,由宿主调度;生成使用宿主 `llm` 服务。把已发布包装进目标 profile:
14
16
 
15
17
  ```sh
16
18
  dsh plugin --profile web add @klarkxy/dsh-current-title
@@ -412,7 +412,7 @@ ${scope} {
412
412
  border: 0.5px solid var(--dsw-alias-border-l3);
413
413
  border-radius: var(--dsw-radius-md);
414
414
  background: var(--dsw-alias-bg-layer-1);
415
- font-family: var(--ds-font-family-code, ui-monospace, 'Cascadia Mono', Consolas, monospace);
415
+ font-family: var(--ds-font-family-code);
416
416
  font-size: 12px;
417
417
  line-height: 20px;
418
418
  color: var(--dsw-alias-label-secondary);
@@ -488,6 +488,162 @@ ${scope} {
488
488
  `.replace(/^& (\.dsh-ui-[a-z0-9-]+)(?![+>~])/gm, (_match, className) => `&${className}, & ${className}`);
489
489
  }
490
490
  //#endregion
491
+ //#region ../dsh-plugin-kit/src/contracts.ts
492
+ /**
493
+ * Remote contribution for model-settings change notifications.
494
+ * Provided by `@klarkxy/dsh-model-hub` (`ModelSettingsChangesController`);
495
+ * subscribers degrade silently when that package is not installed.
496
+ * The stream carries invalidations only — `ready` is never a settings snapshot.
497
+ */
498
+ const MODEL_SETTINGS_CHANGES_REMOTE = {
499
+ package: "@klarkxy/dsh-model-hub",
500
+ descriptors: [{
501
+ id: "modelSettingsChanges/watch",
502
+ service: "modelSettingsChangesController",
503
+ namespace: "modelSettingsChanges",
504
+ method: "watch",
505
+ mode: "stream",
506
+ invocation: { kind: "direct" },
507
+ parameters: [],
508
+ cancellation: { parameter: "signal" }
509
+ }]
510
+ };
511
+ //#endregion
512
+ //#region ../dsh-plugin-kit/src/model-settings-client.ts
513
+ /** This stream carries invalidations only; ready is never a settings snapshot. */
514
+ function parseModelSettingsChangeFrame(value) {
515
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("Invalid model settings change frame");
516
+ const frame = value;
517
+ if (frame.kind === "ready" && Object.keys(frame).length === 1) return { kind: "ready" };
518
+ if (frame.kind === "changed" && Object.keys(frame).length === 3 && typeof frame.ns === "string" && frame.ns.length > 0 && typeof frame.revision === "number" && Number.isSafeInteger(frame.revision) && frame.revision >= 0) return {
519
+ kind: "changed",
520
+ ns: frame.ns,
521
+ revision: frame.revision
522
+ };
523
+ throw new Error("Invalid model settings change frame");
524
+ }
525
+ MODEL_SETTINGS_CHANGES_REMOTE.package, MODEL_SETTINGS_CHANGES_REMOTE.descriptors.map((descriptor) => ({
526
+ ...descriptor,
527
+ result: {
528
+ mode: "strict",
529
+ typeSymbol: "ModelSettings.ChangeFrame",
530
+ create: () => ({ parse: parseModelSettingsChangeFrame })
531
+ }
532
+ }));
533
+ /** Native RemoteStream owns carrier reconnects; no timers or browser transport.
534
+ * A factory shares one logical stream while at least one subscriber remains. */
535
+ function createModelSettingsSubscriber(ctx, namespace) {
536
+ const listeners = /* @__PURE__ */ new Set();
537
+ let injection;
538
+ let current;
539
+ let lifecycle = 0;
540
+ let failureMessage;
541
+ const report = (message) => {
542
+ failureMessage = message;
543
+ for (const listener of [...listeners]) try {
544
+ listener.error?.(message);
545
+ } catch {}
546
+ };
547
+ const invalidate = () => {
548
+ for (const listener of [...listeners]) {
549
+ if (!listeners.has(listener)) continue;
550
+ try {
551
+ listener.change();
552
+ } catch (error) {
553
+ try {
554
+ listener.error?.(error instanceof Error ? error.message : String(error));
555
+ } catch {}
556
+ }
557
+ }
558
+ };
559
+ const start = () => {
560
+ const life = ++lifecycle;
561
+ injection = ctx.inject(["remote", "remote.modelSettingsChanges"], (injected) => {
562
+ if (life !== lifecycle || !listeners.size) return;
563
+ const scope = injected;
564
+ const stream = scope.remote.$stream({
565
+ name: "Model settings sync",
566
+ open: (signal) => scope.remote.modelSettingsChanges.watch(signal),
567
+ ended: (accepted) => /* @__PURE__ */ new Error(`Model settings sync stream ended ${accepted ? "after" : "before"} ready; refresh or reopen the page`)
568
+ });
569
+ const session = {
570
+ stream,
571
+ active: true
572
+ };
573
+ current = session;
574
+ scope.effect(() => () => {
575
+ session.active = false;
576
+ if (current === session) {
577
+ current = void 0;
578
+ if (life === lifecycle && listeners.size) report("Model settings sync service unavailable");
579
+ }
580
+ return stream.dispose();
581
+ });
582
+ (async () => {
583
+ let generation;
584
+ let ready = false;
585
+ try {
586
+ for await (const item of stream) {
587
+ if (!session.active || life !== lifecycle || !listeners.size) break;
588
+ if (item.signal.aborted) continue;
589
+ if (generation !== item.generation) {
590
+ generation = item.generation;
591
+ ready = false;
592
+ }
593
+ const frame = parseModelSettingsChangeFrame(item.value);
594
+ if (frame.kind === "ready") {
595
+ ready = true;
596
+ failureMessage = void 0;
597
+ item.accept();
598
+ invalidate();
599
+ } else {
600
+ if (!ready) throw new Error("Model settings sync change arrived before ready");
601
+ if (namespace === "*" || frame.ns === namespace) invalidate();
602
+ }
603
+ }
604
+ } catch (error) {
605
+ if (session.active && life === lifecycle) report(error instanceof Error ? error.message : String(error));
606
+ } finally {
607
+ if (current === session) current = void 0;
608
+ session.active = false;
609
+ await stream.dispose().catch(() => {});
610
+ }
611
+ })();
612
+ });
613
+ queueMicrotask(() => {
614
+ if (life === lifecycle && listeners.size && !current) report("Model settings sync service unavailable");
615
+ });
616
+ };
617
+ return Object.freeze({ subscribe(onChange, onError) {
618
+ const listener = {
619
+ change: onChange,
620
+ error: onError
621
+ };
622
+ listeners.add(listener);
623
+ if (!injection) start();
624
+ else if (failureMessage) queueMicrotask(() => {
625
+ if (listeners.has(listener) && failureMessage) try {
626
+ listener.error?.(failureMessage);
627
+ } catch {}
628
+ });
629
+ return () => {
630
+ if (!listeners.delete(listener) || listeners.size) return;
631
+ lifecycle++;
632
+ if (current) {
633
+ current.active = false;
634
+ current.stream.dispose().catch(() => {});
635
+ }
636
+ current = void 0;
637
+ failureMessage = void 0;
638
+ const previous = injection;
639
+ injection = void 0;
640
+ try {
641
+ Promise.resolve(previous?.dispose()).catch(() => {});
642
+ } catch {}
643
+ };
644
+ } });
645
+ }
646
+ //#endregion
491
647
  //#region src/contracts.ts
492
648
  const RPC_CHANNEL = "/dsh-current-title";
493
649
  const TITLE_CLIENT_SERVICE = "dshCurrentTitleClient";
@@ -511,7 +667,11 @@ const titleCopy = {
511
667
  cadenceFirst: "仅首条消息",
512
668
  saved: "已保存。",
513
669
  failed: "无法读取标题设置。",
514
- inactive: "标题服务未接管,新消息暂时不会自动更新。"
670
+ inactive: "标题服务未接管,新消息暂时不会自动更新。",
671
+ retry: "重试",
672
+ externalChange: "设置已在别处修改。提示词草稿已保留,请重新加载后确认。",
673
+ syncUnavailable: "自动刷新不可用,请手动重新加载设置。",
674
+ reload: "重新加载(丢弃草稿)"
515
675
  },
516
676
  en: {
517
677
  label: "Auto Title",
@@ -530,9 +690,33 @@ const titleCopy = {
530
690
  cadenceFirst: "First message only",
531
691
  saved: "Saved.",
532
692
  failed: "Could not read title settings.",
533
- inactive: "Title generation is not active, so new messages will not rename this session."
693
+ inactive: "Title generation is not active, so new messages will not rename this session.",
694
+ retry: "Retry",
695
+ externalChange: "Settings changed elsewhere. Prompt draft preserved; reload and review.",
696
+ syncUnavailable: "Automatic refresh unavailable; reload settings manually.",
697
+ reload: "Reload (discard draft)"
534
698
  }
535
699
  };
700
+ /** Only plugin-owned defaults are translated; model output and remote diagnostics stay intact. */
701
+ const titleMessages = [
702
+ ["标题设置无效。", "Invalid title settings."],
703
+ ["设置已更新,请刷新后重试。", "Settings changed. Refresh and try again."],
704
+ ["无法保存标题设置。", "Could not save title settings."],
705
+ ["缺少会话。", "A session is required."],
706
+ ["找不到该会话。", "Session not found."],
707
+ ["标题服务未就绪。", "Title service is not ready."],
708
+ ["已取消。", "Cancelled."],
709
+ ["标题生成失败。", "Title generation failed."],
710
+ ["未知操作。", "Unknown operation."],
711
+ ["缺少所需的宿主服务。", "required host services are missing"],
712
+ ["原生标题服务位置已被占用。", "native title provider slot is occupied"],
713
+ ["其他标题服务已接管;先前的服务未恢复。", "another title provider owns the slot; previous owner was not restored"],
714
+ [titleCopy.zh.saved, titleCopy.en.saved],
715
+ [titleCopy.zh.failed, titleCopy.en.failed]
716
+ ];
717
+ function titleMessage(message, locale) {
718
+ return titleMessages.find((pair) => pair.includes(message))?.[locale === "zh" ? 0 : 1] ?? message;
719
+ }
536
720
  /** `undefined` draft = not edited; an empty string is a real edit (use built-in instruction). */
537
721
  function promptFieldValue(draft, saved) {
538
722
  return draft ?? saved;
@@ -699,12 +883,21 @@ function TitleSettings(props) {
699
883
  const seat = useNativeSeat(props.client);
700
884
  const sessionId = seat.sessionId;
701
885
  const text = titleCopy[seat.locale];
886
+ const messages = titleCopy.en;
702
887
  const [status, setStatus] = (0, react.useState)();
703
888
  const [busy, setBusy] = (0, react.useState)(false);
704
889
  const [note, setNote] = (0, react.useState)("");
705
890
  const [error, setError] = (0, react.useState)("");
706
891
  const [prompt, setPrompt] = (0, react.useState)(void 0);
707
892
  const [choices, setChoices] = (0, react.useState)([]);
893
+ const [externalChange, setExternalChange] = (0, react.useState)(false);
894
+ const [syncError, setSyncError] = (0, react.useState)("");
895
+ const [loadingSettings, setLoadingSettings] = (0, react.useState)(true);
896
+ const dirty = (0, react.useRef)(false);
897
+ const external = (0, react.useRef)(false);
898
+ const reading = (0, react.useRef)(false);
899
+ const pending = (0, react.useRef)(false);
900
+ const syncRef = (0, react.useRef)(() => {});
708
901
  const workRef = (0, react.useRef)(void 0);
709
902
  if (!workRef.current) workRef.current = createTitleClientWork();
710
903
  const work = workRef.current;
@@ -713,6 +906,11 @@ function TitleSettings(props) {
713
906
  const readGate = readGateRef.current;
714
907
  const busyRef = (0, react.useRef)(busy);
715
908
  busyRef.current = busy;
909
+ const changeBusy = (value) => {
910
+ busyRef.current = value;
911
+ setBusy(value);
912
+ if (!value && pending.current) syncRef.current();
913
+ };
716
914
  const call = (endpoint, payload = {}) => props.client.connection.rpc.call(RPC_CHANNEL, endpoint, payload);
717
915
  const readCall = (endpoint, payload) => readGate.run(() => call(endpoint, payload));
718
916
  function commitMarker(next) {
@@ -725,6 +923,8 @@ function TitleSettings(props) {
725
923
  (0, react.useEffect)(() => props.client.connection.generation?.subscribe(() => {
726
924
  readGate.reset();
727
925
  work.beginLoad();
926
+ reading.current = false;
927
+ setLoadingSettings(false);
728
928
  busyRef.current = false;
729
929
  setBusy(false);
730
930
  }), [
@@ -754,6 +954,9 @@ function TitleSettings(props) {
754
954
  ]);
755
955
  (0, react.useEffect)(() => {
756
956
  const generation = work.beginLoad();
957
+ reading.current = true;
958
+ pending.current = false;
959
+ setLoadingSettings(true);
757
960
  readGate.reset();
758
961
  const isCurrent = () => work.isLoad(generation);
759
962
  setBusy(false);
@@ -763,10 +966,22 @@ function TitleSettings(props) {
763
966
  call: readCall,
764
967
  sessionId: sessionId || void 0,
765
968
  isCurrent,
766
- failedMessage: text.failed,
767
- onStatus: setStatus,
768
- onError: setError,
969
+ failedMessage: messages.failed,
970
+ onStatus: (next) => {
971
+ if (!pending.current) setStatus((current) => current && external.current ? {
972
+ ...next,
973
+ settings: current.settings
974
+ } : next);
975
+ },
976
+ onError: (message) => {
977
+ if (!pending.current) setError(message);
978
+ },
769
979
  onMarker: commitMarker
980
+ }).finally(() => {
981
+ if (!work.isLoad(generation)) return;
982
+ reading.current = false;
983
+ setLoadingSettings(false);
984
+ if (pending.current) syncRef.current();
770
985
  });
771
986
  return () => {
772
987
  if (work.isLoad(generation)) work.beginLoad();
@@ -775,49 +990,95 @@ function TitleSettings(props) {
775
990
  props.client,
776
991
  props.marker,
777
992
  sessionId,
778
- text.failed,
779
993
  work
780
994
  ]);
781
995
  const refreshStatus = (0, react.useCallback)(() => {
782
996
  if (!readGate.canRead() || shouldSkipTitleRefresh({ busy: busyRef.current }) || work.disposed) return;
997
+ if (reading.current) return;
998
+ reading.current = true;
999
+ setLoadingSettings(true);
783
1000
  readGate.reset();
784
1001
  const token = work.beginRequest();
785
1002
  runTitleStatusLoad({
786
1003
  call: readCall,
787
1004
  sessionId: sessionId || void 0,
788
1005
  isCurrent: () => work.isRequest(token) && !shouldSkipTitleRefresh({ busy: busyRef.current }),
789
- failedMessage: text.failed,
1006
+ failedMessage: messages.failed,
790
1007
  onStatus: (next) => {
791
- if (shouldSkipTitleRefresh({ busy: busyRef.current })) return;
792
- setStatus(next);
1008
+ if (pending.current || shouldSkipTitleRefresh({ busy: busyRef.current })) return;
1009
+ setStatus((current) => current && external.current ? {
1010
+ ...next,
1011
+ settings: current.settings
1012
+ } : next);
793
1013
  setError("");
794
1014
  },
795
- onError: setError,
1015
+ onError: (message) => {
1016
+ if (!pending.current) setError(message);
1017
+ },
796
1018
  onMarker: (next) => {
797
1019
  if (!next || shouldSkipTitleRefresh({ busy: busyRef.current })) return;
798
1020
  commitMarker(next);
799
1021
  }
1022
+ }).finally(() => {
1023
+ if (!work.isRequest(token)) return;
1024
+ reading.current = false;
1025
+ setLoadingSettings(false);
1026
+ if (pending.current) syncRef.current();
800
1027
  });
801
1028
  }, [
802
1029
  props.client,
803
1030
  props.marker,
804
1031
  sessionId,
805
- text.failed,
806
1032
  work
807
1033
  ]);
808
1034
  useFeatureRefresh(props.client, sessionId, refreshStatus, status?.session?.generating === true && !busy, Boolean(sessionId));
1035
+ syncRef.current = () => {
1036
+ if (work.disposed) return;
1037
+ if (reading.current || busyRef.current) {
1038
+ pending.current = true;
1039
+ return;
1040
+ }
1041
+ pending.current = false;
1042
+ if (dirty.current) {
1043
+ external.current = true;
1044
+ setExternalChange(true);
1045
+ return;
1046
+ }
1047
+ readGate.reset();
1048
+ refreshStatus();
1049
+ };
1050
+ (0, react.useEffect)(() => {
1051
+ setSyncError("");
1052
+ let live = true;
1053
+ const stop = props.subscribeSettings?.(() => {
1054
+ if (!live) return;
1055
+ setSyncError("");
1056
+ if (dirty.current && !busyRef.current) {
1057
+ external.current = true;
1058
+ setExternalChange(true);
1059
+ }
1060
+ syncRef.current();
1061
+ }, (message) => {
1062
+ if (live) setSyncError(message);
1063
+ });
1064
+ return () => {
1065
+ live = false;
1066
+ stop?.();
1067
+ };
1068
+ }, [props.subscribeSettings, props.client]);
809
1069
  const savedPrompt = status?.settings.prompt ?? "";
810
1070
  const savedModel = status?.settings.model ?? {
811
1071
  provider: "",
812
1072
  model: ""
813
1073
  };
1074
+ const selectableChoices = choices.some((choice) => choice.provider === savedModel.provider && choice.model === savedModel.model) ? choices : [...choices, ...catalogChoices({}, savedModel)];
814
1075
  const modelKey = savedModel.provider && savedModel.model ? `${savedModel.provider}\u001f${savedModel.model}` : "";
815
1076
  const title = status?.session?.title;
816
1077
  const pinned = status?.session?.pinned === true;
817
1078
  const weOwn = status?.support.weOwn === true;
818
- const canRegenerate = Boolean(sessionId) && weOwn && !busy;
1079
+ const canRegenerate = Boolean(sessionId) && weOwn && !busy && !loadingSettings;
819
1080
  function saveSettings(patch) {
820
- if (!status) return;
1081
+ if (!status || busyRef.current || reading.current || external.current) return;
821
1082
  readGate.reset();
822
1083
  const token = work.beginRequest();
823
1084
  setError("");
@@ -827,9 +1088,9 @@ function TitleSettings(props) {
827
1088
  ...patch,
828
1089
  expectedRevision: status.settings.revision,
829
1090
  isCurrent: () => work.isRequest(token),
830
- savedMessage: text.saved,
831
- failedMessage: text.failed,
832
- onBusy: setBusy,
1091
+ savedMessage: messages.saved,
1092
+ failedMessage: messages.failed,
1093
+ onBusy: changeBusy,
833
1094
  onSettings: (settings) => setStatus((current) => current ? {
834
1095
  ...current,
835
1096
  settings
@@ -840,6 +1101,7 @@ function TitleSettings(props) {
840
1101
  }
841
1102
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
842
1103
  className: "current-title-settings ct-root dsh-ui-panel",
1104
+ lang: seat.locale,
843
1105
  "data-testid": "current-title-settings",
844
1106
  children: [
845
1107
  !status && !error ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
@@ -864,12 +1126,12 @@ function TitleSettings(props) {
864
1126
  status && !weOwn ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
865
1127
  role: "status",
866
1128
  className: "dsh-ui-hint",
867
- children: [text.inactive, status.support.limitation ? ` ${status.support.limitation}` : ""]
1129
+ children: [text.inactive, status.support.limitation ? ` ${titleMessage(status.support.limitation, seat.locale)}` : ""]
868
1130
  }) : null,
869
1131
  error ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
870
1132
  role: "alert",
871
1133
  className: "dsh-ui-error dsh-ui-wrap",
872
- children: error
1134
+ children: titleMessage(error, seat.locale)
873
1135
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
874
1136
  variant: "outline",
875
1137
  size: "sm",
@@ -878,16 +1140,39 @@ function TitleSettings(props) {
878
1140
  readGate.reset();
879
1141
  refreshStatus();
880
1142
  },
881
- children: seat.locale === "zh" ? "重试" : "Retry"
1143
+ children: text.retry
882
1144
  })] }) : null,
883
1145
  note ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
884
1146
  role: "status",
885
1147
  className: "dsh-ui-notice",
886
- children: note
1148
+ children: titleMessage(note, seat.locale)
1149
+ }) : null,
1150
+ externalChange ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1151
+ role: "alert",
1152
+ className: "dsh-ui-error",
1153
+ children: text.externalChange
1154
+ }) : null,
1155
+ syncError ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1156
+ role: "status",
1157
+ className: "dsh-ui-hint",
1158
+ children: text.syncUnavailable
887
1159
  }) : null,
888
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1160
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
889
1161
  className: "dsh-ui-actions",
890
- children: sessionId ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
1162
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
1163
+ variant: "ghost",
1164
+ size: "sm",
1165
+ disabled: busy || loadingSettings,
1166
+ onClick: () => {
1167
+ setPrompt(void 0);
1168
+ dirty.current = false;
1169
+ external.current = false;
1170
+ setExternalChange(false);
1171
+ readGate.reset();
1172
+ syncRef.current();
1173
+ },
1174
+ children: text.reload
1175
+ }), sessionId ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
891
1176
  variant: "outline",
892
1177
  size: "md",
893
1178
  disabled: !canRegenerate,
@@ -901,15 +1186,15 @@ function TitleSettings(props) {
901
1186
  call,
902
1187
  sessionId,
903
1188
  isCurrent: () => work.isRequest(token),
904
- failedMessage: text.failed,
905
- onBusy: setBusy,
1189
+ failedMessage: messages.failed,
1190
+ onBusy: changeBusy,
906
1191
  onStatus: setStatus,
907
1192
  onError: setError,
908
1193
  onMarker: commitMarker
909
1194
  });
910
1195
  },
911
1196
  children: text.regenerate
912
- }) : null
1197
+ }) : null]
913
1198
  }),
914
1199
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
915
1200
  className: "dsh-ui-field",
@@ -923,15 +1208,17 @@ function TitleSettings(props) {
923
1208
  value: status ? promptFieldValue(prompt, savedPrompt) : "",
924
1209
  maxLength: 4e3,
925
1210
  rows: 5,
926
- disabled: busy || !status,
1211
+ disabled: busy || loadingSettings || !status,
927
1212
  onChange: (event) => {
1213
+ dirty.current = true;
928
1214
  setPrompt(event.target.value);
929
1215
  setNote("");
930
1216
  },
931
1217
  onBlur: () => {
932
- if (!status) return;
1218
+ if (!status || busyRef.current || reading.current || external.current) return;
933
1219
  const next = promptToSave(prompt, savedPrompt);
934
1220
  if (next === void 0) {
1221
+ dirty.current = false;
935
1222
  setPrompt(void 0);
936
1223
  return;
937
1224
  }
@@ -943,14 +1230,15 @@ function TitleSettings(props) {
943
1230
  prompt: next,
944
1231
  expectedRevision: status.settings.revision,
945
1232
  isCurrent: () => work.isRequest(token),
946
- savedMessage: text.saved,
947
- failedMessage: text.failed,
948
- onBusy: setBusy,
1233
+ savedMessage: messages.saved,
1234
+ failedMessage: messages.failed,
1235
+ onBusy: changeBusy,
949
1236
  onSettings: (settings) => {
950
1237
  setStatus((current) => current ? {
951
1238
  ...current,
952
1239
  settings
953
1240
  } : current);
1241
+ dirty.current = false;
954
1242
  setPrompt(void 0);
955
1243
  },
956
1244
  onNote: setNote,
@@ -972,10 +1260,10 @@ function TitleSettings(props) {
972
1260
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
973
1261
  className: "dsh-ui-select",
974
1262
  value: modelKey,
975
- disabled: busy || !status,
1263
+ disabled: busy || loadingSettings || !status,
976
1264
  onChange: (event) => {
977
1265
  const value = event.target.value;
978
- const choice = choices.find((item) => `${item.provider}\u001f${item.model}` === value);
1266
+ const choice = selectableChoices.find((item) => `${item.provider}\u001f${item.model}` === value);
979
1267
  saveSettings({ model: choice ? {
980
1268
  provider: choice.provider,
981
1269
  model: choice.model
@@ -987,7 +1275,7 @@ function TitleSettings(props) {
987
1275
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
988
1276
  value: "",
989
1277
  children: text.modelDefault
990
- }), choices.map((choice) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1278
+ }), selectableChoices.map((choice) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
991
1279
  value: `${choice.provider}\u001f${choice.model}`,
992
1280
  children: choice.label
993
1281
  }, `${choice.provider}\u001f${choice.model}`))]
@@ -1001,7 +1289,7 @@ function TitleSettings(props) {
1001
1289
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
1002
1290
  className: "dsh-ui-select",
1003
1291
  value: status?.settings.cadence ?? "all-prompts",
1004
- disabled: busy || !status,
1292
+ disabled: busy || loadingSettings || !status,
1005
1293
  onChange: (event) => {
1006
1294
  saveSettings({ cadence: event.target.value === "first-prompt" ? "first-prompt" : "all-prompts" });
1007
1295
  },
@@ -1039,6 +1327,8 @@ const styles = `${officialUiCss("ct-root")}
1039
1327
  `;
1040
1328
  function apply(ctx) {
1041
1329
  const client = ctx;
1330
+ const settingsSubscriber = createModelSettingsSubscriber(ctx, "@klarkxy/dsh-current-title");
1331
+ const subscribeSettings = (change, error) => settingsSubscriber.subscribe(change, error);
1042
1332
  const marker = createTitleClientMarker();
1043
1333
  ctx.provide(TITLE_CLIENT_SERVICE, marker);
1044
1334
  ctx.effect(() => {
@@ -1054,7 +1344,8 @@ function apply(ctx) {
1054
1344
  key: "@klarkxy/dsh-current-title"
1055
1345
  }, () => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TitleSettings, {
1056
1346
  client,
1057
- marker
1347
+ marker,
1348
+ subscribeSettings
1058
1349
  }))), "current-title.settings");
1059
1350
  ctx.effect(() => {
1060
1351
  let live = true;