@klarkxy/dsh-session-manager 0.1.2 → 0.2.1

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