@jaychang1989/dsh-webchat 0.5.2 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +7 -4
- package/README.md +8 -5
- package/lib/client.js +292 -392
- package/package.json +6 -2
package/README.en.md
CHANGED
|
@@ -19,6 +19,8 @@ But the desktop shell keeps a door open for **approved browser guests**: the ren
|
|
|
19
19
|
|
|
20
20
|
So the page is **not an iframe** — it is a native guest owned by the host, which is why `frame-ancestors` does not apply to it. It also means the desktop app is what makes it possible.
|
|
21
21
|
|
|
22
|
+
The sidebar row and the page are both standard DSH **slots** (`sidebar.panellist` and `main`, joined by one shared id), so the shell owns the button and the panel switching. That is why this plugin injects nothing into anyone else's DOM and never has to guess when to step aside.
|
|
23
|
+
|
|
22
24
|
Where that bridge is absent (a plain `dsh web` profile) the plugin **falls back** to opening a window, so the entry always does something.
|
|
23
25
|
|
|
24
26
|
## Install
|
|
@@ -42,7 +44,7 @@ dsh plugin --profile desktop add github:jaychang1989/dsh-webchat
|
|
|
42
44
|
1. Click the "DeepSeek 网页 / DeepSeek Web" entry — the page loads in the center column; there is no second click.
|
|
43
45
|
2. Sign in to DeepSeek once; that holds for the rest of the run.
|
|
44
46
|
|
|
45
|
-
Click again to collapse the panel.
|
|
47
|
+
Click again to collapse the panel. Switching to another panel (Plugins, Automation Tasks, the task board, a session …) only hides the guest, never unmounts it, so coming back neither reloads the page nor drops the session.
|
|
46
48
|
|
|
47
49
|
## Requirements
|
|
48
50
|
|
|
@@ -60,9 +62,10 @@ Click again to collapse the panel. The guest stays mounted while the panel is cl
|
|
|
60
62
|
|
|
61
63
|
| Symptom | Cause / fix |
|
|
62
64
|
| --- | --- |
|
|
63
|
-
|
|
|
65
|
+
| No "DeepSeek Web" row in the sidebar | Check that `@jaychang1989/dsh-webchat` is in the profile's `dsh.profile.bundles`, then restart the desktop app |
|
|
64
66
|
| The center column says "载入失败:…" | The host refused the guest (the bridge threw). The text carries the host's reason |
|
|
65
|
-
|
|
|
67
|
+
| The row opens a browser window instead of a panel | This renderer had no `dshDesktop.browser` (a plain web profile, for instance), so the fallback ran |
|
|
68
|
+
| The row opens but the center column is blank | The panel fills the cell the shell allocates; in a very small window, or with the sidebar dragged extremely narrow, that cell can have no area |
|
|
66
69
|
| It asks for a login again after a restart | See the limitations above — it is the host's partition behaviour |
|
|
67
70
|
| The page shows "Abnormal usage environment" | DeepSeek's front end checks `navigator.userAgent` for the string `electron` — which the desktop default carries — and then recommends its official product. Since 0.5.2 the guest presents a plain Chrome user agent and the dialog no longer appears |
|
|
68
71
|
|
|
@@ -72,7 +75,7 @@ Click again to collapse the panel. The guest stays mounted while the panel is cl
|
|
|
72
75
|
node --test
|
|
73
76
|
```
|
|
74
77
|
|
|
75
|
-
|
|
78
|
+
Nineteen cases. The host half is driven through a fake context and fake request/response objects; the browser half really executes against a thin React test double plus a DOM stand-in: it checks that the plugin registers its nav row and its page under the slot contract (one shared id), that the label follows the language, that the icon honours the size the shell asks for, and the whole guest lifecycle — reserving a lease, building the webview the way the host requires (`about:blank#<lease>`), setting the user agent before navigating on `dom-ready`, **hiding the guest without detaching it when the panel unmounts and reusing it on remount**, and releasing the lease on plugin disposal; without a bridge it checks the window fallback and its reporting.
|
|
76
79
|
|
|
77
80
|
There is no build step — `lib/index.js` and `lib/client.js` are the hand-written runtime, and the package has **zero runtime dependencies**. The guest mechanism is written up in [MAINTAINING.md](./MAINTAINING.md) (Chinese).
|
|
78
81
|
|
package/README.md
CHANGED
|
@@ -19,6 +19,8 @@ iframe 和 `<webview>` 常规路径确实都被封死了:
|
|
|
19
19
|
|
|
20
20
|
所以这个页面**不是嵌在 iframe 里**,而是宿主的原生访客——`frame-ancestors` 管不到它。这也解释了为什么它必须由 DSH 桌面端承载。
|
|
21
21
|
|
|
22
|
+
而「侧边栏那一行」和「中栏那一页」都是 DSH 的**标准槽位**(`sidebar.panellist` 与 `main`,同一个 id 关联),按钮和面板切换都由 shell 自己掌管——所以这个插件不往别人的 DOM 里塞东西,也不需要去猜什么时候该让位。
|
|
23
|
+
|
|
22
24
|
在没有该桥接的环境(纯 `dsh web`,非桌面端)会**自动降级**为打开一个窗口,保证入口永远有反应。
|
|
23
25
|
|
|
24
26
|
## 安装
|
|
@@ -42,7 +44,7 @@ dsh plugin --profile desktop add github:jaychang1989/dsh-webchat
|
|
|
42
44
|
1. 点侧边栏的「DeepSeek 网页」入口 —— 页面直接在中栏载入,不需要第二次点击;
|
|
43
45
|
2. 在里面登录一次 DeepSeek,当前这次运行内一直有效。
|
|
44
46
|
|
|
45
|
-
|
|
47
|
+
再点一次入口收起面板。切换去别的面板(插件、自动化任务、任务看板、会话……)时访客只是被隐藏、从不被卸载,所以切回来不会重新加载、也不会掉登录。
|
|
46
48
|
|
|
47
49
|
## 环境要求
|
|
48
50
|
|
|
@@ -60,9 +62,10 @@ dsh plugin --profile desktop add github:jaychang1989/dsh-webchat
|
|
|
60
62
|
|
|
61
63
|
| 现象 | 原因 / 处理 |
|
|
62
64
|
| --- | --- |
|
|
63
|
-
|
|
|
65
|
+
| 侧边栏没出现「DeepSeek 网页」这一行 | 确认 profile 的 `dsh.profile.bundles` 里有 `@jaychang1989/dsh-webchat`,然后重启桌面端 |
|
|
64
66
|
| 中栏提示「载入失败:…」 | 宿主拒绝了访客(桥接返回异常)。文本里带着宿主给的原因 |
|
|
65
|
-
|
|
|
67
|
+
| 这一行点了但中栏没有页面,反而弹出浏览器窗口 | 说明当前渲染进程拿不到 `dshDesktop.browser`(例如在纯 web 环境),插件走了降级路径 |
|
|
68
|
+
| 这一行点了但中栏是空白 | 面板显示的空间是 shell 分配的那个格子;若窗口极小或侧栏被拖到极窄,格子可能没有面积 |
|
|
66
69
|
| 重启后要求重新登录 | 见上面的「已知限制」,属于宿主分区机制 |
|
|
67
70
|
| 页面弹出「使用环境异常」 | DeepSeek 前端会检查 `navigator.userAgent` 里是否含 `electron`(桌面端默认 UA 就含),命中就提示"建议使用官方产品"。0.5.2 起访客改用普通 Chrome UA,不再触发 |
|
|
68
71
|
|
|
@@ -72,9 +75,9 @@ dsh plugin --profile desktop add github:jaychang1989/dsh-webchat
|
|
|
72
75
|
node --test
|
|
73
76
|
```
|
|
74
77
|
|
|
75
|
-
|
|
78
|
+
19 个用例。宿主半区用假 context 与假 request/response 驱动;浏览器半区用一层薄的 React 测试替身 + DOM 桩**真实执行**:验证它按槽位契约注册导航行与页面(同一个 id)、标签随语言变化、图标遵循 shell 要求的尺寸,以及访客生命周期——申请租约、按宿主约定生成 `about:blank#<lease>` 的 webview、`dom-ready` 后先改 UA 再导航、**卸载只隐藏不卸载访客、重挂复用同一个**、插件卸载时释放租约;无桥接环境下验证降级为请求宿主开窗并如实反馈。
|
|
76
79
|
|
|
77
|
-
没有构建步骤——`lib/index.js` 与 `lib/client.js`
|
|
80
|
+
没有构建步骤——`lib/index.js` 与 `lib/client.js` 就是手写的运行时代码(React 取自浏览器的模块表),包内**零运行时依赖**。访客机制与槽位契约见 [MAINTAINING.md](./MAINTAINING.md)。
|
|
78
81
|
|
|
79
82
|
## 来源与许可
|
|
80
83
|
|
package/lib/client.js
CHANGED
|
@@ -1,92 +1,91 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* dsh-webchat — browser half.
|
|
3
3
|
*
|
|
4
|
-
* One sidebar entry that
|
|
5
|
-
* the DSH window
|
|
6
|
-
* views (Automation Tasks and friends).
|
|
4
|
+
* One sidebar entry that renders the official chat.deepseek.com web app inside
|
|
5
|
+
* the DSH window, filling the center column like any other full-page view.
|
|
7
6
|
*
|
|
8
|
-
*
|
|
9
|
-
* renderer asks `globalThis.dshDesktop.browser` for a reservation and then
|
|
10
|
-
* attaches a `<webview>` whose `src` is `about:blank#<lease>` and whose
|
|
11
|
-
* `partition` matches — the shell approves exactly that element and assigns the
|
|
12
|
-
* real web preferences, so the page is not subject to `frame-ancestors` the way
|
|
13
|
-
* an iframe would be. This is the same mechanism the built-in side-card browser
|
|
14
|
-
* uses; see MAINTAINING.md.
|
|
7
|
+
* Both halves of that are **slots**, so the shell owns them:
|
|
15
8
|
*
|
|
16
|
-
*
|
|
17
|
-
* the
|
|
9
|
+
* - `sidebar.panellist` gets an icon and a label; the sidebar owns the button
|
|
10
|
+
* and selects the main panel whose id matches, which is why this plugin no
|
|
11
|
+
* longer injects a row into someone else's DOM;
|
|
12
|
+
* - `main` (keyed by the same id) gets the page. The layout renders only the
|
|
13
|
+
* selected key, so switching panels is the shell's business and this panel
|
|
14
|
+
* never has to hide another page or ask to be closed.
|
|
18
15
|
*
|
|
19
|
-
*
|
|
16
|
+
* The page itself is the host's native browser guest, not an iframe: the
|
|
17
|
+
* renderer asks `globalThis.dshDesktop.browser` for a reservation and attaches a
|
|
18
|
+
* `<webview>` whose `src` is `about:blank#<lease>` and whose `partition`
|
|
19
|
+
* matches, which the shell's `will-attach-webview` approves and rewrites with
|
|
20
|
+
* the real web preferences. That is the same channel the built-in side-card
|
|
21
|
+
* browser uses, and it is why `frame-ancestors 'none'` does not apply.
|
|
22
|
+
*
|
|
23
|
+
* The guest element deliberately lives outside the React tree, in a container
|
|
24
|
+
* this plugin owns at the document root. A `<webview>` is destroyed when it
|
|
25
|
+
* leaves the document, so a guest owned by the slot would be torn down and
|
|
26
|
+
* reloaded every time the user looked at another panel. Instead the slot hands
|
|
27
|
+
* this plugin a measured area, and the persistent guest is positioned onto it
|
|
28
|
+
* while this panel is the selected one, and hidden when it is not.
|
|
29
|
+
*
|
|
30
|
+
* React comes from the browser module table; everything else is plain DOM.
|
|
20
31
|
*/
|
|
21
32
|
window.__ModuleLoader__.load({
|
|
22
33
|
id: "@jaychang1989/dsh-webchat",
|
|
23
|
-
factory: () => {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
|
|
29
|
-
/** The page
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
var FRAME_CLASS = 'dsh-wc-frame'
|
|
47
|
-
|
|
48
|
-
var ZH = typeof navigator === 'object' && typeof navigator.language === 'string'
|
|
49
|
-
&& navigator.language.toLowerCase().indexOf('zh') === 0
|
|
34
|
+
factory: (require) => {
|
|
35
|
+
const React = require("react")
|
|
36
|
+
const h = React.createElement
|
|
37
|
+
|
|
38
|
+
/** This plugin's panel id: the sidebar row and the main cell share it. */
|
|
39
|
+
const PANEL_ID = "webchat"
|
|
40
|
+
/** The page this plugin exists to show. */
|
|
41
|
+
const PAGE_URL = "https://chat.deepseek.com/"
|
|
42
|
+
/** Storage identity: one guest partition per identity, per app run. */
|
|
43
|
+
const STORAGE_IDENTITY = "dsh-webchat"
|
|
44
|
+
/** Host route used by the window fallback. */
|
|
45
|
+
const OPEN_ROUTE = "/api/dsh-webchat/open"
|
|
46
|
+
|
|
47
|
+
const STYLE_ID = "dsh-webchat-style"
|
|
48
|
+
const OVERLAY_ATTR = "data-dsh-webchat-overlay"
|
|
49
|
+
|
|
50
|
+
/** Locale at read time, so the label thunk follows the app's language. */
|
|
51
|
+
function isChinese() {
|
|
52
|
+
const tag = document.documentElement.getAttribute("lang")
|
|
53
|
+
|| (typeof navigator === "object" ? navigator.language : "")
|
|
54
|
+
|| ""
|
|
55
|
+
return String(tag).toLowerCase().indexOf("zh") === 0
|
|
56
|
+
}
|
|
50
57
|
|
|
51
|
-
/**
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
58
|
+
/** Every visible string this plugin owns. */
|
|
59
|
+
function copy() {
|
|
60
|
+
return isChinese()
|
|
61
|
+
? {
|
|
62
|
+
label: "DeepSeek 网页",
|
|
63
|
+
failed: "载入失败",
|
|
64
|
+
fallbackNote: "这个 Harness 没有提供原生浏览器访客(桌面端才有),无法在窗口内显示官方页面。",
|
|
65
|
+
fallbackAction: "在窗口中打开",
|
|
66
|
+
fallbackOpened: "已打开 DeepSeek 网页",
|
|
67
|
+
fallbackHanded: "已交给系统默认浏览器打开",
|
|
68
|
+
fallbackFailed: "打开失败",
|
|
69
|
+
}
|
|
70
|
+
: {
|
|
71
|
+
label: "DeepSeek Web",
|
|
72
|
+
failed: "Could not load it",
|
|
73
|
+
fallbackNote: "This Harness provides no native browser guest (only the desktop app does), so the official page cannot render in the window.",
|
|
74
|
+
fallbackAction: "Open it in a window",
|
|
75
|
+
fallbackOpened: "DeepSeek Web is open",
|
|
76
|
+
fallbackHanded: "Handed to the system default browser",
|
|
77
|
+
fallbackFailed: "Could not open",
|
|
78
|
+
}
|
|
79
|
+
}
|
|
69
80
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
"[
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
"
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
'.dsh-wc-message{margin:auto;max-width:460px;padding:22px;text-align:center;font-size:13px;line-height:1.65;color:var(--dsw-alias-label-secondary);font-family:var(--dsw-font-family)}',
|
|
79
|
-
'[' + 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}',
|
|
80
|
-
'[' + ENTRY_ATTR + ']:hover{background:var(--dsw-specific-sidebar-nav-item-hover);color:var(--dsw-alias-label-primary)}',
|
|
81
|
-
'[' + ENTRY_ATTR + '][data-active]{background:var(--dsw-specific-sidebar-nav-item-active);color:var(--dsw-alias-label-primary);font-weight:600}',
|
|
82
|
-
'[' + ENTRY_ATTR + '][disabled]{opacity:.55;cursor:progress}',
|
|
83
|
-
'[' + ENTRY_ATTR + '] [data-part=icon]{display:inline-flex;align-items:center;justify-content:center;flex:none}',
|
|
84
|
-
'[' + ENTRY_ATTR + '] [data-part=label]{overflow:hidden;text-overflow:ellipsis}',
|
|
85
|
-
'[data-dsh-frame][data-sidebar-collapsed] [' + ENTRY_ATTR + ']{justify-content:center;padding:0}',
|
|
86
|
-
'[data-dsh-frame][data-sidebar-collapsed] [' + ENTRY_ATTR + '] [data-part=label]{display:none}',
|
|
87
|
-
'[' + TOAST_ATTR + ']{position:fixed;right:20px;bottom:20px;z-index:200;max-width:420px;padding:10px 14px;border-radius:10px;background:var(--dsw-alias-bg-elevated);border:1px solid var(--dsw-alias-border-l2);box-shadow:0 8px 24px rgba(0,0,0,.18);font-size:13px;color:var(--dsw-alias-label-primary);overflow-wrap:anywhere}',
|
|
88
|
-
'[' + TOAST_ATTR + '][data-state=bad]{border-color:var(--dsw-alias-state-danger,#ef4444);color:var(--dsw-alias-state-danger,#ef4444)}',
|
|
89
|
-
].join('\n')
|
|
81
|
+
const CSS = [
|
|
82
|
+
".dsh-wc-host{width:100%;height:100%;min-width:0;min-height:0;display:flex;align-items:center;justify-content:center}",
|
|
83
|
+
"[" + OVERLAY_ATTR + "]{position:fixed;display:none;z-index:40}",
|
|
84
|
+
"[" + OVERLAY_ATTR + "] webview{-webkit-app-region:no-drag;border:0;flex:auto;width:100%;min-width:0;height:100%;min-height:0;display:flex}",
|
|
85
|
+
".dsh-wc-message{max-width:460px;padding:22px;text-align:center;font-size:13px;line-height:1.65;color:var(--dsw-alias-label-secondary);font-family:var(--dsw-font-family)}",
|
|
86
|
+
".dsh-wc-action{margin-top:14px;padding:6px 14px;border-radius:8px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-elevated);color:var(--dsw-alias-label-primary);cursor:pointer;font-size:13px;font-family:var(--dsw-font-family)}",
|
|
87
|
+
".dsh-wc-action[disabled]{opacity:.55;cursor:progress}",
|
|
88
|
+
].join("\n")
|
|
90
89
|
|
|
91
90
|
/**
|
|
92
91
|
* The desktop shell's browser bridge, when this renderer is the desktop
|
|
@@ -94,376 +93,277 @@ window.__ModuleLoader__.load({
|
|
|
94
93
|
* version, so the window fallback takes over.
|
|
95
94
|
*/
|
|
96
95
|
const carrier = globalThis.dshDesktop
|
|
97
|
-
const
|
|
96
|
+
const bridge = carrier !== null && typeof carrier === "object" && carrier.protocolVersion === 1
|
|
98
97
|
? carrier.browser
|
|
99
98
|
: undefined
|
|
100
99
|
|
|
100
|
+
/**
|
|
101
|
+
* The only state that outlives a panel mount. `container` and `element`
|
|
102
|
+
* stay in the document for the whole app run so the guest is never torn
|
|
103
|
+
* down; the slot only decides where they are shown.
|
|
104
|
+
*/
|
|
105
|
+
let guest = null
|
|
106
|
+
/** In-flight acquisition, so two mounts cannot reserve two guests. */
|
|
107
|
+
let acquiring = null
|
|
108
|
+
/** Last acquisition failure, if any. */
|
|
109
|
+
let failure = ""
|
|
110
|
+
|
|
111
|
+
/** Render any thrown value as a message. */
|
|
112
|
+
function messageOf(error) {
|
|
113
|
+
return error instanceof Error ? error.message : String(error)
|
|
114
|
+
}
|
|
115
|
+
|
|
101
116
|
/** Inject the stylesheet once. */
|
|
102
117
|
function ensureStyle() {
|
|
103
118
|
if (document.getElementById(STYLE_ID) !== null) return
|
|
104
|
-
|
|
119
|
+
const tag = document.createElement("style")
|
|
105
120
|
tag.id = STYLE_ID
|
|
106
121
|
tag.textContent = CSS
|
|
107
122
|
document.head.appendChild(tag)
|
|
108
123
|
}
|
|
109
124
|
|
|
110
|
-
/**
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
* chat.deepseek.com greets an `electron` token in the user agent with a
|
|
115
|
-
* "使用环境异常 / Abnormal usage environment" dialog, and its dismissal is
|
|
116
|
-
* stored per guest partition — which is process-lifetime here, so it
|
|
117
|
-
* would come back on every restart. Its check is literally
|
|
118
|
-
* `navigator.userAgent.toLowerCase().includes("electron")` (the other
|
|
119
|
-
* arm, `window.process.type`, cannot fire in a sandboxed, isolated
|
|
120
|
-
* guest), so not advertising Electron is what keeps the page clean.
|
|
121
|
-
* @returns a Chrome user agent, or '' when this renderer reports none.
|
|
122
|
-
*/
|
|
123
|
-
function cleanUserAgent() {
|
|
124
|
-
var ua = typeof navigator === 'object' && typeof navigator.userAgent === 'string' ? navigator.userAgent : ''
|
|
125
|
-
var platform = (ua.match(/^Mozilla\/5\.0 \([^)]*\)/) || [])[0]
|
|
126
|
-
var chrome = (ua.match(/Chrome\/([0-9]+)/) || [])[1]
|
|
127
|
-
if (platform === undefined || chrome === undefined) return ''
|
|
128
|
-
return platform + ' AppleWebKit/537.36 (KHTML, like Gecko) Chrome/' + chrome + '.0.0.0 Safari/537.36'
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
/** Show one transient message in the corner. */
|
|
132
|
-
function toast(text, state) {
|
|
133
|
-
var previous = document.querySelector('[' + TOAST_ATTR + ']')
|
|
134
|
-
if (previous !== null) previous.remove()
|
|
135
|
-
var box = document.createElement('div')
|
|
136
|
-
box.setAttribute(TOAST_ATTR, '')
|
|
137
|
-
if (state !== undefined) box.setAttribute('data-state', state)
|
|
125
|
+
/** A centred message, optionally with one action button. */
|
|
126
|
+
function messageBox(text, action) {
|
|
127
|
+
const box = document.createElement("div")
|
|
128
|
+
box.className = "dsh-wc-message"
|
|
138
129
|
box.textContent = text
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
const response = await fetch(OPEN_ROUTE, { method: 'POST', headers: { accept: 'application/json' } })
|
|
148
|
-
let payload = {}
|
|
149
|
-
try { payload = await response.json() } catch (error) { payload = {} }
|
|
150
|
-
if (!response.ok && payload.error === undefined) payload.error = 'HTTP ' + response.status
|
|
151
|
-
return payload
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
/** Tiny observable for the panel's open state. */
|
|
155
|
-
function createController() {
|
|
156
|
-
var open = false
|
|
157
|
-
var listeners = new Set()
|
|
158
|
-
var notify = function () {
|
|
159
|
-
listeners.forEach(function (listener) {
|
|
130
|
+
if (action !== undefined) {
|
|
131
|
+
const button = document.createElement("button")
|
|
132
|
+
button.type = "button"
|
|
133
|
+
button.className = "dsh-wc-action"
|
|
134
|
+
button.textContent = action.label
|
|
135
|
+
button.addEventListener("click", async function () {
|
|
136
|
+
if (button.disabled) return
|
|
137
|
+
button.disabled = true
|
|
160
138
|
try {
|
|
161
|
-
|
|
162
|
-
}
|
|
163
|
-
|
|
139
|
+
await action.run(button)
|
|
140
|
+
} finally {
|
|
141
|
+
button.disabled = false
|
|
164
142
|
}
|
|
165
143
|
})
|
|
144
|
+
box.appendChild(button)
|
|
166
145
|
}
|
|
167
|
-
return
|
|
168
|
-
isOpen: function () { return open },
|
|
169
|
-
subscribe: function (listener) {
|
|
170
|
-
listeners.add(listener)
|
|
171
|
-
return function () { listeners.delete(listener) }
|
|
172
|
-
},
|
|
173
|
-
toggle: function () { open = !open; notify() },
|
|
174
|
-
close: function () { if (!open) return; open = false; notify() },
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
/** The sidebar shell root, or undefined while it is not mounted. */
|
|
179
|
-
function sidebarRoot() {
|
|
180
|
-
var column = document.querySelector('[data-pane="sidebar"], [class*="sidebarCol"]')
|
|
181
|
-
if (column === null) return undefined
|
|
182
|
-
var logoRow = column.querySelector('[class*="logoRow"]')
|
|
183
|
-
var owner = logoRow === null ? null : logoRow.parentElement
|
|
184
|
-
return owner !== null && owner !== undefined ? owner : (column.firstElementChild || undefined)
|
|
146
|
+
return box
|
|
185
147
|
}
|
|
186
148
|
|
|
187
|
-
/**
|
|
188
|
-
function
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
149
|
+
/** Ask the host half to open the page in a window. */
|
|
150
|
+
async function openInWindow(button) {
|
|
151
|
+
const text = copy()
|
|
152
|
+
const note = document.createElement("div")
|
|
153
|
+
note.className = "dsh-wc-message"
|
|
154
|
+
const settle = (message) => {
|
|
155
|
+
note.textContent = message
|
|
156
|
+
const parent = button.parentElement
|
|
157
|
+
if (note.parentElement === null && parent !== null) parent.appendChild(note)
|
|
193
158
|
}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
entry.setAttribute('data-dsh-plugin', 'webchat')
|
|
203
|
-
entry.setAttribute('data-dsh-part', 'sidebar-entry')
|
|
204
|
-
entry.setAttribute('aria-label', T.label)
|
|
205
|
-
entry.setAttribute('title', T.tooltip)
|
|
206
|
-
var icon = document.createElement('span')
|
|
207
|
-
icon.setAttribute('data-part', 'icon')
|
|
208
|
-
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>'
|
|
209
|
-
var label = document.createElement('span')
|
|
210
|
-
label.setAttribute('data-part', 'label')
|
|
211
|
-
label.textContent = T.label
|
|
212
|
-
entry.appendChild(icon)
|
|
213
|
-
entry.appendChild(label)
|
|
214
|
-
entry.addEventListener('click', onClick)
|
|
215
|
-
return entry
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
/** Re-insert the entry after the New Session row when React displaces it. */
|
|
219
|
-
function placeEntry(root, entry) {
|
|
220
|
-
var button = newSessionButton(root)
|
|
221
|
-
if (button === undefined) return false
|
|
222
|
-
if (entry.parentElement === root) return true
|
|
223
|
-
var row = button.closest('[class*="logoRow"]')
|
|
224
|
-
var base = row !== null && row.parentElement === root ? row : button
|
|
225
|
-
var family = Array.prototype.filter.call(root.children, function (el) {
|
|
226
|
-
return el.matches && el.matches('[' + ENTRY_ATTR + '], [data-dsh-taskboard-entry], [data-dsh-ssh-entry]')
|
|
227
|
-
})
|
|
228
|
-
var anchor = family.length > 0 ? family[family.length - 1].nextElementSibling : base.nextElementSibling
|
|
229
|
-
root.insertBefore(entry, anchor)
|
|
230
|
-
return true
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
/** Mount the sidebar entry with its self-healing observers. */
|
|
234
|
-
function mountSidebarEntry(controller, entry) {
|
|
235
|
-
var root
|
|
236
|
-
var placed = false
|
|
237
|
-
|
|
238
|
-
var tryPlace = function () {
|
|
239
|
-
if (root !== undefined && !root.isConnected) {
|
|
240
|
-
rootObserver.disconnect()
|
|
241
|
-
root = undefined
|
|
242
|
-
placed = false
|
|
243
|
-
}
|
|
244
|
-
if (placed) {
|
|
245
|
-
if (document.body.contains(entry)) return
|
|
246
|
-
rootObserver.disconnect()
|
|
247
|
-
root = undefined
|
|
248
|
-
placed = false
|
|
249
|
-
}
|
|
250
|
-
if (root === undefined) root = sidebarRoot()
|
|
251
|
-
if (root === undefined) return
|
|
252
|
-
placed = placeEntry(root, entry)
|
|
253
|
-
if (placed) rootObserver.observe(root, { childList: true, subtree: true })
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
var waitObserver = new MutationObserver(function () { tryPlace() })
|
|
257
|
-
waitObserver.observe(document.body, { childList: true, subtree: true })
|
|
258
|
-
var rootObserver = new MutationObserver(function () {
|
|
259
|
-
if (root === undefined || !root.isConnected) {
|
|
260
|
-
placed = false
|
|
261
|
-
tryPlace()
|
|
262
|
-
return
|
|
159
|
+
try {
|
|
160
|
+
const response = await fetch(OPEN_ROUTE, { method: "POST", headers: { accept: "application/json" } })
|
|
161
|
+
let payload = {}
|
|
162
|
+
try { payload = await response.json() } catch (error) { payload = {} }
|
|
163
|
+
if (payload.ok === true) {
|
|
164
|
+
settle(String(payload.via) === "system-browser" ? text.fallbackHanded : text.fallbackOpened)
|
|
165
|
+
} else {
|
|
166
|
+
settle(text.fallbackFailed + ":" + String(payload.error || payload.via || "HTTP " + response.status))
|
|
263
167
|
}
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
var syncActive = function () {
|
|
268
|
-
if (controller.isOpen()) entry.dataset.active = 'true'
|
|
269
|
-
else delete entry.dataset.active
|
|
270
|
-
}
|
|
271
|
-
var unsubscribe = controller.subscribe(syncActive)
|
|
272
|
-
syncActive()
|
|
273
|
-
tryPlace()
|
|
274
|
-
|
|
275
|
-
return function () {
|
|
276
|
-
waitObserver.disconnect()
|
|
277
|
-
rootObserver.disconnect()
|
|
278
|
-
unsubscribe()
|
|
279
|
-
entry.remove()
|
|
168
|
+
} catch (error) {
|
|
169
|
+
settle(text.fallbackFailed + ":" + messageOf(error))
|
|
280
170
|
}
|
|
281
171
|
}
|
|
282
172
|
|
|
283
|
-
/** The center column that hosts full-page views. */
|
|
284
|
-
function conversationColumn() {
|
|
285
|
-
return document.querySelector('[data-pane="conversation"], [class*="centerCol"]') || undefined
|
|
286
|
-
}
|
|
287
|
-
|
|
288
173
|
/**
|
|
289
|
-
*
|
|
174
|
+
* A plain Chrome user agent for the guest, derived from this renderer's
|
|
175
|
+
* own so the Chrome major version stays truthful.
|
|
290
176
|
*
|
|
291
|
-
*
|
|
292
|
-
*
|
|
293
|
-
*
|
|
294
|
-
*
|
|
295
|
-
*
|
|
177
|
+
* chat.deepseek.com greets an `electron` token in the user agent with a
|
|
178
|
+
* "使用环境异常 / Abnormal usage environment" dialog, and dismissing it is
|
|
179
|
+
* stored per guest partition — process-lifetime here, so it would come
|
|
180
|
+
* back on every restart. Its check is literally
|
|
181
|
+
* `navigator.userAgent.toLowerCase().includes("electron")`, so not
|
|
182
|
+
* advertising Electron is what keeps the page clean.
|
|
183
|
+
* @returns a Chrome user agent, or '' when this renderer reports none.
|
|
296
184
|
*/
|
|
297
|
-
function
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
185
|
+
function cleanUserAgent() {
|
|
186
|
+
const ua = typeof navigator === "object" && typeof navigator.userAgent === "string" ? navigator.userAgent : ""
|
|
187
|
+
const platform = (ua.match(/^Mozilla\/5\.0 \([^)]*\)/) || [])[0]
|
|
188
|
+
const chrome = (ua.match(/Chrome\/([0-9]+)/) || [])[1]
|
|
189
|
+
if (platform === undefined || chrome === undefined) return ""
|
|
190
|
+
return platform + " AppleWebKit/537.36 (KHTML, like Gecko) Chrome/" + chrome + ".0.0.0 Safari/537.36"
|
|
191
|
+
}
|
|
302
192
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
193
|
+
/**
|
|
194
|
+
* The document-root container that keeps the guest alive.
|
|
195
|
+
*/
|
|
196
|
+
function overlayContainer() {
|
|
197
|
+
const existing = document.querySelector("[" + OVERLAY_ATTR + "]")
|
|
198
|
+
if (existing !== null && existing !== undefined) return existing
|
|
199
|
+
const box = document.createElement("div")
|
|
200
|
+
box.setAttribute(OVERLAY_ATTR, "")
|
|
201
|
+
document.body.appendChild(box)
|
|
202
|
+
return box
|
|
203
|
+
}
|
|
309
204
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
205
|
+
/**
|
|
206
|
+
* Reserve a guest and attach its webview, once per app run.
|
|
207
|
+
* @returns a promise resolving once the attempt settled.
|
|
208
|
+
*/
|
|
209
|
+
function ensureGuest() {
|
|
210
|
+
if (guest !== null || bridge === undefined) return Promise.resolve()
|
|
211
|
+
if (acquiring !== null) return acquiring
|
|
212
|
+
acquiring = (async () => {
|
|
313
213
|
try {
|
|
314
|
-
const reservation = await
|
|
315
|
-
if (reservation === null || typeof reservation !==
|
|
316
|
-
|| typeof reservation.lease !==
|
|
317
|
-
throw new Error(
|
|
214
|
+
const reservation = await bridge.acquire(STORAGE_IDENTITY)
|
|
215
|
+
if (reservation === null || typeof reservation !== "object"
|
|
216
|
+
|| typeof reservation.lease !== "string" || typeof reservation.partition !== "string") {
|
|
217
|
+
throw new Error("the desktop browser bridge returned no reservation")
|
|
318
218
|
}
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
element.
|
|
322
|
-
element.setAttribute(
|
|
323
|
-
element.setAttribute(
|
|
324
|
-
element.setAttribute('allowpopups', '')
|
|
325
|
-
element.setAttribute('src', 'about:blank#' + reservation.lease)
|
|
219
|
+
const element = document.createElement("webview")
|
|
220
|
+
element.setAttribute("name", reservation.lease)
|
|
221
|
+
element.setAttribute("partition", reservation.partition)
|
|
222
|
+
element.setAttribute("allowpopups", "")
|
|
223
|
+
element.setAttribute("src", "about:blank#" + reservation.lease)
|
|
326
224
|
const userAgent = cleanUserAgent()
|
|
327
|
-
if (userAgent !==
|
|
328
|
-
element.addEventListener(
|
|
329
|
-
// The guest exists by now
|
|
330
|
-
//
|
|
331
|
-
if (userAgent !==
|
|
225
|
+
if (userAgent !== "") element.setAttribute("useragent", userAgent)
|
|
226
|
+
element.addEventListener("dom-ready", function () {
|
|
227
|
+
// The guest exists by now and nothing has been navigated yet,
|
|
228
|
+
// so this is the user agent the page actually loads with.
|
|
229
|
+
if (userAgent !== "") {
|
|
332
230
|
try {
|
|
333
231
|
element.setUserAgent(userAgent)
|
|
334
232
|
} catch (error) {
|
|
335
|
-
console.warn(
|
|
233
|
+
console.warn("[dsh-webchat] could not set the guest user agent:", error)
|
|
336
234
|
}
|
|
337
235
|
}
|
|
338
236
|
element.loadURL(PAGE_URL).catch(function (error) {
|
|
339
|
-
|
|
237
|
+
failure = messageOf(error)
|
|
340
238
|
})
|
|
341
239
|
}, { once: true })
|
|
342
|
-
|
|
240
|
+
const container = overlayContainer()
|
|
343
241
|
container.appendChild(element)
|
|
242
|
+
guest = { lease: reservation.lease, element, container }
|
|
243
|
+
failure = ""
|
|
344
244
|
} catch (error) {
|
|
345
|
-
|
|
346
|
-
|
|
245
|
+
failure = messageOf(error)
|
|
246
|
+
} finally {
|
|
247
|
+
acquiring = null
|
|
347
248
|
}
|
|
348
|
-
}
|
|
249
|
+
})()
|
|
250
|
+
return acquiring
|
|
251
|
+
}
|
|
349
252
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
if (column === undefined) return
|
|
359
|
-
container = document.createElement('div')
|
|
360
|
-
container.setAttribute(VIEW_ATTR, '')
|
|
361
|
-
container.setAttribute('data-dsh-plugin', 'webchat')
|
|
362
|
-
column.appendChild(container)
|
|
253
|
+
/** Release the guest, for plugin disposal. */
|
|
254
|
+
function disposeGuest() {
|
|
255
|
+
if (guest === null) return
|
|
256
|
+
const releasing = guest
|
|
257
|
+
guest = null
|
|
258
|
+
releasing.container.remove()
|
|
259
|
+
if (bridge !== undefined) {
|
|
260
|
+
try { void bridge.release(releasing.lease) } catch (error) { /* already gone */ }
|
|
363
261
|
}
|
|
262
|
+
}
|
|
364
263
|
|
|
365
|
-
|
|
264
|
+
/** Position the persistent guest over the area the slot gave this panel. */
|
|
265
|
+
function syncBounds(host) {
|
|
266
|
+
if (guest === null) return
|
|
267
|
+
const rect = host.getBoundingClientRect()
|
|
268
|
+
const style = guest.container.style
|
|
269
|
+
style.left = rect.left + "px"
|
|
270
|
+
style.top = rect.top + "px"
|
|
271
|
+
style.width = rect.width + "px"
|
|
272
|
+
style.height = rect.height + "px"
|
|
273
|
+
}
|
|
366
274
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
275
|
+
/** The center-column panel the layout mounts while this id is selected. */
|
|
276
|
+
function WebchatPanel() {
|
|
277
|
+
const hostRef = React.useRef(null)
|
|
278
|
+
|
|
279
|
+
React.useEffect(function () {
|
|
280
|
+
const host = hostRef.current
|
|
281
|
+
if (host === null || host === undefined) return undefined
|
|
282
|
+
let alive = true
|
|
283
|
+
let observer
|
|
284
|
+
let reposition = null
|
|
285
|
+
|
|
286
|
+
const showFallback = function (text) {
|
|
287
|
+
host.replaceChildren()
|
|
288
|
+
host.appendChild(messageBox(text, {
|
|
289
|
+
label: copy().fallbackAction,
|
|
290
|
+
run: (button) => openInWindow(button),
|
|
291
|
+
}))
|
|
379
292
|
}
|
|
380
|
-
}
|
|
381
|
-
var onOtherActivate = function (event) {
|
|
382
|
-
var detail = event.detail
|
|
383
|
-
if ((detail === 'taskboard' || detail === 'ssh') && controller.isOpen()) controller.close()
|
|
384
|
-
}
|
|
385
|
-
/**
|
|
386
|
-
* The shell's own panel list owns the center column. Its rows —
|
|
387
|
-
* Plugins, Automation Tasks, the task board, sessions, workspaces —
|
|
388
|
-
* do not take part in the third-party `data-*-active` handshake, so
|
|
389
|
-
* this panel has to yield on any navigation from the sidebar, or its
|
|
390
|
-
* overlay would keep hiding the page the shell just selected.
|
|
391
|
-
*/
|
|
392
|
-
var SIDEBAR_SELECTOR = '[data-pane="sidebar"], [class*="sidebarCol"]'
|
|
393
|
-
var onClickSidebarRow = function (event) {
|
|
394
|
-
if (!controller.isOpen()) return
|
|
395
|
-
var target = event.target
|
|
396
|
-
if (target === null || target.closest === undefined) return
|
|
397
|
-
// This plugin's own row toggles the panel; it is not navigation.
|
|
398
|
-
if (target.closest('[' + ENTRY_ATTR + ']') !== null) return
|
|
399
|
-
if (target.closest(SIDEBAR_SELECTOR) !== null) controller.close()
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
document.addEventListener('click', onClickSidebarRow, true)
|
|
403
|
-
document.addEventListener(ACTIVATE_EVENT, onOtherActivate)
|
|
404
|
-
var unsubscribe = controller.subscribe(applyActive)
|
|
405
|
-
applyActive()
|
|
406
|
-
ensure()
|
|
407
|
-
waitObserver.observe(document.body, { childList: true, subtree: true })
|
|
408
293
|
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
waitObserver.disconnect()
|
|
413
|
-
unsubscribe()
|
|
414
|
-
document.documentElement.removeAttribute(ACTIVE_ATTR)
|
|
415
|
-
if (frame !== undefined) frame.remove()
|
|
416
|
-
if (container !== undefined) container.remove()
|
|
417
|
-
frame = undefined
|
|
418
|
-
container = undefined
|
|
419
|
-
if (lease !== undefined) {
|
|
420
|
-
const releasing = lease
|
|
421
|
-
lease = undefined
|
|
422
|
-
try { void guests.release(releasing) } catch (error) { /* already gone */ }
|
|
294
|
+
if (bridge === undefined) {
|
|
295
|
+
showFallback(copy().fallbackNote)
|
|
296
|
+
return undefined
|
|
423
297
|
}
|
|
424
|
-
}
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
/** Mount the entry, plus the panel when the desktop bridge is present. */
|
|
428
|
-
function apply(ctx) {
|
|
429
|
-
ensureStyle()
|
|
430
|
-
var controller = createController()
|
|
431
|
-
var disposers = []
|
|
432
298
|
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
toast(T.fallbackFailed + ':' + String(outcome.error || outcome.via || ''), 'bad')
|
|
449
|
-
}
|
|
450
|
-
} catch (error) {
|
|
451
|
-
toast(T.fallbackFailed + ':' + (error instanceof Error ? error.message : String(error)), 'bad')
|
|
299
|
+
void ensureGuest().then(function () {
|
|
300
|
+
if (!alive) return
|
|
301
|
+
if (guest === null) {
|
|
302
|
+
showFallback(copy().failed + ":" + (failure || "unknown"))
|
|
303
|
+
return
|
|
304
|
+
}
|
|
305
|
+
host.replaceChildren()
|
|
306
|
+
syncBounds(host)
|
|
307
|
+
guest.container.style.display = "block"
|
|
308
|
+
reposition = function () {
|
|
309
|
+
if (alive) syncBounds(host)
|
|
310
|
+
}
|
|
311
|
+
if (typeof ResizeObserver === "function") {
|
|
312
|
+
observer = new ResizeObserver(reposition)
|
|
313
|
+
observer.observe(host)
|
|
452
314
|
}
|
|
453
|
-
|
|
315
|
+
window.addEventListener("resize", reposition)
|
|
454
316
|
})
|
|
455
|
-
disposers.push(mountSidebarEntry(controller, fallbackEntry))
|
|
456
|
-
}
|
|
457
317
|
|
|
458
|
-
ctx.effect(function () {
|
|
459
318
|
return function () {
|
|
460
|
-
|
|
319
|
+
alive = false
|
|
320
|
+
if (observer !== undefined) observer.disconnect()
|
|
321
|
+
if (reposition !== null) window.removeEventListener("resize", reposition)
|
|
322
|
+
// Hidden, never detached: the guest keeps its page and its login.
|
|
323
|
+
if (guest !== null) guest.container.style.display = "none"
|
|
461
324
|
}
|
|
462
|
-
},
|
|
325
|
+
}, [])
|
|
326
|
+
|
|
327
|
+
return h("div", { ref: hostRef, className: "dsh-wc-host" })
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/** The sidebar row's icon. The sidebar owns the button around it. */
|
|
331
|
+
function WebchatIcon(props) {
|
|
332
|
+
const size = props !== null && props !== undefined && typeof props.size === "number" ? props.size : 16
|
|
333
|
+
return h("svg", {
|
|
334
|
+
viewBox: "0 0 16 16",
|
|
335
|
+
width: size,
|
|
336
|
+
height: size,
|
|
337
|
+
fill: "none",
|
|
338
|
+
stroke: "currentColor",
|
|
339
|
+
strokeWidth: 1.3,
|
|
340
|
+
strokeLinecap: "round",
|
|
341
|
+
strokeLinejoin: "round",
|
|
342
|
+
"aria-hidden": true,
|
|
343
|
+
},
|
|
344
|
+
h("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" }),
|
|
345
|
+
h("circle", { cx: 8, cy: 8, r: 1.1, fill: "currentColor", stroke: "none" }),
|
|
346
|
+
)
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/** Register the nav row and the page, then own the guest's lifetime. */
|
|
350
|
+
function apply(ctx) {
|
|
351
|
+
ensureStyle()
|
|
352
|
+
ctx.slots.inject("sidebar.panellist", () => ctx.slots.register({
|
|
353
|
+
name: "sidebar.panellist",
|
|
354
|
+
id: PANEL_ID,
|
|
355
|
+
order: -1,
|
|
356
|
+
label: () => copy().label,
|
|
357
|
+
}, WebchatIcon))
|
|
358
|
+
ctx.slots.inject("main", () => ctx.slots.register({
|
|
359
|
+
name: "main",
|
|
360
|
+
key: PANEL_ID,
|
|
361
|
+
}, WebchatPanel))
|
|
362
|
+
ctx.effect(() => () => {
|
|
363
|
+
disposeGuest()
|
|
364
|
+
}, "dsh-webchat: native guest")
|
|
463
365
|
}
|
|
464
366
|
|
|
465
|
-
|
|
466
|
-
exports.inject = []
|
|
467
|
-
return module.exports
|
|
367
|
+
return { inject: ["slots"], apply }
|
|
468
368
|
},
|
|
469
369
|
})
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jaychang1989/dsh-webchat",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Opens the official DeepSeek web app (chat.deepseek.com) inside DeepSeek Harness: one sidebar entry renders the real page in the center column, through the same native browser guest the built-in side-card browser uses. No chat UI, no agent tools, no runtime dependencies.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"dsh",
|
|
@@ -48,7 +48,11 @@
|
|
|
48
48
|
"patch": "./cordis.patch.yml"
|
|
49
49
|
},
|
|
50
50
|
"client": {
|
|
51
|
-
"platform": "web"
|
|
51
|
+
"platform": "web",
|
|
52
|
+
"inject": [
|
|
53
|
+
"@deepseek-ai/dsh-client-ui-layout",
|
|
54
|
+
"@deepseek-ai/dsh-client-ui-sidebar"
|
|
55
|
+
]
|
|
52
56
|
}
|
|
53
57
|
},
|
|
54
58
|
"files": [
|