@klarkxy/dsh-current-title 0.1.7 → 0.2.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.
@@ -3,6 +3,30 @@ let react = require("react");
3
3
  let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
4
4
  let react_jsx_runtime = require("react/jsx-runtime");
5
5
  //#region ../dsh-plugin-kit/src/client-utils.ts
6
+ /** Fixed pause for failing background reads; manual work is never replayed. */
7
+ function createPluginReadGate(now = Date.now) {
8
+ let pausedUntil = 0;
9
+ let epoch = 0;
10
+ return {
11
+ canRead: () => now() >= pausedUntil,
12
+ reset: () => {
13
+ epoch++;
14
+ pausedUntil = 0;
15
+ },
16
+ async run(read) {
17
+ const current = epoch;
18
+ try {
19
+ const value = await read();
20
+ if (current === epoch) pausedUntil = 0;
21
+ return value;
22
+ } catch (error) {
23
+ const message = error instanceof Error ? error.message : "";
24
+ if (current === epoch && (/(?:^|\s)transport failure for .+: HTTP (?:404|405|503)\b/.test(message) || error instanceof TypeError && /^(?:Failed to fetch|fetch failed|NetworkError when attempting to fetch resource\.?)$/i.test(message))) pausedUntil = now() + 3e4;
25
+ throw error;
26
+ }
27
+ }
28
+ };
29
+ }
6
30
  /** The Editor retains its manuscript session separately; native DSH exposes the main-view binding. */
7
31
  function selectedSessionId(client) {
8
32
  const workspace = client.uiWorkspace?.current;
@@ -388,7 +412,7 @@ ${scope} {
388
412
  border: 0.5px solid var(--dsw-alias-border-l3);
389
413
  border-radius: var(--dsw-radius-md);
390
414
  background: var(--dsw-alias-bg-layer-1);
391
- font-family: var(--ds-font-family-code, ui-monospace, 'Cascadia Mono', Consolas, monospace);
415
+ font-family: var(--ds-font-family-code);
392
416
  font-size: 12px;
393
417
  line-height: 20px;
394
418
  color: var(--dsw-alias-label-secondary);
@@ -464,6 +488,162 @@ ${scope} {
464
488
  `.replace(/^& (\.dsh-ui-[a-z0-9-]+)(?![+>~])/gm, (_match, className) => `&${className}, & ${className}`);
465
489
  }
466
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
467
647
  //#region src/contracts.ts
468
648
  const RPC_CHANNEL = "/dsh-current-title";
469
649
  const TITLE_CLIENT_SERVICE = "dshCurrentTitleClient";
@@ -482,14 +662,16 @@ const titleCopy = {
482
662
  loading: "正在读取标题设置…",
483
663
  model: "标题模型",
484
664
  modelDefault: "跟随会话模型",
485
- effort: "思考强度",
486
- effortDefault: "默认",
487
665
  cadence: "更新节奏",
488
666
  cadenceAll: "跟随当前任务(每条消息)",
489
667
  cadenceFirst: "仅首条消息",
490
668
  saved: "已保存。",
491
669
  failed: "无法读取标题设置。",
492
- inactive: "标题服务未接管,新消息暂时不会自动更新。"
670
+ inactive: "标题服务未接管,新消息暂时不会自动更新。",
671
+ retry: "重试",
672
+ externalChange: "设置已在别处修改。提示词草稿已保留,请重新加载后确认。",
673
+ syncUnavailable: "自动刷新不可用,请手动重新加载设置。",
674
+ reload: "重新加载(丢弃草稿)"
493
675
  },
494
676
  en: {
495
677
  label: "Auto Title",
@@ -503,16 +685,38 @@ const titleCopy = {
503
685
  loading: "Loading title settings…",
504
686
  model: "Title model",
505
687
  modelDefault: "Use the session model",
506
- effort: "Reasoning effort",
507
- effortDefault: "Default",
508
688
  cadence: "Update cadence",
509
689
  cadenceAll: "Track the current task (every message)",
510
690
  cadenceFirst: "First message only",
511
691
  saved: "Saved.",
512
692
  failed: "Could not read title settings.",
513
- 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)"
514
698
  }
515
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
+ }
516
720
  /** `undefined` draft = not edited; an empty string is a real edit (use built-in instruction). */
517
721
  function promptFieldValue(draft, saved) {
518
722
  return draft ?? saved;
@@ -661,28 +865,17 @@ function catalogChoices(value, selected) {
661
865
  const id = typeof item.id === "string" ? item.id : "";
662
866
  if (!id) continue;
663
867
  const name = typeof item.name === "string" && item.name ? item.name : id;
664
- const reasoning = item.reasoning && typeof item.reasoning === "object" ? item.reasoning : void 0;
665
- const efforts = Array.isArray(reasoning?.efforts) ? reasoning.efforts.flatMap((effort) => {
666
- if (!effort || typeof effort !== "object") return [];
667
- const entry = effort;
668
- return typeof entry.id === "string" && entry.id ? [{
669
- id: entry.id,
670
- name: typeof entry.name === "string" && entry.name ? entry.name : entry.id
671
- }] : [];
672
- }) : [];
673
868
  choices.push({
674
869
  provider,
675
870
  model: id,
676
- label: `${providerName} / ${name}`,
677
- efforts
871
+ label: `${providerName} / ${name}`
678
872
  });
679
873
  }
680
874
  }
681
875
  if (selected.provider && selected.model && !choices.some((item) => item.provider === selected.provider && item.model === selected.model)) choices.push({
682
876
  provider: selected.provider,
683
877
  model: selected.model,
684
- label: `${selected.provider} / ${selected.model}`,
685
- efforts: []
878
+ label: `${selected.provider} / ${selected.model}`
686
879
  });
687
880
  return choices;
688
881
  }
@@ -690,24 +883,55 @@ function TitleSettings(props) {
690
883
  const seat = useNativeSeat(props.client);
691
884
  const sessionId = seat.sessionId;
692
885
  const text = titleCopy[seat.locale];
886
+ const messages = titleCopy.en;
693
887
  const [status, setStatus] = (0, react.useState)();
694
888
  const [busy, setBusy] = (0, react.useState)(false);
695
889
  const [note, setNote] = (0, react.useState)("");
696
890
  const [error, setError] = (0, react.useState)("");
697
891
  const [prompt, setPrompt] = (0, react.useState)(void 0);
698
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)(() => {});
699
901
  const workRef = (0, react.useRef)(void 0);
700
902
  if (!workRef.current) workRef.current = createTitleClientWork();
701
903
  const work = workRef.current;
904
+ const readGateRef = (0, react.useRef)(void 0);
905
+ if (!readGateRef.current) readGateRef.current = createPluginReadGate();
906
+ const readGate = readGateRef.current;
702
907
  const busyRef = (0, react.useRef)(busy);
703
908
  busyRef.current = busy;
909
+ const changeBusy = (value) => {
910
+ busyRef.current = value;
911
+ setBusy(value);
912
+ if (!value && pending.current) syncRef.current();
913
+ };
704
914
  const call = (endpoint, payload = {}) => props.client.connection.rpc.call(RPC_CHANNEL, endpoint, payload);
915
+ const readCall = (endpoint, payload) => readGate.run(() => call(endpoint, payload));
705
916
  function commitMarker(next) {
706
917
  applyMarkerFromStatus(props.marker, next, true);
707
918
  }
708
919
  (0, react.useEffect)(() => () => {
920
+ readGate.reset();
709
921
  work.dispose();
710
- }, [work]);
922
+ }, [work, readGate]);
923
+ (0, react.useEffect)(() => props.client.connection.generation?.subscribe(() => {
924
+ readGate.reset();
925
+ work.beginLoad();
926
+ reading.current = false;
927
+ setLoadingSettings(false);
928
+ busyRef.current = false;
929
+ setBusy(false);
930
+ }), [
931
+ props.client,
932
+ work,
933
+ readGate
934
+ ]);
711
935
  (0, react.useEffect)(() => {
712
936
  const catalog = props.client.remote?.session?.modelCatalog;
713
937
  if (!catalog) return void 0;
@@ -730,18 +954,34 @@ function TitleSettings(props) {
730
954
  ]);
731
955
  (0, react.useEffect)(() => {
732
956
  const generation = work.beginLoad();
957
+ reading.current = true;
958
+ pending.current = false;
959
+ setLoadingSettings(true);
960
+ readGate.reset();
733
961
  const isCurrent = () => work.isLoad(generation);
734
962
  setBusy(false);
735
963
  setError("");
736
964
  setNote("");
737
965
  runTitleStatusLoad({
738
- call,
966
+ call: readCall,
739
967
  sessionId: sessionId || void 0,
740
968
  isCurrent,
741
- failedMessage: text.failed,
742
- onStatus: setStatus,
743
- 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
+ },
744
979
  onMarker: commitMarker
980
+ }).finally(() => {
981
+ if (!work.isLoad(generation)) return;
982
+ reading.current = false;
983
+ setLoadingSettings(false);
984
+ if (pending.current) syncRef.current();
745
985
  });
746
986
  return () => {
747
987
  if (work.isLoad(generation)) work.beginLoad();
@@ -750,53 +990,96 @@ function TitleSettings(props) {
750
990
  props.client,
751
991
  props.marker,
752
992
  sessionId,
753
- text.failed,
754
993
  work
755
994
  ]);
756
995
  const refreshStatus = (0, react.useCallback)(() => {
757
- if (shouldSkipTitleRefresh({ busy: busyRef.current }) || work.disposed) return;
758
- const generation = work.captureLoad();
996
+ if (!readGate.canRead() || shouldSkipTitleRefresh({ busy: busyRef.current }) || work.disposed) return;
997
+ if (reading.current) return;
998
+ reading.current = true;
999
+ setLoadingSettings(true);
1000
+ readGate.reset();
1001
+ const token = work.beginRequest();
759
1002
  runTitleStatusLoad({
760
- call,
1003
+ call: readCall,
761
1004
  sessionId: sessionId || void 0,
762
- isCurrent: () => work.isLoad(generation) && !shouldSkipTitleRefresh({ busy: busyRef.current }),
763
- failedMessage: text.failed,
1005
+ isCurrent: () => work.isRequest(token) && !shouldSkipTitleRefresh({ busy: busyRef.current }),
1006
+ failedMessage: messages.failed,
764
1007
  onStatus: (next) => {
765
- if (shouldSkipTitleRefresh({ busy: busyRef.current })) return;
766
- 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);
1013
+ setError("");
1014
+ },
1015
+ onError: (message) => {
1016
+ if (!pending.current) setError(message);
767
1017
  },
768
- onError: () => {},
769
1018
  onMarker: (next) => {
770
- if (shouldSkipTitleRefresh({ busy: busyRef.current })) return;
1019
+ if (!next || shouldSkipTitleRefresh({ busy: busyRef.current })) return;
771
1020
  commitMarker(next);
772
1021
  }
1022
+ }).finally(() => {
1023
+ if (!work.isRequest(token)) return;
1024
+ reading.current = false;
1025
+ setLoadingSettings(false);
1026
+ if (pending.current) syncRef.current();
773
1027
  });
774
1028
  }, [
775
1029
  props.client,
776
1030
  props.marker,
777
1031
  sessionId,
778
- text.failed,
779
1032
  work
780
1033
  ]);
781
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]);
782
1069
  const savedPrompt = status?.settings.prompt ?? "";
783
1070
  const savedModel = status?.settings.model ?? {
784
1071
  provider: "",
785
1072
  model: ""
786
1073
  };
1074
+ const selectableChoices = choices.some((choice) => choice.provider === savedModel.provider && choice.model === savedModel.model) ? choices : [...choices, ...catalogChoices({}, savedModel)];
787
1075
  const modelKey = savedModel.provider && savedModel.model ? `${savedModel.provider}\u001f${savedModel.model}` : "";
788
1076
  const title = status?.session?.title;
789
1077
  const pinned = status?.session?.pinned === true;
790
1078
  const weOwn = status?.support.weOwn === true;
791
- const canRegenerate = Boolean(sessionId) && weOwn && !busy;
792
- const selectedChoice = choices.find((item) => item.provider === savedModel.provider && item.model === savedModel.model);
793
- const efforts = selectedChoice ? [...selectedChoice.efforts] : [];
794
- if (selectedChoice && savedModel.reasoningEffort && !efforts.some((item) => item.id === savedModel.reasoningEffort)) efforts.push({
795
- id: savedModel.reasoningEffort,
796
- name: savedModel.reasoningEffort
797
- });
1079
+ const canRegenerate = Boolean(sessionId) && weOwn && !busy && !loadingSettings;
798
1080
  function saveSettings(patch) {
799
- if (!status) return;
1081
+ if (!status || busyRef.current || reading.current || external.current) return;
1082
+ readGate.reset();
800
1083
  const token = work.beginRequest();
801
1084
  setError("");
802
1085
  setNote("");
@@ -805,9 +1088,9 @@ function TitleSettings(props) {
805
1088
  ...patch,
806
1089
  expectedRevision: status.settings.revision,
807
1090
  isCurrent: () => work.isRequest(token),
808
- savedMessage: text.saved,
809
- failedMessage: text.failed,
810
- onBusy: setBusy,
1091
+ savedMessage: messages.saved,
1092
+ failedMessage: messages.failed,
1093
+ onBusy: changeBusy,
811
1094
  onSettings: (settings) => setStatus((current) => current ? {
812
1095
  ...current,
813
1096
  settings
@@ -818,6 +1101,7 @@ function TitleSettings(props) {
818
1101
  }
819
1102
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
820
1103
  className: "current-title-settings ct-root dsh-ui-panel",
1104
+ lang: seat.locale,
821
1105
  "data-testid": "current-title-settings",
822
1106
  children: [
823
1107
  !status && !error ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
@@ -842,26 +1126,59 @@ function TitleSettings(props) {
842
1126
  status && !weOwn ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
843
1127
  role: "status",
844
1128
  className: "dsh-ui-hint",
845
- children: [text.inactive, status.support.limitation ? ` ${status.support.limitation}` : ""]
1129
+ children: [text.inactive, status.support.limitation ? ` ${titleMessage(status.support.limitation, seat.locale)}` : ""]
846
1130
  }) : null,
847
- error ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1131
+ error ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
848
1132
  role: "alert",
849
1133
  className: "dsh-ui-error dsh-ui-wrap",
850
- children: error
851
- }) : null,
1134
+ children: titleMessage(error, seat.locale)
1135
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
1136
+ variant: "outline",
1137
+ size: "sm",
1138
+ disabled: busy,
1139
+ onClick: () => {
1140
+ readGate.reset();
1141
+ refreshStatus();
1142
+ },
1143
+ children: text.retry
1144
+ })] }) : null,
852
1145
  note ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
853
1146
  role: "status",
854
1147
  className: "dsh-ui-notice",
855
- 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
856
1159
  }) : null,
857
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1160
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
858
1161
  className: "dsh-ui-actions",
859
- 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, {
860
1176
  variant: "outline",
861
1177
  size: "md",
862
1178
  disabled: !canRegenerate,
863
1179
  onClick: () => {
864
1180
  if (!sessionId) return;
1181
+ readGate.reset();
865
1182
  const token = work.beginRequest();
866
1183
  setError("");
867
1184
  setNote("");
@@ -869,15 +1186,15 @@ function TitleSettings(props) {
869
1186
  call,
870
1187
  sessionId,
871
1188
  isCurrent: () => work.isRequest(token),
872
- failedMessage: text.failed,
873
- onBusy: setBusy,
1189
+ failedMessage: messages.failed,
1190
+ onBusy: changeBusy,
874
1191
  onStatus: setStatus,
875
1192
  onError: setError,
876
1193
  onMarker: commitMarker
877
1194
  });
878
1195
  },
879
1196
  children: text.regenerate
880
- }) : null
1197
+ }) : null]
881
1198
  }),
882
1199
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
883
1200
  className: "dsh-ui-field",
@@ -891,18 +1208,21 @@ function TitleSettings(props) {
891
1208
  value: status ? promptFieldValue(prompt, savedPrompt) : "",
892
1209
  maxLength: 4e3,
893
1210
  rows: 5,
894
- disabled: busy || !status,
1211
+ disabled: busy || loadingSettings || !status,
895
1212
  onChange: (event) => {
1213
+ dirty.current = true;
896
1214
  setPrompt(event.target.value);
897
1215
  setNote("");
898
1216
  },
899
1217
  onBlur: () => {
900
- if (!status) return;
1218
+ if (!status || busyRef.current || reading.current || external.current) return;
901
1219
  const next = promptToSave(prompt, savedPrompt);
902
1220
  if (next === void 0) {
1221
+ dirty.current = false;
903
1222
  setPrompt(void 0);
904
1223
  return;
905
1224
  }
1225
+ readGate.reset();
906
1226
  const token = work.beginRequest();
907
1227
  setError("");
908
1228
  runTitleSettingsSave({
@@ -910,14 +1230,15 @@ function TitleSettings(props) {
910
1230
  prompt: next,
911
1231
  expectedRevision: status.settings.revision,
912
1232
  isCurrent: () => work.isRequest(token),
913
- savedMessage: text.saved,
914
- failedMessage: text.failed,
915
- onBusy: setBusy,
1233
+ savedMessage: messages.saved,
1234
+ failedMessage: messages.failed,
1235
+ onBusy: changeBusy,
916
1236
  onSettings: (settings) => {
917
1237
  setStatus((current) => current ? {
918
1238
  ...current,
919
1239
  settings
920
1240
  } : current);
1241
+ dirty.current = false;
921
1242
  setPrompt(void 0);
922
1243
  },
923
1244
  onNote: setNote,
@@ -939,10 +1260,10 @@ function TitleSettings(props) {
939
1260
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
940
1261
  className: "dsh-ui-select",
941
1262
  value: modelKey,
942
- disabled: busy || !status,
1263
+ disabled: busy || loadingSettings || !status,
943
1264
  onChange: (event) => {
944
1265
  const value = event.target.value;
945
- const choice = choices.find((item) => `${item.provider}\u001f${item.model}` === value);
1266
+ const choice = selectableChoices.find((item) => `${item.provider}\u001f${item.model}` === value);
946
1267
  saveSettings({ model: choice ? {
947
1268
  provider: choice.provider,
948
1269
  model: choice.model
@@ -954,41 +1275,12 @@ function TitleSettings(props) {
954
1275
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
955
1276
  value: "",
956
1277
  children: text.modelDefault
957
- }), choices.map((choice) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1278
+ }), selectableChoices.map((choice) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
958
1279
  value: `${choice.provider}\u001f${choice.model}`,
959
1280
  children: choice.label
960
1281
  }, `${choice.provider}\u001f${choice.model}`))]
961
1282
  })]
962
1283
  }),
963
- selectedChoice && efforts.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
964
- className: "dsh-ui-field",
965
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
966
- className: "dsh-ui-label",
967
- children: text.effort
968
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
969
- className: "dsh-ui-select",
970
- value: savedModel.reasoningEffort ?? "",
971
- disabled: busy || !status,
972
- onChange: (event) => {
973
- const effort = event.target.value;
974
- const model = {
975
- provider: savedModel.provider,
976
- model: savedModel.model
977
- };
978
- saveSettings({ model: effort ? {
979
- ...model,
980
- reasoningEffort: effort
981
- } : model });
982
- },
983
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
984
- value: "",
985
- children: text.effortDefault
986
- }), efforts.map((effort) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
987
- value: effort.id,
988
- children: effort.name
989
- }, effort.id))]
990
- })]
991
- }) : null,
992
1284
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
993
1285
  className: "dsh-ui-field",
994
1286
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
@@ -997,7 +1289,7 @@ function TitleSettings(props) {
997
1289
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
998
1290
  className: "dsh-ui-select",
999
1291
  value: status?.settings.cadence ?? "all-prompts",
1000
- disabled: busy || !status,
1292
+ disabled: busy || loadingSettings || !status,
1001
1293
  onChange: (event) => {
1002
1294
  saveSettings({ cadence: event.target.value === "first-prompt" ? "first-prompt" : "all-prompts" });
1003
1295
  },
@@ -1035,6 +1327,8 @@ const styles = `${officialUiCss("ct-root")}
1035
1327
  `;
1036
1328
  function apply(ctx) {
1037
1329
  const client = ctx;
1330
+ const settingsSubscriber = createModelSettingsSubscriber(ctx, "@klarkxy/dsh-current-title");
1331
+ const subscribeSettings = (change, error) => settingsSubscriber.subscribe(change, error);
1038
1332
  const marker = createTitleClientMarker();
1039
1333
  ctx.provide(TITLE_CLIENT_SERVICE, marker);
1040
1334
  ctx.effect(() => {
@@ -1050,7 +1344,8 @@ function apply(ctx) {
1050
1344
  key: "@klarkxy/dsh-current-title"
1051
1345
  }, () => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TitleSettings, {
1052
1346
  client,
1053
- marker
1347
+ marker,
1348
+ subscribeSettings
1054
1349
  }))), "current-title.settings");
1055
1350
  ctx.effect(() => {
1056
1351
  let live = true;