dsh-vscode-mode 0.20.3 → 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.3",
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": [
@@ -2,8 +2,11 @@
2
2
  * 官方差异栏派生视图:位置、导航提示、失效说明与 Keep 提示的唯一来源。
3
3
  * 计数口径分离:待审是「记录数」,可跳转是「块数」,两者不同源,
4
4
  * 全部块失效时必须给出可操作叙事,不能只留一句「无法定位」。
5
+ * 另含侧车(legacy)栏的标识文案与官方链路不可用说明:两套栏外观相近,
6
+ * 必须显式区分,否则用户会误判为插件未升级。
5
7
  * 作者 ddj 2026年10月27号
6
8
  */
9
+ import { sessionErrorText } from './sessionError.js'
7
10
 
8
11
  /** 官方差异栏输入:全部来自 EditorView dock 快照,不读取磁盘或 RPC。 */
9
12
  export interface ReviewBarInput {
@@ -83,3 +86,41 @@ export function reviewBarView(input: ReviewBarInput): ReviewBarView {
83
86
 
84
87
  return { position: '—', navPrevTitle: '当前文件没有可定位差异', navNextTitle: '当前文件没有可定位差异', notice: '', noticeTone: 'info', keepHint, pendingTitle }
85
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
+
@@ -305,6 +305,9 @@
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
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; }
308
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; }
309
312
  @container edrv-review (max-width: 420px) {
310
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
  }
@@ -128,6 +128,7 @@ import { clampToolbarPosition, loadToolbarPosition, saveToolbarPosition } from '
128
128
  import { editorModelPathOf } from '../dap/modelPath.js'
129
129
  import { dapTrace } from '../dap/trace.js'
130
130
  import { isSessionMissingError, sessionErrorText, sessionMissingDetail, sessionMissingTitle } from '../sessionError.js'
131
+ import { officialDownView, SESSION_DOWN_NOTICE } from '../reviewBar.js'
131
132
 
132
133
  /** 官方差异读取失败前缀:仅用于清理同源陈旧错误,不吞并保存类错误。@author ddj 2026年10月27号 */
133
134
  const REVIEW_LOAD_ERROR = '官方差异读取失败:'
@@ -4487,13 +4488,22 @@ export function EditorView(props) {
4487
4488
  * @returns 含布局及操作回调的差异快照
4488
4489
  */
4489
4490
  const officialFile = review.page?.overview?.coveredPaths.find((path) => sameFile(path, active))
4491
+ // 官方通道自身的失败(list 失败 / 缺 overview)只影响官方栏,不能混进保存类错误。
4492
+ const officialDown = officialFile ? null : officialDownView(review.error)
4490
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)
4491
4498
  const buildDockSnapshot = () => ({
4492
4499
  mode: editorDockMode(active),
4493
4500
  official: officialFile ? { sessionId, page: review.page, path: officialFile,
4494
4501
  pending: sum.files.find((file) => sameFile(file.path, active))?.pending ?? 0,
4495
4502
  detail: officialDetail, records: officialRecords, onSelect: setSelectedReview } : null,
4496
- reviewError: rawReviewError ? sessionErrorText(rawReviewError) : undefined,
4503
+ officialDown: officialDown ?? undefined,
4504
+ onRetryOfficial: () => refreshReview(sessionId),
4505
+ reviewError: reviewErrorText,
4506
+ reviewErrorTone,
4497
4507
  layout,
4498
4508
  pendingRegions,
4499
4509
  staleRegions,
@@ -3,6 +3,7 @@ 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
5
  import { reviewBarView } from '../reviewBar.js'
6
+ import { ReviewAllRow } from './reviewAllRow.js'
6
7
 
7
8
  export interface OfficialBarProps {
8
9
  official: { sessionId: string; path: string; pending: number; detail?: ReviewDetail; records?: ReviewItem[]; page: ReviewPage; onSelect: (id: string) => void }
@@ -27,6 +28,7 @@ export interface OfficialBarProps {
27
28
  onUndoFile: () => void
28
29
  onOpenLauncher: (tab: string) => void
29
30
  reviewError?: string
31
+ reviewErrorTone?: 'info' | 'error'
30
32
  }
31
33
 
32
34
  type Mark = 'left' | 'right' | 'up' | 'down' | 'check' | 'undo' | 'more' | 'file'
@@ -36,25 +38,17 @@ const marks: Record<Mark, string> = {
36
38
  more: 'M3 8h.01M8 8h.01M13 8h.01', file: 'M9 2H4v12h8V5L9 2Zm0 0v4h3M6 9h4M6 11h3',
37
39
  }
38
40
 
39
- /** @private @author ddj 2026年10月27号 @param mark Toolbar action. @returns Decorative consistent-stroke icon; labels live on buttons. */
40
- 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 {
41
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>
42
50
  }
43
51
 
44
- /** @private @author ddj 2026年10月27号 @param props Global workspace counts and bulk handlers. @returns Persistent global review controls, distinct from current-file actions. */
45
- function ReviewAllRow(props: OfficialBarProps): React.ReactElement {
46
- const total = props.fileTotal ?? 0
47
- const current = total && (props.fileIdx ?? -1) >= 0 ? (props.fileIdx ?? 0) + 1 : 0
48
- const disabled = props.reviewBatchBusy || !!props.batchUnavailable || !props.allPendingCount
49
- return <div className="edrv-review-global" role="group" aria-label="全部待审文件">
50
- <Button size="sm" variant="ghost" className="edrv-review-total" onClick={() => props.onOpenLauncher('pending')} title="打开当前工作区全部待审差异文件">差异文件 <strong>{current} / {total}</strong></Button>
51
- <span className="edrv-review-all-actions">
52
- <Button size="sm" variant="ghost" onClick={props.onAcceptAllFiles} disabled={disabled || !props.onAcceptAllFiles} title={props.batchUnavailable || 'Keep 当前工作区全部待审文件(含其他轮次),不改写磁盘'}>Keep All</Button>
53
- <Button size="sm" variant="ghost" className="edrv-review-undo" onClick={props.onUndoAllFiles} disabled={disabled || !props.onUndoAllFiles} title={props.batchUnavailable || '检查全部待审记录后逐项 Undo;缺少证据或遇到冲突停止,不强制覆盖'}>Undo All</Button>
54
- </span>
55
- </div>
56
- }
57
-
58
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. */
59
53
  export function OfficialDiffBar(props: OfficialBarProps): React.ReactElement {
60
54
  const { official, pendingRegions = [], staleRegions = [], diffIdx = 0 } = props
@@ -84,7 +78,18 @@ export function OfficialDiffBar(props: OfficialBarProps): React.ReactElement {
84
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>
85
79
 
86
80
  return <div className="edrv-diffbar edrv-diffbar-dock edrv-reviewbar" data-edrv-diffbox="1" data-edrv-diff-source="official" role="region" aria-label="文件差异审查">
87
- <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
+ />
88
93
  {props.reviewBatchBusy ? <div className="edrv-review-notice" role="status">正在处理全部待审记录…</div> : null}
89
94
  <div className="edrv-review-heading">
90
95
  <span className="edrv-review-filemark">{barIcon('file')}</span>
@@ -110,7 +115,7 @@ export function OfficialDiffBar(props: OfficialBarProps): React.ReactElement {
110
115
  </div>
111
116
  </div>
112
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}
113
- {props.reviewError ? <div className="edrv-review-notice edrv-review-error" role="alert">{props.reviewError}</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}
114
119
  {expanded ? <div className="edrv-review-body"><ReviewLedger key={`${official.sessionId}:${official.path}`} sessionId={official.sessionId} first={official.page} path={official.path} /></div> : null}
115
120
  </div>
116
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' },
@@ -0,0 +1,154 @@
1
+ import React from 'react'
2
+ import { Button, Menu } from '@deepseek-ai/dsh-client-ui-primitives'
3
+ import { callIdAttr } from '../state/records.js'
4
+ import { SIDECAR_KEEP_TITLE, SIDECAR_LABEL, SIDECAR_LABEL_TITLE, SIDECAR_UNDO_TITLE, reviewBarView } from '../reviewBar.js'
5
+ import type { OfficialDownView } from '../reviewBar.js'
6
+ import { barIcon } from './OfficialDiffBar.js'
7
+ import { ReviewAllRow } from './reviewAllRow.js'
8
+ import { badgeOf } from './shared.js'
9
+
10
+ /** 侧车差异区域:由 records 派生的行范围,不含官方块元数据。 */
11
+ export interface SidecarRegion {
12
+ callId: string
13
+ idx: number
14
+ start?: number
15
+ end?: number
16
+ oldLines?: string[]
17
+ newLines?: string[]
18
+ create?: boolean
19
+ status?: string
20
+ }
21
+
22
+ /** 侧车栏输入:字段与官方栏对齐,缺的是官方记录元数据,多的是侧车区域的 keep/undo 能力。 */
23
+ export interface SidecarBarProps {
24
+ activePath?: string
25
+ pendingRegions?: SidecarRegion[]
26
+ staleRegions?: SidecarRegion[]
27
+ diffIdx?: number
28
+ /** 编辑器内容是否就绪;未就绪时不得断言差异失效 */
29
+ contentReady?: boolean
30
+ fileIdx?: number
31
+ fileTotal?: number
32
+ allPendingCount?: number
33
+ reviewBatchBusy?: boolean
34
+ /** 官方元数据通道失败时的可操作提示 */
35
+ officialDown?: OfficialDownView
36
+ onRetryOfficial?: () => void
37
+ onAcceptFile: () => void
38
+ onUndoFile: () => void
39
+ onAcceptAllFiles: () => void
40
+ onUndoAllFiles: () => void
41
+ onPrevFile: () => void
42
+ onNextFile: () => void
43
+ onPrevDiff: () => void
44
+ onNextDiff: () => void
45
+ onOpenLauncher: (tab: string) => void
46
+ onJump?: (region: SidecarRegion) => void
47
+ onRollback?: () => void
48
+ onRefresh?: () => void
49
+ reviewError?: string
50
+ reviewErrorTone?: 'info' | 'error'
51
+ }
52
+
53
+ /**
54
+ * 侧车(历史捕获)差异栏:与官方栏同一套版式、同一套按钮位置,只把数据源与标识换掉。
55
+ * 两套栏并存是数据现实,但外观不能分家,否则用户会以为插件版本不一致。
56
+ * @public
57
+ * @author ddj 2026年10月28号
58
+ * @param props 侧车区域计数、批量可用性与回调
59
+ * @returns 与官方栏同构的差异工具栏;展开后列出侧车区域
60
+ */
61
+ export function SidecarDiffBar(props: SidecarBarProps): React.ReactElement {
62
+ const pendingRegions = props.pendingRegions ?? []
63
+ const staleRegions = props.staleRegions ?? []
64
+ const [expanded, setExpanded] = React.useState(false)
65
+ const [menu, setMenu] = React.useState<'more' | null>(null)
66
+ const name = String(props.activePath || '').split(/[\\/]/).pop() || '当前文件'
67
+ const count = pendingRegions.length
68
+ const total = count + staleRegions.length
69
+ const busy = props.reviewBatchBusy === true
70
+ const canDecide = total > 0 && !busy
71
+ const view = reviewBarView({
72
+ jumps: count, stale: staleRegions.length, index: props.diffIdx ?? 0,
73
+ ready: props.contentReady === true, pendingRecords: total, canKeep: true,
74
+ })
75
+ const down = props.officialDown
76
+
77
+ /** @private @author ddj 2026年10月28号 @param tab 既有总览页签 @returns 打开既有总览并收起菜单 */
78
+ function openOverview(tab: string): void { props.onOpenLauncher(tab); setMenu(null) }
79
+
80
+ /** @private @author ddj 2026年10月28号 @param mark 工具栏图标 @param label 无障碍名称 @param action 既有回调 @param disabled 是否禁用 @param title 禁用原因 @returns 紧凑导航按钮 */
81
+ function navButton(mark: Parameters<typeof barIcon>[0], label: string, action: () => void, disabled: boolean, title?: string): React.ReactElement {
82
+ return <Button size="sm" variant="ghost" className="edrv-review-icon" aria-label={label} title={title || label} disabled={disabled} onClick={action}>{barIcon(mark)}</Button>
83
+ }
84
+
85
+ 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>
86
+
87
+ const body = expanded
88
+ ? <div className="edrv-review-body">
89
+ {pendingRegions.map((region) => {
90
+ const key = callIdAttr(region.callId, region.idx)
91
+ return <div key={key} className="edrv-diffrow" data-edrv-hunk={key} title={'跳转到 L' + (region.start ?? '?')} onClick={() => props.onJump?.(region)}>
92
+ <span className="edrv-diffrow-l">{'L' + (region.start ?? '?') + '-' + (region.end ?? '?')}</span>
93
+ {(region.oldLines?.length ?? 0) > 0 ? <span className="edrv-diffrow-o">{'-' + (region.oldLines?.length ?? 0)}</span> : null}
94
+ {(region.newLines?.length ?? 0) > 0 ? <span className="edrv-diffrow-n">{'+' + (region.newLines?.length ?? 0)}</span> : null}
95
+ {region.create ? <span className="edrv-diffrow-tag">新建</span> : null}
96
+ </div>
97
+ })}
98
+ {staleRegions.map((region) => <div key={'stale' + callIdAttr(region.callId, region.idx)} className="edrv-diffrow edrv-diffrow-stale" title="差异无法定位(文件可能已被手动修改)">
99
+ <span className="edrv-diffrow-l">{'L' + (region.start ?? '?') + ' 无法定位'}</span>
100
+ <span className="edrv-diffrow-tag">冲突/需刷新</span>
101
+ <span className="edrv-diffrow-tag">{badgeOf(region.status ?? '')}</span>
102
+ </div>)}
103
+ </div>
104
+ : null
105
+
106
+ return <div className="edrv-diffbar edrv-diffbar-dock edrv-reviewbar" data-edrv-diffbox="1" data-edrv-diff-source="sidecar" role="region" aria-label="文件差异审查">
107
+ <ReviewAllRow
108
+ total={props.fileTotal ?? 0}
109
+ current={(props.fileTotal ?? 0) && (props.fileIdx ?? -1) >= 0 ? (props.fileIdx ?? 0) + 1 : 0}
110
+ busy={busy}
111
+ pending={props.allPendingCount}
112
+ onOpen={() => props.onOpenLauncher('pending')}
113
+ onKeepAll={props.onAcceptAllFiles}
114
+ onUndoAll={props.onUndoAllFiles}
115
+ keepTitle="把这批历史改动全部标记为已看过(只改记录,不动磁盘文件)"
116
+ undoTitle="逐项撤销这些历史改动;缺少证据或遇到冲突即停止"
117
+ />
118
+ {busy ? <div className="edrv-review-notice" role="status">正在处理历史改动…</div> : null}
119
+ {down ? <div className="edrv-review-notice edrv-review-notice-warn edrv-sidecar-down" role="status">
120
+ <span className="edrv-sidecar-down-text">{down.text}</span>
121
+ {props.onRetryOfficial ? <Button size="sm" variant="ghost" onClick={props.onRetryOfficial} title="重新读取官方待审记录">{down.retryLabel || '重试'}</Button> : null}
122
+ </div> : null}
123
+ <div className="edrv-review-heading">
124
+ <span className="edrv-review-filemark">{barIcon('file')}</span>
125
+ <span className="edrv-review-filename" title={props.activePath}>{name}</span>
126
+ <span className="edrv-review-source" title={SIDECAR_LABEL_TITLE}>{SIDECAR_LABEL}</span>
127
+ <span className="edrv-review-pending" title={view.pendingTitle}>待审 <strong>{total}</strong></span>
128
+ </div>
129
+ <div className="edrv-review-tools">
130
+ <div className="edrv-review-nav" role="group" aria-label="文件导航">
131
+ {navButton('left', '上一个差异文件', props.onPrevFile, !props.fileTotal)}
132
+ {navButton('right', '下一个差异文件', props.onNextFile, !props.fileTotal)}
133
+ </div>
134
+ <div className="edrv-review-nav edrv-review-blocknav" role="group" aria-label="差异导航">
135
+ {navButton('up', '上一个差异', props.onPrevDiff, !count, view.navPrevTitle)}
136
+ <span className="edrv-review-position" aria-live="polite">{view.position}</span>
137
+ {navButton('down', '下一个差异', props.onNextDiff, !count, view.navNextTitle)}
138
+ </div>
139
+ <div className="edrv-review-actions">
140
+ <Button size="sm" variant="ghost" className="edrv-review-undo" icon={barIcon('undo')} onClick={props.onUndoFile} disabled={!canDecide} title={SIDECAR_UNDO_TITLE}>Undo</Button>
141
+ <Button size="sm" variant="primary" className="edrv-review-keep" icon={barIcon('check')} onClick={props.onAcceptFile} disabled={!canDecide} title={SIDECAR_KEEP_TITLE}>Keep</Button>
142
+ <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>
143
+ <Menu open={menu === 'more'} anchor={moreTrigger} items={[{ id: 'pending', label: '差异总览' }, { id: 'archive', label: '归档 / 历史' }, { id: 'rollback', label: '回滚当前文件' }, { id: 'refresh', label: '刷新' }]} onSelect={(id: string) => {
144
+ if (id === 'rollback') { setMenu(null); props.onRollback?.(); return }
145
+ if (id === 'refresh') { setMenu(null); props.onRefresh?.(); return }
146
+ openOverview(id)
147
+ }} onClose={() => setMenu(null)} side="top" align="end" portal compact autoFocus />
148
+ </div>
149
+ </div>
150
+ {view.notice ? <div className={view.noticeTone === 'warn' ? 'edrv-review-notice edrv-review-notice-warn' : 'edrv-review-notice'} role="status">{view.notice}</div> : null}
151
+ {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}
152
+ {body}
153
+ </div>
154
+ }
@@ -0,0 +1,48 @@
1
+ import React from 'react'
2
+ import { Button } from '@deepseek-ai/dsh-client-ui-primitives'
3
+
4
+ /** 全工作区批量行输入:官方栏与侧车栏共用同一套外观与禁用语义。 */
5
+ export interface ReviewAllRowProps {
6
+ /** 当前文件序号(1 基,0 表示未定位) */
7
+ current: number
8
+ /** 差异文件总数 */
9
+ total: number
10
+ /** 批量操作进行中 */
11
+ busy?: boolean
12
+ /** 批量不可用原因(悬浮提示) */
13
+ unavailable?: string
14
+ /** 工作区待处理项总数;为 0 时两个按钮禁用 */
15
+ pending?: number
16
+ /** 打开差异总览 */
17
+ onOpen: () => void
18
+ /** 全部 Keep */
19
+ onKeepAll?: () => void
20
+ /** 全部 Undo */
21
+ onUndoAll?: () => void
22
+ /** Keep All 悬浮说明 */
23
+ keepTitle?: string
24
+ /** Undo All 悬浮说明 */
25
+ undoTitle?: string
26
+ }
27
+
28
+ /**
29
+ * 全局批量行:差异文件计数 + Keep All / Undo All。
30
+ * 两套差异栏(官方 / 侧车)必须长得一样,否则用户会以为看到的是不同产品。
31
+ * @public
32
+ * @author ddj 2026年10月28号
33
+ * @param props 计数、批量可用性与回调
34
+ * @returns 持久化的全工作区控制条
35
+ */
36
+ export function ReviewAllRow(props: ReviewAllRowProps): React.ReactElement {
37
+ const total = props.total ?? 0
38
+ const current = total && props.current > 0 ? props.current : 0
39
+ const disabled = props.busy || !!props.unavailable || !props.pending
40
+
41
+ return <div className="edrv-review-global" role="group" aria-label="全部待审文件">
42
+ <Button size="sm" variant="ghost" className="edrv-review-total" onClick={props.onOpen} title="打开当前工作区全部待审差异文件">差异文件 <strong>{current} / {total}</strong></Button>
43
+ <span className="edrv-review-all-actions">
44
+ <Button size="sm" variant="ghost" onClick={props.onKeepAll} disabled={disabled || !props.onKeepAll} title={props.unavailable || props.keepTitle || 'Keep 当前工作区全部待审文件'}>Keep All</Button>
45
+ <Button size="sm" variant="ghost" className="edrv-review-undo" onClick={props.onUndoAll} disabled={disabled || !props.onUndoAll} title={props.unavailable || props.undoTitle || 'Undo 当前工作区全部待审文件'}>Undo All</Button>
46
+ </span>
47
+ </div>
48
+ }