@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.
package/lib/client.js CHANGED
@@ -1,168 +1,7 @@
1
- function __createDshNodeShims() {
2
- class EventEmitter {
3
- constructor() {
4
- this._e = Object.create(null)
5
- }
6
- on(type, fn) {
7
- (this._e[type] ||= []).push(fn)
8
- return this
9
- }
10
- addListener(type, fn) {
11
- return this.on(type, fn)
12
- }
13
- once(type, fn) {
14
- const wrap = (...args) => {
15
- this.off(type, wrap)
16
- fn(...args)
17
- }
18
- return this.on(type, wrap)
19
- }
20
- off(type, fn) {
21
- const list = this._e[type]
22
- if (list) this._e[type] = list.filter((item) => item !== fn)
23
- return this
24
- }
25
- removeListener(type, fn) {
26
- return this.off(type, fn)
27
- }
28
- emit(type, ...args) {
29
- const list = this._e[type]
30
- if (!list) return false
31
- for (const fn of list.slice()) fn(...args)
32
- return true
33
- }
34
- removeAllListeners(type) {
35
- if (type) delete this._e[type]
36
- else this._e = Object.create(null)
37
- return this
38
- }
39
- listeners(type) {
40
- return (this._e[type] || []).slice()
41
- }
42
- setMaxListeners() {
43
- return this
44
- }
45
- }
46
-
47
- class Buffer extends Uint8Array {
48
- static from(value, encodingOrOffset, length) {
49
- if (typeof value === 'string') return new Buffer(new TextEncoder().encode(value))
50
- if (value instanceof ArrayBuffer) return new Buffer(new Uint8Array(value, encodingOrOffset, length))
51
- if (ArrayBuffer.isView(value)) return new Buffer(new Uint8Array(value.buffer, value.byteOffset, value.byteLength))
52
- if (Array.isArray(value) || value && typeof value.length === 'number') return new Buffer(Uint8Array.from(value))
53
- return new Buffer(0)
54
- }
55
- static alloc(size, fill) {
56
- const buf = new Buffer(size)
57
- if (fill !== undefined && fill !== 0) {
58
- if (typeof fill === 'string') {
59
- const bytes = new TextEncoder().encode(fill)
60
- for (let i = 0; i < size; i += 1) buf[i] = bytes[i % bytes.length]
61
- } else buf.fill(fill)
62
- }
63
- return buf
64
- }
65
- static allocUnsafe(size) {
66
- return new Buffer(size)
67
- }
68
- static allocUnsafeSlow(size) {
69
- return new Buffer(size)
70
- }
71
- static concat(list, totalLength) {
72
- const length = totalLength ?? list.reduce((sum, item) => sum + item.length, 0)
73
- const out = new Buffer(length)
74
- let offset = 0
75
- for (const item of list) {
76
- out.set(item, offset)
77
- offset += item.length
78
- }
79
- return out
80
- }
81
- static isBuffer(value) {
82
- return value instanceof Buffer
83
- }
84
- static byteLength(value) {
85
- if (typeof value === 'string') return new TextEncoder().encode(value).length
86
- return value?.byteLength ?? value?.length ?? 0
87
- }
88
- static isEncoding(encoding) {
89
- return /^(utf8|utf-8|ascii|latin1|binary|hex|base64|base64url)$/i.test(String(encoding || ''))
90
- }
91
- toString(encoding) {
92
- if (encoding === 'hex') return [...this].map((byte) => byte.toString(16).padStart(2, '0')).join('')
93
- if (encoding === 'base64') {
94
- let binary = ''
95
- this.forEach((byte) => {
96
- binary += String.fromCharCode(byte)
97
- })
98
- return btoa(binary)
99
- }
100
- return new TextDecoder().decode(this)
101
- }
102
- slice(start, end) {
103
- return Buffer.from(super.subarray(start, end))
104
- }
105
- copy(target, targetStart = 0, start = 0, end = this.length) {
106
- const sliced = this.subarray(start, end)
107
- target.set(sliced, targetStart)
108
- return sliced.length
109
- }
110
- write(string, offset = 0) {
111
- const bytes = new TextEncoder().encode(string)
112
- const written = Math.min(bytes.length, this.length - offset)
113
- this.set(bytes.subarray(0, written), offset)
114
- return written
115
- }
116
- }
117
-
118
- function inherits(ctor, superCtor) {
119
- ctor.super_ = superCtor
120
- ctor.prototype = Object.create(superCtor.prototype, {
121
- constructor: { value: ctor, writable: true, configurable: true },
122
- })
123
- }
124
-
125
- if (!globalThis.Buffer) globalThis.Buffer = Buffer
126
-
127
- return {
128
- buffer: { Buffer, SlowBuffer: Buffer, INSPECT_MAX_BYTES: 50, kMaxLength: 0x7fffffff },
129
- stream: {
130
- Stream: EventEmitter,
131
- Readable: EventEmitter,
132
- Writable: EventEmitter,
133
- Duplex: EventEmitter,
134
- Transform: EventEmitter,
135
- PassThrough: EventEmitter,
136
- },
137
- events: { EventEmitter },
138
- util: {
139
- deprecate(fn) {
140
- return fn
141
- },
142
- inherits,
143
- inspect() {
144
- return ''
145
- },
146
- format(value) {
147
- return String(value)
148
- },
149
- types: { isBuffer: Buffer.isBuffer },
150
- },
151
- }
152
- }
153
-
154
1
  window.__ModuleLoader__.load({
155
2
  id: "@klarkxy/dsh-current-title",
156
3
  factory: (dshRequire) => {
157
- var __nodeShims;
158
- var process = globalThis.process || { env: {}, nextTick: function (fn) { var args = [].slice.call(arguments, 1); queueMicrotask(function () { fn.apply(null, args); }); } };
159
- var require = function (id) {
160
- if (id === 'buffer' || id === 'stream' || id === 'util' || id === 'events') {
161
- if (!__nodeShims) __nodeShims = __createDshNodeShims();
162
- return __nodeShims[id];
163
- }
164
- return dshRequire(id);
165
- };
4
+ var require = dshRequire;
166
5
  var module = { exports: {} };
167
6
  var exports = module.exports;
168
7
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
@@ -170,6 +9,30 @@ let react = require("react");
170
9
  let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
171
10
  let react_jsx_runtime = require("react/jsx-runtime");
172
11
  //#region ../dsh-plugin-kit/src/client-utils.ts
12
+ /** Fixed pause for failing background reads; manual work is never replayed. */
13
+ function createPluginReadGate(now = Date.now) {
14
+ let pausedUntil = 0;
15
+ let epoch = 0;
16
+ return {
17
+ canRead: () => now() >= pausedUntil,
18
+ reset: () => {
19
+ epoch++;
20
+ pausedUntil = 0;
21
+ },
22
+ async run(read) {
23
+ const current = epoch;
24
+ try {
25
+ const value = await read();
26
+ if (current === epoch) pausedUntil = 0;
27
+ return value;
28
+ } catch (error) {
29
+ const message = error instanceof Error ? error.message : "";
30
+ 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;
31
+ throw error;
32
+ }
33
+ }
34
+ };
35
+ }
173
36
  /** The Editor retains its manuscript session separately; native DSH exposes the main-view binding. */
174
37
  function selectedSessionId(client) {
175
38
  const workspace = client.uiWorkspace?.current;
@@ -555,7 +418,7 @@ ${scope} {
555
418
  border: 0.5px solid var(--dsw-alias-border-l3);
556
419
  border-radius: var(--dsw-radius-md);
557
420
  background: var(--dsw-alias-bg-layer-1);
558
- font-family: var(--ds-font-family-code, ui-monospace, 'Cascadia Mono', Consolas, monospace);
421
+ font-family: var(--ds-font-family-code);
559
422
  font-size: 12px;
560
423
  line-height: 20px;
561
424
  color: var(--dsw-alias-label-secondary);
@@ -631,6 +494,162 @@ ${scope} {
631
494
  `.replace(/^& (\.dsh-ui-[a-z0-9-]+)(?![+>~])/gm, (_match, className) => `&${className}, & ${className}`);
632
495
  }
633
496
  //#endregion
497
+ //#region ../dsh-plugin-kit/src/contracts.ts
498
+ /**
499
+ * Remote contribution for model-settings change notifications.
500
+ * Provided by `@klarkxy/dsh-model-hub` (`ModelSettingsChangesController`);
501
+ * subscribers degrade silently when that package is not installed.
502
+ * The stream carries invalidations only — `ready` is never a settings snapshot.
503
+ */
504
+ const MODEL_SETTINGS_CHANGES_REMOTE = {
505
+ package: "@klarkxy/dsh-model-hub",
506
+ descriptors: [{
507
+ id: "modelSettingsChanges/watch",
508
+ service: "modelSettingsChangesController",
509
+ namespace: "modelSettingsChanges",
510
+ method: "watch",
511
+ mode: "stream",
512
+ invocation: { kind: "direct" },
513
+ parameters: [],
514
+ cancellation: { parameter: "signal" }
515
+ }]
516
+ };
517
+ //#endregion
518
+ //#region ../dsh-plugin-kit/src/model-settings-client.ts
519
+ /** This stream carries invalidations only; ready is never a settings snapshot. */
520
+ function parseModelSettingsChangeFrame(value) {
521
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("Invalid model settings change frame");
522
+ const frame = value;
523
+ if (frame.kind === "ready" && Object.keys(frame).length === 1) return { kind: "ready" };
524
+ 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 {
525
+ kind: "changed",
526
+ ns: frame.ns,
527
+ revision: frame.revision
528
+ };
529
+ throw new Error("Invalid model settings change frame");
530
+ }
531
+ MODEL_SETTINGS_CHANGES_REMOTE.package, MODEL_SETTINGS_CHANGES_REMOTE.descriptors.map((descriptor) => ({
532
+ ...descriptor,
533
+ result: {
534
+ mode: "strict",
535
+ typeSymbol: "ModelSettings.ChangeFrame",
536
+ create: () => ({ parse: parseModelSettingsChangeFrame })
537
+ }
538
+ }));
539
+ /** Native RemoteStream owns carrier reconnects; no timers or browser transport.
540
+ * A factory shares one logical stream while at least one subscriber remains. */
541
+ function createModelSettingsSubscriber(ctx, namespace) {
542
+ const listeners = /* @__PURE__ */ new Set();
543
+ let injection;
544
+ let current;
545
+ let lifecycle = 0;
546
+ let failureMessage;
547
+ const report = (message) => {
548
+ failureMessage = message;
549
+ for (const listener of [...listeners]) try {
550
+ listener.error?.(message);
551
+ } catch {}
552
+ };
553
+ const invalidate = () => {
554
+ for (const listener of [...listeners]) {
555
+ if (!listeners.has(listener)) continue;
556
+ try {
557
+ listener.change();
558
+ } catch (error) {
559
+ try {
560
+ listener.error?.(error instanceof Error ? error.message : String(error));
561
+ } catch {}
562
+ }
563
+ }
564
+ };
565
+ const start = () => {
566
+ const life = ++lifecycle;
567
+ injection = ctx.inject(["remote", "remote.modelSettingsChanges"], (injected) => {
568
+ if (life !== lifecycle || !listeners.size) return;
569
+ const scope = injected;
570
+ const stream = scope.remote.$stream({
571
+ name: "Model settings sync",
572
+ open: (signal) => scope.remote.modelSettingsChanges.watch(signal),
573
+ ended: (accepted) => /* @__PURE__ */ new Error(`Model settings sync stream ended ${accepted ? "after" : "before"} ready; refresh or reopen the page`)
574
+ });
575
+ const session = {
576
+ stream,
577
+ active: true
578
+ };
579
+ current = session;
580
+ scope.effect(() => () => {
581
+ session.active = false;
582
+ if (current === session) {
583
+ current = void 0;
584
+ if (life === lifecycle && listeners.size) report("Model settings sync service unavailable");
585
+ }
586
+ return stream.dispose();
587
+ });
588
+ (async () => {
589
+ let generation;
590
+ let ready = false;
591
+ try {
592
+ for await (const item of stream) {
593
+ if (!session.active || life !== lifecycle || !listeners.size) break;
594
+ if (item.signal.aborted) continue;
595
+ if (generation !== item.generation) {
596
+ generation = item.generation;
597
+ ready = false;
598
+ }
599
+ const frame = parseModelSettingsChangeFrame(item.value);
600
+ if (frame.kind === "ready") {
601
+ ready = true;
602
+ failureMessage = void 0;
603
+ item.accept();
604
+ invalidate();
605
+ } else {
606
+ if (!ready) throw new Error("Model settings sync change arrived before ready");
607
+ if (namespace === "*" || frame.ns === namespace) invalidate();
608
+ }
609
+ }
610
+ } catch (error) {
611
+ if (session.active && life === lifecycle) report(error instanceof Error ? error.message : String(error));
612
+ } finally {
613
+ if (current === session) current = void 0;
614
+ session.active = false;
615
+ await stream.dispose().catch(() => {});
616
+ }
617
+ })();
618
+ });
619
+ queueMicrotask(() => {
620
+ if (life === lifecycle && listeners.size && !current) report("Model settings sync service unavailable");
621
+ });
622
+ };
623
+ return Object.freeze({ subscribe(onChange, onError) {
624
+ const listener = {
625
+ change: onChange,
626
+ error: onError
627
+ };
628
+ listeners.add(listener);
629
+ if (!injection) start();
630
+ else if (failureMessage) queueMicrotask(() => {
631
+ if (listeners.has(listener) && failureMessage) try {
632
+ listener.error?.(failureMessage);
633
+ } catch {}
634
+ });
635
+ return () => {
636
+ if (!listeners.delete(listener) || listeners.size) return;
637
+ lifecycle++;
638
+ if (current) {
639
+ current.active = false;
640
+ current.stream.dispose().catch(() => {});
641
+ }
642
+ current = void 0;
643
+ failureMessage = void 0;
644
+ const previous = injection;
645
+ injection = void 0;
646
+ try {
647
+ Promise.resolve(previous?.dispose()).catch(() => {});
648
+ } catch {}
649
+ };
650
+ } });
651
+ }
652
+ //#endregion
634
653
  //#region src/contracts.ts
635
654
  const RPC_CHANNEL = "/dsh-current-title";
636
655
  const TITLE_CLIENT_SERVICE = "dshCurrentTitleClient";
@@ -649,14 +668,16 @@ const titleCopy = {
649
668
  loading: "正在读取标题设置…",
650
669
  model: "标题模型",
651
670
  modelDefault: "跟随会话模型",
652
- effort: "思考强度",
653
- effortDefault: "默认",
654
671
  cadence: "更新节奏",
655
672
  cadenceAll: "跟随当前任务(每条消息)",
656
673
  cadenceFirst: "仅首条消息",
657
674
  saved: "已保存。",
658
675
  failed: "无法读取标题设置。",
659
- inactive: "标题服务未接管,新消息暂时不会自动更新。"
676
+ inactive: "标题服务未接管,新消息暂时不会自动更新。",
677
+ retry: "重试",
678
+ externalChange: "设置已在别处修改。提示词草稿已保留,请重新加载后确认。",
679
+ syncUnavailable: "自动刷新不可用,请手动重新加载设置。",
680
+ reload: "重新加载(丢弃草稿)"
660
681
  },
661
682
  en: {
662
683
  label: "Auto Title",
@@ -670,16 +691,38 @@ const titleCopy = {
670
691
  loading: "Loading title settings…",
671
692
  model: "Title model",
672
693
  modelDefault: "Use the session model",
673
- effort: "Reasoning effort",
674
- effortDefault: "Default",
675
694
  cadence: "Update cadence",
676
695
  cadenceAll: "Track the current task (every message)",
677
696
  cadenceFirst: "First message only",
678
697
  saved: "Saved.",
679
698
  failed: "Could not read title settings.",
680
- inactive: "Title generation is not active, so new messages will not rename this session."
699
+ inactive: "Title generation is not active, so new messages will not rename this session.",
700
+ retry: "Retry",
701
+ externalChange: "Settings changed elsewhere. Prompt draft preserved; reload and review.",
702
+ syncUnavailable: "Automatic refresh unavailable; reload settings manually.",
703
+ reload: "Reload (discard draft)"
681
704
  }
682
705
  };
706
+ /** Only plugin-owned defaults are translated; model output and remote diagnostics stay intact. */
707
+ const titleMessages = [
708
+ ["标题设置无效。", "Invalid title settings."],
709
+ ["设置已更新,请刷新后重试。", "Settings changed. Refresh and try again."],
710
+ ["无法保存标题设置。", "Could not save title settings."],
711
+ ["缺少会话。", "A session is required."],
712
+ ["找不到该会话。", "Session not found."],
713
+ ["标题服务未就绪。", "Title service is not ready."],
714
+ ["已取消。", "Cancelled."],
715
+ ["标题生成失败。", "Title generation failed."],
716
+ ["未知操作。", "Unknown operation."],
717
+ ["缺少所需的宿主服务。", "required host services are missing"],
718
+ ["原生标题服务位置已被占用。", "native title provider slot is occupied"],
719
+ ["其他标题服务已接管;先前的服务未恢复。", "another title provider owns the slot; previous owner was not restored"],
720
+ [titleCopy.zh.saved, titleCopy.en.saved],
721
+ [titleCopy.zh.failed, titleCopy.en.failed]
722
+ ];
723
+ function titleMessage(message, locale) {
724
+ return titleMessages.find((pair) => pair.includes(message))?.[locale === "zh" ? 0 : 1] ?? message;
725
+ }
683
726
  /** `undefined` draft = not edited; an empty string is a real edit (use built-in instruction). */
684
727
  function promptFieldValue(draft, saved) {
685
728
  return draft ?? saved;
@@ -828,28 +871,17 @@ function catalogChoices(value, selected) {
828
871
  const id = typeof item.id === "string" ? item.id : "";
829
872
  if (!id) continue;
830
873
  const name = typeof item.name === "string" && item.name ? item.name : id;
831
- const reasoning = item.reasoning && typeof item.reasoning === "object" ? item.reasoning : void 0;
832
- const efforts = Array.isArray(reasoning?.efforts) ? reasoning.efforts.flatMap((effort) => {
833
- if (!effort || typeof effort !== "object") return [];
834
- const entry = effort;
835
- return typeof entry.id === "string" && entry.id ? [{
836
- id: entry.id,
837
- name: typeof entry.name === "string" && entry.name ? entry.name : entry.id
838
- }] : [];
839
- }) : [];
840
874
  choices.push({
841
875
  provider,
842
876
  model: id,
843
- label: `${providerName} / ${name}`,
844
- efforts
877
+ label: `${providerName} / ${name}`
845
878
  });
846
879
  }
847
880
  }
848
881
  if (selected.provider && selected.model && !choices.some((item) => item.provider === selected.provider && item.model === selected.model)) choices.push({
849
882
  provider: selected.provider,
850
883
  model: selected.model,
851
- label: `${selected.provider} / ${selected.model}`,
852
- efforts: []
884
+ label: `${selected.provider} / ${selected.model}`
853
885
  });
854
886
  return choices;
855
887
  }
@@ -857,24 +889,55 @@ function TitleSettings(props) {
857
889
  const seat = useNativeSeat(props.client);
858
890
  const sessionId = seat.sessionId;
859
891
  const text = titleCopy[seat.locale];
892
+ const messages = titleCopy.en;
860
893
  const [status, setStatus] = (0, react.useState)();
861
894
  const [busy, setBusy] = (0, react.useState)(false);
862
895
  const [note, setNote] = (0, react.useState)("");
863
896
  const [error, setError] = (0, react.useState)("");
864
897
  const [prompt, setPrompt] = (0, react.useState)(void 0);
865
898
  const [choices, setChoices] = (0, react.useState)([]);
899
+ const [externalChange, setExternalChange] = (0, react.useState)(false);
900
+ const [syncError, setSyncError] = (0, react.useState)("");
901
+ const [loadingSettings, setLoadingSettings] = (0, react.useState)(true);
902
+ const dirty = (0, react.useRef)(false);
903
+ const external = (0, react.useRef)(false);
904
+ const reading = (0, react.useRef)(false);
905
+ const pending = (0, react.useRef)(false);
906
+ const syncRef = (0, react.useRef)(() => {});
866
907
  const workRef = (0, react.useRef)(void 0);
867
908
  if (!workRef.current) workRef.current = createTitleClientWork();
868
909
  const work = workRef.current;
910
+ const readGateRef = (0, react.useRef)(void 0);
911
+ if (!readGateRef.current) readGateRef.current = createPluginReadGate();
912
+ const readGate = readGateRef.current;
869
913
  const busyRef = (0, react.useRef)(busy);
870
914
  busyRef.current = busy;
915
+ const changeBusy = (value) => {
916
+ busyRef.current = value;
917
+ setBusy(value);
918
+ if (!value && pending.current) syncRef.current();
919
+ };
871
920
  const call = (endpoint, payload = {}) => props.client.connection.rpc.call(RPC_CHANNEL, endpoint, payload);
921
+ const readCall = (endpoint, payload) => readGate.run(() => call(endpoint, payload));
872
922
  function commitMarker(next) {
873
923
  applyMarkerFromStatus(props.marker, next, true);
874
924
  }
875
925
  (0, react.useEffect)(() => () => {
926
+ readGate.reset();
876
927
  work.dispose();
877
- }, [work]);
928
+ }, [work, readGate]);
929
+ (0, react.useEffect)(() => props.client.connection.generation?.subscribe(() => {
930
+ readGate.reset();
931
+ work.beginLoad();
932
+ reading.current = false;
933
+ setLoadingSettings(false);
934
+ busyRef.current = false;
935
+ setBusy(false);
936
+ }), [
937
+ props.client,
938
+ work,
939
+ readGate
940
+ ]);
878
941
  (0, react.useEffect)(() => {
879
942
  const catalog = props.client.remote?.session?.modelCatalog;
880
943
  if (!catalog) return void 0;
@@ -897,18 +960,34 @@ function TitleSettings(props) {
897
960
  ]);
898
961
  (0, react.useEffect)(() => {
899
962
  const generation = work.beginLoad();
963
+ reading.current = true;
964
+ pending.current = false;
965
+ setLoadingSettings(true);
966
+ readGate.reset();
900
967
  const isCurrent = () => work.isLoad(generation);
901
968
  setBusy(false);
902
969
  setError("");
903
970
  setNote("");
904
971
  runTitleStatusLoad({
905
- call,
972
+ call: readCall,
906
973
  sessionId: sessionId || void 0,
907
974
  isCurrent,
908
- failedMessage: text.failed,
909
- onStatus: setStatus,
910
- onError: setError,
975
+ failedMessage: messages.failed,
976
+ onStatus: (next) => {
977
+ if (!pending.current) setStatus((current) => current && external.current ? {
978
+ ...next,
979
+ settings: current.settings
980
+ } : next);
981
+ },
982
+ onError: (message) => {
983
+ if (!pending.current) setError(message);
984
+ },
911
985
  onMarker: commitMarker
986
+ }).finally(() => {
987
+ if (!work.isLoad(generation)) return;
988
+ reading.current = false;
989
+ setLoadingSettings(false);
990
+ if (pending.current) syncRef.current();
912
991
  });
913
992
  return () => {
914
993
  if (work.isLoad(generation)) work.beginLoad();
@@ -917,53 +996,96 @@ function TitleSettings(props) {
917
996
  props.client,
918
997
  props.marker,
919
998
  sessionId,
920
- text.failed,
921
999
  work
922
1000
  ]);
923
1001
  const refreshStatus = (0, react.useCallback)(() => {
924
- if (shouldSkipTitleRefresh({ busy: busyRef.current }) || work.disposed) return;
925
- const generation = work.captureLoad();
1002
+ if (!readGate.canRead() || shouldSkipTitleRefresh({ busy: busyRef.current }) || work.disposed) return;
1003
+ if (reading.current) return;
1004
+ reading.current = true;
1005
+ setLoadingSettings(true);
1006
+ readGate.reset();
1007
+ const token = work.beginRequest();
926
1008
  runTitleStatusLoad({
927
- call,
1009
+ call: readCall,
928
1010
  sessionId: sessionId || void 0,
929
- isCurrent: () => work.isLoad(generation) && !shouldSkipTitleRefresh({ busy: busyRef.current }),
930
- failedMessage: text.failed,
1011
+ isCurrent: () => work.isRequest(token) && !shouldSkipTitleRefresh({ busy: busyRef.current }),
1012
+ failedMessage: messages.failed,
931
1013
  onStatus: (next) => {
932
- if (shouldSkipTitleRefresh({ busy: busyRef.current })) return;
933
- setStatus(next);
1014
+ if (pending.current || shouldSkipTitleRefresh({ busy: busyRef.current })) return;
1015
+ setStatus((current) => current && external.current ? {
1016
+ ...next,
1017
+ settings: current.settings
1018
+ } : next);
1019
+ setError("");
1020
+ },
1021
+ onError: (message) => {
1022
+ if (!pending.current) setError(message);
934
1023
  },
935
- onError: () => {},
936
1024
  onMarker: (next) => {
937
- if (shouldSkipTitleRefresh({ busy: busyRef.current })) return;
1025
+ if (!next || shouldSkipTitleRefresh({ busy: busyRef.current })) return;
938
1026
  commitMarker(next);
939
1027
  }
1028
+ }).finally(() => {
1029
+ if (!work.isRequest(token)) return;
1030
+ reading.current = false;
1031
+ setLoadingSettings(false);
1032
+ if (pending.current) syncRef.current();
940
1033
  });
941
1034
  }, [
942
1035
  props.client,
943
1036
  props.marker,
944
1037
  sessionId,
945
- text.failed,
946
1038
  work
947
1039
  ]);
948
1040
  useFeatureRefresh(props.client, sessionId, refreshStatus, status?.session?.generating === true && !busy, Boolean(sessionId));
1041
+ syncRef.current = () => {
1042
+ if (work.disposed) return;
1043
+ if (reading.current || busyRef.current) {
1044
+ pending.current = true;
1045
+ return;
1046
+ }
1047
+ pending.current = false;
1048
+ if (dirty.current) {
1049
+ external.current = true;
1050
+ setExternalChange(true);
1051
+ return;
1052
+ }
1053
+ readGate.reset();
1054
+ refreshStatus();
1055
+ };
1056
+ (0, react.useEffect)(() => {
1057
+ setSyncError("");
1058
+ let live = true;
1059
+ const stop = props.subscribeSettings?.(() => {
1060
+ if (!live) return;
1061
+ setSyncError("");
1062
+ if (dirty.current && !busyRef.current) {
1063
+ external.current = true;
1064
+ setExternalChange(true);
1065
+ }
1066
+ syncRef.current();
1067
+ }, (message) => {
1068
+ if (live) setSyncError(message);
1069
+ });
1070
+ return () => {
1071
+ live = false;
1072
+ stop?.();
1073
+ };
1074
+ }, [props.subscribeSettings, props.client]);
949
1075
  const savedPrompt = status?.settings.prompt ?? "";
950
1076
  const savedModel = status?.settings.model ?? {
951
1077
  provider: "",
952
1078
  model: ""
953
1079
  };
1080
+ const selectableChoices = choices.some((choice) => choice.provider === savedModel.provider && choice.model === savedModel.model) ? choices : [...choices, ...catalogChoices({}, savedModel)];
954
1081
  const modelKey = savedModel.provider && savedModel.model ? `${savedModel.provider}\u001f${savedModel.model}` : "";
955
1082
  const title = status?.session?.title;
956
1083
  const pinned = status?.session?.pinned === true;
957
1084
  const weOwn = status?.support.weOwn === true;
958
- const canRegenerate = Boolean(sessionId) && weOwn && !busy;
959
- const selectedChoice = choices.find((item) => item.provider === savedModel.provider && item.model === savedModel.model);
960
- const efforts = selectedChoice ? [...selectedChoice.efforts] : [];
961
- if (selectedChoice && savedModel.reasoningEffort && !efforts.some((item) => item.id === savedModel.reasoningEffort)) efforts.push({
962
- id: savedModel.reasoningEffort,
963
- name: savedModel.reasoningEffort
964
- });
1085
+ const canRegenerate = Boolean(sessionId) && weOwn && !busy && !loadingSettings;
965
1086
  function saveSettings(patch) {
966
- if (!status) return;
1087
+ if (!status || busyRef.current || reading.current || external.current) return;
1088
+ readGate.reset();
967
1089
  const token = work.beginRequest();
968
1090
  setError("");
969
1091
  setNote("");
@@ -972,9 +1094,9 @@ function TitleSettings(props) {
972
1094
  ...patch,
973
1095
  expectedRevision: status.settings.revision,
974
1096
  isCurrent: () => work.isRequest(token),
975
- savedMessage: text.saved,
976
- failedMessage: text.failed,
977
- onBusy: setBusy,
1097
+ savedMessage: messages.saved,
1098
+ failedMessage: messages.failed,
1099
+ onBusy: changeBusy,
978
1100
  onSettings: (settings) => setStatus((current) => current ? {
979
1101
  ...current,
980
1102
  settings
@@ -985,6 +1107,7 @@ function TitleSettings(props) {
985
1107
  }
986
1108
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
987
1109
  className: "current-title-settings ct-root dsh-ui-panel",
1110
+ lang: seat.locale,
988
1111
  "data-testid": "current-title-settings",
989
1112
  children: [
990
1113
  !status && !error ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
@@ -1009,26 +1132,59 @@ function TitleSettings(props) {
1009
1132
  status && !weOwn ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
1010
1133
  role: "status",
1011
1134
  className: "dsh-ui-hint",
1012
- children: [text.inactive, status.support.limitation ? ` ${status.support.limitation}` : ""]
1135
+ children: [text.inactive, status.support.limitation ? ` ${titleMessage(status.support.limitation, seat.locale)}` : ""]
1013
1136
  }) : null,
1014
- error ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1137
+ error ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1015
1138
  role: "alert",
1016
1139
  className: "dsh-ui-error dsh-ui-wrap",
1017
- children: error
1018
- }) : null,
1140
+ children: titleMessage(error, seat.locale)
1141
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
1142
+ variant: "outline",
1143
+ size: "sm",
1144
+ disabled: busy,
1145
+ onClick: () => {
1146
+ readGate.reset();
1147
+ refreshStatus();
1148
+ },
1149
+ children: text.retry
1150
+ })] }) : null,
1019
1151
  note ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1020
1152
  role: "status",
1021
1153
  className: "dsh-ui-notice",
1022
- children: note
1154
+ children: titleMessage(note, seat.locale)
1023
1155
  }) : null,
1024
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1156
+ externalChange ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1157
+ role: "alert",
1158
+ className: "dsh-ui-error",
1159
+ children: text.externalChange
1160
+ }) : null,
1161
+ syncError ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1162
+ role: "status",
1163
+ className: "dsh-ui-hint",
1164
+ children: text.syncUnavailable
1165
+ }) : null,
1166
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1025
1167
  className: "dsh-ui-actions",
1026
- children: sessionId ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
1168
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
1169
+ variant: "ghost",
1170
+ size: "sm",
1171
+ disabled: busy || loadingSettings,
1172
+ onClick: () => {
1173
+ setPrompt(void 0);
1174
+ dirty.current = false;
1175
+ external.current = false;
1176
+ setExternalChange(false);
1177
+ readGate.reset();
1178
+ syncRef.current();
1179
+ },
1180
+ children: text.reload
1181
+ }), sessionId ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
1027
1182
  variant: "outline",
1028
1183
  size: "md",
1029
1184
  disabled: !canRegenerate,
1030
1185
  onClick: () => {
1031
1186
  if (!sessionId) return;
1187
+ readGate.reset();
1032
1188
  const token = work.beginRequest();
1033
1189
  setError("");
1034
1190
  setNote("");
@@ -1036,15 +1192,15 @@ function TitleSettings(props) {
1036
1192
  call,
1037
1193
  sessionId,
1038
1194
  isCurrent: () => work.isRequest(token),
1039
- failedMessage: text.failed,
1040
- onBusy: setBusy,
1195
+ failedMessage: messages.failed,
1196
+ onBusy: changeBusy,
1041
1197
  onStatus: setStatus,
1042
1198
  onError: setError,
1043
1199
  onMarker: commitMarker
1044
1200
  });
1045
1201
  },
1046
1202
  children: text.regenerate
1047
- }) : null
1203
+ }) : null]
1048
1204
  }),
1049
1205
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1050
1206
  className: "dsh-ui-field",
@@ -1058,18 +1214,21 @@ function TitleSettings(props) {
1058
1214
  value: status ? promptFieldValue(prompt, savedPrompt) : "",
1059
1215
  maxLength: 4e3,
1060
1216
  rows: 5,
1061
- disabled: busy || !status,
1217
+ disabled: busy || loadingSettings || !status,
1062
1218
  onChange: (event) => {
1219
+ dirty.current = true;
1063
1220
  setPrompt(event.target.value);
1064
1221
  setNote("");
1065
1222
  },
1066
1223
  onBlur: () => {
1067
- if (!status) return;
1224
+ if (!status || busyRef.current || reading.current || external.current) return;
1068
1225
  const next = promptToSave(prompt, savedPrompt);
1069
1226
  if (next === void 0) {
1227
+ dirty.current = false;
1070
1228
  setPrompt(void 0);
1071
1229
  return;
1072
1230
  }
1231
+ readGate.reset();
1073
1232
  const token = work.beginRequest();
1074
1233
  setError("");
1075
1234
  runTitleSettingsSave({
@@ -1077,14 +1236,15 @@ function TitleSettings(props) {
1077
1236
  prompt: next,
1078
1237
  expectedRevision: status.settings.revision,
1079
1238
  isCurrent: () => work.isRequest(token),
1080
- savedMessage: text.saved,
1081
- failedMessage: text.failed,
1082
- onBusy: setBusy,
1239
+ savedMessage: messages.saved,
1240
+ failedMessage: messages.failed,
1241
+ onBusy: changeBusy,
1083
1242
  onSettings: (settings) => {
1084
1243
  setStatus((current) => current ? {
1085
1244
  ...current,
1086
1245
  settings
1087
1246
  } : current);
1247
+ dirty.current = false;
1088
1248
  setPrompt(void 0);
1089
1249
  },
1090
1250
  onNote: setNote,
@@ -1106,10 +1266,10 @@ function TitleSettings(props) {
1106
1266
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
1107
1267
  className: "dsh-ui-select",
1108
1268
  value: modelKey,
1109
- disabled: busy || !status,
1269
+ disabled: busy || loadingSettings || !status,
1110
1270
  onChange: (event) => {
1111
1271
  const value = event.target.value;
1112
- const choice = choices.find((item) => `${item.provider}\u001f${item.model}` === value);
1272
+ const choice = selectableChoices.find((item) => `${item.provider}\u001f${item.model}` === value);
1113
1273
  saveSettings({ model: choice ? {
1114
1274
  provider: choice.provider,
1115
1275
  model: choice.model
@@ -1121,41 +1281,12 @@ function TitleSettings(props) {
1121
1281
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1122
1282
  value: "",
1123
1283
  children: text.modelDefault
1124
- }), choices.map((choice) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1284
+ }), selectableChoices.map((choice) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1125
1285
  value: `${choice.provider}\u001f${choice.model}`,
1126
1286
  children: choice.label
1127
1287
  }, `${choice.provider}\u001f${choice.model}`))]
1128
1288
  })]
1129
1289
  }),
1130
- selectedChoice && efforts.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1131
- className: "dsh-ui-field",
1132
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1133
- className: "dsh-ui-label",
1134
- children: text.effort
1135
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
1136
- className: "dsh-ui-select",
1137
- value: savedModel.reasoningEffort ?? "",
1138
- disabled: busy || !status,
1139
- onChange: (event) => {
1140
- const effort = event.target.value;
1141
- const model = {
1142
- provider: savedModel.provider,
1143
- model: savedModel.model
1144
- };
1145
- saveSettings({ model: effort ? {
1146
- ...model,
1147
- reasoningEffort: effort
1148
- } : model });
1149
- },
1150
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1151
- value: "",
1152
- children: text.effortDefault
1153
- }), efforts.map((effort) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1154
- value: effort.id,
1155
- children: effort.name
1156
- }, effort.id))]
1157
- })]
1158
- }) : null,
1159
1290
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1160
1291
  className: "dsh-ui-field",
1161
1292
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
@@ -1164,7 +1295,7 @@ function TitleSettings(props) {
1164
1295
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
1165
1296
  className: "dsh-ui-select",
1166
1297
  value: status?.settings.cadence ?? "all-prompts",
1167
- disabled: busy || !status,
1298
+ disabled: busy || loadingSettings || !status,
1168
1299
  onChange: (event) => {
1169
1300
  saveSettings({ cadence: event.target.value === "first-prompt" ? "first-prompt" : "all-prompts" });
1170
1301
  },
@@ -1202,6 +1333,8 @@ const styles = `${officialUiCss("ct-root")}
1202
1333
  `;
1203
1334
  function apply(ctx) {
1204
1335
  const client = ctx;
1336
+ const settingsSubscriber = createModelSettingsSubscriber(ctx, "@klarkxy/dsh-current-title");
1337
+ const subscribeSettings = (change, error) => settingsSubscriber.subscribe(change, error);
1205
1338
  const marker = createTitleClientMarker();
1206
1339
  ctx.provide(TITLE_CLIENT_SERVICE, marker);
1207
1340
  ctx.effect(() => {
@@ -1217,7 +1350,8 @@ function apply(ctx) {
1217
1350
  key: "@klarkxy/dsh-current-title"
1218
1351
  }, () => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TitleSettings, {
1219
1352
  client,
1220
- marker
1353
+ marker,
1354
+ subscribeSettings
1221
1355
  }))), "current-title.settings");
1222
1356
  ctx.effect(() => {
1223
1357
  let live = true;