@jaychang1989/dsh-webchat 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/LICENSE +201 -0
  2. package/NOTICE +18 -0
  3. package/README.md +78 -0
  4. package/README.zh.md +83 -0
  5. package/cordis.patch.yml +22 -0
  6. package/lib/client.js +1725 -0
  7. package/lib/client.js.map +1 -0
  8. package/lib/index.js +3887 -0
  9. package/lib/types/client/api.d.ts +74 -0
  10. package/lib/types/client/controller.d.ts +19 -0
  11. package/lib/types/client/index.d.ts +30 -0
  12. package/lib/types/client/locales.d.ts +169 -0
  13. package/lib/types/client/mount.d.ts +23 -0
  14. package/lib/types/client/panel/Markdown.d.ts +15 -0
  15. package/lib/types/client/panel/WebChatPanel.d.ts +28 -0
  16. package/lib/types/client/sidebar-entry-core.d.ts +60 -0
  17. package/lib/types/client/sidebar-entry.d.ts +22 -0
  18. package/lib/types/engine/engine.d.ts +247 -0
  19. package/lib/types/engine/html-md.d.ts +23 -0
  20. package/lib/types/index.d.ts +63 -0
  21. package/lib/types/protocol.d.ts +107 -0
  22. package/lib/types/routes.d.ts +22 -0
  23. package/lib/types/store.d.ts +69 -0
  24. package/lib/types/tools.d.ts +27 -0
  25. package/lib/types/transfer.d.ts +138 -0
  26. package/package.json +96 -0
  27. package/src/client/api.ts +90 -0
  28. package/src/client/controller.ts +43 -0
  29. package/src/client/css-modules.d.ts +5 -0
  30. package/src/client/index.ts +75 -0
  31. package/src/client/locales.ts +172 -0
  32. package/src/client/mount.tsx +125 -0
  33. package/src/client/panel/Markdown.tsx +251 -0
  34. package/src/client/panel/WebChatPanel.tsx +760 -0
  35. package/src/client/panel/panel.module.css +936 -0
  36. package/src/client/sidebar-entry-core.ts +207 -0
  37. package/src/client/sidebar-entry.ts +49 -0
  38. package/src/engine/engine.ts +1385 -0
  39. package/src/engine/html-md.ts +126 -0
  40. package/src/index.ts +212 -0
  41. package/src/protocol.ts +116 -0
  42. package/src/routes.ts +307 -0
  43. package/src/store.ts +217 -0
  44. package/src/tools.ts +257 -0
  45. package/src/transfer.ts +602 -0
package/lib/client.js ADDED
@@ -0,0 +1,1725 @@
1
+ window.__ModuleLoader__.load({
2
+ id: "@jaychang1989/dsh-webchat",
3
+ factory: (require) => {
4
+ var module = { exports: {} };
5
+ var exports = module.exports;
6
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
+ let react_dom_client = require("react-dom/client");
8
+ let react = require("react");
9
+ let react_jsx_runtime = require("react/jsx-runtime");
10
+ //#region src/protocol.ts
11
+ /** API path constants shared by host routes and the browser panel. */
12
+ const WEBChat_API = {
13
+ state: "/api/dsh-webchat/state",
14
+ openLogin: "/api/dsh-webchat/open-login",
15
+ closeBrowser: "/api/dsh-webchat/close-browser",
16
+ newChat: "/api/dsh-webchat/new-chat",
17
+ send: "/api/dsh-webchat/send",
18
+ stop: "/api/dsh-webchat/stop",
19
+ deepThink: "/api/dsh-webchat/deep-think",
20
+ search: "/api/dsh-webchat/search",
21
+ transfer: "/api/dsh-webchat/transfer",
22
+ exportFile: "/api/dsh-webchat/export",
23
+ renameChat: "/api/dsh-webchat/rename",
24
+ deleteChat: "/api/dsh-webchat/delete",
25
+ clearChats: "/api/dsh-webchat/clear",
26
+ webChats: "/api/dsh-webchat/web-chats",
27
+ recover: "/api/dsh-webchat/recover"
28
+ };
29
+ //#endregion
30
+ //#region src/client/api.ts
31
+ /**
32
+ * Browser-side API client for the /api/dsh-webchat route family. The only
33
+ * data access path the panel components use — plain fetch, same origin.
34
+ */
35
+ async function request(path, body) {
36
+ const response = await fetch(path, {
37
+ method: body === void 0 ? "GET" : "POST",
38
+ headers: body === void 0 ? void 0 : { "content-type": "application/json" },
39
+ body: body === void 0 ? void 0 : JSON.stringify(body)
40
+ });
41
+ const payload = await response.json().catch(() => ({}));
42
+ if (!response.ok && payload.ok !== true) return {
43
+ ...payload,
44
+ ok: false,
45
+ error: payload.error ?? `HTTP ${response.status}`
46
+ };
47
+ return payload;
48
+ }
49
+ var WebChatApi = class {
50
+ state() {
51
+ return request(WEBChat_API.state);
52
+ }
53
+ openLogin() {
54
+ return request(WEBChat_API.openLogin);
55
+ }
56
+ closeBrowser() {
57
+ return request(WEBChat_API.closeBrowser);
58
+ }
59
+ newChat() {
60
+ return request(WEBChat_API.newChat);
61
+ }
62
+ send(text, images) {
63
+ return request(WEBChat_API.send, {
64
+ text,
65
+ images
66
+ });
67
+ }
68
+ stop() {
69
+ return request(WEBChat_API.stop);
70
+ }
71
+ setDeepThink(enabled) {
72
+ return request(WEBChat_API.deepThink, { enabled });
73
+ }
74
+ setSearch(enabled) {
75
+ return request(WEBChat_API.search, { enabled });
76
+ }
77
+ transfer(chatId, cwd, mode, workspaceId, targetSessionId) {
78
+ return request(WEBChat_API.transfer, {
79
+ chatId,
80
+ cwd,
81
+ mode,
82
+ workspaceId,
83
+ targetSessionId
84
+ });
85
+ }
86
+ exportFile(chatId, cwd) {
87
+ return request(WEBChat_API.exportFile, {
88
+ chatId,
89
+ cwd
90
+ });
91
+ }
92
+ renameChat(chatId, title) {
93
+ return request(WEBChat_API.renameChat, {
94
+ chatId,
95
+ title
96
+ });
97
+ }
98
+ deleteChat(chatId) {
99
+ return request(WEBChat_API.deleteChat, { chatId });
100
+ }
101
+ clearChats() {
102
+ return request(WEBChat_API.clearChats);
103
+ }
104
+ webChats() {
105
+ return request(WEBChat_API.webChats);
106
+ }
107
+ recover(title) {
108
+ return request(WEBChat_API.recover, { title });
109
+ }
110
+ };
111
+ //#endregion
112
+ //#region src/client/controller.ts
113
+ const INITIAL = { panelOpen: false };
114
+ /** Plain pub/sub store (no React dependency — used by the DOM sidebar entry). */
115
+ var PanelController = class {
116
+ snapshot = INITIAL;
117
+ listeners = /* @__PURE__ */ new Set();
118
+ getSnapshot() {
119
+ return this.snapshot;
120
+ }
121
+ subscribe(listener) {
122
+ this.listeners.add(listener);
123
+ return () => {
124
+ this.listeners.delete(listener);
125
+ };
126
+ }
127
+ set(patch) {
128
+ this.snapshot = {
129
+ ...this.snapshot,
130
+ ...patch
131
+ };
132
+ for (const listener of [...this.listeners]) listener();
133
+ }
134
+ open() {
135
+ if (!this.snapshot.panelOpen) this.set({ panelOpen: true });
136
+ }
137
+ close() {
138
+ if (this.snapshot.panelOpen) this.set({ panelOpen: false });
139
+ }
140
+ toggle() {
141
+ this.set({ panelOpen: !this.snapshot.panelOpen });
142
+ }
143
+ };
144
+ //#endregion
145
+ //#region src/client/locales.ts
146
+ /**
147
+ * dsh-webchat surface copy: zh is the key source, en mirrors every key. The
148
+ * key union (not an object shape) is what the locale machinery wants —
149
+ * LocaleKeysOf<N> intersects the namespace with `string`, so the namespace
150
+ * value must itself be the key union (same pattern as dsh-ssh).
151
+ */
152
+ const zh = {
153
+ "entry.label": "网页聊天",
154
+ "entry.tooltip": "DeepSeek 网页端聊天(deepseek-chat,Codex ChatGPT 模式)",
155
+ "panel.title": "DeepSeek 网页端聊天",
156
+ "status.engine.stopped": "浏览器未启动",
157
+ "status.engine.launching": "浏览器启动中…",
158
+ "status.engine.ready": "浏览器就绪",
159
+ "status.engine.error": "浏览器错误",
160
+ "status.loggedIn": "已登录",
161
+ "status.notLoggedIn": "未登录",
162
+ "status.unknown": "状态未知",
163
+ "action.openLogin": "打开登录窗口",
164
+ "action.closeBrowser": "关闭浏览器",
165
+ "action.newChat": "新对话",
166
+ "action.recover": "从网页恢复",
167
+ "action.refresh": "刷新",
168
+ "action.send": "发送",
169
+ "action.stop": "停止生成",
170
+ "action.transferToHarness": "转移到 Harness",
171
+ "action.exportFile": "导出为文件",
172
+ "transfer.mode.distill": "蒸馏为任务简报",
173
+ "transfer.mode.raw": "原始记录",
174
+ "transfer.mode.hint": "蒸馏:用 harness LLM 把对话提炼成可执行任务简报;原始记录:原样放入新会话首条消息",
175
+ "transfer.workspace.label": "目标工作区",
176
+ "transfer.workspace.ungrouped": "未分组",
177
+ "transfer.workspace.new": "新建工作区…",
178
+ "transfer.workspace.hint": "转移到所选工作区;未分组则不归入任何工作区",
179
+ "transfer.workspace.created": "已创建并选中工作区 {title}",
180
+ "transfer.workspace.createFailed": "创建工作区失败:{error}",
181
+ "transfer.session.label": "延续到会话",
182
+ "transfer.session.hint": "选择已有 harness 会话以延续(把简报作为新消息追加);默认新建会话",
183
+ "transfer.session.new": "新建会话",
184
+ "transfer.attached.failed": "会话已创建({sessionId}),但归入工作区失败,现为未分组",
185
+ "handoff.eyebrow": "交付到 Harness",
186
+ "toggle.deepThink": "深度思考",
187
+ "toggle.search": "智能搜索",
188
+ "toggle.deepThink.hint": "开启 R1 深度思考",
189
+ "toggle.search.hint": "开启联网检索",
190
+ "sidebar.empty": "暂无对话",
191
+ "chats.count": "{count} 条",
192
+ "role.you": "你",
193
+ "chat.rename": "重命名",
194
+ "chat.delete": "删除",
195
+ "chat.delete.confirm": "确认删除?",
196
+ "chat.clearAll": "清空全部",
197
+ "chat.clearAll.confirm": "确认清空全部?",
198
+ "chat.rename.placeholder": "输入新标题…",
199
+ "chat.rename.ok": "确定",
200
+ "chat.rename.cancel": "取消",
201
+ "chat.manage.hint": "管理本地保存的聊天记录(不影响网页端)",
202
+ "toast.rename.done": "已重命名",
203
+ "toast.rename.failed": "重命名失败:{error}",
204
+ "toast.delete.done": "已删除该对话",
205
+ "toast.delete.failed": "删除失败:{error}",
206
+ "toast.clear.done": "已清空全部对话",
207
+ "toast.clear.failed": "清空失败:{error}",
208
+ "model.deepseek-chat": "DeepSeek",
209
+ "model.deepseek-reasoner": "DeepSeek",
210
+ "composer.placeholder": "给 DeepSeek 发送消息…(将发送到网页端)",
211
+ "composer.hint": "Enter 发送 · Shift+Enter 换行",
212
+ "composer.busy": "正在生成回复…",
213
+ "composer.notLoggedIn": "请先登录 DeepSeek 网页端",
214
+ "composer.attach": "图片",
215
+ "composer.attach.hint": "添加本地图片(绝对路径,逗号/换行分隔)",
216
+ "composer.attach.placeholder": "图片绝对路径,逗号或换行分隔",
217
+ "empty.eyebrow": "DeepSeek 网页端 · 桥接开发",
218
+ "empty.title": "先聊,再交付",
219
+ "empty.body": "登录网页端即可开始对话;任何对话都能蒸馏成任务简报,转入 Harness 会话继续开发。",
220
+ "transfer.done": "已创建 Harness 会话 {sessionId},正在为你打开…",
221
+ "transfer.continued": "已延续 Harness 会话 {sessionId},正在为你打开…",
222
+ "transfer.failed": "转移到 Harness 失败:{error}",
223
+ "export.done": "已导出到工作区文件:{filePath}",
224
+ "export.failed": "导出失败:{error}",
225
+ "recover.hint": "列出网页端尚未导入本地的会话",
226
+ "recover.empty": "网页端没有未导入的会话",
227
+ "recover.done": "已恢复「{title}」",
228
+ "msg.empty": "(空)",
229
+ "msg.error": "(该回复可能不完整:{error})",
230
+ "streaming": "生成中"
231
+ };
232
+ /** Bilingual balance enforced: en must cover every zh key. */
233
+ const en = {
234
+ "entry.label": "Web Chat",
235
+ "entry.tooltip": "DeepSeek web chat (deepseek-chat, Codex ChatGPT mode)",
236
+ "panel.title": "DeepSeek Web Chat",
237
+ "status.engine.stopped": "Browser not started",
238
+ "status.engine.launching": "Starting browser…",
239
+ "status.engine.ready": "Browser ready",
240
+ "status.engine.error": "Browser error",
241
+ "status.loggedIn": "Logged in",
242
+ "status.notLoggedIn": "Not logged in",
243
+ "status.unknown": "Unknown",
244
+ "action.openLogin": "Open login window",
245
+ "action.closeBrowser": "Close browser",
246
+ "action.newChat": "New chat",
247
+ "action.recover": "Recover from web",
248
+ "action.refresh": "Refresh",
249
+ "action.send": "Send",
250
+ "action.stop": "Stop",
251
+ "action.transferToHarness": "Transfer to Harness",
252
+ "action.exportFile": "Export to file",
253
+ "transfer.mode.distill": "Distill to task brief",
254
+ "transfer.mode.raw": "Raw transcript",
255
+ "transfer.mode.hint": "Distill: condense the chat into an executable brief via the harness LLM; Raw: replay the full transcript as the session's first message",
256
+ "transfer.workspace.label": "Target workspace",
257
+ "transfer.workspace.ungrouped": "Ungrouped",
258
+ "transfer.workspace.new": "New workspace…",
259
+ "transfer.workspace.hint": "Transfer into the chosen workspace; ungrouped leaves it out of any workspace",
260
+ "transfer.workspace.created": "Created and selected workspace {title}",
261
+ "transfer.workspace.createFailed": "Create workspace failed: {error}",
262
+ "transfer.session.label": "Continue into session",
263
+ "transfer.session.hint": "Continue into an existing harness session (append the brief as a new message); default creates a new one",
264
+ "transfer.session.new": "New session",
265
+ "transfer.attached.failed": "Session created ({sessionId}) but attaching to workspace failed; it is ungrouped",
266
+ "handoff.eyebrow": "Handoff to Harness",
267
+ "toggle.deepThink": "DeepThink",
268
+ "toggle.search": "Search",
269
+ "toggle.deepThink.hint": "Enable R1 deep think",
270
+ "toggle.search.hint": "Enable web search",
271
+ "sidebar.empty": "No chats",
272
+ "chats.count": "{count} msgs",
273
+ "role.you": "You",
274
+ "chat.rename": "Rename",
275
+ "chat.delete": "Delete",
276
+ "chat.delete.confirm": "Delete?",
277
+ "chat.clearAll": "Clear all",
278
+ "chat.clearAll.confirm": "Clear all?",
279
+ "chat.rename.placeholder": "New title…",
280
+ "chat.rename.ok": "OK",
281
+ "chat.rename.cancel": "Cancel",
282
+ "chat.manage.hint": "Manage locally saved transcripts (does not affect the web)",
283
+ "toast.rename.done": "Renamed",
284
+ "toast.rename.failed": "Rename failed: {error}",
285
+ "toast.delete.done": "Chat deleted",
286
+ "toast.delete.failed": "Delete failed: {error}",
287
+ "toast.clear.done": "All chats cleared",
288
+ "toast.clear.failed": "Clear failed: {error}",
289
+ "model.deepseek-chat": "DeepSeek",
290
+ "model.deepseek-reasoner": "DeepSeek",
291
+ "composer.placeholder": "Message DeepSeek… (sent via the web)",
292
+ "composer.hint": "Enter to send · Shift+Enter for newline",
293
+ "composer.busy": "Generating…",
294
+ "composer.notLoggedIn": "Sign in to DeepSeek web first",
295
+ "composer.attach": "Images",
296
+ "composer.attach.hint": "Attach local images (absolute paths, comma/newline separated)",
297
+ "composer.attach.placeholder": "Image absolute paths (comma or newline separated)",
298
+ "empty.eyebrow": "DeepSeek Web · Bridge to Code",
299
+ "empty.title": "Chat first, hand off later",
300
+ "empty.body": "Sign in to the web and start chatting; any chat can be distilled into a task brief and handed off to Harness.",
301
+ "transfer.done": "Created Harness session {sessionId}; opening it…",
302
+ "transfer.continued": "Continued Harness session {sessionId}; opening it…",
303
+ "transfer.failed": "Transfer failed: {error}",
304
+ "export.done": "Exported to workspace file: {filePath}",
305
+ "export.failed": "Export failed: {error}",
306
+ "recover.hint": "List web conversations not yet imported locally",
307
+ "recover.empty": "No unimported web conversations",
308
+ "recover.done": "Recovered \"{title}\"",
309
+ "msg.empty": "(empty)",
310
+ "msg.error": "(reply may be incomplete: {error})",
311
+ "streaming": "generating"
312
+ };
313
+ //#endregion
314
+ //#region \0dsh-css:src/client/panel/panel.module.css.mjs
315
+ const css = "[data-pane=conversation],[class*=centerCol]{position:relative}[data-dsh-webchat-view]{z-index:60;background:var(--dsw-alias-bg-base);display:none;position:absolute;inset:0}html[data-dsh-webchat-active]:not([data-dsh-taskboard-active]):not([data-dsh-ssh-active]) [data-dsh-webchat-view]{display:block}html[data-dsh-webchat-active]:not([data-dsh-taskboard-active]):not([data-dsh-ssh-active]) [data-pane=conversation]>:not([data-dsh-webchat-view]),html[data-dsh-webchat-active]:not([data-dsh-taskboard-active]):not([data-dsh-ssh-active]) [class*=centerCol]>:not([data-dsh-webchat-view]){display:none!important}.jbF1yq_entry{width:100%;height:32px;color:var(--dsw-alias-label-secondary);cursor:pointer;white-space:nowrap;background:0 0;border:none;border-radius:8px;align-items:center;gap:8px;padding:0 12px;font-size:13px;display:flex}.jbF1yq_entry:hover{background:var(--dsw-specific-sidebar-nav-item-hover);color:var(--dsw-alias-label-primary)}.jbF1yq_entry[data-active]{background:var(--dsw-specific-sidebar-nav-item-active);color:var(--dsw-alias-label-primary);font-weight:600}.jbF1yq_entryIcon{flex:none;justify-content:center;align-items:center;display:inline-flex}.jbF1yq_entryLabel{text-overflow:ellipsis;overflow:hidden}[data-dsh-frame][data-sidebar-collapsed] .jbF1yq_entry{justify-content:center;width:100%;padding:0}[data-dsh-frame][data-sidebar-collapsed] .jbF1yq_entryLabel{display:none}.jbF1yq_view{box-sizing:border-box;overflow:hidden}.jbF1yq_panel,.jbF1yq_panel *{box-sizing:border-box}.jbF1yq_panel{background:var(--dsw-alias-bg-base);min-width:0;height:100%;min-height:0;color:var(--dsw-alias-label-primary);font-family:var(--dsw-font-family);flex-direction:column;gap:8px;padding:12px 14px 14px;display:flex}.jbF1yq_panelHeader{flex-wrap:wrap;flex:none;align-items:center;gap:10px;display:flex}.jbF1yq_panelTitle{letter-spacing:-.01em;color:var(--dsw-alias-label-primary);white-space:nowrap;margin:0;font-size:15px;font-weight:700}.jbF1yq_headerSpacer{flex:1;min-width:8px}.jbF1yq_statusChip{border:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer1,var(--dsw-alias-bg-elevated));color:var(--dsw-alias-label-secondary);border-radius:999px;flex:none;align-items:center;gap:7px;padding:4px 10px;font-size:11.5px;display:inline-flex}.jbF1yq_statusText{text-overflow:ellipsis;white-space:nowrap;min-width:0;font-family:var(--dsw-font-mono,ui-monospace, SFMono-Regular, Menlo, Consolas, monospace);letter-spacing:.01em;overflow:hidden}.jbF1yq_statusDot{border-radius:50%;flex:none;width:7px;height:7px}.jbF1yq_statusDot[data-state=ok]{background:var(--dsw-alias-state-success,#22c55e)}.jbF1yq_statusDot[data-state=warn]{background:var(--dsw-alias-state-warning,#f59e0b)}.jbF1yq_statusDot[data-state=bad]{background:var(--dsw-alias-state-danger,#ef4444)}.jbF1yq_statusDot[data-state=busy]{background:var(--dsw-alias-state-info,#3b82f6);animation:1.2s ease-in-out infinite jbF1yq_dshWcPulse}@keyframes jbF1yq_dshWcPulse{0%,to{opacity:1}50%{opacity:.35}}.jbF1yq_panelBody{flex:1;gap:10px;min-height:0;display:flex}.jbF1yq_sidebar{border:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer1,var(--dsw-alias-bg-elevated));border-radius:10px;flex-direction:column;flex:0 0 172px;gap:6px;min-width:0;padding:8px;display:flex;overflow:hidden}.jbF1yq_sidebarNew{justify-content:center;width:100%}.jbF1yq_recoverList{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer1,var(--dsw-alias-bg-elevated));border-radius:8px;flex-direction:column;gap:2px;max-height:160px;padding:4px;display:flex;overflow-y:auto}.jbF1yq_recoverItem{width:100%;color:var(--dsw-alias-label-primary);font:inherit;text-align:left;cursor:pointer;background:0 0;border:none;border-radius:6px;padding:6px 8px;font-size:12.5px;display:block}.jbF1yq_recoverItem:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover,#8080801f)}.jbF1yq_recoverItem:disabled{opacity:.5;cursor:default}.jbF1yq_recoverTitle{text-overflow:ellipsis;white-space:nowrap;max-width:100%;display:inline-block;overflow:hidden}.jbF1yq_recoverEmpty{color:var(--dsw-alias-label-tertiary);padding:6px 8px;font-size:12px}.jbF1yq_sidebarList{flex-direction:column;flex:1;gap:2px;min-height:0;display:flex;overflow-y:auto}.jbF1yq_sidebarItem{border-radius:7px;align-items:center;gap:2px;display:flex}.jbF1yq_sidebarItem:hover{background:var(--dsw-alias-interactive-bg-hover,#8080801f)}.jbF1yq_sidebarItem[data-active]{background:var(--dsw-specific-sidebar-nav-item-active,var(--dsw-alias-interactive-bg-hover))}.jbF1yq_sidebarRow{min-width:0;color:var(--dsw-alias-label-primary);cursor:pointer;text-align:left;background:0 0;border:none;border-radius:7px;flex-direction:column;flex:1;align-items:flex-start;gap:1px;padding:6px 8px;display:flex}.jbF1yq_sidebarTitle{text-overflow:ellipsis;white-space:nowrap;width:100%;font-size:12.5px;line-height:1.3;display:block;overflow:hidden}.jbF1yq_sidebarMeta{font-family:var(--dsw-font-mono,ui-monospace, SFMono-Regular, Menlo, Consolas, monospace);color:var(--dsw-alias-label-tertiary);font-size:10px}.jbF1yq_sidebarActions{opacity:0;flex:none;gap:2px;padding-right:4px;transition:opacity .12s;display:flex}.jbF1yq_sidebarItem:hover .jbF1yq_sidebarActions,.jbF1yq_sidebarItem[data-active] .jbF1yq_sidebarActions{opacity:1}.jbF1yq_sidebarAction{width:22px;height:22px;color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;border-radius:6px;justify-content:center;align-items:center;padding:0;display:inline-flex}.jbF1yq_sidebarAction:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}.jbF1yq_sidebarActionDanger{background:var(--dsw-alias-state-danger,#ef4444);color:#fff;cursor:pointer;white-space:nowrap;border:none;border-radius:6px;justify-content:center;align-items:center;height:22px;padding:0 6px;font-size:11px;display:inline-flex}.jbF1yq_sidebarRename{flex:1;align-items:center;gap:2px;min-width:0;padding:2px;display:flex}.jbF1yq_sidebarRenameInput{min-width:0;color:var(--dsw-alias-label-primary);background:var(--dsw-specific-input-major,var(--dsw-alias-bg-layer1));border:1px solid var(--dsw-alias-state-business-primary,#4f6bfa);border-radius:6px;outline:none;flex:1;padding:4px 6px;font-size:12px}.jbF1yq_sidebarEmpty{text-align:center;color:var(--dsw-alias-label-tertiary);margin:auto;padding:12px 8px;font-size:12px}.jbF1yq_sidebarClear{justify-content:center;width:100%}.jbF1yq_main{flex-direction:column;flex:1;gap:8px;min-width:0;min-height:0;display:flex;overflow:hidden}.jbF1yq_composerToolbar{flex:none;align-items:center;gap:6px;padding:2px 12px;display:flex}.jbF1yq_toggleButton{appearance:none;border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);font-family:var(--dsw-font-mono,ui-monospace, SFMono-Regular, Menlo, Consolas, monospace);cursor:pointer;white-space:nowrap;background:0 0;border-radius:999px;align-items:center;gap:5px;padding:4px 11px;font-size:11.5px;transition:color .12s,border-color .12s,background .12s;display:inline-flex}.jbF1yq_toggleButton:hover:not(:disabled){color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-border-l1)}.jbF1yq_toggleButton:disabled{opacity:.45;cursor:not-allowed}.jbF1yq_toggleButton[data-active]{color:var(--dsw-alias-state-business-primary,#4f6bfa);border-color:var(--dsw-alias-state-business-primary,#4f6bfa);background:var(--dsw-alias-interactive-bg-hover,#8080801a);font-weight:600}.jbF1yq_segmented{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-base);border-radius:8px;flex:none;align-items:stretch;gap:2px;padding:2px;display:inline-flex}.jbF1yq_segmentedButton{appearance:none;font-family:var(--dsw-font-mono,ui-monospace, SFMono-Regular, Menlo, Consolas, monospace);color:var(--dsw-alias-label-secondary);cursor:pointer;white-space:nowrap;background:0 0;border:none;border-radius:6px;padding:4px 10px;font-size:11.5px}.jbF1yq_segmentedButton:hover:not([data-active]):not(:disabled){color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover)}.jbF1yq_segmentedButton[data-active]{background:var(--dsw-alias-bg-elevated,var(--dsw-alias-bg-layer1));color:var(--dsw-alias-state-business-primary,#4f6bfa);font-weight:600;box-shadow:0 1px 2px #0000001f}.jbF1yq_button{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-specific-input-major,var(--dsw-alias-bg-layer1));color:var(--dsw-alias-label-primary);cursor:pointer;white-space:nowrap;border-radius:8px;align-items:center;gap:6px;padding:6px 12px;font-size:12.5px;transition:background .12s,filter .12s;display:inline-flex}.jbF1yq_button:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}.jbF1yq_button:disabled{opacity:.45;cursor:not-allowed}.jbF1yq_buttonPrimary{background:var(--dsw-alias-state-business-primary,#4f6bfa);color:#fff;border-color:#0000}.jbF1yq_buttonPrimary:hover:not(:disabled){filter:brightness(1.08)}.jbF1yq_buttonDanger{background:var(--dsw-alias-state-danger,#ef4444);color:#fff;border-color:#0000}.jbF1yq_buttonGhost{color:var(--dsw-alias-label-secondary);background:0 0;border-color:#0000;padding:5px 8px;font-size:12px}.jbF1yq_buttonGhost:hover:not(:disabled){color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover)}.jbF1yq_messageList{flex-direction:column;flex:1;gap:14px;min-height:0;padding:4px 2px;display:flex;overflow-y:auto}.jbF1yq_messageRow{flex-direction:column;gap:5px;max-width:92%;display:flex}.jbF1yq_messageRow[data-role=user]{align-self:flex-end;align-items:flex-end}.jbF1yq_messageRow[data-role=assistant]{align-self:flex-start;align-items:flex-start}.jbF1yq_messageMeta{font-family:var(--dsw-font-mono,ui-monospace, SFMono-Regular, Menlo, Consolas, monospace);align-items:baseline;gap:8px;padding:0 4px;font-size:10.5px;line-height:1;display:flex}.jbF1yq_messageSpeaker{color:var(--dsw-alias-label-secondary);font-weight:600}.jbF1yq_messageTime{color:var(--dsw-alias-label-tertiary);font-variant-numeric:tabular-nums}.jbF1yq_streamBadge{color:var(--dsw-alias-state-info,#3b82f6)}.jbF1yq_messageBubble{overflow-wrap:anywhere;border-radius:12px;min-width:0;max-width:100%;padding:10px 14px;font-size:13.5px;line-height:1.6}.jbF1yq_messageRow[data-role=user] .jbF1yq_messageBubble{background:var(--dsw-alias-state-business-primary,#4f6bfa);color:#fff;border-bottom-right-radius:4px}.jbF1yq_messageRow[data-role=assistant] .jbF1yq_messageBubble{background:var(--dsw-alias-bg-layer1,var(--dsw-alias-bg-elevated));border:1px solid var(--dsw-alias-border-l1);border-bottom-left-radius:4px}.jbF1yq_messageError{color:var(--dsw-alias-state-danger,#ef4444);font-size:11px}.jbF1yq_emptyState{text-align:center;flex-direction:column;align-items:center;gap:10px;max-width:420px;margin:auto;padding:24px 20px;display:flex}.jbF1yq_emptyEyebrow{font-family:var(--dsw-font-mono,ui-monospace, SFMono-Regular, Menlo, Consolas, monospace);letter-spacing:.12em;text-transform:uppercase;color:var(--dsw-alias-state-business-primary,#4f6bfa);font-size:10.5px}.jbF1yq_emptyTitle{letter-spacing:-.01em;color:var(--dsw-alias-label-primary);margin:0;font-size:17px;font-weight:700}.jbF1yq_emptyBody{color:var(--dsw-alias-label-secondary);margin:0;font-size:13px;line-height:1.6}.jbF1yq_composer{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-specific-input-major,var(--dsw-alias-bg-layer1));border-radius:12px;flex-direction:column;flex:none;transition:border-color .12s;display:flex}.jbF1yq_composer:focus-within{border-color:var(--dsw-alias-state-business-primary,#4f6bfa)}.jbF1yq_composerInput{resize:none;min-height:44px;max-height:180px;color:var(--dsw-alias-label-primary);font:inherit;background:0 0;border:none;outline:none;flex:1;padding:10px 12px 6px;font-size:13.5px;line-height:1.5}.jbF1yq_composerInput::placeholder{color:var(--dsw-alias-label-tertiary)}.jbF1yq_attachInput{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer1,var(--dsw-alias-bg-elevated));color:var(--dsw-alias-label-primary);font:inherit;border-radius:8px;margin:0 12px 4px;padding:7px 10px;font-size:12.5px}.jbF1yq_attachInput::placeholder{color:var(--dsw-alias-label-tertiary)}.jbF1yq_composerFooter{align-items:center;gap:8px;padding:0 8px 8px 12px;display:flex}.jbF1yq_composerHint{font-family:var(--dsw-font-mono,ui-monospace, SFMono-Regular, Menlo, Consolas, monospace);color:var(--dsw-alias-label-tertiary);letter-spacing:.02em;font-size:10.5px}.jbF1yq_composerSpacer{flex:1}.jbF1yq_handoffDock{border:1px solid var(--dsw-alias-border-l2);border-left:2px solid var(--dsw-alias-state-business-primary,#4f6bfa);background:var(--dsw-alias-bg-layer1,var(--dsw-alias-bg-elevated));border-radius:10px;flex-wrap:wrap;flex:none;align-items:center;gap:10px;padding:9px 12px;display:flex}.jbF1yq_handoffEyebrow{font-family:var(--dsw-font-mono,ui-monospace, SFMono-Regular, Menlo, Consolas, monospace);letter-spacing:.1em;text-transform:uppercase;color:var(--dsw-alias-label-tertiary);white-space:nowrap;flex:none;font-size:10.5px}.jbF1yq_handoffSpacer{flex:1;min-width:8px}.jbF1yq_workspaceSelect{appearance:none;max-width:280px;font-size:12px;font-family:var(--dsw-font-mono,ui-monospace, SFMono-Regular, Menlo, Consolas, monospace);color:var(--dsw-alias-label-primary);background:var(--dsw-specific-input-major,var(--dsw-alias-bg-layer1));border:1px solid var(--dsw-alias-border-l2);cursor:pointer;border-radius:8px;flex:none;padding:6px 10px}.jbF1yq_workspaceSelect:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary,#4f6bfa);outline-offset:1px}.jbF1yq_markdown{font-size:13.5px;line-height:1.65}.jbF1yq_markdown p{margin:.4em 0}.jbF1yq_markdown pre{background:var(--dsw-alias-bg-deep,#00000073);border-radius:8px;margin:.6em 0;padding:10px 12px;font-size:12.5px;line-height:1.5;position:relative;overflow-x:auto}.jbF1yq_markdown code{font-family:var(--dsw-font-mono,ui-monospace, SFMono-Regular, Menlo, monospace);font-size:.92em}.jbF1yq_markdown :not(pre)>code{background:var(--dsw-alias-interactive-bg-hover,#8080802e);border-radius:5px;padding:1px 5px}.jbF1yq_markdown h1,.jbF1yq_markdown h2,.jbF1yq_markdown h3,.jbF1yq_markdown h4{margin:.8em 0 .4em;line-height:1.3}.jbF1yq_markdown ul,.jbF1yq_markdown ol{margin:.4em 0;padding-left:1.4em}.jbF1yq_markdown blockquote{border-left:3px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);margin:.5em 0;padding:2px 12px}.jbF1yq_tableWrap{overflow-x:auto}.jbF1yq_markdown table{border-collapse:collapse;margin:.6em 0;font-size:12.5px}.jbF1yq_markdown th,.jbF1yq_markdown td{border:1px solid var(--dsw-alias-border-l2);padding:4px 10px}.jbF1yq_markdown sup.jbF1yq_citation{font-family:var(--dsw-font-mono,ui-monospace, SFMono-Regular, Menlo, Consolas, monospace);color:var(--dsw-alias-state-business-primary,#4f6bfa);background:var(--dsw-alias-interactive-bg-hover,#80808029);vertical-align:super;white-space:nowrap;border-radius:999px;margin:0 2px;padding:0 4px;font-size:9.5px;font-weight:600;line-height:1.4}.jbF1yq_markdown a{color:var(--dsw-alias-state-business-primary,#4f6bfa)}.jbF1yq_markdown details{background:var(--dsw-alias-interactive-bg-hover,#8080801f);border-radius:8px;margin:.5em 0;padding:6px 10px;font-size:12.5px}.jbF1yq_markdown summary{cursor:pointer;color:var(--dsw-alias-label-secondary);font-weight:600}.jbF1yq_toast{z-index:200;background:var(--dsw-alias-bg-elevated);border:1px solid var(--dsw-alias-border-l2);max-width:420px;color:var(--dsw-alias-label-primary);border-radius:10px;padding:10px 14px;font-size:13px;animation:.18s ease-out jbF1yq_dshWcToast;position:fixed;bottom:20px;right:20px;box-shadow:0 8px 24px #0000002e}@keyframes jbF1yq_dshWcToast{0%{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}.jbF1yq_button:focus-visible,.jbF1yq_segmentedButton:focus-visible,.jbF1yq_toggleButton:focus-visible,.jbF1yq_sidebarRow:focus-visible,.jbF1yq_sidebarAction:focus-visible,.jbF1yq_sidebarActionDanger:focus-visible,.jbF1yq_sidebarRenameInput:focus-visible,.jbF1yq_composerInput:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary,#4f6bfa);outline-offset:1px}@media (prefers-reduced-motion:reduce){.jbF1yq_statusDot[data-state=busy],.jbF1yq_toast{animation:none}.jbF1yq_button,.jbF1yq_composer{transition:none}}";
316
+ const tagId = "dsh-webchat/panel.module.css";
317
+ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
318
+ const tag = document.createElement("style");
319
+ tag.dataset.plugin = "dsh-webchat";
320
+ tag.dataset.pluginCss = tagId;
321
+ tag.textContent = css;
322
+ document.head.appendChild(tag);
323
+ }
324
+ var panel_module_css_default = {
325
+ "attachInput": "jbF1yq_attachInput",
326
+ "button": "jbF1yq_button",
327
+ "buttonDanger": "jbF1yq_buttonDanger",
328
+ "buttonGhost": "jbF1yq_buttonGhost",
329
+ "buttonPrimary": "jbF1yq_buttonPrimary",
330
+ "citation": "jbF1yq_citation",
331
+ "composer": "jbF1yq_composer",
332
+ "composerFooter": "jbF1yq_composerFooter",
333
+ "composerHint": "jbF1yq_composerHint",
334
+ "composerInput": "jbF1yq_composerInput",
335
+ "composerSpacer": "jbF1yq_composerSpacer",
336
+ "composerToolbar": "jbF1yq_composerToolbar",
337
+ "dshWcPulse": "jbF1yq_dshWcPulse",
338
+ "dshWcToast": "jbF1yq_dshWcToast",
339
+ "emptyBody": "jbF1yq_emptyBody",
340
+ "emptyEyebrow": "jbF1yq_emptyEyebrow",
341
+ "emptyState": "jbF1yq_emptyState",
342
+ "emptyTitle": "jbF1yq_emptyTitle",
343
+ "entry": "jbF1yq_entry",
344
+ "entryIcon": "jbF1yq_entryIcon",
345
+ "entryLabel": "jbF1yq_entryLabel",
346
+ "handoffDock": "jbF1yq_handoffDock",
347
+ "handoffEyebrow": "jbF1yq_handoffEyebrow",
348
+ "handoffSpacer": "jbF1yq_handoffSpacer",
349
+ "headerSpacer": "jbF1yq_headerSpacer",
350
+ "main": "jbF1yq_main",
351
+ "markdown": "jbF1yq_markdown",
352
+ "messageBubble": "jbF1yq_messageBubble",
353
+ "messageError": "jbF1yq_messageError",
354
+ "messageList": "jbF1yq_messageList",
355
+ "messageMeta": "jbF1yq_messageMeta",
356
+ "messageRow": "jbF1yq_messageRow",
357
+ "messageSpeaker": "jbF1yq_messageSpeaker",
358
+ "messageTime": "jbF1yq_messageTime",
359
+ "panel": "jbF1yq_panel",
360
+ "panelBody": "jbF1yq_panelBody",
361
+ "panelHeader": "jbF1yq_panelHeader",
362
+ "panelTitle": "jbF1yq_panelTitle",
363
+ "recoverEmpty": "jbF1yq_recoverEmpty",
364
+ "recoverItem": "jbF1yq_recoverItem",
365
+ "recoverList": "jbF1yq_recoverList",
366
+ "recoverTitle": "jbF1yq_recoverTitle",
367
+ "segmented": "jbF1yq_segmented",
368
+ "segmentedButton": "jbF1yq_segmentedButton",
369
+ "sidebar": "jbF1yq_sidebar",
370
+ "sidebarAction": "jbF1yq_sidebarAction",
371
+ "sidebarActionDanger": "jbF1yq_sidebarActionDanger",
372
+ "sidebarActions": "jbF1yq_sidebarActions",
373
+ "sidebarClear": "jbF1yq_sidebarClear",
374
+ "sidebarEmpty": "jbF1yq_sidebarEmpty",
375
+ "sidebarItem": "jbF1yq_sidebarItem",
376
+ "sidebarList": "jbF1yq_sidebarList",
377
+ "sidebarMeta": "jbF1yq_sidebarMeta",
378
+ "sidebarNew": "jbF1yq_sidebarNew",
379
+ "sidebarRename": "jbF1yq_sidebarRename",
380
+ "sidebarRenameInput": "jbF1yq_sidebarRenameInput",
381
+ "sidebarRow": "jbF1yq_sidebarRow",
382
+ "sidebarTitle": "jbF1yq_sidebarTitle",
383
+ "statusChip": "jbF1yq_statusChip",
384
+ "statusDot": "jbF1yq_statusDot",
385
+ "statusText": "jbF1yq_statusText",
386
+ "streamBadge": "jbF1yq_streamBadge",
387
+ "tableWrap": "jbF1yq_tableWrap",
388
+ "toast": "jbF1yq_toast",
389
+ "toggleButton": "jbF1yq_toggleButton",
390
+ "view": "jbF1yq_view",
391
+ "workspaceSelect": "jbF1yq_workspaceSelect"
392
+ };
393
+ //#endregion
394
+ //#region src/client/panel/Markdown.tsx
395
+ /**
396
+ * Minimal markdown renderer for the web-chat panel. Deliberately small and
397
+ * dependency-free: headings, paragraphs, bold/italic, inline + fenced code,
398
+ * links, DeepSeek `[citation:N]` markers (rendered as superscript badges),
399
+ * unordered/ordered lists, blockquotes, GFM tables, horizontal rules, and
400
+ * <details> passthrough (used for DeepSeek R1 thinking blocks). Anything
401
+ * unrecognized renders as plain text — never raw HTML.
402
+ */
403
+ /** Escape text for safe rendering (no dangerouslySetInnerHTML anywhere). */
404
+ function esc(text) {
405
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
406
+ }
407
+ /**
408
+ * Inline markdown → React nodes. Single tokenizer pass over the string:
409
+ * code spans win, then links, then `[citation:…]` markers, then bold, then
410
+ * italic — each non-token run is escaped text. Bold/italic recurse so a
411
+ * strong span can contain a citation or link.
412
+ */
413
+ function inline(text, keyBase = "i") {
414
+ const nodes = [];
415
+ const re = /(`[^`]+`)|(\[[^\]]+\]\([^)\s]+\))|(\[citation:[^\]]+\])|(\*\*[^*]+\*\*|__[^_]+__)|(\*[^*\n]+\*|_[^_\n]+_)/g;
416
+ let last = 0;
417
+ let index = 0;
418
+ let match;
419
+ while ((match = re.exec(text)) !== null) {
420
+ if (match.index > last) nodes.push(esc(text.slice(last, match.index)));
421
+ const key = `${keyBase}-${index}`;
422
+ if (match[1] !== void 0) nodes.push((0, react.createElement)("code", { key: `${key}-code` }, match[1].slice(1, -1)));
423
+ else if (match[2] !== void 0) {
424
+ const link = /^\[([^\]]+)\]\(([^)\s]+)\)$/.exec(match[2]);
425
+ nodes.push((0, react.createElement)("a", {
426
+ key: `${key}-a`,
427
+ href: link[2],
428
+ target: "_blank",
429
+ rel: "noreferrer"
430
+ }, link[1]));
431
+ } else if (match[3] !== void 0) {
432
+ const nums = match[3].slice(10, -1).trim();
433
+ nodes.push((0, react.createElement)("sup", {
434
+ key: `${key}-cite`,
435
+ className: panel_module_css_default.citation
436
+ }, nums));
437
+ } else if (match[4] !== void 0) {
438
+ const inner = match[4].slice(2, -2);
439
+ nodes.push((0, react.createElement)("strong", { key: `${key}-b` }, ...inline(inner, `${key}-b`)));
440
+ } else if (match[5] !== void 0) {
441
+ const inner = match[5].slice(1, -1);
442
+ nodes.push((0, react.createElement)("em", { key: `${key}-i` }, ...inline(inner, `${key}-i`)));
443
+ }
444
+ last = re.lastIndex;
445
+ index++;
446
+ }
447
+ if (last < text.length) nodes.push(esc(text.slice(last)));
448
+ return nodes;
449
+ }
450
+ /** One fenced code block → <pre><code>. */
451
+ function codeBlock(language, body, key) {
452
+ return (0, react.createElement)("pre", { key }, (0, react.createElement)("code", { className: language === "" ? void 0 : `language-${language}` }, body));
453
+ }
454
+ /** Split a table row into cells, honoring escaped pipes (\|). */
455
+ function splitCells(line) {
456
+ const cells = [];
457
+ let current = "";
458
+ let escaped = false;
459
+ for (const ch of line) if (escaped) {
460
+ current += ch;
461
+ escaped = false;
462
+ } else if (ch === "\\") escaped = true;
463
+ else if (ch === "|") {
464
+ cells.push(current);
465
+ current = "";
466
+ } else current += ch;
467
+ cells.push(current);
468
+ let start = 0;
469
+ let end = cells.length;
470
+ while (start < end && cells[start].trim() === "") start++;
471
+ while (end > start && cells[end - 1].trim() === "") end--;
472
+ return cells.slice(start, end).map((cell) => cell.trim());
473
+ }
474
+ /** True when a line is a GFM table delimiter row (`:---`, `---:`, `:---:`, …). */
475
+ function isDelimiterRow(line) {
476
+ const cells = splitCells(line);
477
+ return cells.length > 0 && cells.every((cell) => /^:?-{3,}:?$/.test(cell));
478
+ }
479
+ /** Render one block of markdown text into React nodes. */
480
+ function renderMarkdown(source) {
481
+ const raw = source.replace(/\r\n/g, "\n").trim();
482
+ if (raw === "") return [];
483
+ const blocks = [];
484
+ const lines = raw.split("\n");
485
+ let index = 0;
486
+ let blockIndex = 0;
487
+ const push = (node) => {
488
+ blocks.push((0, react.createElement)("div", { key: `b${blockIndex++}` }, node));
489
+ };
490
+ while (index < lines.length) {
491
+ const line = lines[index];
492
+ const trimmed = line.trim();
493
+ const fence = /^```([\w+-]*)\s*$/.exec(trimmed);
494
+ if (fence !== null) {
495
+ const language = fence[1] ?? "";
496
+ const body = [];
497
+ index++;
498
+ while (index < lines.length && !/^```\s*$/.test(lines[index].trim())) {
499
+ body.push(lines[index]);
500
+ index++;
501
+ }
502
+ index++;
503
+ push(codeBlock(language, body.join("\n"), `code${blockIndex}`));
504
+ continue;
505
+ }
506
+ const heading = /^(#{1,6})\s+(.*)$/.exec(trimmed);
507
+ if (heading !== null) {
508
+ const level = heading[1].length;
509
+ push((0, react.createElement)(`h${level}`, null, ...inline(heading[2], `h${blockIndex}`)));
510
+ index++;
511
+ continue;
512
+ }
513
+ if (line.includes("|") && lines[index + 1] !== void 0 && isDelimiterRow(lines[index + 1])) {
514
+ const header = splitCells(line);
515
+ index += 2;
516
+ const rows = [];
517
+ while (index < lines.length) {
518
+ const row = lines[index];
519
+ if (row.trim() === "" || !row.includes("|")) break;
520
+ rows.push(splitCells(row));
521
+ index++;
522
+ }
523
+ const width = Math.max(header.length, ...rows.map((row) => row.length));
524
+ const rowNode = (cells, rowIndex) => (0, react.createElement)("tr", { key: rowIndex }, Array.from({ length: width }, (_, col) => (0, react.createElement)("td", { key: col }, ...inline(cells[col] ?? "", `t${blockIndex}-${rowIndex}-${col}`))));
525
+ blocks.push((0, react.createElement)("div", {
526
+ key: `tw${blockIndex}`,
527
+ className: panel_module_css_default.tableWrap
528
+ }, (0, react.createElement)("table", { key: `t${blockIndex}` }, (0, react.createElement)("thead", null, (0, react.createElement)("tr", null, header.map((cell, col) => (0, react.createElement)("th", { key: col }, ...inline(cell, `th${blockIndex}-${col}`))))), (0, react.createElement)("tbody", null, rows.map(rowNode)))));
529
+ blockIndex++;
530
+ continue;
531
+ }
532
+ if (/^(-{3,}|\*{3,}|_{3,})$/.test(trimmed)) {
533
+ push((0, react.createElement)("hr", { key: `hr${blockIndex}` }));
534
+ index++;
535
+ continue;
536
+ }
537
+ if (trimmed.startsWith(">")) {
538
+ const quote = [];
539
+ while (index < lines.length && lines[index].trim().startsWith(">")) {
540
+ quote.push(lines[index].trim().replace(/^>\s?/, ""));
541
+ index++;
542
+ }
543
+ push((0, react.createElement)("blockquote", { key: `q${blockIndex}` }, ...renderMarkdown(quote.join("\n"))));
544
+ continue;
545
+ }
546
+ if (/^<details>/.test(trimmed)) {
547
+ const body = [];
548
+ const summaryMatch = /^<summary>\s*(.*?)\s*<\/summary>/.exec(trimmed);
549
+ index++;
550
+ while (index < lines.length && !/^<\/details>/.test(lines[index].trim())) {
551
+ body.push(lines[index]);
552
+ index++;
553
+ }
554
+ index++;
555
+ push((0, react.createElement)("details", { key: `d${blockIndex}` }, (0, react.createElement)("summary", null, summaryMatch?.[1] ?? "详情"), ...renderMarkdown(body.join("\n"))));
556
+ continue;
557
+ }
558
+ if (/^[-*+]\s+/.test(trimmed)) {
559
+ const items = [];
560
+ while (index < lines.length && /^[-*+]\s+/.test(lines[index].trim())) {
561
+ items.push(lines[index].trim().replace(/^[-*+]\s+/, ""));
562
+ index++;
563
+ }
564
+ push((0, react.createElement)("ul", { key: `ul${blockIndex}` }, items.map((item, itemIndex) => (0, react.createElement)("li", { key: itemIndex }, ...inline(item, `uli${blockIndex}-${itemIndex}`)))));
565
+ continue;
566
+ }
567
+ if (/^\d+[.)]\s+/.test(trimmed)) {
568
+ const items = [];
569
+ while (index < lines.length && /^\d+[.)]\s+/.test(lines[index].trim())) {
570
+ items.push(lines[index].trim().replace(/^\d+[.)]\s+/, ""));
571
+ index++;
572
+ }
573
+ push((0, react.createElement)("ol", { key: `ol${blockIndex}` }, items.map((item, itemIndex) => (0, react.createElement)("li", { key: itemIndex }, ...inline(item, `oli${blockIndex}-${itemIndex}`)))));
574
+ continue;
575
+ }
576
+ const paragraph = [];
577
+ while (index < lines.length) {
578
+ const current = lines[index].trim();
579
+ if (current === "") break;
580
+ if (/^(```|#{1,6}\s|[-*+]\s|\d+[.)]\s|>\s|<\/(details|table)>)/.test(current)) break;
581
+ if (current.includes("|") && lines[index + 1] !== void 0 && isDelimiterRow(lines[index + 1])) break;
582
+ paragraph.push(current);
583
+ index++;
584
+ }
585
+ if (paragraph.length === 0) {
586
+ index++;
587
+ continue;
588
+ }
589
+ push((0, react.createElement)("p", { key: `p${blockIndex}` }, ...inline(paragraph.join(" "), `p${blockIndex}`)));
590
+ }
591
+ return blocks;
592
+ }
593
+ /** The markdown renderer component. */
594
+ function Markdown({ source }) {
595
+ return (0, react.createElement)("div", { className: panel_module_css_default.markdown }, ...renderMarkdown(source));
596
+ }
597
+ //#endregion
598
+ //#region src/client/panel/WebChatPanel.tsx
599
+ /**
600
+ * The web-chat panel: engine/login status, a conversation sidebar (the local
601
+ * transcript list + management), the chat transcript with markdown rendering,
602
+ * streaming updates (polled), the composer with deep-think / internet-search
603
+ * toggles, and the transfer-to-Harness handoff. All data flows through the
604
+ * /api/dsh-webchat routes; the panel is a plain React root in the center column.
605
+ *
606
+ * Layout mirrors chat.deepseek.com: a left conversation sidebar (new chat +
607
+ * history + per-item rename/delete + clear-all) beside the main chat column
608
+ * (messages → composer with 深度思考 / 智能搜索 toggles → handoff dock). The
609
+ * mono register stays reserved for machine things — timestamps, counts,
610
+ * status, and the handoff eyebrow.
611
+ */
612
+ /** Interpolate {placeholder}s in a localized template. */
613
+ function fmt(template, values) {
614
+ return template.replace(/\{(\w+)\}/g, (_, key) => values[key] ?? `{${key}}`);
615
+ }
616
+ /** Compact inline icons (stroke = currentColor, 16px viewBox). */
617
+ const PENCIL_ICON = "<svg viewBox=\"0 0 16 16\" width=\"13\" height=\"13\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.4\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"M11.3 2.2l2.5 2.5L5.5 13H3v-2.5L11.3 2.2z\"/></svg>";
618
+ const TRASH_ICON = "<svg viewBox=\"0 0 16 16\" width=\"13\" height=\"13\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.4\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"M2.5 4h11M6.5 4V2.5h3V4M4 4l.5 9.5h7L12 4\"/></svg>";
619
+ const CHECK_ICON = "<svg viewBox=\"0 0 16 16\" width=\"13\" height=\"13\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.6\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"M3 8.5L6.5 12 13 4.5\"/></svg>";
620
+ const CROSS_ICON = "<svg viewBox=\"0 0 16 16\" width=\"13\" height=\"13\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"M4 4l8 8M12 4l-8 8\"/></svg>";
621
+ /** Time between /state polls while nothing is streaming. */
622
+ const POLL_IDLE_MS = 1500;
623
+ /** Faster polling while a reply is streaming. */
624
+ const POLL_STREAM_MS = 600;
625
+ function WebChatPanel({ api, tt, sessions, workspaces, currentCwd }) {
626
+ const [state, setState] = (0, react.useState)(null);
627
+ const [viewChatId, setViewChatId] = (0, react.useState)(void 0);
628
+ const [draft, setDraft] = (0, react.useState)("");
629
+ const [attachOpen, setAttachOpen] = (0, react.useState)(false);
630
+ const [imagePaths, setImagePaths] = (0, react.useState)("");
631
+ const [toast, setToast] = (0, react.useState)(null);
632
+ const [transferring, setTransferring] = (0, react.useState)(false);
633
+ const [exporting, setExporting] = (0, react.useState)(false);
634
+ const [transferMode, setTransferMode] = (0, react.useState)("distill");
635
+ const [targetWorkspaceId, setTargetWorkspaceId] = (0, react.useState)(void 0);
636
+ const [targetSessionId, setTargetSessionId] = (0, react.useState)(void 0);
637
+ const [deepThink, setDeepThink] = (0, react.useState)(false);
638
+ const [search, setSearch] = (0, react.useState)(false);
639
+ const [renamingId, setRenamingId] = (0, react.useState)(void 0);
640
+ const [renameDraft, setRenameDraft] = (0, react.useState)("");
641
+ const [deleteArmId, setDeleteArmId] = (0, react.useState)(void 0);
642
+ const [clearArm, setClearArm] = (0, react.useState)(false);
643
+ const [recoverOpen, setRecoverOpen] = (0, react.useState)(false);
644
+ const [webMissing, setWebMissing] = (0, react.useState)([]);
645
+ const [recovering, setRecovering] = (0, react.useState)(false);
646
+ const listRef = (0, react.useRef)(null);
647
+ const toastTimer = (0, react.useRef)(void 0);
648
+ const armTimer = (0, react.useRef)(void 0);
649
+ const stateRef = (0, react.useRef)(null);
650
+ stateRef.current = state;
651
+ const pinnedRef = (0, react.useRef)(true);
652
+ const prevChatRef = (0, react.useRef)(void 0);
653
+ const showToast = (0, react.useCallback)((text, error = false) => {
654
+ setToast({
655
+ text,
656
+ error
657
+ });
658
+ window.clearTimeout(toastTimer.current);
659
+ toastTimer.current = window.setTimeout(() => setToast(null), 5e3);
660
+ }, []);
661
+ const workspaceSnapshot = (0, react.useSyncExternalStore)((0, react.useCallback)((listener) => workspaces.list.subscribe(listener), [workspaces]), () => workspaces.list.getSnapshot());
662
+ const workspaceItems = workspaceSnapshot.items;
663
+ const workspaceBaselinesReady = workspaceSnapshot.baselinesReady;
664
+ const sessionSnapshot = (0, react.useSyncExternalStore)((0, react.useCallback)((listener) => sessions.list.subscribe(listener), [sessions]), () => sessions.list.getSnapshot());
665
+ const selectedWorkspace = targetWorkspaceId === void 0 ? void 0 : workspaceItems.find((ws) => ws.workspaceId === targetWorkspaceId);
666
+ const groupedSessionIds = new Set(workspaceItems.flatMap((ws) => ws.sessionIds));
667
+ const continuationTargets = sessionSnapshot.ids.flatMap((id) => {
668
+ const session = sessionSnapshot.byId[id];
669
+ if (session === void 0 || session.blank === true) return [];
670
+ if (selectedWorkspace !== void 0) {
671
+ if (!selectedWorkspace.sessionIds.includes(id)) return [];
672
+ } else if (groupedSessionIds.has(id)) return [];
673
+ return [{
674
+ sessionId: session.id,
675
+ title: session.displayTitle,
676
+ running: session.running
677
+ }];
678
+ });
679
+ const workspaceOverridden = (0, react.useRef)(false);
680
+ (0, react.useEffect)(() => {
681
+ if (workspaceOverridden.current || !workspaceBaselinesReady) return;
682
+ const currentSessionId = sessions.list.getSnapshot().current;
683
+ const currentWorkspace = currentSessionId === void 0 ? void 0 : workspaceItems.find((ws) => ws.sessionIds.includes(currentSessionId));
684
+ setTargetWorkspaceId(currentWorkspace?.workspaceId ?? workspaceSnapshot.recentWorkspaceId);
685
+ }, [
686
+ workspaceBaselinesReady,
687
+ workspaceItems,
688
+ workspaceSnapshot.recentWorkspaceId,
689
+ sessions
690
+ ]);
691
+ const creatingWorkspace = (0, react.useRef)(false);
692
+ const createWorkspace = (0, react.useCallback)(async () => {
693
+ if (creatingWorkspace.current) return;
694
+ creatingWorkspace.current = true;
695
+ try {
696
+ const path = await workspaces.pickDirectory();
697
+ if (path === null || path === "") return;
698
+ const created = await workspaces.create({ path });
699
+ setTargetWorkspaceId(created.workspaceId);
700
+ showToast(fmt(tt("transfer.workspace.created"), { title: created.title }));
701
+ } catch (error) {
702
+ showToast(fmt(tt("transfer.workspace.createFailed"), { error: String(error) }), true);
703
+ } finally {
704
+ creatingWorkspace.current = false;
705
+ }
706
+ }, [
707
+ workspaces,
708
+ showToast,
709
+ tt
710
+ ]);
711
+ (0, react.useEffect)(() => {
712
+ let cancelled = false;
713
+ let timer;
714
+ const poll = async () => {
715
+ if (cancelled) return;
716
+ try {
717
+ const snapshot = await api.state();
718
+ if (cancelled) return;
719
+ setState(snapshot);
720
+ setViewChatId((previous) => {
721
+ const active = snapshot.activeChatId;
722
+ if (previous !== void 0 && snapshot.chats.some((chat) => chat.id === previous)) return previous;
723
+ return active;
724
+ });
725
+ } catch {}
726
+ const streaming = stateRef.current?.chats.some((chat) => chat.streaming) ?? false;
727
+ timer = window.setTimeout(poll, streaming ? POLL_STREAM_MS : POLL_IDLE_MS);
728
+ };
729
+ poll();
730
+ return () => {
731
+ cancelled = true;
732
+ window.clearTimeout(timer);
733
+ };
734
+ }, [api]);
735
+ (0, react.useEffect)(() => {
736
+ if (state === null) return;
737
+ setDeepThink(state.deepThink);
738
+ setSearch(state.search);
739
+ }, [state?.deepThink, state?.search]);
740
+ (0, react.useEffect)(() => {
741
+ const list = listRef.current;
742
+ if (list === null) return;
743
+ const switched = prevChatRef.current !== viewChatId;
744
+ prevChatRef.current = viewChatId;
745
+ if (switched || pinnedRef.current) list.scrollTop = list.scrollHeight;
746
+ }, [state, viewChatId]);
747
+ const handleScroll = (0, react.useCallback)(() => {
748
+ const list = listRef.current;
749
+ if (list === null) return;
750
+ const nearBottom = list.scrollHeight - list.scrollTop - list.clientHeight < 96;
751
+ pinnedRef.current = nearBottom;
752
+ }, []);
753
+ const chats = state?.chats ?? [];
754
+ const viewChat = chats.find((chat) => chat.id === viewChatId) ?? chats[0];
755
+ const busy = state?.busy ?? false;
756
+ const loggedIn = state?.loggedIn ?? null;
757
+ const streaming = viewChat?.streaming ?? false;
758
+ const modelLabel = (0, react.useCallback)((model) => {
759
+ return model === "deepseek-reasoner" ? tt("model.deepseek-reasoner") : tt("model.deepseek-chat");
760
+ }, [tt]);
761
+ const send = (0, react.useCallback)(async () => {
762
+ const text = draft.trim();
763
+ const images = imagePaths.split(/[\n,;]+/).map((path) => path.trim()).filter((path) => path !== "");
764
+ if (text === "" && images.length === 0 || busy) return;
765
+ setDraft("");
766
+ setImagePaths("");
767
+ pinnedRef.current = true;
768
+ try {
769
+ const result = await api.send(text, images.length > 0 ? images : void 0);
770
+ if (result.ok !== true) showToast(result.error ?? "send failed", true);
771
+ else setViewChatId(result.chatId);
772
+ } catch (error) {
773
+ showToast(String(error), true);
774
+ }
775
+ }, [
776
+ draft,
777
+ imagePaths,
778
+ busy,
779
+ api,
780
+ showToast
781
+ ]);
782
+ const stop = (0, react.useCallback)(async () => {
783
+ await api.stop().catch(() => void 0);
784
+ }, [api]);
785
+ const newChat = (0, react.useCallback)(async () => {
786
+ try {
787
+ const result = await api.newChat();
788
+ if (result.ok === true && result.chatId !== void 0) setViewChatId(result.chatId);
789
+ else showToast(result.error ?? "new chat failed", true);
790
+ } catch (error) {
791
+ showToast(String(error), true);
792
+ }
793
+ }, [api, showToast]);
794
+ const toggleDeepThink = (0, react.useCallback)(async () => {
795
+ const next = !deepThink;
796
+ setDeepThink(next);
797
+ try {
798
+ const result = await api.setDeepThink(next);
799
+ if (result.ok !== true) showToast(result.error ?? "toggle deep think failed", true);
800
+ } catch (error) {
801
+ showToast(String(error), true);
802
+ }
803
+ }, [
804
+ deepThink,
805
+ api,
806
+ showToast
807
+ ]);
808
+ const toggleSearch = (0, react.useCallback)(async () => {
809
+ const next = !search;
810
+ setSearch(next);
811
+ try {
812
+ const result = await api.setSearch(next);
813
+ if (result.ok !== true) showToast(result.error ?? "toggle search failed", true);
814
+ } catch (error) {
815
+ showToast(String(error), true);
816
+ }
817
+ }, [
818
+ search,
819
+ api,
820
+ showToast
821
+ ]);
822
+ const openLogin = (0, react.useCallback)(async () => {
823
+ try {
824
+ const result = await api.openLogin();
825
+ if (result.ok !== true) showToast(result.error ?? "open login failed", true);
826
+ } catch (error) {
827
+ showToast(String(error), true);
828
+ }
829
+ }, [api, showToast]);
830
+ const closeBrowser = (0, react.useCallback)(async () => {
831
+ await api.closeBrowser().catch(() => void 0);
832
+ }, [api]);
833
+ const toggleRecover = (0, react.useCallback)(async () => {
834
+ if (recoverOpen) {
835
+ setRecoverOpen(false);
836
+ return;
837
+ }
838
+ try {
839
+ const result = await api.webChats();
840
+ setWebMissing(result.ok === true ? result.missing ?? [] : []);
841
+ setRecoverOpen(true);
842
+ } catch (error) {
843
+ showToast(String(error), true);
844
+ }
845
+ }, [
846
+ recoverOpen,
847
+ api,
848
+ showToast
849
+ ]);
850
+ const recoverFromWeb = (0, react.useCallback)(async (title) => {
851
+ if (recovering) return;
852
+ setRecovering(true);
853
+ try {
854
+ const result = await api.recover(title);
855
+ if (result.ok !== true || result.chatId === void 0) {
856
+ showToast(result.error ?? "recover failed", true);
857
+ return;
858
+ }
859
+ setWebMissing((list) => list.filter((item) => item !== title));
860
+ showToast(fmt(tt("recover.done"), { title: result.title ?? title }));
861
+ } catch (error) {
862
+ showToast(String(error), true);
863
+ } finally {
864
+ setRecovering(false);
865
+ }
866
+ }, [
867
+ recovering,
868
+ api,
869
+ showToast,
870
+ tt
871
+ ]);
872
+ const transferToHarness = (0, react.useCallback)(async () => {
873
+ if (viewChat === void 0 || transferring) return;
874
+ setTransferring(true);
875
+ try {
876
+ const result = await api.transfer(viewChat.id, currentCwd(), transferMode, targetWorkspaceId, targetSessionId);
877
+ if (result.ok !== true || result.sessionId === void 0) {
878
+ showToast(result.error ?? "transfer failed", true);
879
+ return;
880
+ }
881
+ const target = result.sessionId;
882
+ if (result.continued === true) {
883
+ showToast(fmt(tt("transfer.continued"), { sessionId: target }));
884
+ sessions.open(target);
885
+ return;
886
+ }
887
+ if (result.attached === false && targetWorkspaceId !== void 0) showToast(fmt(tt("transfer.attached.failed"), { sessionId: target }), true);
888
+ else showToast(fmt(tt("transfer.done"), { sessionId: target }));
889
+ const refreshable = sessions;
890
+ try {
891
+ await refreshable.refresh?.();
892
+ } catch {}
893
+ const tryOpen = () => {
894
+ if (sessions.list.getSnapshot().byId[target] !== void 0) {
895
+ sessions.open(target);
896
+ return;
897
+ }
898
+ window.setTimeout(tryOpen, 300);
899
+ };
900
+ window.setTimeout(tryOpen, 200);
901
+ } catch (error) {
902
+ showToast(String(error), true);
903
+ } finally {
904
+ setTransferring(false);
905
+ }
906
+ }, [
907
+ viewChat,
908
+ transferring,
909
+ api,
910
+ currentCwd,
911
+ sessions,
912
+ showToast,
913
+ tt,
914
+ transferMode,
915
+ targetWorkspaceId,
916
+ targetSessionId
917
+ ]);
918
+ const exportFile = (0, react.useCallback)(async () => {
919
+ if (viewChat === void 0 || exporting) return;
920
+ setExporting(true);
921
+ try {
922
+ const result = await api.exportFile(viewChat.id, currentCwd());
923
+ if (result.ok !== true || result.filePath === void 0) showToast(result.error ?? "export failed", true);
924
+ else showToast(fmt(tt("export.done"), { filePath: result.filePath }));
925
+ } catch (error) {
926
+ showToast(String(error), true);
927
+ } finally {
928
+ setExporting(false);
929
+ }
930
+ }, [
931
+ viewChat,
932
+ exporting,
933
+ api,
934
+ currentCwd,
935
+ showToast,
936
+ tt
937
+ ]);
938
+ const resetArmTimer = (0, react.useCallback)((next) => {
939
+ window.clearTimeout(armTimer.current);
940
+ armTimer.current = next === void 0 ? void 0 : window.setTimeout(next, 3e3);
941
+ }, []);
942
+ const startRename = (0, react.useCallback)((chat) => {
943
+ setDeleteArmId(void 0);
944
+ setClearArm(false);
945
+ setRenameDraft(chat.title);
946
+ setRenamingId(chat.id);
947
+ }, []);
948
+ const commitRename = (0, react.useCallback)(async () => {
949
+ const id = renamingId;
950
+ if (id === void 0) return;
951
+ const title = renameDraft.trim().replace(/\s+/g, " ");
952
+ setRenamingId(void 0);
953
+ const original = chats.find((chat) => chat.id === id);
954
+ if (title === "" || original !== void 0 && title === original.title) return;
955
+ try {
956
+ const result = await api.renameChat(id, title);
957
+ if (result.ok !== true) showToast(fmt(tt("toast.rename.failed"), { error: result.error ?? "" }), true);
958
+ else showToast(tt("toast.rename.done"));
959
+ } catch (error) {
960
+ showToast(fmt(tt("toast.rename.failed"), { error: String(error) }), true);
961
+ }
962
+ }, [
963
+ renamingId,
964
+ renameDraft,
965
+ chats,
966
+ api,
967
+ showToast,
968
+ tt
969
+ ]);
970
+ const cancelRename = (0, react.useCallback)(() => {
971
+ setRenamingId(void 0);
972
+ setRenameDraft("");
973
+ }, []);
974
+ const requestDelete = (0, react.useCallback)(async (id) => {
975
+ if (deleteArmId !== id) {
976
+ setClearArm(false);
977
+ setRenamingId(void 0);
978
+ setDeleteArmId(id);
979
+ resetArmTimer(() => setDeleteArmId(void 0));
980
+ return;
981
+ }
982
+ resetArmTimer(void 0);
983
+ setDeleteArmId(void 0);
984
+ try {
985
+ const result = await api.deleteChat(id);
986
+ if (result.ok !== true) showToast(fmt(tt("toast.delete.failed"), { error: result.error ?? "" }), true);
987
+ else showToast(tt("toast.delete.done"));
988
+ } catch (error) {
989
+ showToast(fmt(tt("toast.delete.failed"), { error: String(error) }), true);
990
+ }
991
+ }, [
992
+ deleteArmId,
993
+ api,
994
+ showToast,
995
+ tt,
996
+ resetArmTimer
997
+ ]);
998
+ const requestClear = (0, react.useCallback)(async () => {
999
+ if (chats.length === 0) return;
1000
+ if (!clearArm) {
1001
+ setDeleteArmId(void 0);
1002
+ setRenamingId(void 0);
1003
+ setClearArm(true);
1004
+ resetArmTimer(() => setClearArm(false));
1005
+ return;
1006
+ }
1007
+ resetArmTimer(void 0);
1008
+ setClearArm(false);
1009
+ try {
1010
+ const result = await api.clearChats();
1011
+ if (result.ok !== true) showToast(fmt(tt("toast.clear.failed"), { error: result.error ?? "" }), true);
1012
+ else showToast(tt("toast.clear.done"));
1013
+ } catch (error) {
1014
+ showToast(fmt(tt("toast.clear.failed"), { error: String(error) }), true);
1015
+ }
1016
+ }, [
1017
+ chats.length,
1018
+ clearArm,
1019
+ api,
1020
+ showToast,
1021
+ tt,
1022
+ resetArmTimer
1023
+ ]);
1024
+ const engineState = state?.engine ?? "stopped";
1025
+ const engineError = state?.engineError;
1026
+ const statusDot = busy ? "busy" : engineState === "error" ? "bad" : loggedIn === true ? "ok" : "warn";
1027
+ const statusText = busy ? tt("composer.busy") : engineState === "error" ? `${tt("status.engine.error")}${engineError !== void 0 ? `: ${engineError}` : ""}` : engineState === "launching" ? tt("status.engine.launching") : loggedIn === true ? tt("status.loggedIn") : loggedIn === false ? tt("status.notLoggedIn") : tt("status.unknown");
1028
+ const togglesDisabled = busy || loggedIn !== true;
1029
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1030
+ className: panel_module_css_default.panel,
1031
+ children: [
1032
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", {
1033
+ className: panel_module_css_default.panelHeader,
1034
+ children: [
1035
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
1036
+ className: panel_module_css_default.panelTitle,
1037
+ children: tt("panel.title")
1038
+ }),
1039
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: panel_module_css_default.headerSpacer }),
1040
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1041
+ className: panel_module_css_default.statusChip,
1042
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1043
+ className: panel_module_css_default.statusDot,
1044
+ "data-state": statusDot
1045
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1046
+ className: panel_module_css_default.statusText,
1047
+ children: statusText
1048
+ })]
1049
+ }),
1050
+ loggedIn !== true && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1051
+ className: `${panel_module_css_default.button} ${panel_module_css_default.buttonPrimary}`,
1052
+ onClick: () => {
1053
+ openLogin();
1054
+ },
1055
+ children: tt("action.openLogin")
1056
+ }),
1057
+ engineState !== "stopped" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1058
+ className: panel_module_css_default.buttonGhost,
1059
+ onClick: () => {
1060
+ closeBrowser();
1061
+ },
1062
+ children: tt("action.closeBrowser")
1063
+ })
1064
+ ]
1065
+ }),
1066
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1067
+ className: panel_module_css_default.panelBody,
1068
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("aside", {
1069
+ className: panel_module_css_default.sidebar,
1070
+ children: [
1071
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1072
+ className: `${panel_module_css_default.button} ${panel_module_css_default.buttonPrimary} ${panel_module_css_default.sidebarNew}`,
1073
+ onClick: () => {
1074
+ newChat();
1075
+ },
1076
+ children: tt("action.newChat")
1077
+ }),
1078
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1079
+ className: `${panel_module_css_default.buttonGhost} ${panel_module_css_default.sidebarNew}`,
1080
+ disabled: loggedIn !== true,
1081
+ onClick: () => {
1082
+ toggleRecover();
1083
+ },
1084
+ title: tt("recover.hint"),
1085
+ children: tt("action.recover")
1086
+ }),
1087
+ recoverOpen && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1088
+ className: panel_module_css_default.recoverList,
1089
+ children: webMissing.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1090
+ className: panel_module_css_default.recoverEmpty,
1091
+ children: tt("recover.empty")
1092
+ }) : webMissing.map((title) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1093
+ className: panel_module_css_default.recoverItem,
1094
+ disabled: recovering,
1095
+ onClick: () => {
1096
+ recoverFromWeb(title);
1097
+ },
1098
+ title,
1099
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1100
+ className: panel_module_css_default.recoverTitle,
1101
+ children: title
1102
+ })
1103
+ }, title))
1104
+ }),
1105
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1106
+ className: panel_module_css_default.sidebarList,
1107
+ children: chats.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1108
+ className: panel_module_css_default.sidebarEmpty,
1109
+ children: tt("sidebar.empty")
1110
+ }) : chats.map((chat) => renamingId === chat.id ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1111
+ className: panel_module_css_default.sidebarItem,
1112
+ "data-active": chat.id === viewChatId ? "true" : void 0,
1113
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1114
+ className: panel_module_css_default.sidebarRename,
1115
+ children: [
1116
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1117
+ className: panel_module_css_default.sidebarRenameInput,
1118
+ value: renameDraft,
1119
+ placeholder: tt("chat.rename.placeholder"),
1120
+ "aria-label": "rename",
1121
+ autoFocus: true,
1122
+ onChange: (event) => setRenameDraft(event.target.value),
1123
+ onKeyDown: (event) => {
1124
+ if (event.key === "Enter") {
1125
+ event.preventDefault();
1126
+ commitRename();
1127
+ } else if (event.key === "Escape") cancelRename();
1128
+ }
1129
+ }),
1130
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1131
+ className: panel_module_css_default.sidebarAction,
1132
+ onClick: () => {
1133
+ commitRename();
1134
+ },
1135
+ "aria-label": tt("chat.rename.ok"),
1136
+ dangerouslySetInnerHTML: { __html: CHECK_ICON }
1137
+ }),
1138
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1139
+ className: panel_module_css_default.sidebarAction,
1140
+ onClick: cancelRename,
1141
+ "aria-label": tt("chat.rename.cancel"),
1142
+ dangerouslySetInnerHTML: { __html: CROSS_ICON }
1143
+ })
1144
+ ]
1145
+ })
1146
+ }, chat.id) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1147
+ className: panel_module_css_default.sidebarItem,
1148
+ "data-active": chat.id === viewChatId ? "true" : void 0,
1149
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1150
+ className: panel_module_css_default.sidebarRow,
1151
+ onClick: () => setViewChatId(chat.id),
1152
+ title: chat.title,
1153
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1154
+ className: panel_module_css_default.sidebarTitle,
1155
+ children: chat.title
1156
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1157
+ className: panel_module_css_default.sidebarMeta,
1158
+ children: fmt(tt("chats.count"), { count: String(chat.messages.length) })
1159
+ })]
1160
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1161
+ className: panel_module_css_default.sidebarActions,
1162
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1163
+ className: panel_module_css_default.sidebarAction,
1164
+ onClick: () => startRename(chat),
1165
+ "aria-label": tt("chat.rename"),
1166
+ title: tt("chat.rename"),
1167
+ dangerouslySetInnerHTML: { __html: PENCIL_ICON }
1168
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1169
+ className: deleteArmId === chat.id ? panel_module_css_default.sidebarActionDanger : panel_module_css_default.sidebarAction,
1170
+ onClick: () => {
1171
+ requestDelete(chat.id);
1172
+ },
1173
+ "aria-label": deleteArmId === chat.id ? tt("chat.delete.confirm") : tt("chat.delete"),
1174
+ title: deleteArmId === chat.id ? tt("chat.delete.confirm") : tt("chat.delete"),
1175
+ children: deleteArmId === chat.id ? tt("chat.delete.confirm") : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { dangerouslySetInnerHTML: { __html: TRASH_ICON } })
1176
+ })]
1177
+ })]
1178
+ }, chat.id))
1179
+ }),
1180
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1181
+ className: clearArm ? `${panel_module_css_default.button} ${panel_module_css_default.buttonDanger} ${panel_module_css_default.sidebarClear}` : `${panel_module_css_default.buttonGhost} ${panel_module_css_default.sidebarClear}`,
1182
+ disabled: chats.length === 0,
1183
+ onClick: () => {
1184
+ requestClear();
1185
+ },
1186
+ children: clearArm ? tt("chat.clearAll.confirm") : tt("chat.clearAll")
1187
+ })
1188
+ ]
1189
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1190
+ className: panel_module_css_default.main,
1191
+ children: [
1192
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1193
+ className: panel_module_css_default.messageList,
1194
+ ref: listRef,
1195
+ onScroll: handleScroll,
1196
+ children: viewChat === void 0 || viewChat.messages.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1197
+ className: panel_module_css_default.emptyState,
1198
+ children: [
1199
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1200
+ className: panel_module_css_default.emptyEyebrow,
1201
+ children: tt("empty.eyebrow")
1202
+ }),
1203
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
1204
+ className: panel_module_css_default.emptyTitle,
1205
+ children: tt("empty.title")
1206
+ }),
1207
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1208
+ className: panel_module_css_default.emptyBody,
1209
+ children: tt("empty.body")
1210
+ }),
1211
+ loggedIn !== true && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1212
+ className: `${panel_module_css_default.button} ${panel_module_css_default.buttonPrimary}`,
1213
+ onClick: () => {
1214
+ openLogin();
1215
+ },
1216
+ children: tt("action.openLogin")
1217
+ })
1218
+ ]
1219
+ }) : viewChat.messages.map((message) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1220
+ className: panel_module_css_default.messageRow,
1221
+ "data-role": message.role,
1222
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1223
+ className: panel_module_css_default.messageMeta,
1224
+ children: [
1225
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1226
+ className: panel_module_css_default.messageSpeaker,
1227
+ children: message.role === "user" ? tt("role.you") : modelLabel(viewChat.model)
1228
+ }),
1229
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1230
+ className: panel_module_css_default.messageTime,
1231
+ children: new Date(message.ts).toLocaleTimeString()
1232
+ }),
1233
+ message.streaming === true && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1234
+ className: panel_module_css_default.streamBadge,
1235
+ children: [tt("streaming"), "…"]
1236
+ })
1237
+ ]
1238
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1239
+ className: panel_module_css_default.messageBubble,
1240
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Markdown, { source: message.content }), message.error !== void 0 && message.content !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1241
+ className: panel_module_css_default.messageError,
1242
+ children: fmt(tt("msg.error"), { error: message.error })
1243
+ })]
1244
+ })]
1245
+ }, message.id))
1246
+ }),
1247
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1248
+ className: panel_module_css_default.composer,
1249
+ children: [
1250
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1251
+ className: panel_module_css_default.composerToolbar,
1252
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1253
+ className: panel_module_css_default.toggleButton,
1254
+ "data-active": deepThink ? "true" : void 0,
1255
+ disabled: togglesDisabled,
1256
+ title: tt("toggle.deepThink.hint"),
1257
+ "aria-pressed": deepThink,
1258
+ onClick: () => {
1259
+ toggleDeepThink();
1260
+ },
1261
+ children: tt("toggle.deepThink")
1262
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1263
+ className: panel_module_css_default.toggleButton,
1264
+ "data-active": search ? "true" : void 0,
1265
+ disabled: togglesDisabled,
1266
+ title: tt("toggle.search.hint"),
1267
+ "aria-pressed": search,
1268
+ onClick: () => {
1269
+ toggleSearch();
1270
+ },
1271
+ children: tt("toggle.search")
1272
+ })]
1273
+ }),
1274
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
1275
+ className: panel_module_css_default.composerInput,
1276
+ value: draft,
1277
+ placeholder: busy ? tt("composer.busy") : loggedIn !== true ? tt("composer.notLoggedIn") : tt("composer.placeholder"),
1278
+ disabled: busy || loggedIn !== true,
1279
+ onChange: (event) => setDraft(event.target.value),
1280
+ onKeyDown: (event) => {
1281
+ if (event.key === "Enter" && !event.shiftKey) {
1282
+ event.preventDefault();
1283
+ send();
1284
+ }
1285
+ }
1286
+ }),
1287
+ attachOpen && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1288
+ className: panel_module_css_default.attachInput,
1289
+ value: imagePaths,
1290
+ placeholder: tt("composer.attach.placeholder"),
1291
+ disabled: busy || loggedIn !== true,
1292
+ onChange: (event) => setImagePaths(event.target.value)
1293
+ }),
1294
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1295
+ className: panel_module_css_default.composerFooter,
1296
+ children: [
1297
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1298
+ className: panel_module_css_default.toggleButton,
1299
+ "data-active": attachOpen ? "true" : void 0,
1300
+ title: tt("composer.attach.hint"),
1301
+ "aria-pressed": attachOpen,
1302
+ disabled: busy || loggedIn !== true,
1303
+ onClick: () => setAttachOpen((open) => !open),
1304
+ children: tt("composer.attach")
1305
+ }),
1306
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1307
+ className: panel_module_css_default.composerHint,
1308
+ children: tt("composer.hint")
1309
+ }),
1310
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: panel_module_css_default.composerSpacer }),
1311
+ streaming ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1312
+ className: `${panel_module_css_default.button} ${panel_module_css_default.buttonDanger}`,
1313
+ onClick: () => {
1314
+ stop();
1315
+ },
1316
+ children: tt("action.stop")
1317
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1318
+ className: `${panel_module_css_default.button} ${panel_module_css_default.buttonPrimary}`,
1319
+ disabled: busy || loggedIn !== true || draft.trim() === "" && imagePaths.trim() === "",
1320
+ onClick: () => {
1321
+ send();
1322
+ },
1323
+ children: tt("action.send")
1324
+ })
1325
+ ]
1326
+ })
1327
+ ]
1328
+ }),
1329
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1330
+ className: panel_module_css_default.handoffDock,
1331
+ children: [
1332
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1333
+ className: panel_module_css_default.handoffEyebrow,
1334
+ children: tt("handoff.eyebrow")
1335
+ }),
1336
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: panel_module_css_default.handoffSpacer }),
1337
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1338
+ className: panel_module_css_default.segmented,
1339
+ role: "group",
1340
+ "aria-label": "transfer mode",
1341
+ title: tt("transfer.mode.hint"),
1342
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1343
+ className: panel_module_css_default.segmentedButton,
1344
+ "data-active": transferMode === "distill" ? "true" : void 0,
1345
+ onClick: () => setTransferMode("distill"),
1346
+ children: tt("transfer.mode.distill")
1347
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1348
+ className: panel_module_css_default.segmentedButton,
1349
+ "data-active": transferMode === "raw" ? "true" : void 0,
1350
+ onClick: () => setTransferMode("raw"),
1351
+ children: tt("transfer.mode.raw")
1352
+ })]
1353
+ }),
1354
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
1355
+ className: panel_module_css_default.workspaceSelect,
1356
+ value: targetSessionId ?? "",
1357
+ disabled: continuationTargets.length === 0,
1358
+ title: tt("transfer.session.hint"),
1359
+ "aria-label": tt("transfer.session.label"),
1360
+ onChange: (event) => setTargetSessionId(event.target.value === "" ? void 0 : event.target.value),
1361
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1362
+ value: "",
1363
+ children: tt("transfer.session.new")
1364
+ }), continuationTargets.map((session) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("option", {
1365
+ value: session.sessionId,
1366
+ children: [session.title, session.running === true ? " ·●" : ""]
1367
+ }, session.sessionId))]
1368
+ }),
1369
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
1370
+ className: panel_module_css_default.workspaceSelect,
1371
+ value: targetWorkspaceId ?? "",
1372
+ disabled: targetSessionId !== void 0,
1373
+ title: tt("transfer.workspace.hint"),
1374
+ "aria-label": tt("transfer.workspace.label"),
1375
+ onChange: (event) => {
1376
+ workspaceOverridden.current = true;
1377
+ setTargetWorkspaceId(event.target.value === "" ? void 0 : event.target.value);
1378
+ },
1379
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1380
+ value: "",
1381
+ children: tt("transfer.workspace.ungrouped")
1382
+ }), workspaceItems.map((ws) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("option", {
1383
+ value: ws.workspaceId,
1384
+ children: [ws.title, ws.path !== ws.title ? ` — ${ws.path}` : ""]
1385
+ }, ws.workspaceId))]
1386
+ }),
1387
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1388
+ className: panel_module_css_default.buttonGhost,
1389
+ onClick: () => {
1390
+ createWorkspace();
1391
+ },
1392
+ title: tt("transfer.workspace.new"),
1393
+ disabled: targetSessionId !== void 0,
1394
+ children: tt("transfer.workspace.new")
1395
+ }),
1396
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1397
+ className: `${panel_module_css_default.button} ${panel_module_css_default.buttonPrimary}`,
1398
+ disabled: viewChat === void 0 || transferring,
1399
+ onClick: () => {
1400
+ transferToHarness();
1401
+ },
1402
+ children: tt("action.transferToHarness")
1403
+ }),
1404
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1405
+ className: panel_module_css_default.button,
1406
+ disabled: viewChat === void 0 || exporting,
1407
+ onClick: () => {
1408
+ exportFile();
1409
+ },
1410
+ children: tt("action.exportFile")
1411
+ })
1412
+ ]
1413
+ })
1414
+ ]
1415
+ })]
1416
+ }),
1417
+ toast !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1418
+ className: panel_module_css_default.toast,
1419
+ "data-error": toast.error === true ? "true" : void 0,
1420
+ children: toast.text
1421
+ })
1422
+ ]
1423
+ });
1424
+ }
1425
+ //#endregion
1426
+ //#region src/client/mount.tsx
1427
+ /**
1428
+ * Panel view mounting. The conversation slot is single-occupant and external
1429
+ * plugins cannot declare slots, so the panel takes over the center column at
1430
+ * the DOM level (same strategy as dsh-ssh / dsh-task-board): a container is
1431
+ * appended inside the center column as an extra trailing child React never
1432
+ * manages, and a stylesheet rule hides the conversation content while the
1433
+ * panel is active. Toggling is a data attribute on <html> — no React
1434
+ * involvement, so the conversation subtree underneath stays mounted and
1435
+ * stateful.
1436
+ */
1437
+ const CONVERSATION_COLUMN_SELECTOR = "[data-pane=\"conversation\"], [class*=\"centerCol\"]";
1438
+ const ACTIVE_ATTR = "data-dsh-webchat-active";
1439
+ /** The sibling panels' activation attributes, removed when this panel opens. */
1440
+ const OTHER_ACTIVE_ATTRS = ["data-dsh-taskboard-active", "data-dsh-ssh-active"];
1441
+ /** Cross-plugin activation event; detail is the activating panel name. */
1442
+ const ACTIVATE_EVENT = "dsh-panel-activate";
1443
+ const PANEL_NAME = "webchat";
1444
+ /** Find the center column, or undefined while the frame is not mounted. */
1445
+ function conversationColumn() {
1446
+ return document.querySelector(CONVERSATION_COLUMN_SELECTOR) ?? void 0;
1447
+ }
1448
+ /**
1449
+ * Mount the panel React tree into the center column and bind its visibility
1450
+ * to the controller's panelOpen state.
1451
+ * @param deps - sessions, api, controller, locale accessor.
1452
+ * @returns disposer unmounting the tree and restoring the column.
1453
+ */
1454
+ function mountPanel(deps) {
1455
+ const { sessions, workspaces, api, controller, tt, currentCwd } = deps;
1456
+ let root;
1457
+ let container;
1458
+ const ensure = () => {
1459
+ if (container !== void 0) {
1460
+ if (container.isConnected) return;
1461
+ root?.unmount();
1462
+ root = void 0;
1463
+ container.remove();
1464
+ container = void 0;
1465
+ }
1466
+ const column = conversationColumn();
1467
+ if (column === void 0) return;
1468
+ container = document.createElement("div");
1469
+ container.dataset.dshWebchatView = "";
1470
+ container.dataset.dshPlugin = "webchat";
1471
+ container.className = panel_module_css_default.view;
1472
+ column.appendChild(container);
1473
+ root = (0, react_dom_client.createRoot)(container);
1474
+ root.render(/* @__PURE__ */ (0, react_jsx_runtime.jsx)(WebChatPanel, {
1475
+ api,
1476
+ tt,
1477
+ sessions,
1478
+ workspaces,
1479
+ currentCwd
1480
+ }));
1481
+ };
1482
+ const waitObserver = new MutationObserver(() => {
1483
+ ensure();
1484
+ });
1485
+ waitObserver.observe(document.body, {
1486
+ childList: true,
1487
+ subtree: true
1488
+ });
1489
+ const applyActive = () => {
1490
+ if (controller.getSnapshot().panelOpen) {
1491
+ for (const attr of OTHER_ACTIVE_ATTRS) document.documentElement.removeAttribute(attr);
1492
+ document.documentElement.setAttribute(ACTIVE_ATTR, "");
1493
+ document.dispatchEvent(new CustomEvent(ACTIVATE_EVENT, { detail: PANEL_NAME }));
1494
+ } else document.documentElement.removeAttribute(ACTIVE_ATTR);
1495
+ };
1496
+ const onOtherActivate = (event) => {
1497
+ const detail = event.detail;
1498
+ if ((detail === "taskboard" || detail === "ssh") && controller.getSnapshot().panelOpen) controller.close();
1499
+ };
1500
+ const SIDEBAR_ROW_SELECTOR = "[class*=\"sessionRow\"], [class*=\"projectRow\"], [class*=\"searchResultRow\"], [class*=\"searchResultWorkspace\"], [class*=\"newSession\"]";
1501
+ const onClickSidebarRow = (event) => {
1502
+ if (!controller.getSnapshot().panelOpen) return;
1503
+ const target = event.target;
1504
+ if (target === null) return;
1505
+ if (target.closest(SIDEBAR_ROW_SELECTOR) !== null) controller.close();
1506
+ };
1507
+ document.addEventListener("click", onClickSidebarRow, true);
1508
+ document.addEventListener(ACTIVATE_EVENT, onOtherActivate);
1509
+ const unsubscribe = controller.subscribe(applyActive);
1510
+ applyActive();
1511
+ ensure();
1512
+ return () => {
1513
+ document.removeEventListener("click", onClickSidebarRow, true);
1514
+ document.removeEventListener(ACTIVATE_EVENT, onOtherActivate);
1515
+ waitObserver.disconnect();
1516
+ unsubscribe();
1517
+ document.documentElement.removeAttribute(ACTIVE_ATTR);
1518
+ root?.unmount();
1519
+ root = void 0;
1520
+ container?.remove();
1521
+ container = void 0;
1522
+ };
1523
+ }
1524
+ //#endregion
1525
+ //#region src/client/sidebar-entry-core.ts
1526
+ /** Find the sidebar shell root element, or undefined while not yet mounted. */
1527
+ function sidebarRoot() {
1528
+ const column = document.querySelector("[data-pane=\"sidebar\"], [class*=\"sidebarCol\"]");
1529
+ if (column === null) return void 0;
1530
+ return column.querySelector("[class*=\"logoRow\"]")?.parentElement ?? column.firstElementChild;
1531
+ }
1532
+ /** The New Session button: nested in the logo row on current shells, a direct child on legacy shells. */
1533
+ function newSessionButton(root) {
1534
+ const nested = root.querySelector("button[class*=\"newSession\"]");
1535
+ if (nested !== null) return nested;
1536
+ for (const child of root.children) if (child.tagName === "BUTTON") return child;
1537
+ }
1538
+ /** Build the entry row (a detached button; insert once the shell is up). */
1539
+ function createEntry(options) {
1540
+ const entry = document.createElement("button");
1541
+ entry.type = "button";
1542
+ entry.setAttribute(options.rowAttribute, "");
1543
+ if (options.plugin !== void 0) {
1544
+ entry.setAttribute("data-dsh-plugin", options.plugin);
1545
+ entry.setAttribute("data-dsh-part", "sidebar-entry");
1546
+ }
1547
+ entry.className = options.css["entry"] ?? "";
1548
+ entry.setAttribute("aria-label", options.label());
1549
+ if (options.tooltip !== void 0) entry.setAttribute("title", options.tooltip());
1550
+ entry.innerHTML = "<span class=\"" + (options.css["entryIcon"] ?? "") + "\">" + options.icon + "</span><span class=\"" + (options.css["entryLabel"] ?? "") + "\">" + options.label() + "</span>";
1551
+ entry.addEventListener("click", options.onToggle);
1552
+ return entry;
1553
+ }
1554
+ /** Re-insert the entry after the New Session row (before the browser region). */
1555
+ function placeEntry(root, entry, options) {
1556
+ const button = newSessionButton(root);
1557
+ if (button === void 0) return false;
1558
+ if (entry.parentElement !== root) {
1559
+ const row = button.closest("[class*=\"logoRow\"]");
1560
+ const base = row !== null && row.parentElement === root ? row : button;
1561
+ const family = Array.from(root.children).filter((el) => el instanceof HTMLElement && el.matches(options.familySelectors.join(", ")));
1562
+ const anchor = options.position === "before" ? family.length > 0 ? family[0] : base.nextElementSibling : family.length > 0 ? family[family.length - 1].nextElementSibling : base.nextElementSibling;
1563
+ root.insertBefore(entry, anchor);
1564
+ }
1565
+ return true;
1566
+ }
1567
+ /**
1568
+ * Mount the sidebar entry, waiting for the shell to render and self-healing
1569
+ * on later React re-renders.
1570
+ * @param options - the row's attribute/icon/copy/action/ordering configuration.
1571
+ * @returns disposer removing the entry and its observers.
1572
+ */
1573
+ function mountSidebarEntry$1(options) {
1574
+ if (typeof document !== "undefined" && document.querySelector(options.rowSelector) !== null) return () => {};
1575
+ const entry = createEntry(options);
1576
+ let root;
1577
+ let placed = false;
1578
+ const tryPlace = () => {
1579
+ if (root !== void 0 && !root.isConnected) {
1580
+ rootObserver.disconnect();
1581
+ root = void 0;
1582
+ placed = false;
1583
+ }
1584
+ if (placed) {
1585
+ if (document.body.contains(entry)) return;
1586
+ rootObserver.disconnect();
1587
+ root = void 0;
1588
+ placed = false;
1589
+ }
1590
+ root ??= sidebarRoot();
1591
+ if (root === void 0) return;
1592
+ placed = placeEntry(root, entry, options);
1593
+ if (placed) rootObserver.observe(root, {
1594
+ childList: true,
1595
+ subtree: true
1596
+ });
1597
+ };
1598
+ const waitObserver = new MutationObserver(() => {
1599
+ tryPlace();
1600
+ });
1601
+ waitObserver.observe(document.body, {
1602
+ childList: true,
1603
+ subtree: true
1604
+ });
1605
+ const rootObserver = new MutationObserver(() => {
1606
+ if (root === void 0 || !root.isConnected) {
1607
+ placed = false;
1608
+ tryPlace();
1609
+ return;
1610
+ }
1611
+ if (!root.contains(entry)) placed = placeEntry(root, entry, options);
1612
+ });
1613
+ const unsubscribeActive = options.active === void 0 ? void 0 : (() => {
1614
+ const syncActive = () => {
1615
+ if (options.active.isOpen()) entry.dataset.active = "true";
1616
+ else delete entry.dataset.active;
1617
+ };
1618
+ const unsubscribe = options.active.subscribe(syncActive);
1619
+ syncActive();
1620
+ return unsubscribe;
1621
+ })();
1622
+ tryPlace();
1623
+ return () => {
1624
+ waitObserver.disconnect();
1625
+ rootObserver.disconnect();
1626
+ unsubscribeActive?.();
1627
+ entry.remove();
1628
+ };
1629
+ }
1630
+ //#endregion
1631
+ //#region src/client/sidebar-entry.ts
1632
+ /** Stable data attribute identifying the injected entry row. */
1633
+ const ENTRY_SELECTOR = "[data-dsh-webchat-entry]";
1634
+ /** Inline icon (matches the shell's 16px nav-icon look): a chat bubble with a globe dot. */
1635
+ const ICON = "<svg viewBox=\"0 0 16 16\" width=\"14\" height=\"14\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.3\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"M8 2.5a5.5 5.5 0 0 0-4.7 8.3L2.5 13.5l2.8-.8A5.5 5.5 0 1 0 8 2.5z\"/><circle cx=\"8\" cy=\"8\" r=\"1.1\" fill=\"currentColor\" stroke=\"none\"/></svg>";
1636
+ /**
1637
+ * Mount the sidebar entry, waiting for the shell to render and self-healing
1638
+ * on later React re-renders.
1639
+ * @param deps - controller and copy accessor.
1640
+ * @returns disposer removing the entry and its observers.
1641
+ */
1642
+ function mountSidebarEntry(deps) {
1643
+ return mountSidebarEntry$1({
1644
+ rowAttribute: "data-dsh-webchat-entry",
1645
+ rowSelector: ENTRY_SELECTOR,
1646
+ plugin: "webchat",
1647
+ icon: ICON,
1648
+ css: panel_module_css_default,
1649
+ label: () => deps.tt("entry.label"),
1650
+ tooltip: () => deps.tt("entry.tooltip"),
1651
+ onToggle: () => {
1652
+ deps.controller.toggle();
1653
+ },
1654
+ position: "after",
1655
+ familySelectors: [
1656
+ "[data-dsh-taskboard-entry]",
1657
+ "[data-dsh-ssh-entry]",
1658
+ "[data-dsh-webchat-entry]"
1659
+ ],
1660
+ active: {
1661
+ subscribe: (listener) => deps.controller.subscribe(listener),
1662
+ isOpen: () => deps.controller.getSnapshot().panelOpen
1663
+ }
1664
+ });
1665
+ }
1666
+ //#endregion
1667
+ //#region src/client/index.ts
1668
+ /** Locale namespace this plugin owns. */
1669
+ const NS = "dsh-webchat";
1670
+ /** Required services (fiber inject waiting — the runtime must be up first). */
1671
+ const inject = [
1672
+ "slots",
1673
+ "locale",
1674
+ "sessions",
1675
+ "workspaces"
1676
+ ];
1677
+ /**
1678
+ * Mount the web-chat panel.
1679
+ * @param ctx - client root context (locale + sessions services).
1680
+ */
1681
+ function apply(ctx) {
1682
+ ctx.effect(() => ctx.locale.register(NS, {
1683
+ zh,
1684
+ en
1685
+ }), "dsh-webchat: dictionaries");
1686
+ const controller = new PanelController();
1687
+ const api = new WebChatApi();
1688
+ const sessions = ctx.sessions;
1689
+ const workspaces = ctx.workspaces;
1690
+ const tt = ctx.locale.bind(NS);
1691
+ const currentCwd = () => {
1692
+ const snapshot = sessions.list.getSnapshot();
1693
+ const current = snapshot.current;
1694
+ if (current === void 0) return void 0;
1695
+ return snapshot.byId[current]?.cwd;
1696
+ };
1697
+ const disposers = [];
1698
+ try {
1699
+ disposers.push(mountSidebarEntry({
1700
+ controller,
1701
+ tt
1702
+ }));
1703
+ disposers.push(mountPanel({
1704
+ sessions,
1705
+ workspaces,
1706
+ api,
1707
+ controller,
1708
+ tt,
1709
+ currentCwd
1710
+ }));
1711
+ } catch (error) {
1712
+ console.warn("[dsh-webchat] mount failed:", error);
1713
+ }
1714
+ ctx.effect(() => () => {
1715
+ for (const dispose of disposers.splice(0)) dispose();
1716
+ }, "dsh-webchat: ui mounts");
1717
+ }
1718
+ //#endregion
1719
+ exports.apply = apply;
1720
+ exports.inject = inject;
1721
+ return module.exports;
1722
+ }
1723
+ });
1724
+
1725
+ //# sourceMappingURL=client.js.map