@jaychang1989/dsh-webchat 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/NOTICE +10 -3
  2. package/README.md +44 -40
  3. package/README.zh.md +43 -44
  4. package/cordis.patch.yml +8 -7
  5. package/lib/client.js +402 -1693
  6. package/lib/index.js +246 -3884
  7. package/package.json +3 -35
  8. package/lib/client.js.map +0 -1
  9. package/lib/types/client/api.d.ts +0 -74
  10. package/lib/types/client/controller.d.ts +0 -19
  11. package/lib/types/client/index.d.ts +0 -30
  12. package/lib/types/client/locales.d.ts +0 -169
  13. package/lib/types/client/mount.d.ts +0 -23
  14. package/lib/types/client/panel/Markdown.d.ts +0 -15
  15. package/lib/types/client/panel/WebChatPanel.d.ts +0 -28
  16. package/lib/types/client/sidebar-entry-core.d.ts +0 -60
  17. package/lib/types/client/sidebar-entry.d.ts +0 -22
  18. package/lib/types/engine/engine.d.ts +0 -247
  19. package/lib/types/engine/html-md.d.ts +0 -23
  20. package/lib/types/index.d.ts +0 -63
  21. package/lib/types/protocol.d.ts +0 -107
  22. package/lib/types/routes.d.ts +0 -22
  23. package/lib/types/store.d.ts +0 -69
  24. package/lib/types/tools.d.ts +0 -27
  25. package/lib/types/transfer.d.ts +0 -138
  26. package/src/client/api.ts +0 -90
  27. package/src/client/controller.ts +0 -43
  28. package/src/client/css-modules.d.ts +0 -5
  29. package/src/client/index.ts +0 -75
  30. package/src/client/locales.ts +0 -172
  31. package/src/client/mount.tsx +0 -125
  32. package/src/client/panel/Markdown.tsx +0 -251
  33. package/src/client/panel/WebChatPanel.tsx +0 -760
  34. package/src/client/panel/panel.module.css +0 -936
  35. package/src/client/sidebar-entry-core.ts +0 -207
  36. package/src/client/sidebar-entry.ts +0 -49
  37. package/src/engine/engine.ts +0 -1385
  38. package/src/engine/html-md.ts +0 -126
  39. package/src/index.ts +0 -212
  40. package/src/protocol.ts +0 -116
  41. package/src/routes.ts +0 -307
  42. package/src/store.ts +0 -217
  43. package/src/tools.ts +0 -257
  44. package/src/transfer.ts +0 -602
package/lib/client.js CHANGED
@@ -1,1725 +1,434 @@
1
+ /**
2
+ * dsh-webchat — browser half.
3
+ *
4
+ * One sidebar entry and one panel holding one button. The panel does not chat:
5
+ * chat.deepseek.com cannot be framed (`frame-ancestors 'none'`) and this build
6
+ * has `webviewTag: false`, so the official page opens in a window instead (see
7
+ * lib/index.js). Everything here is plain DOM — no React, no build step.
8
+ *
9
+ * The shell exposes no slot an external plugin can register into, so the entry
10
+ * row and the center-column view are injected at the DOM level and self-heal
11
+ * against React re-renders, exactly as the previous release did.
12
+ */
1
13
  window.__ModuleLoader__.load({
2
14
  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();
15
+ factory: () => {
16
+ var module = { exports: {} }
17
+ var exports = module.exports
18
+
19
+ /** Routes served by lib/index.js. */
20
+ var API = { state: '/api/dsh-webchat/state', open: '/api/dsh-webchat/open' }
21
+ /** The page the launcher opens (mirrors PAGE_URL on the host half). */
22
+ var PAGE_URL = 'https://chat.deepseek.com/'
23
+ var ENTRY_ATTR = 'data-dsh-webchat-entry'
24
+ var VIEW_ATTR = 'data-dsh-webchat-view'
25
+ var ACTIVE_ATTR = 'data-dsh-webchat-active'
26
+ var OTHER_ACTIVE_ATTRS = ['data-dsh-taskboard-active', 'data-dsh-ssh-active']
27
+ var ACTIVATE_EVENT = 'dsh-panel-activate'
28
+ var PANEL_NAME = 'webchat'
29
+ var STYLE_ID = 'dsh-webchat-style'
30
+
31
+ var ZH = typeof navigator === 'object' && typeof navigator.language === 'string'
32
+ && navigator.language.toLowerCase().indexOf('zh') === 0
33
+
34
+ /** Surface copy; the plugin ships both languages rather than a locale service. */
35
+ var T = ZH
36
+ ? {
37
+ label: 'DeepSeek 网页',
38
+ tooltip: '在窗口里打开 chat.deepseek.com',
39
+ title: 'DeepSeek 网页',
40
+ lede: '官方网页版就是完整的客户端:模型选择、深度思考、智能搜索、历史记录、附件上传都在里面。这里只负责把它打开。',
41
+ open: '打开 chat.deepseek.com',
42
+ reopen: '回到已打开的窗口',
43
+ hint: '首次使用请在打开的这个窗口里登录 DeepSeek,之后登录态会保留。',
44
+ checking: '检查中…',
45
+ closed: '窗口未打开',
46
+ opened: '窗口已打开',
47
+ busy: '正在打开…',
48
+ done: '已打开',
49
+ failed: '打开失败',
133
50
  }
134
- open() {
135
- if (!this.snapshot.panelOpen) this.set({ panelOpen: true });
51
+ : {
52
+ label: 'DeepSeek Web',
53
+ tooltip: 'Open chat.deepseek.com in a window',
54
+ title: 'DeepSeek Web',
55
+ lede: 'The official web app is the whole client: model picker, deep think, smart search, history and attachments all live there. This plugin only opens it.',
56
+ open: 'Open chat.deepseek.com',
57
+ reopen: 'Focus the open window',
58
+ hint: 'Sign in once inside the window it opens; the session is kept afterwards.',
59
+ checking: 'Checking…',
60
+ closed: 'Window is closed',
61
+ opened: 'Window is open',
62
+ busy: 'Opening…',
63
+ done: 'Opened',
64
+ failed: 'Could not open',
136
65
  }
137
- close() {
138
- if (this.snapshot.panelOpen) this.set({ panelOpen: false });
66
+
67
+ /** Panel and entry styling, scoped to this plugin's own attributes. */
68
+ var CSS = [
69
+ "[data-pane='conversation'],[class*='centerCol']{position:relative}",
70
+ '[' + VIEW_ATTR + ']{position:absolute;inset:0;display:none;z-index:60;background:var(--dsw-alias-bg-base)}',
71
+ 'html[' + ACTIVE_ATTR + ']:not([data-dsh-taskboard-active]):not([data-dsh-ssh-active]) [' + VIEW_ATTR + ']{display:block}',
72
+ "html[" + ACTIVE_ATTR + "]:not([data-dsh-taskboard-active]):not([data-dsh-ssh-active]) [data-pane='conversation'] > :not([" + VIEW_ATTR + ']),',
73
+ 'html[' + ACTIVE_ATTR + "]:not([data-dsh-taskboard-active]):not([data-dsh-ssh-active]) [class*='centerCol'] > :not([" + VIEW_ATTR + ']){display:none !important}',
74
+ '[' + ENTRY_ATTR + ']{display:flex;align-items:center;gap:8px;width:100%;height:32px;padding:0 12px;background:transparent;border:none;border-radius:8px;color:var(--dsw-alias-label-secondary);cursor:pointer;font-size:13px;white-space:nowrap}',
75
+ '[' + ENTRY_ATTR + ']:hover{background:var(--dsw-specific-sidebar-nav-item-hover);color:var(--dsw-alias-label-primary)}',
76
+ '[' + ENTRY_ATTR + '][data-active]{background:var(--dsw-specific-sidebar-nav-item-active);color:var(--dsw-alias-label-primary);font-weight:600}',
77
+ '[' + ENTRY_ATTR + '] [data-part=icon]{display:inline-flex;align-items:center;justify-content:center;flex:none}',
78
+ '[' + ENTRY_ATTR + '] [data-part=label]{overflow:hidden;text-overflow:ellipsis}',
79
+ '[data-dsh-frame][data-sidebar-collapsed] [' + ENTRY_ATTR + ']{justify-content:center;padding:0}',
80
+ '[data-dsh-frame][data-sidebar-collapsed] [' + ENTRY_ATTR + '] [data-part=label]{display:none}',
81
+ '.dsh-wc-panel{box-sizing:border-box;display:flex;flex-direction:column;height:100%;min-height:0;padding:18px 20px;gap:14px;background:var(--dsw-alias-bg-base);color:var(--dsw-alias-label-primary);font-family:var(--dsw-font-family);overflow:auto}',
82
+ '.dsh-wc-head{display:flex;align-items:center;gap:10px;flex:none}',
83
+ '.dsh-wc-title{margin:0;font-size:16px;font-weight:700;letter-spacing:-.01em}',
84
+ '.dsh-wc-chip{margin-left:auto;display:inline-flex;align-items:center;gap:7px;padding:4px 10px;border:1px solid var(--dsw-alias-border-l1);border-radius:999px;font-size:11.5px;color:var(--dsw-alias-label-secondary)}',
85
+ '.dsh-wc-dot{width:7px;height:7px;border-radius:50%;background:var(--dsw-alias-label-tertiary);flex:none}',
86
+ ".dsh-wc-dot[data-state=ok]{background:var(--dsw-alias-state-success,#22c55e)}",
87
+ ".dsh-wc-dot[data-state=bad]{background:var(--dsw-alias-state-danger,#ef4444)}",
88
+ ".dsh-wc-dot[data-state=busy]{background:var(--dsw-alias-state-info,#3b82f6)}",
89
+ '.dsh-wc-card{margin:auto;max-width:520px;display:flex;flex-direction:column;align-items:center;gap:14px;text-align:center;padding:26px 22px;border:1px solid var(--dsw-alias-border-l1);border-radius:14px;background:var(--dsw-alias-bg-layer1,var(--dsw-alias-bg-elevated))}',
90
+ '.dsh-wc-lede{margin:0;font-size:13px;line-height:1.65;color:var(--dsw-alias-label-secondary)}',
91
+ '.dsh-wc-url{font-family:var(--dsw-font-mono,ui-monospace,Consolas,monospace);font-size:12px;color:var(--dsw-alias-label-tertiary)}',
92
+ '.dsh-wc-button{display:inline-flex;align-items:center;gap:6px;padding:9px 18px;font-size:13.5px;font-weight:600;border:1px solid transparent;border-radius:9px;background:var(--dsw-alias-state-business-primary,#4f6bfa);color:#fff;cursor:pointer}',
93
+ '.dsh-wc-button:hover:not(:disabled){filter:brightness(1.08)}',
94
+ '.dsh-wc-button:disabled{opacity:.5;cursor:not-allowed}',
95
+ '.dsh-wc-hint{margin:0;font-size:12px;line-height:1.6;color:var(--dsw-alias-label-tertiary)}',
96
+ '.dsh-wc-result{margin:0;min-height:18px;font-size:12px;color:var(--dsw-alias-label-secondary);overflow-wrap:anywhere}',
97
+ ".dsh-wc-result[data-state=bad]{color:var(--dsw-alias-state-danger,#ef4444)}",
98
+ ].join('\n')
99
+
100
+ /** Inject the stylesheet once. */
101
+ function ensureStyle() {
102
+ if (document.getElementById(STYLE_ID) !== null) return
103
+ var tag = document.createElement('style')
104
+ tag.id = STYLE_ID
105
+ tag.textContent = CSS
106
+ document.head.appendChild(tag)
107
+ }
108
+
109
+ /** Tiny observable for the panel's open state. */
110
+ function createController() {
111
+ var open = false
112
+ var listeners = new Set()
113
+ var notify = function () {
114
+ listeners.forEach(function (listener) {
115
+ try {
116
+ listener()
117
+ } catch (error) {
118
+ console.warn('[dsh-webchat] listener failed:', error)
119
+ }
120
+ })
139
121
  }
140
- toggle() {
141
- this.set({ panelOpen: !this.snapshot.panelOpen });
122
+ return {
123
+ isOpen: function () { return open },
124
+ subscribe: function (listener) {
125
+ listeners.add(listener)
126
+ return function () { listeners.delete(listener) }
127
+ },
128
+ toggle: function () { open = !open; notify() },
129
+ close: function () { if (!open) return; open = false; notify() },
142
130
  }
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
131
  }
324
- var panel_module_css_default = {
325
- "attachInput": "jbF1yq_attachInput",
326
- "button": "jbF1yq_button",
327
- "buttonDanger": "jbF1yq_buttonDanger",
328
- "buttonGhost": "jbF1yq_buttonGhost",
329
- "buttonPrimary": "jbF1yq_buttonPrimary",
330
- "citation": "jbF1yq_citation",
331
- "composer": "jbF1yq_composer",
332
- "composerFooter": "jbF1yq_composerFooter",
333
- "composerHint": "jbF1yq_composerHint",
334
- "composerInput": "jbF1yq_composerInput",
335
- "composerSpacer": "jbF1yq_composerSpacer",
336
- "composerToolbar": "jbF1yq_composerToolbar",
337
- "dshWcPulse": "jbF1yq_dshWcPulse",
338
- "dshWcToast": "jbF1yq_dshWcToast",
339
- "emptyBody": "jbF1yq_emptyBody",
340
- "emptyEyebrow": "jbF1yq_emptyEyebrow",
341
- "emptyState": "jbF1yq_emptyState",
342
- "emptyTitle": "jbF1yq_emptyTitle",
343
- "entry": "jbF1yq_entry",
344
- "entryIcon": "jbF1yq_entryIcon",
345
- "entryLabel": "jbF1yq_entryLabel",
346
- "handoffDock": "jbF1yq_handoffDock",
347
- "handoffEyebrow": "jbF1yq_handoffEyebrow",
348
- "handoffSpacer": "jbF1yq_handoffSpacer",
349
- "headerSpacer": "jbF1yq_headerSpacer",
350
- "main": "jbF1yq_main",
351
- "markdown": "jbF1yq_markdown",
352
- "messageBubble": "jbF1yq_messageBubble",
353
- "messageError": "jbF1yq_messageError",
354
- "messageList": "jbF1yq_messageList",
355
- "messageMeta": "jbF1yq_messageMeta",
356
- "messageRow": "jbF1yq_messageRow",
357
- "messageSpeaker": "jbF1yq_messageSpeaker",
358
- "messageTime": "jbF1yq_messageTime",
359
- "panel": "jbF1yq_panel",
360
- "panelBody": "jbF1yq_panelBody",
361
- "panelHeader": "jbF1yq_panelHeader",
362
- "panelTitle": "jbF1yq_panelTitle",
363
- "recoverEmpty": "jbF1yq_recoverEmpty",
364
- "recoverItem": "jbF1yq_recoverItem",
365
- "recoverList": "jbF1yq_recoverList",
366
- "recoverTitle": "jbF1yq_recoverTitle",
367
- "segmented": "jbF1yq_segmented",
368
- "segmentedButton": "jbF1yq_segmentedButton",
369
- "sidebar": "jbF1yq_sidebar",
370
- "sidebarAction": "jbF1yq_sidebarAction",
371
- "sidebarActionDanger": "jbF1yq_sidebarActionDanger",
372
- "sidebarActions": "jbF1yq_sidebarActions",
373
- "sidebarClear": "jbF1yq_sidebarClear",
374
- "sidebarEmpty": "jbF1yq_sidebarEmpty",
375
- "sidebarItem": "jbF1yq_sidebarItem",
376
- "sidebarList": "jbF1yq_sidebarList",
377
- "sidebarMeta": "jbF1yq_sidebarMeta",
378
- "sidebarNew": "jbF1yq_sidebarNew",
379
- "sidebarRename": "jbF1yq_sidebarRename",
380
- "sidebarRenameInput": "jbF1yq_sidebarRenameInput",
381
- "sidebarRow": "jbF1yq_sidebarRow",
382
- "sidebarTitle": "jbF1yq_sidebarTitle",
383
- "statusChip": "jbF1yq_statusChip",
384
- "statusDot": "jbF1yq_statusDot",
385
- "statusText": "jbF1yq_statusText",
386
- "streamBadge": "jbF1yq_streamBadge",
387
- "tableWrap": "jbF1yq_tableWrap",
388
- "toast": "jbF1yq_toast",
389
- "toggleButton": "jbF1yq_toggleButton",
390
- "view": "jbF1yq_view",
391
- "workspaceSelect": "jbF1yq_workspaceSelect"
392
- };
393
- //#endregion
394
- //#region src/client/panel/Markdown.tsx
395
- /**
396
- * Minimal markdown renderer for the web-chat panel. Deliberately small and
397
- * dependency-free: headings, paragraphs, bold/italic, inline + fenced code,
398
- * links, DeepSeek `[citation:N]` markers (rendered as superscript badges),
399
- * unordered/ordered lists, blockquotes, GFM tables, horizontal rules, and
400
- * <details> passthrough (used for DeepSeek R1 thinking blocks). Anything
401
- * unrecognized renders as plain text — never raw HTML.
402
- */
403
- /** Escape text for safe rendering (no dangerouslySetInnerHTML anywhere). */
404
- function esc(text) {
405
- return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
132
+
133
+ /** The sidebar shell root, or undefined while it is not mounted. */
134
+ function sidebarRoot() {
135
+ var column = document.querySelector('[data-pane="sidebar"], [class*="sidebarCol"]')
136
+ if (column === null) return undefined
137
+ var logoRow = column.querySelector('[class*="logoRow"]')
138
+ var owner = logoRow === null ? null : logoRow.parentElement
139
+ return owner !== null && owner !== undefined ? owner : (column.firstElementChild || undefined)
406
140
  }
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++;
141
+
142
+ /** The New Session button the entry is ordered against. */
143
+ function newSessionButton(root) {
144
+ var nested = root.querySelector('button[class*="newSession"]')
145
+ if (nested !== null) return nested
146
+ for (var i = 0; i < root.children.length; i++) {
147
+ if (root.children[i].tagName === 'BUTTON') return root.children[i]
446
148
  }
447
- if (last < text.length) nodes.push(esc(text.slice(last)));
448
- return nodes;
149
+ return undefined
449
150
  }
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());
151
+
152
+ /** Build the entry row. */
153
+ function createEntry(controller) {
154
+ var entry = document.createElement('button')
155
+ entry.type = 'button'
156
+ entry.setAttribute(ENTRY_ATTR, '')
157
+ entry.setAttribute('data-dsh-plugin', 'webchat')
158
+ entry.setAttribute('data-dsh-part', 'sidebar-entry')
159
+ entry.setAttribute('aria-label', T.label)
160
+ entry.setAttribute('title', T.tooltip)
161
+ var icon = document.createElement('span')
162
+ icon.setAttribute('data-part', 'icon')
163
+ icon.innerHTML = '<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>'
164
+ var label = document.createElement('span')
165
+ label.setAttribute('data-part', 'label')
166
+ label.textContent = T.label
167
+ entry.appendChild(icon)
168
+ entry.appendChild(label)
169
+ entry.addEventListener('click', function () { controller.toggle() })
170
+ return entry
473
171
  }
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));
172
+
173
+ /** Re-insert the entry after the New Session row when React displaces it. */
174
+ function placeEntry(root, entry) {
175
+ var button = newSessionButton(root)
176
+ if (button === undefined) return false
177
+ if (entry.parentElement === root) return true
178
+ var row = button.closest('[class*="logoRow"]')
179
+ var base = row !== null && row.parentElement === root ? row : button
180
+ var family = Array.prototype.filter.call(root.children, function (el) {
181
+ return el.matches && el.matches('[' + ENTRY_ATTR + '], [data-dsh-taskboard-entry], [data-dsh-ssh-entry]')
182
+ })
183
+ var anchor = family.length > 0 ? family[family.length - 1].nextElementSibling : base.nextElementSibling
184
+ root.insertBefore(entry, anchor)
185
+ return true
478
186
  }
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;
187
+
188
+ /** Mount the sidebar entry with its self-healing observers. */
189
+ function mountSidebarEntry(controller) {
190
+ if (document.querySelector('[' + ENTRY_ATTR + ']') !== null) return function () {}
191
+ var entry = createEntry(controller)
192
+ var root
193
+ var placed = false
194
+
195
+ var tryPlace = function () {
196
+ if (root !== undefined && !root.isConnected) {
197
+ rootObserver.disconnect()
198
+ root = undefined
199
+ placed = false
588
200
  }
589
- push((0, react.createElement)("p", { key: `p${blockIndex}` }, ...inline(paragraph.join(" "), `p${blockIndex}`)));
201
+ if (placed) {
202
+ if (document.body.contains(entry)) return
203
+ rootObserver.disconnect()
204
+ root = undefined
205
+ placed = false
206
+ }
207
+ if (root === undefined) root = sidebarRoot()
208
+ if (root === undefined) return
209
+ placed = placeEntry(root, entry)
210
+ if (placed) rootObserver.observe(root, { childList: true, subtree: true })
211
+ }
212
+
213
+ var waitObserver = new MutationObserver(function () { tryPlace() })
214
+ waitObserver.observe(document.body, { childList: true, subtree: true })
215
+ var rootObserver = new MutationObserver(function () {
216
+ if (root === undefined || !root.isConnected) {
217
+ placed = false
218
+ tryPlace()
219
+ return
220
+ }
221
+ if (!root.contains(entry)) placed = placeEntry(root, entry)
222
+ })
223
+
224
+ var syncActive = function () {
225
+ if (controller.isOpen()) entry.dataset.active = 'true'
226
+ else delete entry.dataset.active
227
+ }
228
+ var unsubscribe = controller.subscribe(syncActive)
229
+ syncActive()
230
+ tryPlace()
231
+
232
+ return function () {
233
+ waitObserver.disconnect()
234
+ rootObserver.disconnect()
235
+ unsubscribe()
236
+ entry.remove()
590
237
  }
591
- return blocks;
592
238
  }
593
- /** The markdown renderer component. */
594
- function Markdown({ source }) {
595
- return (0, react.createElement)("div", { className: panel_module_css_default.markdown }, ...renderMarkdown(source));
239
+
240
+ /** The center column that hosts takeover panels. */
241
+ function conversationColumn() {
242
+ return document.querySelector('[data-pane="conversation"], [class*="centerCol"]') || undefined
596
243
  }
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}}`);
244
+
245
+ /** Call one host route. */
246
+ async function call(path, method) {
247
+ const response = await fetch(path, { method: method || 'GET', headers: { accept: 'application/json' } })
248
+ let payload = {}
249
+ try { payload = await response.json() } catch (error) { payload = {} }
250
+ if (!response.ok && payload.error === undefined) payload.error = 'HTTP ' + response.status
251
+ return payload
615
252
  }
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);
253
+
254
+ /** Build the panel body once; returns the parts the caller updates. */
255
+ function buildPanel(container) {
256
+ var panel = document.createElement('div')
257
+ panel.className = 'dsh-wc-panel'
258
+
259
+ var head = document.createElement('div')
260
+ head.className = 'dsh-wc-head'
261
+ var title = document.createElement('h2')
262
+ title.className = 'dsh-wc-title'
263
+ title.textContent = T.title
264
+ var chip = document.createElement('span')
265
+ chip.className = 'dsh-wc-chip'
266
+ var dot = document.createElement('span')
267
+ dot.className = 'dsh-wc-dot'
268
+ var chipText = document.createElement('span')
269
+ chipText.textContent = T.checking
270
+ chip.appendChild(dot)
271
+ chip.appendChild(chipText)
272
+ head.appendChild(title)
273
+ head.appendChild(chip)
274
+
275
+ var card = document.createElement('div')
276
+ card.className = 'dsh-wc-card'
277
+ var lede = document.createElement('p')
278
+ lede.className = 'dsh-wc-lede'
279
+ lede.textContent = T.lede
280
+ var url = document.createElement('p')
281
+ url.className = 'dsh-wc-url'
282
+ url.textContent = PAGE_URL
283
+ var button = document.createElement('button')
284
+ button.type = 'button'
285
+ button.className = 'dsh-wc-button'
286
+ button.textContent = T.open
287
+ var hint = document.createElement('p')
288
+ hint.className = 'dsh-wc-hint'
289
+ hint.textContent = T.hint
290
+ var result = document.createElement('p')
291
+ result.className = 'dsh-wc-result'
292
+ card.appendChild(lede)
293
+ card.appendChild(url)
294
+ card.appendChild(button)
295
+ card.appendChild(hint)
296
+ card.appendChild(result)
297
+
298
+ panel.appendChild(head)
299
+ panel.appendChild(card)
300
+ container.appendChild(panel)
301
+
302
+ var mark = function (state, text) {
303
+ dot.setAttribute('data-state', state)
304
+ chipText.textContent = text
305
+ }
306
+ var idle = function () {
307
+ button.disabled = false
308
+ button.textContent = T.open
309
+ }
310
+
311
+ var refresh = async function () {
853
312
  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 }));
313
+ var state = await call(API.state)
314
+ mark(state.appWindowOpen ? 'ok' : '', state.appWindowOpen ? T.opened : T.closed)
315
+ button.textContent = state.appWindowOpen ? T.reopen : T.open
861
316
  } catch (error) {
862
- showToast(String(error), true);
863
- } finally {
864
- setRecovering(false);
317
+ mark('bad', T.closed)
865
318
  }
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);
319
+ }
320
+
321
+ button.addEventListener('click', async function () {
322
+ button.disabled = true
323
+ button.textContent = T.busy
324
+ mark('busy', T.busy)
325
+ result.removeAttribute('data-state')
326
+ result.textContent = ''
875
327
  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;
328
+ var outcome = await call(API.open, 'POST')
329
+ if (outcome.ok) {
330
+ var via = String(outcome.via || '')
331
+ mark('ok', T.done)
332
+ result.textContent = via === 'system-browser'
333
+ ? (ZH ? '已交给系统默认浏览器打开。' : 'Handed to the system default browser.')
334
+ : (ZH ? '已打开应用窗口:' + via : 'Opened an app window: ' + via)
335
+ } else {
336
+ mark('bad', T.failed)
337
+ result.setAttribute('data-state', 'bad')
338
+ result.textContent = String(outcome.error || outcome.via || T.failed)
886
339
  }
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
340
  } catch (error) {
989
- showToast(fmt(tt("toast.delete.failed"), { error: String(error) }), true);
341
+ mark('bad', T.failed)
342
+ result.setAttribute('data-state', 'bad')
343
+ result.textContent = error instanceof Error ? error.message : String(error)
990
344
  }
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;
345
+ idle()
346
+ await refresh()
347
+ })
348
+
349
+ return { refresh: refresh }
1447
350
  }
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;
351
+
352
+ /** Mount the panel view into the center column and bind its visibility. */
353
+ function mountPanel(controller) {
354
+ var container
355
+ var panel
356
+
357
+ var ensure = function () {
358
+ if (container !== undefined) {
359
+ if (container.isConnected) return
360
+ container.remove()
361
+ container = undefined
362
+ panel = undefined
363
+ }
364
+ var column = conversationColumn()
365
+ if (column === undefined) return
366
+ container = document.createElement('div')
367
+ container.setAttribute(VIEW_ATTR, '')
368
+ container.setAttribute('data-dsh-plugin', 'webchat')
369
+ column.appendChild(container)
370
+ panel = buildPanel(container)
371
+ void panel.refresh()
372
+ }
373
+
374
+ var waitObserver = new MutationObserver(function () { ensure() })
375
+
376
+ var applyActive = function () {
377
+ if (controller.isOpen()) {
378
+ OTHER_ACTIVE_ATTRS.forEach(function (attr) { document.documentElement.removeAttribute(attr) })
379
+ document.documentElement.setAttribute(ACTIVE_ATTR, '')
380
+ document.dispatchEvent(new CustomEvent(ACTIVATE_EVENT, { detail: PANEL_NAME }))
381
+ ensure()
382
+ if (panel !== undefined) void panel.refresh()
383
+ } else {
384
+ document.documentElement.removeAttribute(ACTIVE_ATTR)
1465
385
  }
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
386
  }
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);
387
+ var onOtherActivate = function (event) {
388
+ var detail = event.detail
389
+ if ((detail === 'taskboard' || detail === 'ssh') && controller.isOpen()) controller.close()
390
+ }
391
+ var SIDEBAR_ROW_SELECTOR = '[class*="sessionRow"], [class*="projectRow"], [class*="searchResultRow"], [class*="searchResultWorkspace"], [class*="newSession"]'
392
+ var onClickSidebarRow = function (event) {
393
+ if (!controller.isOpen()) return
394
+ var target = event.target
395
+ if (target === null || target.closest === undefined) return
396
+ if (target.closest(SIDEBAR_ROW_SELECTOR) !== null) controller.close()
397
+ }
398
+
399
+ document.addEventListener('click', onClickSidebarRow, true)
400
+ document.addEventListener(ACTIVATE_EVENT, onOtherActivate)
401
+ var unsubscribe = controller.subscribe(applyActive)
402
+ applyActive()
403
+ ensure()
404
+ waitObserver.observe(document.body, { childList: true, subtree: true })
405
+
406
+ return function () {
407
+ document.removeEventListener('click', onClickSidebarRow, true)
408
+ document.removeEventListener(ACTIVATE_EVENT, onOtherActivate)
409
+ waitObserver.disconnect()
410
+ unsubscribe()
411
+ document.documentElement.removeAttribute(ACTIVE_ATTR)
412
+ if (container !== undefined) container.remove()
413
+ container = undefined
414
+ panel = undefined
1564
415
  }
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
416
  }
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
- */
417
+
418
+ /** Mount the entry and the panel. */
1681
419
  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");
420
+ ensureStyle()
421
+ var controller = createController()
422
+ var disposers = [mountSidebarEntry(controller), mountPanel(controller)]
423
+ ctx.effect(function () {
424
+ return function () {
425
+ disposers.splice(0).forEach(function (dispose) { dispose() })
426
+ }
427
+ }, 'dsh-webchat: ui mounts')
1717
428
  }
1718
- //#endregion
1719
- exports.apply = apply;
1720
- exports.inject = inject;
1721
- return module.exports;
1722
- }
1723
- });
1724
429
 
1725
- //# sourceMappingURL=client.js.map
430
+ exports.apply = apply
431
+ exports.inject = []
432
+ return module.exports
433
+ },
434
+ })