dsh-code-server-app 0.3.18 → 0.3.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +7 -1
- package/README.md +6 -1
- package/assets/extensions/dshcs-editor-bridge/extension.js +123 -32
- package/assets/extensions/dshcs-editor-bridge/lib/ask-panel.js +230 -0
- package/assets/extensions/dshcs-editor-bridge/lib/bridge-client.js +4 -1
- package/assets/extensions/dshcs-editor-bridge/package.json +3 -16
- package/lib/bridge-answer.mjs +155 -0
- package/lib/bridge-session.mjs +8 -3
- package/lib/index.js +19 -1
- package/package.json +2 -1
- package/vendor/VENDOR.json +1 -1
package/README.en.md
CHANGED
|
@@ -198,11 +198,17 @@ only the editor knows, and lets editor gestures drive the current session.
|
|
|
198
198
|
| Direction | Capability | Mechanism |
|
|
199
199
|
|---|---|---|
|
|
200
200
|
| 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"** → the
|
|
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` |
|
|
202
202
|
| 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
203
|
|
|
204
204
|
- The tools are only registered while the bridge is live (so the model never sees an unusable tool), and the
|
|
205
205
|
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.
|
|
209
|
+
- The question enters the DSH session as a **plain user message** (`source: { kind: 'user' }`, host 0.3.19): earlier
|
|
210
|
+
versions used `{kind:'plugin'}`, which DSH renders as a *context update* — it did not look like something the user
|
|
211
|
+
said. Provenance stays in the first line of the text: `From the editor: <file>:<line>`.
|
|
206
212
|
- **Everything is read-only**: the bridge never writes files, applies edits, or runs commands. The agent's writes
|
|
207
213
|
still go through its own `fs` tools; the bridge only *knows about* them.
|
|
208
214
|
- Status bar shows `$(plug) DSH` while connected (click it for the log in the "DSH Editor Bridge" output channel).
|
package/README.md
CHANGED
|
@@ -194,10 +194,15 @@ DSH 用**资源地址**命名文件,`openFile` 只负责把地址交给右侧栏
|
|
|
194
194
|
| 方向 | 能力 | 落地方式 |
|
|
195
195
|
|---|---|---|
|
|
196
196
|
| 编辑器 → agent | **未保存缓冲区**(磁盘内容 ≠ 用户所见)、活动文件与选区、**语言服务器诊断**(含 file:line、来源、code) | agent 工具 `editor_context` / `editor_diagnostics`;写脏文件前额外附一条提醒 |
|
|
197
|
-
| 编辑器 → DSH | 选中代码 → 右键「DSH: 针对选中内容提问」→
|
|
197
|
+
| 编辑器 → DSH | 选中代码 → 右键「DSH: 针对选中内容提问」→ 打开**提问面板**(带 `文件:行` 与选中内容);提问以**用户输入**进当前会话,回答**同步显示回面板** | 命令 `dsh-code-server.askAboutSelection`(编辑器右键菜单**最上面两条**之一)+ webview 面板 + `POST /ask` + `/sync` 的 `answers` 字段 |
|
|
198
198
|
| agent → 编辑器 | agent 改了哪个文件 → 开**原生 diff** 审阅;缓冲区有未保存改动时**告警而不覆盖** | host 观察 `tools/result`,扩展轮询后开 diff + 非模态告警 |
|
|
199
199
|
|
|
200
200
|
- 工具只在桥就绪时注册(IDE 没起来时模型看不到"有个用不了的工具");提示词段落也只在桥存活时渲染。
|
|
201
|
+
- **提问面板**(扩展 0.2.0 起):右键命令不再弹一行输入框,而是一个面板 —— 上面是问答记录、下面是输入框;
|
|
202
|
+
发送时**现取当前选区**(可以在面板开着的同时改选区再问);回答跟着轮询一起刷新,不用切回 DSH 界面。
|
|
203
|
+
- 提问进 DSH 会话时是**普通用户消息**(`source: { kind: 'user' }`,host 0.3.19 修正):早期版本用
|
|
204
|
+
`{kind:'plugin'}` 会被 DSH 渲染成"上下文更新",看起来不像自己说的话;来源信息靠正文首行
|
|
205
|
+
`From the editor: <file>:<行>` 保留。
|
|
201
206
|
- **全部只读**:桥不写文件、不改文档、不执行命令。agent 的写操作仍然全部走它自己的 `fs` 工具,
|
|
202
207
|
桥只是"知道它写了什么"。
|
|
203
208
|
- 编辑器侧的入口还有状态栏的 `$(plug) DSH`(连通时显示,点击打开日志),日志在输出面板
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
// dshcs-editor-bridge —— 编辑器桥的扩展侧(由 dsh-code-server-app 插件安装)
|
|
2
2
|
//
|
|
3
3
|
// 职责边界(与 host 侧 lib/bridge.mjs / bridge-tools.mjs 的分工):
|
|
4
|
-
// - **本扩展**是唯一知道"用户此刻看到什么"
|
|
5
|
-
//
|
|
6
|
-
// -
|
|
4
|
+
// - **本扩展**是唯一知道"用户此刻看到什么"的一方,所以编辑器状态(活动文件/选区/脏缓冲区/诊断)
|
|
5
|
+
// 在这里采集;
|
|
6
|
+
// - **host** 负责鉴权、给 agent 提供工具、观察 agent 的写操作,并把 agent 的回复同步回来;
|
|
7
|
+
// - 双向流量都走 **一趟轮询**(`POST /sync`:上报状态 + 取回事件与回复)。没有第二个定时器,
|
|
8
|
+
// 也没有 SSE/WS —— 扩展宿主不监听端口,host 反向请求不到它。
|
|
7
9
|
//
|
|
8
10
|
// 三条硬规则:
|
|
9
11
|
// 1. **只读**:本扩展只读编辑器状态,不写文件、不执行命令、不应用编辑(host 侧命名空间同理只读)。
|
|
@@ -11,6 +13,9 @@
|
|
|
11
13
|
// 就什么都不做。状态栏不显示任何东西,更不弹通知。
|
|
12
14
|
// 3. **绝不覆盖未保存改动**:agent 改了文件而该文档是脏的就只告警 + 给 diff,由用户决定。
|
|
13
15
|
//
|
|
16
|
+
// 「问 DSH」面板(0.2.0):右键命令打开一个 webview,提问走 `/ask`(host 侧以**用户输入**进对话),
|
|
17
|
+
// 回答随 `/sync` 的 `answers` 字段回到面板(纯逻辑在 lib/ask-panel.js,可单测)。
|
|
18
|
+
//
|
|
14
19
|
// 只依赖 `vscode` 与 Node 内置模块;纯逻辑在 lib/ 下且不 require('vscode'),便于单测。
|
|
15
20
|
|
|
16
21
|
'use strict';
|
|
@@ -26,6 +31,15 @@ const {
|
|
|
26
31
|
} = require('./lib/bridge-client.js');
|
|
27
32
|
const { createProjector } = require('./lib/context-model.js');
|
|
28
33
|
const { createDiffCache, describeChange } = require('./lib/diff-model.js');
|
|
34
|
+
const {
|
|
35
|
+
applyAnswers,
|
|
36
|
+
createPanelState,
|
|
37
|
+
describeContext,
|
|
38
|
+
panelPayload,
|
|
39
|
+
pushQuestion,
|
|
40
|
+
renderPanelHtml,
|
|
41
|
+
statusText,
|
|
42
|
+
} = require('./lib/ask-panel.js');
|
|
29
43
|
|
|
30
44
|
/** 状态栏项(仅在桥连通时显示)。 */
|
|
31
45
|
let statusBar = null;
|
|
@@ -37,6 +51,8 @@ let client = null;
|
|
|
37
51
|
let pollTimer = null;
|
|
38
52
|
/** 上次成功轮询的时间(状态栏 tooltip 用)。 */
|
|
39
53
|
let lastPollAt = 0;
|
|
54
|
+
/** 「问 DSH」面板:null = 没开。`{panel, state}`(状态模型见 lib/ask-panel.js)。 */
|
|
55
|
+
let askPanel = null;
|
|
40
56
|
/** 是否已经确认过 host 端点可达(避免把"IDE 刚起、扩展先加载"误判为断线)。 */
|
|
41
57
|
let connected = false;
|
|
42
58
|
/** 诊断集合缓存:host 请求时现算,这里只做"有没有变化"的计数上报。 */
|
|
@@ -361,6 +377,11 @@ async function pollOnce() {
|
|
|
361
377
|
await handleEvent(event);
|
|
362
378
|
}
|
|
363
379
|
if ((result.events ?? []).length > 0) client.persist();
|
|
380
|
+
// 回答同步:host 把 agent 的正文放在 answers 里(见 lib/bridge-answer.mjs),
|
|
381
|
+
// 面板每趟轮询刷新一次 —— 不需要第二个定时器,也不需要流式协议。
|
|
382
|
+
if (askPanel !== null && result.answers !== undefined) {
|
|
383
|
+
if (applyAnswers(askPanel.state, result.answers, askPanel.state.sessionId)) refreshAskPanel();
|
|
384
|
+
}
|
|
364
385
|
}
|
|
365
386
|
|
|
366
387
|
function startPolling(context) {
|
|
@@ -386,51 +407,121 @@ function updateStatusBar() {
|
|
|
386
407
|
}
|
|
387
408
|
}
|
|
388
409
|
|
|
389
|
-
// ---------------------------------------------------------------- 命令
|
|
410
|
+
// ---------------------------------------------------------------- 命令 / 提问面板
|
|
390
411
|
|
|
391
|
-
/**
|
|
392
|
-
|
|
412
|
+
/** 当前编辑器上下文(发送时现取一次:用户可以在面板开着的同时换选区)。 */
|
|
413
|
+
function captureAskContext() {
|
|
393
414
|
const editor = vscode.window.activeTextEditor;
|
|
394
|
-
if (editor === undefined || editor === null)
|
|
395
|
-
vscode.window.showInformationMessage('没有活动的编辑器。');
|
|
396
|
-
return;
|
|
397
|
-
}
|
|
398
|
-
if (client === null || client.isDormant()) {
|
|
399
|
-
vscode.window.showInformationMessage('编辑器桥未启用:请在 DSH 里打开 Code Server 标签后重试。');
|
|
400
|
-
return;
|
|
401
|
-
}
|
|
415
|
+
if (editor === undefined || editor === null) return null;
|
|
402
416
|
const doc = editor.document;
|
|
403
417
|
const selection = editor.selection;
|
|
404
418
|
const hasSelection = selection !== undefined && selection !== null && selection.isEmpty === false;
|
|
405
419
|
const selectedText = hasSelection ? doc.getText(selection) : '';
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
420
|
+
return {
|
|
421
|
+
file: doc.uri.scheme === 'file' ? doc.uri.fsPath : null,
|
|
422
|
+
lineStart: hasSelection
|
|
423
|
+
? selection.start.line + 1
|
|
424
|
+
: (doc.isDirty ? null : selection.active.line + 1),
|
|
425
|
+
lineEnd: hasSelection ? selection.end.line + 1 : null,
|
|
426
|
+
selection: selectedText === '' ? null : selectedText,
|
|
427
|
+
languageId: doc.languageId ?? null,
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/** 把状态推给面板(增量更新:输入框与滚动位置都不受影响)。 */
|
|
432
|
+
function refreshAskPanel() {
|
|
433
|
+
if (askPanel === null) return;
|
|
434
|
+
const payload = panelPayload(askPanel.state);
|
|
435
|
+
payload.contextText = askPanel.state.context === null
|
|
436
|
+
? '来自编辑器'
|
|
437
|
+
: `来自编辑器:${describeContext(askPanel.state.context)}`;
|
|
438
|
+
payload.statusText = askPanel.state.status === 'error'
|
|
439
|
+
? (askPanel.state.error ?? '出错了')
|
|
440
|
+
: statusText(askPanel.state);
|
|
441
|
+
void askPanel.panel.webview.postMessage(payload);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/** 面板收到一条提问:投递到 DSH,并把这一轮放进状态里等回答。 */
|
|
445
|
+
async function sendAskFromPanel(text) {
|
|
446
|
+
if (askPanel === null || client === null) return;
|
|
447
|
+
const context = captureAskContext() ?? askPanel.state.context;
|
|
448
|
+
const turn = pushQuestion(askPanel.state, text, context);
|
|
449
|
+
refreshAskPanel();
|
|
413
450
|
try {
|
|
414
451
|
const result = await client.ask({
|
|
415
|
-
text
|
|
416
|
-
file:
|
|
417
|
-
lineStart:
|
|
418
|
-
lineEnd:
|
|
419
|
-
selection:
|
|
420
|
-
languageId:
|
|
452
|
+
text,
|
|
453
|
+
file: context === null ? null : context.file,
|
|
454
|
+
lineStart: context === null ? null : context.lineStart,
|
|
455
|
+
lineEnd: context === null ? null : context.lineEnd,
|
|
456
|
+
selection: context === null ? null : context.selection,
|
|
457
|
+
languageId: context === null ? null : context.languageId,
|
|
421
458
|
});
|
|
422
459
|
if (result.ok === true) {
|
|
423
|
-
|
|
460
|
+
askPanel.state.sessionId = typeof result.sessionId === 'string' ? result.sessionId : null;
|
|
461
|
+
askPanel.state.status = 'thinking';
|
|
424
462
|
log(`已投递编辑器消息(session=${result.sessionId ?? '?'})`);
|
|
425
463
|
} else {
|
|
426
|
-
|
|
464
|
+
askPanel.state.status = 'error';
|
|
465
|
+
askPanel.state.error = `未投递:${result.error ?? '未知原因'}`;
|
|
466
|
+
turn.error = askPanel.state.error;
|
|
427
467
|
}
|
|
428
468
|
} catch (error) {
|
|
429
469
|
const status = error && error.status;
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
470
|
+
const message = status === 409
|
|
471
|
+
? 'DSH 里没有可投递的会话:请先在 DSH 里打开或新建一个会话。'
|
|
472
|
+
: (status === 401 || status === 503
|
|
473
|
+
? '编辑器桥尚未就绪,请稍后重试。'
|
|
474
|
+
: `投递失败:${error && error.message ? error.message : error}`);
|
|
475
|
+
// 桥没就绪时挂断会话:面板据此显示原因,轮询恢复后下一次发送即可。
|
|
476
|
+
askPanel.state.status = 'error';
|
|
477
|
+
askPanel.state.error = message;
|
|
478
|
+
turn.error = message;
|
|
479
|
+
}
|
|
480
|
+
refreshAskPanel();
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/** 打开(或聚焦)「问 DSH」面板。上下文只用来显示;真正的上下文在发送时现取。 */
|
|
484
|
+
function openAskPanel(context) {
|
|
485
|
+
const contextInfo = context ?? captureAskContext();
|
|
486
|
+
if (askPanel === null) {
|
|
487
|
+
const panel = vscode.window.createWebviewPanel(
|
|
488
|
+
'dshAsk',
|
|
489
|
+
'DSH 提问',
|
|
490
|
+
{ viewColumn: vscode.ViewColumn.Beside, preserveFocus: false },
|
|
491
|
+
{ enableScripts: true, retainContextWhenHidden: true },
|
|
492
|
+
);
|
|
493
|
+
askPanel = { panel, state: createPanelState() };
|
|
494
|
+
panel.webview.html = renderPanelHtml({
|
|
495
|
+
cspSource: panel.webview.cspSource,
|
|
496
|
+
nonce: crypto.randomBytes(16).toString('base64url'),
|
|
497
|
+
});
|
|
498
|
+
panel.webview.onDidReceiveMessage((message) => {
|
|
499
|
+
if (message === null || typeof message !== 'object') return;
|
|
500
|
+
if (message.type === 'ready') {
|
|
501
|
+
refreshAskPanel();
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
if (message.type === 'ask' && typeof message.text === 'string') {
|
|
505
|
+
const text = message.text.trim();
|
|
506
|
+
if (text !== '') void sendAskFromPanel(text);
|
|
507
|
+
}
|
|
508
|
+
});
|
|
509
|
+
panel.onDidDispose(() => {
|
|
510
|
+
askPanel = null;
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
askPanel.state.context = contextInfo;
|
|
514
|
+
askPanel.panel.reveal(vscode.ViewColumn.Beside, false);
|
|
515
|
+
refreshAskPanel();
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/** 选中内容 → DSH(编辑器→DSH 的主入口):打开面板并带上当前上下文。 */
|
|
519
|
+
async function askAboutSelection() {
|
|
520
|
+
if (client === null || client.isDormant()) {
|
|
521
|
+
vscode.window.showInformationMessage('编辑器桥未启用:请在 DSH 里打开 Code Server 标签后重试。');
|
|
522
|
+
return;
|
|
433
523
|
}
|
|
524
|
+
openAskPanel(captureAskContext());
|
|
434
525
|
}
|
|
435
526
|
|
|
436
527
|
/** 整个文件 → DSH(不需要选中)。 */
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
// dshcs-editor-bridge / lib/ask-panel.js —— 「问 DSH」面板(0.2.0)
|
|
2
|
+
//
|
|
3
|
+
// 纯逻辑 + HTML 生成,**不 require('vscode')**,所以能在普通 Node 里单测
|
|
4
|
+
// (scripts/test-bridge-extension.mjs)。与 VS Code 交互的部分在 extension.js。
|
|
5
|
+
//
|
|
6
|
+
// 为什么要有这个面板:0.3.0–0.3.18 的提问是「输入框 → 发送 → 一条状态栏提示」,
|
|
7
|
+
// 用户在编辑器里问完必须切回 DSH 界面才看得到回答。面板把三件事放在一处:
|
|
8
|
+
// ① 带着上下文提问(文件:行 + 选中内容,发送时再取一次当前选区);
|
|
9
|
+
// ② 提问以**用户输入**的形态进 DSH 会话(host 侧 bridge-session.mjs 的 source.kind='user');
|
|
10
|
+
// ③ 回答**同步显示在这里** —— host 把 agent 的正文随 /sync 的 answers 字段推回来
|
|
11
|
+
// (见 lib/bridge-answer.mjs),面板每趟轮询刷新一次,不需要第二个定时器。
|
|
12
|
+
|
|
13
|
+
'use strict';
|
|
14
|
+
|
|
15
|
+
/** 面板里最多保留多少轮问答(超过丢最旧的:这是对话窗口,不是存档)。 */
|
|
16
|
+
const MAX_TURNS = 20;
|
|
17
|
+
|
|
18
|
+
/** 单条消息渲染上限(回答太长时截断,避免 webview 卡)。 */
|
|
19
|
+
const MAX_MESSAGE_CHARS = 20000;
|
|
20
|
+
|
|
21
|
+
/** HTML 转义:面板里所有用户/模型文本都必须走它(webview 里就是 XSS 面)。 */
|
|
22
|
+
function escapeHtml(text) {
|
|
23
|
+
return String(text)
|
|
24
|
+
.replace(/&/g, '&')
|
|
25
|
+
.replace(/</g, '<')
|
|
26
|
+
.replace(/>/g, '>')
|
|
27
|
+
.replace(/"/g, '"')
|
|
28
|
+
.replace(/'/g, ''');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* 面板状态模型(纯数据,便于单测)。
|
|
33
|
+
* @returns {{turns: Array<{question: string, answer: string, done: boolean, error: string|null}>,
|
|
34
|
+
* context: object|null, status: string, sessionId: string|null}}
|
|
35
|
+
*/
|
|
36
|
+
function createPanelState() {
|
|
37
|
+
return { turns: [], context: null, status: 'idle', sessionId: null };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** 追加一次提问(开始新一轮)。 */
|
|
41
|
+
function pushQuestion(state, question, context) {
|
|
42
|
+
state.turns.push({ question, answer: '', done: false, error: null });
|
|
43
|
+
while (state.turns.length > MAX_TURNS) state.turns.shift();
|
|
44
|
+
state.context = context ?? state.context;
|
|
45
|
+
state.status = 'sending';
|
|
46
|
+
state.sessionId = null;
|
|
47
|
+
return state.turns[state.turns.length - 1];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** 最后一次提问对应的轮次(没有就返回 null)。 */
|
|
51
|
+
function currentTurn(state) {
|
|
52
|
+
return state.turns.length === 0 ? null : state.turns[state.turns.length - 1];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* 把 host 推回来的 answers 应用到状态上。
|
|
57
|
+
* `answers` 是 `[{sessionId, text, done, at}]`(见 lib/bridge-answer.mjs);只认**本面板登记的会话**。
|
|
58
|
+
* @returns {boolean} 是否有变化(调用方据此决定要不要刷新 webview)
|
|
59
|
+
*/
|
|
60
|
+
function applyAnswers(state, answers, sessionId) {
|
|
61
|
+
const target = sessionId ?? state.sessionId;
|
|
62
|
+
if (target === null || target === undefined || !Array.isArray(answers)) return false;
|
|
63
|
+
const entry = answers.find((item) => item !== null && typeof item === 'object' && item.sessionId === target);
|
|
64
|
+
if (entry === undefined) return false;
|
|
65
|
+
const turn = currentTurn(state);
|
|
66
|
+
if (turn === null) return false;
|
|
67
|
+
const text = typeof entry.text === 'string' ? entry.text.slice(0, MAX_MESSAGE_CHARS) : '';
|
|
68
|
+
const done = entry.done === true;
|
|
69
|
+
const changed = turn.answer !== text || turn.done !== done || state.status !== (done ? 'idle' : 'thinking');
|
|
70
|
+
turn.answer = text;
|
|
71
|
+
turn.done = done;
|
|
72
|
+
state.status = done ? 'idle' : 'thinking';
|
|
73
|
+
return changed;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** 状态 → 面板可渲染的 JSON(postMessage 的载荷)。 */
|
|
77
|
+
function panelPayload(state) {
|
|
78
|
+
return {
|
|
79
|
+
type: 'state',
|
|
80
|
+
status: state.status,
|
|
81
|
+
context: state.context,
|
|
82
|
+
turns: state.turns.map((turn) => ({
|
|
83
|
+
question: turn.question,
|
|
84
|
+
answer: turn.answer,
|
|
85
|
+
done: turn.done === true,
|
|
86
|
+
error: turn.error ?? null,
|
|
87
|
+
})),
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** 上下文描述(标题栏那一行):文件:行 + 选区行数。 */
|
|
92
|
+
function describeContext(context) {
|
|
93
|
+
if (context === null || context === undefined) return '';
|
|
94
|
+
const name = typeof context.file === 'string' && context.file !== '' ? context.file : '(当前文件)';
|
|
95
|
+
const base = name.replace(/^.*[\\/]/, '');
|
|
96
|
+
if (Number.isSafeInteger(context.lineStart)) {
|
|
97
|
+
const range = Number.isSafeInteger(context.lineEnd) && context.lineEnd !== context.lineStart
|
|
98
|
+
? `${context.lineStart}-${context.lineEnd}`
|
|
99
|
+
: `${context.lineStart}`;
|
|
100
|
+
return `${base}:${range}`;
|
|
101
|
+
}
|
|
102
|
+
return base;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* 面板 HTML(一次性的外壳;之后靠 postMessage 增量更新,避免把用户正在输入的内容冲掉)。
|
|
107
|
+
* @param {{cspSource: string, nonce: string}} options
|
|
108
|
+
*/
|
|
109
|
+
function renderPanelHtml({ cspSource, nonce }) {
|
|
110
|
+
return `<!DOCTYPE html>
|
|
111
|
+
<html lang="zh-CN">
|
|
112
|
+
<head>
|
|
113
|
+
<meta charset="utf-8">
|
|
114
|
+
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src ${cspSource} 'unsafe-inline'; script-src 'nonce-${nonce}';">
|
|
115
|
+
<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
|
+
</head>
|
|
139
|
+
<body>
|
|
140
|
+
<header id="ctx">来自编辑器</header>
|
|
141
|
+
<div id="log"></div>
|
|
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>
|
|
204
|
+
</body>
|
|
205
|
+
</html>`;
|
|
206
|
+
}
|
|
207
|
+
|
|
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
|
+
module.exports = {
|
|
219
|
+
MAX_TURNS,
|
|
220
|
+
MAX_MESSAGE_CHARS,
|
|
221
|
+
escapeHtml,
|
|
222
|
+
createPanelState,
|
|
223
|
+
pushQuestion,
|
|
224
|
+
currentTurn,
|
|
225
|
+
applyAnswers,
|
|
226
|
+
panelPayload,
|
|
227
|
+
describeContext,
|
|
228
|
+
renderPanelHtml,
|
|
229
|
+
statusText,
|
|
230
|
+
};
|
|
@@ -277,7 +277,10 @@ 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
|
-
|
|
280
|
+
// answers = 「问过 DSH 的会话」的最新回复(0.3.19)。提问框靠它就地显示回答,
|
|
281
|
+
// 所以必须原样透传 —— 它走独立字段而不是事件环形缓冲(见 host 侧 lib/bridge-answer.mjs)。
|
|
282
|
+
const answers = body !== null && Array.isArray(body.answers) ? body.answers : [];
|
|
283
|
+
return { ok: true, events, answers };
|
|
281
284
|
} catch (error) {
|
|
282
285
|
return { ok: false, error: error.message, status: error.status, code: error.code };
|
|
283
286
|
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "dshcs-editor-bridge",
|
|
3
3
|
"displayName": "DSH Editor Bridge",
|
|
4
4
|
"description": "只读地把编辑器状态(未保存缓冲区、诊断、活动选区)提供给 DSH,并让编辑器里的动作驱动 DSH 会话。由 dsh-code-server-app 插件安装,可禁用。",
|
|
5
|
-
"version": "0.
|
|
5
|
+
"version": "0.2.0",
|
|
6
6
|
"publisher": "dsh-code-server-app",
|
|
7
7
|
"license": "MIT",
|
|
8
8
|
"private": true,
|
|
@@ -21,14 +21,12 @@
|
|
|
21
21
|
{
|
|
22
22
|
"command": "dsh-code-server.askAboutSelection",
|
|
23
23
|
"title": "DSH: 针对选中内容提问",
|
|
24
|
-
"category": "DSH"
|
|
25
|
-
"icon": "$(comment-discussion)"
|
|
24
|
+
"category": "DSH"
|
|
26
25
|
},
|
|
27
26
|
{
|
|
28
27
|
"command": "dsh-code-server.askAboutFile",
|
|
29
28
|
"title": "DSH: 针对当前文件提问",
|
|
30
|
-
"category": "DSH"
|
|
31
|
-
"icon": "$(comment)"
|
|
29
|
+
"category": "DSH"
|
|
32
30
|
},
|
|
33
31
|
{
|
|
34
32
|
"command": "dsh-code-server.showBridgeLog",
|
|
@@ -47,17 +45,6 @@
|
|
|
47
45
|
"command": "dsh-code-server.askAboutFile",
|
|
48
46
|
"group": "navigation@-1"
|
|
49
47
|
}
|
|
50
|
-
],
|
|
51
|
-
"editor/title": [
|
|
52
|
-
{
|
|
53
|
-
"command": "dsh-code-server.askAboutSelection",
|
|
54
|
-
"when": "editorHasSelection",
|
|
55
|
-
"group": "navigation@-2"
|
|
56
|
-
},
|
|
57
|
-
{
|
|
58
|
-
"command": "dsh-code-server.askAboutFile",
|
|
59
|
-
"group": "navigation@-1"
|
|
60
|
-
}
|
|
61
48
|
]
|
|
62
49
|
}
|
|
63
50
|
},
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/bridge-answer.mjs — 把 agent 的回复同步回编辑器(0.3.19)。
|
|
3
|
+
*
|
|
4
|
+
* 方向:DSH → 编辑器。用户在编辑器里问了一句之后,当然想**在原地**看到回答,而不是切回 DSH 界面。
|
|
5
|
+
* 桥本来就是"扩展每 600ms 拉一次 `/sync`",所以回复走同一趟,新增一个字段而不是挤进事件环形缓冲:
|
|
6
|
+
*
|
|
7
|
+
* `/sync` 响应 = `{ ok, events, answers }`
|
|
8
|
+
* `answers` = `[{ sessionId, text, done, at }]` —— **每个会话只保留最新一条**,
|
|
9
|
+
* 且每次都是"到目前为止的完整正文"(不是增量)。这样:
|
|
10
|
+
* · 事件环形缓冲(64 条)不被正文挤爆 —— `agent-edit` 那些提示不会因此丢掉;
|
|
11
|
+
* · 丢中间态无所谓 —— 面板永远渲染最新那条完整正文,不需要做增量合并;
|
|
12
|
+
*
|
|
13
|
+
* 数据来源(都是 agent 作用域的事件,根上下文监听器能看到所有 agent):
|
|
14
|
+
* - `agent/assistant-stream` 的 `text-delta` 帧 → 累积正文(**不含 reasoning**);
|
|
15
|
+
* - `agent/status` → `idle`(这一轮跑完了)→ 标记 `done:true`。
|
|
16
|
+
*
|
|
17
|
+
* 只同步"**被编辑器问过**的会话"(ask 时登记 sessionId):别的会话在 DSH 界面里聊得再多,
|
|
18
|
+
* 也不该往桥里塞数据。会话数有上限,超了丢最旧的。
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/** 每个会话保留的最新回复;超过这么多会话就丢最旧的(编辑器里问过的会话不会有几个)。 */
|
|
22
|
+
export const MAX_TRACKED_SESSIONS = 8;
|
|
23
|
+
|
|
24
|
+
/** 回复正文上限:面板只用来显示,超长截断(避免一次 sync 把响应撑大)。 */
|
|
25
|
+
export const MAX_ANSWER_CHARS = 20000;
|
|
26
|
+
|
|
27
|
+
/** 累积器的实现:`registerBridgeAnswers` 用它,测试也直接用它。 */
|
|
28
|
+
export function createAnswerBoard({ maxSessions = MAX_TRACKED_SESSIONS } = {}) {
|
|
29
|
+
/** sessionId → { text, done, at }(插入顺序即新旧顺序) */
|
|
30
|
+
const entries = new Map();
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
/** ask 成功时登记:之后这个会话的回复才会同步给编辑器。 */
|
|
34
|
+
track(sessionId) {
|
|
35
|
+
if (typeof sessionId !== 'string' || sessionId === '') return false;
|
|
36
|
+
entries.delete(sessionId);
|
|
37
|
+
entries.set(sessionId, { text: '', done: false, at: Date.now() });
|
|
38
|
+
while (entries.size > maxSessions) entries.delete(entries.keys().next().value);
|
|
39
|
+
return true;
|
|
40
|
+
},
|
|
41
|
+
/** 追加一段正文(只在已登记的会话上生效)。 */
|
|
42
|
+
append(sessionId, text) {
|
|
43
|
+
if (typeof text !== 'string' || text === '' || !entries.has(sessionId)) return false;
|
|
44
|
+
const current = entries.get(sessionId);
|
|
45
|
+
const next = current.text + text;
|
|
46
|
+
entries.set(sessionId, {
|
|
47
|
+
text: next.length > MAX_ANSWER_CHARS ? next.slice(-MAX_ANSWER_CHARS) : next,
|
|
48
|
+
done: false,
|
|
49
|
+
at: Date.now(),
|
|
50
|
+
});
|
|
51
|
+
return true;
|
|
52
|
+
},
|
|
53
|
+
/** 一轮跑完(或出错):定稿。 */
|
|
54
|
+
finish(sessionId) {
|
|
55
|
+
const current = entries.get(sessionId);
|
|
56
|
+
if (current === undefined) return false;
|
|
57
|
+
entries.set(sessionId, { ...current, done: true, at: Date.now() });
|
|
58
|
+
return true;
|
|
59
|
+
},
|
|
60
|
+
/** 新的一轮开始:清掉上一次的正文,避免新旧回答拼在一起。 */
|
|
61
|
+
restart(sessionId) {
|
|
62
|
+
if (!entries.has(sessionId)) return false;
|
|
63
|
+
entries.set(sessionId, { text: '', done: false, at: Date.now() });
|
|
64
|
+
return true;
|
|
65
|
+
},
|
|
66
|
+
/** 给 `/sync` 的快照(数组顺序 = 登记顺序)。 */
|
|
67
|
+
snapshot() {
|
|
68
|
+
return [...entries.entries()].map(([sessionId, value]) => ({ sessionId, ...value }));
|
|
69
|
+
},
|
|
70
|
+
/** 某会话当前状态(诊断/测试用)。 */
|
|
71
|
+
get(sessionId) {
|
|
72
|
+
return entries.get(sessionId) ?? null;
|
|
73
|
+
},
|
|
74
|
+
size() {
|
|
75
|
+
return entries.size;
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** 从 agent 对象取会话 id(与 lib/bridge-session.mjs 同一套取值规则)。 */
|
|
81
|
+
export function sessionIdOf(agent) {
|
|
82
|
+
if (agent === null || agent === undefined) return null;
|
|
83
|
+
if (agent.session !== undefined && agent.session !== null && typeof agent.session === 'object') {
|
|
84
|
+
return agent.session.id ?? agent.id ?? null;
|
|
85
|
+
}
|
|
86
|
+
return agent.id ?? null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* 订阅 agent 事件,把回复喂进 board。
|
|
91
|
+
*
|
|
92
|
+
* @param {object} ctx cordis 上下文(需要 `on`)
|
|
93
|
+
* @param {{board: ReturnType<typeof createAnswerBoard>, log?: (message: string) => void}} deps
|
|
94
|
+
* @returns {() => void} 注销函数(永不抛)
|
|
95
|
+
*/
|
|
96
|
+
export function registerBridgeAnswers(ctx, deps) {
|
|
97
|
+
const { board } = deps;
|
|
98
|
+
const log = deps.log ?? (() => {});
|
|
99
|
+
if (ctx === undefined || ctx === null || typeof ctx.on !== 'function') return () => {};
|
|
100
|
+
const disposers = [];
|
|
101
|
+
/** sessionId → 上次看到的 turn(换轮时重新累积正文)。 */
|
|
102
|
+
const lastTurn = new Map();
|
|
103
|
+
const on = (event, handler) => {
|
|
104
|
+
try {
|
|
105
|
+
const dispose = ctx.on(event, handler);
|
|
106
|
+
if (typeof dispose === 'function') disposers.push(dispose);
|
|
107
|
+
} catch (err) {
|
|
108
|
+
log(`订阅 ${event} 失败:${err && err.message ? err.message : String(err)}`);
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
on('agent/assistant-stream', (payload) => {
|
|
113
|
+
try {
|
|
114
|
+
const agent = payload?.agent;
|
|
115
|
+
const frame = payload?.frame;
|
|
116
|
+
if (frame?.type !== 'chunk') return;
|
|
117
|
+
const chunk = frame.chunk;
|
|
118
|
+
// 只同步**正文**:reasoning 是模型的内心戏,不该出现在编辑器的提问框里。
|
|
119
|
+
if (chunk?.type !== 'text-delta' || typeof chunk.text !== 'string' || chunk.text === '') return;
|
|
120
|
+
const sessionId = sessionIdOf(agent);
|
|
121
|
+
if (sessionId === null) return;
|
|
122
|
+
// 换了一轮(turn 变了)就重新累积:新问题给的是新回答,不该和上一轮拼在一起。
|
|
123
|
+
const turn = Number.isSafeInteger(frame.turn) ? frame.turn : null;
|
|
124
|
+
if (turn !== null && lastTurn.get(sessionId) !== turn) {
|
|
125
|
+
lastTurn.set(sessionId, turn);
|
|
126
|
+
board.restart(sessionId);
|
|
127
|
+
}
|
|
128
|
+
board.append(sessionId, chunk.text);
|
|
129
|
+
} catch (err) {
|
|
130
|
+
// 同步失败绝不影响 agent 的一轮(这与 lib/bridge-observe.mjs 同一策略)。
|
|
131
|
+
log(`同步回复失败:${err && err.message ? err.message : String(err)}`);
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
on('agent/status', (payload) => {
|
|
136
|
+
try {
|
|
137
|
+
if (payload?.status !== 'idle') return;
|
|
138
|
+
const sessionId = sessionIdOf(payload?.agent);
|
|
139
|
+
if (sessionId === null) return;
|
|
140
|
+
board.finish(sessionId);
|
|
141
|
+
} catch (err) {
|
|
142
|
+
log(`收尾回复失败:${err && err.message ? err.message : String(err)}`);
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
return () => {
|
|
147
|
+
for (const dispose of disposers) {
|
|
148
|
+
try {
|
|
149
|
+
dispose();
|
|
150
|
+
} catch {
|
|
151
|
+
// 注销失败无关正确性
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
}
|
package/lib/bridge-session.mjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* lib/bridge-session.mjs — 把编辑器里的动作投递成 DSH
|
|
2
|
+
* lib/bridge-session.mjs — 把编辑器里的动作投递成 DSH 的一条**用户消息**(0.3.0;0.3.19 起以用户输入呈现)。
|
|
3
3
|
*
|
|
4
4
|
* 方向:编辑器 → DSH。用户在编辑器里选中一段代码、说一句"这个函数为什么是错的",
|
|
5
|
-
*
|
|
5
|
+
* 这句话应当出现在**当前会话**里(就像用户自己敲进去的),并且带上文件:行与选区文本。
|
|
6
6
|
*
|
|
7
7
|
* 投递目标的选取顺序(都通过 `ctx.get(...)` 特性探测,DSH 版本差异不会炸主流程):
|
|
8
8
|
* 1. `ctx.agents.currentInitiator()` —— 正在跑的那次驱动链的 agent(最贴近"当前会话");
|
|
@@ -122,9 +122,14 @@ export async function deliverEditorPrompt(ctx, input) {
|
|
|
122
122
|
const text = composeEditorPrompt(input);
|
|
123
123
|
let message;
|
|
124
124
|
try {
|
|
125
|
+
// **以"用户输入"进对话(0.3.19)**:`source:{kind:'user'}` 让 DSH 把它渲染成用户消息。
|
|
126
|
+
// 0.3.0–0.3.18 用的是 `{kind:'plugin', form:'notice', summary:'来自编辑器'}` ——
|
|
127
|
+
// `MessageSourceMap.plugin` 是 `plugin + ContextFormed`,DSH 会把它当成**上下文更新**,
|
|
128
|
+
// 于是用户在界面里看到的不是"自己说的话"。来源信息不丢:正文第一行仍是
|
|
129
|
+
// `From the editor: <file>:<行>`,扩展侧面板也标着"来自编辑器"。
|
|
125
130
|
message = createUserMessage({
|
|
126
131
|
content: [{ type: 'text', text }],
|
|
127
|
-
source: { kind: '
|
|
132
|
+
source: { kind: 'user' },
|
|
128
133
|
});
|
|
129
134
|
} catch (err) {
|
|
130
135
|
return { ok: false, code: 'BAD_MESSAGE', error: `构造消息失败:${err && err.message ? err.message : String(err)}` };
|
package/lib/index.js
CHANGED
|
@@ -54,6 +54,7 @@ import { bridgeEndpointPath, startBridgeListener } from './bridge-ipc.mjs';
|
|
|
54
54
|
import { registerEditorPrompt, registerEditorTools, setPromptLiveProbe } from './bridge-tools.mjs';
|
|
55
55
|
import { deliverEditorPrompt } from './bridge-session.mjs';
|
|
56
56
|
import { registerBridgeObserver } from './bridge-observe.mjs';
|
|
57
|
+
import { createAnswerBoard, registerBridgeAnswers } from './bridge-answer.mjs';
|
|
57
58
|
|
|
58
59
|
// schemastery 由 DSH 部署自带(官方核心依赖),仿 auto-open-web 的解析策略:
|
|
59
60
|
// 常规 import 优先,不可用时回退到全局 npm 布局的 DSH 部署副本。
|
|
@@ -722,10 +723,14 @@ export async function apply(ctx, config) {
|
|
|
722
723
|
let bridgePromptDispose = null;
|
|
723
724
|
/** agent 写操作观察器的 disposer。 */
|
|
724
725
|
let bridgeObserveDispose = null;
|
|
726
|
+
/** agent 回复同步(提问框)的 disposer。 */
|
|
727
|
+
let bridgeAnswerDispose = null;
|
|
725
728
|
/** 推给编辑器的事件环形缓冲(tools/result → 扩展轮询)。 */
|
|
726
729
|
const bridgeEvents = createEventRing();
|
|
727
730
|
/** 编辑器状态缓存:扩展在每次 /sync 里推上来,agent 的工具调用来读它。 */
|
|
728
731
|
const bridgeContext = createContextCache();
|
|
732
|
+
/** 「问过 DSH 的会话」的最新回复(随 /sync 的 answers 同步回编辑器提问框)。 */
|
|
733
|
+
const bridgeAnswers = createAnswerBoard();
|
|
729
734
|
|
|
730
735
|
/** 本次启动生成的新桥令牌;null = 本实例没有令牌(adopt 旧实例时会回读 bridge.json)。 */
|
|
731
736
|
let bridgeToken = null;
|
|
@@ -850,6 +855,12 @@ export async function apply(ctx, config) {
|
|
|
850
855
|
isLive: () => bridgeMeta !== null && !bridgeContext.isStale(),
|
|
851
856
|
});
|
|
852
857
|
|
|
858
|
+
// 把 agent 的回复同步给编辑器里的「提问框」(0.3.19):正文随 /sync 的 answers 字段回去。
|
|
859
|
+
bridgeAnswerDispose = registerBridgeAnswers(ctx, {
|
|
860
|
+
board: bridgeAnswers,
|
|
861
|
+
log: (message) => console.warn(`[code-server] 编辑器桥回复同步:${message}`),
|
|
862
|
+
});
|
|
863
|
+
|
|
853
864
|
/** 桥配置要写的目录(可能有**两个**):
|
|
854
865
|
*
|
|
855
866
|
* ① `<extensionsDir>/.dshcs-bridge/` —— 权威位置;host 侧 /status 与诊断都指向它,
|
|
@@ -1661,7 +1672,9 @@ export async function apply(ctx, config) {
|
|
|
1661
1672
|
}
|
|
1662
1673
|
const events = bridgeEvents.since(since);
|
|
1663
1674
|
bridgeEvents.reset();
|
|
1664
|
-
|
|
1675
|
+
// 回复走独立字段(不是事件环形缓冲):每个会话只给**最新一条完整正文**,
|
|
1676
|
+
// 这样丢中间态无所谓、也不会把 agent-edit 事件挤掉(见 lib/bridge-answer.mjs)。
|
|
1677
|
+
return jsonResponse({ ok: true, events, answers: bridgeAnswers.snapshot() });
|
|
1665
1678
|
}
|
|
1666
1679
|
|
|
1667
1680
|
async function handleBridgeAsk(request) {
|
|
@@ -1689,6 +1702,10 @@ export async function apply(ctx, config) {
|
|
|
1689
1702
|
selection,
|
|
1690
1703
|
languageId: typeof body.languageId === 'string' ? body.languageId : null,
|
|
1691
1704
|
});
|
|
1705
|
+
// 登记这个会话:之后它的回复会随 /sync 同步回编辑器(见 lib/bridge-answer.mjs)。
|
|
1706
|
+
if (result.ok === true && typeof result.sessionId === 'string' && result.sessionId !== '') {
|
|
1707
|
+
bridgeAnswers.track(result.sessionId);
|
|
1708
|
+
}
|
|
1692
1709
|
return jsonResponse(result, result.ok === true ? 200 : (result.code === 'NO_AGENT' ? 409 : 200));
|
|
1693
1710
|
}
|
|
1694
1711
|
|
|
@@ -1728,6 +1745,7 @@ export async function apply(ctx, config) {
|
|
|
1728
1745
|
disposeKilled = true;
|
|
1729
1746
|
stopPolling();
|
|
1730
1747
|
bridgeObserveDispose = disposeSafely(bridgeObserveDispose);
|
|
1748
|
+
bridgeAnswerDispose = disposeSafely(bridgeAnswerDispose);
|
|
1731
1749
|
bridgePromptDispose = disposeSafely(bridgePromptDispose);
|
|
1732
1750
|
clearBridgeRuntime();
|
|
1733
1751
|
for (const d of disposers) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-code-server-app",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.19",
|
|
4
4
|
"description": "VS Code (from a code-server release) inside DSH: a right-sidebar tab driven by the plugin's own launcher over the in-process VS Code server (lib/launcher.mjs). Since 0.3.0 the bundle also ships an editor bridge (assets/extensions/dshcs-editor-bridge): a read-only channel between the in-tree VS Code extension host and DSH, giving the agent what only the editor knows (unsaved buffers, language-server diagnostics, the active selection) and letting editor gestures drive the session. The tab claims DSH file addresses (dsh-resource://file/**) by file type (setting claimExtensions), so the product's own produced-file chips, delivered-file previews and inline prose mentions open in the workbench. The IDE is a resident surface moved with Element.moveBefore instead of being remounted, so switching sidebar tabs no longer reloads it. Opening the tab switches the right sidebar to fullscreen by default (setting fullscreenOnOpen). Following a workspace switch is lightweight: the workbench re-navigates with the new ?folder= and the IDE process is not restarted (since 0.2.12). Requires a DSH with the right-sidebar services (sidebarRightTabs/sidebarRight, >= 0.1.5-alpha.1); older DSH versions get a single upgrade notice on the settings page and no other UI. Two serving modes: loopback port (default) or same-origin mount on DSH's own webServer (/code-server, protected by ctx.connection.requestRejection). No code-server Node layer, no argon2, no C++ toolchain.",
|
|
5
5
|
"homepage": "https://github.com/jinsiyu/dsh-code-server-app",
|
|
6
6
|
"repository": {
|
|
@@ -43,6 +43,7 @@
|
|
|
43
43
|
"lib/bridge-tools.mjs",
|
|
44
44
|
"lib/bridge-session.mjs",
|
|
45
45
|
"lib/bridge-observe.mjs",
|
|
46
|
+
"lib/bridge-answer.mjs",
|
|
46
47
|
"lib/dsh-resolve.mjs",
|
|
47
48
|
"lib/launcher.mjs",
|
|
48
49
|
"lib/serve-dsh.mjs",
|
package/vendor/VENDOR.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"vscodeVersion": "1.137.0",
|
|
4
4
|
"productPath": "stable-b11dabdaca0d3369986975be285db92c8795cea5",
|
|
5
5
|
"layout": "vscode-only",
|
|
6
|
-
"preparedAt": "2026-09-13T06:
|
|
6
|
+
"preparedAt": "2026-09-13T06:39:47.464Z",
|
|
7
7
|
"source": "registry",
|
|
8
8
|
"node": "v24.21.0",
|
|
9
9
|
"platform": "win32",
|