dsh-vscode-mode 0.20.2 → 0.20.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-vscode-mode",
3
- "version": "0.20.2",
3
+ "version": "0.20.4",
4
4
  "description": "DSH 上的类 VSCode 编码体验:Monaco 编辑器(文件页签/QuickOpen/状态栏,可驻 DSH 0.1.5+ 官方右侧 Sidebar 与对话同屏)+ 命令栏(Ctrl+Shift+P)与可扩展指令系统 + VS Code 兼容代码片段(.code-snippets 全局/项目,IntelliSense 展开)+ Agent 编辑差异审查(整文件差异/采纳/拒绝/归档/回滚)+ SVN 集成(侧栏变更面板/日志弹窗/并排差异)+ 语言服务器(LSP)智能(跳转定义/引用/hover/大纲 + VSIX 扩展市场安装),状态持久化到工作区旁车,界面状态(页签/光标/侧栏/编辑 Tab/导航历史)镜像到 DSH home 跨重启恢复",
5
5
  "icon": "assets/icon.svg",
6
6
  "keywords": [
@@ -0,0 +1,126 @@
1
+ /**
2
+ * 官方差异栏派生视图:位置、导航提示、失效说明与 Keep 提示的唯一来源。
3
+ * 计数口径分离:待审是「记录数」,可跳转是「块数」,两者不同源,
4
+ * 全部块失效时必须给出可操作叙事,不能只留一句「无法定位」。
5
+ * 另含侧车(legacy)栏的标识文案与官方链路不可用说明:两套栏外观相近,
6
+ * 必须显式区分,否则用户会误判为插件未升级。
7
+ * 作者 ddj 2026年10月27号
8
+ */
9
+ import { sessionErrorText } from './sessionError.js'
10
+
11
+ /** 官方差异栏输入:全部来自 EditorView dock 快照,不读取磁盘或 RPC。 */
12
+ export interface ReviewBarInput {
13
+ /** 可定位(非 stale)的差异块数 */
14
+ jumps: number
15
+ /** 无法定位的历史差异块数 */
16
+ stale: number
17
+ /** 当前差异块索引(0 基) */
18
+ index: number
19
+ /** 编辑器内容是否已就绪;未就绪时不得断言差异失效 */
20
+ ready: boolean
21
+ /** 当前文件的官方待审记录数 */
22
+ pendingRecords: number
23
+ /** 当前记录是否可 Keep(detail.capability.keep) */
24
+ canKeep: boolean
25
+ }
26
+
27
+ /** 官方差异栏派生文案;调用方只负责渲染,不再自行拼装计数与提示。 */
28
+ export interface ReviewBarView {
29
+ /** 差异位置文案,如 `2 / 6`、`无可跳转`、`载入中` */
30
+ position: string
31
+ /** 上一个差异按钮提示(兼作禁用原因) */
32
+ navPrevTitle: string
33
+ /** 下一个差异按钮提示(兼作禁用原因) */
34
+ navNextTitle: string
35
+ /** 失效说明,空串表示不显示 */
36
+ notice: string
37
+ /** 说明级别:warn 表示需要用户决策 */
38
+ noticeTone: 'info' | 'warn'
39
+ /** Keep 按钮提示 */
40
+ keepHint: string
41
+ /** 待审计数提示 */
42
+ pendingTitle: string
43
+ }
44
+
45
+ /** @private @author ddj 2026年10月27号 @param value 任意计数输入 @returns 非负整数,NaN/负数/非数字归零 */
46
+ function safeCount(value: unknown): number {
47
+ const parsed = Number(value)
48
+ return Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : 0
49
+ }
50
+
51
+ /** @private @author ddj 2026年10月27号 @returns 无差异可跳转时的 Keep 提示 */
52
+ function fullStaleKeepHint(): string {
53
+ return '保留该记录的全部改动并结案:这些改动已不在文件中,记录将被标记为已采纳(不改写磁盘)'
54
+ }
55
+
56
+ /**
57
+ * 派生差异栏文案。
58
+ * @public
59
+ * @author ddj 2026年10月27号
60
+ * @param input 差异计数、就绪状态与 Keep 能力
61
+ * @returns 栏内位置、导航提示与失效说明
62
+ */
63
+ export function reviewBarView(input: ReviewBarInput): ReviewBarView {
64
+ const jumps = safeCount(input.jumps)
65
+ const stale = safeCount(input.stale)
66
+ const index = safeCount(input.index)
67
+ const pendingTitle = `${safeCount(input.pendingRecords)} 条官方待审记录` + (input.ready ? `,本文件可跳转 ${jumps} 处` : '')
68
+ const keepHint = jumps === 0 && stale > 0 && input.canKeep ? fullStaleKeepHint() : '保留当前记录的未处理修改'
69
+
70
+ // 载入期所有块天然 stale,此时只能报告进度,不能断言改动已失效。
71
+ if (input.ready !== true) {
72
+ return { position: '载入中', navPrevTitle: '文件载入中', navNextTitle: '文件载入中', notice: '', noticeTone: 'info', keepHint, pendingTitle }
73
+ }
74
+
75
+ if (jumps > 0) {
76
+ const at = Math.min(index + 1, jumps)
77
+ const notice = stale > 0 ? `${stale} 处历史差异无法定位(其余 ${jumps} 处可跳转),展开详情查看原始增删。` : ''
78
+ return { position: `${at} / ${jumps}`, navPrevTitle: '上一个差异', navNextTitle: '下一个差异', notice, noticeTone: 'info', keepHint, pendingTitle }
79
+ }
80
+
81
+ if (stale > 0) {
82
+ const reason = '该记录的改动已不在当前文件中,无可跳转差异'
83
+ const notice = `本记录 ${stale} 处改动均已不在当前文件中(被后续编辑覆盖或已撤销);可点 Keep 结案(只标记已采纳,不改写磁盘),或展开详情核对原始增删。`
84
+ return { position: '无可跳转', navPrevTitle: reason, navNextTitle: reason, notice, noticeTone: 'warn', keepHint, pendingTitle }
85
+ }
86
+
87
+ return { position: '—', navPrevTitle: '当前文件没有可定位差异', navNextTitle: '当前文件没有可定位差异', notice: '', noticeTone: 'info', keepHint, pendingTitle }
88
+ }
89
+
90
+ /** 非官方栏标识:没有官方待审记录时回落的那套栏必须自述来源,避免被当成旧版插件。 */
91
+ export const SIDECAR_LABEL = '历史改动'
92
+
93
+ /** 非官方栏标识的悬停说明:讲清这些改动为何不在官方列表里。 */
94
+ export const SIDECAR_LABEL_TITLE = '这些改动没有进入官方审查列表,是插件早期版本自动保存的改动留痕。Keep 只标记为已看过,不改文件;Undo 会校验后写回文件。'
95
+
96
+ /** 非官方 Keep 的作用域说明:只标记记录,不写磁盘。 */
97
+ export const SIDECAR_KEEP_TITLE = '标记这些历史改动为已看过:只改记录,不改写磁盘文件,也不影响官方审查。'
98
+
99
+ /** 非官方 Undo 的作用域说明:走原有回滚校验写回磁盘,但不影响官方记录。 */
100
+ export const SIDECAR_UNDO_TITLE = '撤销这些历史改动(写回受回滚校验保护):只影响这份历史记录,不影响官方审查。'
101
+
102
+ /** 当前端没有该会话时,差异栏里给中性说明而不是红色报错。 */
103
+ export const SESSION_DOWN_NOTICE = '这个会话没在当前端激活,官方审查数据暂时读不到;下面的历史改动仍可查看、Keep、Undo。'
104
+
105
+ /** 官方链路不可用提示;null 表示官方元数据正常,不需要提示。 */
106
+ export interface OfficialDownView {
107
+ /** 提示正文,已含「官方差异不可用」前缀与可读原因 */
108
+ text: string
109
+ /** 重试按钮文案 */
110
+ retryLabel: string
111
+ }
112
+
113
+ /**
114
+ * 把官方元数据通道的失败翻译成可操作提示。
115
+ * @public
116
+ * @author ddj 2026年10月28号
117
+ * @param error `edrv.review.list` 返回的错误原文(reviewFeed 的 error 字段)
118
+ * @returns 提示正文与重试文案;空错误表示官方通道正常
119
+ */
120
+ export function officialDownView(error: unknown): OfficialDownView | null {
121
+ const reason = sessionErrorText(error).trim()
122
+ if (!reason) return null
123
+
124
+ return { text: `官方差异不可用:${reason}`, retryLabel: '重试' }
125
+ }
126
+
@@ -36,3 +36,15 @@ export function sessionMissingDetail(): string {
36
36
  return '该会话可能正被其他 DSH 实例(桌面端 / Web 端)占用,或尚未在当前实例激活。' +
37
37
  '请在持有该会话的一端查看,或在本端打开该会话后重试。'
38
38
  }
39
+
40
+ /**
41
+ * 把任意错误文本归一为可展示文案:会话缺失类替换为终态提示,其余原样返回。
42
+ * 差异栏等狭窄区域共用,避免裸「会话不存在」与业务提示混排。
43
+ * @author ddj 2026年10月27号
44
+ * @param text 任意错误文本(host 业务 error、异常 message 等原始形态)
45
+ * @returns 会话缺失为「标题:详情」,其他错误为原文本,空值返回空串
46
+ */
47
+ export function sessionErrorText(text: unknown): string {
48
+ if (isSessionMissingError(text)) return sessionMissingTitle() + ':' + sessionMissingDetail()
49
+ return String(text ?? '')
50
+ }
@@ -304,6 +304,10 @@
304
304
  [data-edrv-diff-dock-shell] .edrv-reviewbar button:focus-visible { outline: 2px solid var(--dsw-alias-brand-primary, #3b63d9); outline-offset: 2px; }
305
305
  [data-edrv-diff-dock-shell] .edrv-review-notice { padding: 7px 12px; border-top: 1px solid var(--dsw-alias-border-l1, #e5e9ef); color: var(--dsw-alias-label-secondary, #697586); font-size: 11px; overflow-wrap: anywhere; }
306
306
  [data-edrv-diff-dock-shell] .edrv-review-error { color: var(--dsw-alias-state-error-primary, #b64043); }
307
+ [data-edrv-diff-dock-shell] .edrv-review-notice-warn { color: var(--dsw-alias-state-warning-primary, #a86a1f); }
308
+ [data-edrv-diff-dock-shell] .edrv-review-source { flex: none; padding: 1px 6px; border-radius: 999px; border: 1px solid var(--dsw-alias-border-secondary, #d4dae3); color: var(--dsw-alias-label-secondary, #697586); font-size: 11px; line-height: 16px; white-space: nowrap; }
309
+ [data-edrv-diff-dock-shell] .edrv-sidecar-down { display: flex; align-items: center; gap: 8px; }
310
+ [data-edrv-diff-dock-shell] .edrv-sidecar-down-text { flex: 1; min-width: 0; }
307
311
  [data-edrv-diff-dock-shell] .edrv-review-body { border-top: 1px solid var(--dsw-alias-border-l1, #e5e9ef); max-height: min(240px, 30vh); overflow: auto; padding: 8px 12px; }
308
312
  @container edrv-review (max-width: 420px) {
309
313
  .edrv-reviewbar .edrv-review-heading { gap: 6px; padding-inline: 10px; }
@@ -1,58 +1,24 @@
1
1
  // @ts-nocheck
2
2
  /**
3
- * dsh-vscode-mode client — DiffBox:每个文件底部紧凑差异条(per-file hunk + ⋮ 二级菜单)。
4
- * 迁移自原 src/client/index.ts 的 DiffBox,语义不改。
3
+ * dsh-vscode-mode client — DiffBox:差异条入口,按数据源选栏。
4
+ * 官方记录 → OfficialDiffBar;其余(侧车历史捕获)→ SidecarDiffBar,
5
+ * 两套栏同版式,避免用户把侧车栏误判成旧版插件。
5
6
  * 作者 ddj 2026-08-20
6
7
  */
7
8
  import React from 'react'
8
- import { callIdAttr } from '../state/records.js'
9
- import { canDecideFile, diffDockText } from '../diffDock.js'
10
- import { badgeOf } from './shared.js'
9
+ import { diffDockText } from '../diffDock.js'
11
10
  import { OfficialDiffBar } from './OfficialDiffBar.js'
11
+ import { SidecarDiffBar } from './SidecarDiffBar.js'
12
12
 
13
13
  /**
14
- * 折叠控制使用任务栏同尺寸的 chevron 轮廓,避免依赖未声明的运行时图标包。
14
+ * 差异条:当前文件内 diff 导航、Keep/Undo、详情展开与 ⋮ 二级菜单。
15
+ * @public
15
16
  * @author ddj 2026年08月25号
16
- * @param open 是否已展开
17
- * @returns 14×14 的装饰性 chevron SVG
18
- */
19
- function chevronOf(open) {
20
- const path = open ? 'M3.25 5.25 7 9l3.75-3.75' : 'M3.25 8.75 7 5l3.75 3.75'
21
- return React.createElement('svg', {
22
- width: 14,
23
- height: 14,
24
- viewBox: '0 0 14 14',
25
- fill: 'none',
26
- 'aria-hidden': true,
27
- focusable: false,
28
- }, React.createElement('path', {
29
- d: path,
30
- stroke: 'currentColor',
31
- strokeWidth: 1.2,
32
- strokeLinecap: 'round',
33
- strokeLinejoin: 'round',
34
- }))
35
- }
36
-
37
- /**
38
- * 差异条:当前文件内 diff 导航(↑↓←→)、Keep/Undo、展开 hunk 列表、⋮ 二级菜单
39
- * (Keep All / Undo All / 差异总览 / 归档 / 回滚 / 刷新)+ 其他差异文件小节。
17
+ * @param props 差异上下文(官方或侧车),由 EditorView dock 快照提供
18
+ * @returns 会话态精简入口,或按数据源路由到的差异栏
40
19
  */
41
20
  export function DiffBox(props) {
42
- const {
43
- pendingRegions = [], staleRegions = [], onAct, onAcceptFile, onUndoFile, onAcceptAllFiles, onUndoAllFiles,
44
- allPendingCount = 0, onRollback, onJump, otherFiles = [], onOpenOther, onOpenLauncher, onRefresh, activePath,
45
- diffIdx = 0, diffTotal = 0, fileIdx = 0, fileTotal = 0, onPrevDiff, onNextDiff, onPrevFile, onNextFile,
46
- mode = 'editor', onOpenNextFile, dock = false,
47
- } = props
48
- const [expanded, setExpanded] = React.useState(false)
49
- const [detailsOpen, setDetailsOpen] = React.useState(false)
50
- const detailsId = React.useId()
51
- const canAct = pendingRegions.length > 0
52
- // Keep/Undo 覆盖无法定位的冲突差异:它们仍占待处理列表,但不在 pendingRegions 内
53
- const canDecide = canDecideFile(pendingRegions.length, staleRegions.length)
54
-
55
- const base = String(activePath || '').split(/[\\/]/).pop() || ''
21
+ const { fileTotal = 0, onOpenNextFile, mode = 'editor' } = props
56
22
 
57
23
  if (mode === 'chat' || mode === 'editor-empty') {
58
24
  return React.createElement('div', { className: 'edrv-diffbar edrv-diffbar-dock', 'data-edrv-diffbox': '1', 'data-edrv-diff-mode': mode },
@@ -67,68 +33,5 @@ export function DiffBox(props) {
67
33
  }
68
34
 
69
35
  if (props.official) return React.createElement(OfficialDiffBar, props)
70
-
71
- let bodyEl = null
72
- if (expanded) {
73
- const rows = pendingRegions.map((r) => {
74
- const key = callIdAttr(r.callId, r.idx)
75
- return React.createElement('div', { key, className: 'edrv-diffrow', 'data-edrv-hunk': key, title: '跳转到 L' + (r.start ?? '?'), onClick: () => onJump(r) },
76
- React.createElement('span', { className: 'edrv-diffrow-l' }, 'L' + (r.start ?? '?') + '-' + (r.end ?? '?')),
77
- (r.oldLines.length ? React.createElement('span', { className: 'edrv-diffrow-o' }, '-' + r.oldLines.length) : null),
78
- (r.newLines.length ? React.createElement('span', { className: 'edrv-diffrow-n' }, '+' + r.newLines.length) : null),
79
- (r.create ? React.createElement('span', { className: 'edrv-diffrow-tag' }, '新建') : null),
80
- React.createElement('span', { style: { flex: 1 } }))
81
- })
82
- const stale = staleRegions.map((r) => React.createElement('div', { key: 'stale' + callIdAttr(r.callId, r.idx), className: 'edrv-diffrow edrv-diffrow-stale', title: '差异无法定位(文件可能已被手动修改)' },
83
- React.createElement('span', { className: 'edrv-diffrow-l' }, 'L' + (r.start ?? '?') + ' 无法定位'),
84
- React.createElement('span', { className: 'edrv-diffrow-tag' }, '冲突/需刷新'),
85
- React.createElement('span', { className: 'edrv-diffrow-tag' }, badgeOf(r.status))))
86
- let othersEl = null
87
- if (otherFiles.length) {
88
- othersEl = React.createElement('div', { className: 'edrv-diffbox-others' },
89
- React.createElement('span', { className: 'edrv-diffrow-tag' }, '其他差异文件 (' + otherFiles.length + '):'),
90
- otherFiles.map((f) => React.createElement('button', { key: f.path, className: 'edrv-diffrow-file', title: f.label ?? f.path, onClick: () => onOpenOther(f.path) },
91
- String(f.path).split(/[\\/]/).pop() + ' (' + f.pending + ')')))
92
- }
93
- bodyEl = React.createElement('div', { className: 'edrv-diffbar-body' }, ...rows, ...stale, othersEl)
94
- }
95
-
96
- const details = detailsOpen
97
- ? React.createElement('div', { className: 'edrv-diff-details', id: detailsId },
98
- React.createElement('button', { className: 'edrv-diffmenu-item', disabled: !allPendingCount, onClick: onAcceptAllFiles }, '✓ Keep All(全部文件采纳)'),
99
- React.createElement('button', { className: 'edrv-diffmenu-item danger', disabled: !allPendingCount, onClick: onUndoAllFiles }, '↩ Undo All(全部文件不采纳)'),
100
- React.createElement('div', { className: 'edrv-diffmenu-sep' }),
101
- React.createElement('button', { className: 'edrv-diffmenu-item', onClick: () => onOpenLauncher('pending') }, '🗂 差异总览'),
102
- React.createElement('button', { className: 'edrv-diffmenu-item', onClick: () => onOpenLauncher('archive') }, '📁 归档'),
103
- React.createElement('div', { className: 'edrv-diffmenu-sep' }),
104
- React.createElement('button', { className: 'edrv-diffmenu-item', disabled: !canAct, onClick: onRollback }, '⟲ 回滚文件'),
105
- React.createElement('button', { className: 'edrv-diffmenu-item', onClick: onRefresh }, '⟳ 刷新'))
106
- : null
107
-
108
- return React.createElement('div', { className: 'edrv-diffbar edrv-diffbar-dock', 'data-edrv-diffbox': '1', 'data-edrv-diff-mode': 'editor' },
109
- React.createElement('div', { className: 'edrv-diffbar-row edrv-diffbar-editor-row' },
110
- React.createElement('div', { className: 'edrv-diffbar-editor-content' },
111
- React.createElement('div', { className: 'edrv-diffbar-nav' },
112
- React.createElement('button', { className: 'edrv-pill edrv-pill-ghost', title: '上一个差异', disabled: !canAct, onClick: onPrevDiff }, '↑'),
113
- React.createElement('span', { className: 'edrv-diffbar-count edrv-diffbar-progress', title: '当前文件内差异位置(点击展开/收起差异列表)', onClick: () => setExpanded((v) => !v) }, diffTotal ? (diffIdx + 1) + '/' + diffTotal : '0/0'),
114
- React.createElement('button', { className: 'edrv-pill edrv-pill-ghost', title: '下一个差异', disabled: !canAct, onClick: onNextDiff }, '↓')),
115
- React.createElement('div', { className: 'edrv-diffbar-files' },
116
- React.createElement('button', { className: 'edrv-pill edrv-pill-ghost', title: '上一个差异文件', disabled: !fileTotal, onClick: onPrevFile }, '←'),
117
- React.createElement('span', { className: 'edrv-diffbar-count edrv-count-file edrv-diffbar-progress', title: '差异文件位置' }, fileTotal ? (fileIdx + 1) + '/' + fileTotal + ' 文件' : '0/0 文件'),
118
- React.createElement('button', { className: 'edrv-pill edrv-pill-ghost', title: '下一个差异文件', disabled: !fileTotal, onClick: onNextFile }, '→'),
119
- React.createElement('span', { className: 'edrv-diffbar-file edrv-diffbar-summary', title: activePath || '' }, base)),
120
-
121
- React.createElement('div', { className: 'edrv-diffbar-actions' },
122
- React.createElement('button', { className: 'edrv-pill edrv-pill-keep', title: '采纳当前文件的全部差异', disabled: !canDecide, onClick: onAcceptFile }, '✓ Keep'),
123
- React.createElement('button', { className: 'edrv-pill edrv-pill-undo', title: '不采纳当前文件的全部差异(回滚)', disabled: !canDecide, onClick: onUndoFile }, '↩ Undo'),
124
- React.createElement('button', {
125
- className: 'edrv-pill edrv-pill-ghost edrv-diff-details-toggle',
126
- title: detailsOpen ? '收起差异操作' : '展开差异操作',
127
- 'aria-label': detailsOpen ? '收起差异操作' : '展开差异操作',
128
- 'aria-expanded': detailsOpen,
129
- 'aria-controls': detailsId,
130
- onClick: () => setDetailsOpen((value) => !value),
131
- }, chevronOf(detailsOpen))))),
132
- bodyEl,
133
- details)
36
+ return React.createElement(SidecarDiffBar, props)
134
37
  }
@@ -127,7 +127,11 @@ import { buildBpMenu } from '../dap/bpMenu.js'
127
127
  import { clampToolbarPosition, loadToolbarPosition, saveToolbarPosition } from '../dap/toolbarDrag.js'
128
128
  import { editorModelPathOf } from '../dap/modelPath.js'
129
129
  import { dapTrace } from '../dap/trace.js'
130
- import { isSessionMissingError, sessionMissingDetail, sessionMissingTitle } from '../sessionError.js'
130
+ import { isSessionMissingError, sessionErrorText, sessionMissingDetail, sessionMissingTitle } from '../sessionError.js'
131
+ import { officialDownView, SESSION_DOWN_NOTICE } from '../reviewBar.js'
132
+
133
+ /** 官方差异读取失败前缀:仅用于清理同源陈旧错误,不吞并保存类错误。@author ddj 2026年10月27号 */
134
+ const REVIEW_LOAD_ERROR = '官方差异读取失败:'
131
135
 
132
136
  /**
133
137
  * 只在 Lua 注解类型槽开启注释自动建议;跨文件/离开类型槽立即恢复默认。
@@ -588,7 +592,9 @@ export function EditorView(props) {
588
592
  if (!result.ok) throw new Error(result.error)
589
593
  setOfficialRecords((previous) => JSON.stringify(previous) === JSON.stringify(page.page.items) ? previous : page.page.items)
590
594
  setOfficialDetail((previous) => JSON.stringify(previous) === JSON.stringify(result.detail) ? previous : result.detail)
591
- } catch (cause) { if (!controller.signal.aborted) setError('官方差异读取失败:' + String(cause)) }
595
+ // 读取成功后清除同源陈旧错误,避免会话恢复后仍挂着上一次失败提示。
596
+ setError((previous) => (typeof previous === 'string' && previous.startsWith(REVIEW_LOAD_ERROR) ? null : previous))
597
+ } catch (cause) { if (!controller.signal.aborted) setError(REVIEW_LOAD_ERROR + String(cause)) }
592
598
  }
593
599
  void loadReview()
594
600
  return () => controller.abort()
@@ -4482,12 +4488,22 @@ export function EditorView(props) {
4482
4488
  * @returns 含布局及操作回调的差异快照
4483
4489
  */
4484
4490
  const officialFile = review.page?.overview?.coveredPaths.find((path) => sameFile(path, active))
4491
+ // 官方通道自身的失败(list 失败 / 缺 overview)只影响官方栏,不能混进保存类错误。
4492
+ const officialDown = officialFile ? null : officialDownView(review.error)
4493
+ const rawReviewError = review.error || error
4494
+ // 会话未在当前端激活属于环境提示,不是需要用户处置的错误:降级为中性说明,避免整栏红字。
4495
+ const sessionDown = isSessionMissingError(rawReviewError)
4496
+ const reviewErrorTone = sessionDown ? 'info' : 'error'
4497
+ const reviewErrorText = !rawReviewError ? undefined : sessionDown ? SESSION_DOWN_NOTICE : sessionErrorText(rawReviewError)
4485
4498
  const buildDockSnapshot = () => ({
4486
4499
  mode: editorDockMode(active),
4487
4500
  official: officialFile ? { sessionId, page: review.page, path: officialFile,
4488
4501
  pending: sum.files.find((file) => sameFile(file.path, active))?.pending ?? 0,
4489
4502
  detail: officialDetail, records: officialRecords, onSelect: setSelectedReview } : null,
4490
- reviewError: review.error || error,
4503
+ officialDown: officialDown ?? undefined,
4504
+ onRetryOfficial: () => refreshReview(sessionId),
4505
+ reviewError: reviewErrorText,
4506
+ reviewErrorTone,
4491
4507
  layout,
4492
4508
  pendingRegions,
4493
4509
  staleRegions,
@@ -4507,6 +4523,7 @@ export function EditorView(props) {
4507
4523
  onRefresh: reloadFile,
4508
4524
  activePath: active,
4509
4525
  diffIdx: contentReady ? diffIdx : 0,
4526
+ contentReady,
4510
4527
  diffTotal: displayDiffTotal(contentReady, pendingRegions.length, sum.files.find((file) => sameFile(file.path, active))?.pending ?? 0),
4511
4528
  fileIdx,
4512
4529
  fileTotal: sum.pendingFiles.length,
@@ -2,6 +2,8 @@ import React from 'react'
2
2
  import { Button, Menu } from '@deepseek-ai/dsh-client-ui-primitives'
3
3
  import type { ReviewDetail, ReviewItem, ReviewPage } from '../../shared/review.js'
4
4
  import { ReviewLedger } from './OfficialReview.js'
5
+ import { reviewBarView } from '../reviewBar.js'
6
+ import { ReviewAllRow } from './reviewAllRow.js'
5
7
 
6
8
  export interface OfficialBarProps {
7
9
  official: { sessionId: string; path: string; pending: number; detail?: ReviewDetail; records?: ReviewItem[]; page: ReviewPage; onSelect: (id: string) => void }
@@ -9,6 +11,8 @@ export interface OfficialBarProps {
9
11
  pendingRegions?: unknown[]
10
12
  staleRegions?: unknown[]
11
13
  diffIdx?: number
14
+ /** 编辑器内容是否就绪;未就绪时不得断言差异失效 */
15
+ contentReady?: boolean
12
16
  fileTotal?: number
13
17
  fileIdx?: number
14
18
  allPendingCount?: number
@@ -24,6 +28,7 @@ export interface OfficialBarProps {
24
28
  onUndoFile: () => void
25
29
  onOpenLauncher: (tab: string) => void
26
30
  reviewError?: string
31
+ reviewErrorTone?: 'info' | 'error'
27
32
  }
28
33
 
29
34
  type Mark = 'left' | 'right' | 'up' | 'down' | 'check' | 'undo' | 'more' | 'file'
@@ -33,25 +38,17 @@ const marks: Record<Mark, string> = {
33
38
  more: 'M3 8h.01M8 8h.01M13 8h.01', file: 'M9 2H4v12h8V5L9 2Zm0 0v4h3M6 9h4M6 11h3',
34
39
  }
35
40
 
36
- /** @private @author ddj 2026年10月27号 @param mark Toolbar action. @returns Decorative consistent-stroke icon; labels live on buttons. */
37
- function barIcon(mark: Mark): React.ReactElement {
41
+ /**
42
+ * 差异栏图标:官方栏与侧车栏共用同一套 16×16 线性图标,避免两套栏外观不一致。
43
+ * @public
44
+ * @author ddj 2026年10月27号
45
+ * @param mark 工具栏动作
46
+ * @returns 装饰性一致描边图标(标签文字在按钮上)
47
+ */
48
+ export function barIcon(mark: Mark): React.ReactElement {
38
49
  return <svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth={1.5} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true"><path d={marks[mark]} /></svg>
39
50
  }
40
51
 
41
- /** @private @author ddj 2026年10月27号 @param props Global workspace counts and bulk handlers. @returns Persistent global review controls, distinct from current-file actions. */
42
- function ReviewAllRow(props: OfficialBarProps): React.ReactElement {
43
- const total = props.fileTotal ?? 0
44
- const current = total && (props.fileIdx ?? -1) >= 0 ? (props.fileIdx ?? 0) + 1 : 0
45
- const disabled = props.reviewBatchBusy || !!props.batchUnavailable || !props.allPendingCount
46
- return <div className="edrv-review-global" role="group" aria-label="全部待审文件">
47
- <Button size="sm" variant="ghost" className="edrv-review-total" onClick={() => props.onOpenLauncher('pending')} title="打开当前工作区全部待审差异文件">差异文件 <strong>{current} / {total}</strong></Button>
48
- <span className="edrv-review-all-actions">
49
- <Button size="sm" variant="ghost" onClick={props.onAcceptAllFiles} disabled={disabled || !props.onAcceptAllFiles} title={props.batchUnavailable || 'Keep 当前工作区全部待审文件(含其他轮次),不改写磁盘'}>Keep All</Button>
50
- <Button size="sm" variant="ghost" className="edrv-review-undo" onClick={props.onUndoAllFiles} disabled={disabled || !props.onUndoAllFiles} title={props.batchUnavailable || '检查全部待审记录后逐项 Undo;缺少证据或遇到冲突停止,不强制覆盖'}>Undo All</Button>
51
- </span>
52
- </div>
53
- }
54
-
55
52
  /** @public @author ddj 2026年10月27号 @param props Existing review callbacks and immutable record metadata. @returns Responsive review toolbar; no disk or decision side effects until an explicit action. */
56
53
  export function OfficialDiffBar(props: OfficialBarProps): React.ReactElement {
57
54
  const { official, pendingRegions = [], staleRegions = [], diffIdx = 0 } = props
@@ -61,7 +58,8 @@ export function OfficialDiffBar(props: OfficialBarProps): React.ReactElement {
61
58
  const detail = official.detail
62
59
  const records = official.records ?? []
63
60
  const count = pendingRegions.length
64
- const position = count ? `${Math.min(diffIdx + 1, count)} / ${count}` : '—'
61
+ const view = reviewBarView({ jumps: count, stale: staleRegions.length, index: diffIdx, ready: props.contentReady === true,
62
+ pendingRecords: official.pending, canKeep: detail?.capability.keep === true })
65
63
 
66
64
  /** @private @author ddj 2026年10月27号 @returns Close current popup without changing selection. */
67
65
  function closeMenu(): void { setMenu(null) }
@@ -69,9 +67,9 @@ export function OfficialDiffBar(props: OfficialBarProps): React.ReactElement {
69
67
  function selectRecord(id: string): void { official.onSelect(id); closeMenu() }
70
68
  /** @private @author ddj 2026年10月27号 @param tab Existing launcher tab. @returns Open existing overview, never create another review entry. */
71
69
  function openOverview(tab: string): void { props.onOpenLauncher(tab); closeMenu() }
72
- /** @private @author ddj 2026年10月27号 @param mark Navigation icon. @param label Accessible action. @param action Existing callback. @param disabled Whether navigation is unavailable. @returns Compact keyboard-focusable navigation control. */
73
- function navButton(mark: Mark, label: string, action: () => void, disabled: boolean): React.ReactElement {
74
- return <Button size="sm" variant="ghost" className="edrv-review-icon" aria-label={label} title={label} disabled={disabled} onClick={action}>{barIcon(mark)}</Button>
70
+ /** @private @author ddj 2026年10月27号 @param mark Navigation icon. @param label Accessible action. @param action Existing callback. @param disabled Whether navigation is unavailable. @param title Hover reason, defaults to the label. @returns Compact keyboard-focusable navigation control. */
71
+ function navButton(mark: Mark, label: string, action: () => void, disabled: boolean, title?: string): React.ReactElement {
72
+ return <Button size="sm" variant="ghost" className="edrv-review-icon" aria-label={label} title={title || label} disabled={disabled} onClick={action}>{barIcon(mark)}</Button>
75
73
  }
76
74
 
77
75
  const recordTrigger = <Button size="sm" variant="ghost" className="edrv-review-record" aria-label="选择官方差异记录" aria-haspopup="menu" aria-expanded={menu === 'records'} disabled={!records.length} onClick={() => setMenu(menu === 'records' ? null : 'records')}>
@@ -80,12 +78,23 @@ export function OfficialDiffBar(props: OfficialBarProps): React.ReactElement {
80
78
  const moreTrigger = <Button size="sm" variant="ghost" className="edrv-review-icon" aria-label="更多审查操作" title="更多审查操作" aria-haspopup="menu" aria-expanded={menu === 'more'} onClick={() => setMenu(menu === 'more' ? null : 'more')}>{barIcon('more')}</Button>
81
79
 
82
80
  return <div className="edrv-diffbar edrv-diffbar-dock edrv-reviewbar" data-edrv-diffbox="1" data-edrv-diff-source="official" role="region" aria-label="文件差异审查">
83
- <ReviewAllRow {...props} />
81
+ <ReviewAllRow
82
+ total={props.fileTotal ?? 0}
83
+ current={(props.fileTotal ?? 0) && (props.fileIdx ?? -1) >= 0 ? (props.fileIdx ?? 0) + 1 : 0}
84
+ busy={props.reviewBatchBusy}
85
+ unavailable={props.batchUnavailable}
86
+ pending={props.allPendingCount}
87
+ onOpen={() => props.onOpenLauncher('pending')}
88
+ onKeepAll={props.onAcceptAllFiles}
89
+ onUndoAll={props.onUndoAllFiles}
90
+ keepTitle="Keep 当前工作区全部待审文件(含其他轮次),不改写磁盘"
91
+ undoTitle="检查全部待审记录后逐项 Undo;缺少证据或遇到冲突停止,不强制覆盖"
92
+ />
84
93
  {props.reviewBatchBusy ? <div className="edrv-review-notice" role="status">正在处理全部待审记录…</div> : null}
85
94
  <div className="edrv-review-heading">
86
95
  <span className="edrv-review-filemark">{barIcon('file')}</span>
87
96
  <span className="edrv-review-filename" title={props.activePath}>{name}</span>
88
- <span className="edrv-review-pending" title={`${official.pending} 条官方待审记录`}>待审 <strong>{official.pending}</strong></span>
97
+ <span className="edrv-review-pending" title={view.pendingTitle}>待审 <strong>{official.pending}</strong></span>
89
98
  <Menu open={menu === 'records'} anchor={recordTrigger} items={records.map((item) => ({ id: item.id, label: `轮次 ${item.turn} / 事件 ${item.seq}` }))} selectedId={detail?.item.id} onSelect={selectRecord} onClose={closeMenu} side="top" align="end" portal compact autoFocus />
90
99
  </div>
91
100
  <div className="edrv-review-tools">
@@ -94,19 +103,19 @@ export function OfficialDiffBar(props: OfficialBarProps): React.ReactElement {
94
103
  {navButton('right', '下一个差异文件', props.onNextFile, !props.fileTotal)}
95
104
  </div>
96
105
  <div className="edrv-review-nav edrv-review-blocknav" role="group" aria-label="差异导航">
97
- {navButton('up', '上一个差异', props.onPrevDiff, !count)}
98
- <span className="edrv-review-position" aria-live="polite">{position}</span>
99
- {navButton('down', '下一个差异', props.onNextDiff, !count)}
106
+ {navButton('up', '上一个差异', props.onPrevDiff, !count, view.navPrevTitle)}
107
+ <span className="edrv-review-position" aria-live="polite">{view.position}</span>
108
+ {navButton('down', '下一个差异', props.onNextDiff, !count, view.navNextTitle)}
100
109
  </div>
101
110
  <div className="edrv-review-actions">
102
111
  <Button size="sm" variant="ghost" className="edrv-review-undo" icon={barIcon('undo')} onClick={props.onUndoFile} disabled={props.reviewBatchBusy || !detail?.capability.undo} title={detail?.capability.reason}>Undo</Button>
103
- <Button size="sm" variant="primary" className="edrv-review-keep" icon={barIcon('check')} onClick={props.onAcceptFile} disabled={props.reviewBatchBusy || !detail?.capability.keep} title="保留当前记录的未处理修改">Keep</Button>
112
+ <Button size="sm" variant="primary" className="edrv-review-keep" icon={barIcon('check')} onClick={props.onAcceptFile} disabled={props.reviewBatchBusy || !detail?.capability.keep} title={detail?.capability.keep ? view.keepHint : detail?.capability.reason}>Keep</Button>
104
113
  <Button size="sm" variant="ghost" className="edrv-review-details" onClick={() => setExpanded(!expanded)} aria-expanded={expanded} aria-label={expanded ? '收起差异' : '展开差异'} title={expanded ? '收起差异' : '展开差异'}>详情{barIcon(expanded ? 'up' : 'down')}</Button>
105
114
  <Menu open={menu === 'more'} anchor={moreTrigger} items={[{ id: 'pending', label: '差异总览' }, { id: 'archive', label: '归档 / 历史' }]} onSelect={openOverview} onClose={closeMenu} side="top" align="end" portal compact autoFocus />
106
115
  </div>
107
116
  </div>
108
- {detail && staleRegions.length > 0 ? <div className="edrv-review-notice" role="status">{staleRegions.length} 处历史差异无法定位,展开详情查看原始增删。</div> : null}
109
- {props.reviewError ? <div className="edrv-review-notice edrv-review-error" role="alert">{props.reviewError}</div> : null}
117
+ {detail && view.notice ? <div className={view.noticeTone === 'warn' ? 'edrv-review-notice edrv-review-notice-warn' : 'edrv-review-notice'} role="status">{view.notice}</div> : null}
118
+ {props.reviewError ? <div className={props.reviewErrorTone === 'info' ? 'edrv-review-notice' : 'edrv-review-notice edrv-review-error'} role={props.reviewErrorTone === 'info' ? 'status' : 'alert'}>{props.reviewError}</div> : null}
110
119
  {expanded ? <div className="edrv-review-body"><ReviewLedger key={`${official.sessionId}:${official.path}`} sessionId={official.sessionId} first={official.page} path={official.path} /></div> : null}
111
120
  </div>
112
121
  }
@@ -320,7 +320,7 @@ export function PerfSettings() {
320
320
  ) : null
321
321
 
322
322
  const guideBody = React.createElement('section', { className: 'vsm-panel' },
323
- React.createElement('h3', { className: 'vsm-panel-title' }, '侧车摘要(agent/脚本取关键字段,不整份读 sidecar)'),
323
+ React.createElement('h3', { className: 'vsm-panel-title' }, '历史改动摘要(agent / 脚本只取关键字段,不整份读)'),
324
324
  React.createElement('div', { className: 'vsm-panel-body' },
325
325
  React.createElement('p', null, '工作区根目录的 .dsh-edit-review.json(可能数 MB)只应通过摘要接口读取,禁止 agent 整份 read/write 进对话:'),
326
326
  React.createElement('pre', { className: 'vsm-perf-block' },