dsh-code-server-app 0.3.20 → 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 +82 -19
  2. package/README.md +74 -21
  3. package/assets/extensions/dshcs-editor-bridge/extension.js +120 -64
  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
@@ -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) {
@@ -419,19 +418,25 @@ function updateStatusBar() {
419
418
 
420
419
  // ---------------------------------------------------------------- 命令 / 提问面板
421
420
 
422
- /** 当前编辑器上下文(发送时现取一次:用户可以在面板开着的同时换选区)。 */
423
- function captureAskContext() {
421
+ /**
422
+ * 当前编辑器上下文(发送时现取一次:用户可以在面板开着的同时换选区)。
423
+ *
424
+ * @param {'selection'|'file'} mode 提问意图:
425
+ * - `selection`(「针对选中内容提问」):有选区就带上**行区间 + 选区正文**;
426
+ * 没有选区时退化成**纯文件**(不带行号 —— 光标停在哪一行跟问题无关,行号只会误导 agent);
427
+ * - `file`(「针对当前文件提问」):**永远不带行号、不带选区**,只给文件路径。
428
+ */
429
+ function captureAskContext(mode) {
424
430
  const editor = vscode.window.activeTextEditor;
425
431
  if (editor === undefined || editor === null) return null;
426
432
  const doc = editor.document;
427
433
  const selection = editor.selection;
428
- const hasSelection = selection !== undefined && selection !== null && selection.isEmpty === false;
434
+ const hasSelection = mode === 'selection'
435
+ && selection !== undefined && selection !== null && selection.isEmpty === false;
429
436
  const selectedText = hasSelection ? doc.getText(selection) : '';
430
437
  return {
431
438
  file: doc.uri.scheme === 'file' ? doc.uri.fsPath : null,
432
- lineStart: hasSelection
433
- ? selection.start.line + 1
434
- : (doc.isDirty ? null : selection.active.line + 1),
439
+ lineStart: hasSelection ? selection.start.line + 1 : null,
435
440
  lineEnd: hasSelection ? selection.end.line + 1 : null,
436
441
  selection: selectedText === '' ? null : selectedText,
437
442
  languageId: doc.languageId ?? null,
@@ -441,32 +446,23 @@ function captureAskContext() {
441
446
  /** 把状态推给面板(增量更新:输入框与滚动位置都不受影响)。 */
442
447
  function refreshAskPanel() {
443
448
  if (askPanel === null) return;
444
- const payload = panelPayload(askPanel.state);
445
- payload.contextText = askPanel.state.context === null
446
- ? '来自编辑器'
447
- : `来自编辑器:${describeContext(askPanel.state.context)}`;
448
- payload.statusText = askPanel.state.status === 'error'
449
- ? (askPanel.state.error ?? '出错了')
450
- : statusText(askPanel.state);
451
- void askPanel.panel.webview.postMessage(payload);
449
+ void askPanel.panel.webview.postMessage(panelPayload(askPanel.state));
452
450
  }
453
451
 
454
- /** 面板进入错误态:状态行 + 当前轮都标上原因(绝不静默)。 */
452
+ /** 面板进入错误态:状态行 + 最新一条本地提问都标上原因(绝不静默)。 */
455
453
  function failAskPanel(message) {
456
454
  if (askPanel === null) return;
457
- askPanel.state.status = 'error';
458
- askPanel.state.error = message;
459
- const turn = currentTurn(askPanel.state);
460
- if (turn !== null) turn.error = message;
455
+ failPanel(askPanel.state, message);
461
456
  log(message);
462
457
  refreshAskPanel();
463
458
  }
464
459
 
465
- /** 面板收到一条提问:投递到 DSH,并把这一轮放进状态里等回答。 */
460
+ /** 面板收到一条提问:投递到 DSH,**本地先乐观显示**,回答由 host 的对话流回填。 */
466
461
  async function sendAskFromPanel(text) {
467
462
  if (askPanel === null || client === null) return;
468
- const context = captureAskContext() ?? askPanel.state.context;
469
- const turn = pushQuestion(askPanel.state, text, context);
463
+ // 用面板自己的意图(selection / file)取上下文:文件级提问永远不带行号。
464
+ const context = captureAskContext(askPanel.mode) ?? askPanel.state.context;
465
+ pendingQuestion(askPanel.state, text, context);
470
466
  refreshAskPanel();
471
467
  try {
472
468
  const result = await client.ask({
@@ -482,39 +478,82 @@ async function sendAskFromPanel(text) {
482
478
  askPanel.state.status = 'thinking';
483
479
  log(`已投递编辑器消息(session=${result.sessionId ?? '?'})`);
484
480
  } else {
485
- askPanel.state.status = 'error';
486
- askPanel.state.error = `未投递:${result.error ?? '未知原因'}`;
487
- turn.error = askPanel.state.error;
481
+ failAskPanel(`未投递:${result.error ?? '未知原因'}`);
482
+ return;
488
483
  }
489
484
  } catch (error) {
490
485
  const status = error && error.status;
491
- const message = status === 409
486
+ failAskPanel(status === 409
492
487
  ? 'DSH 里没有可投递的会话:请先在 DSH 里打开或新建一个会话。'
493
488
  : (status === 401 || status === 503
494
489
  ? '编辑器桥尚未就绪,请稍后重试。'
495
- : `投递失败:${error && error.message ? error.message : error}`);
496
- // 桥没就绪时挂断会话:面板据此显示原因,轮询恢复后下一次发送即可。
497
- askPanel.state.status = 'error';
498
- askPanel.state.error = message;
499
- turn.error = message;
490
+ : `投递失败:${error && error.message ? error.message : error}`));
491
+ return;
500
492
  }
501
493
  refreshAskPanel();
502
494
  }
503
495
 
504
- /** 打开(或聚焦)「问 DSH」面板。上下文只用来显示;真正的上下文在发送时现取。 */
505
- function openAskPanel(context) {
506
- const contextInfo = context ?? captureAskContext();
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;
519
+ }
520
+ refreshAskPanel();
521
+ }
522
+
523
+ /**
524
+ * 打开(或聚焦)「问 DSH」面板。
525
+ * @param {'selection'|'file'} mode 提问意图(决定发送时带不带行号/选区),见 captureAskContext。
526
+ */
527
+ function openAskPanel(mode = 'selection') {
528
+ const contextInfo = captureAskContext(mode);
507
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
+ }
508
540
  const panel = vscode.window.createWebviewPanel(
509
541
  'dshAsk',
510
542
  'DSH 提问',
511
543
  { viewColumn: vscode.ViewColumn.Beside, preserveFocus: false },
512
- { enableScripts: true, retainContextWhenHidden: true },
544
+ {
545
+ enableScripts: true,
546
+ retainContextWhenHidden: true,
547
+ // 只允许加载扩展自己的 webview 目录(产物 + 字体),不给别的本地文件开口子。
548
+ localResourceRoots: [webviewDir],
549
+ },
513
550
  );
514
- askPanel = { panel, state: createPanelState() };
551
+ askPanel = { panel, state: createPanelState(), mode };
515
552
  panel.webview.html = renderPanelHtml({
516
553
  cspSource: panel.webview.cspSource,
517
554
  nonce: crypto.randomBytes(16).toString('base64url'),
555
+ scriptUri: panel.webview.asWebviewUri(bundle).toString(),
556
+ styleUri: panel.webview.asWebviewUri(style).toString(),
518
557
  });
519
558
  panel.webview.onDidReceiveMessage((message) => {
520
559
  if (message === null || typeof message !== 'object') return;
@@ -525,6 +564,10 @@ function openAskPanel(context) {
525
564
  if (message.type === 'ask' && typeof message.text === 'string') {
526
565
  const text = message.text.trim();
527
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);
528
571
  }
529
572
  });
530
573
  panel.onDidDispose(() => {
@@ -532,22 +575,32 @@ function openAskPanel(context) {
532
575
  });
533
576
  }
534
577
  askPanel.state.context = contextInfo;
578
+ askPanel.mode = mode;
535
579
  askPanel.panel.reveal(vscode.ViewColumn.Beside, false);
536
580
  refreshAskPanel();
537
581
  }
538
582
 
539
583
  /** 选中内容 → DSH(编辑器→DSH 的主入口):打开面板并带上当前上下文。 */
540
584
  async function askAboutSelection() {
585
+ await openAskPanelFor('selection');
586
+ }
587
+
588
+ /** 整个文件 → DSH(不带选中、**不带行号**)。 */
589
+ async function askAboutFile() {
590
+ await openAskPanelFor('file');
591
+ }
592
+
593
+ /** 两个命令的公共前置检查(桥可用 + 有活动编辑器)。 */
594
+ async function openAskPanelFor(mode) {
541
595
  if (client === null || client.isDormant()) {
542
596
  vscode.window.showInformationMessage('编辑器桥未启用:请在 DSH 里打开 Code Server 标签后重试。');
543
597
  return;
544
598
  }
545
- openAskPanel(captureAskContext());
546
- }
547
-
548
- /** 整个文件 → DSH(不需要选中)。 */
549
- async function askAboutFile() {
550
- await askAboutSelection();
599
+ if (captureAskContext(mode) === null) {
600
+ vscode.window.showInformationMessage('没有活动的编辑器:请先打开一个文件。');
601
+ return;
602
+ }
603
+ openAskPanel(mode);
551
604
  }
552
605
 
553
606
  function showBridgeLog() {
@@ -560,6 +613,9 @@ function activate(context) {
560
613
  output = vscode.window.createOutputChannel('DSH Editor Bridge');
561
614
  context.subscriptions.push(output);
562
615
 
616
+ // 面板 webview 的产物按扩展根目录取 URI(0.2.3 起正文走官方渲染器,产物是打包出来的)。
617
+ extensionRoot = context.extensionUri;
618
+
563
619
  statusBar = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right, 90);
564
620
  context.subscriptions.push(statusBar);
565
621