@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.
- package/LICENSE +201 -0
- package/NOTICE +18 -0
- package/README.md +78 -0
- package/README.zh.md +83 -0
- package/cordis.patch.yml +22 -0
- package/lib/client.js +1725 -0
- package/lib/client.js.map +1 -0
- package/lib/index.js +3887 -0
- package/lib/types/client/api.d.ts +74 -0
- package/lib/types/client/controller.d.ts +19 -0
- package/lib/types/client/index.d.ts +30 -0
- package/lib/types/client/locales.d.ts +169 -0
- package/lib/types/client/mount.d.ts +23 -0
- package/lib/types/client/panel/Markdown.d.ts +15 -0
- package/lib/types/client/panel/WebChatPanel.d.ts +28 -0
- package/lib/types/client/sidebar-entry-core.d.ts +60 -0
- package/lib/types/client/sidebar-entry.d.ts +22 -0
- package/lib/types/engine/engine.d.ts +247 -0
- package/lib/types/engine/html-md.d.ts +23 -0
- package/lib/types/index.d.ts +63 -0
- package/lib/types/protocol.d.ts +107 -0
- package/lib/types/routes.d.ts +22 -0
- package/lib/types/store.d.ts +69 -0
- package/lib/types/tools.d.ts +27 -0
- package/lib/types/transfer.d.ts +138 -0
- package/package.json +96 -0
- package/src/client/api.ts +90 -0
- package/src/client/controller.ts +43 -0
- package/src/client/css-modules.d.ts +5 -0
- package/src/client/index.ts +75 -0
- package/src/client/locales.ts +172 -0
- package/src/client/mount.tsx +125 -0
- package/src/client/panel/Markdown.tsx +251 -0
- package/src/client/panel/WebChatPanel.tsx +760 -0
- package/src/client/panel/panel.module.css +936 -0
- package/src/client/sidebar-entry-core.ts +207 -0
- package/src/client/sidebar-entry.ts +49 -0
- package/src/engine/engine.ts +1385 -0
- package/src/engine/html-md.ts +126 -0
- package/src/index.ts +212 -0
- package/src/protocol.ts +116 -0
- package/src/routes.ts +307 -0
- package/src/store.ts +217 -0
- package/src/tools.ts +257 -0
- 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, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
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
|