dsh-side-chat-plus 0.3.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.
Files changed (44) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +317 -0
  3. package/README.zh.md +261 -0
  4. package/cordis.patch.yml +8 -0
  5. package/dsh.plugin.json +16 -0
  6. package/lib/client-registry.js +2949 -0
  7. package/lib/client-registry.js.map +1 -0
  8. package/lib/client.js +2949 -0
  9. package/lib/client.js.map +1 -0
  10. package/lib/index.js +840 -0
  11. package/lib/types/client/api.d.ts +218 -0
  12. package/lib/types/client/attachments/AttachmentRail.d.ts +39 -0
  13. package/lib/types/client/attachments/DropOverlay.d.ts +18 -0
  14. package/lib/types/client/attachments/ImageLightbox.d.ts +20 -0
  15. package/lib/types/client/attachments/MessageImage.d.ts +38 -0
  16. package/lib/types/client/attachments/index.d.ts +18 -0
  17. package/lib/types/client/index.d.ts +6 -0
  18. package/lib/types/client/locales.d.ts +178 -0
  19. package/lib/types/context-types.d.ts +390 -0
  20. package/lib/types/index.d.ts +7 -0
  21. package/lib/types/settings-shared.d.ts +24 -0
  22. package/lib/types/trust-fence.d.ts +20 -0
  23. package/lib/types/wire.d.ts +25 -0
  24. package/package.json +114 -0
  25. package/src/client/api.ts +112 -0
  26. package/src/client/attachments/AttachmentRail.module.css +89 -0
  27. package/src/client/attachments/AttachmentRail.tsx +173 -0
  28. package/src/client/attachments/DropOverlay.module.css +38 -0
  29. package/src/client/attachments/DropOverlay.tsx +62 -0
  30. package/src/client/attachments/ImageLightbox.module.css +44 -0
  31. package/src/client/attachments/ImageLightbox.tsx +58 -0
  32. package/src/client/attachments/MessageImage.module.css +61 -0
  33. package/src/client/attachments/MessageImage.tsx +120 -0
  34. package/src/client/attachments/index.ts +19 -0
  35. package/src/client/client.module.css +1032 -0
  36. package/src/client/index.tsx +1966 -0
  37. package/src/client/layout.css +16 -0
  38. package/src/client/locales.ts +181 -0
  39. package/src/context-types.ts +384 -0
  40. package/src/css-modules.d.ts +10 -0
  41. package/src/index.ts +840 -0
  42. package/src/settings-shared.ts +33 -0
  43. package/src/trust-fence.ts +70 -0
  44. package/src/wire.ts +81 -0
package/lib/client.js ADDED
@@ -0,0 +1,2949 @@
1
+ window.__ModuleLoader__.load({
2
+ id: "dsh-side-chat-plus",
3
+ factory: (require) => {
4
+ var module = { exports: {} };
5
+ var exports = module.exports;
6
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
+ let react = require("react");
8
+ let react_dom_client = require("react-dom/client");
9
+ let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
10
+ let react_jsx_runtime = require("react/jsx-runtime");
11
+ let react_dom = require("react-dom");
12
+ //#region \0dsh-css:D:\code\DSH-side-chat\src\client\attachments\AttachmentRail.module.css.mjs
13
+ const css$5 = ".Nb9OEG_root{align-items:center;min-width:0;display:flex;position:relative}.Nb9OEG_rail{scrollbar-width:none;gap:8px;padding:2px;display:flex;overflow-x:auto}.Nb9OEG_rail::-webkit-scrollbar{display:none}.Nb9OEG_item{flex:none;position:relative}.Nb9OEG_thumbnail{border:1px solid var(--dsw-color-border,#0000001a);cursor:pointer;background:0 0;border-radius:8px;width:56px;height:56px;padding:0;display:block;overflow:hidden}.Nb9OEG_thumbnail img{object-fit:cover;width:100%;height:100%}.Nb9OEG_remove{background:var(--dsw-color-surface-raised,#fff);width:18px;height:18px;color:var(--dsw-color-text-secondary,#646a73);cursor:pointer;opacity:0;border:none;border-radius:50%;justify-content:center;align-items:center;padding:0;display:flex;position:absolute;top:-6px;right:-6px;box-shadow:0 1px 4px #0003}.Nb9OEG_item:hover .Nb9OEG_remove,.Nb9OEG_remove:focus-visible{opacity:1}.Nb9OEG_arrow{z-index:1;border:1px solid var(--dsw-color-border,#0000001a);background:var(--dsw-color-surface-raised,#fff);width:22px;height:22px;color:var(--dsw-color-text-secondary,#646a73);cursor:pointer;border-radius:50%;justify-content:center;align-items:center;padding:0;display:flex;position:absolute}.Nb9OEG_arrowLeft{left:-10px}.Nb9OEG_arrowRight{right:-10px}";
14
+ const tagId$5 = "dsh-side-chat-plus/AttachmentRail.module.css";
15
+ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$5) + "]") === null) {
16
+ const tag = document.createElement("style");
17
+ tag.dataset.plugin = "dsh-side-chat-plus";
18
+ tag.dataset.pluginCss = tagId$5;
19
+ tag.textContent = css$5;
20
+ document.head.appendChild(tag);
21
+ }
22
+ var AttachmentRail_module_css_default = {
23
+ "arrow": "Nb9OEG_arrow",
24
+ "arrowRight": "Nb9OEG_arrowRight",
25
+ "root": "Nb9OEG_root",
26
+ "rail": "Nb9OEG_rail",
27
+ "arrowLeft": "Nb9OEG_arrowLeft",
28
+ "item": "Nb9OEG_item",
29
+ "thumbnail": "Nb9OEG_thumbnail",
30
+ "remove": "Nb9OEG_remove"
31
+ };
32
+ //#endregion
33
+ //#region src/client/attachments/AttachmentRail.tsx
34
+ /** Approximate pixels per wheel step for `deltaMode` LINE deltas (Firefox notch wheels report lines). */
35
+ const WHEEL_LINE_PX = 16;
36
+ /** Smooth paging unless the user asked for reduced motion. */
37
+ function pageBehavior() {
38
+ return window.matchMedia?.("(prefers-reduced-motion: reduce)").matches ? "auto" : "smooth";
39
+ }
40
+ /**
41
+ * Horizontal thumbnail rail over the caller's draft attachments.
42
+ *
43
+ * The rail scrolls with its scrollbar hidden; overflow is announced by edge
44
+ * arrows recomputed from scroll geometry on scroll, item-count changes, and
45
+ * rail size changes (a ResizeObserver on the rail element, so sidebar or panel
46
+ * resizes count, not only window resizes). A vertical wheel pans the rail
47
+ * horizontally and is consumed exclusively, a newly added item is revealed at
48
+ * the rail's end, and each thumbnail opens on a single click while its remove
49
+ * control sits inside the card. The owner decides mounting.
50
+ */
51
+ function AttachmentRail({ items, labels, onOpen, onRemove }) {
52
+ const railRef = (0, react.useRef)(null);
53
+ const countRef = (0, react.useRef)(null);
54
+ const [edges, setEdges] = (0, react.useState)({
55
+ left: false,
56
+ right: false
57
+ });
58
+ const updateEdges = (0, react.useCallback)(() => {
59
+ const el = railRef.current;
60
+ if (el === null) return;
61
+ const left = el.scrollLeft > 1;
62
+ const right = el.scrollLeft < el.scrollWidth - el.clientWidth - 1;
63
+ setEdges((prev) => prev.left === left && prev.right === right ? prev : {
64
+ left,
65
+ right
66
+ });
67
+ }, []);
68
+ (0, react.useLayoutEffect)(() => {
69
+ const grew = countRef.current !== null && items.length > countRef.current;
70
+ countRef.current = items.length;
71
+ const el = railRef.current;
72
+ if (el === null) return;
73
+ if (grew) el.scrollLeft = el.scrollWidth - el.clientWidth;
74
+ updateEdges();
75
+ }, [items.length, updateEdges]);
76
+ (0, react.useEffect)(() => {
77
+ const el = railRef.current;
78
+ if (el === null) return;
79
+ let disconnect = () => {};
80
+ if (typeof ResizeObserver !== "undefined") {
81
+ const observer = new ResizeObserver(updateEdges);
82
+ observer.observe(el);
83
+ disconnect = () => {
84
+ observer.disconnect();
85
+ };
86
+ }
87
+ const onWheel = (event) => {
88
+ if (event.deltaY === 0) return;
89
+ const scale = event.deltaMode === WheelEvent.DOM_DELTA_LINE ? WHEEL_LINE_PX : event.deltaMode === WheelEvent.DOM_DELTA_PAGE ? el.clientWidth : 1;
90
+ event.preventDefault();
91
+ el.scrollBy({
92
+ left: event.deltaX !== 0 ? event.deltaX * scale : Math.sign(event.deltaY) * Math.min(Math.abs(event.deltaY) * scale, 60),
93
+ behavior: "auto"
94
+ });
95
+ };
96
+ el.addEventListener("wheel", onWheel, { passive: false });
97
+ return () => {
98
+ disconnect();
99
+ el.removeEventListener("wheel", onWheel);
100
+ };
101
+ }, [updateEdges]);
102
+ const page = (direction) => {
103
+ const el = railRef.current;
104
+ if (el === null) return;
105
+ el.scrollBy({
106
+ left: direction * Math.max(el.clientWidth - 64, 200),
107
+ behavior: pageBehavior()
108
+ });
109
+ };
110
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
111
+ className: AttachmentRail_module_css_default.root,
112
+ children: [
113
+ edges.left && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
114
+ type: "button",
115
+ className: `${AttachmentRail_module_css_default.arrow} ${AttachmentRail_module_css_default.arrowLeft}`,
116
+ "aria-label": labels.scrollLeft,
117
+ onClick: () => {
118
+ page(-1);
119
+ },
120
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronLeftOutline14, {})
121
+ }),
122
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
123
+ ref: railRef,
124
+ className: AttachmentRail_module_css_default.rail,
125
+ role: "group",
126
+ "aria-label": labels.group,
127
+ onScroll: updateEdges,
128
+ children: items.map((item) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
129
+ className: AttachmentRail_module_css_default.item,
130
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
131
+ type: "button",
132
+ className: AttachmentRail_module_css_default.thumbnail,
133
+ title: labels.open,
134
+ onClick: () => {
135
+ onOpen(item);
136
+ },
137
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
138
+ src: item.previewUrl,
139
+ alt: item.alt
140
+ })
141
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
142
+ type: "button",
143
+ className: AttachmentRail_module_css_default.remove,
144
+ "aria-label": item.removeLabel,
145
+ onClick: () => {
146
+ onRemove(item);
147
+ },
148
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCloseFill14, { size: 12 })
149
+ })]
150
+ }, item.id))
151
+ }),
152
+ edges.right && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
153
+ type: "button",
154
+ className: `${AttachmentRail_module_css_default.arrow} ${AttachmentRail_module_css_default.arrowRight}`,
155
+ "aria-label": labels.scrollRight,
156
+ onClick: () => {
157
+ page(1);
158
+ },
159
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronRightOutline14, {})
160
+ })
161
+ ]
162
+ });
163
+ }
164
+ //#endregion
165
+ //#region \0dsh-css:D:\code\DSH-side-chat\src\client\attachments\DropOverlay.module.css.mjs
166
+ const css$4 = ".i9bHbW_mask{z-index:999;pointer-events:none;border:2px dashed var(--dsw-color-border-strong,#4d6bfe);background:#ffffffb8;border-radius:12px;justify-content:center;align-items:center;display:flex;position:fixed;inset:0}.i9bHbW_wrap{text-align:center;flex-direction:column;align-items:center;gap:12px;display:flex}.i9bHbW_illustration{justify-content:center;align-items:center;display:flex}.i9bHbW_title{color:var(--dsw-color-text,#1f2329);font-size:16px;font-weight:600}.i9bHbW_desc{color:var(--dsw-color-text-secondary,#646a73);font-size:13px}";
167
+ const tagId$4 = "dsh-side-chat-plus/DropOverlay.module.css";
168
+ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$4) + "]") === null) {
169
+ const tag = document.createElement("style");
170
+ tag.dataset.plugin = "dsh-side-chat-plus";
171
+ tag.dataset.pluginCss = tagId$4;
172
+ tag.textContent = css$4;
173
+ document.head.appendChild(tag);
174
+ }
175
+ var DropOverlay_module_css_default = {
176
+ "illustration": "i9bHbW_illustration",
177
+ "mask": "i9bHbW_mask",
178
+ "title": "i9bHbW_title",
179
+ "desc": "i9bHbW_desc",
180
+ "wrap": "i9bHbW_wrap"
181
+ };
182
+ //#endregion
183
+ //#region src/client/attachments/DropOverlay.tsx
184
+ /**
185
+ * Full-viewport invitation shown while a file drag is over the page.
186
+ * Decoration only: `pointer-events: none` keeps drag targeting on the page
187
+ * below, so the owner's document-level listeners keep an accurate enter/leave
188
+ * count and own accept/reject. Rendered through a body portal for the same
189
+ * transformed-ancestor reason as the lightbox.
190
+ */
191
+ function DropOverlay({ disabled, labels }) {
192
+ return (0, react_dom.createPortal)(/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
193
+ className: DropOverlay_module_css_default.mask,
194
+ role: "status",
195
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
196
+ className: DropOverlay_module_css_default.wrap,
197
+ children: [
198
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
199
+ className: DropOverlay_module_css_default.illustration,
200
+ "aria-hidden": "true",
201
+ children: disabled ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(BlockedIllustration, {}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(UploadIllustration, {})
202
+ }),
203
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
204
+ className: DropOverlay_module_css_default.title,
205
+ children: labels.title
206
+ }),
207
+ !disabled && labels.desc !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
208
+ className: DropOverlay_module_css_default.desc,
209
+ children: labels.desc
210
+ })
211
+ ]
212
+ })
213
+ }), document.body);
214
+ }
215
+ /** Tilted photo-and-note cards. */
216
+ const UploadIllustration = () => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
217
+ width: "96",
218
+ height: "72",
219
+ viewBox: "0 0 96 72",
220
+ fill: "none",
221
+ xmlns: "http://www.w3.org/2000/svg",
222
+ children: [
223
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
224
+ y: "14",
225
+ width: "38",
226
+ height: "38",
227
+ rx: "10",
228
+ transform: "rotate(-22 0 14)",
229
+ fill: "#9ce5ed"
230
+ }),
231
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
232
+ x: "62",
233
+ y: "7",
234
+ width: "37",
235
+ height: "43",
236
+ rx: "8",
237
+ transform: "rotate(17 62 7)",
238
+ fill: "#679efe"
239
+ }),
240
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
241
+ x: "26",
242
+ y: "30",
243
+ width: "38",
244
+ height: "38",
245
+ rx: "10",
246
+ fill: "#3964fe"
247
+ }),
248
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
249
+ d: "M33 62c1-2 4-8 7-14 1-2 5-2 6 1 2 5 5 9 7 10 3 1 7-9 17 2",
250
+ stroke: "#fff",
251
+ strokeWidth: "3",
252
+ fill: "none"
253
+ }),
254
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
255
+ cx: "53",
256
+ cy: "43",
257
+ r: "4",
258
+ fill: "#fff"
259
+ })
260
+ ]
261
+ });
262
+ /** Greyed cards with a blocked badge. */
263
+ const BlockedIllustration = () => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
264
+ width: "96",
265
+ height: "72",
266
+ viewBox: "0 0 96 72",
267
+ fill: "none",
268
+ xmlns: "http://www.w3.org/2000/svg",
269
+ children: [
270
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
271
+ y: "14",
272
+ width: "38",
273
+ height: "38",
274
+ rx: "10",
275
+ transform: "rotate(-22 0 14)",
276
+ fill: "#c4c9d0"
277
+ }),
278
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
279
+ x: "62",
280
+ y: "7",
281
+ width: "37",
282
+ height: "43",
283
+ rx: "8",
284
+ transform: "rotate(17 62 7)",
285
+ fill: "#b0b6be"
286
+ }),
287
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
288
+ x: "26",
289
+ y: "30",
290
+ width: "38",
291
+ height: "38",
292
+ rx: "10",
293
+ fill: "#d8dce1"
294
+ }),
295
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
296
+ cx: "45",
297
+ cy: "49",
298
+ r: "12",
299
+ stroke: "#fff",
300
+ strokeWidth: "3",
301
+ fill: "none"
302
+ }),
303
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
304
+ d: "M36 40l18 18",
305
+ stroke: "#fff",
306
+ strokeWidth: "3",
307
+ strokeLinecap: "round"
308
+ })
309
+ ]
310
+ });
311
+ //#endregion
312
+ //#region \0dsh-css:D:\code\DSH-side-chat\src\client\attachments\ImageLightbox.module.css.mjs
313
+ const css$3 = ".IiCsRq_backdrop{z-index:1000;justify-content:center;align-items:center;padding:32px;display:flex;position:fixed;inset:0}.IiCsRq_mask{background:#000000b8;position:absolute;inset:0}.IiCsRq_image{object-fit:contain;border-radius:8px;max-width:100%;max-height:100%;position:relative}.IiCsRq_close{color:#fff;cursor:pointer;background:#ffffff29;border:none;border-radius:50%;justify-content:center;align-items:center;width:32px;height:32px;padding:0;display:flex;position:absolute;top:20px;right:20px}.IiCsRq_close:hover{background:#ffffff47}";
314
+ const tagId$3 = "dsh-side-chat-plus/ImageLightbox.module.css";
315
+ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$3) + "]") === null) {
316
+ const tag = document.createElement("style");
317
+ tag.dataset.plugin = "dsh-side-chat-plus";
318
+ tag.dataset.pluginCss = tagId$3;
319
+ tag.textContent = css$3;
320
+ document.head.appendChild(tag);
321
+ }
322
+ var ImageLightbox_module_css_default = {
323
+ "close": "IiCsRq_close",
324
+ "mask": "IiCsRq_mask",
325
+ "backdrop": "IiCsRq_backdrop",
326
+ "image": "IiCsRq_image"
327
+ };
328
+ //#endregion
329
+ //#region src/client/attachments/ImageLightbox.tsx
330
+ /**
331
+ * Document-level original-image preview opened by clicking a thumbnail.
332
+ * Closes on Escape, backdrop press, or the close control, and restores focus
333
+ * to the opener on unmount. Rendered through a body portal: an opener inside
334
+ * a transformed or filtered ancestor would otherwise trap the fixed backdrop
335
+ * in that ancestor's box instead of covering the viewport.
336
+ */
337
+ function ImageLightbox({ src, alt, labels, onClose }) {
338
+ const closeRef = (0, react.useRef)(null);
339
+ const restoreRef = (0, react.useRef)(null);
340
+ (0, react.useEffect)(() => {
341
+ restoreRef.current = document.activeElement instanceof HTMLElement ? document.activeElement : null;
342
+ closeRef.current?.focus();
343
+ const onKeyDown = (event) => {
344
+ if (event.key === "Escape") onClose();
345
+ };
346
+ window.addEventListener("keydown", onKeyDown);
347
+ return () => {
348
+ window.removeEventListener("keydown", onKeyDown);
349
+ restoreRef.current?.focus();
350
+ };
351
+ }, [onClose]);
352
+ return (0, react_dom.createPortal)(/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
353
+ className: ImageLightbox_module_css_default.backdrop,
354
+ role: "dialog",
355
+ "aria-modal": "true",
356
+ "aria-label": labels.dialog,
357
+ children: [
358
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
359
+ className: ImageLightbox_module_css_default.mask,
360
+ "aria-hidden": "true",
361
+ onMouseDown: onClose
362
+ }),
363
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
364
+ className: ImageLightbox_module_css_default.image,
365
+ src,
366
+ alt
367
+ }),
368
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
369
+ ref: closeRef,
370
+ type: "button",
371
+ className: ImageLightbox_module_css_default.close,
372
+ "aria-label": labels.close,
373
+ onClick: onClose,
374
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCloseOutline16, { size: 16 })
375
+ })
376
+ ]
377
+ }), document.body);
378
+ }
379
+ //#endregion
380
+ //#region \0dsh-css:D:\code\DSH-side-chat\src\client\attachments\MessageImage.module.css.mjs
381
+ const css$2 = ".HNfuSq_gallery{flex-wrap:wrap;gap:8px;max-width:100%;display:flex}.HNfuSq_gallery[data-align=start]{justify-content:flex-start}.HNfuSq_gallery[data-align=end]{justify-content:flex-end}.HNfuSq_frame{border:1px solid var(--dsw-color-border,#00000014);cursor:pointer;background:0 0;border-radius:8px;padding:0;overflow:hidden}.HNfuSq_frame[data-variant=tile]{width:64px;height:64px}.HNfuSq_frame img{object-fit:cover;width:100%;height:100%}.HNfuSq_loading{width:100%;min-width:64px;height:100%;min-height:64px;color:var(--dsw-color-text-secondary,#646a73);justify-content:center;align-items:center;font-size:12px;display:flex}.HNfuSq_error{border:1px dashed var(--dsw-color-border,#00000029);min-width:64px;min-height:64px;color:var(--dsw-color-text-secondary,#646a73);cursor:pointer;background:0 0;border-radius:8px;justify-content:center;align-items:center;padding:0 8px;font-size:12px;display:flex}";
382
+ const tagId$2 = "dsh-side-chat-plus/MessageImage.module.css";
383
+ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$2) + "]") === null) {
384
+ const tag = document.createElement("style");
385
+ tag.dataset.plugin = "dsh-side-chat-plus";
386
+ tag.dataset.pluginCss = tagId$2;
387
+ tag.textContent = css$2;
388
+ document.head.appendChild(tag);
389
+ }
390
+ var MessageImage_module_css_default = {
391
+ "frame": "HNfuSq_frame",
392
+ "loading": "HNfuSq_loading",
393
+ "error": "HNfuSq_error",
394
+ "gallery": "HNfuSq_gallery"
395
+ };
396
+ //#endregion
397
+ //#region src/client/attachments/MessageImage.tsx
398
+ /** Display box for a lone image: long edge 240px with the rendered aspect
399
+ * ratio clamped to [0.25, 4] — the overflow is cropped by
400
+ * `object-fit: cover` — and never upscaled past the image's natural size. The
401
+ * crop anchor keeps the top of very tall images and the left of very wide
402
+ * ones, where the informative content usually starts. */
403
+ function singleFit(dimensions) {
404
+ const natural = dimensions.width / dimensions.height;
405
+ const ratio = Math.min(4, Math.max(.25, natural));
406
+ const box = ratio >= 1 ? {
407
+ width: 240,
408
+ height: 240 / ratio
409
+ } : {
410
+ width: 240 * ratio,
411
+ height: 240
412
+ };
413
+ const scale = Math.min(1, dimensions.width / box.width, dimensions.height / box.height);
414
+ return {
415
+ width: Math.max(1, Math.round(box.width * scale)),
416
+ height: Math.max(1, Math.round(box.height * scale)),
417
+ objectPosition: natural < .25 ? "center top" : natural > 4 ? "left center" : "center"
418
+ };
419
+ }
420
+ /**
421
+ * Compact history renderer with retryable loading and click-to-open original
422
+ * preview. A lone image renders at its `singleFit` size; an image among
423
+ * several renders as a fixed 64px square tile.
424
+ */
425
+ function MessageImage({ image, load, variant, labels }) {
426
+ const [loaded, setLoaded] = (0, react.useState)(null);
427
+ const [error, setError] = (0, react.useState)(false);
428
+ const [open, setOpen] = (0, react.useState)(false);
429
+ const [attempt, setAttempt] = (0, react.useState)(0);
430
+ const request = (0, react.useCallback)(() => {
431
+ setAttempt((a) => a + 1);
432
+ }, []);
433
+ const close = (0, react.useCallback)(() => {
434
+ setOpen(false);
435
+ }, []);
436
+ const label = image.name === void 0 || image.name === "" ? labels.image : image.name;
437
+ const fit = (0, react.useMemo)(() => variant === "single" ? singleFit(image) : void 0, [image, variant]);
438
+ (0, react.useEffect)(() => {
439
+ let live = true;
440
+ setError(false);
441
+ setLoaded(null);
442
+ load(image).then((url) => {
443
+ if (live) setLoaded(url);
444
+ }).catch(() => {
445
+ if (live) setError(true);
446
+ });
447
+ return () => {
448
+ live = false;
449
+ };
450
+ }, [
451
+ image,
452
+ load,
453
+ attempt
454
+ ]);
455
+ if (error) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
456
+ type: "button",
457
+ className: MessageImage_module_css_default.error,
458
+ "data-variant": variant,
459
+ onClick: request,
460
+ children: labels.loadFailed
461
+ });
462
+ const src = loaded;
463
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
464
+ type: "button",
465
+ className: MessageImage_module_css_default.frame,
466
+ "data-variant": variant,
467
+ style: fit === void 0 ? void 0 : {
468
+ width: fit.width,
469
+ height: fit.height
470
+ },
471
+ title: labels.open,
472
+ "aria-label": labels.openNamed(label),
473
+ onClick: () => {
474
+ if (src !== null) setOpen(true);
475
+ },
476
+ children: src === null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
477
+ className: MessageImage_module_css_default.loading,
478
+ children: labels.loading
479
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
480
+ src,
481
+ alt: label,
482
+ style: fit === void 0 ? void 0 : { objectPosition: fit.objectPosition }
483
+ })
484
+ }), open && src !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ImageLightbox, {
485
+ src,
486
+ alt: label,
487
+ labels: labels.lightbox,
488
+ onClose: close
489
+ })] });
490
+ }
491
+ /** Wrapping image group: a lone image renders large, several render as 64px
492
+ * square tiles. */
493
+ function ImageGallery({ images, load, align, labels }) {
494
+ if (images.length === 0) return null;
495
+ const variant = images.length === 1 ? "single" : "tile";
496
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
497
+ className: MessageImage_module_css_default.gallery,
498
+ "data-align": align,
499
+ children: images.map((image, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MessageImage, {
500
+ image,
501
+ load,
502
+ variant,
503
+ labels
504
+ }, `${image.attachmentId}:${index}`))
505
+ });
506
+ }
507
+ //#endregion
508
+ //#region src/client/api.ts
509
+ /** Call one /sidechat/api method. */
510
+ async function call(method, payload) {
511
+ return await (await fetch(`/sidechat/api/${method}`, {
512
+ method: "POST",
513
+ headers: { "content-type": "application/json" },
514
+ body: JSON.stringify(payload)
515
+ })).json();
516
+ }
517
+ const api = {
518
+ start: (args) => call("sidechat.start", args),
519
+ followup: (args) => call("sidechat.followup", args),
520
+ list: (args) => call("sidechat.list", args),
521
+ history: (args) => call("sidechat.history", args),
522
+ stop: (args) => call("sidechat.stop", args),
523
+ selectModel: (args) => call("sidechat.selectModel", args),
524
+ selectPermission: (args) => call("sidechat.selectPermission", args),
525
+ summarize: (args) => call("sidechat.summarize", args),
526
+ inject: (args) => call("sidechat.inject", args),
527
+ directory: () => call("sidechat.directory", {}),
528
+ permissions: () => call("sidechat.permissions", {}),
529
+ limits: () => call("sidechat.limits", {}),
530
+ attachment: (args) => call("sidechat.attachment", args),
531
+ inherit: (args) => call("sidechat.inherit", args),
532
+ commands: (args) => call("sidechat.commands", args),
533
+ command: (args) => call("sidechat.command", args),
534
+ state: (args) => call("sidechat.state", args),
535
+ settingsGet: () => call("settings.get", {}),
536
+ settingsUpdate: (patch, expectedRevision) => call("settings.update", {
537
+ patch,
538
+ expectedRevision
539
+ }),
540
+ dispose: (args) => call("sidechat.dispose", args)
541
+ };
542
+ //#endregion
543
+ //#region src/client/locales.ts
544
+ /**
545
+ * Client locale dictionaries for dsh-side-chat (zh + en), registered under the
546
+ * `sidechat` namespace.
547
+ */
548
+ const LOCALE_NS = "sidechat";
549
+ const zh = {
550
+ "ask.new": "在侧边聊天中提问",
551
+ "ask.continue": "继续在激活的侧边聊天中提问",
552
+ "panel.title": "侧边聊天",
553
+ "panel.empty": "当前会话还没有侧边聊天。选中消息文本后点「在侧边聊天中提问」即可发起。",
554
+ "panel.close": "关闭",
555
+ "panel.input.placeholder": "输入消息,回车发送",
556
+ "panel.send": "发送",
557
+ "panel.stop": "停止",
558
+ "panel.lookup": "需要时从工作区 / 主会话查信息",
559
+ "panel.model": "模型",
560
+ "panel.provider": "服务商",
561
+ "panel.effort": "思考难度",
562
+ "panel.permission": "权限",
563
+ "panel.noModel": "(继承)",
564
+ "panel.effortDefault": "默认",
565
+ "panel.expand": "展开侧边聊天",
566
+ "panel.collapse": "收起侧边聊天",
567
+ "panel.dispose": "结束此侧边聊天",
568
+ "panel.delete": "删除",
569
+ "panel.deleteAll": "删除全部",
570
+ "panel.error": "侧边聊天出错",
571
+ "panel.running": "思考中…",
572
+ "panel.copy": "复制",
573
+ "panel.copied": "已复制",
574
+ "panel.footnotes": "脚注",
575
+ "panel.think": "思考",
576
+ "panel.commands": "命令",
577
+ "command.model": "/model 模型",
578
+ "command.permission": "/permission 权限",
579
+ "plan.chipTitle": "计划模式已开启 — 点击关闭(/plan off)",
580
+ "plan.chipAria": "计划模式已开启,按下关闭",
581
+ "goal.label": "目标",
582
+ "image.railGroup": "图片附件",
583
+ "image.railOpen": "查看原图",
584
+ "image.railScrollLeft": "向左滚动",
585
+ "image.railScrollRight": "向右滚动",
586
+ "image.remove": "移除图片",
587
+ "image.label": "图片",
588
+ "image.open": "查看原图",
589
+ "image.loading": "加载中…",
590
+ "image.loadFailed": "加载失败",
591
+ "image.lightboxDialog": "图片预览",
592
+ "image.close": "关闭",
593
+ "image.dropTitle": "拖放图片到此处",
594
+ "image.dropDesc": "支持 PNG / JPEG / WebP / GIF",
595
+ "image.unsupported": "不支持的图片格式",
596
+ "image.tooMany": "图片数量超出限制",
597
+ "image.fileTooLarge": "图片过大",
598
+ "settingsNav": "侧边聊天",
599
+ "settings.lookupTitle": "默认勾选「需要时从工作区 / 主会话查信息」",
600
+ "settings.lookupDesc": "新会话的侧边聊天默认打开此开关;可在每次聊天中临时更改",
601
+ "settings.sendImmediatelyTitle": "选中内容后立即发送",
602
+ "settings.sendImmediatelyDesc": "关闭后,选中的内容会作为附件先放进侧边聊天输入框,由你确认后再发送",
603
+ "settings.defaultPromptTitle": "默认用户提示词",
604
+ "settings.defaultPromptDesc": "「立即发送」开启时,选中的内容会自动附加这段提示词,让回答更符合你的需求",
605
+ "settings.defaultPromptPlaceholder": "例如:请用中文回答,并尽量简洁…",
606
+ "settings.bringModeTitle": "带回内容的方式",
607
+ "settings.bringModeDesc": "把侧边聊天的 AI 回复带回主会话时,内容落在哪里(两种方式都不会立即发送)",
608
+ "settings.bringModeDraftTitle": "写入输入框草稿",
609
+ "settings.bringModeDraftDesc": "内容进入主会话输入框,可直接编辑后再发送",
610
+ "settings.bringModeContextTitle": "注入为折叠提示行",
611
+ "settings.bringModeContextDesc": "内容作为一条折叠的上下文提示行注入主会话(带来源标记),不写输入框;下次对话时模型可见",
612
+ "insert.direct": "直接带回",
613
+ "insert.summarize": "摘要后带回",
614
+ "insert.summarizing": "摘要中…",
615
+ "insert.failed": "无法写入主会话输入框",
616
+ "insert.summarizeFailed": "摘要失败",
617
+ "insert.contextSummary": "侧边聊天带回",
618
+ "insert.summarizeContextSummary": "侧边聊天摘要",
619
+ "question.options": "可选选项",
620
+ "question.option": "选项",
621
+ "question.bringAll": "带入全部",
622
+ "question.bringAllNew": "新会话全部",
623
+ "question.bringOne": "带入",
624
+ "question.bringOneNew": "新会话",
625
+ "question.bringing": "带入中…",
626
+ "question.failed": "带入侧边聊天失败",
627
+ "question.delete": "删除",
628
+ "question.deleteAll": "删除全部",
629
+ "question.collapse": "折叠",
630
+ "question.expand": "展开",
631
+ "question.allPrompt": "请帮我理解上面的问题和各个选项的含义,并给出选择建议。",
632
+ "question.onePrompt": "请帮我理解这个选项的含义,以及它在这个问题中的利弊。",
633
+ "question.openHint": "主会话有一个问题弹框,点击在侧边聊天里询问"
634
+ };
635
+ const en = {
636
+ "ask.new": "Ask in side chat",
637
+ "ask.continue": "Continue active side chat",
638
+ "panel.title": "Side chat",
639
+ "panel.empty": "No side chat for this conversation yet. Select some message text and choose \"Ask in side chat\".",
640
+ "panel.close": "Close",
641
+ "panel.input.placeholder": "Type a message, Enter to send",
642
+ "panel.send": "Send",
643
+ "panel.stop": "Stop",
644
+ "panel.lookup": "Look up workspace / parent when needed",
645
+ "panel.model": "Model",
646
+ "panel.provider": "Provider",
647
+ "panel.effort": "Effort",
648
+ "panel.permission": "Permission",
649
+ "panel.noModel": "(inherited)",
650
+ "panel.effortDefault": "Default",
651
+ "panel.expand": "Expand side chat",
652
+ "panel.collapse": "Collapse side chat",
653
+ "panel.dispose": "End this side chat",
654
+ "panel.delete": "Delete",
655
+ "panel.deleteAll": "Delete all",
656
+ "panel.error": "Side chat error",
657
+ "panel.running": "Thinking…",
658
+ "panel.copy": "Copy",
659
+ "panel.copied": "Copied",
660
+ "panel.footnotes": "Footnotes",
661
+ "panel.think": "Think",
662
+ "panel.commands": "Commands",
663
+ "command.model": "/model Model",
664
+ "command.permission": "/permission Permission",
665
+ "plan.chipTitle": "Plan mode on — click to turn off (/plan off)",
666
+ "plan.chipAria": "Plan mode on, press to turn off",
667
+ "goal.label": "Goal",
668
+ "image.railGroup": "Image attachments",
669
+ "image.railOpen": "Open original",
670
+ "image.railScrollLeft": "Scroll left",
671
+ "image.railScrollRight": "Scroll right",
672
+ "image.remove": "Remove image",
673
+ "image.label": "Image",
674
+ "image.open": "Open original",
675
+ "image.loading": "Loading…",
676
+ "image.loadFailed": "Load failed",
677
+ "image.lightboxDialog": "Image preview",
678
+ "image.close": "Close",
679
+ "image.dropTitle": "Drop images here",
680
+ "image.dropDesc": "PNG / JPEG / WebP / GIF",
681
+ "image.unsupported": "Unsupported image type",
682
+ "image.tooMany": "Too many images",
683
+ "image.fileTooLarge": "Image too large",
684
+ "settingsNav": "Side chat",
685
+ "settings.lookupTitle": "Default on: \"look up workspace / parent when needed\"",
686
+ "settings.lookupDesc": "New conversations start with this switch on; you can still change it per chat",
687
+ "settings.sendImmediatelyTitle": "Send selected text immediately",
688
+ "settings.sendImmediatelyDesc": "When off, the selection is staged as an attachment in the composer for you to confirm before sending",
689
+ "settings.defaultPromptTitle": "Default prompt",
690
+ "settings.defaultPromptDesc": "When \"send immediately\" is on, this prompt is appended to the selection so the answer matches your needs",
691
+ "settings.defaultPromptPlaceholder": "e.g. Answer in Chinese and keep it concise…",
692
+ "settings.bringModeTitle": "Bring-back target",
693
+ "settings.bringModeDesc": "Where a side-chat AI reply lands when brought back (neither mode sends immediately)",
694
+ "settings.bringModeDraftTitle": "Into the composer draft",
695
+ "settings.bringModeDraftDesc": "Text lands in the main composer so you can edit before sending",
696
+ "settings.bringModeContextTitle": "As a collapsed context row",
697
+ "settings.bringModeContextDesc": "Text is injected as a collapsed, source-tagged context row — not into the composer; the model sees it next turn",
698
+ "insert.direct": "Insert directly",
699
+ "insert.summarize": "Summarize & insert",
700
+ "insert.summarizing": "Summarizing…",
701
+ "insert.failed": "Could not write to the main composer",
702
+ "insert.summarizeFailed": "Summarization failed",
703
+ "insert.contextSummary": "Brought from side chat",
704
+ "insert.summarizeContextSummary": "Side chat summary",
705
+ "question.options": "Options",
706
+ "question.option": "Option",
707
+ "question.bringAll": "Bring all",
708
+ "question.bringAllNew": "New chat (all)",
709
+ "question.bringOne": "Bring",
710
+ "question.bringOneNew": "New chat",
711
+ "question.bringing": "Bringing…",
712
+ "question.failed": "Failed to bring into the side chat",
713
+ "question.delete": "Delete",
714
+ "question.deleteAll": "Delete all",
715
+ "question.collapse": "Collapse",
716
+ "question.expand": "Expand",
717
+ "question.allPrompt": "Please explain what this question is asking and what each option means, and advise how I should choose.",
718
+ "question.onePrompt": "Please explain what this option means and its trade-offs for this question.",
719
+ "question.openHint": "The main conversation has a question dialog — click to ask in the side chat"
720
+ };
721
+ //#endregion
722
+ //#region src/settings-shared.ts
723
+ /** Fallback prefs used whenever the settings document is unreachable or malformed. */
724
+ const SUBCHAT_PREFS_DEFAULTS = {
725
+ lookupDefault: false,
726
+ sendImmediately: true,
727
+ defaultPrompt: "",
728
+ bringMode: "draft"
729
+ };
730
+ //#endregion
731
+ //#region \0dsh-css:D:\code\DSH-side-chat\src\client\client.module.css.mjs
732
+ const css$1 = ".LU9nbG_selectionMenu{z-index:2147483000;background:var(--dsw-alias-bg-overlay);border:1px solid var(--dsw-alias-border-l3);box-shadow:0 4px 16px var(--dsw-alias-bg-mask-2);pointer-events:auto;border-radius:10px;gap:4px;padding:4px;display:flex;position:fixed;transform:translate(-50%)}.LU9nbG_selectionButton{font-family:var(--dsw-font-family);color:var(--dsw-alias-label-primary);cursor:pointer;white-space:nowrap;background:0 0;border:none;border-radius:7px;padding:6px 10px;font-size:13px;line-height:20px}.LU9nbG_selectionButton:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}.LU9nbG_selectionButton:disabled{cursor:default;opacity:.55}.LU9nbG_panel{width:var(--dsh-subchat-width,360px);z-index:2147482990;background:var(--dsw-specific-sidebar-fill);border-left:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-primary);font-family:var(--dsw-font-family);pointer-events:auto;flex-direction:column;font-size:14px;display:flex;position:fixed;top:0;bottom:0;right:0}.LU9nbG_panelResize{cursor:col-resize;z-index:2;touch-action:none;width:8px;position:absolute;top:0;bottom:0;left:-4px}.LU9nbG_panelHeader{border-bottom:1px solid var(--dsw-alias-border-l1);flex:none;justify-content:space-between;align-items:center;padding:12px 14px;display:flex}.LU9nbG_panelTitle{font-weight:600}.LU9nbG_panelHeaderActions{align-items:center;gap:4px;display:flex}.LU9nbG_panelIconButton{width:28px;height:28px;color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;border-radius:999px;place-items:center;display:grid}.LU9nbG_panelIconButton:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}.LU9nbG_panelClose{font-family:var(--dsw-font-family);color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;border-radius:6px;padding:4px 8px;font-size:13px}.LU9nbG_panelClose:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}.LU9nbG_collapsedHandle{z-index:2147482990;width:32px;height:32px;color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-bg-overlay);border:1px solid var(--dsw-alias-border-l2);cursor:pointer;box-shadow:var(--dsw-shadow-lv2);border-radius:999px;place-items:center;display:grid;position:fixed;top:50%;right:8px;transform:translateY(-50%)}.LU9nbG_collapsedHandle:hover{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover)}.LU9nbG_questionFab{z-index:2147482990;width:32px;height:32px;color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-bg-overlay);border:1px solid var(--dsw-alias-state-business-primary);cursor:pointer;box-shadow:var(--dsw-shadow-lv2);border-radius:999px;place-items:center;display:grid;position:fixed}.LU9nbG_questionFab:hover{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover)}.LU9nbG_questionFabDot{background:var(--dsw-alias-state-business-primary);border-radius:50%;width:8px;height:8px;position:absolute;top:4px;right:4px}.LU9nbG_questionBlock{border-bottom:1px solid var(--dsw-alias-border-l1);flex-direction:column;flex:none;gap:8px;max-height:40%;padding:8px;display:flex;overflow-y:auto}.LU9nbG_questionBlockActions{justify-content:space-between;align-items:center;display:flex}.LU9nbG_questionToggle{font-family:var(--dsw-font-family);color:var(--dsw-alias-label-tertiary);cursor:pointer;background:0 0;border:none;border-radius:6px;padding:2px 6px;font-size:12px;line-height:18px}.LU9nbG_questionToggle:hover{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover)}.LU9nbG_questionDeleteAll{font-family:var(--dsw-font-family);color:var(--dsw-alias-label-tertiary);cursor:pointer;background:0 0;border:none;border-radius:6px;padding:2px 6px;font-size:12px;line-height:18px}.LU9nbG_questionDeleteAll:hover{color:var(--dsw-alias-state-error-primary);background:var(--dsw-alias-interactive-bg-hover)}.LU9nbG_questionItemCollapsed{gap:0;padding:8px 12px}.LU9nbG_questionItem{border:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-2);border-radius:12px;flex-direction:column;gap:6px;padding:10px 12px;display:flex}.LU9nbG_questionHeader{justify-content:space-between;align-items:center;gap:8px;display:flex}.LU9nbG_questionHeaderText{min-width:0;color:var(--dsw-alias-label-primary);text-overflow:ellipsis;white-space:nowrap;flex:1;font-size:13px;font-weight:600;line-height:20px;overflow:hidden}.LU9nbG_questionHeaderActions{flex:none;align-items:center;gap:4px;display:flex}.LU9nbG_questionBody{color:var(--dsw-alias-label-primary);white-space:pre-wrap;word-break:break-word;font-size:14px;line-height:22px}.LU9nbG_questionDetail{color:var(--dsw-alias-label-secondary);white-space:pre-wrap;word-break:break-word;font-size:12px;line-height:18px}.LU9nbG_questionOption{border:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-1);border-radius:10px;justify-content:space-between;align-items:center;gap:8px;padding:6px 8px 6px 10px;display:flex}.LU9nbG_questionOptionText{min-width:0;color:var(--dsw-alias-label-primary);word-break:break-word;flex:1;font-size:13px;line-height:20px}.LU9nbG_questionOptionLabel{font-weight:500}.LU9nbG_questionOptionDesc{color:var(--dsw-alias-label-secondary)}.LU9nbG_questionBringButton{font-family:var(--dsw-font-family);color:var(--dsw-alias-label-primary);border:1px solid var(--dsw-alias-border-l2);cursor:pointer;white-space:nowrap;background:0 0;border-radius:999px;flex:none;padding:2px 10px;font-size:12px;line-height:18px}.LU9nbG_questionBringButton:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}.LU9nbG_questionBringButton:disabled{cursor:default;opacity:.55}.LU9nbG_questionDelete{width:22px;height:22px;color:var(--dsw-alias-label-tertiary);cursor:pointer;background:0 0;border:none;border-radius:6px;flex:none;place-items:center;font-size:14px;line-height:20px;display:grid}.LU9nbG_questionDelete:hover{color:var(--dsw-alias-state-error-primary);background:var(--dsw-alias-interactive-bg-hover)}.LU9nbG_panelList{border-bottom:1px solid var(--dsw-alias-border-l1);flex-direction:column;flex:none;gap:2px;max-height:132px;padding:8px;display:flex;overflow-y:auto}.LU9nbG_panelEmpty{color:var(--dsw-alias-label-tertiary);padding:6px 8px;font-size:12px;line-height:18px}.LU9nbG_panelListActions{justify-content:flex-end;padding:0 2px 2px;display:flex}.LU9nbG_panelListDeleteAll{font-family:var(--dsw-font-family);color:var(--dsw-alias-label-tertiary);cursor:pointer;background:0 0;border:none;border-radius:6px;padding:2px 6px;font-size:12px;line-height:18px}.LU9nbG_panelListDeleteAll:hover{color:var(--dsw-alias-state-error-primary);background:var(--dsw-alias-interactive-bg-hover)}.LU9nbG_panelListItem{font-family:var(--dsw-font-family);color:var(--dsw-alias-label-primary);cursor:pointer;text-align:left;background:0 0;border:none;border-radius:8px;align-items:center;gap:8px;padding:6px 8px;display:flex}.LU9nbG_panelListItem:hover{background:var(--dsw-specific-sidebar-nav-item-hover)}.LU9nbG_panelListItemActive{background:var(--dsw-specific-sidebar-nav-item-active)}.LU9nbG_panelListItemDot{background:var(--dsw-alias-label-tertiary);border-radius:50%;flex:none;width:8px;height:8px}.LU9nbG_panelListItemDot[data-running=\"1\"]{background:var(--dsw-alias-state-business-primary)}.LU9nbG_panelListItemLabel{text-overflow:ellipsis;white-space:nowrap;min-width:0;font-size:12px;font-family:var(--ds-font-family-code);overflow:hidden}.LU9nbG_panelListItemRow{align-items:center;gap:2px;display:flex}.LU9nbG_panelListItemRow .LU9nbG_panelListItem{flex:1;min-width:0}.LU9nbG_panelListItemRemove{width:22px;height:22px;color:var(--dsw-alias-label-tertiary);cursor:pointer;background:0 0;border:none;border-radius:6px;flex:none;place-items:center;font-size:14px;line-height:20px;display:grid}.LU9nbG_panelListItemRemove:hover{color:var(--dsw-alias-state-error-primary);background:var(--dsw-alias-interactive-bg-hover)}.LU9nbG_panelTranscript{flex-direction:column;flex:1;gap:10px;min-height:0;padding:14px;display:flex;overflow-y:auto}.LU9nbG_messageUser{background:var(--dsw-specific-bubble);color:var(--dsw-alias-label-primary);white-space:pre-wrap;word-break:break-word;border-radius:22px;align-self:flex-end;max-width:86%;padding:10px 16px;font-size:16px;line-height:24px}.LU9nbG_messageAssistant{min-width:0;max-width:100%;color:var(--dsw-alias-label-primary);align-self:flex-start}.LU9nbG_messageActions{gap:6px;margin-top:6px;display:flex}.LU9nbG_messageInsertButton{font-family:var(--dsw-font-family);color:var(--dsw-alias-label-secondary);border:1px solid var(--dsw-alias-border-l1);cursor:pointer;background:0 0;border-radius:999px;padding:2px 10px;font-size:12px;line-height:18px}.LU9nbG_messageInsertButton:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}.LU9nbG_messageInsertButton:disabled{cursor:default;opacity:.55}.LU9nbG_reasoningSummary{min-width:0;color:var(--dsw-alias-label-tertiary);text-overflow:ellipsis;white-space:nowrap;font-size:14px;line-height:24px;overflow:hidden}.LU9nbG_reasoningBody{color:var(--dsw-alias-label-tertiary);white-space:pre-wrap;word-break:break-word;padding:4px 0 4px 22px;font-size:14px;line-height:24px}.LU9nbG_panelRunning{color:var(--dsw-alias-label-tertiary);align-self:flex-start;align-items:center;gap:6px;font-size:12px;line-height:18px;display:inline-flex}.LU9nbG_panelRunningDot{background:var(--dsw-alias-state-business-primary);border-radius:50%;flex:none;width:8px;height:8px;animation:1s ease-in-out infinite alternate LU9nbG_panelRunningPulse}.LU9nbG_panelRunningClock{color:var(--dsw-alias-label-caption)}@keyframes LU9nbG_panelRunningPulse{0%{opacity:1}to{opacity:.3}}@media (prefers-reduced-motion:reduce){.LU9nbG_panelRunningDot{animation:none}}.LU9nbG_panelError{color:var(--dsw-alias-state-error-primary);font-size:12px;line-height:18px}.LU9nbG_panelComposer{border:1px solid var(--dsw-alias-border-l2-darkmode-thin);background:var(--dsw-specific-input-major);box-shadow:var(--dsw-shadow-lv2);border-radius:22px;flex-direction:column;flex:none;gap:4px;margin:10px 14px 14px;padding:10px;display:flex}.LU9nbG_panelToolbar{flex:none;justify-content:flex-end;align-items:center;gap:4px;min-width:0;display:flex}.LU9nbG_panelTextarea{box-sizing:border-box;resize:none;width:100%;min-width:0;color:var(--dsw-alias-label-primary);caret-color:var(--dsw-alias-state-business-primary);font-family:var(--dsw-font-family);background:0 0;border:none;outline:none;flex:1;min-height:32px;max-height:160px;padding:4px 8px;font-size:16px;line-height:24px}.LU9nbG_panelLookup{max-width:100%;height:24px;color:var(--dsw-alias-label-secondary);cursor:pointer;white-space:nowrap;border-radius:24px;align-self:flex-start;align-items:center;gap:6px;padding:0 8px;font-size:12px;font-weight:500;line-height:18px;display:inline-flex}.LU9nbG_panelLookup span{text-overflow:ellipsis;min-width:0;overflow:hidden}.LU9nbG_panelLookup:hover{background:var(--dsw-alias-interactive-bg-hover)}.LU9nbG_panelAttachment{border:1px solid var(--dsw-alias-border-l1);background:var(--dsw-specific-tip);border-radius:10px;align-items:center;gap:8px;padding:6px 10px;display:flex}.LU9nbG_panelAttachmentText{min-width:0;color:var(--dsw-alias-label-secondary);white-space:nowrap;text-overflow:ellipsis;flex:1;font-size:12px;line-height:18px;overflow:hidden}.LU9nbG_panelAttachmentRemove{width:20px;height:20px;color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;border-radius:999px;flex:none;font-size:16px;line-height:20px}.LU9nbG_panelAttachmentRemove:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}.LU9nbG_panelFooter{flex:none;padding:0 14px 12px}.LU9nbG_panelDispose{font-family:var(--dsw-font-family);color:var(--dsw-alias-label-tertiary);cursor:pointer;background:0 0;border:none;padding:4px 0;font-size:12px;line-height:18px}.LU9nbG_panelDispose:hover{color:var(--dsw-alias-state-error-primary)}.LU9nbG_primary{background:var(--dsw-alias-button-info-fill);color:#fff;cursor:pointer;border:none;border-radius:999px;flex:none;place-items:center;width:34px;height:34px;transition:background-color .1s;display:grid}.LU9nbG_primary:hover{background:var(--dsw-alias-button-info-hover)}.LU9nbG_panelAttachmentRail{flex:none;min-width:0}.LU9nbG_messageUserText{white-space:pre-wrap;word-break:break-word}.LU9nbG_modelSelect{min-width:0;position:relative}.LU9nbG_modelSelectTrigger{min-width:0;max-width:200px;height:28px;color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;border-radius:24px;outline:none;align-items:center;gap:4px;padding:0 4px 0 8px;font-size:13px;font-weight:500;line-height:20px;display:flex}.LU9nbG_modelSelectTrigger:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}.LU9nbG_modelSelectLabel{text-overflow:ellipsis;white-space:nowrap;min-width:0;overflow:hidden}.LU9nbG_modelSelectEffort{color:var(--dsw-alias-label-caption);flex:none}.LU9nbG_chevronOpen{transform:rotate(180deg)}.LU9nbG_modelSelectMenu{z-index:30;border:1px solid var(--dsw-alias-border-inverted);background:var(--dsw-specific-menu);width:min(260px,100vw - 32px);max-height:min(360px,100vh - 96px);box-shadow:var(--dsw-shadow-lv3);color:var(--dsw-alias-label-primary);border-radius:12px;flex-direction:column;padding:4px;display:flex;position:absolute;bottom:calc(100% + 8px);right:0;overflow:hidden}.LU9nbG_modelCell{width:100%;height:40px;color:var(--dsw-alias-label-primary);cursor:pointer;text-align:left;background:0 0;border:none;border-radius:10px;flex:none;align-items:center;gap:8px;padding:0 10px;font-size:14px;line-height:22px;display:flex}.LU9nbG_modelCell:hover{background:var(--dsw-alias-interactive-bg-hover)}.LU9nbG_modelCellLabel{text-overflow:ellipsis;white-space:nowrap;flex:auto;min-width:0;overflow:hidden}.LU9nbG_modelCellValue{text-overflow:ellipsis;white-space:nowrap;min-width:0;color:var(--dsw-alias-label-tertiary);flex:0 auto;overflow:hidden}.LU9nbG_modelCellChevron{color:var(--dsw-alias-label-tertiary);flex:none}.LU9nbG_modelGroups{min-height:0;overflow-y:auto}.LU9nbG_modelGroup+.LU9nbG_modelGroup{margin-top:4px}.LU9nbG_modelGroupTitle{z-index:1;background:var(--dsw-specific-menu);color:var(--dsw-alias-label-tertiary);padding:5px 8px 3px;font-size:12px;font-weight:500;line-height:18px;position:sticky;top:0}.LU9nbG_modelOption{width:100%;min-height:38px;color:inherit;text-align:left;cursor:pointer;background:0 0;border:none;border-radius:10px;outline:none;align-items:center;gap:8px;padding:6px 8px;display:flex}.LU9nbG_modelOption:hover:not(:disabled),.LU9nbG_modelOptionSelected{background:var(--dsw-alias-interactive-bg-hover)}.LU9nbG_modelOptionCopy{flex-direction:column;flex:1;min-width:0;display:flex}.LU9nbG_modelName{color:inherit;text-overflow:ellipsis;white-space:nowrap;font-size:14px;font-weight:500;line-height:20px;overflow:hidden}.LU9nbG_modelDescription{color:var(--dsw-alias-label-tertiary);text-overflow:ellipsis;white-space:nowrap;font-size:12px;line-height:18px;overflow:hidden}.LU9nbG_modelCheck{color:var(--dsw-alias-label-primary);flex:0 0 18px;place-items:center;display:grid}.LU9nbG_modelEmpty{color:var(--dsw-alias-label-tertiary);padding:10px;font-size:13px;line-height:20px}.LU9nbG_settingsSection{flex-direction:column;gap:8px;display:flex}.LU9nbG_settingsRow{border:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-2);cursor:pointer;border-radius:12px;justify-content:space-between;align-items:center;gap:12px;padding:10px 12px;display:flex}.LU9nbG_settingsRowText{flex-direction:column;flex:1;gap:2px;min-width:0;display:flex}.LU9nbG_settingsRowTitle{color:var(--dsw-alias-label-primary);font-size:14px;line-height:20px}.LU9nbG_settingsRowDesc{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px}.LU9nbG_settingsToggle{width:16px;height:16px;accent-color:var(--dsw-alias-state-business-primary);flex:none}.LU9nbG_settingsBringMode{flex-direction:column;gap:8px;display:flex}.LU9nbG_settingsBringOption{border:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-2);cursor:pointer;border-radius:12px;align-items:flex-start;gap:12px;padding:10px 12px;display:flex}.LU9nbG_settingsBringOptionActive{border-color:var(--dsw-alias-state-business-primary)}.LU9nbG_settingsPromptInput{box-sizing:border-box;resize:vertical;width:100%;min-height:72px;color:var(--dsw-alias-label-primary);background:var(--dsw-specific-input-major);border:1px solid var(--dsw-alias-border-l2);font-family:var(--dsw-font-family);border-radius:10px;padding:8px 10px;font-size:13px;line-height:20px}.LU9nbG_settingsPromptInput:focus{outline:2px solid var(--dsw-alias-border-l3);outline-offset:-1px}";
733
+ const tagId$1 = "dsh-side-chat-plus/client.module.css";
734
+ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$1) + "]") === null) {
735
+ const tag = document.createElement("style");
736
+ tag.dataset.plugin = "dsh-side-chat-plus";
737
+ tag.dataset.pluginCss = tagId$1;
738
+ tag.textContent = css$1;
739
+ document.head.appendChild(tag);
740
+ }
741
+ var client_module_css_default = {
742
+ "questionHeaderText": "LU9nbG_questionHeaderText",
743
+ "panelListItemRow": "LU9nbG_panelListItemRow",
744
+ "panelRunning": "LU9nbG_panelRunning",
745
+ "modelCellChevron": "LU9nbG_modelCellChevron",
746
+ "messageInsertButton": "LU9nbG_messageInsertButton",
747
+ "settingsRowTitle": "LU9nbG_settingsRowTitle",
748
+ "modelSelectEffort": "LU9nbG_modelSelectEffort",
749
+ "questionDetail": "LU9nbG_questionDetail",
750
+ "reasoningBody": "LU9nbG_reasoningBody",
751
+ "panelRunningDot": "LU9nbG_panelRunningDot",
752
+ "questionBlockActions": "LU9nbG_questionBlockActions",
753
+ "questionItem": "LU9nbG_questionItem",
754
+ "questionOptionText": "LU9nbG_questionOptionText",
755
+ "panelFooter": "LU9nbG_panelFooter",
756
+ "modelOptionSelected": "LU9nbG_modelOptionSelected",
757
+ "messageAssistant": "LU9nbG_messageAssistant",
758
+ "settingsSection": "LU9nbG_settingsSection",
759
+ "settingsBringOptionActive": "LU9nbG_settingsBringOptionActive",
760
+ "panelClose": "LU9nbG_panelClose",
761
+ "panelAttachment": "LU9nbG_panelAttachment",
762
+ "panelHeader": "LU9nbG_panelHeader",
763
+ "panelListDeleteAll": "LU9nbG_panelListDeleteAll",
764
+ "panelListItemDot": "LU9nbG_panelListItemDot",
765
+ "panelEmpty": "LU9nbG_panelEmpty",
766
+ "panelListActions": "LU9nbG_panelListActions",
767
+ "questionItemCollapsed": "LU9nbG_questionItemCollapsed",
768
+ "modelCellLabel": "LU9nbG_modelCellLabel",
769
+ "panelDispose": "LU9nbG_panelDispose",
770
+ "questionToggle": "LU9nbG_questionToggle",
771
+ "panelAttachmentRemove": "LU9nbG_panelAttachmentRemove",
772
+ "panelHeaderActions": "LU9nbG_panelHeaderActions",
773
+ "questionFabDot": "LU9nbG_questionFabDot",
774
+ "modelSelect": "LU9nbG_modelSelect",
775
+ "panelError": "LU9nbG_panelError",
776
+ "panelTranscript": "LU9nbG_panelTranscript",
777
+ "messageActions": "LU9nbG_messageActions",
778
+ "questionOptionLabel": "LU9nbG_questionOptionLabel",
779
+ "modelSelectMenu": "LU9nbG_modelSelectMenu",
780
+ "panelComposer": "LU9nbG_panelComposer",
781
+ "messageUserText": "LU9nbG_messageUserText",
782
+ "panelListItem": "LU9nbG_panelListItem",
783
+ "messageUser": "LU9nbG_messageUser",
784
+ "modelGroupTitle": "LU9nbG_modelGroupTitle",
785
+ "modelCell": "LU9nbG_modelCell",
786
+ "modelOption": "LU9nbG_modelOption",
787
+ "settingsBringMode": "LU9nbG_settingsBringMode",
788
+ "questionDeleteAll": "LU9nbG_questionDeleteAll",
789
+ "modelCellValue": "LU9nbG_modelCellValue",
790
+ "panelTextarea": "LU9nbG_panelTextarea",
791
+ "panelLookup": "LU9nbG_panelLookup",
792
+ "selectionButton": "LU9nbG_selectionButton",
793
+ "modelCheck": "LU9nbG_modelCheck",
794
+ "modelGroups": "LU9nbG_modelGroups",
795
+ "settingsRow": "LU9nbG_settingsRow",
796
+ "modelEmpty": "LU9nbG_modelEmpty",
797
+ "questionBringButton": "LU9nbG_questionBringButton",
798
+ "modelSelectTrigger": "LU9nbG_modelSelectTrigger",
799
+ "panelList": "LU9nbG_panelList",
800
+ "panelResize": "LU9nbG_panelResize",
801
+ "panelTitle": "LU9nbG_panelTitle",
802
+ "modelName": "LU9nbG_modelName",
803
+ "panelRunningPulse": "LU9nbG_panelRunningPulse",
804
+ "settingsPromptInput": "LU9nbG_settingsPromptInput",
805
+ "questionOption": "LU9nbG_questionOption",
806
+ "selectionMenu": "LU9nbG_selectionMenu",
807
+ "settingsBringOption": "LU9nbG_settingsBringOption",
808
+ "panelListItemRemove": "LU9nbG_panelListItemRemove",
809
+ "modelGroup": "LU9nbG_modelGroup",
810
+ "modelOptionCopy": "LU9nbG_modelOptionCopy",
811
+ "questionBlock": "LU9nbG_questionBlock",
812
+ "questionOptionDesc": "LU9nbG_questionOptionDesc",
813
+ "questionHeader": "LU9nbG_questionHeader",
814
+ "questionDelete": "LU9nbG_questionDelete",
815
+ "questionFab": "LU9nbG_questionFab",
816
+ "panelToolbar": "LU9nbG_panelToolbar",
817
+ "primary": "LU9nbG_primary",
818
+ "panelAttachmentRail": "LU9nbG_panelAttachmentRail",
819
+ "panelAttachmentText": "LU9nbG_panelAttachmentText",
820
+ "settingsRowDesc": "LU9nbG_settingsRowDesc",
821
+ "questionBody": "LU9nbG_questionBody",
822
+ "panelRunningClock": "LU9nbG_panelRunningClock",
823
+ "chevronOpen": "LU9nbG_chevronOpen",
824
+ "panelListItemActive": "LU9nbG_panelListItemActive",
825
+ "panelIconButton": "LU9nbG_panelIconButton",
826
+ "modelDescription": "LU9nbG_modelDescription",
827
+ "settingsRowText": "LU9nbG_settingsRowText",
828
+ "collapsedHandle": "LU9nbG_collapsedHandle",
829
+ "panelListItemLabel": "LU9nbG_panelListItemLabel",
830
+ "panel": "LU9nbG_panel",
831
+ "reasoningSummary": "LU9nbG_reasoningSummary",
832
+ "questionHeaderActions": "LU9nbG_questionHeaderActions",
833
+ "modelSelectLabel": "LU9nbG_modelSelectLabel",
834
+ "settingsToggle": "LU9nbG_settingsToggle"
835
+ };
836
+ //#endregion
837
+ //#region \0dsh-css:D:\code\DSH-side-chat\src\client\layout.css.mjs
838
+ const css = "/**\n * Layout push: when the side-chat panel is open it OCCUPIES layout instead of\n * floating over it — the app shell (#root) gives up the right margin so the\n * center column (conversation + input bar) squeezes left, like a VSCode\n * sidebar. The width rides a CSS variable the panel sets.\n */\n#root {\n margin-right: var(--dsh-subchat-width, 0px);\n transition: margin-right var(--ds-transition-duration-slow) var(--ds-ease-in-out);\n}\n\n@media (prefers-reduced-motion: reduce) {\n #root {\n transition: none;\n }\n}\n";
839
+ const tagId = "dsh-side-chat-plus/layout.css";
840
+ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
841
+ const tag = document.createElement("style");
842
+ tag.dataset.plugin = "dsh-side-chat-plus";
843
+ tag.dataset.pluginCss = tagId;
844
+ tag.textContent = css;
845
+ document.head.appendChild(tag);
846
+ }
847
+ //#endregion
848
+ //#region src/client/index.tsx
849
+ /**
850
+ * Client half of dsh-side-chat: a text-selection floating menu, a right-side
851
+ * side-chat panel (drag-resizable + collapsible), the main-conversation-style
852
+ * model/permission selectors and send/stop buttons, and a "Side chat" settings
853
+ * section. The panel is isolated per current conversation and talks to the
854
+ * host /sidechat API.
855
+ */
856
+ /** Services required before mounting. */
857
+ const inject = [
858
+ "sessions",
859
+ "locale",
860
+ "slots",
861
+ "conversation",
862
+ "uiSession"
863
+ ];
864
+ function emptyPanel() {
865
+ return {
866
+ open: false,
867
+ parentSessionId: "",
868
+ activeChildId: null,
869
+ items: [],
870
+ messages: [],
871
+ draft: "",
872
+ attachment: null,
873
+ attachments: [],
874
+ lookup: false,
875
+ directory: null,
876
+ permissions: null,
877
+ provider: "",
878
+ model: "",
879
+ effort: "",
880
+ preset: "",
881
+ commandOpen: null,
882
+ planActive: false,
883
+ planPending: false,
884
+ goalObjective: null,
885
+ error: null
886
+ };
887
+ }
888
+ /** Create the browser store (one instance per activation, per the factory rule). */
889
+ function createStore() {
890
+ let current;
891
+ let panel = emptyPanel();
892
+ let anchor = null;
893
+ let prefs = { ...SUBCHAT_PREFS_DEFAULTS };
894
+ let mainQuestion = null;
895
+ let dismissedQuestionIds = [];
896
+ const bySession = /* @__PURE__ */ new Map();
897
+ const listeners = /* @__PURE__ */ new Set();
898
+ let snapshot = {
899
+ current,
900
+ panel,
901
+ anchor,
902
+ prefs,
903
+ mainQuestion,
904
+ dismissedQuestionIds
905
+ };
906
+ const notify = () => {
907
+ snapshot = {
908
+ current,
909
+ panel,
910
+ anchor,
911
+ prefs,
912
+ mainQuestion,
913
+ dismissedQuestionIds
914
+ };
915
+ for (const fn of [...listeners]) fn();
916
+ };
917
+ return {
918
+ getSnapshot: () => snapshot,
919
+ subscribe: (fn) => {
920
+ listeners.add(fn);
921
+ return () => {
922
+ listeners.delete(fn);
923
+ };
924
+ },
925
+ setCurrent(next) {
926
+ if (next === current) return;
927
+ if (current !== void 0) bySession.set(current, panel);
928
+ current = next;
929
+ panel = next === void 0 ? emptyPanel() : bySession.get(next) ?? {
930
+ ...emptyPanel(),
931
+ parentSessionId: next,
932
+ lookup: prefs.lookupDefault
933
+ };
934
+ anchor = null;
935
+ mainQuestion = null;
936
+ dismissedQuestionIds = [];
937
+ notify();
938
+ },
939
+ setAnchor(next) {
940
+ anchor = next;
941
+ notify();
942
+ },
943
+ setPrefs(next) {
944
+ prefs = next;
945
+ notify();
946
+ },
947
+ setMainQuestion(questions) {
948
+ mainQuestion = questions;
949
+ notify();
950
+ },
951
+ dismissQuestion(id) {
952
+ if (!dismissedQuestionIds.includes(id)) {
953
+ dismissedQuestionIds = [...dismissedQuestionIds, id];
954
+ notify();
955
+ }
956
+ },
957
+ dismissAllQuestions(ids) {
958
+ dismissedQuestionIds = [.../* @__PURE__ */ new Set([...dismissedQuestionIds, ...ids])];
959
+ notify();
960
+ },
961
+ openPanel(parentSessionId) {
962
+ panel = {
963
+ ...panel,
964
+ open: true,
965
+ parentSessionId
966
+ };
967
+ notify();
968
+ },
969
+ closePanel() {
970
+ panel = {
971
+ ...panel,
972
+ open: false
973
+ };
974
+ notify();
975
+ },
976
+ setActive(childId) {
977
+ panel = {
978
+ ...panel,
979
+ activeChildId: childId,
980
+ messages: [],
981
+ error: null
982
+ };
983
+ notify();
984
+ },
985
+ patch(partial) {
986
+ panel = {
987
+ ...panel,
988
+ ...partial
989
+ };
990
+ notify();
991
+ }
992
+ };
993
+ }
994
+ /** Resolve the localized label for one locale key (module-level active locale). */
995
+ function translate(activeLocale, key) {
996
+ return (activeLocale === "en" ? en : zh)[key] ?? key;
997
+ }
998
+ /** Format a run duration like the main conversation: "Xs" / "Xm SSs" (or Chinese). */
999
+ function formatRunDuration(ms, activeLocale) {
1000
+ const total = Math.max(0, Math.floor(ms / 1e3));
1001
+ const minutes = Math.floor(total / 60);
1002
+ const seconds = total % 60;
1003
+ if (minutes > 0) return activeLocale === "en" ? `${minutes}m ${String(seconds).padStart(2, "0")}s` : `${minutes}分${String(seconds).padStart(2, "0")}秒`;
1004
+ return activeLocale === "en" ? `${seconds}s` : `${seconds}秒`;
1005
+ }
1006
+ /** Accepted image media types (mirror of dsh-attachment's ImageMediaType). */
1007
+ const IMAGE_MEDIA_TYPES = [
1008
+ "image/png",
1009
+ "image/jpeg",
1010
+ "image/webp",
1011
+ "image/gif"
1012
+ ];
1013
+ /** Create runtime draft images with object URLs (validates media type). */
1014
+ function createDraftImages(files) {
1015
+ return files.map((file) => {
1016
+ if (!IMAGE_MEDIA_TYPES.includes(file.type)) throw new Error(`unsupported image type: ${file.type || "unknown"}`);
1017
+ return {
1018
+ id: crypto.randomUUID(),
1019
+ file,
1020
+ previewUrl: URL.createObjectURL(file)
1021
+ };
1022
+ });
1023
+ }
1024
+ /** Revoke one draft image's preview URL. */
1025
+ function releaseDraftImage(attachment) {
1026
+ URL.revokeObjectURL(attachment.previewUrl);
1027
+ }
1028
+ /** Serialize draft images to base64 prompt parts (mirror of main sendSession). */
1029
+ async function serializeImages(attachments) {
1030
+ return Promise.all(attachments.map(async (a) => {
1031
+ const bytes = new Uint8Array(await a.file.arrayBuffer());
1032
+ let binary = "";
1033
+ const chunk = 32768;
1034
+ for (let offset = 0; offset < bytes.length; offset += chunk) binary += String.fromCharCode(...bytes.subarray(offset, offset + chunk));
1035
+ return {
1036
+ type: "image",
1037
+ mediaType: a.file.type,
1038
+ data: btoa(binary),
1039
+ ...a.file.name === "" ? {} : { name: a.file.name }
1040
+ };
1041
+ }));
1042
+ }
1043
+ /** Convert a base64 string to an object URL for transcript image rendering. */
1044
+ function base64ObjectUrl(mediaType, data) {
1045
+ const binary = atob(data);
1046
+ const bytes = new Uint8Array(binary.length);
1047
+ for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
1048
+ return URL.createObjectURL(new Blob([bytes], { type: mediaType }));
1049
+ }
1050
+ /** Pull the side-chat list for one parent conversation. */
1051
+ async function refreshList(store, parentSessionId) {
1052
+ const result = await api.list({ parentSessionId });
1053
+ if (result.ok) store.patch({ items: result.value.items });
1054
+ }
1055
+ /** Optimistically flip one side-chat's running flag before the list round-trip. */
1056
+ function setItemRunning(store, childId, running) {
1057
+ const items = store.getSnapshot().panel.items;
1058
+ store.patch({ items: items.map((i) => i.childId === childId ? {
1059
+ ...i,
1060
+ running
1061
+ } : i) });
1062
+ }
1063
+ /** Pull the active side chat's transcript. */
1064
+ async function refreshHistory(store, childId) {
1065
+ const result = await api.history({ childId });
1066
+ if (result.ok) store.patch({ messages: result.value.messages });
1067
+ }
1068
+ /** Pull the model directory + permission catalog once. */
1069
+ async function refreshDirectory(store) {
1070
+ const [directoryResult, permissionsResult] = await Promise.all([api.directory(), api.permissions()]);
1071
+ const patch = {};
1072
+ if (directoryResult.ok) patch.directory = directoryResult.value;
1073
+ if (permissionsResult.ok) {
1074
+ patch.permissions = permissionsResult.value;
1075
+ if (store.getSnapshot().panel.preset === "") patch.preset = permissionsResult.value.current;
1076
+ }
1077
+ store.patch(patch);
1078
+ }
1079
+ /** The first non-empty line of a reasoning block (collapsed summary). */
1080
+ function firstLine(text) {
1081
+ const end = text.indexOf("\n");
1082
+ return end === -1 ? text : text.slice(0, end);
1083
+ }
1084
+ /** Main-conversation-style reasoning disclosure row (Think). */
1085
+ function ReasoningRow(props) {
1086
+ const [expanded, setExpanded] = (0, react.useState)(false);
1087
+ const summary = firstLine(props.text);
1088
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.DisclosureRow, {
1089
+ icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconThinkOutline14, { size: 14 }),
1090
+ title: props.t("panel.think"),
1091
+ open: expanded,
1092
+ expandable: true,
1093
+ expandOnRowClick: true,
1094
+ onToggle: () => {
1095
+ setExpanded((value) => !value);
1096
+ },
1097
+ collapsedContent: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1098
+ className: client_module_css_default.reasoningSummary,
1099
+ children: summary
1100
+ }),
1101
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1102
+ className: client_module_css_default.reasoningBody,
1103
+ children: props.text
1104
+ })
1105
+ });
1106
+ }
1107
+ /**
1108
+ * Main-conversation-style model selector: a compact trigger showing
1109
+ * `model · effort`, opening a two-level menu (provider groups → models, then
1110
+ * effort levels). UI mirrors dsh-client-ui-model-selection's ModelSelect.
1111
+ */
1112
+ function ModelSelect(props) {
1113
+ const { directory, selection, onSelect, t, openSignal, onOpenConsumed } = props;
1114
+ const [open, setOpen] = (0, react.useState)(false);
1115
+ const [pane, setPane] = (0, react.useState)("root");
1116
+ const rootRef = (0, react.useRef)(null);
1117
+ (0, react.useEffect)(() => {
1118
+ if (openSignal === true) {
1119
+ setPane("root");
1120
+ setOpen(true);
1121
+ onOpenConsumed?.();
1122
+ }
1123
+ }, [openSignal, onOpenConsumed]);
1124
+ let currentChoice;
1125
+ for (const group of directory?.groups ?? []) {
1126
+ const model = group.models.find((m) => m.id === selection.model && group.id === selection.provider);
1127
+ if (model !== void 0) {
1128
+ currentChoice = {
1129
+ name: model.name,
1130
+ reasoning: model.reasoning
1131
+ };
1132
+ break;
1133
+ }
1134
+ }
1135
+ const reasoning = currentChoice?.reasoning;
1136
+ const effectiveEffort = selection.effort !== "" ? selection.effort : reasoning?.defaultEffort;
1137
+ const effortLabel = reasoning === void 0 ? void 0 : effectiveEffort === void 0 ? t("panel.effortDefault") : reasoning.efforts.find((e) => e.id === effectiveEffort)?.name ?? effectiveEffort;
1138
+ const modelLabel = currentChoice?.name ?? t("panel.noModel");
1139
+ const effortChoices = reasoning === void 0 ? [] : [...reasoning.defaultEffort === void 0 ? [{
1140
+ key: "default",
1141
+ effort: void 0,
1142
+ label: t("panel.effortDefault")
1143
+ }] : [], ...reasoning.efforts.map((e) => ({
1144
+ key: e.id,
1145
+ effort: e.id,
1146
+ label: e.name
1147
+ }))];
1148
+ (0, react.useEffect)(() => {
1149
+ if (!open) return;
1150
+ const onDown = (e) => {
1151
+ if (rootRef.current !== null && !rootRef.current.contains(e.target)) setOpen(false);
1152
+ };
1153
+ const onKey = (e) => {
1154
+ if (e.key === "Escape") setOpen(false);
1155
+ };
1156
+ document.addEventListener("mousedown", onDown);
1157
+ document.addEventListener("keydown", onKey);
1158
+ return () => {
1159
+ document.removeEventListener("mousedown", onDown);
1160
+ document.removeEventListener("keydown", onKey);
1161
+ };
1162
+ }, [open]);
1163
+ const triggerLabel = effortLabel === void 0 ? modelLabel : `${modelLabel} · ${effortLabel}`;
1164
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1165
+ ref: rootRef,
1166
+ className: client_module_css_default.modelSelect,
1167
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1168
+ type: "button",
1169
+ className: client_module_css_default.modelSelectTrigger,
1170
+ "aria-haspopup": "menu",
1171
+ "aria-expanded": open,
1172
+ title: triggerLabel,
1173
+ onClick: () => {
1174
+ if (open) setOpen(false);
1175
+ else {
1176
+ setPane("root");
1177
+ setOpen(true);
1178
+ }
1179
+ },
1180
+ children: [
1181
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1182
+ className: client_module_css_default.modelSelectLabel,
1183
+ children: modelLabel
1184
+ }),
1185
+ effortLabel !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1186
+ className: client_module_css_default.modelSelectEffort,
1187
+ children: effortLabel
1188
+ }),
1189
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutline14, { className: open ? client_module_css_default.chevronOpen : void 0 })
1190
+ ]
1191
+ }), open && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1192
+ className: client_module_css_default.modelSelectMenu,
1193
+ role: "menu",
1194
+ children: [
1195
+ pane === "root" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1196
+ type: "button",
1197
+ role: "menuitem",
1198
+ className: client_module_css_default.modelCell,
1199
+ onClick: () => {
1200
+ setPane("model");
1201
+ },
1202
+ children: [
1203
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1204
+ className: client_module_css_default.modelCellLabel,
1205
+ children: t("panel.model")
1206
+ }),
1207
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1208
+ className: client_module_css_default.modelCellValue,
1209
+ children: modelLabel
1210
+ }),
1211
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronRightOutline14, { className: client_module_css_default.modelCellChevron })
1212
+ ]
1213
+ }), reasoning !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1214
+ type: "button",
1215
+ role: "menuitem",
1216
+ className: client_module_css_default.modelCell,
1217
+ onClick: () => {
1218
+ setPane("effort");
1219
+ },
1220
+ children: [
1221
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1222
+ className: client_module_css_default.modelCellLabel,
1223
+ children: t("panel.effort")
1224
+ }),
1225
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1226
+ className: client_module_css_default.modelCellValue,
1227
+ children: effortLabel
1228
+ }),
1229
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronRightOutline14, { className: client_module_css_default.modelCellChevron })
1230
+ ]
1231
+ })] }),
1232
+ pane === "model" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1233
+ className: client_module_css_default.modelGroups,
1234
+ children: [(directory?.groups ?? []).map((group) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
1235
+ role: "group",
1236
+ "aria-label": group.name,
1237
+ className: client_module_css_default.modelGroup,
1238
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1239
+ className: client_module_css_default.modelGroupTitle,
1240
+ children: group.name
1241
+ }), group.models.map((model) => {
1242
+ const selected = selection.provider === group.id && selection.model === model.id;
1243
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1244
+ type: "button",
1245
+ role: "menuitemradio",
1246
+ "aria-checked": selected,
1247
+ className: selected ? `${client_module_css_default.modelOption} ${client_module_css_default.modelOptionSelected}` : client_module_css_default.modelOption,
1248
+ title: model.name,
1249
+ onClick: () => {
1250
+ onSelect(group.id, model.id);
1251
+ setOpen(false);
1252
+ },
1253
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1254
+ className: client_module_css_default.modelOptionCopy,
1255
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1256
+ className: client_module_css_default.modelName,
1257
+ children: model.name
1258
+ }), model.description !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1259
+ className: client_module_css_default.modelDescription,
1260
+ children: model.description
1261
+ })]
1262
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1263
+ className: client_module_css_default.modelCheck,
1264
+ children: selected ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCheckOutline16, {}) : null
1265
+ })]
1266
+ }, model.id);
1267
+ })]
1268
+ }, group.id)), (directory?.groups ?? []).length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1269
+ className: client_module_css_default.modelEmpty,
1270
+ children: t("panel.noModel")
1271
+ })]
1272
+ }),
1273
+ pane === "effort" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, { children: effortChoices.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1274
+ className: client_module_css_default.modelEmpty,
1275
+ children: t("panel.effort")
1276
+ }) : effortChoices.map((level) => {
1277
+ const selected = effectiveEffort === level.effort;
1278
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1279
+ type: "button",
1280
+ role: "menuitemradio",
1281
+ "aria-checked": selected,
1282
+ className: selected ? `${client_module_css_default.modelOption} ${client_module_css_default.modelOptionSelected}` : client_module_css_default.modelOption,
1283
+ onClick: () => {
1284
+ onSelect(selection.provider, selection.model, level.effort);
1285
+ setOpen(false);
1286
+ },
1287
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1288
+ className: client_module_css_default.modelOptionCopy,
1289
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1290
+ className: client_module_css_default.modelName,
1291
+ children: level.label
1292
+ })
1293
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1294
+ className: client_module_css_default.modelCheck,
1295
+ children: selected ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCheckOutline16, {}) : null
1296
+ })]
1297
+ }, level.key);
1298
+ }) })
1299
+ ]
1300
+ })]
1301
+ });
1302
+ }
1303
+ /** Main-conversation-style permission selector (Menu + compact trigger). */
1304
+ function PermissionSelect(props) {
1305
+ const { permissions, preset, onSelect, openSignal, onOpenConsumed } = props;
1306
+ const [open, setOpen] = (0, react.useState)(false);
1307
+ const options = (permissions?.options ?? []).filter((o) => o.value !== "custom");
1308
+ const current = options.find((o) => o.value === preset);
1309
+ const items = options.map((o) => ({
1310
+ id: o.value,
1311
+ label: o.name
1312
+ }));
1313
+ (0, react.useEffect)(() => {
1314
+ if (openSignal === true) {
1315
+ setOpen(true);
1316
+ onOpenConsumed?.();
1317
+ }
1318
+ }, [openSignal, onOpenConsumed]);
1319
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Menu, {
1320
+ open,
1321
+ side: "top",
1322
+ align: "end",
1323
+ items,
1324
+ selectedId: preset,
1325
+ onSelect: (id) => {
1326
+ setOpen(false);
1327
+ onSelect(id);
1328
+ },
1329
+ onClose: () => {
1330
+ setOpen(false);
1331
+ },
1332
+ anchor: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1333
+ type: "button",
1334
+ className: client_module_css_default.modelSelectTrigger,
1335
+ "aria-haspopup": "menu",
1336
+ "aria-expanded": open,
1337
+ title: current?.description,
1338
+ onClick: () => {
1339
+ setOpen(!open);
1340
+ },
1341
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1342
+ className: client_module_css_default.modelSelectLabel,
1343
+ children: current?.name ?? preset
1344
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutline14, {})]
1345
+ })
1346
+ });
1347
+ }
1348
+ /**
1349
+ * The floating selection menu: listens to the document selection and shows
1350
+ * one or two buttons (start / continue), dispatching to the host API.
1351
+ */
1352
+ function SelectionMenu(props) {
1353
+ const { anchor, current, panel, prefs } = (0, react.useSyncExternalStore)(props.store.subscribe, props.store.getSnapshot);
1354
+ const [local, setLocal] = (0, react.useState)(null);
1355
+ (0, react.useEffect)(() => {
1356
+ const compute = () => {
1357
+ const selection = window.getSelection();
1358
+ if (selection === null || selection.isCollapsed) {
1359
+ setLocal(null);
1360
+ return;
1361
+ }
1362
+ const text = selection.toString().trim();
1363
+ if (text === "") {
1364
+ setLocal(null);
1365
+ return;
1366
+ }
1367
+ const range = selection.getRangeAt(0);
1368
+ const node = range.startContainer;
1369
+ const element = node.nodeType === 1 ? node : node.parentElement;
1370
+ if (element !== null && element.closest("input, textarea, [contenteditable=\"true\"]") !== null) {
1371
+ setLocal(null);
1372
+ return;
1373
+ }
1374
+ if (element !== null && element.closest("[data-dsh-side-chat]") !== null) {
1375
+ setLocal(null);
1376
+ return;
1377
+ }
1378
+ const rect = range.getBoundingClientRect();
1379
+ if (rect.width === 0 && rect.height === 0) {
1380
+ setLocal(null);
1381
+ return;
1382
+ }
1383
+ setLocal({
1384
+ text,
1385
+ x: rect.left + rect.width / 2,
1386
+ y: rect.top
1387
+ });
1388
+ };
1389
+ const onMouseUp = () => {
1390
+ window.setTimeout(compute, 0);
1391
+ };
1392
+ document.addEventListener("mouseup", onMouseUp);
1393
+ document.addEventListener("selectionchange", compute);
1394
+ return () => {
1395
+ document.removeEventListener("mouseup", onMouseUp);
1396
+ document.removeEventListener("selectionchange", compute);
1397
+ };
1398
+ }, []);
1399
+ const start = (0, react.useCallback)(() => {
1400
+ if (local === null || current === void 0) return;
1401
+ const parentSessionId = current;
1402
+ const text = local.text;
1403
+ const snap = props.store.getSnapshot().panel;
1404
+ if (prefs.sendImmediately) {
1405
+ const content = [{
1406
+ type: "text",
1407
+ text
1408
+ }, ...prefs.defaultPrompt.trim() !== "" ? [{
1409
+ type: "text",
1410
+ text: prefs.defaultPrompt.trim()
1411
+ }] : []];
1412
+ api.start({
1413
+ parentSessionId,
1414
+ content,
1415
+ lookupEnabled: prefs.lookupDefault,
1416
+ ...snap.provider !== "" ? { provider: snap.provider } : {},
1417
+ ...snap.model !== "" ? { model: snap.model } : {},
1418
+ ...snap.effort !== "" ? { reasoningEffort: snap.effort } : {}
1419
+ }).then((result) => {
1420
+ if (result.ok) {
1421
+ props.store.openPanel(parentSessionId);
1422
+ props.store.setActive(result.value.childId);
1423
+ props.store.patch({
1424
+ provider: result.value.provider,
1425
+ model: result.value.model,
1426
+ effort: result.value.reasoningEffort ?? ""
1427
+ });
1428
+ refreshList(props.store, parentSessionId);
1429
+ refreshDirectory(props.store);
1430
+ }
1431
+ });
1432
+ } else {
1433
+ props.store.openPanel(parentSessionId);
1434
+ props.store.patch({
1435
+ attachment: text,
1436
+ activeChildId: null,
1437
+ messages: [],
1438
+ draft: "",
1439
+ provider: "",
1440
+ model: "",
1441
+ effort: ""
1442
+ });
1443
+ api.inherit({ parentSessionId }).then((result) => {
1444
+ if (result.ok) props.store.patch({
1445
+ provider: result.value.provider,
1446
+ model: result.value.model,
1447
+ effort: result.value.reasoningEffort ?? ""
1448
+ });
1449
+ });
1450
+ refreshDirectory(props.store);
1451
+ }
1452
+ setLocal(null);
1453
+ }, [
1454
+ local,
1455
+ current,
1456
+ prefs,
1457
+ props.store
1458
+ ]);
1459
+ const continueChat = (0, react.useCallback)(() => {
1460
+ if (local === null || current === void 0) return;
1461
+ const parentSessionId = current;
1462
+ const text = local.text;
1463
+ const active = props.store.getSnapshot().panel.activeChildId;
1464
+ if (active === null) return;
1465
+ props.store.openPanel(parentSessionId);
1466
+ if (prefs.sendImmediately) {
1467
+ const content = [{
1468
+ type: "text",
1469
+ text
1470
+ }, ...prefs.defaultPrompt.trim() !== "" ? [{
1471
+ type: "text",
1472
+ text: prefs.defaultPrompt.trim()
1473
+ }] : []];
1474
+ setItemRunning(props.store, active, true);
1475
+ api.followup({
1476
+ childId: active,
1477
+ content,
1478
+ lookupEnabled: prefs.lookupDefault
1479
+ }).then((result) => {
1480
+ if (!result.ok) props.store.patch({ error: result.error.message });
1481
+ refreshList(props.store, parentSessionId);
1482
+ refreshHistory(props.store, active);
1483
+ });
1484
+ } else props.store.patch({ attachment: text });
1485
+ setLocal(null);
1486
+ }, [
1487
+ local,
1488
+ current,
1489
+ prefs,
1490
+ props.store
1491
+ ]);
1492
+ if (local === null || current === void 0) return null;
1493
+ const hasActive = panel.activeChildId !== null;
1494
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1495
+ className: client_module_css_default.selectionMenu,
1496
+ style: {
1497
+ left: local.x,
1498
+ top: local.y - 46
1499
+ },
1500
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1501
+ type: "button",
1502
+ className: client_module_css_default.selectionButton,
1503
+ onClick: start,
1504
+ children: props.t("ask.new")
1505
+ }), hasActive && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1506
+ type: "button",
1507
+ className: client_module_css_default.selectionButton,
1508
+ onClick: continueChat,
1509
+ children: props.t("ask.continue")
1510
+ })]
1511
+ });
1512
+ }
1513
+ /**
1514
+ * The floating bring-back-to-main menu: listens to the document selection and,
1515
+ * when the selection is inside an assistant reply in the side-chat panel, shows
1516
+ * two actions — "insert directly" and "summarize then insert" — both appending
1517
+ * into the main composer without sending.
1518
+ */
1519
+ function BringBackMenu(props) {
1520
+ const [local, setLocal] = (0, react.useState)(null);
1521
+ const [summarizing, setSummarizing] = (0, react.useState)(false);
1522
+ (0, react.useEffect)(() => {
1523
+ const compute = () => {
1524
+ const selection = window.getSelection();
1525
+ if (selection === null || selection.isCollapsed) {
1526
+ setLocal(null);
1527
+ return;
1528
+ }
1529
+ const text = selection.toString().trim();
1530
+ if (text === "") {
1531
+ setLocal(null);
1532
+ return;
1533
+ }
1534
+ const range = selection.getRangeAt(0);
1535
+ const node = range.startContainer;
1536
+ const element = node.nodeType === 1 ? node : node.parentElement;
1537
+ if (element !== null && element.closest("input, textarea, [contenteditable=\"true\"]") !== null) {
1538
+ setLocal(null);
1539
+ return;
1540
+ }
1541
+ if (element === null || element.closest("[data-sidechat-role=\"assistant\"]") === null) {
1542
+ setLocal(null);
1543
+ return;
1544
+ }
1545
+ const rect = range.getBoundingClientRect();
1546
+ if (rect.width === 0 && rect.height === 0) {
1547
+ setLocal(null);
1548
+ return;
1549
+ }
1550
+ setLocal({
1551
+ text,
1552
+ x: rect.left + rect.width / 2,
1553
+ y: rect.top
1554
+ });
1555
+ };
1556
+ const onMouseUp = () => {
1557
+ window.setTimeout(compute, 0);
1558
+ };
1559
+ document.addEventListener("mouseup", onMouseUp);
1560
+ document.addEventListener("selectionchange", compute);
1561
+ return () => {
1562
+ document.removeEventListener("mouseup", onMouseUp);
1563
+ document.removeEventListener("selectionchange", compute);
1564
+ };
1565
+ }, []);
1566
+ if (local === null) return null;
1567
+ const summarize = async () => {
1568
+ setSummarizing(true);
1569
+ const ok = await props.summarizeBring(local.text);
1570
+ setSummarizing(false);
1571
+ if (!ok) props.store.patch({ error: props.t("insert.summarizeFailed") });
1572
+ else setLocal(null);
1573
+ };
1574
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1575
+ className: client_module_css_default.selectionMenu,
1576
+ style: {
1577
+ left: local.x,
1578
+ top: local.y - 46
1579
+ },
1580
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1581
+ type: "button",
1582
+ className: client_module_css_default.selectionButton,
1583
+ onClick: () => {
1584
+ props.bringToMain(local.text).then((ok) => {
1585
+ if (!ok) props.store.patch({ error: props.t("insert.failed") });
1586
+ else setLocal(null);
1587
+ });
1588
+ },
1589
+ children: props.t("insert.direct")
1590
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1591
+ type: "button",
1592
+ className: client_module_css_default.selectionButton,
1593
+ disabled: summarizing,
1594
+ onClick: () => {
1595
+ summarize();
1596
+ },
1597
+ children: summarizing ? props.t("insert.summarizing") : props.t("insert.summarize")
1598
+ })]
1599
+ });
1600
+ }
1601
+ /**
1602
+ * Floating entry shown while the panel is closed and the main conversation has
1603
+ * a pending question dialog. It anchors beside the dialog's header (without
1604
+ * covering its text) and disappears once clicked (the panel opens instead).
1605
+ */
1606
+ function QuestionFab(props) {
1607
+ const [pos, setPos] = (0, react.useState)(null);
1608
+ (0, react.useEffect)(() => {
1609
+ let raf = 0;
1610
+ let missing = 0;
1611
+ const tick = () => {
1612
+ const el = document.querySelector("[data-question-key], [data-approval-key]");
1613
+ if (el === null) {
1614
+ missing += 1;
1615
+ if (missing > 30) {
1616
+ props.store.setMainQuestion(null);
1617
+ return;
1618
+ }
1619
+ setPos(null);
1620
+ raf = requestAnimationFrame(tick);
1621
+ return;
1622
+ }
1623
+ missing = 0;
1624
+ const rect = (el.querySelector("header") ?? el.firstElementChild ?? el).getBoundingClientRect();
1625
+ const left = Math.min(rect.right + 8, window.innerWidth - 32 - 8);
1626
+ const top = rect.top + rect.height / 2;
1627
+ setPos({
1628
+ left,
1629
+ top
1630
+ });
1631
+ raf = requestAnimationFrame(tick);
1632
+ };
1633
+ tick();
1634
+ return () => {
1635
+ cancelAnimationFrame(raf);
1636
+ };
1637
+ }, [props.store]);
1638
+ const style = pos !== null ? {
1639
+ left: pos.left,
1640
+ top: pos.top,
1641
+ transform: "translateY(-50%)"
1642
+ } : {
1643
+ left: "50%",
1644
+ bottom: 160,
1645
+ transform: "translateX(-50%)"
1646
+ };
1647
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
1648
+ label: props.t("question.openHint"),
1649
+ side: "top",
1650
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1651
+ type: "button",
1652
+ className: client_module_css_default.questionFab,
1653
+ style,
1654
+ "aria-label": props.t("question.openHint"),
1655
+ onClick: props.onOpen,
1656
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconPanelLeftOutline16, { size: 16 }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: client_module_css_default.questionFabDot })]
1657
+ })
1658
+ });
1659
+ }
1660
+ /** Panel width bounds. The panel never takes more than ~40% of the window and
1661
+ * never squeezes the main chat below a usable minimum — so the panel adapts to
1662
+ * whatever resolution / zoom the browser window is at. */
1663
+ const PANEL_MIN_WIDTH = 280;
1664
+ const PANEL_MAX_WIDTH = 720;
1665
+ const PANEL_DEFAULT_WIDTH = 360;
1666
+ const MAIN_CHAT_MIN_WIDTH = 480;
1667
+ /** localStorage key remembering the last panel width across reloads. */
1668
+ const PANEL_WIDTH_KEY = "dsh-side-chat.panelWidth";
1669
+ /** The viewport-aware maximum panel width for the current window. */
1670
+ function panelCap() {
1671
+ const vw = window.innerWidth;
1672
+ return Math.max(PANEL_MIN_WIDTH, Math.min(PANEL_MAX_WIDTH, vw * .4, vw - MAIN_CHAT_MIN_WIDTH));
1673
+ }
1674
+ /** The last user-chosen width, if any (re-clamped to the viewport on load). */
1675
+ function savedPanelWidth() {
1676
+ try {
1677
+ const raw = window.localStorage.getItem(PANEL_WIDTH_KEY);
1678
+ if (raw === null) return null;
1679
+ const n = Number(raw);
1680
+ return Number.isFinite(n) && n > 0 ? n : null;
1681
+ } catch {
1682
+ return null;
1683
+ }
1684
+ }
1685
+ /** The side-chat panel body. */
1686
+ function SidechatPanel(props) {
1687
+ const { panel, mainQuestion, dismissedQuestionIds } = (0, react.useSyncExternalStore)(props.store.subscribe, props.store.getSnapshot);
1688
+ const scrollRef = (0, react.useRef)(null);
1689
+ const markdownLabels = (0, react.useMemo)(() => ({
1690
+ code: {
1691
+ copyLabel: props.t("panel.copy"),
1692
+ copiedLabel: props.t("panel.copied")
1693
+ },
1694
+ footnotes: props.t("panel.footnotes")
1695
+ }), [props.t]);
1696
+ const attachmentRailLabels = (0, react.useMemo)(() => ({
1697
+ group: props.t("image.railGroup"),
1698
+ open: props.t("image.railOpen"),
1699
+ scrollLeft: props.t("image.railScrollLeft"),
1700
+ scrollRight: props.t("image.railScrollRight")
1701
+ }), [props.t]);
1702
+ const messageImageLabels = (0, react.useMemo)(() => ({
1703
+ image: props.t("image.label"),
1704
+ open: props.t("image.open"),
1705
+ openNamed: (name) => name,
1706
+ loading: props.t("image.loading"),
1707
+ loadFailed: props.t("image.loadFailed"),
1708
+ lightbox: {
1709
+ dialog: props.t("image.lightboxDialog"),
1710
+ close: props.t("image.close")
1711
+ }
1712
+ }), [props.t]);
1713
+ const dropOverlayLabels = (0, react.useMemo)(() => ({
1714
+ title: props.t("image.dropTitle"),
1715
+ desc: props.t("image.dropDesc")
1716
+ }), [props.t]);
1717
+ const [width, setWidth] = (0, react.useState)(() => {
1718
+ const base = savedPanelWidth() ?? PANEL_DEFAULT_WIDTH;
1719
+ return Math.max(PANEL_MIN_WIDTH, Math.min(panelCap(), base));
1720
+ });
1721
+ const [collapsed, setCollapsed] = (0, react.useState)(false);
1722
+ const [now, setNow] = (0, react.useState)(() => Date.now());
1723
+ const [dragActive, setDragActive] = (0, react.useState)(false);
1724
+ const [lightbox, setLightbox] = (0, react.useState)(null);
1725
+ const [limits, setLimits] = (0, react.useState)(null);
1726
+ /** Index of the assistant message whose "summarize then insert" is in flight. */
1727
+ const [summarizingIndex, setSummarizingIndex] = (0, react.useState)(null);
1728
+ /** Which question-dialog item is being brought into the side chat ('all' or an option label). */
1729
+ const [bringingKey, setBringingKey] = (0, react.useState)(null);
1730
+ /** Whether the question-dialog list is collapsed (headers only). */
1731
+ const [questionCollapsed, setQuestionCollapsed] = (0, react.useState)(false);
1732
+ (0, react.useEffect)(() => {
1733
+ if (panel.open && panel.activeChildId !== null) setCollapsed(false);
1734
+ }, [panel.open, panel.activeChildId]);
1735
+ const openQuestionPanel = (0, react.useCallback)(() => {
1736
+ props.store.openPanel(panel.parentSessionId);
1737
+ setCollapsed(false);
1738
+ }, [props.store, panel.parentSessionId]);
1739
+ /** Assemble one question + all its options into a prompt. */
1740
+ const buildAllText = (q) => {
1741
+ const lines = [];
1742
+ if (q.header !== void 0 && q.header !== "") lines.push(`【${q.header}】`);
1743
+ lines.push(q.question);
1744
+ if (q.detail !== void 0 && q.detail !== "") lines.push(q.detail);
1745
+ const options = q.options ?? [];
1746
+ if (options.length > 0) {
1747
+ lines.push(props.t("question.options"));
1748
+ for (const o of options) lines.push(`- ${o.label}${o.description !== void 0 && o.description !== "" ? ` — ${o.description}` : ""}`);
1749
+ }
1750
+ lines.push(props.t("question.allPrompt"));
1751
+ return lines.join("\n");
1752
+ };
1753
+ /** Assemble one question + one specific option into a prompt. */
1754
+ const buildOneText = (q, o) => {
1755
+ const lines = [];
1756
+ if (q.header !== void 0 && q.header !== "") lines.push(`【${q.header}】`);
1757
+ lines.push(q.question);
1758
+ if (q.detail !== void 0 && q.detail !== "") lines.push(q.detail);
1759
+ lines.push(`${props.t("question.option")}:${o.label}${o.description !== void 0 && o.description !== "" ? ` — ${o.description}` : ""}`);
1760
+ lines.push(props.t("question.onePrompt"));
1761
+ return lines.join("\n");
1762
+ };
1763
+ const bringQuestionText = (text, key, useNew) => {
1764
+ setBringingKey(key);
1765
+ (useNew ? props.askSidechatNew : props.askSidechat)(text).then((ok) => {
1766
+ setBringingKey(null);
1767
+ if (!ok) props.store.patch({ error: props.t("question.failed") });
1768
+ });
1769
+ };
1770
+ const activeItem = panel.items.find((i) => i.childId === panel.activeChildId);
1771
+ const activeRunning = activeItem?.running ?? false;
1772
+ const [anchor, setAnchor] = (0, react.useState)(null);
1773
+ (0, react.useEffect)(() => {
1774
+ const w = panel.open && !collapsed ? `${width}px` : "0px";
1775
+ document.documentElement.style.setProperty("--dsh-subchat-width", w);
1776
+ return () => {
1777
+ document.documentElement.style.setProperty("--dsh-subchat-width", "0px");
1778
+ };
1779
+ }, [
1780
+ panel.open,
1781
+ collapsed,
1782
+ width
1783
+ ]);
1784
+ (0, react.useEffect)(() => {
1785
+ const onResize = () => {
1786
+ setWidth((w) => Math.min(w, panelCap()));
1787
+ };
1788
+ window.addEventListener("resize", onResize);
1789
+ return () => {
1790
+ window.removeEventListener("resize", onResize);
1791
+ };
1792
+ }, []);
1793
+ (0, react.useEffect)(() => {
1794
+ try {
1795
+ window.localStorage.setItem(PANEL_WIDTH_KEY, String(width));
1796
+ } catch {}
1797
+ }, [width]);
1798
+ (0, react.useEffect)(() => {
1799
+ if (panel.open && panel.directory === null) refreshDirectory(props.store);
1800
+ }, [
1801
+ panel.open,
1802
+ panel.directory,
1803
+ props.store
1804
+ ]);
1805
+ (0, react.useEffect)(() => {
1806
+ if (activeRunning) {
1807
+ if (anchor === null) setAnchor(activeItem?.runningSince ?? Date.now());
1808
+ } else if (anchor !== null) setAnchor(null);
1809
+ }, [
1810
+ activeRunning,
1811
+ activeItem?.runningSince,
1812
+ anchor
1813
+ ]);
1814
+ (0, react.useEffect)(() => {
1815
+ if (!activeRunning) return;
1816
+ const id = window.setInterval(() => {
1817
+ setNow(Date.now());
1818
+ }, 1e3);
1819
+ return () => {
1820
+ window.clearInterval(id);
1821
+ };
1822
+ }, [activeRunning]);
1823
+ (0, react.useEffect)(() => {
1824
+ const el = scrollRef.current;
1825
+ if (el !== null) el.scrollTop = el.scrollHeight;
1826
+ }, [panel.messages.length, panel.activeChildId]);
1827
+ (0, react.useEffect)(() => {
1828
+ if (!panel.open || panel.activeChildId === null) return;
1829
+ const tick = () => {
1830
+ const snap = props.store.getSnapshot().panel;
1831
+ const childId = snap.activeChildId;
1832
+ if (childId === null) return;
1833
+ refreshList(props.store, snap.parentSessionId);
1834
+ refreshHistory(props.store, childId);
1835
+ };
1836
+ tick();
1837
+ const id = window.setInterval(tick, 1200);
1838
+ return () => {
1839
+ window.clearInterval(id);
1840
+ };
1841
+ }, [
1842
+ panel.open,
1843
+ panel.activeChildId,
1844
+ props.store
1845
+ ]);
1846
+ const send = (0, react.useCallback)(() => {
1847
+ const draft = panel.draft.trim();
1848
+ const attachment = panel.attachment === null ? "" : panel.attachment;
1849
+ const text = attachment === "" ? draft : draft === "" ? attachment : `${attachment}\n\n${draft}`;
1850
+ serializeImages(panel.attachments).then((imageParts) => {
1851
+ const content = [...imageParts, ...text === "" ? [] : [{
1852
+ type: "text",
1853
+ text
1854
+ }]];
1855
+ if (content.length === 0) return;
1856
+ const toRelease = panel.attachments;
1857
+ props.store.patch({
1858
+ draft: "",
1859
+ attachment: null,
1860
+ attachments: []
1861
+ });
1862
+ toRelease.forEach(releaseDraftImage);
1863
+ if (panel.activeChildId === null) {
1864
+ api.start({
1865
+ parentSessionId: panel.parentSessionId,
1866
+ content,
1867
+ lookupEnabled: panel.lookup,
1868
+ ...panel.provider !== "" ? { provider: panel.provider } : {},
1869
+ ...panel.model !== "" ? { model: panel.model } : {},
1870
+ ...panel.effort !== "" ? { reasoningEffort: panel.effort } : {},
1871
+ ...panel.preset !== "" ? { preset: panel.preset } : {}
1872
+ }).then((result) => {
1873
+ if (result.ok) {
1874
+ props.store.setActive(result.value.childId);
1875
+ props.store.patch({
1876
+ provider: result.value.provider,
1877
+ model: result.value.model,
1878
+ effort: result.value.reasoningEffort ?? ""
1879
+ });
1880
+ refreshList(props.store, panel.parentSessionId);
1881
+ refreshDirectory(props.store);
1882
+ } else props.store.patch({ error: result.error.message });
1883
+ });
1884
+ return;
1885
+ }
1886
+ const childId = panel.activeChildId;
1887
+ setItemRunning(props.store, childId, true);
1888
+ api.followup({
1889
+ childId,
1890
+ content,
1891
+ lookupEnabled: panel.lookup
1892
+ }).then((result) => {
1893
+ if (!result.ok) {
1894
+ setItemRunning(props.store, childId, false);
1895
+ props.store.patch({ error: result.error.message });
1896
+ }
1897
+ refreshList(props.store, panel.parentSessionId);
1898
+ refreshHistory(props.store, childId);
1899
+ });
1900
+ }).catch((error) => {
1901
+ props.store.patch({ error: error instanceof Error ? error.message : String(error) });
1902
+ });
1903
+ }, [panel, props.store]);
1904
+ const stop = (0, react.useCallback)(() => {
1905
+ if (panel.activeChildId === null) return;
1906
+ const childId = panel.activeChildId;
1907
+ setItemRunning(props.store, childId, false);
1908
+ api.stop({ childId }).then(() => {
1909
+ refreshList(props.store, panel.parentSessionId);
1910
+ refreshHistory(props.store, childId);
1911
+ });
1912
+ }, [panel, props.store]);
1913
+ const onModelSelect = (0, react.useCallback)((provider, model, effort) => {
1914
+ props.store.patch({
1915
+ provider,
1916
+ model,
1917
+ effort: effort ?? ""
1918
+ });
1919
+ if (panel.activeChildId !== null) api.selectModel({
1920
+ childId: panel.activeChildId,
1921
+ provider,
1922
+ model,
1923
+ ...effort === void 0 ? {} : { reasoningEffort: effort }
1924
+ });
1925
+ }, [panel.activeChildId, props.store]);
1926
+ const onPresetChange = (0, react.useCallback)((value) => {
1927
+ if (value === "custom" || value === "") return;
1928
+ props.store.patch({ preset: value });
1929
+ if (panel.activeChildId !== null) api.selectPermission({
1930
+ childId: panel.activeChildId,
1931
+ presetName: value
1932
+ }).then((result) => {
1933
+ if (!result.ok) props.store.patch({ error: result.error.message });
1934
+ });
1935
+ }, [panel.activeChildId, props.store]);
1936
+ const dispose = (0, react.useCallback)(() => {
1937
+ if (panel.activeChildId === null) return;
1938
+ const childId = panel.activeChildId;
1939
+ api.dispose({ childId }).then(() => {
1940
+ refreshList(props.store, panel.parentSessionId);
1941
+ props.store.patch({
1942
+ activeChildId: null,
1943
+ messages: []
1944
+ });
1945
+ });
1946
+ }, [panel, props.store]);
1947
+ /** Delete one side chat from the list. */
1948
+ const disposeItem = (0, react.useCallback)((childId) => {
1949
+ api.dispose({ childId }).then(() => {
1950
+ refreshList(props.store, panel.parentSessionId);
1951
+ if (panel.activeChildId === childId) props.store.patch({
1952
+ activeChildId: null,
1953
+ messages: []
1954
+ });
1955
+ });
1956
+ }, [
1957
+ panel.activeChildId,
1958
+ panel.parentSessionId,
1959
+ props.store
1960
+ ]);
1961
+ /** Delete every side chat of this conversation at once. */
1962
+ const disposeAll = (0, react.useCallback)(() => {
1963
+ const ids = panel.items.map((i) => i.childId);
1964
+ if (ids.length === 0) return;
1965
+ Promise.all(ids.map((id) => api.dispose({ childId: id }))).then(() => {
1966
+ refreshList(props.store, panel.parentSessionId);
1967
+ props.store.patch({
1968
+ activeChildId: null,
1969
+ messages: []
1970
+ });
1971
+ });
1972
+ }, [
1973
+ panel.items,
1974
+ panel.parentSessionId,
1975
+ props.store
1976
+ ]);
1977
+ const startResize = (0, react.useCallback)((e) => {
1978
+ e.preventDefault();
1979
+ const startX = e.clientX;
1980
+ const startWidth = width;
1981
+ const onMove = (ev) => {
1982
+ setWidth(Math.max(PANEL_MIN_WIDTH, Math.min(panelCap(), startWidth + (startX - ev.clientX))));
1983
+ };
1984
+ const onUp = () => {
1985
+ window.removeEventListener("mousemove", onMove);
1986
+ window.removeEventListener("mouseup", onUp);
1987
+ };
1988
+ window.addEventListener("mousemove", onMove);
1989
+ window.addEventListener("mouseup", onUp);
1990
+ }, [width]);
1991
+ const intakeImages = (0, react.useCallback)((files) => {
1992
+ if (files.length === 0) return;
1993
+ const images = files.filter((f) => f.type.startsWith("image/"));
1994
+ if (images.length === 0) {
1995
+ props.store.patch({ error: props.t("image.unsupported") });
1996
+ return;
1997
+ }
1998
+ try {
1999
+ if (limits !== null) {
2000
+ if (images.some((f) => !limits.mediaTypes.includes(f.type))) {
2001
+ props.store.patch({ error: props.t("image.unsupported") });
2002
+ return;
2003
+ }
2004
+ if (panel.attachments.length + images.length > limits.maxImagesPerMessage) {
2005
+ props.store.patch({ error: props.t("image.tooMany") });
2006
+ return;
2007
+ }
2008
+ if (images.some((f) => f.size > limits.maxImageBytes)) {
2009
+ props.store.patch({ error: props.t("image.fileTooLarge") });
2010
+ return;
2011
+ }
2012
+ }
2013
+ const created = createDraftImages(images);
2014
+ props.store.patch({
2015
+ attachments: [...panel.attachments, ...created],
2016
+ error: null
2017
+ });
2018
+ } catch (error) {
2019
+ props.store.patch({ error: error instanceof Error ? error.message : String(error) });
2020
+ }
2021
+ }, [
2022
+ limits,
2023
+ panel.attachments,
2024
+ props.store,
2025
+ props.t
2026
+ ]);
2027
+ (0, react.useEffect)(() => {
2028
+ api.limits().then((result) => {
2029
+ if (result.ok) setLimits(result.value);
2030
+ });
2031
+ }, []);
2032
+ (0, react.useEffect)(() => {
2033
+ if (!panel.open || collapsed) return;
2034
+ const dragDepth = { value: 0 };
2035
+ const hasFiles = (event) => event.dataTransfer?.types.includes("Files") ?? false;
2036
+ const reset = () => {
2037
+ dragDepth.value = 0;
2038
+ setDragActive(false);
2039
+ };
2040
+ const onDragEnter = (event) => {
2041
+ if (!hasFiles(event)) return;
2042
+ event.preventDefault();
2043
+ dragDepth.value += 1;
2044
+ setDragActive(true);
2045
+ };
2046
+ const onDragOver = (event) => {
2047
+ if (!hasFiles(event) || event.dataTransfer === null) return;
2048
+ event.preventDefault();
2049
+ event.dataTransfer.dropEffect = "copy";
2050
+ };
2051
+ const onDragLeave = (event) => {
2052
+ if (!hasFiles(event)) return;
2053
+ dragDepth.value = Math.max(0, dragDepth.value - 1);
2054
+ if (dragDepth.value === 0) setDragActive(false);
2055
+ };
2056
+ const onDrop = (event) => {
2057
+ if (!hasFiles(event)) return;
2058
+ event.preventDefault();
2059
+ reset();
2060
+ intakeImages([...event.dataTransfer?.files ?? []]);
2061
+ };
2062
+ document.addEventListener("dragenter", onDragEnter);
2063
+ document.addEventListener("dragover", onDragOver);
2064
+ document.addEventListener("dragleave", onDragLeave);
2065
+ document.addEventListener("drop", onDrop);
2066
+ window.addEventListener("dragend", reset);
2067
+ return () => {
2068
+ document.removeEventListener("dragenter", onDragEnter);
2069
+ document.removeEventListener("dragover", onDragOver);
2070
+ document.removeEventListener("dragleave", onDragLeave);
2071
+ document.removeEventListener("drop", onDrop);
2072
+ window.removeEventListener("dragend", reset);
2073
+ };
2074
+ }, [
2075
+ panel.open,
2076
+ collapsed,
2077
+ intakeImages
2078
+ ]);
2079
+ const onPaste = (0, react.useCallback)((e) => {
2080
+ const files = Array.from(e.clipboardData.items).filter((item) => item.kind === "file").map((item) => item.getAsFile()).filter((f) => f !== null);
2081
+ if (files.length === 0) return;
2082
+ e.preventDefault();
2083
+ intakeImages(files);
2084
+ }, [intakeImages]);
2085
+ const removeImage = (0, react.useCallback)((id) => {
2086
+ const target = panel.attachments.find((a) => a.id === id);
2087
+ if (target !== void 0) releaseDraftImage(target);
2088
+ props.store.patch({ attachments: panel.attachments.filter((a) => a.id !== id) });
2089
+ }, [panel.attachments, props.store]);
2090
+ const imageLoader = (0, react.useCallback)(async (ref) => {
2091
+ const result = await api.attachment({
2092
+ childId: panel.activeChildId ?? "",
2093
+ attachmentId: ref.attachmentId
2094
+ });
2095
+ if (!result.ok) throw new Error(result.error.message);
2096
+ return base64ObjectUrl(result.value.mediaType, result.value.data);
2097
+ }, [panel.activeChildId]);
2098
+ if (!panel.open) {
2099
+ if (mainQuestion !== null) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(QuestionFab, {
2100
+ store: props.store,
2101
+ t: props.t,
2102
+ onOpen: openQuestionPanel
2103
+ });
2104
+ return null;
2105
+ }
2106
+ if (collapsed) {
2107
+ if (mainQuestion !== null) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(QuestionFab, {
2108
+ store: props.store,
2109
+ t: props.t,
2110
+ onOpen: openQuestionPanel
2111
+ });
2112
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
2113
+ label: props.t("panel.expand"),
2114
+ side: "bottom",
2115
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2116
+ type: "button",
2117
+ className: client_module_css_default.collapsedHandle,
2118
+ onClick: () => {
2119
+ setCollapsed(false);
2120
+ },
2121
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconPanelLeftOutline16, { size: 16 })
2122
+ })
2123
+ });
2124
+ }
2125
+ const elapsedMs = anchor === null ? 0 : Math.max(0, now - anchor);
2126
+ const showClock = elapsedMs >= 15e3;
2127
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2128
+ className: client_module_css_default.panel,
2129
+ style: { width },
2130
+ children: [
2131
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2132
+ className: client_module_css_default.panelResize,
2133
+ onMouseDown: startResize
2134
+ }),
2135
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2136
+ className: client_module_css_default.panelHeader,
2137
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2138
+ className: client_module_css_default.panelTitle,
2139
+ children: props.t("panel.title")
2140
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2141
+ className: client_module_css_default.panelHeaderActions,
2142
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
2143
+ label: props.t("panel.collapse"),
2144
+ side: "bottom",
2145
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2146
+ type: "button",
2147
+ className: client_module_css_default.panelIconButton,
2148
+ onClick: () => {
2149
+ setCollapsed(true);
2150
+ },
2151
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconPanelLeftOutline16, { size: 16 })
2152
+ })
2153
+ })
2154
+ })]
2155
+ }),
2156
+ mainQuestion !== null && (() => {
2157
+ const visible = mainQuestion.filter((q) => !dismissedQuestionIds.includes(q.id));
2158
+ if (visible.length === 0) return null;
2159
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2160
+ className: client_module_css_default.questionBlock,
2161
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2162
+ className: client_module_css_default.questionBlockActions,
2163
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2164
+ type: "button",
2165
+ className: client_module_css_default.questionToggle,
2166
+ onClick: () => {
2167
+ setQuestionCollapsed(!questionCollapsed);
2168
+ },
2169
+ children: questionCollapsed ? props.t("question.expand") : props.t("question.collapse")
2170
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2171
+ type: "button",
2172
+ className: client_module_css_default.questionDeleteAll,
2173
+ onClick: () => {
2174
+ props.store.dismissAllQuestions(visible.map((q) => q.id));
2175
+ },
2176
+ children: props.t("question.deleteAll")
2177
+ })]
2178
+ }), visible.map((q) => {
2179
+ if (questionCollapsed) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2180
+ className: `${client_module_css_default.questionItem} ${client_module_css_default.questionItemCollapsed}`,
2181
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2182
+ className: client_module_css_default.questionHeaderText,
2183
+ children: q.header ?? q.question
2184
+ })
2185
+ }, q.id);
2186
+ const options = q.options ?? [];
2187
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2188
+ className: client_module_css_default.questionItem,
2189
+ children: [
2190
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2191
+ className: client_module_css_default.questionHeader,
2192
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2193
+ className: client_module_css_default.questionHeaderText,
2194
+ children: q.header ?? q.question
2195
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2196
+ className: client_module_css_default.questionHeaderActions,
2197
+ children: [
2198
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2199
+ type: "button",
2200
+ className: client_module_css_default.questionBringButton,
2201
+ disabled: bringingKey !== null,
2202
+ onClick: () => {
2203
+ bringQuestionText(buildAllText(q), `newall:${q.id}`, true);
2204
+ },
2205
+ children: bringingKey === `newall:${q.id}` ? props.t("question.bringing") : props.t("question.bringAllNew")
2206
+ }),
2207
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2208
+ type: "button",
2209
+ className: client_module_css_default.questionBringButton,
2210
+ disabled: bringingKey !== null,
2211
+ onClick: () => {
2212
+ bringQuestionText(buildAllText(q), `all:${q.id}`, false);
2213
+ },
2214
+ children: bringingKey === `all:${q.id}` ? props.t("question.bringing") : props.t("question.bringAll")
2215
+ }),
2216
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2217
+ type: "button",
2218
+ className: client_module_css_default.questionDelete,
2219
+ "aria-label": props.t("question.delete"),
2220
+ title: props.t("question.delete"),
2221
+ onClick: () => {
2222
+ props.store.dismissQuestion(q.id);
2223
+ },
2224
+ children: "×"
2225
+ })
2226
+ ]
2227
+ })]
2228
+ }),
2229
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2230
+ className: client_module_css_default.questionBody,
2231
+ children: q.question
2232
+ }),
2233
+ q.detail !== void 0 && q.detail !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2234
+ className: client_module_css_default.questionDetail,
2235
+ children: q.detail
2236
+ }),
2237
+ options.map((o) => {
2238
+ const key = `${q.id}:${o.label}`;
2239
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2240
+ className: client_module_css_default.questionOption,
2241
+ children: [
2242
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2243
+ className: client_module_css_default.questionOptionText,
2244
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2245
+ className: client_module_css_default.questionOptionLabel,
2246
+ children: o.label
2247
+ }), o.description !== void 0 && o.description !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2248
+ className: client_module_css_default.questionOptionDesc,
2249
+ children: [" — ", o.description]
2250
+ })]
2251
+ }),
2252
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2253
+ type: "button",
2254
+ className: client_module_css_default.questionBringButton,
2255
+ disabled: bringingKey !== null,
2256
+ onClick: () => {
2257
+ bringQuestionText(buildOneText(q, o), `new:${key}`, true);
2258
+ },
2259
+ children: bringingKey === `new:${key}` ? props.t("question.bringing") : props.t("question.bringOneNew")
2260
+ }),
2261
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2262
+ type: "button",
2263
+ className: client_module_css_default.questionBringButton,
2264
+ disabled: bringingKey !== null,
2265
+ onClick: () => {
2266
+ bringQuestionText(buildOneText(q, o), key, false);
2267
+ },
2268
+ children: bringingKey === key ? props.t("question.bringing") : props.t("question.bringOne")
2269
+ })
2270
+ ]
2271
+ }, o.label);
2272
+ })
2273
+ ]
2274
+ }, q.id);
2275
+ })]
2276
+ });
2277
+ })(),
2278
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2279
+ className: client_module_css_default.panelList,
2280
+ children: panel.items.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2281
+ className: client_module_css_default.panelEmpty,
2282
+ children: props.t("panel.empty")
2283
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2284
+ className: client_module_css_default.panelListActions,
2285
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2286
+ type: "button",
2287
+ className: client_module_css_default.panelListDeleteAll,
2288
+ onClick: disposeAll,
2289
+ children: props.t("panel.deleteAll")
2290
+ })
2291
+ }), panel.items.map((item) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2292
+ className: client_module_css_default.panelListItemRow,
2293
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2294
+ type: "button",
2295
+ className: `${client_module_css_default.panelListItem} ${item.childId === panel.activeChildId ? client_module_css_default.panelListItemActive : ""}`,
2296
+ onClick: () => {
2297
+ props.store.setActive(item.childId);
2298
+ refreshHistory(props.store, item.childId);
2299
+ },
2300
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2301
+ className: client_module_css_default.panelListItemDot,
2302
+ "data-running": item.running ? "1" : void 0
2303
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2304
+ className: client_module_css_default.panelListItemLabel,
2305
+ children: item.childId
2306
+ })]
2307
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2308
+ type: "button",
2309
+ className: client_module_css_default.panelListItemRemove,
2310
+ "aria-label": props.t("panel.delete"),
2311
+ title: props.t("panel.delete"),
2312
+ onClick: () => {
2313
+ disposeItem(item.childId);
2314
+ },
2315
+ children: "×"
2316
+ })]
2317
+ }, item.childId))] })
2318
+ }),
2319
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2320
+ className: client_module_css_default.panelTranscript,
2321
+ ref: scrollRef,
2322
+ children: [
2323
+ panel.messages.map((message, index) => {
2324
+ const textBlocks = message.blocks.filter((b) => b.type === "text");
2325
+ const imageBlocks = message.blocks.filter((b) => b.type === "image");
2326
+ const reasoningBlocks = message.blocks.filter((b) => b.type === "reasoning");
2327
+ const text = textBlocks.map((b) => b.type === "text" ? b.text : "").join("\n");
2328
+ const images = imageBlocks.map((b) => b.type === "image" ? { attachment: b.ref } : null).filter((x) => x !== null);
2329
+ if (message.role === "user") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2330
+ className: client_module_css_default.messageUser,
2331
+ children: [text !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2332
+ className: client_module_css_default.messageUserText,
2333
+ children: text
2334
+ }), images.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ImageGallery, {
2335
+ images: images.map((image) => image.attachment),
2336
+ load: imageLoader,
2337
+ align: "end",
2338
+ labels: messageImageLabels
2339
+ })]
2340
+ }, index);
2341
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2342
+ className: client_module_css_default.messageAssistant,
2343
+ "data-sidechat-role": "assistant",
2344
+ children: [
2345
+ reasoningBlocks.map((block, rIndex) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ReasoningRow, {
2346
+ text: block.type === "reasoning" ? block.text : "",
2347
+ t: props.t
2348
+ }, rIndex)),
2349
+ text !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.MarkdownText, {
2350
+ text,
2351
+ labels: markdownLabels
2352
+ }),
2353
+ text !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2354
+ className: client_module_css_default.messageActions,
2355
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2356
+ type: "button",
2357
+ className: client_module_css_default.messageInsertButton,
2358
+ onClick: () => {
2359
+ props.bringToMain(text).then((ok) => {
2360
+ if (!ok) props.store.patch({ error: props.t("insert.failed") });
2361
+ });
2362
+ },
2363
+ children: props.t("insert.direct")
2364
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2365
+ type: "button",
2366
+ className: client_module_css_default.messageInsertButton,
2367
+ disabled: summarizingIndex === index,
2368
+ onClick: () => {
2369
+ setSummarizingIndex(index);
2370
+ props.summarizeBring(text).then((ok) => {
2371
+ setSummarizingIndex(null);
2372
+ if (!ok) props.store.patch({ error: props.t("insert.summarizeFailed") });
2373
+ });
2374
+ },
2375
+ children: summarizingIndex === index ? props.t("insert.summarizing") : props.t("insert.summarize")
2376
+ })]
2377
+ })
2378
+ ]
2379
+ }, index);
2380
+ }),
2381
+ activeRunning && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2382
+ className: client_module_css_default.panelRunning,
2383
+ role: "status",
2384
+ "aria-live": "polite",
2385
+ children: [
2386
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: client_module_css_default.panelRunningDot }),
2387
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: props.t("panel.running") }),
2388
+ showClock && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2389
+ className: client_module_css_default.panelRunningClock,
2390
+ children: props.formatDuration(elapsedMs)
2391
+ })
2392
+ ]
2393
+ }),
2394
+ panel.error !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2395
+ className: client_module_css_default.panelError,
2396
+ children: [
2397
+ props.t("panel.error"),
2398
+ ": ",
2399
+ panel.error
2400
+ ]
2401
+ })
2402
+ ]
2403
+ }),
2404
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2405
+ className: client_module_css_default.panelComposer,
2406
+ children: [
2407
+ panel.attachment !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2408
+ className: client_module_css_default.panelAttachment,
2409
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2410
+ className: client_module_css_default.panelAttachmentText,
2411
+ children: panel.attachment
2412
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2413
+ type: "button",
2414
+ className: client_module_css_default.panelAttachmentRemove,
2415
+ onClick: () => {
2416
+ props.store.patch({ attachment: null });
2417
+ },
2418
+ children: "×"
2419
+ })]
2420
+ }),
2421
+ panel.attachments.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2422
+ className: client_module_css_default.panelAttachmentRail,
2423
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AttachmentRail, {
2424
+ items: panel.attachments.map((a) => ({
2425
+ id: a.id,
2426
+ previewUrl: a.previewUrl,
2427
+ alt: a.file.name || props.t("image.label"),
2428
+ removeLabel: props.t("image.remove")
2429
+ })),
2430
+ labels: attachmentRailLabels,
2431
+ onOpen: (item) => {
2432
+ const a = panel.attachments.find((x) => x.id === item.id);
2433
+ if (a !== void 0) setLightbox(a);
2434
+ },
2435
+ onRemove: (item) => {
2436
+ removeImage(item.id);
2437
+ }
2438
+ })
2439
+ }),
2440
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
2441
+ className: client_module_css_default.panelTextarea,
2442
+ placeholder: props.t("panel.input.placeholder"),
2443
+ value: panel.draft,
2444
+ onChange: (e) => {
2445
+ props.store.patch({ draft: e.target.value });
2446
+ },
2447
+ onPaste,
2448
+ onKeyDown: (e) => {
2449
+ if (e.key === "Enter" && !e.shiftKey) {
2450
+ e.preventDefault();
2451
+ send();
2452
+ }
2453
+ }
2454
+ }),
2455
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2456
+ className: client_module_css_default.panelToolbar,
2457
+ children: [
2458
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelSelect, {
2459
+ directory: panel.directory,
2460
+ selection: {
2461
+ provider: panel.provider,
2462
+ model: panel.model,
2463
+ effort: panel.effort
2464
+ },
2465
+ onSelect: onModelSelect,
2466
+ t: props.t
2467
+ }),
2468
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PermissionSelect, {
2469
+ permissions: panel.permissions,
2470
+ preset: panel.preset,
2471
+ onSelect: onPresetChange,
2472
+ t: props.t
2473
+ }),
2474
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
2475
+ label: activeRunning ? props.t("panel.stop") : props.t("panel.send"),
2476
+ side: "top",
2477
+ delayMs: 500,
2478
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2479
+ type: "button",
2480
+ className: client_module_css_default.primary,
2481
+ "aria-label": activeRunning ? props.t("panel.stop") : props.t("panel.send"),
2482
+ onClick: activeRunning ? stop : send,
2483
+ children: activeRunning ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconStopFill16, { size: 16 }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconSendOutline16, { size: 16 })
2484
+ })
2485
+ })
2486
+ ]
2487
+ }),
2488
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
2489
+ className: client_module_css_default.panelLookup,
2490
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2491
+ type: "checkbox",
2492
+ checked: panel.lookup,
2493
+ onChange: (e) => {
2494
+ props.store.patch({ lookup: e.target.checked });
2495
+ }
2496
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: props.t("panel.lookup") })]
2497
+ })
2498
+ ]
2499
+ }),
2500
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2501
+ className: client_module_css_default.panelFooter,
2502
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2503
+ type: "button",
2504
+ className: client_module_css_default.panelDispose,
2505
+ onClick: dispose,
2506
+ children: props.t("panel.dispose")
2507
+ })
2508
+ }),
2509
+ dragActive && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DropOverlay, {
2510
+ disabled: false,
2511
+ labels: dropOverlayLabels
2512
+ }),
2513
+ lightbox !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ImageLightbox, {
2514
+ src: lightbox.previewUrl,
2515
+ alt: lightbox.file.name || props.t("image.label"),
2516
+ labels: {
2517
+ dialog: props.t("image.lightboxDialog"),
2518
+ close: props.t("image.close")
2519
+ },
2520
+ onClose: () => {
2521
+ setLightbox(null);
2522
+ }
2523
+ })
2524
+ ]
2525
+ });
2526
+ }
2527
+ /** The "Side chat" settings section (two switches + a prompt textarea). */
2528
+ function SettingsSection(props) {
2529
+ const { store, t } = props;
2530
+ const { prefs } = (0, react.useSyncExternalStore)(store.subscribe, store.getSnapshot);
2531
+ const [promptDraft, setPromptDraft] = (0, react.useState)(prefs.defaultPrompt);
2532
+ (0, react.useEffect)(() => {
2533
+ setPromptDraft(prefs.defaultPrompt);
2534
+ }, [prefs.defaultPrompt]);
2535
+ const toggle = (0, react.useCallback)((patch) => {
2536
+ const previous = prefs;
2537
+ const next = {
2538
+ ...previous,
2539
+ ...patch
2540
+ };
2541
+ store.setPrefs(next);
2542
+ api.settingsUpdate(patch).then((result) => {
2543
+ if (!result.ok) store.setPrefs(previous);
2544
+ });
2545
+ }, [prefs, store]);
2546
+ const commitPrompt = (0, react.useCallback)(() => {
2547
+ const value = promptDraft.trim();
2548
+ if (value !== prefs.defaultPrompt) toggle({ defaultPrompt: value });
2549
+ }, [
2550
+ promptDraft,
2551
+ prefs.defaultPrompt,
2552
+ toggle
2553
+ ]);
2554
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2555
+ className: client_module_css_default.settingsSection,
2556
+ children: [
2557
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
2558
+ className: client_module_css_default.settingsRow,
2559
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2560
+ className: client_module_css_default.settingsRowText,
2561
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2562
+ className: client_module_css_default.settingsRowTitle,
2563
+ children: t("settings.lookupTitle")
2564
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2565
+ className: client_module_css_default.settingsRowDesc,
2566
+ children: t("settings.lookupDesc")
2567
+ })]
2568
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2569
+ type: "checkbox",
2570
+ className: client_module_css_default.settingsToggle,
2571
+ checked: prefs.lookupDefault,
2572
+ "aria-label": t("settings.lookupTitle"),
2573
+ onChange: (e) => {
2574
+ toggle({ lookupDefault: e.currentTarget.checked });
2575
+ }
2576
+ })]
2577
+ }),
2578
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
2579
+ className: client_module_css_default.settingsRow,
2580
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2581
+ className: client_module_css_default.settingsRowText,
2582
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2583
+ className: client_module_css_default.settingsRowTitle,
2584
+ children: t("settings.sendImmediatelyTitle")
2585
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2586
+ className: client_module_css_default.settingsRowDesc,
2587
+ children: t("settings.sendImmediatelyDesc")
2588
+ })]
2589
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2590
+ type: "checkbox",
2591
+ className: client_module_css_default.settingsToggle,
2592
+ checked: prefs.sendImmediately,
2593
+ "aria-label": t("settings.sendImmediatelyTitle"),
2594
+ onChange: (e) => {
2595
+ toggle({ sendImmediately: e.currentTarget.checked });
2596
+ }
2597
+ })]
2598
+ }),
2599
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2600
+ className: client_module_css_default.settingsRow,
2601
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2602
+ className: client_module_css_default.settingsRowText,
2603
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2604
+ className: client_module_css_default.settingsRowTitle,
2605
+ children: t("settings.bringModeTitle")
2606
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2607
+ className: client_module_css_default.settingsRowDesc,
2608
+ children: t("settings.bringModeDesc")
2609
+ })]
2610
+ })
2611
+ }),
2612
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2613
+ className: client_module_css_default.settingsBringMode,
2614
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
2615
+ className: `${client_module_css_default.settingsBringOption} ${prefs.bringMode === "draft" ? client_module_css_default.settingsBringOptionActive : ""}`,
2616
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2617
+ type: "radio",
2618
+ name: "dsh-side-chat-bring-mode",
2619
+ className: client_module_css_default.settingsToggle,
2620
+ checked: prefs.bringMode === "draft",
2621
+ onChange: () => {
2622
+ toggle({ bringMode: "draft" });
2623
+ }
2624
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2625
+ className: client_module_css_default.settingsRowText,
2626
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2627
+ className: client_module_css_default.settingsRowTitle,
2628
+ children: t("settings.bringModeDraftTitle")
2629
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2630
+ className: client_module_css_default.settingsRowDesc,
2631
+ children: t("settings.bringModeDraftDesc")
2632
+ })]
2633
+ })]
2634
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
2635
+ className: `${client_module_css_default.settingsBringOption} ${prefs.bringMode === "context" ? client_module_css_default.settingsBringOptionActive : ""}`,
2636
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2637
+ type: "radio",
2638
+ name: "dsh-side-chat-bring-mode",
2639
+ className: client_module_css_default.settingsToggle,
2640
+ checked: prefs.bringMode === "context",
2641
+ onChange: () => {
2642
+ toggle({ bringMode: "context" });
2643
+ }
2644
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2645
+ className: client_module_css_default.settingsRowText,
2646
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2647
+ className: client_module_css_default.settingsRowTitle,
2648
+ children: t("settings.bringModeContextTitle")
2649
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2650
+ className: client_module_css_default.settingsRowDesc,
2651
+ children: t("settings.bringModeContextDesc")
2652
+ })]
2653
+ })]
2654
+ })]
2655
+ }),
2656
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2657
+ className: client_module_css_default.settingsRow,
2658
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2659
+ className: client_module_css_default.settingsRowText,
2660
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2661
+ className: client_module_css_default.settingsRowTitle,
2662
+ children: t("settings.defaultPromptTitle")
2663
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2664
+ className: client_module_css_default.settingsRowDesc,
2665
+ children: t("settings.defaultPromptDesc")
2666
+ })]
2667
+ })
2668
+ }),
2669
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
2670
+ className: client_module_css_default.settingsPromptInput,
2671
+ value: promptDraft,
2672
+ placeholder: t("settings.defaultPromptPlaceholder"),
2673
+ "aria-label": t("settings.defaultPromptTitle"),
2674
+ onChange: (e) => {
2675
+ setPromptDraft(e.currentTarget.value);
2676
+ },
2677
+ onBlur: commitPrompt
2678
+ })
2679
+ ]
2680
+ });
2681
+ }
2682
+ /** Client plugin body. */
2683
+ function apply(ctx) {
2684
+ const store = createStore();
2685
+ let activeLocale = ctx.locale.getSnapshot().active;
2686
+ /** Append text to the main composer draft (draft bring mode). */
2687
+ const draftBring = (text) => {
2688
+ const trimmed = text.trim();
2689
+ if (trimmed === "") return false;
2690
+ const sessionId = ctx.sessions.list.getSnapshot().current;
2691
+ if (sessionId === void 0) return false;
2692
+ try {
2693
+ const actx = ctx.sessions.scope(sessionId);
2694
+ if (actx === void 0) return false;
2695
+ const input = ctx.conversation.input.for(actx);
2696
+ const draft = input.state.getSnapshot().draft;
2697
+ input.setDraft(draft === "" ? trimmed : `${draft}\n\n${trimmed}`);
2698
+ return true;
2699
+ } catch {
2700
+ return false;
2701
+ }
2702
+ };
2703
+ /** Inject text into the main conversation as a collapsed, source-tagged context row. */
2704
+ const injectBring = async (text, summary) => {
2705
+ const trimmed = text.trim();
2706
+ if (trimmed === "") return false;
2707
+ const sessionId = ctx.sessions.list.getSnapshot().current;
2708
+ if (sessionId === void 0) return false;
2709
+ return (await api.inject({
2710
+ parentSessionId: sessionId,
2711
+ text: trimmed,
2712
+ summary
2713
+ })).ok;
2714
+ };
2715
+ /** Land text in the main conversation per the configured bring mode. */
2716
+ const landText = async (text, summaryKey) => {
2717
+ if (store.getSnapshot().prefs.bringMode === "context") return injectBring(text, translate(activeLocale, summaryKey));
2718
+ return draftBring(text);
2719
+ };
2720
+ /** Bring a reply back directly (routed through the configured mode). */
2721
+ const bringToMain = (text) => landText(text, "insert.contextSummary");
2722
+ /** Summarize text with the side chat's inherited model, then bring the summary back. */
2723
+ const summarizeBring = async (text) => {
2724
+ const trimmed = text.trim();
2725
+ if (trimmed === "") return false;
2726
+ const snap = store.getSnapshot().panel;
2727
+ if (snap.parentSessionId === "") return false;
2728
+ const result = await api.summarize({
2729
+ parentSessionId: snap.parentSessionId,
2730
+ text: trimmed,
2731
+ ...snap.provider !== "" ? { provider: snap.provider } : {},
2732
+ ...snap.model !== "" ? { model: snap.model } : {},
2733
+ ...snap.effort !== "" ? { reasoningEffort: snap.effort } : {},
2734
+ locale: activeLocale
2735
+ });
2736
+ if (!result.ok) return false;
2737
+ return landText(result.value.summary, "insert.summarizeContextSummary");
2738
+ };
2739
+ /** Ask a piece of text in the side chat (start a new one, or continue the active one). */
2740
+ const askSidechat = async (text) => {
2741
+ const trimmed = text.trim();
2742
+ if (trimmed === "") return false;
2743
+ const parentSessionId = ctx.sessions.list.getSnapshot().current;
2744
+ if (parentSessionId === void 0) return false;
2745
+ const panel = store.getSnapshot().panel;
2746
+ const content = [{
2747
+ type: "text",
2748
+ text: trimmed
2749
+ }];
2750
+ const target = panel.activeChildId ?? panel.items[0]?.childId ?? null;
2751
+ if (target === null) {
2752
+ const result = await api.start({
2753
+ parentSessionId,
2754
+ content,
2755
+ lookupEnabled: panel.lookup,
2756
+ ...panel.provider !== "" ? { provider: panel.provider } : {},
2757
+ ...panel.model !== "" ? { model: panel.model } : {},
2758
+ ...panel.effort !== "" ? { reasoningEffort: panel.effort } : {}
2759
+ });
2760
+ if (result.ok) {
2761
+ store.openPanel(parentSessionId);
2762
+ store.setActive(result.value.childId);
2763
+ store.patch({
2764
+ provider: result.value.provider,
2765
+ model: result.value.model,
2766
+ effort: result.value.reasoningEffort ?? ""
2767
+ });
2768
+ refreshList(store, parentSessionId);
2769
+ refreshDirectory(store);
2770
+ return true;
2771
+ }
2772
+ return false;
2773
+ }
2774
+ const childId = target;
2775
+ if (childId !== panel.activeChildId) store.setActive(childId);
2776
+ setItemRunning(store, childId, true);
2777
+ const result = await api.followup({
2778
+ childId,
2779
+ content,
2780
+ lookupEnabled: panel.lookup
2781
+ });
2782
+ if (!result.ok) {
2783
+ setItemRunning(store, childId, false);
2784
+ store.patch({ error: result.error.message });
2785
+ }
2786
+ refreshList(store, parentSessionId);
2787
+ refreshHistory(store, childId);
2788
+ return result.ok;
2789
+ };
2790
+ /** Ask a piece of text in a brand-new side chat (never reuses an existing one). */
2791
+ const askSidechatNew = async (text) => {
2792
+ const trimmed = text.trim();
2793
+ if (trimmed === "") return false;
2794
+ const parentSessionId = ctx.sessions.list.getSnapshot().current;
2795
+ if (parentSessionId === void 0) return false;
2796
+ const panel = store.getSnapshot().panel;
2797
+ const content = [{
2798
+ type: "text",
2799
+ text: trimmed
2800
+ }];
2801
+ const result = await api.start({
2802
+ parentSessionId,
2803
+ content,
2804
+ lookupEnabled: panel.lookup,
2805
+ ...panel.provider !== "" ? { provider: panel.provider } : {},
2806
+ ...panel.model !== "" ? { model: panel.model } : {},
2807
+ ...panel.effort !== "" ? { reasoningEffort: panel.effort } : {}
2808
+ });
2809
+ if (result.ok) {
2810
+ store.openPanel(parentSessionId);
2811
+ store.setActive(result.value.childId);
2812
+ store.patch({
2813
+ provider: result.value.provider,
2814
+ model: result.value.model,
2815
+ effort: result.value.reasoningEffort ?? ""
2816
+ });
2817
+ refreshList(store, parentSessionId);
2818
+ refreshDirectory(store);
2819
+ return true;
2820
+ }
2821
+ return false;
2822
+ };
2823
+ ctx.effect(() => {
2824
+ const offZh = ctx.locale.register(LOCALE_NS, "zh", zh);
2825
+ const offEn = ctx.locale.register(LOCALE_NS, "en", en);
2826
+ const offSub = ctx.locale.subscribe(() => {
2827
+ activeLocale = ctx.locale.getSnapshot().active;
2828
+ store.patch({});
2829
+ });
2830
+ return () => {
2831
+ offZh();
2832
+ offEn();
2833
+ offSub();
2834
+ };
2835
+ }, "dsh-side-chat: dictionaries");
2836
+ api.settingsGet().then((result) => {
2837
+ if (!result.ok) return;
2838
+ const raw = result.value.value;
2839
+ if (raw === null || raw === void 0) return;
2840
+ store.setPrefs({
2841
+ lookupDefault: typeof raw.lookupDefault === "boolean" ? raw.lookupDefault : SUBCHAT_PREFS_DEFAULTS.lookupDefault,
2842
+ sendImmediately: typeof raw.sendImmediately === "boolean" ? raw.sendImmediately : SUBCHAT_PREFS_DEFAULTS.sendImmediately,
2843
+ defaultPrompt: typeof raw.defaultPrompt === "string" ? raw.defaultPrompt : SUBCHAT_PREFS_DEFAULTS.defaultPrompt,
2844
+ bringMode: raw.bringMode === "context" ? "context" : "draft"
2845
+ });
2846
+ });
2847
+ ctx.effect(() => {
2848
+ let lastId;
2849
+ const sync = () => {
2850
+ const next = ctx.sessions.list.getSnapshot().current;
2851
+ if (next === lastId) return;
2852
+ lastId = next;
2853
+ store.setCurrent(next);
2854
+ if (next === void 0) return;
2855
+ refreshList(store, next);
2856
+ const panel = store.getSnapshot().panel;
2857
+ if (panel.open && panel.activeChildId !== null) refreshHistory(store, panel.activeChildId);
2858
+ };
2859
+ sync();
2860
+ return ctx.sessions.list.subscribe(sync);
2861
+ }, "dsh-side-chat: follow current conversation");
2862
+ ctx.effect(() => {
2863
+ let lastQuestion = void 0;
2864
+ const read = () => {
2865
+ const sessionId = ctx.sessions.list.getSnapshot().current;
2866
+ if (sessionId === void 0) {
2867
+ store.setMainQuestion(null);
2868
+ return;
2869
+ }
2870
+ const interaction = ctx.uiSession.pendingInteractions.getSnapshot().get(sessionId);
2871
+ const question = interaction !== void 0 && (interaction.kind === "question" || interaction.kind === "plan-review") ? interaction : void 0;
2872
+ if (question === lastQuestion) return;
2873
+ lastQuestion = question;
2874
+ const questions = question?.questions ?? null;
2875
+ store.setMainQuestion(questions === null ? null : [...questions]);
2876
+ };
2877
+ read();
2878
+ const offPending = ctx.uiSession.pendingInteractions.subscribe(read);
2879
+ const offList = ctx.sessions.list.subscribe(read);
2880
+ return () => {
2881
+ offPending();
2882
+ offList();
2883
+ };
2884
+ }, "dsh-side-chat: track main question dialog");
2885
+ ctx.effect(() => {
2886
+ const timer = window.setInterval(() => {
2887
+ if (store.getSnapshot().mainQuestion === null) return;
2888
+ const sessionId = ctx.sessions.list.getSnapshot().current;
2889
+ if (sessionId === void 0) return;
2890
+ const interaction = ctx.uiSession.pendingInteractions.getSnapshot().get(sessionId);
2891
+ if (!(interaction !== void 0 && (interaction.kind === "question" || interaction.kind === "plan-review"))) store.setMainQuestion(null);
2892
+ }, 1200);
2893
+ return () => {
2894
+ window.clearInterval(timer);
2895
+ };
2896
+ }, "dsh-side-chat: clear stale question dialog");
2897
+ const settingsT = (key) => translate(activeLocale, key);
2898
+ ctx.slots.inject("settings.section", () => ctx.slots.register({
2899
+ name: "settings.section",
2900
+ id: "dsh-side-chat",
2901
+ order: 110,
2902
+ label: () => settingsT("settingsNav"),
2903
+ inject: () => ({
2904
+ store,
2905
+ t: settingsT
2906
+ })
2907
+ }, SettingsSection));
2908
+ ctx.effect(() => {
2909
+ const host = document.createElement("div");
2910
+ host.setAttribute("data-dsh-side-chat", "");
2911
+ document.body.appendChild(host);
2912
+ const root = (0, react_dom_client.createRoot)(host);
2913
+ const t = (key) => translate(activeLocale, key);
2914
+ const formatDuration = (ms) => formatRunDuration(ms, activeLocale);
2915
+ root.render(/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
2916
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SelectionMenu, {
2917
+ store,
2918
+ t
2919
+ }),
2920
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(BringBackMenu, {
2921
+ store,
2922
+ t,
2923
+ bringToMain,
2924
+ summarizeBring
2925
+ }),
2926
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SidechatPanel, {
2927
+ store,
2928
+ t,
2929
+ formatDuration,
2930
+ bringToMain,
2931
+ summarizeBring,
2932
+ askSidechat,
2933
+ askSidechatNew
2934
+ })
2935
+ ] }));
2936
+ return () => {
2937
+ root.unmount();
2938
+ host.remove();
2939
+ };
2940
+ }, "dsh-side-chat: panel mount");
2941
+ }
2942
+ //#endregion
2943
+ exports.apply = apply;
2944
+ exports.inject = inject;
2945
+ return module.exports;
2946
+ }
2947
+ });
2948
+
2949
+ //# sourceMappingURL=client.js.map