dsh-code-server-app 0.3.21 → 0.3.22
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 +77 -18
- package/README.md +70 -20
- package/assets/extensions/dshcs-editor-bridge/extension.js +83 -47
- package/assets/extensions/dshcs-editor-bridge/lib/ask-panel.js +343 -174
- package/assets/extensions/dshcs-editor-bridge/lib/bridge-client.js +30 -4
- package/assets/extensions/dshcs-editor-bridge/package.json +2 -2
- package/assets/extensions/dshcs-editor-bridge/webview/THIRD-PARTY.md +21 -0
- package/assets/extensions/dshcs-editor-bridge/webview/fonts/KaTeX_AMS-Regular-U6PRYMIZ.woff2 +0 -0
- package/assets/extensions/dshcs-editor-bridge/webview/fonts/KaTeX_Caligraphic-Bold-5QL5CMTE.woff2 +0 -0
- package/assets/extensions/dshcs-editor-bridge/webview/fonts/KaTeX_Caligraphic-Regular-KX5MEWCF.woff2 +0 -0
- package/assets/extensions/dshcs-editor-bridge/webview/fonts/KaTeX_Fraktur-Bold-2QVFK6NQ.woff2 +0 -0
- package/assets/extensions/dshcs-editor-bridge/webview/fonts/KaTeX_Fraktur-Regular-2PEIFJSJ.woff2 +0 -0
- package/assets/extensions/dshcs-editor-bridge/webview/fonts/KaTeX_Main-Bold-YP5VVQRP.woff2 +0 -0
- package/assets/extensions/dshcs-editor-bridge/webview/fonts/KaTeX_Main-BoldItalic-N4V3DX7S.woff2 +0 -0
- package/assets/extensions/dshcs-editor-bridge/webview/fonts/KaTeX_Main-Italic-RELBIK7M.woff2 +0 -0
- package/assets/extensions/dshcs-editor-bridge/webview/fonts/KaTeX_Main-Regular-ARRPAO67.woff2 +0 -0
- package/assets/extensions/dshcs-editor-bridge/webview/fonts/KaTeX_Math-BoldItalic-K4WTGH3J.woff2 +0 -0
- package/assets/extensions/dshcs-editor-bridge/webview/fonts/KaTeX_Math-Italic-6KGCHLFN.woff2 +0 -0
- package/assets/extensions/dshcs-editor-bridge/webview/fonts/KaTeX_SansSerif-Bold-RRNVJFFW.woff2 +0 -0
- package/assets/extensions/dshcs-editor-bridge/webview/fonts/KaTeX_SansSerif-Italic-HMPFTM52.woff2 +0 -0
- package/assets/extensions/dshcs-editor-bridge/webview/fonts/KaTeX_SansSerif-Regular-XIQ62X4E.woff2 +0 -0
- package/assets/extensions/dshcs-editor-bridge/webview/fonts/KaTeX_Script-Regular-APUWIHLP.woff2 +0 -0
- package/assets/extensions/dshcs-editor-bridge/webview/fonts/KaTeX_Size1-Regular-5LRUTBFT.woff2 +0 -0
- package/assets/extensions/dshcs-editor-bridge/webview/fonts/KaTeX_Size2-Regular-LELKET5D.woff2 +0 -0
- package/assets/extensions/dshcs-editor-bridge/webview/fonts/KaTeX_Size3-Regular-WQRQ47UD.woff2 +0 -0
- package/assets/extensions/dshcs-editor-bridge/webview/fonts/KaTeX_Size4-Regular-CDMV7U5C.woff2 +0 -0
- package/assets/extensions/dshcs-editor-bridge/webview/fonts/KaTeX_Typewriter-Regular-VBYJ4NRC.woff2 +0 -0
- package/assets/extensions/dshcs-editor-bridge/webview/src/app.jsx +189 -0
- package/assets/extensions/dshcs-editor-bridge/webview/src/approval.jsx +53 -0
- package/assets/extensions/dshcs-editor-bridge/webview/src/official-tokens.css +8 -0
- package/assets/extensions/dshcs-editor-bridge/webview/src/panel.css +283 -0
- package/assets/extensions/dshcs-editor-bridge/webview/src/thread.jsx +72 -0
- package/assets/extensions/dshcs-editor-bridge/webview/thread.css +1 -0
- package/assets/extensions/dshcs-editor-bridge/webview/thread.js +462 -0
- package/lib/bridge-approval.mjs +189 -0
- package/lib/bridge-session.mjs +3 -1
- package/lib/bridge-thread.mjs +345 -0
- package/lib/bridge.mjs +15 -6
- package/lib/index.js +142 -17
- package/package.json +29 -6
- package/vendor/VENDOR.json +1 -1
- package/lib/bridge-answer.mjs +0 -155
|
@@ -1,94 +1,258 @@
|
|
|
1
|
-
// dshcs-editor-bridge / lib/ask-panel.js —— 「问 DSH
|
|
1
|
+
// dshcs-editor-bridge / lib/ask-panel.js —— 「问 DSH」面板的纯逻辑(0.2.3)
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
// (
|
|
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 界面同一份代码)。
|
|
5
8
|
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
// (见 lib/bridge-answer.mjs),面板每趟轮询刷新一次,不需要第二个定时器。
|
|
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)。
|
|
12
14
|
|
|
13
15
|
'use strict';
|
|
14
16
|
|
|
15
|
-
/**
|
|
16
|
-
const
|
|
17
|
+
/** 面板保留的对话条目上限(宿主侧 lib/bridge-thread.mjs 也有上限;这里再兜一层)。 */
|
|
18
|
+
const MAX_ENTRIES = 200;
|
|
17
19
|
|
|
18
|
-
/**
|
|
19
|
-
const
|
|
20
|
+
/** 本地乐观提问(已发出、宿主还没回显)的上限。 */
|
|
21
|
+
const MAX_PENDING = 6;
|
|
20
22
|
|
|
21
|
-
/**
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
+
streaming: raw.streaming === true,
|
|
78
|
+
name: typeof raw.name === 'string' ? raw.name : null,
|
|
79
|
+
summary: typeof raw.summary === 'string' ? raw.summary : null,
|
|
80
|
+
status: typeof raw.status === 'string' ? raw.status : null,
|
|
81
|
+
callId: typeof raw.callId === 'string' ? raw.callId : null,
|
|
82
|
+
approvalId: typeof raw.approvalId === 'string' ? raw.approvalId : null,
|
|
83
|
+
};
|
|
29
84
|
}
|
|
30
85
|
|
|
31
86
|
/**
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
* context: object|null, status: string, sessionId: string|null}}
|
|
87
|
+
* 条目签名:正文只会"流式变长"或"被耐久消息原地替换"(此时 streaming 翻转),
|
|
88
|
+
* 所以 角色 + 状态 + 长度 + 是否流式 足够判变化,不必每趟比对几十 KB 文本。
|
|
35
89
|
*/
|
|
36
|
-
function
|
|
37
|
-
|
|
90
|
+
function entriesSignature(entries) {
|
|
91
|
+
const parts = [];
|
|
92
|
+
for (const entry of entries) {
|
|
93
|
+
parts.push(`${entry.role}:${entry.status ?? ''}:${entry.streaming ? 1 : 0}:${entry.text.length}:${entry.summary === null ? '' : entry.summary.length}`);
|
|
94
|
+
}
|
|
95
|
+
return `${entries.length}|${parts.join(',')}`;
|
|
38
96
|
}
|
|
39
97
|
|
|
40
|
-
/**
|
|
41
|
-
function
|
|
42
|
-
state.
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
98
|
+
/** 本地乐观提问 → 渲染条目。 */
|
|
99
|
+
function pendingEntries(state) {
|
|
100
|
+
return state.pending.map((item) => ({
|
|
101
|
+
role: 'user',
|
|
102
|
+
text: item.text,
|
|
103
|
+
streaming: false,
|
|
104
|
+
name: null,
|
|
105
|
+
summary: null,
|
|
106
|
+
status: item.error === null || item.error === undefined ? 'sending' : 'error',
|
|
107
|
+
callId: null,
|
|
108
|
+
approvalId: null,
|
|
109
|
+
}));
|
|
48
110
|
}
|
|
49
111
|
|
|
50
|
-
/**
|
|
51
|
-
function
|
|
52
|
-
|
|
112
|
+
/** 面板要渲染的完整列表(宿主条目 + 尚未回显的本地提问)。 */
|
|
113
|
+
function entriesOf(state) {
|
|
114
|
+
const all = [...state.entries, ...pendingEntries(state)];
|
|
115
|
+
return all.length > MAX_ENTRIES ? all.slice(all.length - MAX_ENTRIES) : all;
|
|
53
116
|
}
|
|
54
117
|
|
|
55
118
|
/**
|
|
56
|
-
*
|
|
57
|
-
*
|
|
119
|
+
* 应用宿主 `/sync` 的 `thread` 快照。
|
|
120
|
+
*
|
|
121
|
+
* `thread === null` = 旧版宿主(没有这个字段)—— 必须**说出原因**,否则面板会永远停在"正在回答…"。
|
|
122
|
+
* `thread.available === false` = 宿主有这个能力但当前没有 watch 到会话(还没提问过)。
|
|
123
|
+
*
|
|
58
124
|
* @returns {boolean} 是否有变化(调用方据此决定要不要刷新 webview)
|
|
59
125
|
*/
|
|
60
|
-
function
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
126
|
+
function applyThread(state, thread, sessionId) {
|
|
127
|
+
if (thread === null || thread === undefined || typeof thread !== 'object') {
|
|
128
|
+
const changed = state.available !== false || state.threadError === null;
|
|
129
|
+
state.available = false;
|
|
130
|
+
state.threadError = '宿主没有对话流能力(插件版本过旧?):请重启 dsh web 让 host 侧升级到 0.3.22 以上';
|
|
131
|
+
// 已经问过了却拿不到对话流:不能只留一行提示,面板必须报错(否则永远停在"正在回答…")。
|
|
132
|
+
if (state.pending.length > 0 && state.status !== 'error') {
|
|
133
|
+
state.status = 'error';
|
|
134
|
+
state.error = state.threadError;
|
|
135
|
+
}
|
|
136
|
+
return changed;
|
|
137
|
+
}
|
|
138
|
+
let changed = false;
|
|
139
|
+
const next = [];
|
|
140
|
+
if (Array.isArray(thread.entries)) {
|
|
141
|
+
for (const raw of thread.entries) {
|
|
142
|
+
const entry = cleanEntry(raw);
|
|
143
|
+
if (entry !== null) next.push(entry);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
const sig = entriesSignature(next);
|
|
147
|
+
if (sig !== state.entriesSig) {
|
|
148
|
+
state.entries = next;
|
|
149
|
+
state.entriesSig = sig;
|
|
150
|
+
changed = true;
|
|
151
|
+
}
|
|
152
|
+
const available = thread.available !== false;
|
|
153
|
+
if (available !== state.available) {
|
|
154
|
+
state.available = available;
|
|
155
|
+
changed = true;
|
|
156
|
+
}
|
|
157
|
+
const error = typeof thread.error === 'string' && thread.error !== '' ? thread.error : null;
|
|
158
|
+
if (error !== state.threadError) {
|
|
159
|
+
state.threadError = error;
|
|
160
|
+
changed = true;
|
|
161
|
+
}
|
|
162
|
+
const session = typeof thread.sessionId === 'string' && thread.sessionId !== ''
|
|
163
|
+
? thread.sessionId
|
|
164
|
+
: (typeof sessionId === 'string' && sessionId !== '' ? sessionId : null);
|
|
165
|
+
if (session !== null && session !== state.sessionId) {
|
|
166
|
+
state.sessionId = session;
|
|
167
|
+
changed = true;
|
|
168
|
+
}
|
|
169
|
+
// 宿主回显了同一段文字 → 本地乐观条目退场(不重复显示)。
|
|
170
|
+
const before = state.pending.length;
|
|
171
|
+
state.pending = state.pending.filter((item) => !next.some(
|
|
172
|
+
(entry) => entry.role === 'user' && normalizeText(entry.text) === normalizeText(item.text),
|
|
173
|
+
));
|
|
174
|
+
if (state.pending.length !== before) changed = true;
|
|
175
|
+
|
|
176
|
+
// 忙碌判定:有待回声的提问、有流式条目、或有还在跑的工具 → thinking。
|
|
177
|
+
const busy = state.pending.length > 0
|
|
178
|
+
|| next.some((entry) => entry.streaming === true || (entry.role === 'tool' && entry.status === 'running'));
|
|
179
|
+
if (busy && state.status !== 'error') state.status = 'thinking';
|
|
180
|
+
else if (!busy && (state.status === 'thinking' || state.status === 'sending')) state.status = 'idle';
|
|
73
181
|
return changed;
|
|
74
182
|
}
|
|
75
183
|
|
|
76
|
-
/**
|
|
77
|
-
function
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
184
|
+
/** 应用宿主 `/sync` 的 `approvals` 快照(待决授权请求)。 */
|
|
185
|
+
function applyApprovals(state, approvals, holdMs) {
|
|
186
|
+
const list = [];
|
|
187
|
+
if (Array.isArray(approvals)) {
|
|
188
|
+
for (const raw of approvals) {
|
|
189
|
+
if (raw === null || typeof raw !== 'object') continue;
|
|
190
|
+
if (typeof raw.id !== 'string' || raw.id === '') continue;
|
|
191
|
+
list.push({
|
|
192
|
+
id: raw.id,
|
|
193
|
+
toolName: typeof raw.toolName === 'string' && raw.toolName !== '' ? raw.toolName : 'tool',
|
|
194
|
+
reason: typeof raw.reason === 'string' ? raw.reason : '',
|
|
195
|
+
callId: typeof raw.callId === 'string' ? raw.callId : null,
|
|
196
|
+
at: Number.isSafeInteger(raw.at) ? raw.at : Date.now(),
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
list.sort((a, b) => a.at - b.at);
|
|
200
|
+
}
|
|
201
|
+
const window = Number.isSafeInteger(holdMs) && holdMs > 0 ? holdMs : DEFAULT_HOLD_MS;
|
|
202
|
+
const sig = `${window}|${JSON.stringify(list)}`;
|
|
203
|
+
if (sig === state.approvalSig) return false;
|
|
204
|
+
state.approvals = list;
|
|
205
|
+
state.approvalHoldMs = window;
|
|
206
|
+
state.approvalSig = sig;
|
|
207
|
+
return true;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* 把宿主一趟 `/sync` 的结果并进面板状态(扩展侧只调这一个)。
|
|
212
|
+
*
|
|
213
|
+
* `result.thread === undefined` = 旧版宿主(没有这个字段)。
|
|
214
|
+
* @returns {boolean} 是否有变化(调用方据此决定要不要刷新 webview)
|
|
215
|
+
*/
|
|
216
|
+
function applySync(state, result) {
|
|
217
|
+
let changed = false;
|
|
218
|
+
if (applyThread(state, result === null || result === undefined ? null : result.thread, result?.sessionId ?? null)) {
|
|
219
|
+
changed = true;
|
|
220
|
+
}
|
|
221
|
+
if (applyApprovals(state, result === null || result === undefined ? [] : result.approvals, result?.approvalHoldMs)) {
|
|
222
|
+
changed = true;
|
|
223
|
+
}
|
|
224
|
+
const version = result === null || result === undefined ? undefined : result.uiVersion;
|
|
225
|
+
if (typeof version === 'string' && version !== '' && version !== state.uiVersion) {
|
|
226
|
+
state.uiVersion = version;
|
|
227
|
+
changed = true;
|
|
228
|
+
}
|
|
229
|
+
return changed;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* 登记一条本地提问(乐观显示;宿主回显后自动退场)。
|
|
234
|
+
* @returns {object} 刚登记的 pending 条目
|
|
235
|
+
*/
|
|
236
|
+
function pendingQuestion(state, text, context) {
|
|
237
|
+
const item = { text: String(text ?? ''), at: Date.now(), error: null };
|
|
238
|
+
state.pending.push(item);
|
|
239
|
+
while (state.pending.length > MAX_PENDING) state.pending.shift();
|
|
240
|
+
if (context !== null && context !== undefined) state.context = context;
|
|
241
|
+
state.status = 'sending';
|
|
242
|
+
state.error = null;
|
|
243
|
+
return item;
|
|
89
244
|
}
|
|
90
245
|
|
|
91
|
-
/**
|
|
246
|
+
/** 投递失败 / 面板错误:状态行 + 最新一条本地提问都标上原因。 */
|
|
247
|
+
function failPanel(state, message) {
|
|
248
|
+
state.status = 'error';
|
|
249
|
+
state.error = message;
|
|
250
|
+
const last = state.pending[state.pending.length - 1];
|
|
251
|
+
if (last !== undefined) last.error = message;
|
|
252
|
+
return state;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** 上下文 → 一行描述(标题栏):文件:行 + 选区行数。 */
|
|
92
256
|
function describeContext(context) {
|
|
93
257
|
if (context === null || context === undefined) return '';
|
|
94
258
|
const name = typeof context.file === 'string' && context.file !== '' ? context.file : '(当前文件)';
|
|
@@ -102,129 +266,134 @@ function describeContext(context) {
|
|
|
102
266
|
return base;
|
|
103
267
|
}
|
|
104
268
|
|
|
269
|
+
/** 标题栏那一行:上下文 + 提问意图。 */
|
|
270
|
+
function contextLine(state) {
|
|
271
|
+
const where = state.context === null || state.context === undefined
|
|
272
|
+
? '来自编辑器'
|
|
273
|
+
: `来自编辑器:${describeContext(state.context)}`;
|
|
274
|
+
if (state.context === null || state.context === undefined) return where;
|
|
275
|
+
return state.mode === 'file' ? `${where} · 针对当前文件` : `${where} · 针对选中内容`;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/** 状态 → 状态行文案(扩展侧组装,面板只显示)。 */
|
|
279
|
+
function statusText(state) {
|
|
280
|
+
if (state.status === 'error') return state.error ?? '出错了';
|
|
281
|
+
if (state.status === 'sending') return '正在发给 DSH…';
|
|
282
|
+
if (state.status === 'thinking') return 'DSH 正在回答…';
|
|
283
|
+
return state.threadError ?? '';
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/** 状态 → 面板可渲染的 JSON(postMessage 的载荷)。 */
|
|
287
|
+
function panelPayload(state) {
|
|
288
|
+
return {
|
|
289
|
+
type: 'state',
|
|
290
|
+
entries: entriesOf(state),
|
|
291
|
+
approvals: state.approvals,
|
|
292
|
+
approvalHoldMs: state.approvalHoldMs,
|
|
293
|
+
contextText: contextLine(state),
|
|
294
|
+
statusText: statusText(state),
|
|
295
|
+
status: state.status,
|
|
296
|
+
error: state.error,
|
|
297
|
+
sessionId: state.sessionId,
|
|
298
|
+
available: state.available,
|
|
299
|
+
threadError: state.threadError,
|
|
300
|
+
uiVersion: state.uiVersion,
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/** 面板侧:把载荷并进本地视图状态(只认白名单字段)。 */
|
|
305
|
+
function applyPayload(state, payload) {
|
|
306
|
+
if (payload === null || typeof payload !== 'object' || payload.type !== 'state') return state;
|
|
307
|
+
return {
|
|
308
|
+
...state,
|
|
309
|
+
entries: Array.isArray(payload.entries) ? payload.entries : [],
|
|
310
|
+
approvals: Array.isArray(payload.approvals) ? payload.approvals : [],
|
|
311
|
+
approvalHoldMs: Number.isSafeInteger(payload.approvalHoldMs) && payload.approvalHoldMs > 0
|
|
312
|
+
? payload.approvalHoldMs
|
|
313
|
+
: DEFAULT_HOLD_MS,
|
|
314
|
+
contextText: typeof payload.contextText === 'string' ? payload.contextText : '',
|
|
315
|
+
statusText: typeof payload.statusText === 'string' ? payload.statusText : '',
|
|
316
|
+
status: typeof payload.status === 'string' ? payload.status : 'idle',
|
|
317
|
+
error: typeof payload.error === 'string' ? payload.error : null,
|
|
318
|
+
sessionId: typeof payload.sessionId === 'string' ? payload.sessionId : null,
|
|
319
|
+
available: payload.available === undefined ? null : payload.available,
|
|
320
|
+
threadError: typeof payload.threadError === 'string' ? payload.threadError : null,
|
|
321
|
+
uiVersion: typeof payload.uiVersion === 'string' ? payload.uiVersion : null,
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/** 面板视图的初始状态(同 applyPayload 的输出形状,首帧渲染用)。 */
|
|
326
|
+
function createViewState() {
|
|
327
|
+
return {
|
|
328
|
+
entries: [],
|
|
329
|
+
approvals: [],
|
|
330
|
+
approvalHoldMs: DEFAULT_HOLD_MS,
|
|
331
|
+
contextText: '来自编辑器',
|
|
332
|
+
statusText: '',
|
|
333
|
+
status: 'idle',
|
|
334
|
+
error: null,
|
|
335
|
+
sessionId: null,
|
|
336
|
+
available: null,
|
|
337
|
+
threadError: null,
|
|
338
|
+
uiVersion: null,
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/** HTML 属性转义(只用于外壳里的 URI;正文一律走 React,不做字符串拼接)。 */
|
|
343
|
+
function escapeAttribute(text) {
|
|
344
|
+
return String(text)
|
|
345
|
+
.replace(/&/g, '&')
|
|
346
|
+
.replace(/</g, '<')
|
|
347
|
+
.replace(/>/g, '>')
|
|
348
|
+
.replace(/"/g, '"')
|
|
349
|
+
.replace(/'/g, ''');
|
|
350
|
+
}
|
|
351
|
+
|
|
105
352
|
/**
|
|
106
|
-
* 面板 HTML(
|
|
107
|
-
*
|
|
353
|
+
* 面板 HTML(一次性外壳):只加载打包好的 thread.js / thread.css,
|
|
354
|
+
* 之后全靠 postMessage 更新 —— 输入框与滚动位置不受影响。
|
|
355
|
+
*
|
|
356
|
+
* @param {{cspSource: string, nonce: string, scriptUri: string, styleUri: string}} options
|
|
108
357
|
*/
|
|
109
|
-
function renderPanelHtml({ cspSource, nonce }) {
|
|
358
|
+
function renderPanelHtml({ cspSource, nonce, scriptUri, styleUri }) {
|
|
359
|
+
const script = escapeAttribute(scriptUri);
|
|
360
|
+
const style = escapeAttribute(styleUri);
|
|
110
361
|
return `<!DOCTYPE html>
|
|
111
362
|
<html lang="zh-CN">
|
|
112
363
|
<head>
|
|
113
364
|
<meta charset="utf-8">
|
|
114
|
-
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src ${cspSource} 'unsafe-inline'; script-src 'nonce-${nonce}';">
|
|
365
|
+
<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}';">
|
|
366
|
+
<link rel="stylesheet" href="${style}">
|
|
115
367
|
<title>DSH 提问</title>
|
|
116
|
-
<style>
|
|
117
|
-
:root { color-scheme: light dark; }
|
|
118
|
-
body { margin: 0; font-family: var(--vscode-font-family); font-size: var(--vscode-font-size, 13px);
|
|
119
|
-
color: var(--vscode-foreground); background: var(--vscode-editor-background); display: flex; flex-direction: column; height: 100vh; }
|
|
120
|
-
header { padding: 8px 12px; border-bottom: 1px solid var(--vscode-panel-border, #8884); font-size: 12px; opacity: .85; }
|
|
121
|
-
#log { flex: 1; overflow-y: auto; padding: 12px; }
|
|
122
|
-
.turn { margin-bottom: 16px; }
|
|
123
|
-
.q { background: var(--vscode-input-background); border: 1px solid var(--vscode-input-border, #8884);
|
|
124
|
-
border-radius: 6px; padding: 6px 8px; white-space: pre-wrap; word-break: break-word; }
|
|
125
|
-
.a { margin-top: 8px; padding: 2px 2px 0 8px; border-left: 2px solid var(--vscode-focusBorder, #8886);
|
|
126
|
-
white-space: pre-wrap; word-break: break-word; }
|
|
127
|
-
.a.empty { opacity: .5; font-style: italic; }
|
|
128
|
-
.err { color: var(--vscode-errorForeground, #f66); margin-top: 6px; white-space: pre-wrap; }
|
|
129
|
-
footer { border-top: 1px solid var(--vscode-panel-border, #8884); padding: 8px; display: flex; gap: 8px; align-items: flex-end; }
|
|
130
|
-
textarea { flex: 1; resize: vertical; min-height: 54px; max-height: 40vh; padding: 6px 8px; box-sizing: border-box;
|
|
131
|
-
color: var(--vscode-input-foreground); background: var(--vscode-input-background);
|
|
132
|
-
border: 1px solid var(--vscode-input-border, #8884); border-radius: 4px; font-family: inherit; font-size: inherit; }
|
|
133
|
-
button { padding: 6px 14px; border: none; border-radius: 4px; cursor: pointer;
|
|
134
|
-
color: var(--vscode-button-foreground); background: var(--vscode-button-background); }
|
|
135
|
-
button:disabled { opacity: .5; cursor: default; }
|
|
136
|
-
#status { padding: 0 12px 8px; font-size: 12px; opacity: .8; min-height: 16px; }
|
|
137
|
-
</style>
|
|
138
368
|
</head>
|
|
139
369
|
<body>
|
|
140
|
-
<
|
|
141
|
-
<
|
|
142
|
-
<div id="status"></div>
|
|
143
|
-
<footer>
|
|
144
|
-
<textarea id="box" placeholder="问 DSH…(Enter 发送,Shift+Enter 换行)"></textarea>
|
|
145
|
-
<button id="send">发送</button>
|
|
146
|
-
</footer>
|
|
147
|
-
<script nonce="${nonce}">
|
|
148
|
-
const vscode = acquireVsCodeApi();
|
|
149
|
-
const log = document.getElementById('log');
|
|
150
|
-
const ctx = document.getElementById('ctx');
|
|
151
|
-
const status = document.getElementById('status');
|
|
152
|
-
const box = document.getElementById('box');
|
|
153
|
-
const send = document.getElementById('send');
|
|
154
|
-
|
|
155
|
-
function render(state) {
|
|
156
|
-
ctx.textContent = state.contextText || '来自编辑器';
|
|
157
|
-
const atBottom = log.scrollHeight - log.scrollTop - log.clientHeight < 40;
|
|
158
|
-
log.textContent = '';
|
|
159
|
-
for (const turn of state.turns) {
|
|
160
|
-
const wrap = document.createElement('div');
|
|
161
|
-
wrap.className = 'turn';
|
|
162
|
-
const q = document.createElement('div');
|
|
163
|
-
q.className = 'q';
|
|
164
|
-
q.textContent = turn.question;
|
|
165
|
-
wrap.appendChild(q);
|
|
166
|
-
const a = document.createElement('div');
|
|
167
|
-
a.className = 'a' + (turn.answer ? '' : ' empty');
|
|
168
|
-
a.textContent = turn.answer || (turn.error ? '' : (turn.done ? '(没有文字回答)' : '思考中…'));
|
|
169
|
-
wrap.appendChild(a);
|
|
170
|
-
if (turn.error) {
|
|
171
|
-
const err = document.createElement('div');
|
|
172
|
-
err.className = 'err';
|
|
173
|
-
err.textContent = turn.error;
|
|
174
|
-
wrap.appendChild(err);
|
|
175
|
-
}
|
|
176
|
-
log.appendChild(wrap);
|
|
177
|
-
}
|
|
178
|
-
status.textContent = state.statusText || '';
|
|
179
|
-
send.disabled = state.status === 'sending';
|
|
180
|
-
if (atBottom) log.scrollTop = log.scrollHeight;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
function submit() {
|
|
184
|
-
const text = box.value.trim();
|
|
185
|
-
if (text === '') return;
|
|
186
|
-
vscode.postMessage({ type: 'ask', text });
|
|
187
|
-
box.value = '';
|
|
188
|
-
}
|
|
189
|
-
send.addEventListener('click', submit);
|
|
190
|
-
box.addEventListener('keydown', (event) => {
|
|
191
|
-
if (event.key === 'Enter' && !event.shiftKey) { event.preventDefault(); submit(); }
|
|
192
|
-
});
|
|
193
|
-
window.addEventListener('message', (event) => { if (event.data && event.data.type === 'state') render(event.data); });
|
|
194
|
-
// 出错就写在状态行上:面板静默空白是最难查的一种失败。
|
|
195
|
-
window.addEventListener('error', (event) => {
|
|
196
|
-
status.textContent = '面板脚本出错:' + (event && event.message ? event.message : '未知');
|
|
197
|
-
});
|
|
198
|
-
window.addEventListener('unhandledrejection', (event) => {
|
|
199
|
-
status.textContent = '面板脚本出错:' + (event && event.reason ? String(event.reason) : '未知');
|
|
200
|
-
});
|
|
201
|
-
vscode.postMessage({ type: 'ready' });
|
|
202
|
-
box.focus();
|
|
203
|
-
</script>
|
|
370
|
+
<div id="root"></div>
|
|
371
|
+
<script nonce="${nonce}" src="${script}"></script>
|
|
204
372
|
</body>
|
|
205
373
|
</html>`;
|
|
206
374
|
}
|
|
207
375
|
|
|
208
|
-
/** 状态 → 状态行文案(扩展侧组装,面板只显示)。 */
|
|
209
|
-
function statusText(state) {
|
|
210
|
-
switch (state.status) {
|
|
211
|
-
case 'sending': return '发送中…';
|
|
212
|
-
case 'thinking': return 'DSH 正在回答…';
|
|
213
|
-
case 'error': return '出错了';
|
|
214
|
-
default: return '';
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
|
|
218
376
|
module.exports = {
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
377
|
+
MAX_ENTRIES,
|
|
378
|
+
MAX_PENDING,
|
|
379
|
+
MAX_TEXT,
|
|
380
|
+
DEFAULT_HOLD_MS,
|
|
381
|
+
normalizeText,
|
|
222
382
|
createPanelState,
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
383
|
+
cleanEntry,
|
|
384
|
+
entriesSignature,
|
|
385
|
+
entriesOf,
|
|
386
|
+
applyThread,
|
|
387
|
+
applyApprovals,
|
|
388
|
+
applySync,
|
|
389
|
+
pendingQuestion,
|
|
390
|
+
failPanel,
|
|
227
391
|
describeContext,
|
|
228
|
-
|
|
392
|
+
contextLine,
|
|
229
393
|
statusText,
|
|
394
|
+
panelPayload,
|
|
395
|
+
applyPayload,
|
|
396
|
+
createViewState,
|
|
397
|
+
escapeAttribute,
|
|
398
|
+
renderPanelHtml,
|
|
230
399
|
};
|
|
@@ -277,10 +277,20 @@ function createClient(options) {
|
|
|
277
277
|
for (const event of events) {
|
|
278
278
|
if (Number.isSafeInteger(event.seq) && event.seq > since) since = event.seq;
|
|
279
279
|
}
|
|
280
|
-
//
|
|
281
|
-
//
|
|
282
|
-
|
|
283
|
-
|
|
280
|
+
// 0.2.3 起面板像 DSH 对话一样显示内容,数据走这三个字段(见 host 侧 lib/bridge-thread.mjs
|
|
281
|
+
// 与 lib/bridge-approval.mjs):
|
|
282
|
+
// - thread:被面板观看的会话的**新内容**条目(旧版宿主没有这个字段 → 面板明确报错);
|
|
283
|
+
// - approvals:待决授权请求(工作区外写入 / 命令执行),面板就地作答;
|
|
284
|
+
// - uiVersion:DSH 界面的版本(面板据此提示"渲染器与界面版本不一致")。
|
|
285
|
+
// 全部原样透传,解析与合并交给 lib/ask-panel.js 的纯模型。
|
|
286
|
+
return {
|
|
287
|
+
ok: true,
|
|
288
|
+
events,
|
|
289
|
+
thread: body !== null && body.thread !== undefined ? body.thread : undefined,
|
|
290
|
+
approvals: body !== null && Array.isArray(body.approvals) ? body.approvals : [],
|
|
291
|
+
approvalHoldMs: body !== null && Number.isSafeInteger(body.approvalHoldMs) ? body.approvalHoldMs : undefined,
|
|
292
|
+
uiVersion: body !== null && typeof body.uiVersion === 'string' ? body.uiVersion : undefined,
|
|
293
|
+
};
|
|
284
294
|
} catch (error) {
|
|
285
295
|
return { ok: false, error: error.message, status: error.status, code: error.code };
|
|
286
296
|
}
|
|
@@ -293,6 +303,22 @@ function createClient(options) {
|
|
|
293
303
|
body: JSON.stringify(payload),
|
|
294
304
|
});
|
|
295
305
|
},
|
|
306
|
+
/**
|
|
307
|
+
* 回答一条待决的授权请求(桥里**唯一**的非只读路由)。
|
|
308
|
+
*
|
|
309
|
+
* 约束在 host 侧写死:只认本进程发起、仍未决的 id(单次使用),outcome 只接受
|
|
310
|
+
* `allowed-once` / `rejected`,不接受任何自由文本 —— 这条路由只能"回答问题",不能"发起动作"。
|
|
311
|
+
*
|
|
312
|
+
* @param {string} id 授权请求 id(来自 /sync 的 approvals)
|
|
313
|
+
* @param {'allowed-once'|'rejected'} outcome
|
|
314
|
+
*/
|
|
315
|
+
async approve(id, outcome) {
|
|
316
|
+
return request(`${BRIDGE_BASE}/approve`, {
|
|
317
|
+
method: 'POST',
|
|
318
|
+
headers: { 'content-type': 'application/json' },
|
|
319
|
+
body: JSON.stringify({ id, outcome }),
|
|
320
|
+
});
|
|
321
|
+
},
|
|
296
322
|
/** 把游标落盘(实例重启后不重复播报旧事件)。 */
|
|
297
323
|
persist() {
|
|
298
324
|
return writeState(extensionsDir, { since, pid: config === null ? null : config.pid });
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dshcs-editor-bridge",
|
|
3
3
|
"displayName": "DSH Editor Bridge",
|
|
4
|
-
"description": "只读地把编辑器状态(未保存缓冲区、诊断、活动选区)提供给 DSH,并让编辑器里的动作驱动 DSH
|
|
5
|
-
"version": "0.2.
|
|
4
|
+
"description": "只读地把编辑器状态(未保存缓冲区、诊断、活动选区)提供给 DSH,并让编辑器里的动作驱动 DSH 会话;0.2.3 起「问 DSH」面板用 DSH 官方 markdown 渲染器显示该会话的新内容,并可就地回答授权请求(写工作区外文件 / 执行命令)。由 dsh-code-server-app 插件安装,可禁用。",
|
|
5
|
+
"version": "0.2.3",
|
|
6
6
|
"publisher": "dsh-code-server-app",
|
|
7
7
|
"license": "MIT",
|
|
8
8
|
"private": true,
|
|
@@ -0,0 +1,21 @@
|
|
|
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.5-rc.2 | MIT |
|
|
10
|
+
| @deepseek-ai/dsh-client-ui-theme | 0.1.5-rc.2 | MIT |
|
|
11
|
+
| react | 18.3.1 | MIT |
|
|
12
|
+
| react-dom | 18.3.1 | MIT |
|
|
13
|
+
|
|
14
|
+
生成时间:2026-09-13T08:28:22.125Z
|
|
15
|
+
渲染器:@deepseek-ai/dsh-client-ui-primitives@0.1.5-rc.2(DSH 部署的界面版本:0.1.5-rc.2)
|
|
16
|
+
设计令牌:@deepseek-ai/dsh-client-ui-theme@0.1.5-rc.2(5 段)
|
|
17
|
+
KaTeX:已打包(公式排版与 DSH 界面一致)
|
|
18
|
+
shiki 语法:typescript / shellscript / json(与 DSH 启动集一致)
|
|
19
|
+
令牌自检:通过
|
|
20
|
+
|
|
21
|
+
许可证原文见各包目录下的 LICENSE 文件(例如 `node_modules/@deepseek-ai/dsh-client-ui-primitives/LICENSE`)。
|
|
Binary file
|
package/assets/extensions/dshcs-editor-bridge/webview/fonts/KaTeX_Caligraphic-Bold-5QL5CMTE.woff2
ADDED
|
Binary file
|