dsh-code-server-app 0.3.58 → 0.3.63
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE-Apache-2.0.txt +201 -0
- package/README.en.md +215 -98
- package/README.md +186 -94
- package/THIRD_PARTY_NOTICES.md +60 -0
- package/assets/extensions/dshcs-editor-bridge/extension.js +192 -222
- package/assets/extensions/dshcs-editor-bridge/lib/bridge-client.js +43 -39
- package/assets/extensions/dshcs-editor-bridge/lib/fim-completion.js +264 -0
- package/assets/extensions/dshcs-editor-bridge/package.json +2 -2
- package/lib/bridge-session.mjs +55 -5
- package/lib/bridge.mjs +14 -7
- package/lib/client.js +1061 -90
- package/lib/fim-adapter.mjs +603 -0
- package/lib/index.js +366 -205
- package/package.json +21 -41
- package/vendor/VENDOR.json +5 -5
- package/assets/extensions/dshcs-editor-bridge/lib/ask-panel.js +0 -416
- package/assets/extensions/dshcs-editor-bridge/webview/THIRD-PARTY.md +0 -22
- package/assets/extensions/dshcs-editor-bridge/webview/src/app.jsx +0 -242
- package/assets/extensions/dshcs-editor-bridge/webview/src/approval.jsx +0 -61
- package/assets/extensions/dshcs-editor-bridge/webview/src/official-tokens.css +0 -576
- package/assets/extensions/dshcs-editor-bridge/webview/src/panel.css +0 -380
- package/assets/extensions/dshcs-editor-bridge/webview/src/thread.jsx +0 -160
- package/assets/extensions/dshcs-editor-bridge/webview/thread.css +0 -1
- package/assets/extensions/dshcs-editor-bridge/webview/thread.js +0 -465
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-code-server-app",
|
|
3
|
-
"version": "0.3.
|
|
4
|
-
"description": "VS Code (from a code-server release) inside DSH: a right-sidebar tab driven by the plugin's own launcher over the in-process VS Code server (lib/launcher.mjs)
|
|
3
|
+
"version": "0.3.63",
|
|
4
|
+
"description": "VS Code (from a code-server release) inside DSH: a right-sidebar tab driven by the plugin's own launcher over the in-process VS Code server (lib/launcher.mjs), plus an editor bridge (assets/extensions/dshcs-editor-bridge) that gives the agent what only the editor knows (unsaved buffers, language-server diagnostics, the active selection) and lets editor gestures drive the session. The bridge is completely read-only: all four of its routes (/health, /sync, /old, /event) are reads. Asking DSH from the editor opens a floating dialog inside the DSH page (draggable, resizable, bottom-right): the plugin's client half sends the question as user input (POST /api/code-server/ask/send) and renders that session's new content with DSH's own Markdown renderer - the same @deepseek-ai/dsh-client-ui-primitives instance the UI uses, required from the page's module table, so typography, highlighting and math match the interface and a renderer version mismatch is impossible. Thinking appears as collapsed rows and approval requests (writing outside the workspace / running commands) can be answered in place: whole-plugin write route POST /api/code-server/ask/approve, constrained to pending in-process requests and the outcomes allowed-once/rejected; dialog-first for 5 minutes, handed back to the official card the moment the dialog closes, never auto-approved. There is no build step anywhere: lib/client.js (client half including the dialog panel) and the extension are committed hand-written source, devDependencies holds a single test-only entry, and the published package carries no frontend bundle. Also: the tab claims DSH file addresses (dsh-resource://file/**) by file type (setting claimExtensions), so produced-file chips, delivered-file previews and inline prose mentions open in the workbench; the IDE is a resident surface moved with Element.moveBefore instead of being remounted, so switching sidebar tabs does not reload it; opening the tab switches the right sidebar to fullscreen by default (setting fullscreenOnOpen); a workspace switch re-navigates the workbench with the new ?folder= without restarting the IDE process. Requires a DSH with the right-sidebar services (sidebarRightTabs/sidebarRight, >= 0.1.5-alpha.1); older DSH versions only get an upgrade notice on the settings page. Two serving modes: loopback port (default) or same-origin mount on DSH's own webServer (/code-server, protected by ctx.connection.requestRejection). The diff the bridge opens always has the real pre-write text on the left, taken from result.value.before in tools/post-execute into a bounded snapshot cache and fetched by opaque key over the read-only GET /old, so files that are not open in the editor work too. No code-server Node layer, no argon2, no C++ toolchain.",
|
|
5
5
|
"homepage": "https://github.com/jinsiyu/dsh-code-server-app",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -48,6 +48,7 @@
|
|
|
48
48
|
"lib/dsh-resolve.mjs",
|
|
49
49
|
"lib/child-node.mjs",
|
|
50
50
|
"lib/edit-snapshot.mjs",
|
|
51
|
+
"lib/fim-adapter.mjs",
|
|
51
52
|
"lib/launcher.mjs",
|
|
52
53
|
"lib/serve-dsh.mjs",
|
|
53
54
|
"lib/vendor.js",
|
|
@@ -63,34 +64,13 @@
|
|
|
63
64
|
"vendor/VENDOR.json",
|
|
64
65
|
"cordis.patch.yml",
|
|
65
66
|
"LICENSE",
|
|
67
|
+
"LICENSE-Apache-2.0.txt",
|
|
68
|
+
"THIRD_PARTY_NOTICES.md",
|
|
66
69
|
"README.md",
|
|
67
70
|
"README.en.md"
|
|
68
71
|
],
|
|
69
72
|
"devDependencies": {
|
|
70
|
-
"@deepseek-ai/
|
|
71
|
-
"@deepseek-ai/dsh-client-ui-theme": "0.1.6-alpha.1",
|
|
72
|
-
"@deepseek-ai/schemastery": "3.18.2",
|
|
73
|
-
"@shikijs/langs": "4.3.1",
|
|
74
|
-
"anser": "2.3.5",
|
|
75
|
-
"clsx": "2.0.0",
|
|
76
|
-
"diff": "9.0.0",
|
|
77
|
-
"esbuild": "^0.25.0",
|
|
78
|
-
"katex": "0.16.47",
|
|
79
|
-
"mdast-util-from-markdown": "2.0.3",
|
|
80
|
-
"mdast-util-gfm": "3.1.0",
|
|
81
|
-
"mdast-util-math": "3.0.0",
|
|
82
|
-
"micromark-core-commonmark": "2.0.3",
|
|
83
|
-
"micromark-extension-gfm": "3.0.0",
|
|
84
|
-
"micromark-extension-math": "3.1.0",
|
|
85
|
-
"micromark-factory-space": "2.0.1",
|
|
86
|
-
"micromark-util-character": "2.1.1",
|
|
87
|
-
"micromark-util-classify-character": "2.0.1",
|
|
88
|
-
"micromark-util-sanitize-uri": "2.0.1",
|
|
89
|
-
"micromark-util-symbol": "2.0.1",
|
|
90
|
-
"micromark-util-types": "2.0.2",
|
|
91
|
-
"react": "18.3.1",
|
|
92
|
-
"react-dom": "18.3.1",
|
|
93
|
-
"shiki": "4.3.1"
|
|
73
|
+
"@deepseek-ai/schemastery": "3.18.2"
|
|
94
74
|
},
|
|
95
75
|
"license": "MIT",
|
|
96
76
|
"dependencies": {
|
|
@@ -102,7 +82,7 @@
|
|
|
102
82
|
"@jinsiyu/dshcs-ssh2": "1.17.0",
|
|
103
83
|
"@jinsiyu/dshcs-vscode-fs-copyfile": "2.0.0",
|
|
104
84
|
"@jinsiyu/dshcs-vscode-proxy-agent": "0.44.0",
|
|
105
|
-
"@jinsiyu/dshcs-vscode-server": "4.
|
|
85
|
+
"@jinsiyu/dshcs-vscode-server": "4.138.0",
|
|
106
86
|
"@microsoft/1ds-core-js": "3.2.13",
|
|
107
87
|
"@microsoft/1ds-post-js": "3.2.13",
|
|
108
88
|
"@vscode/iconv-lite-umd": "0.7.1",
|
|
@@ -110,16 +90,16 @@
|
|
|
110
90
|
"@vscode/sandbox-runtime": "0.0.1",
|
|
111
91
|
"@vscode/tree-sitter-wasm": "0.3.1",
|
|
112
92
|
"@vscode/vscode-languagedetection": "1.0.23",
|
|
113
|
-
"@xterm/addon-clipboard": "0.3.0-beta.
|
|
114
|
-
"@xterm/addon-image": "0.10.0-beta.
|
|
115
|
-
"@xterm/addon-ligatures": "0.11.0-beta.
|
|
116
|
-
"@xterm/addon-progress": "0.3.0-beta.
|
|
117
|
-
"@xterm/addon-search": "0.17.0-beta.
|
|
118
|
-
"@xterm/addon-serialize": "0.15.0-beta.
|
|
119
|
-
"@xterm/addon-unicode11": "0.10.0-beta.
|
|
120
|
-
"@xterm/addon-webgl": "0.20.0-beta.
|
|
121
|
-
"@xterm/headless": "6.1.0-beta.
|
|
122
|
-
"@xterm/xterm": "6.1.0-beta.
|
|
93
|
+
"@xterm/addon-clipboard": "0.3.0-beta.303",
|
|
94
|
+
"@xterm/addon-image": "0.10.0-beta.301",
|
|
95
|
+
"@xterm/addon-ligatures": "0.11.0-beta.301",
|
|
96
|
+
"@xterm/addon-progress": "0.3.0-beta.301",
|
|
97
|
+
"@xterm/addon-search": "0.17.0-beta.301",
|
|
98
|
+
"@xterm/addon-serialize": "0.15.0-beta.301",
|
|
99
|
+
"@xterm/addon-unicode11": "0.10.0-beta.301",
|
|
100
|
+
"@xterm/addon-webgl": "0.20.0-beta.300",
|
|
101
|
+
"@xterm/headless": "6.1.0-beta.303",
|
|
102
|
+
"@xterm/xterm": "6.1.0-beta.304",
|
|
123
103
|
"bindings": "1.5.0",
|
|
124
104
|
"cookie": "0.7.2",
|
|
125
105
|
"detect-libc": "2.1.2",
|
|
@@ -127,12 +107,12 @@
|
|
|
127
107
|
"http-proxy-agent": "7.0.0",
|
|
128
108
|
"https-proxy-agent": "7.0.2",
|
|
129
109
|
"jschardet": "3.1.4",
|
|
110
|
+
"katex": "0.16.22",
|
|
130
111
|
"minimist": "1.2.8",
|
|
131
112
|
"mkdirp": "1.0.4",
|
|
132
113
|
"node-addon-api": "6.1.0",
|
|
133
114
|
"tar": "7.5.22",
|
|
134
|
-
"tas-client": "0.4.
|
|
135
|
-
"tslib": "2.8.1",
|
|
115
|
+
"tas-client": "0.4.6",
|
|
136
116
|
"typescript": "6.0.3",
|
|
137
117
|
"uuid": "14.0.2",
|
|
138
118
|
"vscode-oniguruma": "1.7.0",
|
|
@@ -183,7 +163,6 @@
|
|
|
183
163
|
"publish:repacks": "node scripts/publish-repacks.mjs",
|
|
184
164
|
"publish:plugin": "node scripts/publish-plugin.mjs",
|
|
185
165
|
"promote": "node scripts/promote.mjs",
|
|
186
|
-
"build:webview": "node scripts/build-webview.mjs",
|
|
187
166
|
"test:apply": "node scripts/test-plugin-apply.mjs",
|
|
188
167
|
"test:launcher-routes": "node scripts/test-launcher-routes.mjs",
|
|
189
168
|
"test:fullscreen": "node scripts/test-sidebar-fullscreen.mjs",
|
|
@@ -193,6 +172,7 @@
|
|
|
193
172
|
"test:client-cwd": "node scripts/test-client-bundle-cwd.mjs",
|
|
194
173
|
"test:client-tabs": "node scripts/test-client-bundle-tabs.mjs",
|
|
195
174
|
"test:client-entry": "node scripts/test-client-entry.mjs",
|
|
175
|
+
"test:ask-panel": "node scripts/test-ask-panel-inline.mjs",
|
|
196
176
|
"test:client-seat": "node scripts/test-client-settings-seat.mjs",
|
|
197
177
|
"test:dsh-resolve": "node scripts/test-dsh-resolve.mjs",
|
|
198
178
|
"test:child-node": "node scripts/test-child-node.mjs",
|
|
@@ -200,7 +180,7 @@
|
|
|
200
180
|
"test:bridge-routes": "node scripts/test-bridge-routes.mjs",
|
|
201
181
|
"test:edit-snapshot": "node scripts/test-edit-snapshot.mjs",
|
|
202
182
|
"test:bridge-extension": "node scripts/test-bridge-extension.mjs",
|
|
203
|
-
"test:
|
|
183
|
+
"test:ask-dialog": "node scripts/test-ask-dialog.mjs",
|
|
204
184
|
"test:vendored": "node scripts/test-vendored-table.mjs"
|
|
205
185
|
}
|
|
206
186
|
}
|
package/vendor/VENDOR.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
|
-
"codeServerVersion": "4.
|
|
3
|
-
"vscodeVersion": "1.
|
|
4
|
-
"productPath": "stable-
|
|
2
|
+
"codeServerVersion": "4.138.0",
|
|
3
|
+
"vscodeVersion": "1.138.0",
|
|
4
|
+
"productPath": "stable-59c988c744a240b05b039f57b856a5312f19d5b1",
|
|
5
5
|
"layout": "vscode-only",
|
|
6
|
-
"preparedAt": "2026-09-
|
|
6
|
+
"preparedAt": "2026-09-22T02:29:14.118Z",
|
|
7
7
|
"source": "registry",
|
|
8
8
|
"node": "v24.20.0",
|
|
9
9
|
"platform": "linux",
|
|
10
10
|
"arch": "x64",
|
|
11
|
-
"sizeMB":
|
|
11
|
+
"sizeMB": 198.3
|
|
12
12
|
}
|
|
@@ -1,416 +0,0 @@
|
|
|
1
|
-
// dshcs-editor-bridge / lib/ask-panel.js —— 「问 DSH」面板的纯逻辑(0.2.3)
|
|
2
|
-
//
|
|
3
|
-
// **一份模型,两边用**:
|
|
4
|
-
// - 扩展侧(extension.js)用它保存面板状态,并组装每趟轮询 postMessage 的载荷;
|
|
5
|
-
// - 面板侧(webview/src/app.jsx,由 scripts/build-webview.mjs 打成 webview/thread.js)
|
|
6
|
-
// 用它把载荷变成视图 —— 消息正文交给 DSH **官方** markdown 渲染器
|
|
7
|
-
// (`@deepseek-ai/dsh-client-ui-primitives` 的 MarkdownText,与 DSH 界面同一份代码)。
|
|
8
|
-
//
|
|
9
|
-
// 0.2.0–0.2.2 的面板自己拼 HTML 字符串、正文按 `white-space: pre-wrap` 原样显示,与 DSH 界面
|
|
10
|
-
// 的排版不一致;0.2.3 起正文走官方渲染器,面板只保留"外壳"逻辑(上下文 / 状态 / 输入框)。
|
|
11
|
-
//
|
|
12
|
-
// 本文件不 require('vscode')、不碰 DOM,所以能在普通 Node 里单测
|
|
13
|
-
// (scripts/test-bridge-extension.mjs)。
|
|
14
|
-
|
|
15
|
-
'use strict';
|
|
16
|
-
|
|
17
|
-
/** 面板保留的对话条目上限(宿主侧 lib/bridge-thread.mjs 也有上限;这里再兜一层)。 */
|
|
18
|
-
const MAX_ENTRIES = 200;
|
|
19
|
-
|
|
20
|
-
/** 本地乐观提问(已发出、宿主还没回显)的上限。 */
|
|
21
|
-
const MAX_PENDING = 6;
|
|
22
|
-
|
|
23
|
-
/** 单条正文渲染上限(宿主已截到 8000,这里只防意外)。 */
|
|
24
|
-
const MAX_TEXT = 20000;
|
|
25
|
-
|
|
26
|
-
/** 授权卡片的默认等待窗口(毫秒);宿主随 /sync 给出真实值,这里只是兜底。 */
|
|
27
|
-
const DEFAULT_HOLD_MS = 8000;
|
|
28
|
-
|
|
29
|
-
/** 条目角色(与 lib/bridge-thread.mjs 的投影一致)。 */
|
|
30
|
-
const ROLES = ['user', 'assistant', 'tool', 'approval'];
|
|
31
|
-
|
|
32
|
-
/** 折叠空白:用于"本地乐观提问"与"宿主回显"的同一性比较。 */
|
|
33
|
-
function normalizeText(text) {
|
|
34
|
-
return String(text ?? '').replace(/\s+/g, ' ').trim();
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/** 面板状态(纯数据,便于单测)。 */
|
|
38
|
-
function createPanelState() {
|
|
39
|
-
return {
|
|
40
|
-
/** 对话条目(宿主权威;0.3.22 起只含**新内容**)。 */
|
|
41
|
-
entries: [],
|
|
42
|
-
/** 条目签名:变了才刷新面板(600ms 一趟轮询,不能每趟都重传几十 KB)。 */
|
|
43
|
-
entriesSig: '',
|
|
44
|
-
/** 已发出、宿主还没回显的提问(乐观显示)。 */
|
|
45
|
-
pending: [],
|
|
46
|
-
/** 待决授权请求 [{id, toolName, reason, callId, at}]。 */
|
|
47
|
-
approvals: [],
|
|
48
|
-
approvalSig: '',
|
|
49
|
-
approvalHoldMs: DEFAULT_HOLD_MS,
|
|
50
|
-
/** 宿主有没有对话流能力:null=还不知道,false=旧版宿主。 */
|
|
51
|
-
available: null,
|
|
52
|
-
/** 对话流的错误(订阅失败 / 旧版宿主)。 */
|
|
53
|
-
threadError: null,
|
|
54
|
-
/** 当前提问意图:'selection' | 'file'。 */
|
|
55
|
-
mode: 'selection',
|
|
56
|
-
/** 发送时那一次取的编辑器上下文。 */
|
|
57
|
-
context: null,
|
|
58
|
-
/** idle | sending | thinking | error */
|
|
59
|
-
status: 'idle',
|
|
60
|
-
error: null,
|
|
61
|
-
/** 面板绑定的会话(host 回话里的 sessionId)。 */
|
|
62
|
-
sessionId: null,
|
|
63
|
-
/** DSH 界面的版本(host 的 /sync 报回来;面板据此提示渲染器版本不一致)。 */
|
|
64
|
-
uiVersion: null,
|
|
65
|
-
};
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/** 一条宿主条目 → 规整过的渲染条目(字段白名单,不接受宿主塞别的东西)。 */
|
|
69
|
-
function cleanEntry(raw) {
|
|
70
|
-
if (raw === null || typeof raw !== 'object') return null;
|
|
71
|
-
const role = ROLES.includes(raw.role) ? raw.role : null;
|
|
72
|
-
if (role === null) return null;
|
|
73
|
-
const text = typeof raw.text === 'string' ? raw.text.slice(0, MAX_TEXT) : '';
|
|
74
|
-
return {
|
|
75
|
-
role,
|
|
76
|
-
text,
|
|
77
|
-
// 思考过程(0.3.23):渲染成默认收起的「思考」行。
|
|
78
|
-
thinking: typeof raw.thinking === 'string' ? raw.thinking.slice(0, MAX_TEXT) : '',
|
|
79
|
-
// 注入的上下文(0.3.24):桥自己拼的位置行 + 选区代码块,渲染成默认收起的「上下文」行。
|
|
80
|
-
context: typeof raw.context === 'string' ? raw.context.slice(0, MAX_TEXT) : '',
|
|
81
|
-
// 消息来源(0.3.43):非 'user' = 上下文注入,渲染成折叠行。
|
|
82
|
-
sourceKind: typeof raw.sourceKind === 'string' && raw.sourceKind !== '' ? raw.sourceKind : null,
|
|
83
|
-
streaming: raw.streaming === true,
|
|
84
|
-
name: typeof raw.name === 'string' ? raw.name : null,
|
|
85
|
-
summary: typeof raw.summary === 'string' ? raw.summary : null,
|
|
86
|
-
status: typeof raw.status === 'string' ? raw.status : null,
|
|
87
|
-
callId: typeof raw.callId === 'string' ? raw.callId : null,
|
|
88
|
-
approvalId: typeof raw.approvalId === 'string' ? raw.approvalId : null,
|
|
89
|
-
};
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* 条目签名:正文/思考只会"流式变长"或"被耐久消息原地替换"(此时 streaming 翻转),
|
|
94
|
-
* 所以 角色 + 状态 + 长度 + 是否流式 足够判变化,不必每趟比对几十 KB 文本。
|
|
95
|
-
*/
|
|
96
|
-
function entriesSignature(entries) {
|
|
97
|
-
const parts = [];
|
|
98
|
-
for (const entry of entries) {
|
|
99
|
-
parts.push([
|
|
100
|
-
entry.role,
|
|
101
|
-
entry.status ?? '',
|
|
102
|
-
entry.streaming ? 1 : 0,
|
|
103
|
-
entry.text.length,
|
|
104
|
-
entry.thinking.length,
|
|
105
|
-
entry.context.length,
|
|
106
|
-
entry.sourceKind === null ? '' : entry.sourceKind.length,
|
|
107
|
-
entry.summary === null ? '' : entry.summary.length,
|
|
108
|
-
].join(':'));
|
|
109
|
-
}
|
|
110
|
-
return `${entries.length}|${parts.join(',')}`;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/** 本地乐观提问 → 渲染条目。 */
|
|
114
|
-
function pendingEntries(state) {
|
|
115
|
-
return state.pending.map((item) => ({
|
|
116
|
-
role: 'user',
|
|
117
|
-
text: item.text,
|
|
118
|
-
thinking: '',
|
|
119
|
-
context: '',
|
|
120
|
-
streaming: false,
|
|
121
|
-
name: null,
|
|
122
|
-
summary: null,
|
|
123
|
-
status: item.error === null || item.error === undefined ? 'sending' : 'error',
|
|
124
|
-
callId: null,
|
|
125
|
-
approvalId: null,
|
|
126
|
-
}));
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/** 面板要渲染的完整列表(宿主条目 + 尚未回显的本地提问)。 */
|
|
130
|
-
function entriesOf(state) {
|
|
131
|
-
const all = [...state.entries, ...pendingEntries(state)];
|
|
132
|
-
return all.length > MAX_ENTRIES ? all.slice(all.length - MAX_ENTRIES) : all;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
/**
|
|
136
|
-
* 应用宿主 `/sync` 的 `thread` 快照。
|
|
137
|
-
*
|
|
138
|
-
* `thread === null` = 旧版宿主(没有这个字段)—— 必须**说出原因**,否则面板会永远停在"正在回答…"。
|
|
139
|
-
* `thread.available === false` = 宿主有这个能力但当前没有 watch 到会话(还没提问过)。
|
|
140
|
-
*
|
|
141
|
-
* @returns {boolean} 是否有变化(调用方据此决定要不要刷新 webview)
|
|
142
|
-
*/
|
|
143
|
-
function applyThread(state, thread, sessionId) {
|
|
144
|
-
if (thread === null || thread === undefined || typeof thread !== 'object') {
|
|
145
|
-
const changed = state.available !== false || state.threadError === null;
|
|
146
|
-
state.available = false;
|
|
147
|
-
state.threadError = '宿主没有对话流能力(插件版本过旧?):请重启 dsh web 让 host 侧升级到 0.3.22 以上';
|
|
148
|
-
// 已经问过了却拿不到对话流:不能只留一行提示,面板必须报错(否则永远停在"正在回答…")。
|
|
149
|
-
if (state.pending.length > 0 && state.status !== 'error') {
|
|
150
|
-
state.status = 'error';
|
|
151
|
-
state.error = state.threadError;
|
|
152
|
-
}
|
|
153
|
-
return changed;
|
|
154
|
-
}
|
|
155
|
-
let changed = false;
|
|
156
|
-
const next = [];
|
|
157
|
-
if (Array.isArray(thread.entries)) {
|
|
158
|
-
for (const raw of thread.entries) {
|
|
159
|
-
const entry = cleanEntry(raw);
|
|
160
|
-
if (entry !== null) next.push(entry);
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
const sig = entriesSignature(next);
|
|
164
|
-
if (sig !== state.entriesSig) {
|
|
165
|
-
state.entries = next;
|
|
166
|
-
state.entriesSig = sig;
|
|
167
|
-
changed = true;
|
|
168
|
-
}
|
|
169
|
-
const available = thread.available !== false;
|
|
170
|
-
if (available !== state.available) {
|
|
171
|
-
state.available = available;
|
|
172
|
-
changed = true;
|
|
173
|
-
}
|
|
174
|
-
const error = typeof thread.error === 'string' && thread.error !== '' ? thread.error : null;
|
|
175
|
-
if (error !== state.threadError) {
|
|
176
|
-
state.threadError = error;
|
|
177
|
-
changed = true;
|
|
178
|
-
}
|
|
179
|
-
const session = typeof thread.sessionId === 'string' && thread.sessionId !== ''
|
|
180
|
-
? thread.sessionId
|
|
181
|
-
: (typeof sessionId === 'string' && sessionId !== '' ? sessionId : null);
|
|
182
|
-
if (session !== null && session !== state.sessionId) {
|
|
183
|
-
state.sessionId = session;
|
|
184
|
-
changed = true;
|
|
185
|
-
}
|
|
186
|
-
// 宿主回显了同一段文字 → 本地乐观条目退场(不重复显示)。
|
|
187
|
-
const before = state.pending.length;
|
|
188
|
-
state.pending = state.pending.filter((item) => !next.some(
|
|
189
|
-
(entry) => entry.role === 'user' && normalizeText(entry.text) === normalizeText(item.text),
|
|
190
|
-
));
|
|
191
|
-
if (state.pending.length !== before) changed = true;
|
|
192
|
-
|
|
193
|
-
// 忙碌判定:有待回声的提问、有流式条目、或有还在跑的工具 → thinking。
|
|
194
|
-
const busy = state.pending.length > 0
|
|
195
|
-
|| next.some((entry) => entry.streaming === true || (entry.role === 'tool' && entry.status === 'running'));
|
|
196
|
-
if (busy && state.status !== 'error') state.status = 'thinking';
|
|
197
|
-
else if (!busy && (state.status === 'thinking' || state.status === 'sending')) state.status = 'idle';
|
|
198
|
-
return changed;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
/** 应用宿主 `/sync` 的 `approvals` 快照(待决授权请求)。 */
|
|
202
|
-
function applyApprovals(state, approvals, holdMs) {
|
|
203
|
-
const list = [];
|
|
204
|
-
if (Array.isArray(approvals)) {
|
|
205
|
-
for (const raw of approvals) {
|
|
206
|
-
if (raw === null || typeof raw !== 'object') continue;
|
|
207
|
-
if (typeof raw.id !== 'string' || raw.id === '') continue;
|
|
208
|
-
list.push({
|
|
209
|
-
id: raw.id,
|
|
210
|
-
toolName: typeof raw.toolName === 'string' && raw.toolName !== '' ? raw.toolName : 'tool',
|
|
211
|
-
reason: typeof raw.reason === 'string' ? raw.reason : '',
|
|
212
|
-
callId: typeof raw.callId === 'string' ? raw.callId : null,
|
|
213
|
-
at: Number.isSafeInteger(raw.at) ? raw.at : Date.now(),
|
|
214
|
-
});
|
|
215
|
-
}
|
|
216
|
-
list.sort((a, b) => a.at - b.at);
|
|
217
|
-
}
|
|
218
|
-
const window = Number.isSafeInteger(holdMs) && holdMs > 0 ? holdMs : DEFAULT_HOLD_MS;
|
|
219
|
-
const sig = `${window}|${JSON.stringify(list)}`;
|
|
220
|
-
if (sig === state.approvalSig) return false;
|
|
221
|
-
state.approvals = list;
|
|
222
|
-
state.approvalHoldMs = window;
|
|
223
|
-
state.approvalSig = sig;
|
|
224
|
-
return true;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
/**
|
|
228
|
-
* 把宿主一趟 `/sync` 的结果并进面板状态(扩展侧只调这一个)。
|
|
229
|
-
*
|
|
230
|
-
* `result.thread === undefined` = 旧版宿主(没有这个字段)。
|
|
231
|
-
* @returns {boolean} 是否有变化(调用方据此决定要不要刷新 webview)
|
|
232
|
-
*/
|
|
233
|
-
function applySync(state, result) {
|
|
234
|
-
let changed = false;
|
|
235
|
-
if (applyThread(state, result === null || result === undefined ? null : result.thread, result?.sessionId ?? null)) {
|
|
236
|
-
changed = true;
|
|
237
|
-
}
|
|
238
|
-
if (applyApprovals(state, result === null || result === undefined ? [] : result.approvals, result?.approvalHoldMs)) {
|
|
239
|
-
changed = true;
|
|
240
|
-
}
|
|
241
|
-
const version = result === null || result === undefined ? undefined : result.uiVersion;
|
|
242
|
-
if (typeof version === 'string' && version !== '' && version !== state.uiVersion) {
|
|
243
|
-
state.uiVersion = version;
|
|
244
|
-
changed = true;
|
|
245
|
-
}
|
|
246
|
-
return changed;
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
/**
|
|
250
|
-
* 登记一条本地提问(乐观显示;宿主回显后自动退场)。
|
|
251
|
-
* @returns {object} 刚登记的 pending 条目
|
|
252
|
-
*/
|
|
253
|
-
function pendingQuestion(state, text, context) {
|
|
254
|
-
const item = { text: String(text ?? ''), at: Date.now(), error: null };
|
|
255
|
-
state.pending.push(item);
|
|
256
|
-
while (state.pending.length > MAX_PENDING) state.pending.shift();
|
|
257
|
-
if (context !== null && context !== undefined) state.context = context;
|
|
258
|
-
state.status = 'sending';
|
|
259
|
-
state.error = null;
|
|
260
|
-
return item;
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
/** 投递失败 / 面板错误:状态行 + 最新一条本地提问都标上原因。 */
|
|
264
|
-
function failPanel(state, message) {
|
|
265
|
-
state.status = 'error';
|
|
266
|
-
state.error = message;
|
|
267
|
-
const last = state.pending[state.pending.length - 1];
|
|
268
|
-
if (last !== undefined) last.error = message;
|
|
269
|
-
return state;
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
/** 上下文 → 一行描述(标题栏):文件:行 + 选区行数。 */
|
|
273
|
-
function describeContext(context) {
|
|
274
|
-
if (context === null || context === undefined) return '';
|
|
275
|
-
const name = typeof context.file === 'string' && context.file !== '' ? context.file : '(当前文件)';
|
|
276
|
-
const base = name.replace(/^.*[\\/]/, '');
|
|
277
|
-
if (Number.isSafeInteger(context.lineStart)) {
|
|
278
|
-
const range = Number.isSafeInteger(context.lineEnd) && context.lineEnd !== context.lineStart
|
|
279
|
-
? `${context.lineStart}-${context.lineEnd}`
|
|
280
|
-
: `${context.lineStart}`;
|
|
281
|
-
return `${base}:${range}`;
|
|
282
|
-
}
|
|
283
|
-
return base;
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
/** 标题栏那一行:上下文 + 提问意图。 */
|
|
287
|
-
function contextLine(state) {
|
|
288
|
-
const where = state.context === null || state.context === undefined
|
|
289
|
-
? '来自编辑器'
|
|
290
|
-
: `来自编辑器:${describeContext(state.context)}`;
|
|
291
|
-
if (state.context === null || state.context === undefined) return where;
|
|
292
|
-
return state.mode === 'file' ? `${where} · 针对当前文件` : `${where} · 针对选中内容`;
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
/** 状态 → 状态行文案(扩展侧组装,面板只显示)。 */
|
|
296
|
-
function statusText(state) {
|
|
297
|
-
if (state.status === 'error') return state.error ?? '出错了';
|
|
298
|
-
if (state.status === 'sending') return '正在发给 DSH…';
|
|
299
|
-
if (state.status === 'thinking') return 'DSH 正在回答…';
|
|
300
|
-
return state.threadError ?? '';
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
/** 状态 → 面板可渲染的 JSON(postMessage 的载荷)。 */
|
|
304
|
-
function panelPayload(state) {
|
|
305
|
-
return {
|
|
306
|
-
type: 'state',
|
|
307
|
-
entries: entriesOf(state),
|
|
308
|
-
approvals: state.approvals,
|
|
309
|
-
approvalHoldMs: state.approvalHoldMs,
|
|
310
|
-
contextText: contextLine(state),
|
|
311
|
-
statusText: statusText(state),
|
|
312
|
-
status: state.status,
|
|
313
|
-
error: state.error,
|
|
314
|
-
sessionId: state.sessionId,
|
|
315
|
-
available: state.available,
|
|
316
|
-
threadError: state.threadError,
|
|
317
|
-
uiVersion: state.uiVersion,
|
|
318
|
-
};
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
/** 面板侧:把载荷并进本地视图状态(只认白名单字段)。 */
|
|
322
|
-
function applyPayload(state, payload) {
|
|
323
|
-
if (payload === null || typeof payload !== 'object' || payload.type !== 'state') return state;
|
|
324
|
-
return {
|
|
325
|
-
...state,
|
|
326
|
-
entries: Array.isArray(payload.entries) ? payload.entries : [],
|
|
327
|
-
approvals: Array.isArray(payload.approvals) ? payload.approvals : [],
|
|
328
|
-
approvalHoldMs: Number.isSafeInteger(payload.approvalHoldMs) && payload.approvalHoldMs > 0
|
|
329
|
-
? payload.approvalHoldMs
|
|
330
|
-
: DEFAULT_HOLD_MS,
|
|
331
|
-
contextText: typeof payload.contextText === 'string' ? payload.contextText : '',
|
|
332
|
-
statusText: typeof payload.statusText === 'string' ? payload.statusText : '',
|
|
333
|
-
status: typeof payload.status === 'string' ? payload.status : 'idle',
|
|
334
|
-
error: typeof payload.error === 'string' ? payload.error : null,
|
|
335
|
-
sessionId: typeof payload.sessionId === 'string' ? payload.sessionId : null,
|
|
336
|
-
available: payload.available === undefined ? null : payload.available,
|
|
337
|
-
threadError: typeof payload.threadError === 'string' ? payload.threadError : null,
|
|
338
|
-
uiVersion: typeof payload.uiVersion === 'string' ? payload.uiVersion : null,
|
|
339
|
-
};
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
/** 面板视图的初始状态(同 applyPayload 的输出形状,首帧渲染用)。 */
|
|
343
|
-
function createViewState() {
|
|
344
|
-
return {
|
|
345
|
-
entries: [],
|
|
346
|
-
approvals: [],
|
|
347
|
-
approvalHoldMs: DEFAULT_HOLD_MS,
|
|
348
|
-
contextText: '来自编辑器',
|
|
349
|
-
statusText: '',
|
|
350
|
-
status: 'idle',
|
|
351
|
-
error: null,
|
|
352
|
-
sessionId: null,
|
|
353
|
-
available: null,
|
|
354
|
-
threadError: null,
|
|
355
|
-
uiVersion: null,
|
|
356
|
-
};
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
/** HTML 属性转义(只用于外壳里的 URI;正文一律走 React,不做字符串拼接)。 */
|
|
360
|
-
function escapeAttribute(text) {
|
|
361
|
-
return String(text)
|
|
362
|
-
.replace(/&/g, '&')
|
|
363
|
-
.replace(/</g, '<')
|
|
364
|
-
.replace(/>/g, '>')
|
|
365
|
-
.replace(/"/g, '"')
|
|
366
|
-
.replace(/'/g, ''');
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
/**
|
|
370
|
-
* 面板 HTML(一次性外壳):只加载打包好的 thread.js / thread.css,
|
|
371
|
-
* 之后全靠 postMessage 更新 —— 输入框与滚动位置不受影响。
|
|
372
|
-
*
|
|
373
|
-
* @param {{cspSource: string, nonce: string, scriptUri: string, styleUri: string}} options
|
|
374
|
-
*/
|
|
375
|
-
function renderPanelHtml({ cspSource, nonce, scriptUri, styleUri }) {
|
|
376
|
-
const script = escapeAttribute(scriptUri);
|
|
377
|
-
const style = escapeAttribute(styleUri);
|
|
378
|
-
return `<!DOCTYPE html>
|
|
379
|
-
<html lang="zh-CN">
|
|
380
|
-
<head>
|
|
381
|
-
<meta charset="utf-8">
|
|
382
|
-
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; img-src ${cspSource} data:; font-src ${cspSource}; style-src ${cspSource} 'unsafe-inline'; script-src 'nonce-${nonce}';">
|
|
383
|
-
<link rel="stylesheet" href="${style}">
|
|
384
|
-
<title>DSH 对话</title>
|
|
385
|
-
</head>
|
|
386
|
-
<body class="dshcs-panel">
|
|
387
|
-
<div id="root"></div>
|
|
388
|
-
<script nonce="${nonce}" src="${script}"></script>
|
|
389
|
-
</body>
|
|
390
|
-
</html>`;
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
module.exports = {
|
|
394
|
-
MAX_ENTRIES,
|
|
395
|
-
MAX_PENDING,
|
|
396
|
-
MAX_TEXT,
|
|
397
|
-
DEFAULT_HOLD_MS,
|
|
398
|
-
normalizeText,
|
|
399
|
-
createPanelState,
|
|
400
|
-
cleanEntry,
|
|
401
|
-
entriesSignature,
|
|
402
|
-
entriesOf,
|
|
403
|
-
applyThread,
|
|
404
|
-
applyApprovals,
|
|
405
|
-
applySync,
|
|
406
|
-
pendingQuestion,
|
|
407
|
-
failPanel,
|
|
408
|
-
describeContext,
|
|
409
|
-
contextLine,
|
|
410
|
-
statusText,
|
|
411
|
-
panelPayload,
|
|
412
|
-
applyPayload,
|
|
413
|
-
createViewState,
|
|
414
|
-
escapeAttribute,
|
|
415
|
-
renderPanelHtml,
|
|
416
|
-
};
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
# 面板 webview 打包进去的第三方代码
|
|
2
|
-
|
|
3
|
-
本目录下的 `thread.js` / `thread.css` 由 `scripts/build-webview.mjs` 打包生成,
|
|
4
|
-
其中包含下列第三方包(全部 MIT 许可)。它们不是本插件的代码,原样打包只为让面板的消息正文
|
|
5
|
-
与 DSH 界面完全一致(markdown 管线 / 语法高亮 / 排版令牌)。
|
|
6
|
-
|
|
7
|
-
| 包 | 版本 | 许可 |
|
|
8
|
-
| --- | --- | --- |
|
|
9
|
-
| @deepseek-ai/dsh-client-ui-primitives | 0.1.6-alpha.1 | MIT |
|
|
10
|
-
| @deepseek-ai/dsh-client-ui-theme | 0.1.6-alpha.1 | MIT |
|
|
11
|
-
| @deepseek-ai/schemastery | 3.18.2 | MIT |
|
|
12
|
-
| react | 18.3.1 | MIT |
|
|
13
|
-
| react-dom | 18.3.1 | MIT |
|
|
14
|
-
|
|
15
|
-
生成时间:2026-09-20T14:00:30.980Z
|
|
16
|
-
渲染器:@deepseek-ai/dsh-client-ui-primitives@0.1.6-alpha.1(DSH 部署的界面版本:未知)
|
|
17
|
-
设计令牌:@deepseek-ai/dsh-client-ui-theme@0.1.6-alpha.1(11 段)
|
|
18
|
-
KaTeX:已打包(公式排版与 DSH 界面一致)
|
|
19
|
-
shiki 语法:typescript / shellscript / json(与 DSH 启动集一致)
|
|
20
|
-
令牌自检:通过
|
|
21
|
-
|
|
22
|
-
许可证原文见各包目录下的 LICENSE 文件(例如 `node_modules/@deepseek-ai/dsh-client-ui-primitives/LICENSE`)。
|