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