@klarkxy/dsh-session-manager 0.1.0-rc.1 → 0.1.0

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,6 +8,15 @@ 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
+ //#region src/contracts.ts
12
+ const COPY_THREAD_MENU_ID = "@klarkxy/dsh-session-manager.copy-thread-id";
13
+ const COPY_THREAD_SHORTCUT_ID = "session-manager.copy-thread-id";
14
+ const TEMP_WORKSPACE_CHANNEL = "/dsh-session-manager";
15
+ const TEMP_WORKSPACE_FLOW_SLOT = "session-manager.workspace.directoryFlow";
16
+ const USER_ACTIONS_SLOT = "conversation.chat.user-actions";
17
+ const USER_ACTIONS_ID = "@klarkxy/dsh-session-manager.user-actions";
18
+ const DELETE_DIALOG_ID = "@klarkxy/dsh-session-manager.delete-chat";
19
+ //#endregion
11
20
  //#region src/copy-action.ts
12
21
  const COPY_THREAD_LABEL = {
13
22
  zh: "复制线程 ID",
@@ -17,6 +26,14 @@ const COPY_THREAD_FAILED = {
17
26
  zh: "无法复制线程 ID",
18
27
  en: "Could not copy the thread ID"
19
28
  };
29
+ const COPY_THREAD_COPIED = {
30
+ zh: "已复制线程 ID",
31
+ en: "Thread ID copied"
32
+ };
33
+ const COPY_THREAD_NO_SESSION = {
34
+ zh: "请先打开一个会话",
35
+ en: "Open a session first"
36
+ };
20
37
  function copyLocale(active) {
21
38
  return active?.toLowerCase().startsWith("zh") ? "zh" : "en";
22
39
  }
@@ -25,49 +42,138 @@ async function copyThreadId(sessionId, writeText) {
25
42
  await writeText(sessionId);
26
43
  }
27
44
  //#endregion
28
- //#region src/contracts.ts
29
- const COPY_THREAD_MENU_ID = "@klarkxy/dsh-session-manager.copy-thread-id";
30
- /** Sidebar list id and the matching main panel key. */
31
- const FREE_PANEL_ID = "free-chat";
32
- const USER_ACTIONS_SLOT = "conversation.chat.user-actions";
33
- const USER_ACTIONS_ID = "@klarkxy/dsh-session-manager.user-actions";
34
- const DELETE_DIALOG_ID = "@klarkxy/dsh-session-manager.delete-chat";
45
+ //#region src/copy-shortcut.tsx
46
+ async function writeClipboard(value) {
47
+ const clipboard = globalThis.navigator?.clipboard;
48
+ if (clipboard === void 0) throw new Error("clipboard unavailable");
49
+ await clipboard.writeText(value);
50
+ }
51
+ /** Uses the same mainView reference as ui-workspace/src/client/shortcuts.ts.
52
+ * The host registry owns key dispatch, remapping, conflicts, IME and repeat guards.
53
+ */
54
+ function CopyThreadShortcutNotice({ shortcuts, list, locale, currentLocale, writeText = writeClipboard }) {
55
+ const [notice, setNotice] = (0, react.useState)(null);
56
+ const sequence = (0, react.useRef)(0);
57
+ (0, react.useEffect)(() => {
58
+ if (shortcuts === void 0) return;
59
+ let active = true;
60
+ const notify = (copied) => {
61
+ if (active) setNotice({
62
+ copied,
63
+ seq: ++sequence.current
64
+ });
65
+ };
66
+ const binding = {
67
+ code: "KeyC",
68
+ modifiers: ["primary", "alt"]
69
+ };
70
+ const stop = shortcuts.register({
71
+ id: COPY_THREAD_SHORTCUT_ID,
72
+ label: () => COPY_THREAD_LABEL[copyLocale(currentLocale())],
73
+ aliases: [
74
+ "copy thread id",
75
+ "copy session id",
76
+ "复制线程 ID"
77
+ ],
78
+ defaults: {
79
+ "desktop:windows": binding,
80
+ "desktop:macos": binding,
81
+ "web:windows": binding,
82
+ "web:macos": binding
83
+ },
84
+ regions: ["page", "editable"],
85
+ modals: [],
86
+ resolve(context) {
87
+ const target = Object.values(list?.getSnapshot().byId ?? {}).find((row) => (row?.retainedBy?.mainView ?? 0) > 0);
88
+ if (target === void 0 || context.modal !== null || context.region === "terminal") return {
89
+ status: "blocked",
90
+ reason: COPY_THREAD_NO_SESSION[copyLocale(currentLocale())]
91
+ };
92
+ return {
93
+ status: "handled",
94
+ run() {
95
+ copyThreadId(target.id, writeText).then(() => notify(true), () => notify(false));
96
+ }
97
+ };
98
+ }
99
+ });
100
+ return () => {
101
+ active = false;
102
+ stop();
103
+ };
104
+ }, [
105
+ shortcuts,
106
+ list,
107
+ currentLocale,
108
+ writeText
109
+ ]);
110
+ return notice === null ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Toast, {
111
+ text: (notice.copied ? COPY_THREAD_COPIED : COPY_THREAD_FAILED)[locale],
112
+ tone: notice.copied ? "success" : void 0,
113
+ onDone: () => setNotice(null)
114
+ }, notice.seq);
115
+ }
35
116
  //#endregion
36
- //#region src/free-list.ts
117
+ //#region src/context-menu.ts
37
118
  /**
38
- * User-visible free chats. Host list membership is `ids` only.
39
- * Owned subagents stay out; an independent fork with classification free stays in.
119
+ * Right-click forwarding for the sidebar browser: a `contextmenu` gesture on a
120
+ * session or workspace row clicks that row's native "..." trigger, so the
121
+ * host's own menu — including every `sidebar.workspaces.session.menu.item`
122
+ * slot contribution, this plugin's rows included — opens exactly as a
123
+ * left-click on the trigger would. The host owns the menu; this module only
124
+ * relays the gesture. No host code registers a contextmenu listener, so the
125
+ * capture-phase relay does not fight an existing handler.
126
+ *
127
+ * DOM contract (dsh-client-ui-workspace 0.2.0-rc.2, lib/client.js):
128
+ * - Rows are `div[role="treeitem"][data-row-key="session:<id>"]` or
129
+ * `data-row-key="workspace:<id>"` inside `div[role="tree"]`. Overflow and
130
+ * empty rows carry other prefixes, search rows carry no `data-row-key`, and
131
+ * AnimatedRows exit clones have the attribute removed — all are ignored by
132
+ * the prefix match. (client.js:1287, 1590-1596, 1729, 1842-1846)
133
+ * - The trigger is the first `button[type="button"]` inside the row's trailing
134
+ * `span` whose CSS-module class ends in `_rowActions`. It stays mounted —
135
+ * CSS `display:none` until hover or menu-open — so a programmatic click
136
+ * toggles the menu. Blank new-session rows and the ungrouped bucket render
137
+ * no trigger; right-click does nothing there. (client.js:1061, 1314-1340,
138
+ * 1640-1660)
139
+ * - Class names carry a per-build hash prefix and the trigger's aria-label is
140
+ * localized; only `data-row-key` and the structural suffix are stable.
141
+ * - The opened menu portals to `document.body`, outside the tree, so a
142
+ * right-click on a menu row never re-enters this relay.
40
143
  */
41
- function freeChatRows(snapshot) {
42
- const rows = [];
43
- for (const id of snapshot.ids) {
44
- const row = snapshot.byId[id];
45
- if (row === void 0 || row.classification !== "free" || row.origin === "subagent") continue;
46
- rows.push(row);
47
- }
48
- return rows;
144
+ const ROW_SELECTOR = "[data-row-key^=\"session:\"], [data-row-key^=\"workspace:\"]";
145
+ const TRIGGER_SELECTOR = "span[class*=\"rowActions\"] button[type=\"button\"]";
146
+ /** Resolve the native "..." trigger of the row containing `target`, if any. */
147
+ function findRowMenuTrigger(target) {
148
+ if (typeof target.closest !== "function") return null;
149
+ const row = target.closest(ROW_SELECTOR);
150
+ if (row === null || row.closest("[role=\"tree\"]") === null) return null;
151
+ const trigger = row.querySelector(TRIGGER_SELECTOR);
152
+ if (trigger === null || typeof trigger.click !== "function") return null;
153
+ return trigger;
49
154
  }
50
- /** Match the visible title or id. Directory text is not a class and is not searched. */
51
- function matchFreeRows(rows, query) {
52
- const needle = query.trim().toLowerCase();
53
- if (needle === "") return [...rows];
54
- return rows.filter((row) => row.displayTitle.toLowerCase().includes(needle) || row.id.toLowerCase().includes(needle));
155
+ /**
156
+ * Forward one `contextmenu` event to the row's native menu trigger.
157
+ * Returns true when the gesture was relayed (and the default suppressed).
158
+ */
159
+ function forwardContextMenu(event) {
160
+ const target = event.target;
161
+ if (target === null || typeof target.closest !== "function") return false;
162
+ const trigger = findRowMenuTrigger(target);
163
+ if (trigger === null) return false;
164
+ event.preventDefault();
165
+ trigger.click();
166
+ return true;
55
167
  }
56
- /** Quiet clock: time when it is today, month and day otherwise. */
57
- function formatQuietTime(updatedAt, locale, now = Date.now()) {
58
- if (!Number.isFinite(updatedAt)) return "";
59
- const date = new Date(updatedAt);
60
- const today = new Date(now);
61
- const sameDay = date.getFullYear() === today.getFullYear() && date.getMonth() === today.getMonth() && date.getDate() === today.getDate();
62
- const tag = locale === "zh" ? "zh-CN" : "en";
63
- if (sameDay) return new Intl.DateTimeFormat(tag, {
64
- hour: "2-digit",
65
- minute: "2-digit"
66
- }).format(date);
67
- return new Intl.DateTimeFormat(tag, {
68
- month: "short",
69
- day: "numeric"
70
- }).format(date);
168
+ /** Capture-phase relay on the document; the returned disposer uninstalls it. */
169
+ function installContextMenuForwarding(doc) {
170
+ const listener = (event) => {
171
+ forwardContextMenu(event);
172
+ };
173
+ doc.addEventListener("contextmenu", listener, true);
174
+ return () => {
175
+ doc.removeEventListener("contextmenu", listener, true);
176
+ };
71
177
  }
72
178
  Object.values({
73
179
  /** Surfaces, from the page up to the topmost nested card. */
@@ -366,7 +472,7 @@ ${scope} {
366
472
  border: 0.5px solid var(--dsw-alias-border-l3);
367
473
  border-radius: var(--dsw-radius-md);
368
474
  background: var(--dsw-alias-bg-layer-1);
369
- font-family: var(--ds-font-family-code, ui-monospace, 'Cascadia Mono', Consolas, monospace);
475
+ font-family: var(--ds-font-family-code);
370
476
  font-size: 12px;
371
477
  line-height: 20px;
372
478
  color: var(--dsw-alias-label-secondary);
@@ -444,60 +550,9 @@ ${scope} {
444
550
  //#endregion
445
551
  //#region src/styles.ts
446
552
  const FREE_CHAT_ROOT = "dsh-free-chat";
447
- /** Host tokens and official scaffolding. The panel stays a flat list; the modal reuses the root class. */
553
+ /** Host tokens and official scaffolding. Only the edit draft, the inline action row,
554
+ * and the alert line survive the panel removal; the modals reuse the root class. */
448
555
  const freeChatCss = `${officialUiCss(FREE_CHAT_ROOT)}
449
- .${FREE_CHAT_ROOT}.dsh-free-panel {
450
- display: flex;
451
- flex-direction: column;
452
- gap: 12px;
453
- min-width: 0;
454
- height: 100%;
455
- padding: 16px;
456
- background: var(--dsw-alias-bg-base);
457
- }
458
- .${FREE_CHAT_ROOT} .dsh-free-head {
459
- display: flex;
460
- align-items: center;
461
- gap: 12px;
462
- min-width: 0;
463
- }
464
- .${FREE_CHAT_ROOT} .dsh-free-head .dsh-ui-title { flex: 1; min-width: 0; }
465
- .${FREE_CHAT_ROOT} .dsh-free-search { width: 100%; }
466
- .${FREE_CHAT_ROOT} .dsh-free-line {
467
- display: flex;
468
- align-items: center;
469
- gap: 4px;
470
- min-width: 0;
471
- }
472
- .${FREE_CHAT_ROOT} .dsh-free-open {
473
- display: flex;
474
- align-items: center;
475
- gap: 8px;
476
- flex: 1;
477
- min-width: 0;
478
- min-height: 36px;
479
- padding: 4px 8px;
480
- border: 0;
481
- border-radius: var(--dsw-radius-sm);
482
- background: none;
483
- color: inherit;
484
- font: inherit;
485
- text-align: left;
486
- }
487
- .${FREE_CHAT_ROOT} .dsh-free-open:hover { background: var(--dsw-alias-interactive-bg-hover); }
488
- .${FREE_CHAT_ROOT} .dsh-free-title {
489
- flex: 1;
490
- min-width: 0;
491
- overflow: hidden;
492
- text-overflow: ellipsis;
493
- white-space: nowrap;
494
- }
495
- .${FREE_CHAT_ROOT} .dsh-free-time {
496
- flex: none;
497
- color: var(--dsw-alias-label-tertiary);
498
- font-size: 12px;
499
- line-height: 18px;
500
- }
501
556
  .${FREE_CHAT_ROOT} .dsh-free-draft {
502
557
  box-sizing: border-box;
503
558
  width: 100%;
@@ -519,24 +574,6 @@ const freeChatCss = `${officialUiCss(FREE_CHAT_ROOT)}
519
574
  min-width: 0;
520
575
  }
521
576
  .${FREE_CHAT_ROOT} .dsh-free-alert { margin: 0; }
522
- .${FREE_CHAT_ROOT} .dsh-free-sr {
523
- position: absolute;
524
- width: 1px;
525
- height: 1px;
526
- padding: 0;
527
- margin: -1px;
528
- overflow: hidden;
529
- clip: rect(0, 0, 0, 0);
530
- white-space: nowrap;
531
- border: 0;
532
- }
533
- @media (max-width: 640px) {
534
- .${FREE_CHAT_ROOT}.dsh-free-panel { padding: 12px; }
535
- .${FREE_CHAT_ROOT} .dsh-free-head { flex-wrap: wrap; }
536
- }
537
- @media (prefers-reduced-motion: reduce) {
538
- .${FREE_CHAT_ROOT} .dsh-free-open { transition: none; }
539
- }
540
577
  `;
541
578
  //#endregion
542
579
  //#region src/ui-copy.ts
@@ -562,18 +599,9 @@ const reasons = {
562
599
  };
563
600
  const text = {
564
601
  zh: {
565
- panel: "自由聊天",
566
- newChat: "新建聊天",
567
- empty: "还没有自由聊天。",
568
- loading: "正在读取聊天",
569
- noMatch: "没有匹配的聊天。",
570
- listError: "聊天列表没有读出来。",
602
+ tempWorkspace: "临时工作区",
603
+ createFailed: "无法创建临时工作区",
571
604
  retryList: "重试",
572
- search: "搜索聊天",
573
- running: "运行中",
574
- idle: "空闲",
575
- menu: "聊天操作",
576
- fork: "分叉",
577
605
  deleteChat: "删除聊天",
578
606
  deleting: "正在删除",
579
607
  deleteFree: "将删除这个聊天及其拥有的文件。",
@@ -587,18 +615,9 @@ const text = {
587
615
  draft: "消息文本"
588
616
  },
589
617
  en: {
590
- panel: "Free chat",
591
- newChat: "New chat",
592
- empty: "No free chats yet.",
593
- loading: "Reading chats",
594
- noMatch: "No matching chats.",
595
- listError: "The chat list could not be read.",
618
+ tempWorkspace: "Temporary Workspace",
619
+ createFailed: "Could not create the temporary workspace",
596
620
  retryList: "Retry",
597
- search: "Search chats",
598
- running: "Running",
599
- idle: "Idle",
600
- menu: "Chat actions",
601
- fork: "Fork",
602
621
  deleteChat: "Delete chat",
603
622
  deleting: "Deleting",
604
623
  deleteFree: "This deletes this chat and its owned files.",
@@ -616,7 +635,7 @@ function uiText(locale, key) {
616
635
  return text[locale][key];
617
636
  }
618
637
  function failureReason(failure) {
619
- if (failure.code === "session/lifecycle-unavailable") return "unsupported";
638
+ if (failure.code === "session/lifecycle-unavailable" || failure.code === "session/fork-unavailable") return "unsupported";
620
639
  if (failure.code === "session/retry-revision" || failure.code === "session/branch-moved" || failure.code === "session/retry-input" || failure.code === "session/retry-conflict") return "stale";
621
640
  if (failure.code === "session/delete-pending") return "cleanup";
622
641
  if (failure.code === "session/agent-busy" && failure.reason === "free-session-work-active") return "running";
@@ -647,179 +666,33 @@ function FreeChatIcon({ size }) {
647
666
  })
648
667
  });
649
668
  }
650
- function FreeChatPanel(props) {
651
- const snapshot = (0, react.useSyncExternalStore)(props.model.subscribe, props.model.getSnapshot, props.model.getSnapshot);
652
- const titleRef = (0, react.useRef)(null);
653
- const searchId = (0, react.useId)();
654
- (0, react.useEffect)(() => {
655
- if (snapshot.focusNonce === 0) return;
656
- titleRef.current?.focus();
657
- }, [snapshot.focusNonce]);
658
- const copy = (key) => uiText(props.locale, key);
659
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
660
- className: `${FREE_CHAT_ROOT} dsh-free-panel`,
661
- children: [
662
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("style", { children: freeChatCss }),
663
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
664
- className: "dsh-free-head",
665
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h1", {
666
- ref: titleRef,
667
- tabIndex: -1,
668
- className: "dsh-ui-title",
669
- children: copy("panel")
670
- }), snapshot.controls.create ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
671
- variant: "primary",
672
- size: "sm",
673
- disabled: snapshot.creating,
674
- onClick: () => {
675
- props.model.create();
676
- },
677
- children: copy("newChat")
678
- }) : null]
679
- }),
680
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Input, {
681
- id: searchId,
682
- className: "dsh-free-search",
683
- type: "search",
684
- value: snapshot.query,
685
- "aria-label": copy("search"),
686
- onChange: (event) => {
687
- props.model.setQuery(event.target.value);
688
- }
689
- }),
690
- snapshot.notice ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
691
- className: "dsh-ui-error dsh-free-alert",
692
- role: "alert",
693
- children: failureText(props.locale, snapshot.notice)
694
- }) : null,
695
- snapshot.listStatus === "loading" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
696
- className: "dsh-ui-loading",
697
- children: copy("loading")
698
- }) : null,
699
- snapshot.listStatus === "error" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
700
- className: "dsh-ui-empty",
701
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
702
- role: "alert",
703
- children: snapshot.listFailure?.reason && snapshot.listFailure.reason !== "unknown" ? snapshot.listFailure.reason : copy("listError")
704
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
705
- size: "sm",
706
- onClick: () => {
707
- props.model.refreshList();
708
- },
709
- children: copy("retryList")
710
- })]
711
- }) : null,
712
- snapshot.listStatus === "empty" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
713
- className: "dsh-ui-empty",
714
- children: copy("empty")
715
- }) : null,
716
- snapshot.listStatus === "nomatch" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
717
- className: "dsh-ui-empty",
718
- children: copy("noMatch")
719
- }) : null,
720
- snapshot.rows.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
721
- className: "dsh-ui-list",
722
- "aria-label": copy("panel"),
723
- children: snapshot.rows.map((row) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FreeChatRow, {
724
- model: props.model,
725
- locale: props.locale,
726
- row
727
- }, row.id))
728
- }) : null
729
- ]
730
- });
731
- }
732
- function FreeChatRow(props) {
733
- const [open, setOpen] = (0, react.useState)(false);
734
- const [copyFailed, setCopyFailed] = (0, react.useState)(false);
735
- const snapshot = (0, react.useSyncExternalStore)(props.model.subscribe, props.model.getSnapshot, props.model.getSnapshot);
669
+ /** Create failures used to surface inside the standalone panel. The panel is gone,
670
+ * so they get a small overlay instead; closing it dismisses the stored notice. */
671
+ function CreateNoticeDialog(props) {
672
+ const notice = (0, react.useSyncExternalStore)(props.model.subscribe, props.model.getSnapshot, props.model.getSnapshot).notice;
673
+ if (notice === void 0) return null;
736
674
  const copy = (key) => uiText(props.locale, key);
737
- const items = [];
738
- if (snapshot.controls.fork) items.push({
739
- id: "fork",
740
- label: copy("fork")
741
- });
742
- items.push({
743
- id: "copy",
744
- label: copyFailed ? COPY_THREAD_FAILED[props.locale] : COPY_THREAD_LABEL[props.locale]
745
- });
746
- if (snapshot.controls.delete) items.push({
747
- id: "delete",
748
- label: copy("deleteChat"),
749
- danger: true
750
- });
751
- const onSelect = (id) => {
752
- if (id === "copy") {
753
- copyThreadId(props.row.id, writeClipboard$1).then(() => {
754
- setCopyFailed(false);
755
- setOpen(false);
756
- }, () => {
757
- setCopyFailed(true);
758
- setOpen(true);
759
- });
760
- return;
761
- }
762
- setOpen(false);
763
- if (id === "fork") props.model.fork(props.row.id);
764
- if (id === "delete") props.model.requestDelete(props.row.id);
765
- };
766
- const state = props.row.running ? copy("running") : copy("idle");
767
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
768
- className: "dsh-ui-list-row dsh-free-line",
769
- children: [
770
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
771
- type: "button",
772
- className: "dsh-free-open",
773
- onClick: () => {
774
- props.model.open(props.row.id);
775
- },
776
- children: [
777
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.StateDot, {
778
- state: props.row.running ? "ongoing" : "idle",
779
- size: 8
780
- }),
781
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
782
- className: "dsh-free-sr",
783
- children: state
784
- }),
785
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
786
- className: "dsh-free-title",
787
- children: props.row.displayTitle
788
- }),
789
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("time", {
790
- className: "dsh-free-time",
791
- dateTime: new Date(props.row.updatedAt).toISOString(),
792
- children: formatQuietTime(props.row.updatedAt, props.locale)
793
- })
794
- ]
795
- }),
796
- copyFailed ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
797
- className: "dsh-ui-error dsh-free-alert",
798
- role: "alert",
799
- children: COPY_THREAD_FAILED[props.locale]
800
- }) : null,
801
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Menu, {
802
- open,
803
- onClose: () => {
804
- setOpen(false);
805
- },
806
- portal: true,
807
- listClassName: FREE_CHAT_ROOT,
808
- anchor: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
809
- size: "sm",
810
- variant: "ghost",
811
- "aria-label": copy("menu"),
812
- "aria-haspopup": "menu",
813
- "aria-expanded": open,
814
- onClick: () => {
815
- setOpen((value) => !value);
816
- },
817
- children: "···"
818
- }),
819
- items,
820
- onSelect
821
- })
822
- ]
675
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Modal, {
676
+ open: true,
677
+ className: FREE_CHAT_ROOT,
678
+ title: copy("createFailed"),
679
+ closeLabel: copy("close"),
680
+ onClose: () => {
681
+ props.model.dismissNotice();
682
+ },
683
+ footer: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
684
+ variant: "primary",
685
+ "data-modal-autofocus": true,
686
+ onClick: () => {
687
+ props.model.dismissNotice();
688
+ },
689
+ children: copy("close")
690
+ }),
691
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
692
+ className: "dsh-ui-error dsh-free-alert",
693
+ role: "alert",
694
+ children: failureText(props.locale, notice)
695
+ })
823
696
  });
824
697
  }
825
698
  function DeleteChatDialog(props) {
@@ -864,10 +737,310 @@ function DeleteChatDialog(props) {
864
737
  }) : null]
865
738
  });
866
739
  }
867
- async function writeClipboard$1(value) {
868
- const clipboard = globalThis.navigator?.clipboard;
869
- if (clipboard === void 0) throw new Error("clipboard unavailable");
870
- await clipboard.writeText(value);
740
+ //#endregion
741
+ //#region src/temporary-picker.tsx
742
+ const PICKER_SLOT = "conversation.hero.workspace";
743
+ const DIRECTORY_SLOT = "conversation.hero.workspace.directoryFlow";
744
+ /** Keep the host's native/web picker implementation and its injected capabilities.
745
+ * Standard rc.2 offers no exported directory-flow factory. Only its simple root
746
+ * registration can be mirrored; more complex future registrations restore the host picker.
747
+ * Original entries and their ownership ledger are never changed.
748
+ */
749
+ function installTemporaryWorkspacePicker(slots, model, createWorkspace, locale) {
750
+ if (typeof slots.entriesOfSlot !== "function" || typeof slots.spec !== "function" || typeof slots.subscribe !== "function") {
751
+ model.reportFailure(copyLocale(locale?.getSnapshot().active) === "zh" ? "当前版本无法扩展工作区菜单,已保留原菜单。" : "Could not extend the workspace menu. The original menu has been preserved.");
752
+ return () => {};
753
+ }
754
+ return slots.inject(PICKER_SLOT, () => {
755
+ let mirror, stopMirror = () => {}, restored = false;
756
+ const failureMessage = () => copyLocale(locale?.getSnapshot().active) === "zh" ? "当前版本无法扩展工作区菜单,已保留原菜单。" : "Could not extend the workspace menu. The original menu has been preserved.";
757
+ const renderPicker = (props) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TemporaryWorkspacePicker, {
758
+ ...props,
759
+ model,
760
+ createWorkspace,
761
+ locale,
762
+ getDirectoryFlow: () => mirror,
763
+ subscribeFlow: (listener) => slots.subscribe(DIRECTORY_SLOT, listener)
764
+ });
765
+ const stopPicker = slots.register({
766
+ name: PICKER_SLOT,
767
+ priority: -10,
768
+ locale: "workspace",
769
+ children: { [TEMP_WORKSPACE_FLOW_SLOT]: {
770
+ kind: "single",
771
+ scope: "root"
772
+ } }
773
+ }, renderPicker);
774
+ const stopErrors = slots.onEntryError?.((key, entry) => {
775
+ if (restored || key !== "session-manager.workspace.directoryFlow" && !(key === PICKER_SLOT && entry.component === renderPicker)) return;
776
+ restored = true;
777
+ stopMirror();
778
+ mirror = void 0;
779
+ stopPicker();
780
+ model.reportFailure(failureMessage());
781
+ }) ?? (() => {});
782
+ const sync = () => {
783
+ if (restored) return;
784
+ const next = slots.entriesOfSlot(DIRECTORY_SLOT)[0];
785
+ if (next === mirror) return;
786
+ stopMirror();
787
+ stopMirror = () => {};
788
+ mirror = void 0;
789
+ if (next === void 0) return;
790
+ const spec = slots.spec(DIRECTORY_SLOT);
791
+ if (spec?.kind !== "single" || spec.scope !== "root" || typeof next.component !== "function" || next.children !== void 0 || next.store !== void 0 || next.locale !== void 0 || next.select !== void 0) {
792
+ restored = true;
793
+ stopPicker();
794
+ model.reportFailure(failureMessage());
795
+ return;
796
+ }
797
+ try {
798
+ stopMirror = slots.register({
799
+ name: TEMP_WORKSPACE_FLOW_SLOT,
800
+ ...next.inject ? { inject: next.inject } : {}
801
+ }, next.component);
802
+ mirror = next;
803
+ } catch (error) {
804
+ restored = true;
805
+ stopPicker();
806
+ model.reportFailure(error instanceof Error ? error.message : "Could not extend the workspace picker");
807
+ }
808
+ };
809
+ const stopSource = slots.subscribe(DIRECTORY_SLOT, sync);
810
+ sync();
811
+ return () => {
812
+ restored = true;
813
+ stopErrors();
814
+ stopSource();
815
+ stopMirror();
816
+ stopPicker();
817
+ };
818
+ });
819
+ }
820
+ function TemporaryWorkspacePicker(props) {
821
+ const workspaces = props.useWorkspaces((snapshot) => snapshot);
822
+ const state = (0, react.useSyncExternalStore)(props.model.subscribe, props.model.getSnapshot, props.model.getSnapshot);
823
+ const source = (0, react.useSyncExternalStore)(props.subscribeFlow, props.getDirectoryFlow, props.getDirectoryFlow);
824
+ const available = source !== void 0;
825
+ const locale = copyLocale((0, react.useSyncExternalStore)((listener) => props.locale?.subscribe(listener) ?? (() => {}), () => props.locale?.getSnapshot().active ?? "en", () => "en"));
826
+ const [flowOpen, setFlowOpen] = (0, react.useState)(false), [adopting, setAdopting] = (0, react.useState)(false);
827
+ const [failure, setFailure] = (0, react.useState)();
828
+ const epoch = (0, react.useRef)(0);
829
+ (0, react.useEffect)(() => () => {
830
+ epoch.current++;
831
+ }, []);
832
+ (0, react.useEffect)(() => {
833
+ epoch.current++;
834
+ setFlowOpen(false);
835
+ setAdopting(false);
836
+ }, [source]);
837
+ const busy = flowOpen || adopting || state.creating;
838
+ const getAnchorRect = (0, react.useCallback)(() => props.anchorRef?.current?.getBoundingClientRect() ?? null, [props.anchorRef]);
839
+ const openDirectory = () => {
840
+ props.onClose();
841
+ setFailure(void 0);
842
+ setFlowOpen(true);
843
+ };
844
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
845
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Menu, {
846
+ open: props.open,
847
+ anchor: null,
848
+ portal: true,
849
+ getAnchorRect,
850
+ selectedId: props.selectedId,
851
+ onClose: props.onClose,
852
+ items: workspaces.items.map((workspace) => ({
853
+ id: workspace.workspaceId,
854
+ label: workspace.title === "default-workspace" ? props.t("workspace.defaultName") : workspace.title,
855
+ icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconFolderCloseRegular, { size: 16 }),
856
+ disabled: busy
857
+ })),
858
+ footer: [...available ? [{
859
+ id: "::add-workspace",
860
+ label: props.t("menu.addWorkspace"),
861
+ icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconPlusOutlineRegular, { size: 16 }),
862
+ disabled: busy
863
+ }] : [], {
864
+ id: "::temporary-workspace",
865
+ label: uiText(locale, "tempWorkspace"),
866
+ icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FreeChatIcon, { size: 16 }),
867
+ disabled: busy
868
+ }],
869
+ onSelect: (id) => {
870
+ if (id === "::add-workspace") openDirectory();
871
+ else if (id === "::temporary-workspace") {
872
+ props.onClose();
873
+ props.model.create();
874
+ } else props.onPick(id);
875
+ }
876
+ }),
877
+ props.open && workspaces.phase === "pending" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
878
+ role: "status",
879
+ children: props.t("picker.loading")
880
+ }),
881
+ props.renderSlot(TEMP_WORKSPACE_FLOW_SLOT, {
882
+ open: flowOpen,
883
+ busy: adopting,
884
+ onCancel: () => {
885
+ epoch.current++;
886
+ setFlowOpen(false);
887
+ setAdopting(false);
888
+ },
889
+ onError: (message) => {
890
+ epoch.current++;
891
+ setFlowOpen(false);
892
+ setAdopting(false);
893
+ setFailure(message);
894
+ },
895
+ onPicked: (path) => {
896
+ if (adopting) return;
897
+ const ticket = ++epoch.current;
898
+ setAdopting(true);
899
+ props.createWorkspace(path).then((workspace) => {
900
+ if (ticket !== epoch.current) return;
901
+ setFlowOpen(false);
902
+ props.onPick(workspace.workspaceId);
903
+ }, (error) => {
904
+ if (ticket !== epoch.current) return;
905
+ setFlowOpen(false);
906
+ setFailure(error instanceof Error ? error.message : String(error));
907
+ }).finally(() => {
908
+ if (ticket === epoch.current) setAdopting(false);
909
+ });
910
+ }
911
+ }),
912
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Modal, {
913
+ open: failure !== void 0,
914
+ title: props.t("folderError.title"),
915
+ closeLabel: props.t("close"),
916
+ onClose: () => setFailure(void 0),
917
+ 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, {
918
+ variant: "outline",
919
+ onClick: () => setFailure(void 0),
920
+ children: props.t("cancel")
921
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
922
+ variant: "primary",
923
+ disabled: !available,
924
+ onClick: openDirectory,
925
+ children: props.t("folderError.retry")
926
+ })] }),
927
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
928
+ role: "alert",
929
+ children: failure
930
+ })
931
+ })
932
+ ] });
933
+ }
934
+ function TemporaryWorkspaceNotice({ model, locale }) {
935
+ const state = (0, react.useSyncExternalStore)(model.subscribe, model.getSnapshot, model.getSnapshot);
936
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Modal, {
937
+ open: state.failure !== void 0,
938
+ title: uiText(locale, "createFailed"),
939
+ closeLabel: uiText(locale, "close"),
940
+ onClose: model.dismissFailure,
941
+ 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, {
942
+ variant: "outline",
943
+ onClick: model.dismissFailure,
944
+ children: uiText(locale, "close")
945
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
946
+ variant: "primary",
947
+ disabled: state.creating,
948
+ onClick: () => {
949
+ model.create();
950
+ },
951
+ children: uiText(locale, "retry")
952
+ })] }),
953
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
954
+ role: "alert",
955
+ children: state.failure
956
+ })
957
+ });
958
+ }
959
+ //#endregion
960
+ //#region src/temporary-workspace.ts
961
+ function createTemporaryWorkspaceModel(host, ids = () => crypto.randomUUID(), label = () => "Temporary Workspace") {
962
+ const lifetime = new AbortController(), listeners = /* @__PURE__ */ new Set();
963
+ let snapshot = { creating: false };
964
+ let pending;
965
+ const publish = (next) => {
966
+ if (lifetime.signal.aborted) return;
967
+ snapshot = next;
968
+ for (const listener of listeners) listener();
969
+ };
970
+ return {
971
+ getSnapshot: () => snapshot,
972
+ subscribe(listener) {
973
+ listeners.add(listener);
974
+ return () => {
975
+ listeners.delete(listener);
976
+ };
977
+ },
978
+ dismissFailure() {
979
+ publish({ creating: snapshot.creating });
980
+ },
981
+ reportFailure(failure) {
982
+ publish({
983
+ ...snapshot,
984
+ failure
985
+ });
986
+ },
987
+ async create() {
988
+ if (snapshot.creating || lifetime.signal.aborted) return;
989
+ const signal = AbortSignal.any([host.beginNavigation(), lifetime.signal]);
990
+ const action = pending ??= {
991
+ requestId: ids(),
992
+ title: label()
993
+ };
994
+ let opening = false;
995
+ publish({ creating: true });
996
+ try {
997
+ signal.throwIfAborted();
998
+ if (action.path === void 0) {
999
+ const allocated = await host.allocate(action.requestId, signal);
1000
+ if (allocated.requestId !== action.requestId || !allocated.path) throw new Error("Invalid temporary workspace response");
1001
+ action.path = allocated.path;
1002
+ }
1003
+ signal.throwIfAborted();
1004
+ if (action.workspaceId === void 0) action.workspaceId = (await host.createWorkspace(action.path)).workspaceId;
1005
+ signal.throwIfAborted();
1006
+ if (!action.named) {
1007
+ await host.renameWorkspace(action.workspaceId, `${action.title} ${action.requestId.slice(0, 8)}`);
1008
+ action.named = true;
1009
+ }
1010
+ signal.throwIfAborted();
1011
+ opening = true;
1012
+ await host.openWorkspace(action.workspaceId, () => lifetime.signal.throwIfAborted());
1013
+ if (!lifetime.signal.aborted) pending = void 0;
1014
+ } catch (error) {
1015
+ if (!lifetime.signal.aborted && (opening ? !(error instanceof Error && error.name === "AbortError") : !signal.aborted)) publish({
1016
+ creating: false,
1017
+ failure: error instanceof Error ? error.message : "Could not create the temporary workspace"
1018
+ });
1019
+ } finally {
1020
+ publish({
1021
+ ...snapshot,
1022
+ creating: false
1023
+ });
1024
+ }
1025
+ },
1026
+ cancel() {
1027
+ lifetime.abort();
1028
+ listeners.clear();
1029
+ }
1030
+ };
1031
+ }
1032
+ function bindTemporaryWorkspaceHost(ctx) {
1033
+ return {
1034
+ beginNavigation: () => ctx.layout.beginNavigation(),
1035
+ async allocate(requestId, signal) {
1036
+ const response = await ctx.connection.rpc.call(TEMP_WORKSPACE_CHANNEL, "temporary.allocate", { requestId }, signal);
1037
+ if (response?.ok !== true || typeof response.value?.path !== "string" || typeof response.value.requestId !== "string") throw new Error(response?.error?.message ?? "Could not allocate the temporary workspace");
1038
+ return response.value;
1039
+ },
1040
+ createWorkspace: (path) => ctx.workspaces.create({ path }),
1041
+ renameWorkspace: (workspaceId, title) => ctx.workspaces.rename(workspaceId, title),
1042
+ openWorkspace: (workspaceId, beforeOpen) => ctx.uiWorkspace.openWorkspace(workspaceId, beforeOpen)
1043
+ };
871
1044
  }
872
1045
  //#endregion
873
1046
  //#region src/session-actions.ts
@@ -948,7 +1121,6 @@ async function readCapabilities(host) {
948
1121
  function controlsFor(capabilities, ready, host) {
949
1122
  return {
950
1123
  create: ready && capabilities.freeSession && typeof host.create === "function",
951
- fork: ready && capabilities.freeSession && (typeof host.forkSession === "function" || typeof host.fork === "function"),
952
1124
  delete: ready && capabilities.deleteSession && typeof host.deleteSession === "function",
953
1125
  retry: ready && capabilities.retry && typeof host.previewRetry === "function" && typeof host.retry === "function"
954
1126
  };
@@ -963,25 +1135,13 @@ async function createFreeSession(host) {
963
1135
  if (typeof host.create !== "function") throw new HostOperationError("session/lifecycle-unavailable");
964
1136
  return sessionIdOf(await unwrap(host.create({ kind: "free" })));
965
1137
  }
966
- /** Returns the child id. The caller opens it only while its generation is still current. */
967
- async function forkOwnedChild(host, sessionId) {
968
- if (typeof host.forkSession === "function") return sessionIdOf(await unwrap(host.forkSession(sessionId)));
969
- if (typeof host.fork === "function") return sessionIdOf(await unwrap(host.fork({
970
- sessionId,
971
- increaseTitle: true
972
- })));
973
- throw new HostOperationError("session/fork-unavailable");
974
- }
975
1138
  async function deleteOwnedSession(host, sessionId, requestId) {
976
1139
  if (typeof host.deleteSession !== "function") throw new HostOperationError("session/lifecycle-unavailable");
977
1140
  const value = await unwrap(host.deleteSession(sessionId, requestId));
978
1141
  if (typeof value !== "object" || value === null) throw new HostOperationError("session/delete-pending", "missing-outcome");
979
1142
  const phase = value.phase;
980
1143
  if (phase !== "deleted" && phase !== "cleanup-pending") throw new HostOperationError("session/delete-pending", "missing-outcome");
981
- return {
982
- phase,
983
- releasedSelection: value.releasedSelection === true
984
- };
1144
+ return { phase };
985
1145
  }
986
1146
  function directRetryRequest(input) {
987
1147
  return {
@@ -1058,57 +1218,18 @@ function createFreeChatModel(host, ids = () => crypto.randomUUID()) {
1058
1218
  const events = store();
1059
1219
  let capabilities = UNSUPPORTED_CAPABILITIES;
1060
1220
  let capabilitiesReady = false;
1061
- let query = "";
1062
1221
  let creating = false;
1063
1222
  let notice;
1064
1223
  let deleteRequest;
1065
- let focusNonce = 0;
1066
- let listFailure;
1067
1224
  let generation = 0;
1068
1225
  let snapshot = build();
1069
- const stopList = host.list.subscribe(() => publish());
1070
- function listError(listed) {
1071
- const code = typeof listed.error?.code === "string" && listed.error.code !== "" ? listed.error.code : "transport";
1072
- const message = listed.error?.message;
1073
- return {
1074
- code,
1075
- ...typeof message === "string" && message !== "" ? { reason: message } : {}
1076
- };
1077
- }
1078
1226
  function build() {
1079
- let status = "ready";
1080
- let rows = [];
1081
- try {
1082
- const listed = host.list.getSnapshot();
1083
- if (listed.phase === "error") {
1084
- status = "error";
1085
- listFailure = listError(listed);
1086
- rows = [];
1087
- } else {
1088
- listFailure = void 0;
1089
- const matched = matchFreeRows(freeChatRows(listed), query);
1090
- if (listed.phase === "pending" && matched.length === 0) status = "loading";
1091
- else if (listed.phase === "ready" && matched.length === 0 && query.trim() === "") status = "empty";
1092
- else if (matched.length === 0) status = "nomatch";
1093
- else status = "ready";
1094
- rows = matched;
1095
- }
1096
- } catch (error) {
1097
- status = "error";
1098
- listFailure = readFailure(error);
1099
- rows = [];
1100
- }
1101
1227
  return {
1102
- rows,
1103
- listStatus: status,
1104
- ...listFailure === void 0 ? {} : { listFailure },
1105
- query,
1106
1228
  controls: controlsFor(capabilities, capabilitiesReady, host),
1107
1229
  capabilitiesReady,
1108
1230
  creating,
1109
1231
  ...notice === void 0 ? {} : { notice },
1110
- ...deleteRequest === void 0 ? {} : { deleteRequest },
1111
- focusNonce
1232
+ ...deleteRequest === void 0 ? {} : { deleteRequest }
1112
1233
  };
1113
1234
  }
1114
1235
  function publish() {
@@ -1123,15 +1244,6 @@ function createFreeChatModel(host, ids = () => crypto.randomUUID()) {
1123
1244
  capabilitiesReady = true;
1124
1245
  publish();
1125
1246
  },
1126
- setQuery(next) {
1127
- query = next;
1128
- publish();
1129
- },
1130
- async refreshList() {
1131
- if (typeof host.refresh !== "function") return;
1132
- await unwrap(host.refresh());
1133
- publish();
1134
- },
1135
1247
  async create() {
1136
1248
  if (!controlsFor(capabilities, capabilitiesReady, host).create || creating) return;
1137
1249
  const ticket = generation;
@@ -1153,22 +1265,10 @@ function createFreeChatModel(host, ids = () => crypto.randomUUID()) {
1153
1265
  }
1154
1266
  }
1155
1267
  },
1156
- open(sessionId) {
1157
- host.openSession?.(sessionId);
1158
- },
1159
- async fork(sessionId) {
1160
- const ticket = generation;
1268
+ dismissNotice() {
1269
+ if (notice === void 0) return;
1161
1270
  notice = void 0;
1162
- try {
1163
- const child = await forkOwnedChild(host, sessionId);
1164
- if (ticket !== generation) return;
1165
- if (typeof host.openSession !== "function") throw new HostOperationError("session/lifecycle-unavailable", "open-session");
1166
- host.openSession(child);
1167
- } catch (error) {
1168
- if (ticket !== generation) return;
1169
- notice = readFailure(error);
1170
- }
1171
- if (ticket === generation) publish();
1271
+ publish();
1172
1272
  },
1173
1273
  requestDelete(sessionId) {
1174
1274
  if (!controlsFor(capabilities, capabilitiesReady, host).delete) return;
@@ -1213,10 +1313,6 @@ function createFreeChatModel(host, ids = () => crypto.randomUUID()) {
1213
1313
  return;
1214
1314
  }
1215
1315
  deleteRequest = void 0;
1216
- if (outcome.releasedSelection) {
1217
- host.selectPanel?.(FREE_PANEL_ID);
1218
- focusNonce += 1;
1219
- }
1220
1316
  } catch (error) {
1221
1317
  if (ticket !== generation) return;
1222
1318
  const failure = readFailure(error);
@@ -1233,7 +1329,6 @@ function createFreeChatModel(host, ids = () => crypto.randomUUID()) {
1233
1329
  creating = false;
1234
1330
  notice = void 0;
1235
1331
  deleteRequest = void 0;
1236
- stopList();
1237
1332
  publish();
1238
1333
  }
1239
1334
  };
@@ -1478,14 +1573,10 @@ function bindHost(ctx) {
1478
1573
  },
1479
1574
  ...typeof sessions?.capabilities === "function" ? { capabilities: () => sessions.capabilities() } : {},
1480
1575
  ...typeof sessions?.create === "function" ? { create: (opts) => sessions.create(opts) } : {},
1481
- ...typeof sessions?.fork === "function" ? { fork: (opts) => sessions.fork(opts) } : {},
1482
- ...typeof sessions?.refresh === "function" ? { refresh: () => sessions.refresh() } : {},
1483
1576
  ...typeof sessions?.previewRetry === "function" ? { previewRetry: (request) => sessions.previewRetry(request) } : {},
1484
1577
  ...typeof sessions?.retry === "function" ? { retry: (request) => sessions.retry(request) } : {},
1485
1578
  ...typeof workspace?.openSession === "function" ? { openSession: (sessionId) => workspace.openSession(sessionId) } : {},
1486
- ...typeof workspace?.forkSession === "function" ? { forkSession: (sessionId) => workspace.forkSession(sessionId) } : {},
1487
- ...typeof workspace?.deleteSession === "function" ? { deleteSession: (sessionId, requestId) => workspace.deleteSession(sessionId, requestId) } : {},
1488
- ...typeof ctx.layout?.selectPanel === "function" ? { selectPanel: (id) => ctx.layout.selectPanel(id) } : {}
1579
+ ...typeof workspace?.deleteSession === "function" ? { deleteSession: (sessionId, requestId) => workspace.deleteSession(sessionId, requestId) } : {}
1489
1580
  };
1490
1581
  }
1491
1582
  //#endregion
@@ -1591,6 +1682,9 @@ const inject = [
1591
1682
  "locale",
1592
1683
  "sessions",
1593
1684
  "uiWorkspace",
1685
+ "shortcuts",
1686
+ "connection",
1687
+ "workspaces",
1594
1688
  "layout"
1595
1689
  ];
1596
1690
  function CopyThreadMenuItem(props) {
@@ -1605,6 +1699,7 @@ function CopyThreadMenuItem(props) {
1605
1699
  });
1606
1700
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.MenuItemButton, {
1607
1701
  separatorBefore: true,
1702
+ shortcut: props.shortcut,
1608
1703
  onSelect,
1609
1704
  children: label
1610
1705
  });
@@ -1614,19 +1709,19 @@ function apply(ctx) {
1614
1709
  const host = bindHost(client);
1615
1710
  const chats = createFreeChatModel(host);
1616
1711
  const retries = createRetryModel(host);
1712
+ const temporaryHost = bindTemporaryWorkspaceHost(client);
1713
+ const temporary = createTemporaryWorkspaceModel(temporaryHost, void 0, () => uiText(copyLocale(client.locale?.getSnapshot().active), "tempWorkspace"));
1714
+ const currentLocale = () => client.locale?.getSnapshot().active;
1617
1715
  client.effect(() => {
1618
1716
  const stops = [];
1619
- const localeStop = client.locale?.register?.("sessionManager", {
1620
- zh: { panel: uiText("zh", "panel") },
1621
- en: { panel: uiText("en", "panel") }
1622
- });
1623
- if (typeof localeStop === "function") stops.push(localeStop);
1717
+ if (typeof document !== "undefined") stops.push(installContextMenuForwarding(document));
1624
1718
  stops.push(client.slots.inject("sidebar.workspaces.session.menu.item", () => client.slots.register({
1625
1719
  name: "sidebar.workspaces.session.menu.item",
1626
1720
  id: COPY_THREAD_MENU_ID,
1627
1721
  order: 500
1628
1722
  }, (props) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CopyThreadSlot, {
1629
1723
  locale: client.locale,
1724
+ shortcuts: client.shortcuts,
1630
1725
  ...props
1631
1726
  }))));
1632
1727
  stops.push(client.slots.inject("sidebar.workspaces.session.menu.item", () => client.slots.register({
@@ -1638,19 +1733,7 @@ function apply(ctx) {
1638
1733
  model: chats,
1639
1734
  ...props
1640
1735
  }))));
1641
- stops.push(client.slots.inject("main", () => client.slots.register({
1642
- name: "main",
1643
- key: FREE_PANEL_ID
1644
- }, () => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FreeChatPanel, {
1645
- model: chats,
1646
- locale: useLiveLocale(client.locale)
1647
- }))));
1648
- stops.push(client.slots.inject("sidebar.panellist", () => client.slots.register({
1649
- name: "sidebar.panellist",
1650
- id: FREE_PANEL_ID,
1651
- order: 20,
1652
- label: () => uiText(copyLocale(client.locale?.getSnapshot().active), "panel")
1653
- }, (props) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FreeChatIcon, { size: props.size ?? 16 }))));
1736
+ stops.push(installTemporaryWorkspacePicker(client.slots, temporary, temporaryHost.createWorkspace, client.locale));
1654
1737
  stops.push(client.slots.inject(USER_ACTIONS_SLOT, () => client.slots.register({
1655
1738
  name: USER_ACTIONS_SLOT,
1656
1739
  id: USER_ACTIONS_ID,
@@ -1667,10 +1750,30 @@ function apply(ctx) {
1667
1750
  name: "shell.overlay",
1668
1751
  id: DELETE_DIALOG_ID,
1669
1752
  order: 40
1670
- }, () => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("style", { children: freeChatCss }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DeleteChatDialog, {
1671
- model: chats,
1672
- locale: useLiveLocale(client.locale)
1673
- })] }))));
1753
+ }, () => {
1754
+ const locale = useLiveLocale(client.locale);
1755
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
1756
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("style", { children: freeChatCss }),
1757
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CopyThreadShortcutNotice, {
1758
+ shortcuts: client.shortcuts,
1759
+ list: client.sessions?.list,
1760
+ locale,
1761
+ currentLocale
1762
+ }),
1763
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TemporaryWorkspaceNotice, {
1764
+ model: temporary,
1765
+ locale
1766
+ }),
1767
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DeleteChatDialog, {
1768
+ model: chats,
1769
+ locale
1770
+ }),
1771
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CreateNoticeDialog, {
1772
+ model: chats,
1773
+ locale
1774
+ })
1775
+ ] });
1776
+ })));
1674
1777
  const reset = client.on?.("connection/reset", () => {
1675
1778
  chats.reloadCapabilities();
1676
1779
  retries.load();
@@ -1681,16 +1784,20 @@ function apply(ctx) {
1681
1784
  return () => {
1682
1785
  chats.cancel();
1683
1786
  retries.cancel();
1787
+ temporary.cancel();
1684
1788
  for (const stop of stops) stop();
1685
1789
  };
1686
1790
  });
1687
1791
  }
1792
+ const EMPTY_SHORTCUTS = [];
1688
1793
  function CopyThreadSlot(props) {
1689
1794
  const locale = useLiveLocale(props.locale);
1795
+ const shortcuts = (0, react.useSyncExternalStore)((listener) => props.shortcuts?.catalog.subscribe(listener) ?? (() => {}), () => props.shortcuts?.catalog.getSnapshot() ?? EMPTY_SHORTCUTS, () => EMPTY_SHORTCUTS);
1690
1796
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CopyThreadMenuItem, {
1691
1797
  sessionId: props.sessionId,
1692
1798
  useMenuOpenState: props.useMenuOpenState,
1693
- locale
1799
+ locale,
1800
+ shortcut: shortcuts.find((row) => row.id === COPY_THREAD_SHORTCUT_ID)
1694
1801
  });
1695
1802
  }
1696
1803
  function DeleteMenuItem(props) {
@@ -1707,12 +1814,7 @@ function DeleteMenuItem(props) {
1707
1814
  });
1708
1815
  }
1709
1816
  function useLiveLocale(locale) {
1710
- return copyLocale((0, react.useSyncExternalStore)((listener) => locale?.subscribe(listener) ?? (() => {}), () => locale?.getSnapshot().active ?? "zh", () => "zh"));
1711
- }
1712
- async function writeClipboard(value) {
1713
- const clipboard = globalThis.navigator?.clipboard;
1714
- if (clipboard === void 0) throw new Error("clipboard unavailable");
1715
- await clipboard.writeText(value);
1817
+ return copyLocale((0, react.useSyncExternalStore)((listener) => locale?.subscribe(listener) ?? (() => {}), () => locale?.getSnapshot().active ?? "en", () => locale?.getSnapshot().active ?? "en"));
1716
1818
  }
1717
1819
  //#endregion
1718
1820
  exports.CopyThreadMenuItem = CopyThreadMenuItem;