@klarkxy/dsh-current-title 0.2.0 → 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/README.md +3 -0
- package/docs/README.zh-CN.md +3 -0
- package/lib/client.inner.cjs +327 -36
- package/lib/client.inner.cjs.map +1 -1
- package/lib/client.js +327 -36
- package/lib/contracts.d.ts +7 -8
- package/lib/contracts.js +1 -22
- package/lib/contracts.js.map +1 -1
- package/lib/index.d.ts +1 -0
- package/lib/index.js +78 -11
- package/lib/index.js.map +1 -1
- package/package.json +10 -4
package/lib/client.js
CHANGED
|
@@ -418,7 +418,7 @@ ${scope} {
|
|
|
418
418
|
border: 0.5px solid var(--dsw-alias-border-l3);
|
|
419
419
|
border-radius: var(--dsw-radius-md);
|
|
420
420
|
background: var(--dsw-alias-bg-layer-1);
|
|
421
|
-
font-family: var(--ds-font-family-code
|
|
421
|
+
font-family: var(--ds-font-family-code);
|
|
422
422
|
font-size: 12px;
|
|
423
423
|
line-height: 20px;
|
|
424
424
|
color: var(--dsw-alias-label-secondary);
|
|
@@ -494,6 +494,162 @@ ${scope} {
|
|
|
494
494
|
`.replace(/^& (\.dsh-ui-[a-z0-9-]+)(?![+>~])/gm, (_match, className) => `&${className}, & ${className}`);
|
|
495
495
|
}
|
|
496
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
|
|
497
653
|
//#region src/contracts.ts
|
|
498
654
|
const RPC_CHANNEL = "/dsh-current-title";
|
|
499
655
|
const TITLE_CLIENT_SERVICE = "dshCurrentTitleClient";
|
|
@@ -517,7 +673,11 @@ const titleCopy = {
|
|
|
517
673
|
cadenceFirst: "仅首条消息",
|
|
518
674
|
saved: "已保存。",
|
|
519
675
|
failed: "无法读取标题设置。",
|
|
520
|
-
inactive: "标题服务未接管,新消息暂时不会自动更新。"
|
|
676
|
+
inactive: "标题服务未接管,新消息暂时不会自动更新。",
|
|
677
|
+
retry: "重试",
|
|
678
|
+
externalChange: "设置已在别处修改。提示词草稿已保留,请重新加载后确认。",
|
|
679
|
+
syncUnavailable: "自动刷新不可用,请手动重新加载设置。",
|
|
680
|
+
reload: "重新加载(丢弃草稿)"
|
|
521
681
|
},
|
|
522
682
|
en: {
|
|
523
683
|
label: "Auto Title",
|
|
@@ -536,9 +696,33 @@ const titleCopy = {
|
|
|
536
696
|
cadenceFirst: "First message only",
|
|
537
697
|
saved: "Saved.",
|
|
538
698
|
failed: "Could not read title settings.",
|
|
539
|
-
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)"
|
|
540
704
|
}
|
|
541
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
|
+
}
|
|
542
726
|
/** `undefined` draft = not edited; an empty string is a real edit (use built-in instruction). */
|
|
543
727
|
function promptFieldValue(draft, saved) {
|
|
544
728
|
return draft ?? saved;
|
|
@@ -705,12 +889,21 @@ function TitleSettings(props) {
|
|
|
705
889
|
const seat = useNativeSeat(props.client);
|
|
706
890
|
const sessionId = seat.sessionId;
|
|
707
891
|
const text = titleCopy[seat.locale];
|
|
892
|
+
const messages = titleCopy.en;
|
|
708
893
|
const [status, setStatus] = (0, react.useState)();
|
|
709
894
|
const [busy, setBusy] = (0, react.useState)(false);
|
|
710
895
|
const [note, setNote] = (0, react.useState)("");
|
|
711
896
|
const [error, setError] = (0, react.useState)("");
|
|
712
897
|
const [prompt, setPrompt] = (0, react.useState)(void 0);
|
|
713
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)(() => {});
|
|
714
907
|
const workRef = (0, react.useRef)(void 0);
|
|
715
908
|
if (!workRef.current) workRef.current = createTitleClientWork();
|
|
716
909
|
const work = workRef.current;
|
|
@@ -719,6 +912,11 @@ function TitleSettings(props) {
|
|
|
719
912
|
const readGate = readGateRef.current;
|
|
720
913
|
const busyRef = (0, react.useRef)(busy);
|
|
721
914
|
busyRef.current = busy;
|
|
915
|
+
const changeBusy = (value) => {
|
|
916
|
+
busyRef.current = value;
|
|
917
|
+
setBusy(value);
|
|
918
|
+
if (!value && pending.current) syncRef.current();
|
|
919
|
+
};
|
|
722
920
|
const call = (endpoint, payload = {}) => props.client.connection.rpc.call(RPC_CHANNEL, endpoint, payload);
|
|
723
921
|
const readCall = (endpoint, payload) => readGate.run(() => call(endpoint, payload));
|
|
724
922
|
function commitMarker(next) {
|
|
@@ -731,6 +929,8 @@ function TitleSettings(props) {
|
|
|
731
929
|
(0, react.useEffect)(() => props.client.connection.generation?.subscribe(() => {
|
|
732
930
|
readGate.reset();
|
|
733
931
|
work.beginLoad();
|
|
932
|
+
reading.current = false;
|
|
933
|
+
setLoadingSettings(false);
|
|
734
934
|
busyRef.current = false;
|
|
735
935
|
setBusy(false);
|
|
736
936
|
}), [
|
|
@@ -760,6 +960,9 @@ function TitleSettings(props) {
|
|
|
760
960
|
]);
|
|
761
961
|
(0, react.useEffect)(() => {
|
|
762
962
|
const generation = work.beginLoad();
|
|
963
|
+
reading.current = true;
|
|
964
|
+
pending.current = false;
|
|
965
|
+
setLoadingSettings(true);
|
|
763
966
|
readGate.reset();
|
|
764
967
|
const isCurrent = () => work.isLoad(generation);
|
|
765
968
|
setBusy(false);
|
|
@@ -769,10 +972,22 @@ function TitleSettings(props) {
|
|
|
769
972
|
call: readCall,
|
|
770
973
|
sessionId: sessionId || void 0,
|
|
771
974
|
isCurrent,
|
|
772
|
-
failedMessage:
|
|
773
|
-
onStatus:
|
|
774
|
-
|
|
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
|
+
},
|
|
775
985
|
onMarker: commitMarker
|
|
986
|
+
}).finally(() => {
|
|
987
|
+
if (!work.isLoad(generation)) return;
|
|
988
|
+
reading.current = false;
|
|
989
|
+
setLoadingSettings(false);
|
|
990
|
+
if (pending.current) syncRef.current();
|
|
776
991
|
});
|
|
777
992
|
return () => {
|
|
778
993
|
if (work.isLoad(generation)) work.beginLoad();
|
|
@@ -781,49 +996,95 @@ function TitleSettings(props) {
|
|
|
781
996
|
props.client,
|
|
782
997
|
props.marker,
|
|
783
998
|
sessionId,
|
|
784
|
-
text.failed,
|
|
785
999
|
work
|
|
786
1000
|
]);
|
|
787
1001
|
const refreshStatus = (0, react.useCallback)(() => {
|
|
788
1002
|
if (!readGate.canRead() || shouldSkipTitleRefresh({ busy: busyRef.current }) || work.disposed) return;
|
|
1003
|
+
if (reading.current) return;
|
|
1004
|
+
reading.current = true;
|
|
1005
|
+
setLoadingSettings(true);
|
|
789
1006
|
readGate.reset();
|
|
790
1007
|
const token = work.beginRequest();
|
|
791
1008
|
runTitleStatusLoad({
|
|
792
1009
|
call: readCall,
|
|
793
1010
|
sessionId: sessionId || void 0,
|
|
794
1011
|
isCurrent: () => work.isRequest(token) && !shouldSkipTitleRefresh({ busy: busyRef.current }),
|
|
795
|
-
failedMessage:
|
|
1012
|
+
failedMessage: messages.failed,
|
|
796
1013
|
onStatus: (next) => {
|
|
797
|
-
if (shouldSkipTitleRefresh({ busy: busyRef.current })) return;
|
|
798
|
-
setStatus(
|
|
1014
|
+
if (pending.current || shouldSkipTitleRefresh({ busy: busyRef.current })) return;
|
|
1015
|
+
setStatus((current) => current && external.current ? {
|
|
1016
|
+
...next,
|
|
1017
|
+
settings: current.settings
|
|
1018
|
+
} : next);
|
|
799
1019
|
setError("");
|
|
800
1020
|
},
|
|
801
|
-
onError:
|
|
1021
|
+
onError: (message) => {
|
|
1022
|
+
if (!pending.current) setError(message);
|
|
1023
|
+
},
|
|
802
1024
|
onMarker: (next) => {
|
|
803
1025
|
if (!next || shouldSkipTitleRefresh({ busy: busyRef.current })) return;
|
|
804
1026
|
commitMarker(next);
|
|
805
1027
|
}
|
|
1028
|
+
}).finally(() => {
|
|
1029
|
+
if (!work.isRequest(token)) return;
|
|
1030
|
+
reading.current = false;
|
|
1031
|
+
setLoadingSettings(false);
|
|
1032
|
+
if (pending.current) syncRef.current();
|
|
806
1033
|
});
|
|
807
1034
|
}, [
|
|
808
1035
|
props.client,
|
|
809
1036
|
props.marker,
|
|
810
1037
|
sessionId,
|
|
811
|
-
text.failed,
|
|
812
1038
|
work
|
|
813
1039
|
]);
|
|
814
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]);
|
|
815
1075
|
const savedPrompt = status?.settings.prompt ?? "";
|
|
816
1076
|
const savedModel = status?.settings.model ?? {
|
|
817
1077
|
provider: "",
|
|
818
1078
|
model: ""
|
|
819
1079
|
};
|
|
1080
|
+
const selectableChoices = choices.some((choice) => choice.provider === savedModel.provider && choice.model === savedModel.model) ? choices : [...choices, ...catalogChoices({}, savedModel)];
|
|
820
1081
|
const modelKey = savedModel.provider && savedModel.model ? `${savedModel.provider}\u001f${savedModel.model}` : "";
|
|
821
1082
|
const title = status?.session?.title;
|
|
822
1083
|
const pinned = status?.session?.pinned === true;
|
|
823
1084
|
const weOwn = status?.support.weOwn === true;
|
|
824
|
-
const canRegenerate = Boolean(sessionId) && weOwn && !busy;
|
|
1085
|
+
const canRegenerate = Boolean(sessionId) && weOwn && !busy && !loadingSettings;
|
|
825
1086
|
function saveSettings(patch) {
|
|
826
|
-
if (!status) return;
|
|
1087
|
+
if (!status || busyRef.current || reading.current || external.current) return;
|
|
827
1088
|
readGate.reset();
|
|
828
1089
|
const token = work.beginRequest();
|
|
829
1090
|
setError("");
|
|
@@ -833,9 +1094,9 @@ function TitleSettings(props) {
|
|
|
833
1094
|
...patch,
|
|
834
1095
|
expectedRevision: status.settings.revision,
|
|
835
1096
|
isCurrent: () => work.isRequest(token),
|
|
836
|
-
savedMessage:
|
|
837
|
-
failedMessage:
|
|
838
|
-
onBusy:
|
|
1097
|
+
savedMessage: messages.saved,
|
|
1098
|
+
failedMessage: messages.failed,
|
|
1099
|
+
onBusy: changeBusy,
|
|
839
1100
|
onSettings: (settings) => setStatus((current) => current ? {
|
|
840
1101
|
...current,
|
|
841
1102
|
settings
|
|
@@ -846,6 +1107,7 @@ function TitleSettings(props) {
|
|
|
846
1107
|
}
|
|
847
1108
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
848
1109
|
className: "current-title-settings ct-root dsh-ui-panel",
|
|
1110
|
+
lang: seat.locale,
|
|
849
1111
|
"data-testid": "current-title-settings",
|
|
850
1112
|
children: [
|
|
851
1113
|
!status && !error ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
@@ -870,12 +1132,12 @@ function TitleSettings(props) {
|
|
|
870
1132
|
status && !weOwn ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
|
|
871
1133
|
role: "status",
|
|
872
1134
|
className: "dsh-ui-hint",
|
|
873
|
-
children: [text.inactive, status.support.limitation ? ` ${status.support.limitation}` : ""]
|
|
1135
|
+
children: [text.inactive, status.support.limitation ? ` ${titleMessage(status.support.limitation, seat.locale)}` : ""]
|
|
874
1136
|
}) : null,
|
|
875
1137
|
error ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
876
1138
|
role: "alert",
|
|
877
1139
|
className: "dsh-ui-error dsh-ui-wrap",
|
|
878
|
-
children: error
|
|
1140
|
+
children: titleMessage(error, seat.locale)
|
|
879
1141
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
880
1142
|
variant: "outline",
|
|
881
1143
|
size: "sm",
|
|
@@ -884,16 +1146,39 @@ function TitleSettings(props) {
|
|
|
884
1146
|
readGate.reset();
|
|
885
1147
|
refreshStatus();
|
|
886
1148
|
},
|
|
887
|
-
children:
|
|
1149
|
+
children: text.retry
|
|
888
1150
|
})] }) : null,
|
|
889
1151
|
note ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
890
1152
|
role: "status",
|
|
891
1153
|
className: "dsh-ui-notice",
|
|
892
|
-
children: note
|
|
1154
|
+
children: titleMessage(note, seat.locale)
|
|
1155
|
+
}) : null,
|
|
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
|
|
893
1165
|
}) : null,
|
|
894
|
-
/* @__PURE__ */ (0, react_jsx_runtime.
|
|
1166
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
895
1167
|
className: "dsh-ui-actions",
|
|
896
|
-
children:
|
|
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, {
|
|
897
1182
|
variant: "outline",
|
|
898
1183
|
size: "md",
|
|
899
1184
|
disabled: !canRegenerate,
|
|
@@ -907,15 +1192,15 @@ function TitleSettings(props) {
|
|
|
907
1192
|
call,
|
|
908
1193
|
sessionId,
|
|
909
1194
|
isCurrent: () => work.isRequest(token),
|
|
910
|
-
failedMessage:
|
|
911
|
-
onBusy:
|
|
1195
|
+
failedMessage: messages.failed,
|
|
1196
|
+
onBusy: changeBusy,
|
|
912
1197
|
onStatus: setStatus,
|
|
913
1198
|
onError: setError,
|
|
914
1199
|
onMarker: commitMarker
|
|
915
1200
|
});
|
|
916
1201
|
},
|
|
917
1202
|
children: text.regenerate
|
|
918
|
-
}) : null
|
|
1203
|
+
}) : null]
|
|
919
1204
|
}),
|
|
920
1205
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
921
1206
|
className: "dsh-ui-field",
|
|
@@ -929,15 +1214,17 @@ function TitleSettings(props) {
|
|
|
929
1214
|
value: status ? promptFieldValue(prompt, savedPrompt) : "",
|
|
930
1215
|
maxLength: 4e3,
|
|
931
1216
|
rows: 5,
|
|
932
|
-
disabled: busy || !status,
|
|
1217
|
+
disabled: busy || loadingSettings || !status,
|
|
933
1218
|
onChange: (event) => {
|
|
1219
|
+
dirty.current = true;
|
|
934
1220
|
setPrompt(event.target.value);
|
|
935
1221
|
setNote("");
|
|
936
1222
|
},
|
|
937
1223
|
onBlur: () => {
|
|
938
|
-
if (!status) return;
|
|
1224
|
+
if (!status || busyRef.current || reading.current || external.current) return;
|
|
939
1225
|
const next = promptToSave(prompt, savedPrompt);
|
|
940
1226
|
if (next === void 0) {
|
|
1227
|
+
dirty.current = false;
|
|
941
1228
|
setPrompt(void 0);
|
|
942
1229
|
return;
|
|
943
1230
|
}
|
|
@@ -949,14 +1236,15 @@ function TitleSettings(props) {
|
|
|
949
1236
|
prompt: next,
|
|
950
1237
|
expectedRevision: status.settings.revision,
|
|
951
1238
|
isCurrent: () => work.isRequest(token),
|
|
952
|
-
savedMessage:
|
|
953
|
-
failedMessage:
|
|
954
|
-
onBusy:
|
|
1239
|
+
savedMessage: messages.saved,
|
|
1240
|
+
failedMessage: messages.failed,
|
|
1241
|
+
onBusy: changeBusy,
|
|
955
1242
|
onSettings: (settings) => {
|
|
956
1243
|
setStatus((current) => current ? {
|
|
957
1244
|
...current,
|
|
958
1245
|
settings
|
|
959
1246
|
} : current);
|
|
1247
|
+
dirty.current = false;
|
|
960
1248
|
setPrompt(void 0);
|
|
961
1249
|
},
|
|
962
1250
|
onNote: setNote,
|
|
@@ -978,10 +1266,10 @@ function TitleSettings(props) {
|
|
|
978
1266
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
|
|
979
1267
|
className: "dsh-ui-select",
|
|
980
1268
|
value: modelKey,
|
|
981
|
-
disabled: busy || !status,
|
|
1269
|
+
disabled: busy || loadingSettings || !status,
|
|
982
1270
|
onChange: (event) => {
|
|
983
1271
|
const value = event.target.value;
|
|
984
|
-
const choice =
|
|
1272
|
+
const choice = selectableChoices.find((item) => `${item.provider}\u001f${item.model}` === value);
|
|
985
1273
|
saveSettings({ model: choice ? {
|
|
986
1274
|
provider: choice.provider,
|
|
987
1275
|
model: choice.model
|
|
@@ -993,7 +1281,7 @@ function TitleSettings(props) {
|
|
|
993
1281
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
994
1282
|
value: "",
|
|
995
1283
|
children: text.modelDefault
|
|
996
|
-
}),
|
|
1284
|
+
}), selectableChoices.map((choice) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
997
1285
|
value: `${choice.provider}\u001f${choice.model}`,
|
|
998
1286
|
children: choice.label
|
|
999
1287
|
}, `${choice.provider}\u001f${choice.model}`))]
|
|
@@ -1007,7 +1295,7 @@ function TitleSettings(props) {
|
|
|
1007
1295
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
|
|
1008
1296
|
className: "dsh-ui-select",
|
|
1009
1297
|
value: status?.settings.cadence ?? "all-prompts",
|
|
1010
|
-
disabled: busy || !status,
|
|
1298
|
+
disabled: busy || loadingSettings || !status,
|
|
1011
1299
|
onChange: (event) => {
|
|
1012
1300
|
saveSettings({ cadence: event.target.value === "first-prompt" ? "first-prompt" : "all-prompts" });
|
|
1013
1301
|
},
|
|
@@ -1045,6 +1333,8 @@ const styles = `${officialUiCss("ct-root")}
|
|
|
1045
1333
|
`;
|
|
1046
1334
|
function apply(ctx) {
|
|
1047
1335
|
const client = ctx;
|
|
1336
|
+
const settingsSubscriber = createModelSettingsSubscriber(ctx, "@klarkxy/dsh-current-title");
|
|
1337
|
+
const subscribeSettings = (change, error) => settingsSubscriber.subscribe(change, error);
|
|
1048
1338
|
const marker = createTitleClientMarker();
|
|
1049
1339
|
ctx.provide(TITLE_CLIENT_SERVICE, marker);
|
|
1050
1340
|
ctx.effect(() => {
|
|
@@ -1060,7 +1350,8 @@ function apply(ctx) {
|
|
|
1060
1350
|
key: "@klarkxy/dsh-current-title"
|
|
1061
1351
|
}, () => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TitleSettings, {
|
|
1062
1352
|
client,
|
|
1063
|
-
marker
|
|
1353
|
+
marker,
|
|
1354
|
+
subscribeSettings
|
|
1064
1355
|
}))), "current-title.settings");
|
|
1065
1356
|
ctx.effect(() => {
|
|
1066
1357
|
let live = true;
|
package/lib/contracts.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ModelRoute, defaultModelRoute, normalizeModelRoute } from "@klarkxy/dsh-model-route";
|
|
1
2
|
import { RpcResult } from "@klarkxy/dsh-plugin-kit/contracts";
|
|
2
3
|
//#region src/contracts.d.ts
|
|
3
4
|
declare const PLUGIN_NAME = "@klarkxy/dsh-current-title";
|
|
@@ -36,12 +37,12 @@ interface TitleSnapshot {
|
|
|
36
37
|
readonly eventSeq: number;
|
|
37
38
|
readonly updatedAt: number;
|
|
38
39
|
}
|
|
39
|
-
/**
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
40
|
+
/**
|
|
41
|
+
* Empty provider and model means the shared weak role. Both must be set together.
|
|
42
|
+
* No title-only fields: the stored value is the shared ModelRoute (250/80 caps
|
|
43
|
+
* live in @klarkxy/dsh-model-route). The alias keeps the title-specific name.
|
|
44
|
+
*/
|
|
45
|
+
type TitleModelRoute = ModelRoute;
|
|
45
46
|
interface TitleSettings {
|
|
46
47
|
revision: number;
|
|
47
48
|
locale: TitleLocaleMode;
|
|
@@ -79,11 +80,9 @@ interface GenerateConfig {
|
|
|
79
80
|
readonly timeoutMs: number;
|
|
80
81
|
readonly maxTitleBytes: number;
|
|
81
82
|
}
|
|
82
|
-
declare function defaultModelRoute(): TitleModelRoute;
|
|
83
83
|
declare function defaultSettings(locale?: TitleLocaleMode): TitleSettings;
|
|
84
84
|
declare function normalizeCadence(value: unknown): TitleCadence;
|
|
85
85
|
declare function normalizePrompt(value: unknown): string;
|
|
86
|
-
declare function normalizeModelRoute(value: unknown): TitleModelRoute;
|
|
87
86
|
declare function defaultGenerateConfig(locale?: TitleLocaleMode): GenerateConfig;
|
|
88
87
|
declare function isTitleLocaleMode(value: unknown): value is TitleLocaleMode;
|
|
89
88
|
//#endregion
|
package/lib/contracts.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { defaultModelRoute, normalizeModelRoute } from "@klarkxy/dsh-model-route";
|
|
1
2
|
//#region src/contracts.ts
|
|
2
3
|
const PLUGIN_NAME = "@klarkxy/dsh-current-title";
|
|
3
4
|
const FEATURE_ID = "current-title";
|
|
@@ -17,12 +18,6 @@ const DEFAULT_MAX_INPUT_BYTES = 4096;
|
|
|
17
18
|
const DEFAULT_MAX_OUTPUT_TOKENS = 256;
|
|
18
19
|
const DEFAULT_TIMEOUT_MS = 6e4;
|
|
19
20
|
const DEFAULT_MAX_TITLE_BYTES = 80;
|
|
20
|
-
function defaultModelRoute() {
|
|
21
|
-
return {
|
|
22
|
-
provider: "",
|
|
23
|
-
model: ""
|
|
24
|
-
};
|
|
25
|
-
}
|
|
26
21
|
function defaultSettings(locale = "auto") {
|
|
27
22
|
return {
|
|
28
23
|
revision: 0,
|
|
@@ -39,22 +34,6 @@ function normalizePrompt(value) {
|
|
|
39
34
|
if (typeof value !== "string") return "";
|
|
40
35
|
return value.replace(/\u0000/gu, "").trim().slice(0, MAX_PROMPT_CHARS);
|
|
41
36
|
}
|
|
42
|
-
function normalizeModelRoute(value) {
|
|
43
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) return defaultModelRoute();
|
|
44
|
-
const route = value;
|
|
45
|
-
const provider = typeof route.provider === "string" ? route.provider.trim() : "";
|
|
46
|
-
const model = typeof route.model === "string" ? route.model.trim() : "";
|
|
47
|
-
const effort = typeof route.reasoningEffort === "string" ? route.reasoningEffort.trim() : "";
|
|
48
|
-
if (!provider || !model || provider.length > 250 || model.length > 250) return defaultModelRoute();
|
|
49
|
-
return effort && effort.length <= 80 ? {
|
|
50
|
-
provider,
|
|
51
|
-
model,
|
|
52
|
-
reasoningEffort: effort
|
|
53
|
-
} : {
|
|
54
|
-
provider,
|
|
55
|
-
model
|
|
56
|
-
};
|
|
57
|
-
}
|
|
58
37
|
function defaultGenerateConfig(locale = "auto") {
|
|
59
38
|
return {
|
|
60
39
|
maxRecentMessages: 8,
|