@klarkxy/dsh-session-manager 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 +6 -1
- package/README.zh-CN.md +6 -1
- package/lib/client.inner.cjs +2546 -20
- package/lib/client.inner.cjs.map +1 -1
- package/lib/client.js +2546 -20
- package/lib/index.d.ts +13 -0
- package/lib/index.js +901 -30
- package/lib/index.js.map +1 -1
- package/locale/en.json +1 -1
- package/locale/zh.json +1 -1
- package/package.json +14 -5
package/lib/client.inner.cjs
CHANGED
|
@@ -2,7 +2,10 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
|
2
2
|
let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
|
|
3
3
|
let react = require("react");
|
|
4
4
|
let react_jsx_runtime = require("react/jsx-runtime");
|
|
5
|
+
let react_dom = require("react-dom");
|
|
5
6
|
//#region src/contracts.ts
|
|
7
|
+
/** Shared names and bounds for cross-session discovery. */
|
|
8
|
+
const PLUGIN_NAME = "@klarkxy/dsh-session-manager";
|
|
6
9
|
const COPY_THREAD_MENU_ID = "@klarkxy/dsh-session-manager.copy-thread-id";
|
|
7
10
|
const COPY_THREAD_SHORTCUT_ID = "session-manager.copy-thread-id";
|
|
8
11
|
const TEMP_WORKSPACE_CHANNEL = "/dsh-session-manager";
|
|
@@ -10,6 +13,17 @@ const TEMP_WORKSPACE_FLOW_SLOT = "session-manager.workspace.directoryFlow";
|
|
|
10
13
|
const USER_ACTIONS_SLOT = "conversation.chat.user-actions";
|
|
11
14
|
const USER_ACTIONS_ID = "@klarkxy/dsh-session-manager.user-actions";
|
|
12
15
|
const DELETE_DIALOG_ID = "@klarkxy/dsh-session-manager.delete-chat";
|
|
16
|
+
/** Endpoints on the shared TEMP_WORKSPACE_CHANNEL beyond temporary-directory allocation. */
|
|
17
|
+
const WELCOME_BACK_PREPARE_ENDPOINT = "welcomeback.prepare";
|
|
18
|
+
const WELCOME_BACK_COMPACT_ENDPOINT = "welcomeback.compact";
|
|
19
|
+
const CONTINUE_SUGGEST_ENDPOINT = "continue.suggest";
|
|
20
|
+
const CONFIG_READ_ENDPOINT = "config.read";
|
|
21
|
+
const CONFIG_WRITE_ENDPOINT = "config.write";
|
|
22
|
+
/** Card entry id in the chat flow; the recap renders at the latest completed turn's tail. */
|
|
23
|
+
const WELCOME_BACK_CARD_ID = "@klarkxy/dsh-session-manager.welcome-back";
|
|
24
|
+
const WELCOME_BACK_CARD_SLOT = "conversation.chat.turnTail";
|
|
25
|
+
const CONTINUE_GHOST_ID = "@klarkxy/dsh-session-manager.continue-ghost";
|
|
26
|
+
const CONTINUE_GHOST_SLOT = "conversation.input.overlay";
|
|
13
27
|
//#endregion
|
|
14
28
|
//#region src/copy-action.ts
|
|
15
29
|
const COPY_THREAD_LABEL = {
|
|
@@ -531,6 +545,29 @@ ${scope} {
|
|
|
531
545
|
& .dsh-ui-select:disabled { color: var(--dsw-alias-label-tertiary); cursor: default; }
|
|
532
546
|
& .dsh-ui-select[aria-invalid='true'] { border-color: var(--dsw-alias-state-error-primary); }
|
|
533
547
|
|
|
548
|
+
/* --- Native textarea. The primitives ship no textarea either, so a settings
|
|
549
|
+
* prompt keeps the platform element and the same field shell as the select:
|
|
550
|
+
* 0.5px l4, radius-md, layer-3, 13/1.5 type. It grows vertically instead of
|
|
551
|
+
* locking to the select's 34px row. --- */
|
|
552
|
+
& .dsh-ui-textarea {
|
|
553
|
+
box-sizing: border-box;
|
|
554
|
+
width: 100%;
|
|
555
|
+
min-width: 0;
|
|
556
|
+
resize: vertical;
|
|
557
|
+
padding: 8px 12px;
|
|
558
|
+
border: 0.5px solid var(--dsw-alias-border-l4);
|
|
559
|
+
border-radius: var(--dsw-radius-md);
|
|
560
|
+
background: var(--dsw-alias-bg-layer-3);
|
|
561
|
+
font: inherit;
|
|
562
|
+
font-size: 13px;
|
|
563
|
+
line-height: 1.5;
|
|
564
|
+
color: var(--dsw-alias-label-primary);
|
|
565
|
+
}
|
|
566
|
+
& .dsh-ui-textarea:focus-visible { outline: none; border-color: var(--dsw-alias-state-business-primary); }
|
|
567
|
+
& .dsh-ui-textarea:disabled { color: var(--dsw-alias-label-tertiary); cursor: default; }
|
|
568
|
+
& .dsh-ui-textarea::placeholder { color: var(--dsw-alias-label-dimmed); }
|
|
569
|
+
& .dsh-ui-textarea[aria-invalid='true'] { border-color: var(--dsw-alias-state-error-primary); }
|
|
570
|
+
|
|
534
571
|
/* Long panels scroll inside themselves instead of growing the host page. */
|
|
535
572
|
& .dsh-ui-panel { display: flex; flex-direction: column; gap: 16px; min-width: 0; }
|
|
536
573
|
& .dsh-ui-panel-wide { max-width: 100%; }
|
|
@@ -542,24 +579,984 @@ ${scope} {
|
|
|
542
579
|
`.replace(/^& (\.dsh-ui-[a-z0-9-]+)(?![+>~])/gm, (_match, className) => `&${className}, & ${className}`);
|
|
543
580
|
}
|
|
544
581
|
//#endregion
|
|
582
|
+
//#region ../dsh-model-route/src/route.ts
|
|
583
|
+
const KEY_SEPARATOR = "";
|
|
584
|
+
/** Stable key for one route, safe to use as a select-option value. */
|
|
585
|
+
function modelRouteKey(provider, model) {
|
|
586
|
+
return provider && model ? `${provider}${KEY_SEPARATOR}${model}` : "";
|
|
587
|
+
}
|
|
588
|
+
/** Inverse of modelRouteKey; undefined for malformed keys. */
|
|
589
|
+
function parseModelRouteKey(value) {
|
|
590
|
+
const index = value.indexOf(KEY_SEPARATOR);
|
|
591
|
+
if (index <= 0) return void 0;
|
|
592
|
+
const provider = value.slice(0, index);
|
|
593
|
+
const model = value.slice(index + 1);
|
|
594
|
+
if (!provider || !model) return void 0;
|
|
595
|
+
return {
|
|
596
|
+
provider,
|
|
597
|
+
model
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
//#endregion
|
|
601
|
+
//#region ../dsh-model-route/src/catalog.ts
|
|
602
|
+
/** Both services are required: Cordis checks the nested namespace separately. */
|
|
603
|
+
const MODEL_CATALOG_INJECT = ["remote", "remote.session"];
|
|
604
|
+
function asRecord(value) {
|
|
605
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
606
|
+
}
|
|
607
|
+
function asText(value) {
|
|
608
|
+
return typeof value === "string" && value.trim() ? value.trim() : void 0;
|
|
609
|
+
}
|
|
610
|
+
/** A useful diagnostic even when a transport rejects with a plain object. */
|
|
611
|
+
function modelCatalogErrorMessage(value) {
|
|
612
|
+
if (value instanceof Error && value.message) return value.message;
|
|
613
|
+
return asText(asRecord(value)?.message) ?? asText(value) ?? "Unable to load the model catalog.";
|
|
614
|
+
}
|
|
615
|
+
/**
|
|
616
|
+
* Validate and unwrap a successful catalog without discarding provider failures
|
|
617
|
+
* or newer host metadata. Names and metadata remain optional for older hosts.
|
|
618
|
+
* A broken response is never presented as a successfully loaded empty menu.
|
|
619
|
+
*/
|
|
620
|
+
function checkedModelCatalog(value) {
|
|
621
|
+
const envelope = asRecord(value);
|
|
622
|
+
if (envelope?.ok === false) throw new Error(modelCatalogErrorMessage(envelope.error ?? envelope));
|
|
623
|
+
const root = envelope?.ok === true ? asRecord(envelope.value) : envelope;
|
|
624
|
+
const malformed = (field) => {
|
|
625
|
+
throw new Error(`Invalid model catalog: ${field}.`);
|
|
626
|
+
};
|
|
627
|
+
if (!root || !Array.isArray(root.groups)) return malformed("groups must be an array");
|
|
628
|
+
for (const value of root.groups) {
|
|
629
|
+
const group = asRecord(value);
|
|
630
|
+
if (!group || !asText(group.id) || !Array.isArray(group.models)) return malformed("each group needs an id and models array");
|
|
631
|
+
for (const value of group.models) {
|
|
632
|
+
const model = asRecord(value);
|
|
633
|
+
if (!model || !asText(model.id)) return malformed("each model needs an id");
|
|
634
|
+
if (model.reasoning !== void 0) {
|
|
635
|
+
const reasoning = asRecord(model.reasoning);
|
|
636
|
+
if (!reasoning || !Array.isArray(reasoning.efforts) || reasoning.efforts.some((value) => !parseEffort(value))) return malformed("reasoning efforts must be an array of identified efforts");
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
if (root.failures !== void 0 && (!Array.isArray(root.failures) || root.failures.some((value) => {
|
|
641
|
+
const failure = asRecord(value);
|
|
642
|
+
return !failure || !asText(failure.id) || typeof failure.name !== "string" || typeof failure.message !== "string";
|
|
643
|
+
}))) return malformed("failures must be an array of provider failures");
|
|
644
|
+
return root;
|
|
645
|
+
}
|
|
646
|
+
/** Read through the injected session namespace; absence and rejection are errors. */
|
|
647
|
+
async function readModelCatalog(client) {
|
|
648
|
+
const session = client.remote?.session;
|
|
649
|
+
if (typeof session?.modelCatalog !== "function") throw new Error("Model catalog unavailable: inject both remote and remote.session.");
|
|
650
|
+
return checkedModelCatalog(await session.modelCatalog());
|
|
651
|
+
}
|
|
652
|
+
/** Provider failures are partial results, not a transport failure. */
|
|
653
|
+
function modelCatalogFailures(value) {
|
|
654
|
+
try {
|
|
655
|
+
return checkedModelCatalog(value).failures ?? [];
|
|
656
|
+
} catch {
|
|
657
|
+
return [];
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
function parseEffort(value) {
|
|
661
|
+
const row = asRecord(value);
|
|
662
|
+
const id = row ? asText(row.id) : void 0;
|
|
663
|
+
if (!id) return void 0;
|
|
664
|
+
return {
|
|
665
|
+
id,
|
|
666
|
+
name: row ? asText(row.name) ?? id : id
|
|
667
|
+
};
|
|
668
|
+
}
|
|
669
|
+
/** Parse the host session catalog into flat choices, keeping a saved route selectable. */
|
|
670
|
+
function parseModelMenuChoices(value, bound) {
|
|
671
|
+
const envelope = asRecord(value);
|
|
672
|
+
const root = (envelope && envelope.ok === true ? asRecord(envelope.value) : void 0) ?? envelope ?? {};
|
|
673
|
+
const groups = Array.isArray(root.groups) ? root.groups : [];
|
|
674
|
+
const choices = [];
|
|
675
|
+
for (const group of groups) {
|
|
676
|
+
const groupRow = asRecord(group);
|
|
677
|
+
const provider = groupRow ? asText(groupRow.id) : void 0;
|
|
678
|
+
if (!provider || !groupRow || !Array.isArray(groupRow.models)) continue;
|
|
679
|
+
const providerName = asText(groupRow.name) ?? provider;
|
|
680
|
+
for (const entry of groupRow.models) {
|
|
681
|
+
const modelRow = asRecord(entry);
|
|
682
|
+
const model = modelRow ? asText(modelRow.id) : void 0;
|
|
683
|
+
if (!model || !modelRow) continue;
|
|
684
|
+
const name = asText(modelRow.name) ?? model;
|
|
685
|
+
const reasoning = asRecord(modelRow.reasoning);
|
|
686
|
+
const efforts = reasoning && Array.isArray(reasoning.efforts) ? reasoning.efforts.flatMap((effort) => {
|
|
687
|
+
const parsed = parseEffort(effort);
|
|
688
|
+
return parsed ? [parsed] : [];
|
|
689
|
+
}) : [];
|
|
690
|
+
choices.push({
|
|
691
|
+
provider,
|
|
692
|
+
model,
|
|
693
|
+
label: `${providerName} / ${name}`,
|
|
694
|
+
efforts
|
|
695
|
+
});
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
if (bound?.provider && bound.model && !choices.some((item) => item.provider === bound.provider && item.model === bound.model)) choices.push({
|
|
699
|
+
provider: bound.provider,
|
|
700
|
+
model: bound.model,
|
|
701
|
+
label: `${bound.provider} / ${bound.model}`,
|
|
702
|
+
efforts: []
|
|
703
|
+
});
|
|
704
|
+
return choices;
|
|
705
|
+
}
|
|
706
|
+
//#endregion
|
|
707
|
+
//#region ../dsh-model-route/src/catalog-ui.tsx
|
|
708
|
+
/**
|
|
709
|
+
* Latest refresh wins, including retries while a previous request is pending.
|
|
710
|
+
* Keep load stable (for example with useCallback); changing it invalidates the
|
|
711
|
+
* previous source and fetches again, so switching clients cannot publish stale data.
|
|
712
|
+
*/
|
|
713
|
+
function useModelCatalog(load, options = {}) {
|
|
714
|
+
const enabled = options.enabled !== false;
|
|
715
|
+
const enabledRef = (0, react.useRef)(enabled);
|
|
716
|
+
enabledRef.current = enabled;
|
|
717
|
+
const live = (0, react.useRef)(false);
|
|
718
|
+
const source = (0, react.useRef)(load);
|
|
719
|
+
const revision = (0, react.useRef)(0);
|
|
720
|
+
const [state, setState] = (0, react.useState)(() => {
|
|
721
|
+
try {
|
|
722
|
+
return {
|
|
723
|
+
catalog: options.initialCatalog === void 0 ? void 0 : checkedModelCatalog(options.initialCatalog),
|
|
724
|
+
loading: enabled,
|
|
725
|
+
error: ""
|
|
726
|
+
};
|
|
727
|
+
} catch (error) {
|
|
728
|
+
return {
|
|
729
|
+
catalog: void 0,
|
|
730
|
+
loading: enabled,
|
|
731
|
+
error: modelCatalogErrorMessage(error)
|
|
732
|
+
};
|
|
733
|
+
}
|
|
734
|
+
});
|
|
735
|
+
const refresh = (0, react.useCallback)(async () => {
|
|
736
|
+
if (!live.current || !enabledRef.current) return;
|
|
737
|
+
const request = ++revision.current;
|
|
738
|
+
setState((previous) => ({
|
|
739
|
+
...previous,
|
|
740
|
+
loading: true,
|
|
741
|
+
error: ""
|
|
742
|
+
}));
|
|
743
|
+
try {
|
|
744
|
+
const catalog = checkedModelCatalog(await load());
|
|
745
|
+
if (live.current && request === revision.current) setState({
|
|
746
|
+
catalog,
|
|
747
|
+
loading: false,
|
|
748
|
+
error: ""
|
|
749
|
+
});
|
|
750
|
+
} catch (error) {
|
|
751
|
+
if (live.current && request === revision.current) setState((previous) => ({
|
|
752
|
+
...previous,
|
|
753
|
+
loading: false,
|
|
754
|
+
error: modelCatalogErrorMessage(error)
|
|
755
|
+
}));
|
|
756
|
+
}
|
|
757
|
+
}, [load, enabled]);
|
|
758
|
+
(0, react.useEffect)(() => {
|
|
759
|
+
live.current = true;
|
|
760
|
+
if (source.current !== load) {
|
|
761
|
+
source.current = load;
|
|
762
|
+
setState({
|
|
763
|
+
catalog: void 0,
|
|
764
|
+
loading: enabled,
|
|
765
|
+
error: ""
|
|
766
|
+
});
|
|
767
|
+
}
|
|
768
|
+
if (enabled) refresh();
|
|
769
|
+
else setState((previous) => ({
|
|
770
|
+
...previous,
|
|
771
|
+
loading: false
|
|
772
|
+
}));
|
|
773
|
+
return () => {
|
|
774
|
+
live.current = false;
|
|
775
|
+
++revision.current;
|
|
776
|
+
};
|
|
777
|
+
}, [enabled, refresh]);
|
|
778
|
+
(0, react.useEffect)(() => {
|
|
779
|
+
if (!enabled) return;
|
|
780
|
+
const reload = () => {
|
|
781
|
+
refresh();
|
|
782
|
+
};
|
|
783
|
+
if (typeof window !== "undefined") window.addEventListener("focus", reload);
|
|
784
|
+
let unsubscribe;
|
|
785
|
+
try {
|
|
786
|
+
unsubscribe = options.subscribe?.(reload);
|
|
787
|
+
} catch (error) {
|
|
788
|
+
setState((previous) => ({
|
|
789
|
+
...previous,
|
|
790
|
+
error: modelCatalogErrorMessage(error)
|
|
791
|
+
}));
|
|
792
|
+
}
|
|
793
|
+
return () => {
|
|
794
|
+
if (typeof window !== "undefined") window.removeEventListener("focus", reload);
|
|
795
|
+
unsubscribe?.();
|
|
796
|
+
};
|
|
797
|
+
}, [
|
|
798
|
+
enabled,
|
|
799
|
+
refresh,
|
|
800
|
+
options.subscribe
|
|
801
|
+
]);
|
|
802
|
+
const choices = (0, react.useMemo)(() => parseModelMenuChoices(state.catalog), [state.catalog]);
|
|
803
|
+
const failures = (0, react.useMemo)(() => state.catalog === void 0 ? [] : modelCatalogFailures(state.catalog), [state.catalog]);
|
|
804
|
+
return {
|
|
805
|
+
...state,
|
|
806
|
+
choices,
|
|
807
|
+
failures,
|
|
808
|
+
refresh
|
|
809
|
+
};
|
|
810
|
+
}
|
|
811
|
+
/** Also accepts a mapped state from a caller that owns broader async orchestration. */
|
|
812
|
+
function ModelCatalogStatus({ state, locale }) {
|
|
813
|
+
const zh = locale?.toLowerCase().startsWith("zh") === true;
|
|
814
|
+
const copy = zh ? {
|
|
815
|
+
loading: "正在加载模型…",
|
|
816
|
+
error: "模型列表加载失败",
|
|
817
|
+
partial: "部分提供商的模型未能加载",
|
|
818
|
+
retry: "重试"
|
|
819
|
+
} : {
|
|
820
|
+
loading: "Loading models…",
|
|
821
|
+
error: "Unable to load models",
|
|
822
|
+
partial: "Some providers could not load their models",
|
|
823
|
+
retry: "Retry"
|
|
824
|
+
};
|
|
825
|
+
if (!state.loading && !state.error && state.failures.length === 0) return null;
|
|
826
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
827
|
+
className: "dsh-model-catalog-status",
|
|
828
|
+
lang: zh ? "zh" : "en",
|
|
829
|
+
children: [
|
|
830
|
+
state.loading ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
831
|
+
role: "status",
|
|
832
|
+
children: copy.loading
|
|
833
|
+
}) : null,
|
|
834
|
+
state.error ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
|
|
835
|
+
role: "alert",
|
|
836
|
+
children: [
|
|
837
|
+
copy.error,
|
|
838
|
+
": ",
|
|
839
|
+
state.error
|
|
840
|
+
]
|
|
841
|
+
}) : null,
|
|
842
|
+
state.failures.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
843
|
+
role: "status",
|
|
844
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: copy.partial }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", { children: state.failures.map((failure, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", { children: [
|
|
845
|
+
failure.name || failure.id,
|
|
846
|
+
": ",
|
|
847
|
+
failure.message
|
|
848
|
+
] }, `${failure.id}:${index}`)) })]
|
|
849
|
+
}) : null,
|
|
850
|
+
state.error || state.failures.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
851
|
+
type: "button",
|
|
852
|
+
size: "sm",
|
|
853
|
+
disabled: state.loading,
|
|
854
|
+
onClick: () => {
|
|
855
|
+
state.refresh();
|
|
856
|
+
},
|
|
857
|
+
children: copy.retry
|
|
858
|
+
}) : null
|
|
859
|
+
]
|
|
860
|
+
});
|
|
861
|
+
}
|
|
862
|
+
//#endregion
|
|
863
|
+
//#region ../dsh-model-route/src/menu.ts
|
|
864
|
+
/** Localize standard effort ids only when the provider has not supplied a display name. */
|
|
865
|
+
function modelMenuEffortLabel(effort, locale) {
|
|
866
|
+
if (effort.name !== effort.id) return effort.name;
|
|
867
|
+
return {
|
|
868
|
+
off: ["关闭", "Off"],
|
|
869
|
+
none: ["无", "None"],
|
|
870
|
+
minimal: ["最低", "Minimal"],
|
|
871
|
+
low: ["低", "Low"],
|
|
872
|
+
medium: ["中", "Medium"],
|
|
873
|
+
high: ["高", "High"],
|
|
874
|
+
xhigh: ["极高", "Extra high"],
|
|
875
|
+
max: ["最高", "Maximum"],
|
|
876
|
+
ultra: ["超高", "Ultra"]
|
|
877
|
+
}[effort.id]?.[locale?.toLowerCase().startsWith("zh") ? 0 : 1] ?? effort.name;
|
|
878
|
+
}
|
|
879
|
+
/** The short model label shown inside a provider group. */
|
|
880
|
+
function modelMenuShortName(choice) {
|
|
881
|
+
const sep = choice.label.indexOf(" / ");
|
|
882
|
+
return sep > 0 ? choice.label.slice(sep + 3) : choice.model;
|
|
883
|
+
}
|
|
884
|
+
/** Group catalog choices by provider and filter them by a case-insensitive query. */
|
|
885
|
+
function groupModelMenuChoices(choices, query) {
|
|
886
|
+
const needle = query.trim().toLowerCase();
|
|
887
|
+
const matched = needle ? choices.filter((choice) => choice.label.toLowerCase().includes(needle) || choice.model.toLowerCase().includes(needle) || choice.provider.toLowerCase().includes(needle)) : choices;
|
|
888
|
+
const groups = [];
|
|
889
|
+
for (const choice of matched) {
|
|
890
|
+
const sep = choice.label.indexOf(" / ");
|
|
891
|
+
const name = sep > 0 ? choice.label.slice(0, sep) : choice.provider;
|
|
892
|
+
const last = groups.at(-1);
|
|
893
|
+
if (last && last.provider === choice.provider) last.items.push(choice);
|
|
894
|
+
else groups.push({
|
|
895
|
+
provider: choice.provider,
|
|
896
|
+
name,
|
|
897
|
+
items: [choice]
|
|
898
|
+
});
|
|
899
|
+
}
|
|
900
|
+
return groups;
|
|
901
|
+
}
|
|
902
|
+
//#endregion
|
|
903
|
+
//#region ../dsh-model-route/src/ui.tsx
|
|
904
|
+
/**
|
|
905
|
+
* ModelMenu: the standard editor of one model-route field — the visible half
|
|
906
|
+
* of the x-model-route contract. A feature plugin's settings page renders it
|
|
907
|
+
* for its own marked field; the model hub renders one per detected field.
|
|
908
|
+
*
|
|
909
|
+
* It is a plugin-side copy of the host's composer model picker
|
|
910
|
+
* (dsh-client-ui-model-selection's ModelSelect), rebuilt from the official
|
|
911
|
+
* primitives and the published bundle's behavior: one card that opens on the
|
|
912
|
+
* root pane (Model / Effort cells), drills into the provider-grouped,
|
|
913
|
+
* searchable model list or the effort list, and Escapes back before closing.
|
|
914
|
+
* Sticky group headings, subsequence ranking, highlight walking and focus
|
|
915
|
+
* hand-back all match the host. SYNC POINT: when the host's ModelSelect or
|
|
916
|
+
* ModelSelect.module.css changes, this copy must be re-transcribed; styles
|
|
917
|
+
* below may only reference --dsw-* theme tokens, never literal colors.
|
|
918
|
+
*
|
|
919
|
+
* It statically imports the primitives, so only browser clients should load
|
|
920
|
+
* this entry; host code and node tests use the core entry (`./index.ts`).
|
|
921
|
+
*/
|
|
922
|
+
/** Unplaced portal card: hidden but laid out at a fixed origin so measurements are real. */
|
|
923
|
+
const MEASURE_STYLE = {
|
|
924
|
+
visibility: "hidden",
|
|
925
|
+
left: 0,
|
|
926
|
+
top: 0
|
|
927
|
+
};
|
|
928
|
+
function ModelMenu(props) {
|
|
929
|
+
const language = props.locale?.toLowerCase().startsWith("zh") ? "zh" : "en";
|
|
930
|
+
const copy = language === "zh" ? {
|
|
931
|
+
model: "模型",
|
|
932
|
+
effort: "思考强度",
|
|
933
|
+
default: "默认",
|
|
934
|
+
search: "搜索模型…",
|
|
935
|
+
empty: "没有匹配的模型",
|
|
936
|
+
clear: "清除搜索"
|
|
937
|
+
} : {
|
|
938
|
+
model: "Model",
|
|
939
|
+
effort: "Reasoning",
|
|
940
|
+
default: "Default",
|
|
941
|
+
search: "Search models…",
|
|
942
|
+
empty: "No matching models",
|
|
943
|
+
clear: "Clear search"
|
|
944
|
+
};
|
|
945
|
+
const { open, onOpenChange, anchor, choices, selected, onPick, leading = [], onPickLeading, efforts = [], selectedEffort, onPickEffort, modelLabel = copy.model, modelValue = "—", effortLabel = copy.effort, effortValue, defaultEffortLabel = copy.default, searchPlaceholder = copy.search, emptyLabel = copy.empty, clearSearchLabel = copy.clear, className, listClassName } = props;
|
|
946
|
+
const rootRef = (0, react.useRef)(null);
|
|
947
|
+
const openRef = (0, react.useRef)(open);
|
|
948
|
+
openRef.current = open;
|
|
949
|
+
const pendingOpenRef = (0, react.useRef)(false);
|
|
950
|
+
const pendingRestoreFocus = (0, react.useRef)(false);
|
|
951
|
+
const [requestedOpen, setRequestedOpen] = (0, react.useState)(false);
|
|
952
|
+
const menuRef = (0, react.useRef)(null);
|
|
953
|
+
const groupsRef = (0, react.useRef)(null);
|
|
954
|
+
const searchRef = (0, react.useRef)(null);
|
|
955
|
+
const itemRefs = (0, react.useRef)([]);
|
|
956
|
+
const paneFocus = (0, react.useRef)(null);
|
|
957
|
+
const [pane, setPane] = (0, react.useState)("root");
|
|
958
|
+
const [query, setQuery] = (0, react.useState)("");
|
|
959
|
+
const [highlightedIndex, setHighlightedIndex] = (0, react.useState)(null);
|
|
960
|
+
const [menuPos, setMenuPos] = (0, react.useState)(null);
|
|
961
|
+
const id = (0, react.useId)();
|
|
962
|
+
const showSearch = choices.length > 4;
|
|
963
|
+
const effortRows = (0, react.useMemo)(() => [{
|
|
964
|
+
id: "",
|
|
965
|
+
name: defaultEffortLabel
|
|
966
|
+
}, ...efforts.map((effort) => ({
|
|
967
|
+
...effort,
|
|
968
|
+
name: modelMenuEffortLabel(effort, language)
|
|
969
|
+
}))], [
|
|
970
|
+
defaultEffortLabel,
|
|
971
|
+
efforts,
|
|
972
|
+
language
|
|
973
|
+
]);
|
|
974
|
+
const filteredGroups = (0, react.useMemo)(() => groupModelMenuChoices(choices, "").map((group) => ({
|
|
975
|
+
...group,
|
|
976
|
+
items: (0, _deepseek_ai_dsh_client_ui_primitives.rankByName)(group.items.map((choice) => ({
|
|
977
|
+
choice,
|
|
978
|
+
name: modelMenuShortName(choice),
|
|
979
|
+
label: choice.model
|
|
980
|
+
})), showSearch ? query.trim() : "")
|
|
981
|
+
})).filter((group) => group.items.length > 0), [
|
|
982
|
+
choices,
|
|
983
|
+
query,
|
|
984
|
+
showSearch
|
|
985
|
+
]);
|
|
986
|
+
const visibleModels = (0, react.useMemo)(() => filteredGroups.flatMap((group) => group.items.map((item) => item.choice)), [filteredGroups]);
|
|
987
|
+
const currentVisibleIndex = visibleModels.findIndex((choice) => choice.provider === selected?.provider && choice.model === selected.model);
|
|
988
|
+
const activeModelIndex = Math.min(highlightedIndex ?? Math.max(0, currentVisibleIndex), visibleModels.length - 1);
|
|
989
|
+
const changeOpen = (next) => {
|
|
990
|
+
if (openRef.current === next) return;
|
|
991
|
+
openRef.current = next;
|
|
992
|
+
onOpenChange(next);
|
|
993
|
+
};
|
|
994
|
+
const requestOpen = () => {
|
|
995
|
+
if (open || pendingOpenRef.current) return;
|
|
996
|
+
pendingOpenRef.current = true;
|
|
997
|
+
pendingRestoreFocus.current = false;
|
|
998
|
+
setRequestedOpen(true);
|
|
999
|
+
};
|
|
1000
|
+
const close = (restoreFocus = false) => {
|
|
1001
|
+
const active = document.activeElement;
|
|
1002
|
+
pendingRestoreFocus.current = restoreFocus && (active === document.body || rootRef.current?.contains(active) === true || menuRef.current?.contains(active) === true);
|
|
1003
|
+
changeOpen(false);
|
|
1004
|
+
};
|
|
1005
|
+
const drill = (next) => {
|
|
1006
|
+
setQuery("");
|
|
1007
|
+
setHighlightedIndex(null);
|
|
1008
|
+
paneFocus.current = "drill";
|
|
1009
|
+
setPane(next);
|
|
1010
|
+
};
|
|
1011
|
+
/** Leave a drilled pane for the root one, handing the keyboard back to its cell. */
|
|
1012
|
+
const back = (from) => {
|
|
1013
|
+
paneFocus.current = from;
|
|
1014
|
+
setPane("root");
|
|
1015
|
+
};
|
|
1016
|
+
const moveFocus = (offset) => {
|
|
1017
|
+
const items = itemRefs.current.filter((item) => item !== null && !item.disabled);
|
|
1018
|
+
if (!items.length) return;
|
|
1019
|
+
const active = items.findIndex((item) => item === document.activeElement);
|
|
1020
|
+
items[active === -1 ? offset > 0 ? 0 : items.length - 1 : (active + offset + items.length) % items.length]?.focus();
|
|
1021
|
+
};
|
|
1022
|
+
(0, react.useLayoutEffect)(() => {
|
|
1023
|
+
if (open || !pendingRestoreFocus.current) return;
|
|
1024
|
+
const active = document.activeElement;
|
|
1025
|
+
if (active !== document.body && !rootRef.current?.contains(active)) {
|
|
1026
|
+
pendingRestoreFocus.current = false;
|
|
1027
|
+
return;
|
|
1028
|
+
}
|
|
1029
|
+
const trigger = rootRef.current?.querySelector("button");
|
|
1030
|
+
if (!trigger || trigger.disabled) return;
|
|
1031
|
+
trigger.focus();
|
|
1032
|
+
if (document.activeElement === trigger) pendingRestoreFocus.current = false;
|
|
1033
|
+
});
|
|
1034
|
+
(0, react.useEffect)(() => {
|
|
1035
|
+
if (open || !pendingRestoreFocus.current) return;
|
|
1036
|
+
const cancelOutside = (event) => {
|
|
1037
|
+
if (event.target instanceof Node && !rootRef.current?.contains(event.target)) pendingRestoreFocus.current = false;
|
|
1038
|
+
};
|
|
1039
|
+
document.addEventListener("focusin", cancelOutside);
|
|
1040
|
+
document.addEventListener("pointerdown", cancelOutside);
|
|
1041
|
+
return () => {
|
|
1042
|
+
document.removeEventListener("focusin", cancelOutside);
|
|
1043
|
+
document.removeEventListener("pointerdown", cancelOutside);
|
|
1044
|
+
};
|
|
1045
|
+
});
|
|
1046
|
+
(0, react.useEffect)(() => {
|
|
1047
|
+
if (!requestedOpen) return;
|
|
1048
|
+
pendingOpenRef.current = false;
|
|
1049
|
+
setRequestedOpen(false);
|
|
1050
|
+
if (!open) changeOpen(true);
|
|
1051
|
+
}, [requestedOpen, open]);
|
|
1052
|
+
(0, react.useEffect)(() => {
|
|
1053
|
+
if (!open) return;
|
|
1054
|
+
setPane("root");
|
|
1055
|
+
setQuery("");
|
|
1056
|
+
setHighlightedIndex(null);
|
|
1057
|
+
}, [open]);
|
|
1058
|
+
(0, react.useEffect)(() => {
|
|
1059
|
+
if (!open) return;
|
|
1060
|
+
const closeOutside = (event) => {
|
|
1061
|
+
const target = event.target;
|
|
1062
|
+
if (rootRef.current?.contains(target) || menuRef.current?.contains(target)) return;
|
|
1063
|
+
changeOpen(false);
|
|
1064
|
+
};
|
|
1065
|
+
document.addEventListener("mousedown", closeOutside);
|
|
1066
|
+
return () => document.removeEventListener("mousedown", closeOutside);
|
|
1067
|
+
}, [open, onOpenChange]);
|
|
1068
|
+
(0, react.useEffect)(() => {
|
|
1069
|
+
const intent = paneFocus.current;
|
|
1070
|
+
if (!open || menuPos === null || intent === null) return;
|
|
1071
|
+
if (intent !== "drill" && pane !== "root") return;
|
|
1072
|
+
paneFocus.current = null;
|
|
1073
|
+
if (intent === "drill") {
|
|
1074
|
+
if (pane === "model" && showSearch) {
|
|
1075
|
+
searchRef.current?.focus();
|
|
1076
|
+
return;
|
|
1077
|
+
}
|
|
1078
|
+
(menuRef.current?.querySelector("[role=\"menuitemradio\"][aria-checked=\"true\"]:not([disabled])") ?? itemRefs.current.find((item) => item !== null && !item.disabled))?.focus();
|
|
1079
|
+
return;
|
|
1080
|
+
}
|
|
1081
|
+
itemRefs.current[intent === "effort" ? 1 : 0]?.focus();
|
|
1082
|
+
}, [
|
|
1083
|
+
open,
|
|
1084
|
+
pane,
|
|
1085
|
+
showSearch,
|
|
1086
|
+
menuPos
|
|
1087
|
+
]);
|
|
1088
|
+
(0, react.useEffect)(() => {
|
|
1089
|
+
const viewport = groupsRef.current;
|
|
1090
|
+
if (viewport === null) return;
|
|
1091
|
+
return (0, _deepseek_ai_dsh_client_ui_primitives.observeStickyMenuGroups)(viewport);
|
|
1092
|
+
}, [
|
|
1093
|
+
open,
|
|
1094
|
+
pane,
|
|
1095
|
+
filteredGroups
|
|
1096
|
+
]);
|
|
1097
|
+
(0, react.useLayoutEffect)(() => {
|
|
1098
|
+
if (open && pane === "model" && activeModelIndex >= 0) itemRefs.current[leading.length + activeModelIndex]?.scrollIntoView({ block: "nearest" });
|
|
1099
|
+
}, [
|
|
1100
|
+
open,
|
|
1101
|
+
pane,
|
|
1102
|
+
activeModelIndex,
|
|
1103
|
+
visibleModels,
|
|
1104
|
+
leading.length
|
|
1105
|
+
]);
|
|
1106
|
+
(0, react.useLayoutEffect)(() => {
|
|
1107
|
+
if (!open) {
|
|
1108
|
+
setMenuPos(null);
|
|
1109
|
+
return;
|
|
1110
|
+
}
|
|
1111
|
+
const place = () => {
|
|
1112
|
+
const rect = rootRef.current?.getBoundingClientRect();
|
|
1113
|
+
if (!rect) return;
|
|
1114
|
+
const margin = 12, lw = menuRef.current?.offsetWidth ?? 0, lh = menuRef.current?.offsetHeight ?? 0;
|
|
1115
|
+
let x = rect.right - lw, y = rect.top - 8 - lh;
|
|
1116
|
+
if (lw > 0) x = Math.min(Math.max(x, margin), window.innerWidth - lw - margin);
|
|
1117
|
+
if (lh > 0) y = Math.min(Math.max(y, margin), window.innerHeight - lh - margin);
|
|
1118
|
+
setMenuPos({
|
|
1119
|
+
left: x,
|
|
1120
|
+
top: y
|
|
1121
|
+
});
|
|
1122
|
+
};
|
|
1123
|
+
place();
|
|
1124
|
+
window.addEventListener("scroll", place, true);
|
|
1125
|
+
window.addEventListener("resize", place);
|
|
1126
|
+
return () => {
|
|
1127
|
+
window.removeEventListener("scroll", place, true);
|
|
1128
|
+
window.removeEventListener("resize", place);
|
|
1129
|
+
};
|
|
1130
|
+
}, [
|
|
1131
|
+
open,
|
|
1132
|
+
pane,
|
|
1133
|
+
choices,
|
|
1134
|
+
query
|
|
1135
|
+
]);
|
|
1136
|
+
function onKeyDown(event) {
|
|
1137
|
+
if (event.defaultPrevented || event.nativeEvent.isComposing) return;
|
|
1138
|
+
if (!open && [
|
|
1139
|
+
"Enter",
|
|
1140
|
+
" ",
|
|
1141
|
+
"ArrowDown",
|
|
1142
|
+
"ArrowUp"
|
|
1143
|
+
].includes(event.key)) {
|
|
1144
|
+
const button = event.target instanceof Element ? event.target.closest("button") : null;
|
|
1145
|
+
if (!(button instanceof HTMLButtonElement) || button.disabled || !rootRef.current?.contains(button)) return;
|
|
1146
|
+
event.preventDefault();
|
|
1147
|
+
paneFocus.current = "model";
|
|
1148
|
+
requestOpen();
|
|
1149
|
+
return;
|
|
1150
|
+
}
|
|
1151
|
+
if (event.key === "Escape" && open) {
|
|
1152
|
+
event.preventDefault();
|
|
1153
|
+
if (pane !== "root") back(pane);
|
|
1154
|
+
else close(true);
|
|
1155
|
+
return;
|
|
1156
|
+
}
|
|
1157
|
+
if (!open) return;
|
|
1158
|
+
if (pane === "model" && showSearch && (event.key === "ArrowDown" || event.key === "ArrowUp")) {
|
|
1159
|
+
event.preventDefault();
|
|
1160
|
+
if (visibleModels.length > 0) {
|
|
1161
|
+
setHighlightedIndex((activeModelIndex + (event.key === "ArrowDown" ? 1 : -1) + visibleModels.length) % visibleModels.length);
|
|
1162
|
+
searchRef.current?.focus();
|
|
1163
|
+
}
|
|
1164
|
+
return;
|
|
1165
|
+
}
|
|
1166
|
+
if (pane === "model" && showSearch && event.target instanceof HTMLInputElement && (event.key === "Enter" || event.key === "Tab" && !event.shiftKey)) {
|
|
1167
|
+
if (event.key === "Tab" && visibleModels.length === 0) return;
|
|
1168
|
+
event.preventDefault();
|
|
1169
|
+
const highlighted = visibleModels[activeModelIndex];
|
|
1170
|
+
if (highlighted) {
|
|
1171
|
+
onPick({
|
|
1172
|
+
provider: highlighted.provider,
|
|
1173
|
+
model: highlighted.model
|
|
1174
|
+
});
|
|
1175
|
+
close(true);
|
|
1176
|
+
}
|
|
1177
|
+
return;
|
|
1178
|
+
}
|
|
1179
|
+
if (event.key === "Tab") {
|
|
1180
|
+
if (event.shiftKey) {
|
|
1181
|
+
event.preventDefault();
|
|
1182
|
+
if (pane !== "root") back(pane);
|
|
1183
|
+
else close(true);
|
|
1184
|
+
return;
|
|
1185
|
+
}
|
|
1186
|
+
const focused = document.activeElement;
|
|
1187
|
+
const rows = itemRefs.current.filter((item) => item !== null);
|
|
1188
|
+
if (focused instanceof HTMLButtonElement && rows.includes(focused)) {
|
|
1189
|
+
event.preventDefault();
|
|
1190
|
+
focused.click();
|
|
1191
|
+
return;
|
|
1192
|
+
}
|
|
1193
|
+
if (pane === "model" && showSearch) {
|
|
1194
|
+
event.preventDefault();
|
|
1195
|
+
setHighlightedIndex(null);
|
|
1196
|
+
searchRef.current?.focus();
|
|
1197
|
+
}
|
|
1198
|
+
return;
|
|
1199
|
+
}
|
|
1200
|
+
if (event.key === "ArrowDown" || event.key === "ArrowUp") {
|
|
1201
|
+
event.preventDefault();
|
|
1202
|
+
moveFocus(event.key === "ArrowDown" ? 1 : -1);
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
function onBlur(event) {
|
|
1206
|
+
if (event.relatedTarget instanceof Node && (rootRef.current?.contains(event.relatedTarget) || menuRef.current?.contains(event.relatedTarget))) return;
|
|
1207
|
+
if (open) changeOpen(false);
|
|
1208
|
+
}
|
|
1209
|
+
itemRefs.current = [];
|
|
1210
|
+
let itemIndex = 0;
|
|
1211
|
+
let modelIndex = 0;
|
|
1212
|
+
const itemRef = () => {
|
|
1213
|
+
const at = itemIndex++;
|
|
1214
|
+
return (node) => {
|
|
1215
|
+
itemRefs.current[at] = node;
|
|
1216
|
+
};
|
|
1217
|
+
};
|
|
1218
|
+
const menu = open && typeof document !== "undefined" ? (0, react_dom.createPortal)(/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(_deepseek_ai_dsh_client_ui_primitives.MenuSurface, {
|
|
1219
|
+
ref: menuRef,
|
|
1220
|
+
id: `${id}-menu`,
|
|
1221
|
+
className: `dsh-model-menu-menu${listClassName ? ` ${listClassName}` : ""}`,
|
|
1222
|
+
style: menuPos ?? MEASURE_STYLE,
|
|
1223
|
+
role: pane === "model" ? "group" : "menu",
|
|
1224
|
+
"aria-label": modelLabel,
|
|
1225
|
+
lang: language,
|
|
1226
|
+
children: [
|
|
1227
|
+
pane === "root" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
1228
|
+
ref: itemRef(),
|
|
1229
|
+
type: "button",
|
|
1230
|
+
role: "menuitem",
|
|
1231
|
+
className: "dsh-model-menu-cell",
|
|
1232
|
+
onClick: () => drill("model"),
|
|
1233
|
+
children: [
|
|
1234
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1235
|
+
className: "dsh-model-menu-cellLabel",
|
|
1236
|
+
children: modelLabel
|
|
1237
|
+
}),
|
|
1238
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1239
|
+
className: "dsh-model-menu-cellValue",
|
|
1240
|
+
children: modelValue
|
|
1241
|
+
}),
|
|
1242
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronRightOutlineRegular, { className: "dsh-model-menu-cellChevron" })
|
|
1243
|
+
]
|
|
1244
|
+
}), efforts.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
1245
|
+
ref: itemRef(),
|
|
1246
|
+
type: "button",
|
|
1247
|
+
role: "menuitem",
|
|
1248
|
+
className: "dsh-model-menu-cell",
|
|
1249
|
+
onClick: () => drill("effort"),
|
|
1250
|
+
children: [
|
|
1251
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1252
|
+
className: "dsh-model-menu-cellLabel",
|
|
1253
|
+
children: effortLabel
|
|
1254
|
+
}),
|
|
1255
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1256
|
+
className: "dsh-model-menu-cellValue",
|
|
1257
|
+
children: effortValue ?? effortRows.find((row) => row.id === (selectedEffort ?? ""))?.name ?? defaultEffortLabel
|
|
1258
|
+
}),
|
|
1259
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronRightOutlineRegular, { className: "dsh-model-menu-cellChevron" })
|
|
1260
|
+
]
|
|
1261
|
+
}) : null] }) : null,
|
|
1262
|
+
pane === "model" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
1263
|
+
showSearch ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1264
|
+
className: "dsh-model-menu-searchRow",
|
|
1265
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Input, {
|
|
1266
|
+
ref: searchRef,
|
|
1267
|
+
className: `dsh-model-menu-search${query !== "" ? " dsh-model-menu-searchWithQuery" : ""}`,
|
|
1268
|
+
type: "text",
|
|
1269
|
+
role: "searchbox",
|
|
1270
|
+
"aria-label": searchPlaceholder,
|
|
1271
|
+
"aria-controls": `${id}-models`,
|
|
1272
|
+
"aria-activedescendant": activeModelIndex < 0 ? void 0 : `${id}-model-${activeModelIndex}`,
|
|
1273
|
+
placeholder: searchPlaceholder,
|
|
1274
|
+
value: query,
|
|
1275
|
+
onChange: (event) => {
|
|
1276
|
+
setQuery(event.target.value);
|
|
1277
|
+
setHighlightedIndex(0);
|
|
1278
|
+
}
|
|
1279
|
+
}), query !== "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1280
|
+
type: "button",
|
|
1281
|
+
className: "dsh-model-menu-searchClear",
|
|
1282
|
+
"aria-label": clearSearchLabel,
|
|
1283
|
+
onClick: () => {
|
|
1284
|
+
setQuery("");
|
|
1285
|
+
setHighlightedIndex(null);
|
|
1286
|
+
searchRef.current?.focus();
|
|
1287
|
+
},
|
|
1288
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCloseFillRegular, {})
|
|
1289
|
+
}) : null]
|
|
1290
|
+
}) : null,
|
|
1291
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1292
|
+
ref: groupsRef,
|
|
1293
|
+
id: `${id}-models`,
|
|
1294
|
+
className: "dsh-model-menu-groups scrollable",
|
|
1295
|
+
role: "menu",
|
|
1296
|
+
"aria-label": modelLabel,
|
|
1297
|
+
hidden: filteredGroups.length === 0,
|
|
1298
|
+
children: [leading.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1299
|
+
className: "dsh-model-menu-leading",
|
|
1300
|
+
children: leading.map((item) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
1301
|
+
ref: itemRef(),
|
|
1302
|
+
type: "button",
|
|
1303
|
+
role: "menuitemradio",
|
|
1304
|
+
"aria-checked": item.selected === true,
|
|
1305
|
+
className: `dsh-model-menu-option${item.selected === true ? " dsh-model-menu-selected" : ""}`,
|
|
1306
|
+
onClick: () => {
|
|
1307
|
+
onPickLeading?.(item.id);
|
|
1308
|
+
close(true);
|
|
1309
|
+
},
|
|
1310
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1311
|
+
className: "dsh-model-menu-optionCopy",
|
|
1312
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1313
|
+
className: "dsh-model-menu-modelName",
|
|
1314
|
+
children: item.label
|
|
1315
|
+
})
|
|
1316
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1317
|
+
className: "dsh-model-menu-check",
|
|
1318
|
+
children: item.selected === true ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCheckOutlineRegular, {}) : null
|
|
1319
|
+
})]
|
|
1320
|
+
}, item.id))
|
|
1321
|
+
}) : null, filteredGroups.map((group) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.MenuGroup, {
|
|
1322
|
+
label: group.name,
|
|
1323
|
+
children: group.items.map((item) => {
|
|
1324
|
+
const index = modelIndex++;
|
|
1325
|
+
const choice = item.choice;
|
|
1326
|
+
const checked = selected?.provider === choice.provider && selected.model === choice.model;
|
|
1327
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
1328
|
+
ref: itemRef(),
|
|
1329
|
+
type: "button",
|
|
1330
|
+
role: "menuitemradio",
|
|
1331
|
+
"aria-checked": checked,
|
|
1332
|
+
id: `${id}-model-${index}`,
|
|
1333
|
+
tabIndex: showSearch ? -1 : 0,
|
|
1334
|
+
onFocus: () => setHighlightedIndex(index),
|
|
1335
|
+
"data-highlighted": index === activeModelIndex ? "" : void 0,
|
|
1336
|
+
className: `dsh-model-menu-option dsh-model-menu-modelOption${checked ? " dsh-model-menu-selected" : ""}${index === activeModelIndex ? " dsh-model-menu-optionActive" : ""}`,
|
|
1337
|
+
onMouseMove: index === activeModelIndex ? void 0 : () => {
|
|
1338
|
+
if (showSearch) setHighlightedIndex(index);
|
|
1339
|
+
else itemRefs.current[leading.length + index]?.focus();
|
|
1340
|
+
},
|
|
1341
|
+
title: choice.label,
|
|
1342
|
+
onClick: () => {
|
|
1343
|
+
onPick({
|
|
1344
|
+
provider: choice.provider,
|
|
1345
|
+
model: choice.model
|
|
1346
|
+
});
|
|
1347
|
+
close(true);
|
|
1348
|
+
},
|
|
1349
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1350
|
+
className: "dsh-model-menu-optionCopy",
|
|
1351
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1352
|
+
className: "dsh-model-menu-modelName",
|
|
1353
|
+
children: modelMenuShortName(choice)
|
|
1354
|
+
})
|
|
1355
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1356
|
+
className: "dsh-model-menu-check",
|
|
1357
|
+
children: checked ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCheckOutlineRegular, {}) : null
|
|
1358
|
+
})]
|
|
1359
|
+
}, `${choice.provider}/${choice.model}`);
|
|
1360
|
+
})
|
|
1361
|
+
}, group.provider))]
|
|
1362
|
+
}),
|
|
1363
|
+
filteredGroups.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1364
|
+
className: "dsh-model-menu-empty",
|
|
1365
|
+
role: "status",
|
|
1366
|
+
children: emptyLabel
|
|
1367
|
+
}) : null
|
|
1368
|
+
] }) : null,
|
|
1369
|
+
pane === "effort" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, { children: effortRows.map((row) => {
|
|
1370
|
+
const checked = (selectedEffort ?? "") === row.id;
|
|
1371
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
1372
|
+
ref: itemRef(),
|
|
1373
|
+
type: "button",
|
|
1374
|
+
role: "menuitemradio",
|
|
1375
|
+
"aria-checked": checked,
|
|
1376
|
+
className: `dsh-model-menu-option${checked ? " dsh-model-menu-selected" : ""}`,
|
|
1377
|
+
onClick: () => {
|
|
1378
|
+
onPickEffort?.(row.id || void 0);
|
|
1379
|
+
close(true);
|
|
1380
|
+
},
|
|
1381
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1382
|
+
className: "dsh-model-menu-optionCopy",
|
|
1383
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1384
|
+
className: "dsh-model-menu-modelName",
|
|
1385
|
+
children: row.name
|
|
1386
|
+
})
|
|
1387
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1388
|
+
className: "dsh-model-menu-check",
|
|
1389
|
+
children: checked ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCheckOutlineRegular, {}) : null
|
|
1390
|
+
})]
|
|
1391
|
+
}, row.id || "__default");
|
|
1392
|
+
}) }) : null
|
|
1393
|
+
]
|
|
1394
|
+
}), document.body) : null;
|
|
1395
|
+
const trigger = (0, react.isValidElement)(anchor) ? (0, react.cloneElement)(anchor, {
|
|
1396
|
+
"aria-haspopup": "menu",
|
|
1397
|
+
"aria-expanded": open,
|
|
1398
|
+
"aria-controls": open ? `${id}-menu` : void 0
|
|
1399
|
+
}) : anchor;
|
|
1400
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1401
|
+
ref: rootRef,
|
|
1402
|
+
className: `dsh-model-menu-root${className ? ` ${className}` : ""}`,
|
|
1403
|
+
onKeyDown,
|
|
1404
|
+
onBlur,
|
|
1405
|
+
onClick: (event) => {
|
|
1406
|
+
if (event.defaultPrevented) return;
|
|
1407
|
+
const button = event.target instanceof Element ? event.target.closest("button") : null;
|
|
1408
|
+
if (!(button instanceof HTMLButtonElement) || button.disabled || !rootRef.current?.contains(button)) return;
|
|
1409
|
+
requestOpen();
|
|
1410
|
+
},
|
|
1411
|
+
onMouseDown: (event) => {
|
|
1412
|
+
if (event.target instanceof Element && menuRef.current?.contains(event.target) && event.target.closest("button") !== null) event.preventDefault();
|
|
1413
|
+
},
|
|
1414
|
+
children: [trigger, menu]
|
|
1415
|
+
});
|
|
1416
|
+
}
|
|
1417
|
+
/**
|
|
1418
|
+
* ModelMenu styles, transcribed from the official ModelSelect.module.css (the
|
|
1419
|
+
* published dsh-client-ui-model-selection bundle): card capped at
|
|
1420
|
+
* min(360px, 100vh - 96px), 34px option cells at 13/18 with 5px 7px padding,
|
|
1421
|
+
* 34px root cells at 13/20, a borderless 12px search row with a clear button,
|
|
1422
|
+
* sticky group headings (MenuGroup's own CSS), and the l1-stroke prominent
|
|
1423
|
+
* elevation at portal z-index 1100. The trigger mirrors the composer's 28px
|
|
1424
|
+
* model pill, chevron rotating open. MenuSurface paints fill and backdrop.
|
|
1425
|
+
* Only --dsw-* theme tokens may appear here — a literal color is a bug.
|
|
1426
|
+
*/
|
|
1427
|
+
const modelMenuCss = `
|
|
1428
|
+
.dsh-model-menu-root { min-width: 0; position: relative; display: inline-flex; }
|
|
1429
|
+
.dsh-model-menu-trigger {
|
|
1430
|
+
border-radius: var(--dsw-radius-sm);
|
|
1431
|
+
min-width: 0;
|
|
1432
|
+
max-width: min(360px, 45cqw);
|
|
1433
|
+
height: 28px;
|
|
1434
|
+
color: var(--dsw-alias-label-secondary);
|
|
1435
|
+
cursor: pointer;
|
|
1436
|
+
background: none;
|
|
1437
|
+
border: none;
|
|
1438
|
+
outline: none;
|
|
1439
|
+
align-items: center;
|
|
1440
|
+
gap: 4px;
|
|
1441
|
+
padding: 0 4px 0 8px;
|
|
1442
|
+
font-size: 13px;
|
|
1443
|
+
font-weight: 400;
|
|
1444
|
+
line-height: 20px;
|
|
1445
|
+
display: flex;
|
|
1446
|
+
}
|
|
1447
|
+
.dsh-model-menu-trigger:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover); }
|
|
1448
|
+
.dsh-model-menu-trigger:focus-visible:not([data-selection-focus]) { box-shadow: 0 0 0 2px var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary)); }
|
|
1449
|
+
.dsh-model-menu-trigger:disabled { color: var(--dsw-alias-label-dimmed); cursor: default; }
|
|
1450
|
+
.dsh-model-menu-triggerLabel { text-overflow: ellipsis; white-space: nowrap; min-width: 0; overflow: hidden; }
|
|
1451
|
+
.dsh-model-menu-triggerEffort { text-overflow: ellipsis; white-space: nowrap; min-width: 0; color: var(--dsw-alias-label-caption); flex-shrink: 1000; overflow: hidden; }
|
|
1452
|
+
.dsh-model-menu-chevron { color: var(--dsw-alias-label-caption); flex: none; transition: transform 0.12s; }
|
|
1453
|
+
.dsh-model-menu-chevronOpen { transform: rotate(180deg); }
|
|
1454
|
+
.dsh-model-menu-menu {
|
|
1455
|
+
z-index: 1100;
|
|
1456
|
+
--dsw-elevation-stroke-color: var(--dsw-alias-border-l1);
|
|
1457
|
+
width: max-content;
|
|
1458
|
+
min-width: min(240px, 100vw - 32px);
|
|
1459
|
+
max-width: min(420px, 100vw - 32px);
|
|
1460
|
+
max-height: min(360px, 100vh - 96px);
|
|
1461
|
+
box-shadow: var(--dsw-elevation-prominent);
|
|
1462
|
+
color: var(--dsw-alias-label-primary);
|
|
1463
|
+
--dsh-scrollbar-thumb: var(--dsw-alias-scrollbar-bg-l2);
|
|
1464
|
+
--dsh-scrollbar-thumb-hover: var(--dsw-alias-scrollbar-hover-l2);
|
|
1465
|
+
border: 0;
|
|
1466
|
+
flex-direction: column;
|
|
1467
|
+
padding: 4px;
|
|
1468
|
+
display: flex;
|
|
1469
|
+
position: fixed;
|
|
1470
|
+
overflow: hidden;
|
|
1471
|
+
}
|
|
1472
|
+
.dsh-model-menu-status, .dsh-model-menu-empty { color: var(--dsw-alias-label-tertiary); padding: 8px; font-size: 12px; line-height: 18px; }
|
|
1473
|
+
.dsh-model-menu-searchRow { flex-shrink: 0; margin: 2px 0 3px; position: relative; }
|
|
1474
|
+
.dsh-model-menu-searchRow .dsh-model-menu-search { border-radius: var(--dsw-radius-md); background: none; border: 0 solid transparent; height: auto; padding: 5px 7px; display: flex; }
|
|
1475
|
+
.dsh-model-menu-searchRow .dsh-model-menu-search:focus-within { border-color: transparent; }
|
|
1476
|
+
.dsh-model-menu-searchWithQuery { padding-right: 34px; }
|
|
1477
|
+
.dsh-model-menu-searchRow .dsh-model-menu-search input { padding: 0; font-size: 12px; line-height: normal; }
|
|
1478
|
+
.dsh-model-menu-searchRow .dsh-model-menu-search input::placeholder { color: var(--dsw-alias-label-caption); }
|
|
1479
|
+
.dsh-model-menu-searchClear {
|
|
1480
|
+
width: 24px;
|
|
1481
|
+
height: 24px;
|
|
1482
|
+
color: var(--dsw-alias-label-secondary);
|
|
1483
|
+
cursor: pointer;
|
|
1484
|
+
background: none;
|
|
1485
|
+
border: none;
|
|
1486
|
+
border-radius: 50%;
|
|
1487
|
+
justify-content: center;
|
|
1488
|
+
align-items: center;
|
|
1489
|
+
padding: 0;
|
|
1490
|
+
display: inline-flex;
|
|
1491
|
+
position: absolute;
|
|
1492
|
+
top: 50%;
|
|
1493
|
+
right: 4px;
|
|
1494
|
+
transform: translateY(-50%);
|
|
1495
|
+
}
|
|
1496
|
+
.dsh-model-menu-searchClear:hover, .dsh-model-menu-searchClear:focus-visible { background: var(--dsw-alias-interactive-bg-hover); outline: none; }
|
|
1497
|
+
.dsh-model-menu-groups { min-height: 0; overflow-y: auto; }
|
|
1498
|
+
.dsh-model-menu-leading { border-bottom: 0.5px solid var(--dsw-alias-border-l2); margin-bottom: 3px; padding-bottom: 3px; }
|
|
1499
|
+
.dsh-model-menu-option {
|
|
1500
|
+
box-sizing: border-box;
|
|
1501
|
+
border-radius: var(--dsw-radius-md);
|
|
1502
|
+
width: auto;
|
|
1503
|
+
min-width: 100%;
|
|
1504
|
+
min-height: 34px;
|
|
1505
|
+
color: inherit;
|
|
1506
|
+
text-align: left;
|
|
1507
|
+
cursor: pointer;
|
|
1508
|
+
background: none;
|
|
1509
|
+
border: none;
|
|
1510
|
+
outline: none;
|
|
1511
|
+
align-items: center;
|
|
1512
|
+
gap: 6px;
|
|
1513
|
+
padding: 5px 7px;
|
|
1514
|
+
display: flex;
|
|
1515
|
+
}
|
|
1516
|
+
.dsh-model-menu-option:not(.dsh-model-menu-modelOption):hover:not(:disabled),
|
|
1517
|
+
.dsh-model-menu-option:focus-visible,
|
|
1518
|
+
.dsh-model-menu-optionActive:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover); }
|
|
1519
|
+
.dsh-model-menu-modelOption:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover); }
|
|
1520
|
+
.dsh-model-menu-selected { background: none; }
|
|
1521
|
+
.dsh-model-menu-option:disabled { color: var(--dsw-alias-label-dimmed); cursor: default; }
|
|
1522
|
+
.dsh-model-menu-optionCopy { flex-direction: column; flex: 1; min-width: 0; display: flex; }
|
|
1523
|
+
.dsh-model-menu-modelName { color: inherit; text-overflow: ellipsis; white-space: nowrap; font-size: 13px; font-weight: 400; line-height: 18px; overflow: hidden; }
|
|
1524
|
+
.dsh-model-menu-check { color: var(--dsw-alias-label-primary); flex: 0 0 14px; place-items: center; display: grid; }
|
|
1525
|
+
.dsh-model-menu-check svg { width: 14px; height: 14px; }
|
|
1526
|
+
.dsh-model-menu-cell {
|
|
1527
|
+
box-sizing: border-box;
|
|
1528
|
+
border-radius: var(--dsw-radius-md);
|
|
1529
|
+
width: auto;
|
|
1530
|
+
min-width: 100%;
|
|
1531
|
+
height: 34px;
|
|
1532
|
+
color: var(--dsw-alias-label-primary);
|
|
1533
|
+
cursor: pointer;
|
|
1534
|
+
text-align: left;
|
|
1535
|
+
background: none;
|
|
1536
|
+
border: none;
|
|
1537
|
+
outline: none;
|
|
1538
|
+
align-items: center;
|
|
1539
|
+
gap: 6px;
|
|
1540
|
+
padding: 0 8px;
|
|
1541
|
+
font-size: 13px;
|
|
1542
|
+
line-height: 20px;
|
|
1543
|
+
display: flex;
|
|
1544
|
+
}
|
|
1545
|
+
.dsh-model-menu-cell:hover, .dsh-model-menu-cell:focus-visible { background: var(--dsw-alias-interactive-bg-hover); }
|
|
1546
|
+
.dsh-model-menu-cellLabel { white-space: nowrap; flex: none; }
|
|
1547
|
+
.dsh-model-menu-cellValue { text-overflow: ellipsis; white-space: nowrap; text-align: right; min-width: 0; color: var(--dsw-alias-label-tertiary); flex: auto; overflow: hidden; }
|
|
1548
|
+
.dsh-model-menu-cellChevron { width: 12px; height: 12px; color: var(--dsw-alias-menu-icon); flex: none; }
|
|
1549
|
+
`;
|
|
1550
|
+
//#endregion
|
|
545
1551
|
//#region src/styles.ts
|
|
546
1552
|
const FREE_CHAT_ROOT = "dsh-free-chat";
|
|
547
1553
|
/** Host tokens and official scaffolding. Only the edit draft, the inline action row,
|
|
548
1554
|
* and the alert line survive the panel removal; the modals reuse the root class. */
|
|
549
1555
|
const freeChatCss = `${officialUiCss(FREE_CHAT_ROOT)}
|
|
1556
|
+
${modelMenuCss}
|
|
550
1557
|
.${FREE_CHAT_ROOT} .dsh-free-draft {
|
|
551
|
-
box-sizing: border-box;
|
|
552
1558
|
width: 100%;
|
|
553
1559
|
min-height: 8em;
|
|
554
|
-
padding: 8px 12px;
|
|
555
|
-
border: 0.5px solid var(--dsw-alias-border-l4);
|
|
556
|
-
border-radius: var(--dsw-radius-md);
|
|
557
|
-
background: var(--dsw-alias-bg-layer-3);
|
|
558
|
-
color: var(--dsw-alias-label-primary);
|
|
559
|
-
font: inherit;
|
|
560
|
-
font-size: 14px;
|
|
561
|
-
line-height: 22px;
|
|
562
|
-
resize: vertical;
|
|
563
1560
|
}
|
|
564
1561
|
.${FREE_CHAT_ROOT} .dsh-free-actions {
|
|
565
1562
|
display: inline-flex;
|
|
@@ -568,6 +1565,82 @@ const freeChatCss = `${officialUiCss(FREE_CHAT_ROOT)}
|
|
|
568
1565
|
min-width: 0;
|
|
569
1566
|
}
|
|
570
1567
|
.${FREE_CHAT_ROOT} .dsh-free-alert { margin: 0; }
|
|
1568
|
+
/* Settings page: a feature is its toggle row plus the nested well that holds
|
|
1569
|
+
* the fields the switch gates. The well reuses the nested-card surface
|
|
1570
|
+
* (layer-2 on layer-1) so the grouping reads without another heading. */
|
|
1571
|
+
.${FREE_CHAT_ROOT} .dsm-feature + .dsm-feature {
|
|
1572
|
+
border-top: 0.5px solid var(--dsw-alias-border-l2);
|
|
1573
|
+
}
|
|
1574
|
+
.${FREE_CHAT_ROOT} .dsm-subs {
|
|
1575
|
+
display: flex;
|
|
1576
|
+
flex-direction: column;
|
|
1577
|
+
gap: 12px;
|
|
1578
|
+
margin: 0 0 4px;
|
|
1579
|
+
padding: 12px;
|
|
1580
|
+
border-radius: var(--dsw-radius-md);
|
|
1581
|
+
background: var(--dsw-alias-bg-layer-2);
|
|
1582
|
+
}
|
|
1583
|
+
.${FREE_CHAT_ROOT} .dsm-subs[data-disabled='true'] .dsh-ui-label {
|
|
1584
|
+
color: var(--dsw-alias-label-tertiary);
|
|
1585
|
+
}
|
|
1586
|
+
.${FREE_CHAT_ROOT} .dsm-minutes { width: 120px; }
|
|
1587
|
+
/* Welcome-back card: one flat full-width row at the latest turn's tail in the
|
|
1588
|
+
* chat flow — a single bordered surface with the recap laid out inline, no
|
|
1589
|
+
* nested box. */
|
|
1590
|
+
.${FREE_CHAT_ROOT}.dsh-welcome-card {
|
|
1591
|
+
box-sizing: border-box;
|
|
1592
|
+
width: 100%;
|
|
1593
|
+
margin: 8px 0 0;
|
|
1594
|
+
padding: 8px 12px;
|
|
1595
|
+
border: 0.5px solid var(--dsw-alias-border-l4);
|
|
1596
|
+
border-radius: var(--dsw-radius-md);
|
|
1597
|
+
background: var(--dsw-alias-bg-layer-3);
|
|
1598
|
+
}
|
|
1599
|
+
.${FREE_CHAT_ROOT} .dsh-welcome-card-head {
|
|
1600
|
+
display: flex;
|
|
1601
|
+
align-items: baseline;
|
|
1602
|
+
gap: 8px;
|
|
1603
|
+
margin: 0 0 4px;
|
|
1604
|
+
}
|
|
1605
|
+
.${FREE_CHAT_ROOT} .dsh-welcome-card-title { font-weight: 600; }
|
|
1606
|
+
.${FREE_CHAT_ROOT} .dsh-welcome-card-idle {
|
|
1607
|
+
flex: 1;
|
|
1608
|
+
min-width: 0;
|
|
1609
|
+
color: var(--dsw-alias-label-secondary);
|
|
1610
|
+
font-size: 12px;
|
|
1611
|
+
}
|
|
1612
|
+
.${FREE_CHAT_ROOT} .dsh-welcome-card-close {
|
|
1613
|
+
align-self: flex-start;
|
|
1614
|
+
margin-left: auto;
|
|
1615
|
+
}
|
|
1616
|
+
.${FREE_CHAT_ROOT} .dsh-welcome-card-actions {
|
|
1617
|
+
display: flex;
|
|
1618
|
+
justify-content: flex-start;
|
|
1619
|
+
flex-wrap: wrap;
|
|
1620
|
+
gap: 8px;
|
|
1621
|
+
margin: 8px 0 0;
|
|
1622
|
+
}
|
|
1623
|
+
.${FREE_CHAT_ROOT} .dsh-welcome-recap {
|
|
1624
|
+
margin: 0;
|
|
1625
|
+
padding: 0;
|
|
1626
|
+
border: none;
|
|
1627
|
+
}
|
|
1628
|
+
.${FREE_CHAT_ROOT} .dsh-welcome-recap dt {
|
|
1629
|
+
margin: 6px 0 0;
|
|
1630
|
+
font-size: 12px;
|
|
1631
|
+
color: var(--dsw-alias-label-tertiary);
|
|
1632
|
+
}
|
|
1633
|
+
.${FREE_CHAT_ROOT} .dsh-welcome-recap dt:first-child { margin-top: 0; }
|
|
1634
|
+
.${FREE_CHAT_ROOT} .dsh-welcome-recap dd {
|
|
1635
|
+
margin: 0;
|
|
1636
|
+
white-space: pre-wrap;
|
|
1637
|
+
word-break: break-word;
|
|
1638
|
+
}
|
|
1639
|
+
.${FREE_CHAT_ROOT} .dsh-welcome-cache {
|
|
1640
|
+
margin: 6px 0 0;
|
|
1641
|
+
color: var(--dsw-alias-label-secondary);
|
|
1642
|
+
}
|
|
1643
|
+
.${FREE_CHAT_ROOT} .dsh-welcome-done { margin: 6px 0 0; }
|
|
571
1644
|
`;
|
|
572
1645
|
//#endregion
|
|
573
1646
|
//#region src/ui-copy.ts
|
|
@@ -606,7 +1679,45 @@ const text = {
|
|
|
606
1679
|
edit: "编辑并重发",
|
|
607
1680
|
send: "发送",
|
|
608
1681
|
editTitle: "编辑并重发",
|
|
609
|
-
draft: "消息文本"
|
|
1682
|
+
draft: "消息文本",
|
|
1683
|
+
continueDefault: "继续",
|
|
1684
|
+
welcomeBack: "欢迎回来",
|
|
1685
|
+
recapSummary: "在聊什么",
|
|
1686
|
+
recapProgress: "目前进展",
|
|
1687
|
+
recapNextStep: "建议下一步",
|
|
1688
|
+
cacheNotice: "离开时间较久,模型的提示缓存可能已失效,下一次发送会重新处理完整上下文。可以压缩上下文,或在新会话中继续;也可以忽略提示直接发送。",
|
|
1689
|
+
compactNow: "压缩上下文",
|
|
1690
|
+
compacting: "正在压缩",
|
|
1691
|
+
compactCommandSent: "已调用宿主压缩命令,结果请查看对话中的压缩记录。",
|
|
1692
|
+
continueNew: "在新会话中继续",
|
|
1693
|
+
creatingContinuation: "正在创建新会话",
|
|
1694
|
+
continueNewHint: "在来源会话的工作区新建会话,将摘要和原会话 ID 填入草稿,不自动发送。",
|
|
1695
|
+
compactNothing: "没有值得压缩的内容。",
|
|
1696
|
+
settingsMode: "继续灰影",
|
|
1697
|
+
modeFixed: "固定文本",
|
|
1698
|
+
modeAuto: "模型生成",
|
|
1699
|
+
settingsModeHint: "输入框为空时显示的灰色继续文本:Tab 填入,点击发送。",
|
|
1700
|
+
continueTextLabel: "固定文本内容",
|
|
1701
|
+
continueTextPlaceholder: "留空使用默认「继续」",
|
|
1702
|
+
ghostModelLabel: "灰影模型",
|
|
1703
|
+
ghostModelHint: "只在上一轮结束后生成一次,并缓存在这个会话中;留空跟随会话或宿主默认。",
|
|
1704
|
+
welcomeBackGroup: "欢迎回来",
|
|
1705
|
+
recapEnabledLabel: "欢迎回来摘要",
|
|
1706
|
+
recapEnabledHint: "回到空闲的会话时,在对话顶部生成回顾卡片。",
|
|
1707
|
+
recapMinutesLabel: "摘要阈值(分钟)",
|
|
1708
|
+
compactEnabledLabel: "压缩建议",
|
|
1709
|
+
compactEnabledHint: "空闲较久时,回到会话会建议先压缩上下文。",
|
|
1710
|
+
compactMinutesLabel: "建议阈值(分钟)",
|
|
1711
|
+
modelLabel: "摘要模型",
|
|
1712
|
+
modelFollowOption: "当前会话 / 宿主默认模型",
|
|
1713
|
+
modelGroupHint: "只用于欢迎回来摘要;留空跟随会话或宿主默认。",
|
|
1714
|
+
saveSettings: "保存",
|
|
1715
|
+
settingsSaving: "正在保存",
|
|
1716
|
+
settingsSaved: "已保存",
|
|
1717
|
+
settingsLoading: "正在加载",
|
|
1718
|
+
settingsUnavailable: "当前宿主不支持在线修改配置",
|
|
1719
|
+
minutesInvalid: "请填写大于等于 1 的整数分钟。",
|
|
1720
|
+
externalChange: "配置已在别处修改。草稿已保留,请刷新页面后再保存。"
|
|
610
1721
|
},
|
|
611
1722
|
en: {
|
|
612
1723
|
tempWorkspace: "Temporary Workspace",
|
|
@@ -622,14 +1733,53 @@ const text = {
|
|
|
622
1733
|
edit: "Edit and resend",
|
|
623
1734
|
send: "Send",
|
|
624
1735
|
editTitle: "Edit and resend",
|
|
625
|
-
draft: "Message text"
|
|
1736
|
+
draft: "Message text",
|
|
1737
|
+
continueDefault: "Continue",
|
|
1738
|
+
welcomeBack: "Welcome back",
|
|
1739
|
+
recapSummary: "What you were discussing",
|
|
1740
|
+
recapProgress: "Where things stand",
|
|
1741
|
+
recapNextStep: "Suggested next step",
|
|
1742
|
+
cacheNotice: "The model prompt cache may have expired, so the next message reprocesses the full context. Compact the context, continue in a new session, or ignore this notice and send normally.",
|
|
1743
|
+
compactNow: "Compact context",
|
|
1744
|
+
compacting: "Compacting",
|
|
1745
|
+
compactCommandSent: "Host compaction command invoked. See the compaction entry in the conversation for its result.",
|
|
1746
|
+
continueNew: "Continue in a new session",
|
|
1747
|
+
creatingContinuation: "Creating new session",
|
|
1748
|
+
continueNewHint: "Create a session in the source session's workspace and fill its draft with the recap and source session ID, without sending.",
|
|
1749
|
+
compactNothing: "There is nothing useful to compact.",
|
|
1750
|
+
settingsMode: "Continue ghost",
|
|
1751
|
+
modeFixed: "Fixed text",
|
|
1752
|
+
modeAuto: "Model-generated",
|
|
1753
|
+
settingsModeHint: "The grey continuation shown on an empty composer: Tab fills it, clicking sends it.",
|
|
1754
|
+
continueTextLabel: "Fixed text",
|
|
1755
|
+
continueTextPlaceholder: "Empty uses the default \"Continue\"",
|
|
1756
|
+
ghostModelLabel: "Ghost model",
|
|
1757
|
+
ghostModelHint: "Generated once after the previous turn ends and cached for this session; empty follows the conversation or host default.",
|
|
1758
|
+
welcomeBackGroup: "Welcome back",
|
|
1759
|
+
recapEnabledLabel: "Welcome-back recap",
|
|
1760
|
+
recapEnabledHint: "Shows a recap card at the top of the conversation when you return to an idle session.",
|
|
1761
|
+
recapMinutesLabel: "Recap threshold (minutes)",
|
|
1762
|
+
compactEnabledLabel: "Compaction suggestion",
|
|
1763
|
+
compactEnabledHint: "Suggests compacting the context when you return after a longer idle stretch.",
|
|
1764
|
+
compactMinutesLabel: "Suggestion threshold (minutes)",
|
|
1765
|
+
modelLabel: "Recap model",
|
|
1766
|
+
modelFollowOption: "Current conversation / host default",
|
|
1767
|
+
modelGroupHint: "Used only for the welcome-back recap; empty follows the conversation or host default.",
|
|
1768
|
+
saveSettings: "Save",
|
|
1769
|
+
settingsSaving: "Saving",
|
|
1770
|
+
settingsSaved: "Saved",
|
|
1771
|
+
settingsLoading: "Loading",
|
|
1772
|
+
settingsUnavailable: "This runtime cannot edit the plugin configuration",
|
|
1773
|
+
minutesInvalid: "Enter a whole number of minutes of at least 1.",
|
|
1774
|
+
externalChange: "Settings changed elsewhere. The draft was kept; reload this page before saving."
|
|
626
1775
|
}
|
|
627
1776
|
};
|
|
628
1777
|
function uiText(locale, key) {
|
|
629
1778
|
return text[locale][key];
|
|
630
1779
|
}
|
|
631
1780
|
function failureReason(failure) {
|
|
632
|
-
if (failure.code === "session/lifecycle-unavailable" || failure.code === "session/fork-unavailable") return "unsupported";
|
|
1781
|
+
if (failure.code === "session/lifecycle-unavailable" || failure.code === "session/fork-unavailable" || failure.code === "session-manager/unavailable") return "unsupported";
|
|
1782
|
+
if (failure.code === "session-manager/busy") return "running";
|
|
633
1783
|
if (failure.code === "session/retry-revision" || failure.code === "session/branch-moved" || failure.code === "session/retry-input" || failure.code === "session/retry-conflict") return "stale";
|
|
634
1784
|
if (failure.code === "session/delete-pending") return "cleanup";
|
|
635
1785
|
if (failure.code === "session/agent-busy" && failure.reason === "free-session-work-active") return "running";
|
|
@@ -644,6 +1794,15 @@ function failureText(locale, failure) {
|
|
|
644
1794
|
function deleteBody(locale, classification) {
|
|
645
1795
|
return classification === "free" ? text[locale].deleteFree : text[locale].deleteOrdinary;
|
|
646
1796
|
}
|
|
1797
|
+
/** Idle duration sentence on the welcome-back card. */
|
|
1798
|
+
function idleText(locale, minutes) {
|
|
1799
|
+
return locale === "zh" ? `距离上次响应已约 ${minutes} 分钟。` : `About ${minutes} minutes have passed since the last response.`;
|
|
1800
|
+
}
|
|
1801
|
+
/** Compaction outcome line; a null token count means the host did not report one. */
|
|
1802
|
+
function compactedText(locale, tokens) {
|
|
1803
|
+
if (locale === "zh") return tokens === null ? "上下文已压缩。" : `上下文已压缩,约 ${tokens} tokens 的历史被折叠。`;
|
|
1804
|
+
return tokens === null ? "Context compacted." : `Context compacted; about ${tokens} tokens of history were folded.`;
|
|
1805
|
+
}
|
|
647
1806
|
//#endregion
|
|
648
1807
|
//#region src/free-panel.tsx
|
|
649
1808
|
function FreeChatIcon({ size }) {
|
|
@@ -709,6 +1868,7 @@ function DeleteChatDialog(props) {
|
|
|
709
1868
|
},
|
|
710
1869
|
footer: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
711
1870
|
variant: "ghost",
|
|
1871
|
+
"data-modal-autofocus": true,
|
|
712
1872
|
disabled: request.busy,
|
|
713
1873
|
onClick: () => {
|
|
714
1874
|
props.model.cancelDelete();
|
|
@@ -717,7 +1877,6 @@ function DeleteChatDialog(props) {
|
|
|
717
1877
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
718
1878
|
ref: confirmRef,
|
|
719
1879
|
variant: "primary",
|
|
720
|
-
"data-modal-autofocus": true,
|
|
721
1880
|
disabled: request.busy,
|
|
722
1881
|
onClick: () => {
|
|
723
1882
|
props.model.confirmDelete();
|
|
@@ -1194,7 +2353,7 @@ var SubmissionLedger = class {
|
|
|
1194
2353
|
}
|
|
1195
2354
|
}
|
|
1196
2355
|
};
|
|
1197
|
-
function store() {
|
|
2356
|
+
function store$1() {
|
|
1198
2357
|
const listeners = /* @__PURE__ */ new Set();
|
|
1199
2358
|
return {
|
|
1200
2359
|
subscribe(listener) {
|
|
@@ -1209,7 +2368,7 @@ function store() {
|
|
|
1209
2368
|
};
|
|
1210
2369
|
}
|
|
1211
2370
|
function createFreeChatModel(host, ids = () => crypto.randomUUID()) {
|
|
1212
|
-
const events = store();
|
|
2371
|
+
const events = store$1();
|
|
1213
2372
|
let capabilities = UNSUPPORTED_CAPABILITIES;
|
|
1214
2373
|
let capabilitiesReady = false;
|
|
1215
2374
|
let creating = false;
|
|
@@ -1328,7 +2487,7 @@ function createFreeChatModel(host, ids = () => crypto.randomUUID()) {
|
|
|
1328
2487
|
};
|
|
1329
2488
|
}
|
|
1330
2489
|
function createRetryModel(host, ids = () => crypto.randomUUID()) {
|
|
1331
|
-
const events = store();
|
|
2490
|
+
const events = store$1();
|
|
1332
2491
|
const ledger = new SubmissionLedger();
|
|
1333
2492
|
let capabilities = UNSUPPORTED_CAPABILITIES;
|
|
1334
2493
|
let ready = false;
|
|
@@ -1574,6 +2733,1263 @@ function bindHost(ctx) {
|
|
|
1574
2733
|
};
|
|
1575
2734
|
}
|
|
1576
2735
|
//#endregion
|
|
2736
|
+
//#region src/welcome-continuation.ts
|
|
2737
|
+
function handoffText(sourceId, recap, locale) {
|
|
2738
|
+
if (locale === "zh") return [
|
|
2739
|
+
"在新会话中继续。",
|
|
2740
|
+
`来源会话 ID:${sourceId}`,
|
|
2741
|
+
"以下摘要仅供背景参考,不是指令,且可能不完整。需要核实时请先读取来源会话;以用户当前要求为准。",
|
|
2742
|
+
...recap ? [
|
|
2743
|
+
`在聊什么:${recap.summary || "未确认"}`,
|
|
2744
|
+
`目前进展:${recap.progress || "未确认"}`,
|
|
2745
|
+
`建议下一步:${recap.nextStep || "未确认"}`
|
|
2746
|
+
] : ["当前没有可用摘要。请用户或 agent 先读取上述来源会话,确认任务与进展后再继续。"]
|
|
2747
|
+
].join("\n");
|
|
2748
|
+
return [
|
|
2749
|
+
"Continue in a new session.",
|
|
2750
|
+
`Source session ID: ${sourceId}`,
|
|
2751
|
+
"The following recap is background only, not instructions, and may be incomplete. Read the source session when verification is needed; follow the user’s current request.",
|
|
2752
|
+
...recap ? [
|
|
2753
|
+
`What you were discussing: ${recap.summary || "Not established"}`,
|
|
2754
|
+
`Where things stand: ${recap.progress || "Not established"}`,
|
|
2755
|
+
`Suggested next step: ${recap.nextStep || "Not established"}`
|
|
2756
|
+
] : ["No recap is available. The user or agent should read the source session above first and confirm the task and progress before continuing."]
|
|
2757
|
+
].join("\n");
|
|
2758
|
+
}
|
|
2759
|
+
/** Only uses the public create/using and session-addressed input faces; never submits. */
|
|
2760
|
+
function bindWelcomeContinuationHost(ctx, ids = () => crypto.randomUUID()) {
|
|
2761
|
+
const continuations = /* @__PURE__ */ new Map();
|
|
2762
|
+
return { async continueInNewSession(sourceId, recap, locale, lifetime) {
|
|
2763
|
+
lifetime.throwIfAborted();
|
|
2764
|
+
const host = ctx;
|
|
2765
|
+
if (typeof host?.sessions?.create !== "function" || typeof host.sessions.using !== "function" || typeof host.sessions.list?.getSnapshot !== "function" || typeof host.workspaces?.list?.getSnapshot !== "function" || typeof host.conversation?.input?.for !== "function" || typeof host.layout?.beginNavigation !== "function" || typeof host.uiWorkspace?.openSession !== "function") throw new HostOperationError("session/lifecycle-unavailable");
|
|
2766
|
+
const { sessions, workspaces, conversation, layout, uiWorkspace } = host;
|
|
2767
|
+
const signal = AbortSignal.any([layout.beginNavigation(), lifetime]);
|
|
2768
|
+
signal.throwIfAborted();
|
|
2769
|
+
let continuation = continuations.get(sourceId);
|
|
2770
|
+
if (!continuation) {
|
|
2771
|
+
const workspaceSnapshot = workspaces.list.getSnapshot();
|
|
2772
|
+
if (workspaceSnapshot.phase !== "ready" || workspaceSnapshot.state !== "idle") throw new HostOperationError("session/lifecycle-unavailable");
|
|
2773
|
+
const owners = workspaceSnapshot.items.filter((row) => row.sessionIds.includes(sourceId));
|
|
2774
|
+
if (owners.length > 1) throw new HostOperationError("session/lifecycle-unavailable");
|
|
2775
|
+
const owner = owners[0];
|
|
2776
|
+
let location;
|
|
2777
|
+
if (owner !== void 0) {
|
|
2778
|
+
if (!owner.workspaceId.trim()) throw new HostOperationError("session/lifecycle-unavailable");
|
|
2779
|
+
location = { workspaceId: owner.workspaceId };
|
|
2780
|
+
} else {
|
|
2781
|
+
const cwd = sessions.list.getSnapshot()?.byId?.[sourceId]?.cwd;
|
|
2782
|
+
if (typeof cwd !== "string" || !cwd.trim()) throw new HostOperationError("session/lifecycle-unavailable");
|
|
2783
|
+
location = { cwd };
|
|
2784
|
+
}
|
|
2785
|
+
const id = ids();
|
|
2786
|
+
if (!id || id === sourceId) throw new HostOperationError("session/lifecycle-unavailable");
|
|
2787
|
+
continuation = {
|
|
2788
|
+
id,
|
|
2789
|
+
location,
|
|
2790
|
+
handoff: handoffText(sourceId, recap, locale),
|
|
2791
|
+
created: false,
|
|
2792
|
+
seeded: false
|
|
2793
|
+
};
|
|
2794
|
+
continuations.set(sourceId, continuation);
|
|
2795
|
+
}
|
|
2796
|
+
const target = continuation;
|
|
2797
|
+
if (!target.created) {
|
|
2798
|
+
target.creating ??= (async () => {
|
|
2799
|
+
if (await sessions.create({
|
|
2800
|
+
...target.location,
|
|
2801
|
+
sessionId: target.id
|
|
2802
|
+
}) !== target.id) throw new HostOperationError("session/lifecycle-unavailable");
|
|
2803
|
+
target.created = true;
|
|
2804
|
+
})().finally(() => {
|
|
2805
|
+
target.creating = void 0;
|
|
2806
|
+
});
|
|
2807
|
+
await target.creating;
|
|
2808
|
+
}
|
|
2809
|
+
signal.throwIfAborted();
|
|
2810
|
+
await sessions.using(target.id, {
|
|
2811
|
+
source: "controllerOperation",
|
|
2812
|
+
signal
|
|
2813
|
+
}, async (reference) => {
|
|
2814
|
+
const binding = await reference.ready;
|
|
2815
|
+
signal.throwIfAborted();
|
|
2816
|
+
if (!binding?.ctx) throw new HostOperationError("session/lifecycle-unavailable");
|
|
2817
|
+
const input = conversation.input.for(binding.ctx);
|
|
2818
|
+
if (typeof input?.setDraft !== "function" || typeof input.state?.getSnapshot !== "function") throw new HostOperationError("session/lifecycle-unavailable");
|
|
2819
|
+
const state = input.state.getSnapshot();
|
|
2820
|
+
if (typeof state?.draft !== "string" || !Array.isArray(state.attachmentIds)) throw new HostOperationError("session/lifecycle-unavailable");
|
|
2821
|
+
signal.throwIfAborted();
|
|
2822
|
+
if (state.attachmentIds.length || state.draft !== "" && !(target.seeded && state.draft === target.handoff)) throw new HostOperationError("session/retry-conflict", "modified");
|
|
2823
|
+
if (!target.seeded || target.seededInput !== input) {
|
|
2824
|
+
input.setDraft(target.handoff);
|
|
2825
|
+
target.seeded = true;
|
|
2826
|
+
target.seededInput = input;
|
|
2827
|
+
}
|
|
2828
|
+
signal.throwIfAborted();
|
|
2829
|
+
await uiWorkspace.openSession(target.id);
|
|
2830
|
+
});
|
|
2831
|
+
if (continuations.get(sourceId) === target) continuations.delete(sourceId);
|
|
2832
|
+
} };
|
|
2833
|
+
}
|
|
2834
|
+
//#endregion
|
|
2835
|
+
//#region src/presence-model.ts
|
|
2836
|
+
/**
|
|
2837
|
+
* Client models for the continue ghost and the welcome-back card.
|
|
2838
|
+
* Business state lives here, not in React state, mirroring session-actions.ts.
|
|
2839
|
+
*/
|
|
2840
|
+
/** Narrow the plugin client's connection face without redeclaring the Context type. */
|
|
2841
|
+
function bindPresenceHost(ctx) {
|
|
2842
|
+
const client = ctx;
|
|
2843
|
+
return {
|
|
2844
|
+
...bindWelcomeContinuationHost(ctx),
|
|
2845
|
+
...typeof client.connection?.rpc?.call === "function" ? { rpc: client.connection.rpc } : {},
|
|
2846
|
+
...typeof client.sessions?.using === "function" ? { compactContext: async (sessionId, signal) => {
|
|
2847
|
+
await client.sessions.using(sessionId, {
|
|
2848
|
+
source: "controllerOperation",
|
|
2849
|
+
signal
|
|
2850
|
+
}, async (reference) => {
|
|
2851
|
+
const binding = await reference.ready;
|
|
2852
|
+
signal.throwIfAborted();
|
|
2853
|
+
if ((await unwrap(binding.session.command("/compact")))?.matched !== true) throw new HostOperationError("session-manager/unavailable");
|
|
2854
|
+
});
|
|
2855
|
+
} } : {}
|
|
2856
|
+
};
|
|
2857
|
+
}
|
|
2858
|
+
function requireRpc(host) {
|
|
2859
|
+
if (typeof host.rpc?.call !== "function") throw new HostOperationError("session/lifecycle-unavailable");
|
|
2860
|
+
return host.rpc;
|
|
2861
|
+
}
|
|
2862
|
+
async function callPresence(host, endpoint, payload, signal) {
|
|
2863
|
+
return await unwrap(requireRpc(host).call(TEMP_WORKSPACE_CHANNEL, endpoint, payload, signal));
|
|
2864
|
+
}
|
|
2865
|
+
function store() {
|
|
2866
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
2867
|
+
return {
|
|
2868
|
+
subscribe(listener) {
|
|
2869
|
+
listeners.add(listener);
|
|
2870
|
+
return () => {
|
|
2871
|
+
listeners.delete(listener);
|
|
2872
|
+
};
|
|
2873
|
+
},
|
|
2874
|
+
emit() {
|
|
2875
|
+
for (const listener of listeners) listener();
|
|
2876
|
+
}
|
|
2877
|
+
};
|
|
2878
|
+
}
|
|
2879
|
+
const EMPTY_CONTINUE = {};
|
|
2880
|
+
function createContinueModel(host) {
|
|
2881
|
+
const events = store();
|
|
2882
|
+
const states = /* @__PURE__ */ new Map();
|
|
2883
|
+
const inflight = /* @__PURE__ */ new Map();
|
|
2884
|
+
const lifetime = new AbortController();
|
|
2885
|
+
let empty = EMPTY_CONTINUE;
|
|
2886
|
+
function read(sessionId) {
|
|
2887
|
+
return states.get(sessionId) ?? empty;
|
|
2888
|
+
}
|
|
2889
|
+
function write(sessionId, next) {
|
|
2890
|
+
if (lifetime.signal.aborted) return;
|
|
2891
|
+
states.set(sessionId, next);
|
|
2892
|
+
events.emit();
|
|
2893
|
+
}
|
|
2894
|
+
return {
|
|
2895
|
+
subscribe: events.subscribe,
|
|
2896
|
+
getSnapshot: read,
|
|
2897
|
+
load(sessionId) {
|
|
2898
|
+
if (sessionId === "" || lifetime.signal.aborted || inflight.has(sessionId)) return;
|
|
2899
|
+
const current = read(sessionId);
|
|
2900
|
+
if (current.text !== void 0 && current.seq === void 0) return;
|
|
2901
|
+
const ticket = new AbortController();
|
|
2902
|
+
inflight.set(sessionId, ticket);
|
|
2903
|
+
const signal = AbortSignal.any([lifetime.signal, ticket.signal]);
|
|
2904
|
+
callContinueSuggest(host, sessionId, current.seq, signal).then((value) => {
|
|
2905
|
+
if (signal.aborted || inflight.get(sessionId) !== ticket) return;
|
|
2906
|
+
write(sessionId, value);
|
|
2907
|
+
}).catch(() => {}).finally(() => {
|
|
2908
|
+
if (inflight.get(sessionId) === ticket) inflight.delete(sessionId);
|
|
2909
|
+
});
|
|
2910
|
+
},
|
|
2911
|
+
invalidate(sessionId) {
|
|
2912
|
+
if (sessionId === "") return;
|
|
2913
|
+
inflight.get(sessionId)?.abort();
|
|
2914
|
+
inflight.delete(sessionId);
|
|
2915
|
+
if (!states.has(sessionId)) return;
|
|
2916
|
+
states.delete(sessionId);
|
|
2917
|
+
events.emit();
|
|
2918
|
+
},
|
|
2919
|
+
invalidateAll() {
|
|
2920
|
+
for (const ticket of inflight.values()) ticket.abort();
|
|
2921
|
+
inflight.clear();
|
|
2922
|
+
states.clear();
|
|
2923
|
+
empty = {};
|
|
2924
|
+
events.emit();
|
|
2925
|
+
},
|
|
2926
|
+
cancel() {
|
|
2927
|
+
lifetime.abort();
|
|
2928
|
+
for (const ticket of inflight.values()) ticket.abort();
|
|
2929
|
+
inflight.clear();
|
|
2930
|
+
states.clear();
|
|
2931
|
+
}
|
|
2932
|
+
};
|
|
2933
|
+
}
|
|
2934
|
+
async function callContinueSuggest(host, sessionId, knownSeq, signal) {
|
|
2935
|
+
const value = await callPresence(host, CONTINUE_SUGGEST_ENDPOINT, {
|
|
2936
|
+
sessionId,
|
|
2937
|
+
...knownSeq === void 0 ? {} : { knownSeq }
|
|
2938
|
+
}, signal);
|
|
2939
|
+
const text = typeof value?.text === "string" && value.text.trim() !== "" ? value.text : null;
|
|
2940
|
+
const seq = typeof value?.seq === "number" && Number.isSafeInteger(value.seq) && value.seq >= 0 ? value.seq : void 0;
|
|
2941
|
+
return {
|
|
2942
|
+
text,
|
|
2943
|
+
...seq === void 0 ? {} : { seq }
|
|
2944
|
+
};
|
|
2945
|
+
}
|
|
2946
|
+
function createPresenceConfigModel(host, delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms))) {
|
|
2947
|
+
const events = store();
|
|
2948
|
+
const lifetime = new AbortController();
|
|
2949
|
+
let phase = "loading";
|
|
2950
|
+
let values;
|
|
2951
|
+
let saving = false;
|
|
2952
|
+
let saved = false;
|
|
2953
|
+
let failure;
|
|
2954
|
+
let snapshot = build();
|
|
2955
|
+
let readGeneration = 0;
|
|
2956
|
+
function build() {
|
|
2957
|
+
return {
|
|
2958
|
+
phase,
|
|
2959
|
+
...values === void 0 ? {} : { values },
|
|
2960
|
+
saving,
|
|
2961
|
+
saved,
|
|
2962
|
+
...failure === void 0 ? {} : { failure }
|
|
2963
|
+
};
|
|
2964
|
+
}
|
|
2965
|
+
function publish() {
|
|
2966
|
+
snapshot = build();
|
|
2967
|
+
events.emit();
|
|
2968
|
+
}
|
|
2969
|
+
function sameRoute(current, value) {
|
|
2970
|
+
const route = value;
|
|
2971
|
+
if (current === null || current === void 0) return route === null || route === void 0;
|
|
2972
|
+
return route !== null && route !== void 0 && current.provider === route.provider && current.model === route.model && current.reasoningEffort === route.reasoningEffort;
|
|
2973
|
+
}
|
|
2974
|
+
function same(view, patch) {
|
|
2975
|
+
if (view === void 0) return false;
|
|
2976
|
+
return Object.entries(patch).every(([key, value]) => {
|
|
2977
|
+
if (key === "model" || key === "ghostModel") return sameRoute(view[key], value);
|
|
2978
|
+
return view[key] === value;
|
|
2979
|
+
});
|
|
2980
|
+
}
|
|
2981
|
+
return {
|
|
2982
|
+
subscribe: events.subscribe,
|
|
2983
|
+
getSnapshot: () => snapshot,
|
|
2984
|
+
async load() {
|
|
2985
|
+
const generation = ++readGeneration;
|
|
2986
|
+
try {
|
|
2987
|
+
const view = await callPresence(host, CONFIG_READ_ENDPOINT, {}, lifetime.signal);
|
|
2988
|
+
if (lifetime.signal.aborted || generation !== readGeneration) return;
|
|
2989
|
+
values = view;
|
|
2990
|
+
phase = "ready";
|
|
2991
|
+
failure = void 0;
|
|
2992
|
+
} catch (error) {
|
|
2993
|
+
if (lifetime.signal.aborted || generation !== readGeneration) return;
|
|
2994
|
+
phase = "unavailable";
|
|
2995
|
+
failure = readFailure(error);
|
|
2996
|
+
}
|
|
2997
|
+
publish();
|
|
2998
|
+
},
|
|
2999
|
+
async save(patch) {
|
|
3000
|
+
if (saving || lifetime.signal.aborted) return;
|
|
3001
|
+
saving = true;
|
|
3002
|
+
++readGeneration;
|
|
3003
|
+
saved = false;
|
|
3004
|
+
failure = void 0;
|
|
3005
|
+
publish();
|
|
3006
|
+
try {
|
|
3007
|
+
await callPresence(host, CONFIG_WRITE_ENDPOINT, { patch }, lifetime.signal);
|
|
3008
|
+
} catch {}
|
|
3009
|
+
try {
|
|
3010
|
+
for (let attempt = 0; attempt < 10 && !lifetime.signal.aborted; attempt++) {
|
|
3011
|
+
await delay(400);
|
|
3012
|
+
let view;
|
|
3013
|
+
try {
|
|
3014
|
+
view = await callPresence(host, CONFIG_READ_ENDPOINT, {}, lifetime.signal);
|
|
3015
|
+
} catch (error) {
|
|
3016
|
+
if (lifetime.signal.aborted || attempt === 9) throw error;
|
|
3017
|
+
continue;
|
|
3018
|
+
}
|
|
3019
|
+
if (same(view, patch)) {
|
|
3020
|
+
++readGeneration;
|
|
3021
|
+
values = view;
|
|
3022
|
+
phase = "ready";
|
|
3023
|
+
saved = true;
|
|
3024
|
+
saving = false;
|
|
3025
|
+
publish();
|
|
3026
|
+
return;
|
|
3027
|
+
}
|
|
3028
|
+
}
|
|
3029
|
+
throw new HostOperationError("transport", "unknown");
|
|
3030
|
+
} catch (error) {
|
|
3031
|
+
if (lifetime.signal.aborted) return;
|
|
3032
|
+
saving = false;
|
|
3033
|
+
failure = readFailure(error);
|
|
3034
|
+
publish();
|
|
3035
|
+
}
|
|
3036
|
+
},
|
|
3037
|
+
cancel() {
|
|
3038
|
+
lifetime.abort();
|
|
3039
|
+
}
|
|
3040
|
+
};
|
|
3041
|
+
}
|
|
3042
|
+
const EMPTY_WELCOME_BACK = { compactBusy: false };
|
|
3043
|
+
function createWelcomeBackModel(host, ids = () => crypto.randomUUID(), minIntervalMs = 1e4) {
|
|
3044
|
+
const events = store();
|
|
3045
|
+
const lifetime = new AbortController();
|
|
3046
|
+
/** Sessions already answered for one exact lastResponseAt; new activity re-arms the card. */
|
|
3047
|
+
const settled = /* @__PURE__ */ new Map();
|
|
3048
|
+
const checkedAt = /* @__PURE__ */ new Map();
|
|
3049
|
+
const capMarks = (map) => {
|
|
3050
|
+
while (map.size > 64) map.delete(map.keys().next().value);
|
|
3051
|
+
};
|
|
3052
|
+
/** Per-session card plus its live compact operation fields. */
|
|
3053
|
+
const states = /* @__PURE__ */ new Map();
|
|
3054
|
+
/** Stable snapshot per session, rebuilt only when its fields change. */
|
|
3055
|
+
const snapshots = /* @__PURE__ */ new Map();
|
|
3056
|
+
const inflight = /* @__PURE__ */ new Map();
|
|
3057
|
+
function read(sessionId) {
|
|
3058
|
+
return snapshots.get(sessionId) ?? EMPTY_WELCOME_BACK;
|
|
3059
|
+
}
|
|
3060
|
+
function publish(sessionId) {
|
|
3061
|
+
const state = states.get(sessionId);
|
|
3062
|
+
if (state === void 0) snapshots.delete(sessionId);
|
|
3063
|
+
else snapshots.set(sessionId, {
|
|
3064
|
+
card: state.card,
|
|
3065
|
+
compactBusy: state.compactBusy,
|
|
3066
|
+
continueBusy: state.continueBusy === true,
|
|
3067
|
+
compactCommandSent: state.compactCommandSent === true,
|
|
3068
|
+
...state.compactedTokens === void 0 ? {} : { compactedTokens: state.compactedTokens },
|
|
3069
|
+
...state.compactReason === void 0 ? {} : { compactReason: state.compactReason },
|
|
3070
|
+
...state.failure === void 0 ? {} : { failure: state.failure }
|
|
3071
|
+
});
|
|
3072
|
+
events.emit();
|
|
3073
|
+
}
|
|
3074
|
+
return {
|
|
3075
|
+
subscribe: events.subscribe,
|
|
3076
|
+
getSnapshot: read,
|
|
3077
|
+
async check(sessionId) {
|
|
3078
|
+
if (sessionId === "" || lifetime.signal.aborted || states.has(sessionId)) return;
|
|
3079
|
+
const now = Date.now();
|
|
3080
|
+
if (now - (checkedAt.get(sessionId) ?? 0) < minIntervalMs) return;
|
|
3081
|
+
if (inflight.has(sessionId)) return;
|
|
3082
|
+
checkedAt.set(sessionId, now);
|
|
3083
|
+
capMarks(checkedAt);
|
|
3084
|
+
const ticket = new AbortController();
|
|
3085
|
+
inflight.set(sessionId, ticket);
|
|
3086
|
+
const signal = AbortSignal.any([lifetime.signal, ticket.signal]);
|
|
3087
|
+
try {
|
|
3088
|
+
const value = await callPresence(host, WELCOME_BACK_PREPARE_ENDPOINT, {
|
|
3089
|
+
sessionId,
|
|
3090
|
+
...settled.has(sessionId) ? { knownLastResponseAt: settled.get(sessionId) } : {}
|
|
3091
|
+
}, signal);
|
|
3092
|
+
if (signal.aborted || inflight.get(sessionId) !== ticket || states.has(sessionId) || value.show !== true) return;
|
|
3093
|
+
if (value.sessionId !== sessionId) return;
|
|
3094
|
+
if (settled.get(sessionId) === value.lastResponseAt) return;
|
|
3095
|
+
states.set(sessionId, {
|
|
3096
|
+
card: {
|
|
3097
|
+
sessionId,
|
|
3098
|
+
idleMinutes: value.idleMinutes,
|
|
3099
|
+
lastResponseAt: value.lastResponseAt,
|
|
3100
|
+
recap: value.recap,
|
|
3101
|
+
compactSuggested: value.compactSuggested,
|
|
3102
|
+
compactAvailable: value.compactAvailable,
|
|
3103
|
+
compactMinutes: value.compactMinutes
|
|
3104
|
+
},
|
|
3105
|
+
compactBusy: false,
|
|
3106
|
+
compactedTokens: void 0,
|
|
3107
|
+
compactReason: void 0,
|
|
3108
|
+
failure: void 0
|
|
3109
|
+
});
|
|
3110
|
+
publish(sessionId);
|
|
3111
|
+
} catch {} finally {
|
|
3112
|
+
if (inflight.get(sessionId) === ticket) inflight.delete(sessionId);
|
|
3113
|
+
}
|
|
3114
|
+
},
|
|
3115
|
+
invalidate() {
|
|
3116
|
+
for (const ticket of inflight.values()) ticket.abort();
|
|
3117
|
+
inflight.clear();
|
|
3118
|
+
checkedAt.clear();
|
|
3119
|
+
settled.clear();
|
|
3120
|
+
for (const [sessionId, state] of states) {
|
|
3121
|
+
if (state.compactBusy || state.continueBusy) continue;
|
|
3122
|
+
states.delete(sessionId);
|
|
3123
|
+
snapshots.delete(sessionId);
|
|
3124
|
+
}
|
|
3125
|
+
events.emit();
|
|
3126
|
+
},
|
|
3127
|
+
dismiss(sessionId) {
|
|
3128
|
+
inflight.get(sessionId)?.abort();
|
|
3129
|
+
inflight.delete(sessionId);
|
|
3130
|
+
const state = states.get(sessionId);
|
|
3131
|
+
if (state === void 0) return;
|
|
3132
|
+
state.continueAbort?.abort();
|
|
3133
|
+
settled.set(sessionId, state.card.lastResponseAt);
|
|
3134
|
+
capMarks(settled);
|
|
3135
|
+
states.delete(sessionId);
|
|
3136
|
+
publish(sessionId);
|
|
3137
|
+
},
|
|
3138
|
+
async compact(sessionId) {
|
|
3139
|
+
const state = states.get(sessionId);
|
|
3140
|
+
if (state === void 0 || state.compactBusy || state.continueBusy || lifetime.signal.aborted) return;
|
|
3141
|
+
state.compactBusy = true;
|
|
3142
|
+
state.failure = void 0;
|
|
3143
|
+
state.compactReason = void 0;
|
|
3144
|
+
publish(sessionId);
|
|
3145
|
+
try {
|
|
3146
|
+
if (host.compactContext !== void 0) {
|
|
3147
|
+
await host.compactContext(sessionId, lifetime.signal);
|
|
3148
|
+
if (lifetime.signal.aborted || states.get(sessionId) !== state) return;
|
|
3149
|
+
state.compactBusy = false;
|
|
3150
|
+
state.compactCommandSent = true;
|
|
3151
|
+
publish(sessionId);
|
|
3152
|
+
return;
|
|
3153
|
+
}
|
|
3154
|
+
const value = await callPresence(host, WELCOME_BACK_COMPACT_ENDPOINT, {
|
|
3155
|
+
sessionId: state.card.sessionId,
|
|
3156
|
+
requestId: ids()
|
|
3157
|
+
}, lifetime.signal);
|
|
3158
|
+
if (lifetime.signal.aborted || states.get(sessionId) !== state) return;
|
|
3159
|
+
state.compactBusy = false;
|
|
3160
|
+
if (value.compacted) state.compactedTokens = value.shadowedTokens;
|
|
3161
|
+
else state.compactReason = value.reason;
|
|
3162
|
+
publish(sessionId);
|
|
3163
|
+
} catch (error) {
|
|
3164
|
+
if (lifetime.signal.aborted || states.get(sessionId) !== state) return;
|
|
3165
|
+
state.compactBusy = false;
|
|
3166
|
+
state.failure = readFailure(error);
|
|
3167
|
+
publish(sessionId);
|
|
3168
|
+
}
|
|
3169
|
+
},
|
|
3170
|
+
async continueInNewSession(sessionId, locale) {
|
|
3171
|
+
const state = states.get(sessionId);
|
|
3172
|
+
if (state === void 0 || state.compactBusy || state.continueBusy || lifetime.signal.aborted) return;
|
|
3173
|
+
state.continueBusy = true;
|
|
3174
|
+
state.continueAbort = new AbortController();
|
|
3175
|
+
state.failure = void 0;
|
|
3176
|
+
publish(sessionId);
|
|
3177
|
+
try {
|
|
3178
|
+
if (host.continueInNewSession === void 0) throw new HostOperationError("session/lifecycle-unavailable");
|
|
3179
|
+
await host.continueInNewSession(sessionId, state.card.recap, locale, AbortSignal.any([lifetime.signal, state.continueAbort.signal]));
|
|
3180
|
+
if (lifetime.signal.aborted || states.get(sessionId) !== state) return;
|
|
3181
|
+
settled.set(sessionId, state.card.lastResponseAt);
|
|
3182
|
+
capMarks(settled);
|
|
3183
|
+
states.delete(sessionId);
|
|
3184
|
+
publish(sessionId);
|
|
3185
|
+
} catch (error) {
|
|
3186
|
+
if (lifetime.signal.aborted || states.get(sessionId) !== state) return;
|
|
3187
|
+
state.continueBusy = false;
|
|
3188
|
+
if (!(error instanceof Error && error.name === "AbortError")) state.failure = readFailure(error);
|
|
3189
|
+
publish(sessionId);
|
|
3190
|
+
}
|
|
3191
|
+
},
|
|
3192
|
+
cancel() {
|
|
3193
|
+
lifetime.abort();
|
|
3194
|
+
inflight.clear();
|
|
3195
|
+
states.clear();
|
|
3196
|
+
snapshots.clear();
|
|
3197
|
+
settled.clear();
|
|
3198
|
+
checkedAt.clear();
|
|
3199
|
+
}
|
|
3200
|
+
};
|
|
3201
|
+
}
|
|
3202
|
+
//#endregion
|
|
3203
|
+
//#region ../dsh-plugin-kit/src/contracts.ts
|
|
3204
|
+
/**
|
|
3205
|
+
* Remote contribution for model-settings change notifications.
|
|
3206
|
+
* Provided by `@klarkxy/dsh-model-hub` (`ModelSettingsChangesController`);
|
|
3207
|
+
* subscribers degrade silently when that package is not installed.
|
|
3208
|
+
* The stream carries invalidations only — `ready` is never a settings snapshot.
|
|
3209
|
+
*/
|
|
3210
|
+
const MODEL_SETTINGS_CHANGES_REMOTE = {
|
|
3211
|
+
package: "@klarkxy/dsh-model-hub",
|
|
3212
|
+
descriptors: [{
|
|
3213
|
+
id: "modelSettingsChanges/watch",
|
|
3214
|
+
service: "modelSettingsChangesController",
|
|
3215
|
+
namespace: "modelSettingsChanges",
|
|
3216
|
+
method: "watch",
|
|
3217
|
+
mode: "stream",
|
|
3218
|
+
invocation: { kind: "direct" },
|
|
3219
|
+
parameters: [],
|
|
3220
|
+
cancellation: { parameter: "signal" }
|
|
3221
|
+
}]
|
|
3222
|
+
};
|
|
3223
|
+
//#endregion
|
|
3224
|
+
//#region ../dsh-plugin-kit/src/model-settings-client.ts
|
|
3225
|
+
/** This stream carries invalidations only; ready is never a settings snapshot. */
|
|
3226
|
+
function parseModelSettingsChangeFrame(value) {
|
|
3227
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("Invalid model settings change frame");
|
|
3228
|
+
const frame = value;
|
|
3229
|
+
if (frame.kind === "ready" && Object.keys(frame).length === 1) return { kind: "ready" };
|
|
3230
|
+
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 {
|
|
3231
|
+
kind: "changed",
|
|
3232
|
+
ns: frame.ns,
|
|
3233
|
+
revision: frame.revision
|
|
3234
|
+
};
|
|
3235
|
+
throw new Error("Invalid model settings change frame");
|
|
3236
|
+
}
|
|
3237
|
+
MODEL_SETTINGS_CHANGES_REMOTE.package, MODEL_SETTINGS_CHANGES_REMOTE.descriptors.map((descriptor) => ({
|
|
3238
|
+
...descriptor,
|
|
3239
|
+
result: {
|
|
3240
|
+
mode: "strict",
|
|
3241
|
+
typeSymbol: "ModelSettings.ChangeFrame",
|
|
3242
|
+
create: () => ({ parse: parseModelSettingsChangeFrame })
|
|
3243
|
+
}
|
|
3244
|
+
}));
|
|
3245
|
+
/** Native RemoteStream owns carrier reconnects; no timers or browser transport.
|
|
3246
|
+
* A factory shares one logical stream while at least one subscriber remains. */
|
|
3247
|
+
function createModelSettingsSubscriber(ctx, namespace) {
|
|
3248
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
3249
|
+
let injection;
|
|
3250
|
+
let current;
|
|
3251
|
+
let lifecycle = 0;
|
|
3252
|
+
let failureMessage;
|
|
3253
|
+
const report = (message) => {
|
|
3254
|
+
failureMessage = message;
|
|
3255
|
+
for (const listener of [...listeners]) try {
|
|
3256
|
+
listener.error?.(message);
|
|
3257
|
+
} catch {}
|
|
3258
|
+
};
|
|
3259
|
+
const invalidate = () => {
|
|
3260
|
+
for (const listener of [...listeners]) {
|
|
3261
|
+
if (!listeners.has(listener)) continue;
|
|
3262
|
+
try {
|
|
3263
|
+
listener.change();
|
|
3264
|
+
} catch (error) {
|
|
3265
|
+
try {
|
|
3266
|
+
listener.error?.(error instanceof Error ? error.message : String(error));
|
|
3267
|
+
} catch {}
|
|
3268
|
+
}
|
|
3269
|
+
}
|
|
3270
|
+
};
|
|
3271
|
+
const start = () => {
|
|
3272
|
+
const life = ++lifecycle;
|
|
3273
|
+
injection = ctx.inject(["remote", "remote.modelSettingsChanges"], (injected) => {
|
|
3274
|
+
if (life !== lifecycle || !listeners.size) return;
|
|
3275
|
+
const scope = injected;
|
|
3276
|
+
const stream = scope.remote.$stream({
|
|
3277
|
+
name: "Model settings sync",
|
|
3278
|
+
open: (signal) => scope.remote.modelSettingsChanges.watch(signal),
|
|
3279
|
+
ended: (accepted) => /* @__PURE__ */ new Error(`Model settings sync stream ended ${accepted ? "after" : "before"} ready; refresh or reopen the page`)
|
|
3280
|
+
});
|
|
3281
|
+
const session = {
|
|
3282
|
+
stream,
|
|
3283
|
+
active: true
|
|
3284
|
+
};
|
|
3285
|
+
current = session;
|
|
3286
|
+
scope.effect(() => () => {
|
|
3287
|
+
session.active = false;
|
|
3288
|
+
if (current === session) {
|
|
3289
|
+
current = void 0;
|
|
3290
|
+
if (life === lifecycle && listeners.size) report("Model settings sync service unavailable");
|
|
3291
|
+
}
|
|
3292
|
+
return stream.dispose();
|
|
3293
|
+
});
|
|
3294
|
+
(async () => {
|
|
3295
|
+
let generation;
|
|
3296
|
+
let ready = false;
|
|
3297
|
+
try {
|
|
3298
|
+
for await (const item of stream) {
|
|
3299
|
+
if (!session.active || life !== lifecycle || !listeners.size) break;
|
|
3300
|
+
if (item.signal.aborted) continue;
|
|
3301
|
+
if (generation !== item.generation) {
|
|
3302
|
+
generation = item.generation;
|
|
3303
|
+
ready = false;
|
|
3304
|
+
}
|
|
3305
|
+
const frame = parseModelSettingsChangeFrame(item.value);
|
|
3306
|
+
if (frame.kind === "ready") {
|
|
3307
|
+
ready = true;
|
|
3308
|
+
failureMessage = void 0;
|
|
3309
|
+
item.accept();
|
|
3310
|
+
invalidate();
|
|
3311
|
+
} else {
|
|
3312
|
+
if (!ready) throw new Error("Model settings sync change arrived before ready");
|
|
3313
|
+
if (namespace === "*" || frame.ns === namespace) invalidate();
|
|
3314
|
+
}
|
|
3315
|
+
}
|
|
3316
|
+
} catch (error) {
|
|
3317
|
+
if (session.active && life === lifecycle) report(error instanceof Error ? error.message : String(error));
|
|
3318
|
+
} finally {
|
|
3319
|
+
if (current === session) current = void 0;
|
|
3320
|
+
session.active = false;
|
|
3321
|
+
await stream.dispose().catch(() => {});
|
|
3322
|
+
}
|
|
3323
|
+
})();
|
|
3324
|
+
});
|
|
3325
|
+
queueMicrotask(() => {
|
|
3326
|
+
if (life === lifecycle && listeners.size && !current) report("Model settings sync service unavailable");
|
|
3327
|
+
});
|
|
3328
|
+
};
|
|
3329
|
+
return Object.freeze({ subscribe(onChange, onError) {
|
|
3330
|
+
const listener = {
|
|
3331
|
+
change: onChange,
|
|
3332
|
+
error: onError
|
|
3333
|
+
};
|
|
3334
|
+
listeners.add(listener);
|
|
3335
|
+
if (!injection) start();
|
|
3336
|
+
else if (failureMessage) queueMicrotask(() => {
|
|
3337
|
+
if (listeners.has(listener) && failureMessage) try {
|
|
3338
|
+
listener.error?.(failureMessage);
|
|
3339
|
+
} catch {}
|
|
3340
|
+
});
|
|
3341
|
+
return () => {
|
|
3342
|
+
if (!listeners.delete(listener) || listeners.size) return;
|
|
3343
|
+
lifecycle++;
|
|
3344
|
+
if (current) {
|
|
3345
|
+
current.active = false;
|
|
3346
|
+
current.stream.dispose().catch(() => {});
|
|
3347
|
+
}
|
|
3348
|
+
current = void 0;
|
|
3349
|
+
failureMessage = void 0;
|
|
3350
|
+
const previous = injection;
|
|
3351
|
+
injection = void 0;
|
|
3352
|
+
try {
|
|
3353
|
+
Promise.resolve(previous?.dispose()).catch(() => {});
|
|
3354
|
+
} catch {}
|
|
3355
|
+
};
|
|
3356
|
+
} });
|
|
3357
|
+
}
|
|
3358
|
+
//#endregion
|
|
3359
|
+
//#region src/presence-panel.tsx
|
|
3360
|
+
/**
|
|
3361
|
+
* Grey inline-completion for an empty composer, IDE style: the shipped
|
|
3362
|
+
* placeholder's own text becomes the continuation (native look, zero
|
|
3363
|
+
* positioning), and Tab fills it into the draft for a normal send.
|
|
3364
|
+
* The rendered element is a hidden anchor; all work happens in effects.
|
|
3365
|
+
*/
|
|
3366
|
+
function ContinueGhost(props) {
|
|
3367
|
+
const sessionId = props.sessionId ?? "";
|
|
3368
|
+
const draft = props.useInput?.((state) => state.draft) ?? "";
|
|
3369
|
+
const phase = props.useInput?.((state) => state.phase) ?? "plain";
|
|
3370
|
+
const bare = (props.useInput?.((state) => state.attachmentIds.length) ?? 0) === 0;
|
|
3371
|
+
const blank = props.useSession?.((state) => state.blank) ?? true;
|
|
3372
|
+
const running = props.useSessionStatus?.((state) => state.get(sessionId)?.running === true) ?? false;
|
|
3373
|
+
const snapshot = (0, react.useSyncExternalStore)(props.model.subscribe, () => props.model.getSnapshot(sessionId), () => props.model.getSnapshot(sessionId));
|
|
3374
|
+
const text = snapshot.text ?? uiText(props.locale, "continueDefault");
|
|
3375
|
+
const active = sessionId !== "" && !blank && bare && draft === "" && phase === "plain";
|
|
3376
|
+
const latest = (0, react.useRef)({
|
|
3377
|
+
active,
|
|
3378
|
+
text
|
|
3379
|
+
});
|
|
3380
|
+
(0, react.useEffect)(() => {
|
|
3381
|
+
latest.current = {
|
|
3382
|
+
active,
|
|
3383
|
+
text
|
|
3384
|
+
};
|
|
3385
|
+
}, [active, text]);
|
|
3386
|
+
const wasRunning = (0, react.useRef)(false);
|
|
3387
|
+
(0, react.useEffect)(() => {
|
|
3388
|
+
const finished = wasRunning.current && !running;
|
|
3389
|
+
wasRunning.current = running;
|
|
3390
|
+
if (finished && sessionId !== "") props.model.invalidate(sessionId);
|
|
3391
|
+
if (active && !running) props.model.load(sessionId);
|
|
3392
|
+
}, [
|
|
3393
|
+
props.model,
|
|
3394
|
+
active,
|
|
3395
|
+
running,
|
|
3396
|
+
sessionId
|
|
3397
|
+
]);
|
|
3398
|
+
(0, react.useEffect)(() => {
|
|
3399
|
+
if (active && !running && snapshot.text === void 0) props.model.load(sessionId);
|
|
3400
|
+
}, [
|
|
3401
|
+
props.model,
|
|
3402
|
+
active,
|
|
3403
|
+
running,
|
|
3404
|
+
sessionId,
|
|
3405
|
+
snapshot
|
|
3406
|
+
]);
|
|
3407
|
+
const anchorRef = (0, react.useRef)(null);
|
|
3408
|
+
(0, react.useEffect)(() => {
|
|
3409
|
+
if (!active) return;
|
|
3410
|
+
const card = anchorRef.current?.closest("[data-composer-card]") ?? document.querySelector("[data-composer-card]");
|
|
3411
|
+
if (!card) return;
|
|
3412
|
+
const write = () => {
|
|
3413
|
+
const placeholder = card.querySelector("[data-composer-placeholder]");
|
|
3414
|
+
if (placeholder !== null && placeholder.textContent !== latest.current.text) placeholder.textContent = latest.current.text;
|
|
3415
|
+
};
|
|
3416
|
+
write();
|
|
3417
|
+
const observer = new MutationObserver(write);
|
|
3418
|
+
observer.observe(card, {
|
|
3419
|
+
subtree: true,
|
|
3420
|
+
childList: true,
|
|
3421
|
+
characterData: true
|
|
3422
|
+
});
|
|
3423
|
+
return () => {
|
|
3424
|
+
observer.disconnect();
|
|
3425
|
+
const placeholder = card.querySelector("[data-composer-placeholder]");
|
|
3426
|
+
const original = card.querySelector("[data-composer-input]")?.getAttribute("data-placeholder");
|
|
3427
|
+
if (placeholder !== null && typeof original === "string" && placeholder.textContent === latest.current.text) placeholder.textContent = original;
|
|
3428
|
+
};
|
|
3429
|
+
}, [active, text]);
|
|
3430
|
+
(0, react.useEffect)(() => {
|
|
3431
|
+
const onKey = (event) => {
|
|
3432
|
+
if (event.key !== "Tab" || event.shiftKey || event.ctrlKey || event.metaKey || event.altKey) return;
|
|
3433
|
+
if (event.isComposing || event.keyCode === 229) return;
|
|
3434
|
+
const current = latest.current;
|
|
3435
|
+
if (!current.active) return;
|
|
3436
|
+
const target = event.target;
|
|
3437
|
+
if (!(target instanceof HTMLElement)) return;
|
|
3438
|
+
if (target.closest("[role=\"dialog\"]") !== null) return;
|
|
3439
|
+
const anchored = anchorRef.current?.closest("[data-composer-card]");
|
|
3440
|
+
const fromTarget = target.closest("[data-composer-card]");
|
|
3441
|
+
const only = document.querySelectorAll("[data-composer-card]");
|
|
3442
|
+
const composer = (anchored ?? (fromTarget !== null && only.length === 1 ? fromTarget : null) ?? (only.length === 1 ? only[0] : null))?.querySelector("[data-composer-input]") ?? null;
|
|
3443
|
+
if (composer === null || target !== composer && !composer.contains(target)) return;
|
|
3444
|
+
event.preventDefault();
|
|
3445
|
+
event.stopPropagation();
|
|
3446
|
+
props.inputActions?.setDraft(current.text);
|
|
3447
|
+
};
|
|
3448
|
+
document.addEventListener("keydown", onKey, true);
|
|
3449
|
+
return () => document.removeEventListener("keydown", onKey, true);
|
|
3450
|
+
}, [props.inputActions]);
|
|
3451
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3452
|
+
ref: anchorRef,
|
|
3453
|
+
hidden: true,
|
|
3454
|
+
"data-sm-ghost-anchor": ""
|
|
3455
|
+
});
|
|
3456
|
+
}
|
|
3457
|
+
/**
|
|
3458
|
+
* The merged return card inside the chat flow: model recap past the recap
|
|
3459
|
+
* threshold, compaction offer past the compact one. It renders at the latest
|
|
3460
|
+
* completed turn's tail instead of opening a modal, so it suggests rather
|
|
3461
|
+
* than intercepts a send.
|
|
3462
|
+
*/
|
|
3463
|
+
function WelcomeBackCard(props) {
|
|
3464
|
+
const sessionId = props.sessionId ?? "";
|
|
3465
|
+
const snapshot = (0, react.useSyncExternalStore)(props.model.subscribe, () => props.model.getSnapshot(sessionId), () => props.model.getSnapshot(sessionId));
|
|
3466
|
+
const lastTurn = props.useChat?.((state) => state.timeline.turnOrder[state.timeline.turnOrder.length - 1]);
|
|
3467
|
+
const inputPhase = props.useInput?.((state) => state.phase);
|
|
3468
|
+
const card = snapshot.card;
|
|
3469
|
+
const [liveMinutes, setLiveMinutes] = (0, react.useState)(() => card === void 0 ? 0 : Math.max(0, Math.floor((Date.now() - card.lastResponseAt) / 6e4)));
|
|
3470
|
+
(0, react.useEffect)(() => {
|
|
3471
|
+
if (card === void 0) return;
|
|
3472
|
+
const tick = () => setLiveMinutes(Math.max(0, Math.floor((Date.now() - card.lastResponseAt) / 6e4)));
|
|
3473
|
+
tick();
|
|
3474
|
+
const timer = setInterval(tick, 3e4);
|
|
3475
|
+
return () => clearInterval(timer);
|
|
3476
|
+
}, [card]);
|
|
3477
|
+
const phaseRef = (0, react.useRef)(inputPhase);
|
|
3478
|
+
(0, react.useEffect)(() => {
|
|
3479
|
+
const previous = phaseRef.current;
|
|
3480
|
+
phaseRef.current = inputPhase;
|
|
3481
|
+
if (inputPhase === void 0 || previous === inputPhase) return;
|
|
3482
|
+
if (previous === "plain" && sessionId !== "") props.model.dismiss(sessionId);
|
|
3483
|
+
}, [
|
|
3484
|
+
inputPhase,
|
|
3485
|
+
sessionId,
|
|
3486
|
+
props.model
|
|
3487
|
+
]);
|
|
3488
|
+
if (sessionId === "" || card === void 0) return null;
|
|
3489
|
+
if (props.turn === void 0 || lastTurn === void 0 || props.turn.turn !== lastTurn) return null;
|
|
3490
|
+
const respondedAt = props.turn.end?.time ?? props.turn.start?.time;
|
|
3491
|
+
if (respondedAt !== void 0 && card.lastResponseAt < respondedAt) return null;
|
|
3492
|
+
const copy = (key) => uiText(props.locale, key);
|
|
3493
|
+
const recap = card.recap;
|
|
3494
|
+
const settled = snapshot.compactedTokens !== void 0 || snapshot.compactReason === "nothing";
|
|
3495
|
+
const liveCompactSuggested = card.compactSuggested || card.compactMinutes > 0 && liveMinutes >= card.compactMinutes;
|
|
3496
|
+
const offerCompact = liveCompactSuggested && !settled;
|
|
3497
|
+
const actionBusy = snapshot.compactBusy || snapshot.continueBusy === true;
|
|
3498
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3499
|
+
className: `${FREE_CHAT_ROOT} dsh-welcome-card`,
|
|
3500
|
+
children: [
|
|
3501
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3502
|
+
className: "dsh-welcome-card-head",
|
|
3503
|
+
children: [
|
|
3504
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3505
|
+
className: "dsh-welcome-card-title",
|
|
3506
|
+
children: copy("welcomeBack")
|
|
3507
|
+
}),
|
|
3508
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3509
|
+
className: "dsh-welcome-card-idle",
|
|
3510
|
+
children: idleText(props.locale, liveMinutes)
|
|
3511
|
+
}),
|
|
3512
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
3513
|
+
type: "button",
|
|
3514
|
+
variant: "ghost",
|
|
3515
|
+
size: "sm",
|
|
3516
|
+
className: "dsh-welcome-card-close",
|
|
3517
|
+
"aria-label": copy("close"),
|
|
3518
|
+
icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCloseFillRegular, {}),
|
|
3519
|
+
onClick: () => {
|
|
3520
|
+
props.model.dismiss(sessionId);
|
|
3521
|
+
}
|
|
3522
|
+
})
|
|
3523
|
+
]
|
|
3524
|
+
}),
|
|
3525
|
+
recap !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("dl", {
|
|
3526
|
+
className: "dsh-welcome-recap",
|
|
3527
|
+
children: [
|
|
3528
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("dt", { children: copy("recapSummary") }),
|
|
3529
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("dd", { children: recap.summary }),
|
|
3530
|
+
recap.progress !== "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dt", { children: copy("recapProgress") }) : null,
|
|
3531
|
+
recap.progress !== "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dd", { children: recap.progress }) : null,
|
|
3532
|
+
recap.nextStep !== "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dt", { children: copy("recapNextStep") }) : null,
|
|
3533
|
+
recap.nextStep !== "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dd", { children: recap.nextStep }) : null
|
|
3534
|
+
]
|
|
3535
|
+
}) : null,
|
|
3536
|
+
liveCompactSuggested ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
3537
|
+
className: "dsh-welcome-cache",
|
|
3538
|
+
children: copy("cacheNotice")
|
|
3539
|
+
}) : null,
|
|
3540
|
+
liveCompactSuggested ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3541
|
+
className: "dsh-welcome-card-actions",
|
|
3542
|
+
children: [offerCompact ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
3543
|
+
type: "button",
|
|
3544
|
+
variant: "outline",
|
|
3545
|
+
disabled: actionBusy,
|
|
3546
|
+
onClick: () => {
|
|
3547
|
+
props.model.compact(sessionId);
|
|
3548
|
+
},
|
|
3549
|
+
children: snapshot.compactBusy ? copy("compacting") : copy("compactNow")
|
|
3550
|
+
}) : null, /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
3551
|
+
type: "button",
|
|
3552
|
+
variant: "outline",
|
|
3553
|
+
disabled: actionBusy,
|
|
3554
|
+
title: copy("continueNewHint"),
|
|
3555
|
+
onClick: () => {
|
|
3556
|
+
props.model.continueInNewSession(sessionId, props.locale);
|
|
3557
|
+
},
|
|
3558
|
+
children: snapshot.continueBusy ? copy("creatingContinuation") : copy("continueNew")
|
|
3559
|
+
})]
|
|
3560
|
+
}) : null,
|
|
3561
|
+
snapshot.compactCommandSent ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
3562
|
+
className: "dsh-welcome-done",
|
|
3563
|
+
role: "status",
|
|
3564
|
+
children: copy("compactCommandSent")
|
|
3565
|
+
}) : null,
|
|
3566
|
+
snapshot.compactedTokens !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
3567
|
+
className: "dsh-welcome-done",
|
|
3568
|
+
role: "status",
|
|
3569
|
+
children: compactedText(props.locale, snapshot.compactedTokens)
|
|
3570
|
+
}) : null,
|
|
3571
|
+
snapshot.compactReason === "nothing" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
3572
|
+
className: "dsh-welcome-done",
|
|
3573
|
+
role: "status",
|
|
3574
|
+
children: copy("compactNothing")
|
|
3575
|
+
}) : null,
|
|
3576
|
+
snapshot.compactReason === "busy" || snapshot.compactReason === "unavailable" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
3577
|
+
className: "dsh-ui-error dsh-free-alert",
|
|
3578
|
+
role: "alert",
|
|
3579
|
+
children: failureText(props.locale, { code: snapshot.compactReason === "busy" ? "session-manager/busy" : "session-manager/unavailable" })
|
|
3580
|
+
}) : null,
|
|
3581
|
+
snapshot.failure ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
3582
|
+
className: "dsh-ui-error dsh-free-alert",
|
|
3583
|
+
role: "alert",
|
|
3584
|
+
children: failureText(props.locale, snapshot.failure)
|
|
3585
|
+
}) : null
|
|
3586
|
+
]
|
|
3587
|
+
});
|
|
3588
|
+
}
|
|
3589
|
+
//#endregion
|
|
3590
|
+
//#region src/settings-panel.tsx
|
|
3591
|
+
function toDraft(values) {
|
|
3592
|
+
const recapEnabled = values.recapMinutes > 0;
|
|
3593
|
+
const compactEnabled = values.compactMinutes > 0;
|
|
3594
|
+
return {
|
|
3595
|
+
recapEnabled,
|
|
3596
|
+
recapMinutes: String(recapEnabled ? values.recapMinutes : 15),
|
|
3597
|
+
compactEnabled,
|
|
3598
|
+
compactMinutes: String(compactEnabled ? values.compactMinutes : 60),
|
|
3599
|
+
continueText: values.continueText,
|
|
3600
|
+
continueMode: values.continueMode,
|
|
3601
|
+
model: values.model === null ? "" : modelRouteKey(values.model.provider, values.model.model),
|
|
3602
|
+
ghostModel: values.ghostModel === null ? "" : modelRouteKey(values.ghostModel.provider, values.ghostModel.model)
|
|
3603
|
+
};
|
|
3604
|
+
}
|
|
3605
|
+
/** Enabled thresholds are whole minutes of at least 1. 0 stays the stored "off" value and is not a valid enabled draft. */
|
|
3606
|
+
function minutesOf(text) {
|
|
3607
|
+
if (!/^[1-9]\d*$/.test(text.trim())) return void 0;
|
|
3608
|
+
const value = Number.parseInt(text.trim(), 10);
|
|
3609
|
+
return Number.isSafeInteger(value) && value >= 1 ? value : void 0;
|
|
3610
|
+
}
|
|
3611
|
+
function sameDraft(left, right) {
|
|
3612
|
+
return left.recapEnabled === right.recapEnabled && left.recapMinutes === right.recapMinutes && left.compactEnabled === right.compactEnabled && left.compactMinutes === right.compactMinutes && left.continueText === right.continueText && left.continueMode === right.continueMode && left.model === right.model && left.ghostModel === right.ghostModel;
|
|
3613
|
+
}
|
|
3614
|
+
const missingCatalog = async () => {
|
|
3615
|
+
throw new Error("Model catalog service unavailable");
|
|
3616
|
+
};
|
|
3617
|
+
/** The bundle config page: edits the cordis plugin config through the host config editor. */
|
|
3618
|
+
function SessionManagerSettings(props) {
|
|
3619
|
+
const snapshot = (0, react.useSyncExternalStore)(props.model.subscribe, props.model.getSnapshot, props.model.getSnapshot);
|
|
3620
|
+
const [draft, setDraft] = (0, react.useState)(void 0);
|
|
3621
|
+
const catalog = useModelCatalog(props.modelCatalog ?? missingCatalog, { subscribe: props.subscribeCatalog });
|
|
3622
|
+
const [menuOpen, setMenuOpen] = (0, react.useState)(false);
|
|
3623
|
+
const [ghostMenuOpen, setGhostMenuOpen] = (0, react.useState)(false);
|
|
3624
|
+
const [externalChange, setExternalChange] = (0, react.useState)(false);
|
|
3625
|
+
const dirty = (0, react.useRef)(false);
|
|
3626
|
+
const baseline = (0, react.useRef)();
|
|
3627
|
+
const tabsId = (0, react.useId)();
|
|
3628
|
+
const copy = (key) => uiText(props.locale, key);
|
|
3629
|
+
const edit = (next) => {
|
|
3630
|
+
dirty.current = true;
|
|
3631
|
+
setDraft(next);
|
|
3632
|
+
};
|
|
3633
|
+
(0, react.useEffect)(() => {
|
|
3634
|
+
if (snapshot.phase === "loading") props.model.load();
|
|
3635
|
+
}, [props.model, snapshot.phase]);
|
|
3636
|
+
(0, react.useEffect)(() => {
|
|
3637
|
+
if (snapshot.values === void 0) return;
|
|
3638
|
+
const next = toDraft(snapshot.values);
|
|
3639
|
+
setDraft((current) => {
|
|
3640
|
+
if (dirty.current && (current === void 0 || !sameDraft(current, next))) return current;
|
|
3641
|
+
dirty.current = false;
|
|
3642
|
+
baseline.current = next;
|
|
3643
|
+
return next;
|
|
3644
|
+
});
|
|
3645
|
+
setExternalChange(dirty.current);
|
|
3646
|
+
}, [snapshot.values]);
|
|
3647
|
+
(0, react.useEffect)(() => props.subscribeSettings?.(() => {
|
|
3648
|
+
props.model.load();
|
|
3649
|
+
catalog.refresh();
|
|
3650
|
+
}) ?? (() => {}), [
|
|
3651
|
+
props.model,
|
|
3652
|
+
props.subscribeSettings,
|
|
3653
|
+
catalog.refresh
|
|
3654
|
+
]);
|
|
3655
|
+
if (snapshot.phase === "loading") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
3656
|
+
className: `${FREE_CHAT_ROOT} dsh-ui-loading`,
|
|
3657
|
+
children: copy("settingsLoading")
|
|
3658
|
+
});
|
|
3659
|
+
if (snapshot.phase === "unavailable" || draft === void 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
3660
|
+
className: `${FREE_CHAT_ROOT} dsh-ui-error`,
|
|
3661
|
+
role: "alert",
|
|
3662
|
+
children: snapshot.failure ? failureText(props.locale, snapshot.failure) : copy("settingsUnavailable")
|
|
3663
|
+
});
|
|
3664
|
+
const selectedRoute = parseModelRouteKey(draft.model);
|
|
3665
|
+
const ghostRoute = parseModelRouteKey(draft.ghostModel);
|
|
3666
|
+
const selectableChoices = parseModelMenuChoices(catalog.catalog, selectedRoute ?? ghostRoute);
|
|
3667
|
+
const selectedLabel = selectableChoices.find((choice) => modelRouteKey(choice.provider, choice.model) === draft.model)?.label ?? copy("modelFollowOption");
|
|
3668
|
+
const ghostLabel = selectableChoices.find((choice) => modelRouteKey(choice.provider, choice.model) === draft.ghostModel)?.label ?? copy("modelFollowOption");
|
|
3669
|
+
const recapMinutes = draft.recapEnabled ? minutesOf(draft.recapMinutes) : 0;
|
|
3670
|
+
const compactMinutes = draft.compactEnabled ? minutesOf(draft.compactMinutes) : 0;
|
|
3671
|
+
const recapInvalid = recapMinutes === void 0;
|
|
3672
|
+
const compactInvalid = compactMinutes === void 0;
|
|
3673
|
+
const invalid = recapInvalid || compactInvalid;
|
|
3674
|
+
const fixedPanelId = `${tabsId}-fixed-panel`;
|
|
3675
|
+
const autoPanelId = `${tabsId}-auto-panel`;
|
|
3676
|
+
const save = () => {
|
|
3677
|
+
if (invalid) return;
|
|
3678
|
+
const route = parseModelRouteKey(draft.model);
|
|
3679
|
+
const ghost = parseModelRouteKey(draft.ghostModel);
|
|
3680
|
+
setExternalChange(false);
|
|
3681
|
+
props.model.save({
|
|
3682
|
+
recapMinutes,
|
|
3683
|
+
compactMinutes,
|
|
3684
|
+
continueText: draft.continueText,
|
|
3685
|
+
continueMode: draft.continueMode,
|
|
3686
|
+
...draft.model === baseline.current?.model ? {} : { model: route === void 0 ? null : {
|
|
3687
|
+
provider: route.provider,
|
|
3688
|
+
model: route.model
|
|
3689
|
+
} },
|
|
3690
|
+
...draft.ghostModel === baseline.current?.ghostModel ? {} : { ghostModel: ghost === void 0 ? null : {
|
|
3691
|
+
provider: ghost.provider,
|
|
3692
|
+
model: ghost.model
|
|
3693
|
+
} }
|
|
3694
|
+
});
|
|
3695
|
+
};
|
|
3696
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3697
|
+
className: `${FREE_CHAT_ROOT} dsh-ui-panel`,
|
|
3698
|
+
children: [
|
|
3699
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
3700
|
+
className: "dsh-ui-card",
|
|
3701
|
+
children: [
|
|
3702
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
|
|
3703
|
+
className: "dsh-ui-heading",
|
|
3704
|
+
children: copy("settingsMode")
|
|
3705
|
+
}),
|
|
3706
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
3707
|
+
className: "dsh-ui-hint",
|
|
3708
|
+
children: copy("settingsModeHint")
|
|
3709
|
+
}),
|
|
3710
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.SegmentedTabs, {
|
|
3711
|
+
label: copy("settingsMode"),
|
|
3712
|
+
items: [{
|
|
3713
|
+
id: `${tabsId}-fixed`,
|
|
3714
|
+
panelId: fixedPanelId,
|
|
3715
|
+
value: "fixed",
|
|
3716
|
+
label: copy("modeFixed")
|
|
3717
|
+
}, {
|
|
3718
|
+
id: `${tabsId}-auto`,
|
|
3719
|
+
panelId: autoPanelId,
|
|
3720
|
+
value: "auto",
|
|
3721
|
+
label: copy("modeAuto")
|
|
3722
|
+
}],
|
|
3723
|
+
value: draft.continueMode,
|
|
3724
|
+
onChange: (value) => edit({
|
|
3725
|
+
...draft,
|
|
3726
|
+
continueMode: value
|
|
3727
|
+
})
|
|
3728
|
+
}) }),
|
|
3729
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3730
|
+
role: "tabpanel",
|
|
3731
|
+
id: fixedPanelId,
|
|
3732
|
+
"aria-labelledby": `${tabsId}-fixed`,
|
|
3733
|
+
hidden: draft.continueMode !== "fixed",
|
|
3734
|
+
children: draft.continueMode === "fixed" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
3735
|
+
className: "dsh-ui-field",
|
|
3736
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3737
|
+
className: "dsh-ui-label",
|
|
3738
|
+
children: copy("continueTextLabel")
|
|
3739
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Input, {
|
|
3740
|
+
value: draft.continueText,
|
|
3741
|
+
placeholder: copy("continueTextPlaceholder"),
|
|
3742
|
+
onChange: (event) => edit({
|
|
3743
|
+
...draft,
|
|
3744
|
+
continueText: event.target.value
|
|
3745
|
+
})
|
|
3746
|
+
})]
|
|
3747
|
+
}) : null
|
|
3748
|
+
}),
|
|
3749
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3750
|
+
role: "tabpanel",
|
|
3751
|
+
id: autoPanelId,
|
|
3752
|
+
"aria-labelledby": `${tabsId}-auto`,
|
|
3753
|
+
hidden: draft.continueMode !== "auto",
|
|
3754
|
+
children: draft.continueMode === "auto" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3755
|
+
className: "dsh-ui-field",
|
|
3756
|
+
children: [
|
|
3757
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3758
|
+
className: "dsh-ui-label",
|
|
3759
|
+
children: copy("ghostModelLabel")
|
|
3760
|
+
}),
|
|
3761
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelMenu, {
|
|
3762
|
+
locale: props.locale,
|
|
3763
|
+
open: ghostMenuOpen,
|
|
3764
|
+
onOpenChange: setGhostMenuOpen,
|
|
3765
|
+
listClassName: FREE_CHAT_ROOT,
|
|
3766
|
+
anchor: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
3767
|
+
type: "button",
|
|
3768
|
+
className: "dsh-model-menu-trigger",
|
|
3769
|
+
"aria-label": `${copy("ghostModelLabel")}: ${ghostLabel}`,
|
|
3770
|
+
"aria-haspopup": "menu",
|
|
3771
|
+
"aria-expanded": ghostMenuOpen,
|
|
3772
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3773
|
+
className: "dsh-model-menu-triggerLabel",
|
|
3774
|
+
children: ghostLabel
|
|
3775
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutlineRegular, { className: `dsh-model-menu-chevron${ghostMenuOpen ? " dsh-model-menu-chevronOpen" : ""}` })]
|
|
3776
|
+
}),
|
|
3777
|
+
choices: selectableChoices,
|
|
3778
|
+
selected: ghostRoute,
|
|
3779
|
+
onPick: (route) => edit({
|
|
3780
|
+
...draft,
|
|
3781
|
+
ghostModel: modelRouteKey(route.provider, route.model)
|
|
3782
|
+
}),
|
|
3783
|
+
leading: [{
|
|
3784
|
+
id: "follow",
|
|
3785
|
+
label: copy("modelFollowOption"),
|
|
3786
|
+
selected: !ghostRoute
|
|
3787
|
+
}],
|
|
3788
|
+
onPickLeading: () => edit({
|
|
3789
|
+
...draft,
|
|
3790
|
+
ghostModel: ""
|
|
3791
|
+
}),
|
|
3792
|
+
modelValue: ghostLabel
|
|
3793
|
+
}) }),
|
|
3794
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
3795
|
+
className: "dsh-ui-hint",
|
|
3796
|
+
children: copy("ghostModelHint")
|
|
3797
|
+
})
|
|
3798
|
+
]
|
|
3799
|
+
}) : null
|
|
3800
|
+
})
|
|
3801
|
+
]
|
|
3802
|
+
}),
|
|
3803
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
3804
|
+
className: "dsh-ui-card",
|
|
3805
|
+
children: [
|
|
3806
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
|
|
3807
|
+
className: "dsh-ui-heading",
|
|
3808
|
+
children: copy("welcomeBackGroup")
|
|
3809
|
+
}),
|
|
3810
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3811
|
+
className: "dsm-feature",
|
|
3812
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3813
|
+
className: "dsh-ui-toggle-row",
|
|
3814
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
3815
|
+
className: "dsh-ui-toggle-text",
|
|
3816
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3817
|
+
className: "dsh-ui-toggle-label",
|
|
3818
|
+
children: copy("recapEnabledLabel")
|
|
3819
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3820
|
+
className: "dsh-ui-hint",
|
|
3821
|
+
children: copy("recapEnabledHint")
|
|
3822
|
+
})]
|
|
3823
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Switch, {
|
|
3824
|
+
label: copy("recapEnabledLabel"),
|
|
3825
|
+
checked: draft.recapEnabled,
|
|
3826
|
+
onChange: (next) => edit({
|
|
3827
|
+
...draft,
|
|
3828
|
+
recapEnabled: next
|
|
3829
|
+
})
|
|
3830
|
+
})]
|
|
3831
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3832
|
+
className: "dsm-subs",
|
|
3833
|
+
"data-disabled": draft.recapEnabled ? void 0 : "true",
|
|
3834
|
+
children: [
|
|
3835
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
3836
|
+
className: "dsh-ui-field",
|
|
3837
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3838
|
+
className: "dsh-ui-label",
|
|
3839
|
+
children: copy("recapMinutesLabel")
|
|
3840
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Input, {
|
|
3841
|
+
className: "dsm-minutes",
|
|
3842
|
+
value: draft.recapMinutes,
|
|
3843
|
+
inputMode: "numeric",
|
|
3844
|
+
disabled: !draft.recapEnabled,
|
|
3845
|
+
"aria-invalid": recapInvalid || void 0,
|
|
3846
|
+
"aria-describedby": recapInvalid ? `${tabsId}-recap-error` : void 0,
|
|
3847
|
+
onChange: (event) => edit({
|
|
3848
|
+
...draft,
|
|
3849
|
+
recapMinutes: event.target.value
|
|
3850
|
+
})
|
|
3851
|
+
})]
|
|
3852
|
+
}), recapInvalid ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
3853
|
+
id: `${tabsId}-recap-error`,
|
|
3854
|
+
className: "dsh-ui-error",
|
|
3855
|
+
role: "alert",
|
|
3856
|
+
children: copy("minutesInvalid")
|
|
3857
|
+
}) : null] }),
|
|
3858
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3859
|
+
className: "dsh-ui-field",
|
|
3860
|
+
children: [
|
|
3861
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3862
|
+
className: "dsh-ui-label",
|
|
3863
|
+
children: copy("modelLabel")
|
|
3864
|
+
}),
|
|
3865
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelMenu, {
|
|
3866
|
+
locale: props.locale,
|
|
3867
|
+
open: menuOpen,
|
|
3868
|
+
onOpenChange: setMenuOpen,
|
|
3869
|
+
listClassName: FREE_CHAT_ROOT,
|
|
3870
|
+
anchor: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
3871
|
+
type: "button",
|
|
3872
|
+
className: "dsh-model-menu-trigger",
|
|
3873
|
+
disabled: !draft.recapEnabled,
|
|
3874
|
+
"aria-label": `${copy("modelLabel")}: ${selectedLabel}`,
|
|
3875
|
+
"aria-haspopup": "menu",
|
|
3876
|
+
"aria-expanded": menuOpen,
|
|
3877
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3878
|
+
className: "dsh-model-menu-triggerLabel",
|
|
3879
|
+
children: selectedLabel
|
|
3880
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutlineRegular, { className: `dsh-model-menu-chevron${menuOpen ? " dsh-model-menu-chevronOpen" : ""}` })]
|
|
3881
|
+
}),
|
|
3882
|
+
choices: selectableChoices,
|
|
3883
|
+
selected: selectedRoute,
|
|
3884
|
+
onPick: (route) => edit({
|
|
3885
|
+
...draft,
|
|
3886
|
+
model: modelRouteKey(route.provider, route.model)
|
|
3887
|
+
}),
|
|
3888
|
+
leading: [{
|
|
3889
|
+
id: "follow",
|
|
3890
|
+
label: copy("modelFollowOption"),
|
|
3891
|
+
selected: !selectedRoute
|
|
3892
|
+
}],
|
|
3893
|
+
onPickLeading: () => edit({
|
|
3894
|
+
...draft,
|
|
3895
|
+
model: ""
|
|
3896
|
+
}),
|
|
3897
|
+
modelValue: selectedLabel
|
|
3898
|
+
}) }),
|
|
3899
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
3900
|
+
className: "dsh-ui-hint",
|
|
3901
|
+
children: copy("modelGroupHint")
|
|
3902
|
+
})
|
|
3903
|
+
]
|
|
3904
|
+
}),
|
|
3905
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelCatalogStatus, {
|
|
3906
|
+
state: catalog,
|
|
3907
|
+
locale: props.locale
|
|
3908
|
+
})
|
|
3909
|
+
]
|
|
3910
|
+
})]
|
|
3911
|
+
}),
|
|
3912
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3913
|
+
className: "dsm-feature",
|
|
3914
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3915
|
+
className: "dsh-ui-toggle-row",
|
|
3916
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
3917
|
+
className: "dsh-ui-toggle-text",
|
|
3918
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3919
|
+
className: "dsh-ui-toggle-label",
|
|
3920
|
+
children: copy("compactEnabledLabel")
|
|
3921
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3922
|
+
className: "dsh-ui-hint",
|
|
3923
|
+
children: copy("compactEnabledHint")
|
|
3924
|
+
})]
|
|
3925
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Switch, {
|
|
3926
|
+
label: copy("compactEnabledLabel"),
|
|
3927
|
+
checked: draft.compactEnabled,
|
|
3928
|
+
onChange: (next) => edit({
|
|
3929
|
+
...draft,
|
|
3930
|
+
compactEnabled: next
|
|
3931
|
+
})
|
|
3932
|
+
})]
|
|
3933
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3934
|
+
className: "dsm-subs",
|
|
3935
|
+
"data-disabled": draft.compactEnabled ? void 0 : "true",
|
|
3936
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
3937
|
+
className: "dsh-ui-field",
|
|
3938
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3939
|
+
className: "dsh-ui-label",
|
|
3940
|
+
children: copy("compactMinutesLabel")
|
|
3941
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Input, {
|
|
3942
|
+
className: "dsm-minutes",
|
|
3943
|
+
value: draft.compactMinutes,
|
|
3944
|
+
inputMode: "numeric",
|
|
3945
|
+
disabled: !draft.compactEnabled,
|
|
3946
|
+
"aria-invalid": compactInvalid || void 0,
|
|
3947
|
+
"aria-describedby": compactInvalid ? `${tabsId}-compact-error` : void 0,
|
|
3948
|
+
onChange: (event) => edit({
|
|
3949
|
+
...draft,
|
|
3950
|
+
compactMinutes: event.target.value
|
|
3951
|
+
})
|
|
3952
|
+
})]
|
|
3953
|
+
}), compactInvalid ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
3954
|
+
id: `${tabsId}-compact-error`,
|
|
3955
|
+
className: "dsh-ui-error",
|
|
3956
|
+
role: "alert",
|
|
3957
|
+
children: copy("minutesInvalid")
|
|
3958
|
+
}) : null] })
|
|
3959
|
+
})]
|
|
3960
|
+
})
|
|
3961
|
+
]
|
|
3962
|
+
}),
|
|
3963
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3964
|
+
className: "dsh-ui-actions",
|
|
3965
|
+
children: [
|
|
3966
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
3967
|
+
variant: "primary",
|
|
3968
|
+
disabled: invalid || snapshot.saving,
|
|
3969
|
+
onClick: save,
|
|
3970
|
+
children: snapshot.saving ? copy("settingsSaving") : copy("saveSettings")
|
|
3971
|
+
}),
|
|
3972
|
+
snapshot.saved ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3973
|
+
className: "dsh-ui-notice",
|
|
3974
|
+
role: "status",
|
|
3975
|
+
children: copy("settingsSaved")
|
|
3976
|
+
}) : null,
|
|
3977
|
+
externalChange ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3978
|
+
className: "dsh-ui-notice dsh-ui-notice--error",
|
|
3979
|
+
role: "alert",
|
|
3980
|
+
children: copy("externalChange")
|
|
3981
|
+
}) : null,
|
|
3982
|
+
snapshot.failure ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3983
|
+
className: "dsh-ui-notice dsh-ui-notice--error",
|
|
3984
|
+
role: "alert",
|
|
3985
|
+
children: failureText(props.locale, snapshot.failure)
|
|
3986
|
+
}) : null
|
|
3987
|
+
]
|
|
3988
|
+
})
|
|
3989
|
+
]
|
|
3990
|
+
});
|
|
3991
|
+
}
|
|
3992
|
+
//#endregion
|
|
1577
3993
|
//#region src/user-actions.tsx
|
|
1578
3994
|
function UserMessageActions(props) {
|
|
1579
3995
|
const snapshot = (0, react.useSyncExternalStore)(props.model.subscribe, props.model.getSnapshot, props.model.getSnapshot);
|
|
@@ -1649,7 +4065,6 @@ function UserMessageActions(props) {
|
|
|
1649
4065
|
children: copy("cancel")
|
|
1650
4066
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
1651
4067
|
variant: "primary",
|
|
1652
|
-
"data-modal-autofocus": true,
|
|
1653
4068
|
disabled: snapshot.busy,
|
|
1654
4069
|
onClick: () => {
|
|
1655
4070
|
props.model.sendEdit(props.sessionId, props.messageId, signal.current.signal);
|
|
@@ -1657,7 +4072,8 @@ function UserMessageActions(props) {
|
|
|
1657
4072
|
children: copy("send")
|
|
1658
4073
|
})] }),
|
|
1659
4074
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
|
|
1660
|
-
className: "dsh-free-draft",
|
|
4075
|
+
className: "dsh-ui-textarea dsh-free-draft",
|
|
4076
|
+
"data-modal-autofocus": true,
|
|
1661
4077
|
"aria-label": copy("draft"),
|
|
1662
4078
|
value: draft,
|
|
1663
4079
|
onChange: (event) => {
|
|
@@ -1679,7 +4095,9 @@ const inject = [
|
|
|
1679
4095
|
"shortcuts",
|
|
1680
4096
|
"connection",
|
|
1681
4097
|
"workspaces",
|
|
1682
|
-
"layout"
|
|
4098
|
+
"layout",
|
|
4099
|
+
"conversation",
|
|
4100
|
+
...MODEL_CATALOG_INJECT
|
|
1683
4101
|
];
|
|
1684
4102
|
function CopyThreadMenuItem(props) {
|
|
1685
4103
|
const [failed, setFailed] = (0, react.useState)(false);
|
|
@@ -1706,6 +4124,14 @@ function apply(ctx) {
|
|
|
1706
4124
|
const temporaryHost = bindTemporaryWorkspaceHost(client);
|
|
1707
4125
|
const temporary = createTemporaryWorkspaceModel(temporaryHost, void 0, () => uiText(copyLocale(client.locale?.getSnapshot().active), "tempWorkspace"));
|
|
1708
4126
|
const currentLocale = () => client.locale?.getSnapshot().active;
|
|
4127
|
+
const presenceHost = bindPresenceHost(client);
|
|
4128
|
+
const continues = createContinueModel(presenceHost);
|
|
4129
|
+
const welcomeBack = createWelcomeBackModel(presenceHost);
|
|
4130
|
+
const presenceConfig = createPresenceConfigModel(presenceHost);
|
|
4131
|
+
const settingsSubscriber = createModelSettingsSubscriber(client, PLUGIN_NAME);
|
|
4132
|
+
const subscribeSettings = (onChange, onError) => settingsSubscriber.subscribe(onChange, onError);
|
|
4133
|
+
const modelCatalog = () => readModelCatalog(client);
|
|
4134
|
+
const subscribeCatalog = (onChange) => client.connection?.generation?.subscribe(onChange);
|
|
1709
4135
|
client.effect(() => {
|
|
1710
4136
|
const stops = [];
|
|
1711
4137
|
if (typeof document !== "undefined") stops.push(installContextMenuForwarding(document));
|
|
@@ -1740,6 +4166,40 @@ function apply(ctx) {
|
|
|
1740
4166
|
locale: useLiveLocale(client.locale),
|
|
1741
4167
|
model: retries
|
|
1742
4168
|
}))));
|
|
4169
|
+
stops.push(client.slots.inject(CONTINUE_GHOST_SLOT, () => client.slots.register({
|
|
4170
|
+
name: CONTINUE_GHOST_SLOT,
|
|
4171
|
+
id: CONTINUE_GHOST_ID,
|
|
4172
|
+
order: 10
|
|
4173
|
+
}, (props) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ContinueGhost, {
|
|
4174
|
+
sessionId: props.sessionId,
|
|
4175
|
+
useInput: props.useInput,
|
|
4176
|
+
useSession: props.useSession,
|
|
4177
|
+
useSessionStatus: props.useSessionStatus,
|
|
4178
|
+
inputActions: props.inputActions,
|
|
4179
|
+
locale: useLiveLocale(client.locale),
|
|
4180
|
+
model: continues
|
|
4181
|
+
}))));
|
|
4182
|
+
stops.push(client.slots.inject(WELCOME_BACK_CARD_SLOT, () => client.slots.register({
|
|
4183
|
+
name: WELCOME_BACK_CARD_SLOT,
|
|
4184
|
+
id: WELCOME_BACK_CARD_ID,
|
|
4185
|
+
order: 30
|
|
4186
|
+
}, (props) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WelcomeBackCard, {
|
|
4187
|
+
sessionId: props.sessionId,
|
|
4188
|
+
turn: props.turn,
|
|
4189
|
+
useChat: props.useChat,
|
|
4190
|
+
useInput: props.useInput,
|
|
4191
|
+
locale: useLiveLocale(client.locale),
|
|
4192
|
+
model: welcomeBack
|
|
4193
|
+
}))));
|
|
4194
|
+
stops.push(client.slots.inject("plugins.bundle.config", () => client.slots.register({
|
|
4195
|
+
name: "plugins.bundle.config",
|
|
4196
|
+
key: "@klarkxy/dsh-session-manager"
|
|
4197
|
+
}, () => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SessionManagerSettings, {
|
|
4198
|
+
model: presenceConfig,
|
|
4199
|
+
locale: useLiveLocale(client.locale),
|
|
4200
|
+
modelCatalog,
|
|
4201
|
+
subscribeCatalog
|
|
4202
|
+
}))));
|
|
1743
4203
|
stops.push(client.slots.inject("shell.overlay", () => client.slots.register({
|
|
1744
4204
|
name: "shell.overlay",
|
|
1745
4205
|
id: DELETE_DIALOG_ID,
|
|
@@ -1768,9 +4228,71 @@ function apply(ctx) {
|
|
|
1768
4228
|
})
|
|
1769
4229
|
] });
|
|
1770
4230
|
})));
|
|
4231
|
+
const findCurrent = () => Object.values(client.sessions?.list.getSnapshot().byId ?? {}).find((row) => (row?.retainedBy?.mainView ?? 0) > 0)?.id ?? "";
|
|
4232
|
+
let current = findCurrent();
|
|
4233
|
+
const onReturn = () => {
|
|
4234
|
+
const id = findCurrent();
|
|
4235
|
+
if (id !== "") welcomeBack.check(id);
|
|
4236
|
+
};
|
|
4237
|
+
let lastThresholds;
|
|
4238
|
+
let lastContinuation;
|
|
4239
|
+
stops.push(presenceConfig.subscribe(() => {
|
|
4240
|
+
const config = presenceConfig.getSnapshot();
|
|
4241
|
+
if (config.phase !== "ready" || config.saving || config.values === void 0) return;
|
|
4242
|
+
const continuation = JSON.stringify([
|
|
4243
|
+
config.values.continueMode,
|
|
4244
|
+
config.values.continueText,
|
|
4245
|
+
config.values.ghostModel
|
|
4246
|
+
]);
|
|
4247
|
+
if (continuation !== lastContinuation) {
|
|
4248
|
+
lastContinuation = continuation;
|
|
4249
|
+
continues.invalidateAll();
|
|
4250
|
+
}
|
|
4251
|
+
const thresholds = `${config.values.recapMinutes}:${config.values.compactMinutes}`;
|
|
4252
|
+
if (thresholds !== lastThresholds) {
|
|
4253
|
+
lastThresholds = thresholds;
|
|
4254
|
+
welcomeBack.invalidate();
|
|
4255
|
+
onReturn();
|
|
4256
|
+
}
|
|
4257
|
+
}));
|
|
4258
|
+
stops.push(subscribeSettings(() => {
|
|
4259
|
+
presenceConfig.load();
|
|
4260
|
+
}));
|
|
4261
|
+
const refreshConfig = () => {
|
|
4262
|
+
presenceConfig.load().then(onReturn);
|
|
4263
|
+
};
|
|
4264
|
+
const watchGeneration = client.connection?.generation?.subscribe(refreshConfig);
|
|
4265
|
+
if (watchGeneration) stops.push(watchGeneration);
|
|
4266
|
+
if (typeof window !== "undefined") {
|
|
4267
|
+
const refreshOnReturn = refreshConfig;
|
|
4268
|
+
const onVisibility = () => {
|
|
4269
|
+
if (!document.hidden) refreshOnReturn();
|
|
4270
|
+
};
|
|
4271
|
+
window.addEventListener("focus", refreshOnReturn);
|
|
4272
|
+
document.addEventListener("visibilitychange", onVisibility);
|
|
4273
|
+
const timer = setTimeout(refreshOnReturn, 1500);
|
|
4274
|
+
const idleTimer = setInterval(() => {
|
|
4275
|
+
if (!document.hidden) onReturn();
|
|
4276
|
+
}, 3e4);
|
|
4277
|
+
stops.push(() => {
|
|
4278
|
+
window.removeEventListener("focus", refreshOnReturn);
|
|
4279
|
+
document.removeEventListener("visibilitychange", onVisibility);
|
|
4280
|
+
clearTimeout(timer);
|
|
4281
|
+
clearInterval(idleTimer);
|
|
4282
|
+
});
|
|
4283
|
+
}
|
|
4284
|
+
const watchList = client.sessions?.list.subscribe(() => {
|
|
4285
|
+
const id = findCurrent();
|
|
4286
|
+
if (id !== current) {
|
|
4287
|
+
current = id;
|
|
4288
|
+
if (id !== "") welcomeBack.check(id);
|
|
4289
|
+
}
|
|
4290
|
+
});
|
|
4291
|
+
if (typeof watchList === "function") stops.push(watchList);
|
|
1771
4292
|
const reset = client.on?.("connection/reset", () => {
|
|
1772
4293
|
chats.reloadCapabilities();
|
|
1773
4294
|
retries.load();
|
|
4295
|
+
refreshConfig();
|
|
1774
4296
|
});
|
|
1775
4297
|
if (typeof reset === "function") stops.push(reset);
|
|
1776
4298
|
chats.reloadCapabilities();
|
|
@@ -1779,6 +4301,9 @@ function apply(ctx) {
|
|
|
1779
4301
|
chats.cancel();
|
|
1780
4302
|
retries.cancel();
|
|
1781
4303
|
temporary.cancel();
|
|
4304
|
+
continues.cancel();
|
|
4305
|
+
welcomeBack.cancel();
|
|
4306
|
+
presenceConfig.cancel();
|
|
1782
4307
|
for (const stop of stops) stop();
|
|
1783
4308
|
};
|
|
1784
4309
|
});
|
|
@@ -1800,6 +4325,7 @@ function DeleteMenuItem(props) {
|
|
|
1800
4325
|
const [, setMenuOpen] = props.useMenuOpenState();
|
|
1801
4326
|
if (!snapshot.controls.delete) return null;
|
|
1802
4327
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.MenuItemButton, {
|
|
4328
|
+
danger: true,
|
|
1803
4329
|
onSelect: () => {
|
|
1804
4330
|
props.model.requestDelete(props.sessionId);
|
|
1805
4331
|
setMenuOpen(false);
|