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.
Files changed (42) hide show
  1. package/README.en.md +77 -18
  2. package/README.md +70 -20
  3. package/assets/extensions/dshcs-editor-bridge/extension.js +83 -47
  4. package/assets/extensions/dshcs-editor-bridge/lib/ask-panel.js +343 -174
  5. package/assets/extensions/dshcs-editor-bridge/lib/bridge-client.js +30 -4
  6. package/assets/extensions/dshcs-editor-bridge/package.json +2 -2
  7. package/assets/extensions/dshcs-editor-bridge/webview/THIRD-PARTY.md +21 -0
  8. package/assets/extensions/dshcs-editor-bridge/webview/fonts/KaTeX_AMS-Regular-U6PRYMIZ.woff2 +0 -0
  9. package/assets/extensions/dshcs-editor-bridge/webview/fonts/KaTeX_Caligraphic-Bold-5QL5CMTE.woff2 +0 -0
  10. package/assets/extensions/dshcs-editor-bridge/webview/fonts/KaTeX_Caligraphic-Regular-KX5MEWCF.woff2 +0 -0
  11. package/assets/extensions/dshcs-editor-bridge/webview/fonts/KaTeX_Fraktur-Bold-2QVFK6NQ.woff2 +0 -0
  12. package/assets/extensions/dshcs-editor-bridge/webview/fonts/KaTeX_Fraktur-Regular-2PEIFJSJ.woff2 +0 -0
  13. package/assets/extensions/dshcs-editor-bridge/webview/fonts/KaTeX_Main-Bold-YP5VVQRP.woff2 +0 -0
  14. package/assets/extensions/dshcs-editor-bridge/webview/fonts/KaTeX_Main-BoldItalic-N4V3DX7S.woff2 +0 -0
  15. package/assets/extensions/dshcs-editor-bridge/webview/fonts/KaTeX_Main-Italic-RELBIK7M.woff2 +0 -0
  16. package/assets/extensions/dshcs-editor-bridge/webview/fonts/KaTeX_Main-Regular-ARRPAO67.woff2 +0 -0
  17. package/assets/extensions/dshcs-editor-bridge/webview/fonts/KaTeX_Math-BoldItalic-K4WTGH3J.woff2 +0 -0
  18. package/assets/extensions/dshcs-editor-bridge/webview/fonts/KaTeX_Math-Italic-6KGCHLFN.woff2 +0 -0
  19. package/assets/extensions/dshcs-editor-bridge/webview/fonts/KaTeX_SansSerif-Bold-RRNVJFFW.woff2 +0 -0
  20. package/assets/extensions/dshcs-editor-bridge/webview/fonts/KaTeX_SansSerif-Italic-HMPFTM52.woff2 +0 -0
  21. package/assets/extensions/dshcs-editor-bridge/webview/fonts/KaTeX_SansSerif-Regular-XIQ62X4E.woff2 +0 -0
  22. package/assets/extensions/dshcs-editor-bridge/webview/fonts/KaTeX_Script-Regular-APUWIHLP.woff2 +0 -0
  23. package/assets/extensions/dshcs-editor-bridge/webview/fonts/KaTeX_Size1-Regular-5LRUTBFT.woff2 +0 -0
  24. package/assets/extensions/dshcs-editor-bridge/webview/fonts/KaTeX_Size2-Regular-LELKET5D.woff2 +0 -0
  25. package/assets/extensions/dshcs-editor-bridge/webview/fonts/KaTeX_Size3-Regular-WQRQ47UD.woff2 +0 -0
  26. package/assets/extensions/dshcs-editor-bridge/webview/fonts/KaTeX_Size4-Regular-CDMV7U5C.woff2 +0 -0
  27. package/assets/extensions/dshcs-editor-bridge/webview/fonts/KaTeX_Typewriter-Regular-VBYJ4NRC.woff2 +0 -0
  28. package/assets/extensions/dshcs-editor-bridge/webview/src/app.jsx +189 -0
  29. package/assets/extensions/dshcs-editor-bridge/webview/src/approval.jsx +53 -0
  30. package/assets/extensions/dshcs-editor-bridge/webview/src/official-tokens.css +8 -0
  31. package/assets/extensions/dshcs-editor-bridge/webview/src/panel.css +283 -0
  32. package/assets/extensions/dshcs-editor-bridge/webview/src/thread.jsx +72 -0
  33. package/assets/extensions/dshcs-editor-bridge/webview/thread.css +1 -0
  34. package/assets/extensions/dshcs-editor-bridge/webview/thread.js +462 -0
  35. package/lib/bridge-approval.mjs +189 -0
  36. package/lib/bridge-session.mjs +3 -1
  37. package/lib/bridge-thread.mjs +345 -0
  38. package/lib/bridge.mjs +15 -6
  39. package/lib/index.js +142 -17
  40. package/package.json +29 -6
  41. package/vendor/VENDOR.json +1 -1
  42. package/lib/bridge-answer.mjs +0 -155
package/README.en.md CHANGED
@@ -12,6 +12,11 @@
12
12
 
13
13
  A static profile plugin (npm package with host + client bundle) that ships the **VS Code server tree** from a [code-server](https://github.com/coder/code-server) release as a **platform-independent dependency package** (pack-time artifact `vendor/vscode` → `@jinsiyu/dshcs-vscode-server`, no install scripts, no postinstall). The code-server **Node service layer is replaced by the plugin's own `lib/launcher.mjs`**: it drives `<tree>/lib/vscode/out/server-main.js` (`loadCodeWithNls()` / `createServer()` / `handleRequest()` / `handleUpgrade()`) directly and re-adds the few HTTP endpoints code-server used to provide (`/healthz`, `/manifest.json`, `/_static/*`, `/proxy/:port`). The 16 native modules (node-pty / @vscode/sqlite3 / spdlog / …) come from `@jinsiyu/dshcs-*-win32-<arch>` platform packages selected automatically per architecture by the platform aggregator. VS Code's inner dependencies and the prebuilt native modules are **all installed by the package manager together with the plugin** — no global npm install, no `bin` configuration, no profile config changes, no second install command, **no argon2/C++ toolchain**.
14
14
 
15
+ > Since 0.3.22 the ask panel renders the session's new content with **DSH's own Markdown renderer** (the same
16
+ > renderer and design tokens as the DSH UI; new content only) and can **answer approval requests in place**
17
+ > (writing outside the workspace / running commands). See "Working with DSH: the editor bridge" and section 21 of
18
+ > `docs/analysis-code-server-as-dsh-plugin.md`.
19
+
15
20
  ## UI carrier and required DSH version (0.2.3: right-sidebar DSH only)
16
21
 
17
22
  | DSH version | Carrier | Entry points |
@@ -198,23 +203,35 @@ only the editor knows, and lets editor gestures drive the current session.
198
203
  | Direction | Capability | Mechanism |
199
204
  |---|---|---|
200
205
  | editor → agent | **unsaved buffers** (disk ≠ what the user sees), active file and selection, **language-server diagnostics** with `file:line`, source and code | agent tools `editor_context` / `editor_diagnostics`; plus a notice attached before writing a dirty file |
201
- | editor → DSH | select code → context menu **"DSH: ask about selection"** → an **ask panel** opens (carrying `file:line` and the selection); the question enters the current session as **user input**, and the reply is mirrored back into the panel | extension command `dsh-code-server.askAboutSelection` (one of the **top two** editor context-menu items) + a webview panel + `POST /ask` + the `answers` field of `/sync` |
206
+ | editor → DSH | select code → context menu **"DSH: ask about selection"** → an **ask panel** opens (carrying `file:line` and the selection); the question enters the current session as **user input**, and that session's **new content** is rendered in the panel by DSH's own Markdown renderer | extension command `dsh-code-server.askAboutSelection` (one of the **top two** editor context-menu items) + a webview panel + `POST /ask` + the `thread` field of `/sync` |
207
+ | DSH → editor (approval) | when the agent wants to **write outside the workspace or run a command**, the approval request shows up as a card in the panel (tool, reason, countdown); "allow once" / "reject" takes effect immediately | the `approvals` field of `/sync` + `POST /approve` (the bridge's **only** non-read-only route; constraints under "Security model") |
202
208
  | agent → editor | the agent changed a file → a **native diff** opens; if that buffer has unsaved changes you get a warning and **no overwrite** | host watches `tools/result`, the extension polls and opens the diff |
203
209
 
204
210
  - The tools are only registered while the bridge is live (so the model never sees an unusable tool), and the
205
211
  system-prompt section renders only then too.
206
- - **Ask panel** (extension 0.2.0): the context-menu command no longer pops a one-line input box but opens a panel —
207
- transcript on top, input at the bottom. The selection is captured **at send time** (so you can change it while the
208
- panel stays open), and the reply refreshes with the same polling round trip: no need to switch back to the DSH UI.
212
+ - **Ask panel** (extension 0.2.0; since 0.2.3 the body goes through the official renderer): the context-menu command
213
+ no longer pops a one-line input box but opens a panel — that session's **new content** on top, input at the bottom.
214
+ The selection is captured **at send time** (so you can change it while the panel stays open).
209
215
  The two commands **remember their intent** (0.3.21): "ask about selection" carries a **line range + selection text**
210
216
  only when something is actually selected, while "ask about file" **never carries line numbers or a selection** — the
211
217
  cursor line is irrelevant to the question and only misleads the agent. With no selection, the selection command also
212
218
  degrades to the plain file.
219
+ - **The panel renders exactly what DSH renders** (0.3.22): the panel bundles DSH's official Markdown renderer
220
+ (`MarkdownText` from `@deepseek-ai/dsh-client-ui-primitives`) plus the official design tokens — the same
221
+ micromark/mdast pipeline, the same incremental streaming parser, the same shiki highlighting (boot set:
222
+ typescript / shellscript / json), KaTeX math and the same heading/table typography. Only **new content** is
223
+ rendered (from the moment the panel subscribes); history is **not replayed** and there is no "load earlier".
224
+ - **Approvals are handled right in the panel** (0.3.22): while the panel is open, that session's approval requests
225
+ ask the panel first (8 s by default). Clicking "allow once" / "reject" settles it immediately; if nobody answers,
226
+ the request is handed back **unchanged** to the official path (the DSH UI shows the same card).
227
+ **Nothing is ever auto-approved** — `allowed-once` can only come from a click, and there is no "always allow".
213
228
  - The question enters the DSH session as a **plain user message** (`source: { kind: 'user' }`, host 0.3.19): earlier
214
229
  versions used `{kind:'plugin'}`, which DSH renders as a *context update* — it did not look like something the user
215
230
  said. Provenance stays in the first line of the text: `From the editor: <file>[:<line>]`.
216
- - **Everything is read-only**: the bridge never writes files, applies edits, or runs commands. The agent's writes
217
- still go through its own `fs` tools; the bridge only *knows about* them.
231
+ - **Read-only with one constrained exception**: the bridge never writes files, applies edits, or runs commands; the
232
+ single non-read-only route is `POST /approve`, which can only **answer an approval request that already exists**
233
+ (see invariant 2 below). The agent's writes still go through its own `fs` tools; the bridge only *knows about* them
234
+ and carries your answer back.
218
235
  - Status bar shows `$(plug) DSH` while connected (click it for the log in the "DSH Editor Bridge" output channel).
219
236
  - **The extension ships as a built-in** (fixed in 0.3.12): `dshcs-editor-bridge` is installed into
220
237
  `<tree>/lib/vscode/extensions/` next to `dshcs-open-file`. 0.3.0–0.3.11 installed it as a *user* extension
@@ -226,8 +243,9 @@ only the editor knows, and lets editor gestures drive the current session.
226
243
  ### The channels (since 0.3.13 over **local IPC**: a Windows named pipe / unix socket)
227
244
 
228
245
  ```
229
- extension → host POST /code-server-bridge/sync one round trip: push editor state + take pending events
246
+ extension → host POST /code-server-bridge/sync one round trip: push editor state (+ which session the panel watches) + take events and thread deltas
230
247
  extension → host POST /code-server-bridge/ask push an editor question into the current session
248
+ extension → host POST /code-server-bridge/approve answer an approval request that **already exists** (the only non-read-only route)
231
249
  extension → host GET /code-server-bridge/health unauthenticated liveness probe
232
250
  extension → host POST /code-server-bridge/event extension reports open/close etc. (host log tail)
233
251
  host → extension <extensionsDir>/.dshcs-bridge/bridge.json endpoint + token, re-read every 5s
@@ -239,6 +257,14 @@ host → extension <extensionsDir>/.dshcs-bridge/bridge.json endpoint + toke
239
257
 
240
258
  Requests use `http.request({ socketPath })` (`fetch` has no socket support) and **no port is ever opened**.
241
259
 
260
+ The three `/sync` fields the panel actually consumes (0.3.22):
261
+
262
+ | Field | Content | How the panel uses it |
263
+ |---|---|---|
264
+ | `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
+ | `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 (8000 ms by default) / the DSH UI version | countdown basis; a renderer-version mismatch is surfaced in the panel |
267
+
242
268
  > **Why not HTTP (settled in 0.3.13, all three measured)**
243
269
  > 1. **Desktop has no HTTP surface at all**: the renderer calls `host.fetch()` through Electron IPC
244
270
  > (`createSharedFetchHandler('/api')` in `apps/desktop-host/src/index.ts:308`) — an in-process call, unreachable
@@ -264,20 +290,29 @@ of passing stale data off as fresh).
264
290
  request/response round trip every 600 ms. Polling also buys two useful properties: it is idempotent (a dropped event
265
291
  only costs one notification — the data always lives in the editor) and the cached state is inherently fresh.
266
292
 
267
- ### Security model (four invariants; read before touching `lib/bridge.mjs`)
293
+ ### Security model (five invariants; read before touching `lib/bridge.mjs`)
268
294
 
269
295
  The token lives in `<extensionsDir>/.dshcs-bridge/bridge.json`, **readable by any process of the same local
270
296
  user**, so:
271
297
 
272
- 1. **`/code-server-bridge/*` is permanently read-only.** No route writes files, edits documents, or runs
273
- commands. A leaked token is therefore bounded to "sees information that is in the editor" and **can never**
274
- become arbitrary file writes or command execution. A whitelist assertion in `scripts/test-bridge-routes.mjs`
275
- guards this.
276
- 2. **Any request carrying `Origin` gets 403.** Browsers always send one (including a sandboxed iframe's literal
298
+ 1. **`/code-server-bridge/*` is read-only, with `/approve` as the single exception.** No route writes files,
299
+ edits documents, runs commands, or spawns processes. A leaked token is therefore bounded to "sees information
300
+ that is in the editor" and **can never** become arbitrary file writes or command execution. A whitelist
301
+ assertion in `scripts/test-bridge-routes.mjs` guards this.
302
+ 2. **The four constraints on `/approve`** (drop one and it becomes an arbitrary-command-execution back door):
303
+ (a) it can only **answer** an approval request that already exists — the body is exactly `{id, outcome}`, with
304
+ **no free text, paths, or command arguments**, so it can answer questions but never start an action;
305
+ (b) `id` must belong to a request this process created and that is **still pending** (single use);
306
+ (c) `outcome` accepts only `allowed-once` / `rejected` — there is **no "always allow"**;
307
+ (d) when no panel is watching, or the window (8 s by default) expires, the request goes **back to the official
308
+ path** — never auto-approved (DSH's `approval/request` itself fails closed; this bridge can only keep
309
+ "nobody answered" as "nobody answered"). `pnpm test:webview` asserts these four plus the host-side whitelist.
310
+ 3. **Any request carrying `Origin` gets 403.** Browsers always send one (including a sandboxed iframe's literal
277
311
  `Origin: null`); the Node extension host never does. Origin is checked **before** the token — otherwise the
278
312
  bridge would be a "did you guess the token right" oracle for a web page.
279
- 3. **Paths are confined to the editor's current workspace folders.**
280
- 4. **Everything is bounded**: 200 diagnostics, 500-char messages, 256 KB request bodies, a 64-entry event ring.
313
+ 4. **Paths are confined to the editor's current workspace folders.**
314
+ 5. **Everything is bounded**: 200 diagnostics, 500-char messages, 256 KB request bodies, a 64-entry event ring,
315
+ ≤120 thread entries per session (≤8000 chars each, ≤4 watched sessions) and ≤4 pending approvals.
281
316
 
282
317
  This layer stops "another local app or a browser page that got hold of the file". A malicious program running as
283
318
  the same user could read your files and the token anyway — that is outside this plugin's threat model, exactly
@@ -354,13 +389,14 @@ Diagnostics: `GET /api/code-server/status` exposes
354
389
 
355
390
  ```powershell
356
391
  cd C:\Users\User\Desktop\dsh-code-server-app
357
- pnpm install # dev deps (esbuild + motion); allowBuilds is explicit → no postinstall runs
392
+ pnpm install # dev deps (esbuild + the official-renderer bundling deps); allowBuilds is explicit → no postinstall runs
358
393
  pnpm run build:client # src/factory.js → lib/client.js (not committed; must be built first)
394
+ pnpm run build:webview # ask panel: official Markdown renderer + panel shell → webview/thread.{js,css} (not committed; must be built first)
359
395
  pnpm run vendor:check # optional: show the bundled tree version vs the latest code-server release
360
396
  pnpm run vendor:vscode # ① produce vendor/vscode (the trimmed VS Code tree, ~197MB)
361
397
  pnpm run repack:build -- --target win32-arm64,win32-x64 --pack # ② one script builds every sub-package
362
398
  pnpm run publish:repacks # ③ publish every @jinsiyu/* sub-package (default dist-tag: next)
363
- pnpm pack # ④ → dsh-code-server-app-<version>.tgz (~107KB)
399
+ pnpm pack # ④ → dsh-code-server-app-<version>.tgz (~750KB, including the panel renderer assets)
364
400
  pnpm run publish:plugin # ⑤ publish the plugin itself (default dist-tag: next)
365
401
  # once the user has restarted dsh web and confirmed it works, promote latest:
366
402
  pnpm run promote -- <version>
@@ -375,6 +411,14 @@ pnpm run promote -- <version>
375
411
  > so their dist-tags do not affect resolution, but they default to `next` as well.
376
412
  > Inspect the current tags with `npm dist-tag ls dsh-code-server-app`.
377
413
 
414
+ > `build:webview` bundles DSH's **official** Markdown renderer and design tokens into the panel assets
415
+ > (~1.34MB: 996KB JS + 87KB CSS + 254KB KaTeX fonts), so it needs a local DSH deployment: the script reads the
416
+ > `@deepseek-ai/dsh-web-frontend` version from that deployment and compares it with the renderer version pinned in
417
+ > devDependencies — a mismatch **fails the build** (unless `--allow-version-mismatch`). Same convention as
418
+ > `lib/client.js`: the artifacts are not committed and `prepack` rebuilds them.
419
+ > Rationale (why not an iframe, where the tokens come from, size trade-offs) is section 21 of
420
+ > `docs/analysis-code-server-as-dsh-plugin.md`.
421
+
378
422
  `repack:build` (`scripts/vendor-repacks.mjs`) is the **single script that produces every sub-package**:
379
423
 
380
424
  | Sub-package | Content | os/cpu |
@@ -484,7 +528,9 @@ dsh plugin --profile web add C:\Users\User\Desktop\dsh-code-server-app
484
528
  >
485
529
  > **Changing the client bundle**: edit `src/factory.js` then run `pnpm run build:client`
486
530
  > to regenerate `lib/client.js` (that artifact is not tracked; a browser refresh picks it up — no host restart needed).
487
- > Window animations are driven by the embedded `motion`; feel parameters live in `winPhysics` (one spot) in `src/factory.js`.
531
+ > **Changing the ask panel**: edit `assets/extensions/dshcs-editor-bridge/webview/src/*` then run
532
+ > `pnpm run build:webview` (same convention: generated, not tracked; the IDE must be restarted once to pick it up,
533
+ > because the extension host caches the webview resources).
488
534
 
489
535
  ### Pack-machine environment (the user machine needs nothing)
490
536
 
@@ -704,6 +750,19 @@ What remains on the plugin side:
704
750
  or by simply calling `editor_context` — 0.3.0–0.3.11 sat in the state "health says bridge:true, extension never
705
751
  loaded" (cause above: the user-level install was marked `.obsolete`).
706
752
  - **Bridged state can lag by up to 600 ms**, and the tools say "stale" rather than serving data older than 10 s.
753
+ - **The ask panel renders only "new content" (0.3.22)**: the subscription starts when the panel opens, the history
754
+ `records` from `follow`'s opening frame are discarded, and the panel has **no "load earlier"** (the history-paging
755
+ API `sessionController.page()` is deliberately not called in this version). Switch to the DSH UI for older content.
756
+ - **Panel highlighting ships only DSH's boot grammar set** (typescript / shellscript / json): the rest of the
757
+ official grammars load lazily through `import()` (~1.6MB total), and the panel is a single-file IIFE with no
758
+ lazy loading, so those languages render as plain text (exactly like DSH's own first render, no errors).
759
+ For the full set: `node scripts/build-webview.mjs --all-grammars`.
760
+ - **Panel assets are pinned to the DSH version**: the renderer is bundled against the UI version of the deployed
761
+ DSH, so after upgrading DSH you must rebuild the panel (`pnpm run build:webview`; the build fails loudly on a
762
+ 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 8 seconds**: while the panel is open, approvals ask the panel first and are
764
+ handed back to the DSH UI after 8 s — once handed back, that request can **only** be answered in the DSH UI (the
765
+ panel card says "handed to the DSH UI").
707
766
  - **Unsaved buffers are reported, not taken over.** The agent still edits via its own `fs` tools, i.e. against
708
767
  disk. What the bridge adds is a notice *before* writing a dirty file, a diff *after*, and a warning instead of
709
768
  an overwrite. It does not decide whether the user saves — that would mean changing the agent's read path,
package/README.md CHANGED
@@ -21,6 +21,10 @@
21
21
  > 0.2.0 起:**argon2 与 code-server 的 136 个运行时依赖(express / proxy-agent / js-yaml / pem / limiter …)
22
22
  > 全部不再随包分发**(减少 ~34.5MB + 一条原生构建链);IDE 提供方式见下方「服务方式(serve)」。
23
23
  > 依据与实测证据见 `docs/analysis-code-server-as-dsh-plugin.md`(含子路径挂载、WS 路径、命名管道、fence 的逐项验证)。
24
+ >
25
+ > 0.3.22 起:「问 DSH」面板直接渲染 **DSH 官方的 markdown 结果**(与 DSH 界面同一份渲染器 + 同一套设计令牌,
26
+ > 只渲染该会话的新内容),并且**就在面板里处理授权**(工作区外写入 / 执行命令)—— 详见
27
+ > 「与 DSH 的协同:编辑器桥」与 `docs/analysis-code-server-as-dsh-plugin.md` 第 21 节。
24
28
 
25
29
  ## UI 载体与 DSH 版本要求(0.2.3 起只支持带右侧栏的 DSH)
26
30
 
@@ -194,20 +198,29 @@ DSH 用**资源地址**命名文件,`openFile` 只负责把地址交给右侧栏
194
198
  | 方向 | 能力 | 落地方式 |
195
199
  |---|---|---|
196
200
  | 编辑器 → agent | **未保存缓冲区**(磁盘内容 ≠ 用户所见)、活动文件与选区、**语言服务器诊断**(含 file:line、来源、code) | agent 工具 `editor_context` / `editor_diagnostics`;写脏文件前额外附一条提醒 |
197
- | 编辑器 → DSH | 选中代码 → 右键「DSH: 针对选中内容提问」→ 打开**提问面板**(带 `文件:行` 与选中内容);提问以**用户输入**进当前会话,回答**同步显示回面板** | 命令 `dsh-code-server.askAboutSelection`(编辑器右键菜单**最上面两条**之一)+ webview 面板 + `POST /ask` + `/sync` 的 `answers` 字段 |
201
+ | 编辑器 → DSH | 选中代码 → 右键「DSH: 针对选中内容提问」→ 打开**提问面板**(带 `文件:行` 与选中内容);提问以**用户输入**进当前会话,该会话的**新内容**用 DSH 官方 markdown 渲染器显示在面板里 | 命令 `dsh-code-server.askAboutSelection`(编辑器右键菜单**最上面两条**之一)+ webview 面板 + `POST /ask` + `/sync` 的 `thread` 字段 |
202
+ | DSH → 编辑器(授权) | agent 要**写工作区外的文件 / 执行命令**时的授权请求 → 面板里就地弹卡片(工具名 + 原因 + 倒计时),点「允许一次 / 拒绝」立刻生效 | `/sync` 的 `approvals` 字段 + `POST /approve`(桥里**唯一**的非只读路由,约束见「安全模型」) |
198
203
  | agent → 编辑器 | agent 改了哪个文件 → 开**原生 diff** 审阅;缓冲区有未保存改动时**告警而不覆盖** | host 观察 `tools/result`,扩展轮询后开 diff + 非模态告警 |
199
204
 
200
205
  - 工具只在桥就绪时注册(IDE 没起来时模型看不到"有个用不了的工具");提示词段落也只在桥存活时渲染。
201
- - **提问面板**(扩展 0.2.0 起):右键命令不再弹一行输入框,而是一个面板 —— 上面是问答记录、下面是输入框;
202
- 发送时**现取当前选区**(可以在面板开着的同时改选区再问);回答跟着轮询一起刷新,不用切回 DSH 界面。
206
+ - **提问面板**(扩展 0.2.0 起;0.2.3 起正文走官方渲染器):右键命令不再弹一行输入框,而是一个面板 ——
207
+ 上面是该会话的**新内容**、下面是输入框;发送时**现取当前选区**(可以在面板开着的同时改选区再问)。
203
208
  两个命令的**意图分开记**(0.3.21):「针对选中内容提问」只有真的选了内容才带**行区间 + 选区正文**;
204
209
  「针对当前文件提问」**永远不带行号、不带选区** —— 光标停在哪一行跟问题无关,行号只会误导 agent;
205
210
  没选区时用选中命令提问也会退化成纯文件。
211
+ - **面板里的正文就是 DSH 的渲染结果**(0.3.22):面板打包了 DSH 官方的 markdown 渲染器
212
+ (`@deepseek-ai/dsh-client-ui-primitives` 的 `MarkdownText`)与官方设计令牌 —— 同一套 micromark/mdast 管线、
213
+ 同一个增量流式解析器、同一个 shiki 高亮(启动集 typescript / shellscript / json)、KaTeX 公式、同样的标题与表格排版。
214
+ 只渲染**新内容**(从面板订阅那一刻起),**不重放历史**、没有"加载更早"。
215
+ - **授权就在面板里处理**(0.3.22):面板打开着的时候,该会话的授权请求**先问面板**(默认 8 秒),
216
+ 点「允许一次」/「拒绝」立刻生效;没人答就把请求**原样交回官方链路**(DSH 界面照旧弹卡)。
217
+ **永不自动放行** —— `allowed-once` 只能来自你的一次点击,面板里没有"以后都允许"这种入口。
206
218
  - 提问进 DSH 会话时是**普通用户消息**(`source: { kind: 'user' }`,host 0.3.19 修正):早期版本用
207
219
  `{kind:'plugin'}` 会被 DSH 渲染成"上下文更新",看起来不像自己说的话;来源信息靠正文首行
208
220
  `From the editor: <file>[:<行>]` 保留。
209
- - **全部只读**:桥不写文件、不改文档、不执行命令。agent 的写操作仍然全部走它自己的 `fs` 工具,
210
- 桥只是"知道它写了什么"。
221
+ - **只读 + 一个受限例外**:桥不写文件、不改文档、不执行命令;唯一的非只读路由是 `POST /approve`,
222
+ 它只能**回答**已经存在的授权请求(见「安全模型」第 2 条)。agent 的写操作仍然全部走它自己的 `fs` 工具,
223
+ 桥只是"知道它写了什么"、并把你对授权的答复带回去。
211
224
  - 编辑器侧的入口还有状态栏的 `$(plug) DSH`(连通时显示,点击打开日志),日志在输出面板
212
225
  「DSH Editor Bridge」里 —— 出问题时先看它。
213
226
  - **扩展装在内置目录**(0.3.12 修正):`dshcs-editor-bridge` 与 `dshcs-open-file` 一样装进
@@ -219,10 +232,11 @@ DSH 用**资源地址**命名文件,`openFile` 只负责把地址交给右侧栏
219
232
  ### 三条通道(0.3.13 起走**本机 IPC**:Windows 命名管道 / unix socket)
220
233
 
221
234
  ```
222
- 扩展 → host POST /code-server-bridge/sync 一趟来回:上报编辑器状态 + 取回待处理事件
223
- 扩展 → host POST /code-server-bridge/ask 把编辑器里的提问投进当前会话
224
- 扩展 → host GET /code-server-bridge/health 无鉴权探活(便于重启后一眼确认)
225
- 扩展 → host POST /code-server-bridge/event 扩展上报打开/关闭文件等(进 host 日志尾)
235
+ 扩展 → host POST /code-server-bridge/sync 一趟来回:上报编辑器状态(+ 面板在看哪个会话)+ 取回待处理事件与对话流
236
+ 扩展 → host POST /code-server-bridge/ask 把编辑器里的提问投进当前会话
237
+ 扩展 → host POST /code-server-bridge/approve 回答一条**已经存在**的授权请求(唯一的非只读路由)
238
+ 扩展 → host GET /code-server-bridge/health 无鉴权探活(便于重启后一眼确认)
239
+ 扩展 → host POST /code-server-bridge/event 扩展上报打开/关闭文件等(进 host 日志尾)
226
240
  host → 扩展 <extensionsDir>/.dshcs-bridge/bridge.json 端点 + 令牌(扩展每 5s 重读)
227
241
  (同一份内容还会写到**内置扩展旁边** `<树>/lib/vscode/extensions/.dshcs-bridge/` ——
228
242
  环境变量只在 host spawn IDE 时注入,而被**接管**的 IDE 是上一次启动的进程、拿不到它,
@@ -231,6 +245,14 @@ host → 扩展 <extensionsDir>/.dshcs-bridge/bridge.json 端点 + 令牌(
231
245
 
232
246
  请求走 `http.request({ socketPath })`(`fetch` 不支持 socket),**不开任何端口**。
233
247
 
248
+ `/sync` 的响应里,面板真正用到的三段(0.3.22):
249
+
250
+ | 字段 | 内容 | 面板怎么用 |
251
+ |---|---|---|
252
+ | `thread` | 被面板 `watch` 的会话的**新内容**条目(user / assistant / tool / approval),有界:每会话 ≤120 条、单条正文 ≤8000 字符、同时 watch ≤4 个会话 | 助手正文交给官方渲染器;工具与授权是紧凑摘要行 |
253
+ | `approvals` | 待决授权请求 `[{id, toolName, reason, at}]`(≤4 条) | 弹卡片 + 倒计时;点按后 `POST /approve` |
254
+ | `approvalHoldMs` / `uiVersion` | 授权窗口长度(默认 8000ms)/ DSH 界面版本 | 倒计时基准;渲染器版本不一致时提示 |
255
+
234
256
  > **为什么不是 HTTP(0.3.13 定论,三条都实测过)**
235
257
  > 1. **desktop 根本没有 HTTP 面**:渲染进程经 Electron IPC 调 `host.fetch()`
236
258
  > (`apps/desktop-host/src/index.ts:308` 的 `createSharedFetchHandler('/api')`)—— 那是进程内函数调用,
@@ -253,21 +275,29 @@ host 反向请求不到它。所以编辑器状态只能在扩展主动发起的
253
275
  **为什么不用 SSE/WebSocket**:扩展宿主里没有 HTTP 服务器,而桥的形态是"每 600ms 一趟请求/响应"。
254
276
  轮询给了两条好性质:幂等(丢一次事件只是少一次提示,数据本身永远在编辑器里),以及状态天然最新(每趟都刷新)。
255
277
 
256
- ### 安全模型(四条不变量,改 `lib/bridge.mjs` 之前先读)
278
+ ### 安全模型(五条不变量,改 `lib/bridge.mjs` 之前先读)
257
279
 
258
280
  桥的令牌写在 `<extensionsDir>/.dshcs-bridge/bridge.json`(**对本机同用户进程可读**),所以:
259
281
 
260
- 1. **`/code-server-bridge/*` 永久只读。** 没有写文件、改文档、执行命令的路由。
261
- 令牌泄露的爆炸半径被封在"看到编辑器里的信息",**不会**变成任意文件写/任意命令执行。
282
+ 1. **`/code-server-bridge/*` 只读,只有 `/approve` 一个例外。** 没有写文件、改文档、执行命令、
283
+ 拉起进程的路由。令牌泄露的爆炸半径被封在"看到编辑器里的信息",**不会**变成任意文件写/任意命令执行。
262
284
  `scripts/test-bridge-routes.mjs` 里有一条白名单断言盯着这件事(未知后缀一律 404)。
263
- 2. **带 `Origin` 的请求一律 403。** 浏览器发起必带 Origin(含沙箱 iframe 的 `Origin: null`),
285
+ 2. **`/approve` 的四条约束(缺一条就等于开了任意命令执行的后门,不许放宽)**:
286
+ (a) 只能**回答**已经存在的授权请求,请求体只有 `{id, outcome}`,**不接受任何自由文本 / 路径 / 命令参数**
287
+ —— 它只能"回答问题",不能"发起动作";(b) `id` 必须是本进程自己发起、且**仍未决**的请求(用后即废);
288
+ (c) `outcome` 只接受 `allowed-once` / `rejected`,**没有"永久允许"**;
289
+ (d) 没有面板在看 / 窗口超时(默认 8s)→ **交回官方链路**,绝不自动放行
290
+ (DSH 的 `approval/request` 本身 fail closed,这里只能把"没人答"保持成"没人答")。
291
+ `pnpm test:webview-bundle` 里有针对这四条与宿主侧白名单的断言。
292
+ 3. **带 `Origin` 的请求一律 403。** 浏览器发起必带 Origin(含沙箱 iframe 的 `Origin: null`),
264
293
  扩展宿主是 Node 进程、不带。判定顺序上 Origin **先于令牌** —— 否则等于给浏览器一个
265
294
  "令牌猜对没有"的 oracle。
266
295
  实现细节:Node 路由 → Fetch 适配器把**原始 headers** 挂在 request 上(`dshcsRawHeaders`),
267
296
  因为 undici 的 `Request` 构造器会把 `origin` 当 forbidden header 归一化掉 —— 读 `request.headers`
268
297
  会让这道 403 静默失效(测试里有这条实测记录)。
269
- 3. **路径收敛在编辑器当前工作区**(`workspaceFolder` 之外的诊断直接丢弃)。
270
- 4. **有界**:诊断默认 200 条 / 单条截断 500 字符 / 上报体上限 256KB / 事件环形缓冲 64 条。
298
+ 4. **路径收敛在编辑器当前工作区**(`workspaceFolder` 之外的诊断直接丢弃)。
299
+ 5. **有界**:诊断默认 200 条 / 单条截断 500 字符 / 上报体上限 256KB / 事件环形缓冲 64 条 /
300
+ 对话流每会话 ≤120 条(单条正文 ≤8000 字符、同时 watch ≤4 个会话)/ 待决授权 ≤4 条。
271
301
 
272
302
  这一层挡的是"本机其它应用或浏览器页面拿到那个文件后乱调桥";**同用户的本地恶意程序**
273
303
  本来就能直接读你的文件与令牌文件 —— 那不在本插件的威胁模型内(与「回环端口的安全模型」同一句话)。
@@ -337,18 +367,25 @@ host 反向请求不到它。所以编辑器状态只能在扩展主动发起的
337
367
 
338
368
  ```powershell
339
369
  cd C:\Users\User\Desktop\dsh-code-server-app
340
- pnpm install # 开发依赖(esbuild + motion);allowBuilds 已显式声明 → 不执行任何 postinstall
370
+ pnpm install # 开发依赖(esbuild + 官方渲染器打包依赖);allowBuilds 已显式声明 → 不执行任何 postinstall
341
371
  pnpm run build:client # src/factory.js → lib/client.js(不入库,必须先构建)
372
+ pnpm run build:webview # 「问 DSH」面板:官方 markdown 渲染器 + 面板外壳 → webview/thread.{js,css}(不入库,必须先构建)
342
373
  pnpm run vendor:check # 可选:查看内置 VS Code 树版本 vs code-server 最新版
343
374
  pnpm run vendor:vscode # ① 生成 vendor/vscode(精简 VS Code 树,≈197MB)
344
375
  pnpm run repack:build -- --target win32-arm64,win32-x64 --pack # ② 统一脚本产出全部子包(见下表)
345
376
  pnpm run publish:repacks # ③ 发布全部 @jinsiyu/* 子包(默认 dist-tag = next)
346
- pnpm pack # ④ → dsh-code-server-app-<version>.tgz(约 107KB)
377
+ pnpm pack # ④ → dsh-code-server-app-<version>.tgz(约 750KB,含面板渲染器产物)
347
378
  pnpm run publish:plugin # ⑤ 发布插件本体(默认 dist-tag = next)
348
379
  # 用户重启 dsh web 确认无误后,再把 latest 推进到该版本:
349
380
  pnpm run promote -- <version>
350
381
  ```
351
382
 
383
+ > `build:webview` 会把 DSH **官方**的 markdown 渲染器与设计令牌打进面板产物(≈1.34MB:JS 996KB +
384
+ > CSS 87KB + KaTeX 字体 254KB),所以它要求本机有 DSH 部署:脚本读部署里 `@deepseek-ai/dsh-web-frontend`
385
+ > 的版本,与 devDependency 钉住的渲染器版本比对,**不一致就报错退出**(`--allow-version-mismatch` 才放行)。
386
+ > 它与 `lib/client.js` 同一约定:产物不入 git,`prepack` 里会自动重跑。
387
+ > 细节(为什么不是 iframe、令牌从哪来、体积取舍)见 `docs/analysis-code-server-as-dsh-plugin.md` 第 21 节。
388
+
352
389
  > **dist-tag 政策(必须遵守)**:发布一律发到 **`next`**,**不动 `latest`**;
353
390
  > `latest` 只保留「最近一个确认无 bug 的版本」,由 `pnpm run promote -- <version>`
354
391
  > (= `npm dist-tag add dsh-code-server-app@<version> latest`)在**用户重启 dsh web 确认无误后**才推进。
@@ -388,8 +425,9 @@ pnpm run promote -- <version>
388
425
  ```powershell
389
426
  pnpm test:apply # 桩 ctx 下跑通 apply(回归:apply 期的 ReferenceError)
390
427
  pnpm test:claim-types # 认领类型语法与默认值
391
- pnpm test:bridge-routes # 编辑器桥:路由表只读白名单 / Origin 与令牌的判定顺序 / 令牌头三处一致
392
- pnpm test:bridge-extension # 编辑器桥扩展侧纯逻辑:未保存缓冲区上报、诊断排序截断、diff 判据、投递降级
428
+ pnpm test:bridge-routes # 编辑器桥:路由表白名单(只读 + /approve)/ Origin 与令牌的判定顺序 / 令牌头三处一致
429
+ pnpm test:bridge-extension # 编辑器桥扩展侧纯逻辑:未保存缓冲区上报、诊断排序截断、diff 判据、投递降级、面板状态机
430
+ pnpm test:webview # 面板 webview 产物:官方渲染器与令牌打包、版本一致、/approve 的四条约束(先跑 build:webview)
393
431
  pnpm test:launcher-routes # launcher 的 HTTP 面(起真进程,较慢)
394
432
  pnpm test:workspace-switch # 切工作区不重启进程
395
433
  pnpm test:fullscreen # 打开标签即全屏
@@ -490,7 +528,8 @@ dsh plugin --profile web add C:\Users\User\Desktop\dsh-code-server-app
490
528
  >
491
529
  > **改动 client bundle**:编辑 `src/factory.js` 后执行 `pnpm run build:client`
492
530
  > 重新生成 `lib/client.js`(仓库不跟踪该产物;浏览器刷新即生效,host 无需重启)。
493
- > 窗口动画由内嵌 `motion` 驱动,手感参数在 `src/factory.js` 的 `winPhysics`(一处)。
531
+ > **改动提问面板**:编辑 `assets/extensions/dshcs-editor-bridge/webview/src/*` 后执行
532
+ > `pnpm run build:webview`(同一约定:产物不入库;IDE 需重启一次才会加载新产物,扩展宿主会缓存 webview 资源)。
494
533
 
495
534
  ### 打包机环境要求(使用者机器什么都不需要)
496
535
 
@@ -684,6 +723,17 @@ desktop profile 由 `apps/desktop-host` 把 `/api/*` 交给同一个 `createShar
684
723
  - **未保存缓冲区是"上报"而不是"接管"**:agent 仍然通过它自己的 `fs` 工具按磁盘内容编辑。
685
724
  桥能做的是**在写之前提醒**、**写之后给 diff**、**冲突时告警而不覆盖** ——
686
725
  它不能替用户决定保存与否(那需要改动 agent 的读路径,不在本版本范围内)。
726
+ - **提问面板只渲染"新内容"(0.3.22)**:订阅从面板建立那一刻开始,`follow` 开帧里的历史 `records` 被丢弃,
727
+ 面板里**没有"加载更早"**(历史分页 API `sessionController.page()` 在这个版本里刻意不调用)。
728
+ 想看更早的内容请回 DSH 界面。
729
+ - **面板的高亮只带 DSH 启动集的三套语法**(typescript / shellscript / json):官方其余语法走**懒加载**
730
+ (按需 `import()`,合计约 1.6MB),面板是单文件 IIFE、没有按需加载,所以那些语言的代码块**纯文本**显示
731
+ (与 DSH 首次渲染时的样子一致,不报错)。要全量:`node scripts/build-webview.mjs --all-grammars`。
732
+ - **面板产物与 DSH 版本绑定**:渲染器按构建时 DSH 部署的界面版本打包,DSH 升级后**要重打面板**
733
+ (`pnpm run build:webview`;构建脚本会在版本不一致时直接报错)。运行期面板顶部也会提示版本不一致,
734
+ 不会悄悄用错版本的渲染器。
735
+ - **面板里的授权窗口是 8 秒**:面板打开着的时候授权先问面板,8 秒没人答就交回 DSH 界面
736
+ —— 交回之后这一条**只能**在 DSH 界面里处理(面板上的卡片会显示"已交给 DSH 界面")。
687
737
 
688
738
  - ~~子路径不支持~~ **已不成立(0.2.0 实测更正)**:VS Code 渲染出的 workbench HTML 里
689
739
  **资源引用全是相对路径**(实测 9 条引用中绝对路径 0 条,`serverBasePath="."`、`rootEndpoint="."`),
@@ -13,8 +13,13 @@
13
13
  // 就什么都不做。状态栏不显示任何东西,更不弹通知。
14
14
  // 3. **绝不覆盖未保存改动**:agent 改了文件而该文档是脏的就只告警 + 给 diff,由用户决定。
15
15
  //
16
- // 「问 DSH」面板(0.2.0):右键命令打开一个 webview,提问走 `/ask`(host 侧以**用户输入**进对话),
17
- // 回答随 `/sync` 的 `answers` 字段回到面板(纯逻辑在 lib/ask-panel.js,可单测)。
16
+ // 「问 DSH」面板(0.2.0 起;0.2.3 起正文走 DSH 官方渲染器):
17
+ // - 提问走 `/ask`(host 侧以**用户输入**进对话);
18
+ // - 回答**不再是另一条通道**:host 用 `sessionController.follow` 把该会话的**新内容**
19
+ // 投影成条目,随 `/sync` 的 `thread` 回来,面板用官方 markdown 渲染器显示
20
+ // (产物由 scripts/build-webview.mjs 打成 webview/thread.js + thread.css);
21
+ // - 授权(写工作区外文件 / 执行命令):host 的 `approvals` 字段回到面板,用户在面板上
22
+ // 「允许一次 / 拒绝」,扩展调 `/approve` 提交(桥里唯一的非只读路由)。
18
23
  //
19
24
  // 只依赖 `vscode` 与 Node 内置模块;纯逻辑在 lib/ 下且不 require('vscode'),便于单测。
20
25
 
@@ -22,6 +27,7 @@
22
27
 
23
28
  const vscode = require('vscode');
24
29
  const crypto = require('crypto');
30
+ const fs = require('fs');
25
31
  const path = require('path');
26
32
 
27
33
  const {
@@ -32,14 +38,12 @@ const {
32
38
  const { createProjector } = require('./lib/context-model.js');
33
39
  const { createDiffCache, describeChange } = require('./lib/diff-model.js');
34
40
  const {
35
- applyAnswers,
41
+ applySync,
36
42
  createPanelState,
37
- currentTurn,
38
- describeContext,
43
+ failPanel,
39
44
  panelPayload,
40
- pushQuestion,
45
+ pendingQuestion,
41
46
  renderPanelHtml,
42
- statusText,
43
47
  } = require('./lib/ask-panel.js');
44
48
 
45
49
  /** 状态栏项(仅在桥连通时显示)。 */
@@ -52,8 +56,10 @@ let client = null;
52
56
  let pollTimer = null;
53
57
  /** 上次成功轮询的时间(状态栏 tooltip 用)。 */
54
58
  let lastPollAt = 0;
55
- /** 「问 DSH」面板:null = 没开。`{panel, state}`(状态模型见 lib/ask-panel.js)。 */
59
+ /** 「问 DSH」面板:null = 没开。`{panel, state, mode}`(状态模型见 lib/ask-panel.js)。 */
56
60
  let askPanel = null;
61
+ /** 扩展根目录(activate 时记下;面板要按它取 webview 产物 URI)。 */
62
+ let extensionRoot = null;
57
63
  /** 是否已经确认过 host 端点可达(避免把"IDE 刚起、扩展先加载"误判为断线)。 */
58
64
  let connected = false;
59
65
  /** 诊断集合缓存:host 请求时现算,这里只做"有没有变化"的计数上报。 */
@@ -357,6 +363,10 @@ async function pollOnce() {
357
363
  }),
358
364
  diagnostics,
359
365
  at: Date.now(),
366
+ // 面板声明它要看哪个会话:host 据此对齐 `sessionController.follow` 订阅,
367
+ // 并用"有没有人在看"决定授权请求是先问面板还是直接交给 DSH 界面
368
+ // (见 host 侧 lib/bridge-thread.mjs / lib/bridge-approval.mjs)。
369
+ watch: askPanel !== null && askPanel.state.sessionId !== null ? [askPanel.state.sessionId] : [],
360
370
  };
361
371
  const result = await client.sync(payload);
362
372
  if (result.ok !== true) {
@@ -378,20 +388,9 @@ async function pollOnce() {
378
388
  await handleEvent(event);
379
389
  }
380
390
  if ((result.events ?? []).length > 0) client.persist();
381
- // 回答同步:host 把 agent 的正文放在 answers 里(见 lib/bridge-answer.mjs),
382
- // 面板每趟轮询刷新一次 —— 不需要第二个定时器,也不需要流式协议。
383
- if (askPanel !== null) {
384
- if (result.answers === undefined) {
385
- // 旧版 host(≤0.3.18)没有这个字段:此时面板会永远停在"正在回答…",必须说出原因。
386
- askPanel.stalePolls = (askPanel.stalePolls ?? 0) + 1;
387
- if (askPanel.stalePolls === 15 && askPanel.state.sessionId !== null && askPanel.state.status === 'thinking') {
388
- failAskPanel('宿主没有回复同步能力(插件版本过旧?):请重启 dsh web 让 host 侧升级到 0.3.19 以上');
389
- }
390
- } else {
391
- askPanel.stalePolls = 0;
392
- if (applyAnswers(askPanel.state, result.answers, askPanel.state.sessionId)) refreshAskPanel();
393
- }
394
- }
391
+ // 对话流 + 授权待决:host 每趟把被观看会话的**新内容**与待决授权一起回来
392
+ // (见 host 侧 lib/bridge-thread.mjs / lib/bridge-approval.mjs),面板有变化才重画。
393
+ if (askPanel !== null && applySync(askPanel.state, result)) refreshAskPanel();
395
394
  }
396
395
 
397
396
  function startPolling(context) {
@@ -447,33 +446,23 @@ function captureAskContext(mode) {
447
446
  /** 把状态推给面板(增量更新:输入框与滚动位置都不受影响)。 */
448
447
  function refreshAskPanel() {
449
448
  if (askPanel === null) return;
450
- const payload = panelPayload(askPanel.state);
451
- payload.contextText = askPanel.state.context === null
452
- ? '来自编辑器'
453
- : `来自编辑器:${describeContext(askPanel.state.context)}`;
454
- payload.statusText = askPanel.state.status === 'error'
455
- ? (askPanel.state.error ?? '出错了')
456
- : statusText(askPanel.state);
457
- void askPanel.panel.webview.postMessage(payload);
449
+ void askPanel.panel.webview.postMessage(panelPayload(askPanel.state));
458
450
  }
459
451
 
460
- /** 面板进入错误态:状态行 + 当前轮都标上原因(绝不静默)。 */
452
+ /** 面板进入错误态:状态行 + 最新一条本地提问都标上原因(绝不静默)。 */
461
453
  function failAskPanel(message) {
462
454
  if (askPanel === null) return;
463
- askPanel.state.status = 'error';
464
- askPanel.state.error = message;
465
- const turn = currentTurn(askPanel.state);
466
- if (turn !== null) turn.error = message;
455
+ failPanel(askPanel.state, message);
467
456
  log(message);
468
457
  refreshAskPanel();
469
458
  }
470
459
 
471
- /** 面板收到一条提问:投递到 DSH,并把这一轮放进状态里等回答。 */
460
+ /** 面板收到一条提问:投递到 DSH,**本地先乐观显示**,回答由 host 的对话流回填。 */
472
461
  async function sendAskFromPanel(text) {
473
462
  if (askPanel === null || client === null) return;
474
463
  // 用面板自己的意图(selection / file)取上下文:文件级提问永远不带行号。
475
464
  const context = captureAskContext(askPanel.mode) ?? askPanel.state.context;
476
- const turn = pushQuestion(askPanel.state, text, context);
465
+ pendingQuestion(askPanel.state, text, context);
477
466
  refreshAskPanel();
478
467
  try {
479
468
  const result = await client.ask({
@@ -489,21 +478,44 @@ async function sendAskFromPanel(text) {
489
478
  askPanel.state.status = 'thinking';
490
479
  log(`已投递编辑器消息(session=${result.sessionId ?? '?'})`);
491
480
  } else {
492
- askPanel.state.status = 'error';
493
- askPanel.state.error = `未投递:${result.error ?? '未知原因'}`;
494
- turn.error = askPanel.state.error;
481
+ failAskPanel(`未投递:${result.error ?? '未知原因'}`);
482
+ return;
495
483
  }
496
484
  } catch (error) {
497
485
  const status = error && error.status;
498
- const message = status === 409
486
+ failAskPanel(status === 409
499
487
  ? 'DSH 里没有可投递的会话:请先在 DSH 里打开或新建一个会话。'
500
488
  : (status === 401 || status === 503
501
489
  ? '编辑器桥尚未就绪,请稍后重试。'
502
- : `投递失败:${error && error.message ? error.message : error}`);
503
- // 桥没就绪时挂断会话:面板据此显示原因,轮询恢复后下一次发送即可。
504
- askPanel.state.status = 'error';
505
- askPanel.state.error = message;
506
- turn.error = message;
490
+ : `投递失败:${error && error.message ? error.message : error}`));
491
+ return;
492
+ }
493
+ refreshAskPanel();
494
+ }
495
+
496
+ /**
497
+ * 面板对一条授权请求的决策 → `POST /approve`。
498
+ *
499
+ * 只接受白名单的两个值(host 侧同样只认这两个);失败必须说出来:面板里点不动的时候,
500
+ * 用户唯一的线索就是这行状态/日志。
501
+ */
502
+ async function decideApproval(id, outcome) {
503
+ if (askPanel === null || client === null) return;
504
+ if (outcome !== 'allowed-once' && outcome !== 'rejected') return;
505
+ try {
506
+ await client.approve(id, outcome);
507
+ log(`面板授权决策已提交:${id} → ${outcome}`);
508
+ } catch (error) {
509
+ const status = error && error.status;
510
+ const message = status === 409
511
+ ? '这条授权请求已经过期或已被处理(DSH 界面里会弹同一张卡片)。'
512
+ : (status === 401 || status === 503
513
+ ? '编辑器桥尚未就绪,授权没有提交。'
514
+ : `授权提交失败:${error && error.message ? error.message : error}`);
515
+ // 卡片已经没意义了:本地撤掉,避免用户反复点。
516
+ askPanel.state.approvals = askPanel.state.approvals.filter((item) => item.id !== id);
517
+ failAskPanel(message);
518
+ return;
507
519
  }
508
520
  refreshAskPanel();
509
521
  }
@@ -515,16 +527,33 @@ async function sendAskFromPanel(text) {
515
527
  function openAskPanel(mode = 'selection') {
516
528
  const contextInfo = captureAskContext(mode);
517
529
  if (askPanel === null) {
530
+ // webview 产物是构建出来的(scripts/build-webview.mjs)。缺了就别开一个空白面板。
531
+ const webviewDir = vscode.Uri.joinPath(extensionRoot, 'webview');
532
+ const bundle = vscode.Uri.joinPath(webviewDir, 'thread.js');
533
+ const style = vscode.Uri.joinPath(webviewDir, 'thread.css');
534
+ if (!fs.existsSync(bundle.fsPath) || !fs.existsSync(style.fsPath)) {
535
+ const message = `面板产物缺失(${bundle.fsPath}):请重新安装插件,或在源码里跑 pnpm run build:webview`;
536
+ log(message);
537
+ void vscode.window.showErrorMessage(message);
538
+ return;
539
+ }
518
540
  const panel = vscode.window.createWebviewPanel(
519
541
  'dshAsk',
520
542
  'DSH 提问',
521
543
  { viewColumn: vscode.ViewColumn.Beside, preserveFocus: false },
522
- { enableScripts: true, retainContextWhenHidden: true },
544
+ {
545
+ enableScripts: true,
546
+ retainContextWhenHidden: true,
547
+ // 只允许加载扩展自己的 webview 目录(产物 + 字体),不给别的本地文件开口子。
548
+ localResourceRoots: [webviewDir],
549
+ },
523
550
  );
524
551
  askPanel = { panel, state: createPanelState(), mode };
525
552
  panel.webview.html = renderPanelHtml({
526
553
  cspSource: panel.webview.cspSource,
527
554
  nonce: crypto.randomBytes(16).toString('base64url'),
555
+ scriptUri: panel.webview.asWebviewUri(bundle).toString(),
556
+ styleUri: panel.webview.asWebviewUri(style).toString(),
528
557
  });
529
558
  panel.webview.onDidReceiveMessage((message) => {
530
559
  if (message === null || typeof message !== 'object') return;
@@ -535,6 +564,10 @@ function openAskPanel(mode = 'selection') {
535
564
  if (message.type === 'ask' && typeof message.text === 'string') {
536
565
  const text = message.text.trim();
537
566
  if (text !== '') void sendAskFromPanel(text);
567
+ return;
568
+ }
569
+ if (message.type === 'approve' && typeof message.id === 'string') {
570
+ void decideApproval(message.id, message.outcome);
538
571
  }
539
572
  });
540
573
  panel.onDidDispose(() => {
@@ -580,6 +613,9 @@ function activate(context) {
580
613
  output = vscode.window.createOutputChannel('DSH Editor Bridge');
581
614
  context.subscriptions.push(output);
582
615
 
616
+ // 面板 webview 的产物按扩展根目录取 URI(0.2.3 起正文走官方渲染器,产物是打包出来的)。
617
+ extensionRoot = context.extensionUri;
618
+
583
619
  statusBar = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right, 90);
584
620
  context.subscriptions.push(statusBar);
585
621