dsh-code-server-app 0.3.22 → 0.3.23
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 +19 -11
- package/README.md +12 -8
- package/assets/extensions/dshcs-editor-bridge/extension.js +11 -3
- package/assets/extensions/dshcs-editor-bridge/lib/ask-panel.js +12 -2
- package/assets/extensions/dshcs-editor-bridge/package.json +2 -2
- package/assets/extensions/dshcs-editor-bridge/webview/THIRD-PARTY.md +1 -1
- package/assets/extensions/dshcs-editor-bridge/webview/src/app.jsx +11 -1
- package/assets/extensions/dshcs-editor-bridge/webview/src/approval.jsx +28 -20
- package/assets/extensions/dshcs-editor-bridge/webview/src/panel.css +63 -0
- package/assets/extensions/dshcs-editor-bridge/webview/src/thread.jsx +52 -9
- package/assets/extensions/dshcs-editor-bridge/webview/thread.css +1 -1
- package/assets/extensions/dshcs-editor-bridge/webview/thread.js +63 -61
- package/lib/bridge-approval.mjs +40 -14
- package/lib/bridge-thread.mjs +47 -18
- package/package.json +2 -2
- package/vendor/VENDOR.json +1 -1
package/README.en.md
CHANGED
|
@@ -209,9 +209,9 @@ only the editor knows, and lets editor gestures drive the current session.
|
|
|
209
209
|
|
|
210
210
|
- The tools are only registered while the bridge is live (so the model never sees an unusable tool), and the
|
|
211
211
|
system-prompt section renders only then too.
|
|
212
|
-
- **Ask panel** (extension 0.2.0; since 0.2.3
|
|
213
|
-
|
|
214
|
-
|
|
212
|
+
- **Ask panel** (extension 0.2.0; the official renderer since 0.2.3; a **dialog** since 0.2.4): the context-menu
|
|
213
|
+
command opens a chat window that fills the editor area with a centred, width-limited column (the ✕ in its header
|
|
214
|
+
closes it) instead of a cramped side column — and the selection can still be changed while it stays open.
|
|
215
215
|
The two commands **remember their intent** (0.3.21): "ask about selection" carries a **line range + selection text**
|
|
216
216
|
only when something is actually selected, while "ask about file" **never carries line numbers or a selection** — the
|
|
217
217
|
cursor line is irrelevant to the question and only misleads the agent. With no selection, the selection command also
|
|
@@ -221,9 +221,15 @@ only the editor knows, and lets editor gestures drive the current session.
|
|
|
221
221
|
micromark/mdast pipeline, the same incremental streaming parser, the same shiki highlighting (boot set:
|
|
222
222
|
typescript / shellscript / json), KaTeX math and the same heading/table typography. Only **new content** is
|
|
223
223
|
rendered (from the moment the panel subscribes); history is **not replayed** and there is no "load earlier".
|
|
224
|
-
- **
|
|
225
|
-
|
|
226
|
-
|
|
224
|
+
- **Thinking shows up like in DSH** (0.3.23): assistant reasoning becomes a Think row — **collapsed by default**,
|
|
225
|
+
showing its first line (or the latest line while streaming) and expanding on a row click, built from the official
|
|
226
|
+
`DisclosureRow` plus the official think icon and typography language.
|
|
227
|
+
- **Approvals are handled right in the panel** (0.3.22; window fixed in 0.3.23): while the panel is open, that
|
|
228
|
+
session's approval requests ask the panel first (5 minutes by default). Clicking "allow once" / "reject" settles it
|
|
229
|
+
immediately; **closing the panel** or letting the window expire hands the request back **unchanged** to the official
|
|
230
|
+
path (the DSH UI shows the same card). 0.3.22's 8-second window was far too short for a human — the buttons went
|
|
231
|
+
grey before anyone could click (reported as "the approval box stopped working"); the window is now 5 minutes and
|
|
232
|
+
closing the panel hands off immediately instead of waiting it out.
|
|
227
233
|
**Nothing is ever auto-approved** — `allowed-once` can only come from a click, and there is no "always allow".
|
|
228
234
|
- The question enters the DSH session as a **plain user message** (`source: { kind: 'user' }`, host 0.3.19): earlier
|
|
229
235
|
versions used `{kind:'plugin'}`, which DSH renders as a *context update* — it did not look like something the user
|
|
@@ -263,7 +269,7 @@ The three `/sync` fields the panel actually consumes (0.3.22):
|
|
|
263
269
|
|---|---|---|
|
|
264
270
|
| `thread` | **new content** entries (user / assistant / tool / approval) of the session the panel watches; bounded: ≤120 entries per session, ≤8000 chars per body, ≤4 watched sessions | assistant bodies go to the official renderer; tools and approvals become compact summary rows |
|
|
265
271
|
| `approvals` | pending approval requests `[{id, toolName, reason, at}]` (≤4) | renders the card with a countdown; a click posts `/approve` |
|
|
266
|
-
| `approvalHoldMs` / `uiVersion` | the approval window (
|
|
272
|
+
| `approvalHoldMs` / `uiVersion` | the approval window (300000 ms = 5 minutes by default) / the DSH UI version | countdown basis; a renderer-version mismatch is surfaced in the panel |
|
|
267
273
|
|
|
268
274
|
> **Why not HTTP (settled in 0.3.13, all three measured)**
|
|
269
275
|
> 1. **Desktop has no HTTP surface at all**: the renderer calls `host.fetch()` through Electron IPC
|
|
@@ -304,7 +310,8 @@ user**, so:
|
|
|
304
310
|
**no free text, paths, or command arguments**, so it can answer questions but never start an action;
|
|
305
311
|
(b) `id` must belong to a request this process created and that is **still pending** (single use);
|
|
306
312
|
(c) `outcome` accepts only `allowed-once` / `rejected` — there is **no "always allow"**;
|
|
307
|
-
(d) when no panel is watching, or the window (
|
|
313
|
+
(d) when no panel is watching, the panel is closed, or the window (5 minutes by default) expires, the request goes
|
|
314
|
+
**back to the official
|
|
308
315
|
path** — never auto-approved (DSH's `approval/request` itself fails closed; this bridge can only keep
|
|
309
316
|
"nobody answered" as "nobody answered"). `pnpm test:webview` asserts these four plus the host-side whitelist.
|
|
310
317
|
3. **Any request carrying `Origin` gets 403.** Browsers always send one (including a sandboxed iframe's literal
|
|
@@ -760,9 +767,10 @@ What remains on the plugin side:
|
|
|
760
767
|
- **Panel assets are pinned to the DSH version**: the renderer is bundled against the UI version of the deployed
|
|
761
768
|
DSH, so after upgrading DSH you must rebuild the panel (`pnpm run build:webview`; the build fails loudly on a
|
|
762
769
|
version mismatch). The panel also shows a mismatch notice at runtime instead of silently using the wrong renderer.
|
|
763
|
-
- **The approval window in the panel is
|
|
764
|
-
|
|
765
|
-
|
|
770
|
+
- **The approval window in the panel is 5 minutes**: while the panel is open, approvals ask the panel first (the card
|
|
771
|
+
shows a countdown); **closing the panel** or letting the 5 minutes run out hands the request back to the DSH UI —
|
|
772
|
+
after that, that request can **only** be answered there (the card disappears from the panel and the thread keeps an
|
|
773
|
+
audit row).
|
|
766
774
|
- **Unsaved buffers are reported, not taken over.** The agent still edits via its own `fs` tools, i.e. against
|
|
767
775
|
disk. What the bridge adds is a notice *before* writing a dirty file, a diff *after*, and a warning instead of
|
|
768
776
|
an overwrite. It does not decide whether the user saves — that would mean changing the agent's read path,
|
package/README.md
CHANGED
|
@@ -203,8 +203,8 @@ DSH 用**资源地址**命名文件,`openFile` 只负责把地址交给右侧栏
|
|
|
203
203
|
| agent → 编辑器 | agent 改了哪个文件 → 开**原生 diff** 审阅;缓冲区有未保存改动时**告警而不覆盖** | host 观察 `tools/result`,扩展轮询后开 diff + 非模态告警 |
|
|
204
204
|
|
|
205
205
|
- 工具只在桥就绪时注册(IDE 没起来时模型看不到"有个用不了的工具");提示词段落也只在桥存活时渲染。
|
|
206
|
-
- **提问面板**(扩展 0.2.0 起;0.2.3
|
|
207
|
-
|
|
206
|
+
- **提问面板**(扩展 0.2.0 起;0.2.3 起正文走官方渲染器;0.2.4 起是**对话框形式**):右键命令打开的不是
|
|
207
|
+
侧栏里的一列,而是一扇占满编辑器区、内容居中限宽的**对话窗**(右上角 ✕ 关闭;面板开着的**同时**还能改选区再问)。
|
|
208
208
|
两个命令的**意图分开记**(0.3.21):「针对选中内容提问」只有真的选了内容才带**行区间 + 选区正文**;
|
|
209
209
|
「针对当前文件提问」**永远不带行号、不带选区** —— 光标停在哪一行跟问题无关,行号只会误导 agent;
|
|
210
210
|
没选区时用选中命令提问也会退化成纯文件。
|
|
@@ -212,8 +212,12 @@ DSH 用**资源地址**命名文件,`openFile` 只负责把地址交给右侧栏
|
|
|
212
212
|
(`@deepseek-ai/dsh-client-ui-primitives` 的 `MarkdownText`)与官方设计令牌 —— 同一套 micromark/mdast 管线、
|
|
213
213
|
同一个增量流式解析器、同一个 shiki 高亮(启动集 typescript / shellscript / json)、KaTeX 公式、同样的标题与表格排版。
|
|
214
214
|
只渲染**新内容**(从面板订阅那一刻起),**不重放历史**、没有"加载更早"。
|
|
215
|
-
-
|
|
216
|
-
|
|
215
|
+
- **思考过程也照官方显示**(0.3.23):助手的 reasoning 以「思考」行出现 —— **默认收起**、收起时显示首行
|
|
216
|
+
(流式时显示最新一行)、点整行展开全文,用的就是官方 `DisclosureRow` + 官方的思考图标与排版语言。
|
|
217
|
+
- **授权就在面板里处理**(0.3.22;0.3.23 修好窗口):面板打开着的时候,该会话的授权请求**先问面板**(默认 5 分钟),
|
|
218
|
+
点「允许一次」/「拒绝」立刻生效;**关掉面板**或等满窗口就把请求**原样交回官方链路**(DSH 界面照旧弹卡)。
|
|
219
|
+
0.3.22 的 8 秒窗口对人来说太短 —— 卡片还没读完按钮就灰了(实测反馈「授权框失效了」),现在窗口是 5 分钟,
|
|
220
|
+
而且面板一关就立刻交回、不干等。
|
|
217
221
|
**永不自动放行** —— `allowed-once` 只能来自你的一次点击,面板里没有"以后都允许"这种入口。
|
|
218
222
|
- 提问进 DSH 会话时是**普通用户消息**(`source: { kind: 'user' }`,host 0.3.19 修正):早期版本用
|
|
219
223
|
`{kind:'plugin'}` 会被 DSH 渲染成"上下文更新",看起来不像自己说的话;来源信息靠正文首行
|
|
@@ -251,7 +255,7 @@ host → 扩展 <extensionsDir>/.dshcs-bridge/bridge.json 端点 + 令牌(
|
|
|
251
255
|
|---|---|---|
|
|
252
256
|
| `thread` | 被面板 `watch` 的会话的**新内容**条目(user / assistant / tool / approval),有界:每会话 ≤120 条、单条正文 ≤8000 字符、同时 watch ≤4 个会话 | 助手正文交给官方渲染器;工具与授权是紧凑摘要行 |
|
|
253
257
|
| `approvals` | 待决授权请求 `[{id, toolName, reason, at}]`(≤4 条) | 弹卡片 + 倒计时;点按后 `POST /approve` |
|
|
254
|
-
| `approvalHoldMs` / `uiVersion` | 授权窗口长度(默认
|
|
258
|
+
| `approvalHoldMs` / `uiVersion` | 授权窗口长度(默认 300000ms = 5 分钟)/ DSH 界面版本 | 倒计时基准;渲染器版本不一致时提示 |
|
|
255
259
|
|
|
256
260
|
> **为什么不是 HTTP(0.3.13 定论,三条都实测过)**
|
|
257
261
|
> 1. **desktop 根本没有 HTTP 面**:渲染进程经 Electron IPC 调 `host.fetch()`
|
|
@@ -286,7 +290,7 @@ host 反向请求不到它。所以编辑器状态只能在扩展主动发起的
|
|
|
286
290
|
(a) 只能**回答**已经存在的授权请求,请求体只有 `{id, outcome}`,**不接受任何自由文本 / 路径 / 命令参数**
|
|
287
291
|
—— 它只能"回答问题",不能"发起动作";(b) `id` 必须是本进程自己发起、且**仍未决**的请求(用后即废);
|
|
288
292
|
(c) `outcome` 只接受 `allowed-once` / `rejected`,**没有"永久允许"**;
|
|
289
|
-
(d) 没有面板在看 / 窗口超时(默认
|
|
293
|
+
(d) 没有面板在看 / 面板关掉 / 窗口超时(默认 5 分钟)→ **交回官方链路**,绝不自动放行
|
|
290
294
|
(DSH 的 `approval/request` 本身 fail closed,这里只能把"没人答"保持成"没人答")。
|
|
291
295
|
`pnpm test:webview-bundle` 里有针对这四条与宿主侧白名单的断言。
|
|
292
296
|
3. **带 `Origin` 的请求一律 403。** 浏览器发起必带 Origin(含沙箱 iframe 的 `Origin: null`),
|
|
@@ -732,8 +736,8 @@ desktop profile 由 `apps/desktop-host` 把 `/api/*` 交给同一个 `createShar
|
|
|
732
736
|
- **面板产物与 DSH 版本绑定**:渲染器按构建时 DSH 部署的界面版本打包,DSH 升级后**要重打面板**
|
|
733
737
|
(`pnpm run build:webview`;构建脚本会在版本不一致时直接报错)。运行期面板顶部也会提示版本不一致,
|
|
734
738
|
不会悄悄用错版本的渲染器。
|
|
735
|
-
- **面板里的授权窗口是
|
|
736
|
-
—— 交回之后这一条**只能**在 DSH 界面里处理(
|
|
739
|
+
- **面板里的授权窗口是 5 分钟**:面板打开着的时候授权先问面板(卡片上有倒计时);**关掉面板**或等满 5 分钟
|
|
740
|
+
就交回 DSH 界面 —— 交回之后这一条**只能**在 DSH 界面里处理(卡片从面板消失,对话流里留一行授权审计)。
|
|
737
741
|
|
|
738
742
|
- ~~子路径不支持~~ **已不成立(0.2.0 实测更正)**:VS Code 渲染出的 workbench HTML 里
|
|
739
743
|
**资源引用全是相对路径**(实测 9 条引用中绝对路径 0 条,`serverBasePath="."`、`rootEndpoint="."`),
|
|
@@ -539,8 +539,10 @@ function openAskPanel(mode = 'selection') {
|
|
|
539
539
|
}
|
|
540
540
|
const panel = vscode.window.createWebviewPanel(
|
|
541
541
|
'dshAsk',
|
|
542
|
-
'DSH
|
|
543
|
-
|
|
542
|
+
'DSH 对话',
|
|
543
|
+
// 对话框形状(0.2.4):开在**当前编辑器组**,占满工作台宽度 —— 不再用 `Beside` 挤成一条侧栏。
|
|
544
|
+
// 面板内容自己居中限宽,看起来就是编辑器上的一扇对话窗;右上角的 ✕ 关掉它。
|
|
545
|
+
{ viewColumn: vscode.ViewColumn.Active, preserveFocus: false },
|
|
544
546
|
{
|
|
545
547
|
enableScripts: true,
|
|
546
548
|
retainContextWhenHidden: true,
|
|
@@ -568,6 +570,11 @@ function openAskPanel(mode = 'selection') {
|
|
|
568
570
|
}
|
|
569
571
|
if (message.type === 'approve' && typeof message.id === 'string') {
|
|
570
572
|
void decideApproval(message.id, message.outcome);
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
575
|
+
if (message.type === 'close') {
|
|
576
|
+
// 面板上的 ✕:关掉它(= 不再看这个会话 ⇒ 待决授权立刻交回 DSH 界面,不用干等窗口)。
|
|
577
|
+
panel.dispose();
|
|
571
578
|
}
|
|
572
579
|
});
|
|
573
580
|
panel.onDidDispose(() => {
|
|
@@ -576,7 +583,8 @@ function openAskPanel(mode = 'selection') {
|
|
|
576
583
|
}
|
|
577
584
|
askPanel.state.context = contextInfo;
|
|
578
585
|
askPanel.mode = mode;
|
|
579
|
-
|
|
586
|
+
// 已经开着就提到前面(对话框不重复开)。
|
|
587
|
+
askPanel.panel.reveal(vscode.ViewColumn.Active, false);
|
|
580
588
|
refreshAskPanel();
|
|
581
589
|
}
|
|
582
590
|
|
|
@@ -74,6 +74,8 @@ function cleanEntry(raw) {
|
|
|
74
74
|
return {
|
|
75
75
|
role,
|
|
76
76
|
text,
|
|
77
|
+
// 思考过程(0.3.23):渲染成默认收起的「思考」行。
|
|
78
|
+
thinking: typeof raw.thinking === 'string' ? raw.thinking.slice(0, MAX_TEXT) : '',
|
|
77
79
|
streaming: raw.streaming === true,
|
|
78
80
|
name: typeof raw.name === 'string' ? raw.name : null,
|
|
79
81
|
summary: typeof raw.summary === 'string' ? raw.summary : null,
|
|
@@ -84,13 +86,20 @@ function cleanEntry(raw) {
|
|
|
84
86
|
}
|
|
85
87
|
|
|
86
88
|
/**
|
|
87
|
-
*
|
|
89
|
+
* 条目签名:正文/思考只会"流式变长"或"被耐久消息原地替换"(此时 streaming 翻转),
|
|
88
90
|
* 所以 角色 + 状态 + 长度 + 是否流式 足够判变化,不必每趟比对几十 KB 文本。
|
|
89
91
|
*/
|
|
90
92
|
function entriesSignature(entries) {
|
|
91
93
|
const parts = [];
|
|
92
94
|
for (const entry of entries) {
|
|
93
|
-
parts.push(
|
|
95
|
+
parts.push([
|
|
96
|
+
entry.role,
|
|
97
|
+
entry.status ?? '',
|
|
98
|
+
entry.streaming ? 1 : 0,
|
|
99
|
+
entry.text.length,
|
|
100
|
+
entry.thinking.length,
|
|
101
|
+
entry.summary === null ? '' : entry.summary.length,
|
|
102
|
+
].join(':'));
|
|
94
103
|
}
|
|
95
104
|
return `${entries.length}|${parts.join(',')}`;
|
|
96
105
|
}
|
|
@@ -100,6 +109,7 @@ function pendingEntries(state) {
|
|
|
100
109
|
return state.pending.map((item) => ({
|
|
101
110
|
role: 'user',
|
|
102
111
|
text: item.text,
|
|
112
|
+
thinking: '',
|
|
103
113
|
streaming: false,
|
|
104
114
|
name: null,
|
|
105
115
|
summary: null,
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dshcs-editor-bridge",
|
|
3
3
|
"displayName": "DSH Editor Bridge",
|
|
4
|
-
"description": "只读地把编辑器状态(未保存缓冲区、诊断、活动选区)提供给 DSH,并让编辑器里的动作驱动 DSH 会话;0.2.3 起「问 DSH」面板用 DSH 官方 markdown
|
|
5
|
-
"version": "0.2.
|
|
4
|
+
"description": "只读地把编辑器状态(未保存缓冲区、诊断、活动选区)提供给 DSH,并让编辑器里的动作驱动 DSH 会话;0.2.3 起「问 DSH」面板用 DSH 官方 markdown 渲染器显示该会话的新内容,0.2.4 起是对话框形状 + 思考过程默认折叠 + 就地回答授权请求(写工作区外文件 / 执行命令)。由 dsh-code-server-app 插件安装,可禁用。",
|
|
5
|
+
"version": "0.2.4",
|
|
6
6
|
"publisher": "dsh-code-server-app",
|
|
7
7
|
"license": "MIT",
|
|
8
8
|
"private": true,
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
| react | 18.3.1 | MIT |
|
|
12
12
|
| react-dom | 18.3.1 | MIT |
|
|
13
13
|
|
|
14
|
-
生成时间:2026-09-13T08:
|
|
14
|
+
生成时间:2026-09-13T08:52:40.615Z
|
|
15
15
|
渲染器:@deepseek-ai/dsh-client-ui-primitives@0.1.5-rc.2(DSH 部署的界面版本:0.1.5-rc.2)
|
|
16
16
|
设计令牌:@deepseek-ai/dsh-client-ui-theme@0.1.5-rc.2(5 段)
|
|
17
17
|
KaTeX:已打包(公式排版与 DSH 界面一致)
|
|
@@ -143,8 +143,18 @@ function App() {
|
|
|
143
143
|
return (
|
|
144
144
|
<div className="dshcs-app">
|
|
145
145
|
<header className="dshcs-header">
|
|
146
|
+
<span className="dshcs-title">DSH</span>
|
|
146
147
|
<span className="dshcs-where">{view.contextText}</span>
|
|
147
148
|
{RENDERER_VERSION === '' ? null : <span className="dshcs-version">渲染器 {RENDERER_VERSION}</span>}
|
|
149
|
+
<button
|
|
150
|
+
type="button"
|
|
151
|
+
className="dshcs-close"
|
|
152
|
+
title="关闭(Shift+Esc)"
|
|
153
|
+
aria-label="关闭"
|
|
154
|
+
onClick={() => vscode.postMessage({ type: 'close' })}
|
|
155
|
+
>
|
|
156
|
+
✕
|
|
157
|
+
</button>
|
|
148
158
|
</header>
|
|
149
159
|
|
|
150
160
|
{view.threadError === null ? null : <div className="dshcs-warn">{view.threadError}</div>}
|
|
@@ -152,7 +162,7 @@ function App() {
|
|
|
152
162
|
|
|
153
163
|
<main className="dshcs-log" ref={logRef} onScroll={onScroll}>
|
|
154
164
|
{view.entries.length === 0
|
|
155
|
-
? <div className="dshcs-empty">在下面提问:DSH 的回答会像 DSH
|
|
165
|
+
? <div className="dshcs-empty">在下面提问:DSH 的回答会像 DSH 界面那样显示在这里(思考折叠、正文按官方渲染)。</div>
|
|
156
166
|
: view.entries.map((entry, index) => (
|
|
157
167
|
<ThreadEntry key={`${entry.role}-${index}-${entry.callId ?? entry.approvalId ?? ''}`} entry={entry} />
|
|
158
168
|
))}
|
|
@@ -1,32 +1,41 @@
|
|
|
1
|
-
// dshcs-editor-bridge / webview/src/approval.jsx —— 待决授权卡片(0.2.3)
|
|
1
|
+
// dshcs-editor-bridge / webview/src/approval.jsx —— 待决授权卡片(0.2.3;0.2.4 修"失效")
|
|
2
2
|
//
|
|
3
3
|
// 背景(见 lib/bridge-approval.mjs):DSH 的敏感动作(写工作区外的文件、执行命令)会走
|
|
4
4
|
// `approval/request` 这条 agent 作用域的 waterfall。面板在旁边看的时候先接住它:
|
|
5
5
|
// - 用户在这张卡片上点「允许一次 / 拒绝」→ 立刻生效(官方卡片不再出现);
|
|
6
|
-
// -
|
|
6
|
+
// - **面板关掉**(或等满宿主给的窗口,默认 5 分钟)→ 交回官方链路,DSH 界面里照旧弹同一张卡片。
|
|
7
|
+
//
|
|
8
|
+
// 0.2.3 的教训(用户实测:"授权框失效了"):卡片自己按 8 秒判过期 → 读完再点,按钮已经是灰的。
|
|
9
|
+
// 现在**不再由前端判过期**:只要卡片还在(宿主这边仍未决),按钮就可点;请求被交回官方链路时
|
|
10
|
+
// 它会从 `approvals` 里消失,面板改为显示审计行。倒计时只是"还有多久交回官方链路"的提示,
|
|
11
|
+
// 不再决定能不能点。
|
|
7
12
|
//
|
|
8
13
|
// 卡片只做两件事:**显示**这次请求要干什么(工具名 + 原因),**回答**它。
|
|
9
14
|
// 没有任何"记住这次选择""以后都允许"的入口 —— 授权只对这一次动作有效。
|
|
10
15
|
|
|
11
|
-
/**
|
|
12
|
-
function
|
|
13
|
-
const
|
|
14
|
-
|
|
16
|
+
/** 剩余时间文案(超过一分钟就 mm:ss,避免秒数跳动带来的紧张感)。 */
|
|
17
|
+
function remainingText(item, holdMs, now) {
|
|
18
|
+
const at = Number.isSafeInteger(item.at) ? item.at : now;
|
|
19
|
+
const left = Math.max(0, at + holdMs - now);
|
|
20
|
+
if (left === 0) return '已交回 DSH 界面';
|
|
21
|
+
const totalSeconds = Math.ceil(left / 1000);
|
|
22
|
+
const minutes = Math.floor(totalSeconds / 60);
|
|
23
|
+
const seconds = totalSeconds % 60;
|
|
24
|
+
return minutes > 0
|
|
25
|
+
? `${minutes} 分 ${String(seconds).padStart(2, '0')} 秒后交回 DSH 界面`
|
|
26
|
+
: `${seconds} 秒后交回 DSH 界面`;
|
|
15
27
|
}
|
|
16
28
|
|
|
17
29
|
export function ApprovalCard({ item, holdMs, now, decided, onDecide }) {
|
|
18
|
-
const left = secondsLeft(item, holdMs, now);
|
|
19
|
-
const expired = left <= 0;
|
|
20
30
|
const choice = decided ?? null;
|
|
21
|
-
|
|
31
|
+
// 已提交(点了但宿主还没确认):锁住按钮避免连点;这张卡片何时消失由宿主决定。
|
|
32
|
+
const locked = choice !== null;
|
|
22
33
|
|
|
23
34
|
return (
|
|
24
35
|
<div className="dshcs-approval" data-decided={choice ?? undefined}>
|
|
25
36
|
<div className="dshcs-approval-head">
|
|
26
37
|
<span className="dshcs-approval-title">需要你的授权</span>
|
|
27
|
-
<span className="dshcs-approval-timer">
|
|
28
|
-
{expired ? '已交给 DSH 界面' : `${left}s 后交给 DSH 界面`}
|
|
29
|
-
</span>
|
|
38
|
+
<span className="dshcs-approval-timer">{remainingText(item, holdMs, now)}</span>
|
|
30
39
|
</div>
|
|
31
40
|
<div className="dshcs-approval-body">
|
|
32
41
|
<span className="dshcs-approval-tool">{item.toolName}</span>
|
|
@@ -39,14 +48,13 @@ export function ApprovalCard({ item, holdMs, now, decided, onDecide }) {
|
|
|
39
48
|
<button type="button" disabled={locked} onClick={() => onDecide(item.id, 'rejected')}>
|
|
40
49
|
拒绝
|
|
41
50
|
</button>
|
|
42
|
-
{choice === null
|
|
43
|
-
<span className="dshcs-approval-note">
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
) : null}
|
|
51
|
+
{choice === null
|
|
52
|
+
? <span className="dshcs-approval-note">只对这一次动作有效;关掉面板即交回 DSH 界面</span>
|
|
53
|
+
: (
|
|
54
|
+
<span className="dshcs-approval-note">
|
|
55
|
+
{choice === 'allowed-once' ? '已允许一次(DSH 继续执行)' : '已拒绝'}
|
|
56
|
+
</span>
|
|
57
|
+
)}
|
|
50
58
|
</div>
|
|
51
59
|
</div>
|
|
52
60
|
);
|
|
@@ -36,6 +36,10 @@ body {
|
|
|
36
36
|
display: flex;
|
|
37
37
|
flex-direction: column;
|
|
38
38
|
height: 100%;
|
|
39
|
+
/* 对话框形状(0.2.4):面板开在编辑器区、占满工作台宽度,内容居中限宽。 */
|
|
40
|
+
width: 100%;
|
|
41
|
+
max-width: 900px;
|
|
42
|
+
margin: 0 auto;
|
|
39
43
|
}
|
|
40
44
|
|
|
41
45
|
.dshcs-header {
|
|
@@ -48,6 +52,11 @@ body {
|
|
|
48
52
|
opacity: 0.9;
|
|
49
53
|
}
|
|
50
54
|
|
|
55
|
+
.dshcs-header .dshcs-title {
|
|
56
|
+
font-weight: 600;
|
|
57
|
+
letter-spacing: 0.04em;
|
|
58
|
+
}
|
|
59
|
+
|
|
51
60
|
.dshcs-header .dshcs-where {
|
|
52
61
|
flex: 1;
|
|
53
62
|
overflow: hidden;
|
|
@@ -61,6 +70,60 @@ body {
|
|
|
61
70
|
font-size: 11px;
|
|
62
71
|
}
|
|
63
72
|
|
|
73
|
+
.dshcs-header .dshcs-close {
|
|
74
|
+
border: none;
|
|
75
|
+
background: transparent;
|
|
76
|
+
color: inherit;
|
|
77
|
+
cursor: pointer;
|
|
78
|
+
font-size: 13px;
|
|
79
|
+
line-height: 1;
|
|
80
|
+
padding: 2px 6px;
|
|
81
|
+
border-radius: 4px;
|
|
82
|
+
opacity: 0.75;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.dshcs-header .dshcs-close:hover {
|
|
86
|
+
background: var(--vscode-toolbar-hoverBackground, rgba(128, 128, 128, 0.2));
|
|
87
|
+
opacity: 1;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/* ---- 思考行(默认收起;外形照抄官方 ReasoningRow 的排版语言) ---- */
|
|
91
|
+
|
|
92
|
+
.dshcs-think {
|
|
93
|
+
display: flex;
|
|
94
|
+
flex-direction: column;
|
|
95
|
+
margin: 2px 0 6px;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.dshcs-think .dshcs-think-separator {
|
|
99
|
+
flex: none;
|
|
100
|
+
width: 2px;
|
|
101
|
+
height: 2px;
|
|
102
|
+
margin: 0 8px;
|
|
103
|
+
border-radius: 1px;
|
|
104
|
+
background: var(--dsw-alias-label-caption, currentColor);
|
|
105
|
+
display: inline-block;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.dshcs-think .dshcs-think-summary {
|
|
109
|
+
min-width: 0;
|
|
110
|
+
overflow: hidden;
|
|
111
|
+
flex: 1 1 auto;
|
|
112
|
+
color: var(--dsw-alias-label-tertiary, var(--vscode-descriptionForeground));
|
|
113
|
+
font-size: var(--dsh-content-font-size-secondary, 13px);
|
|
114
|
+
white-space: nowrap;
|
|
115
|
+
text-overflow: ellipsis;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.dshcs-think .dshcs-think-body {
|
|
119
|
+
padding: 4px 0 4px calc(22px + var(--dsh-content-font-delta, 0px));
|
|
120
|
+
color: var(--dsw-alias-label-tertiary, var(--vscode-descriptionForeground));
|
|
121
|
+
font-size: var(--dsh-content-font-size-secondary, 13px);
|
|
122
|
+
line-height: 1.55;
|
|
123
|
+
white-space: pre-wrap;
|
|
124
|
+
word-break: break-word;
|
|
125
|
+
}
|
|
126
|
+
|
|
64
127
|
.dshcs-warn {
|
|
65
128
|
margin: 8px 12px 0;
|
|
66
129
|
padding: 6px 8px;
|
|
@@ -1,14 +1,18 @@
|
|
|
1
|
-
// dshcs-editor-bridge / webview/src/thread.jsx —— 一个对话条目的渲染(0.2.3)
|
|
1
|
+
// dshcs-editor-bridge / webview/src/thread.jsx —— 一个对话条目的渲染(0.2.3 起;0.2.4 加思考行)
|
|
2
2
|
//
|
|
3
3
|
// 助手正文**不再自己排版**:直接调用 DSH 官方渲染器
|
|
4
4
|
// (`@deepseek-ai/dsh-client-ui-primitives` 的 `MarkdownText` —— 与 DSH 界面同一份代码,
|
|
5
5
|
// 同一套 mdast 管线、KaTeX、shiki 高亮、增量流式解析),面板只负责"外壳"。
|
|
6
6
|
//
|
|
7
|
+
// 思考过程(0.2.4):DSH 界面把它渲染成一行可折叠的「思考」(官方 `ReasoningRow` 用的就是
|
|
8
|
+
// `DisclosureRow` + `IconThinkOutline14`,**默认收起**,收起时显示首行 / 流式时的最新一行)。
|
|
9
|
+
// 面板用同一套官方部件重现它,差别只在文案与类名。
|
|
10
|
+
//
|
|
7
11
|
// 用户消息:DSH 界面里用户消息是纯文本气泡,这里保持一致(不渲染 markdown)。
|
|
8
|
-
// 工具 / 授权:面板只给一行紧凑摘要(
|
|
12
|
+
// 工具 / 授权:面板只给一行紧凑摘要(不做逐工具的自定义卡片)。
|
|
9
13
|
|
|
10
|
-
import { memo } from 'react';
|
|
11
|
-
import { MarkdownText } from '@deepseek-ai/dsh-client-ui-primitives';
|
|
14
|
+
import { memo, useState } from 'react';
|
|
15
|
+
import { DisclosureRow, IconThinkOutline14, MarkdownText } from '@deepseek-ai/dsh-client-ui-primitives';
|
|
12
16
|
|
|
13
17
|
/** 官方渲染器的界面文案(面板自己给中文;DSH 界面走它自己的 locale)。 */
|
|
14
18
|
const LABELS = { code: { copyLabel: '复制', copiedLabel: '已复制' }, footnotes: '脚注' };
|
|
@@ -25,6 +29,43 @@ const APPROVAL_STATE = {
|
|
|
25
29
|
unavailable: '无人处理',
|
|
26
30
|
};
|
|
27
31
|
|
|
32
|
+
/** 折叠摘要取首行(还在流式时取最新一行)—— 与官方 ReasoningRow 同一规则。 */
|
|
33
|
+
function summaryLine(text, running) {
|
|
34
|
+
const source = running ? text.trimEnd() : text;
|
|
35
|
+
if (source === '') return '';
|
|
36
|
+
const index = running ? source.lastIndexOf('\n') : source.indexOf('\n');
|
|
37
|
+
const line = index === -1 ? source : (running ? source.slice(index + 1) : source.slice(0, index));
|
|
38
|
+
return line.replaceAll('**', '');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* 一行「思考」(默认收起,点行即展开)—— 外形与交互照抄官方 `ReasoningRow`。
|
|
43
|
+
* @param {{text: string, running: boolean}} props running = 这一轮的正文还没开始落
|
|
44
|
+
*/
|
|
45
|
+
function ThinkingRow({ text, running }) {
|
|
46
|
+
const [expanded, setExpanded] = useState(false);
|
|
47
|
+
return (
|
|
48
|
+
<div className="dshcs-think" data-state={running ? 'running' : 'ok'} data-expanded={expanded || undefined}>
|
|
49
|
+
<DisclosureRow
|
|
50
|
+
icon={<IconThinkOutline14 size={14} />}
|
|
51
|
+
title={running ? '思考中' : '思考'}
|
|
52
|
+
open={expanded}
|
|
53
|
+
expandable
|
|
54
|
+
expandOnRowClick
|
|
55
|
+
onToggle={() => setExpanded((value) => !value)}
|
|
56
|
+
collapsedContent={(
|
|
57
|
+
<>
|
|
58
|
+
<span className="dshcs-think-separator" aria-hidden />
|
|
59
|
+
<span className="dshcs-think-summary">{summaryLine(text, running)}</span>
|
|
60
|
+
</>
|
|
61
|
+
)}
|
|
62
|
+
>
|
|
63
|
+
<div className="dshcs-think-body">{text}</div>
|
|
64
|
+
</DisclosureRow>
|
|
65
|
+
</div>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
28
69
|
export const ThreadEntry = memo(function ThreadEntry({ entry }) {
|
|
29
70
|
if (entry.role === 'user') {
|
|
30
71
|
return (
|
|
@@ -35,13 +76,15 @@ export const ThreadEntry = memo(function ThreadEntry({ entry }) {
|
|
|
35
76
|
}
|
|
36
77
|
|
|
37
78
|
if (entry.role === 'assistant') {
|
|
79
|
+
const thinking = typeof entry.thinking === 'string' ? entry.thinking : '';
|
|
80
|
+
// 正文还没到(整轮都是思考)→ 思考行标"思考中";正文到了就标"思考"(官方同一判据)。
|
|
81
|
+
const running = entry.streaming === true && entry.text === '';
|
|
38
82
|
return (
|
|
39
83
|
<div className="dshcs-msg dshcs-assistant">
|
|
40
|
-
<
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
labels={LABELS}
|
|
44
|
-
/>
|
|
84
|
+
{thinking === '' ? null : <ThinkingRow text={thinking} running={running} />}
|
|
85
|
+
{entry.text === ''
|
|
86
|
+
? null
|
|
87
|
+
: <MarkdownText text={entry.text} streaming={entry.streaming === true} labels={LABELS} />}
|
|
45
88
|
</div>
|
|
46
89
|
);
|
|
47
90
|
}
|