dsh-vscode-mode 0.4.2 → 0.4.3

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.4.2",
3
+ "version": "0.4.3",
4
4
  "description": "DSH 上的类 VSCode 编码体验:Monaco 编辑器(文件页签/QuickOpen/状态栏,可驻 DSH 0.1.5+ 官方右侧 Sidebar 与对话同屏)+ 命令栏(Ctrl+Shift+P)与可扩展指令系统 + VS Code 兼容代码片段(.code-snippets 全局/项目,IntelliSense 展开)+ Agent 编辑差异审查(整文件差异/采纳/拒绝/归档/回滚)+ 语言服务器(LSP)智能(跳转定义/引用/hover/大纲 + VSIX 扩展市场安装),状态持久化到工作区旁车",
5
5
  "keywords": [
6
6
  "dsh",
@@ -38,6 +38,18 @@ export function editorDockMode(activePath: string | null | undefined): 'editor'
38
38
  return activePath ? 'editor' : 'editor-empty'
39
39
  }
40
40
 
41
+ /**
42
+ * 文件是否仍可执行 Keep/Undo:可定位差异与无法定位的冲突差异都算待处理。
43
+ * 冲突差异的 newText 已不在文件中(被后续修改覆盖),由 host 按"已不存在"记录决策。
44
+ * @author ddj 2026年09月15号
45
+ * @param pendingCount 可定位的待处理差异数
46
+ * @param staleCount 无法定位(冲突)的待处理差异数
47
+ * @returns 两者任一大于 0 时为 true
48
+ */
49
+ export function canDecideFile(pendingCount: number, staleCount: number): boolean {
50
+ return Math.max(0, pendingCount) + Math.max(0, staleCount) > 0
51
+ }
52
+
41
53
  /**
42
54
  * 获取切换文件期间稳定展示的文件内差异数量。
43
55
  * @author ddj 2026年08月26号
@@ -6,7 +6,7 @@
6
6
  */
7
7
  import React from 'react'
8
8
  import { callIdAttr } from '../state/records.js'
9
- import { diffDockText } from '../diffDock.js'
9
+ import { canDecideFile, diffDockText } from '../diffDock.js'
10
10
  import { badgeOf } from './shared.js'
11
11
 
12
12
  /**
@@ -48,6 +48,8 @@ export function DiffBox(props) {
48
48
  const [detailsOpen, setDetailsOpen] = React.useState(false)
49
49
  const detailsId = React.useId()
50
50
  const canAct = pendingRegions.length > 0
51
+ // Keep/Undo 覆盖无法定位的冲突差异:它们仍占待处理列表,但不在 pendingRegions 内
52
+ const canDecide = canDecideFile(pendingRegions.length, staleRegions.length)
51
53
 
52
54
  const base = String(activePath || '').split(/[\\/]/).pop() || ''
53
55
 
@@ -114,8 +116,8 @@ export function DiffBox(props) {
114
116
  React.createElement('span', { className: 'edrv-diffbar-file edrv-diffbar-summary', title: activePath || '' }, base)),
115
117
 
116
118
  React.createElement('div', { className: 'edrv-diffbar-actions' },
117
- React.createElement('button', { className: 'edrv-pill edrv-pill-keep', title: '采纳当前文件的全部差异', disabled: !canAct, onClick: onAcceptFile }, '✓ Keep'),
118
- React.createElement('button', { className: 'edrv-pill edrv-pill-undo', title: '不采纳当前文件的全部差异(回滚)', disabled: !canAct, onClick: onUndoFile }, '↩ Undo'),
119
+ React.createElement('button', { className: 'edrv-pill edrv-pill-keep', title: '采纳当前文件的全部差异', disabled: !canDecide, onClick: onAcceptFile }, '✓ Keep'),
120
+ React.createElement('button', { className: 'edrv-pill edrv-pill-undo', title: '不采纳当前文件的全部差异(回滚)', disabled: !canDecide, onClick: onUndoFile }, '↩ Undo'),
119
121
  React.createElement('button', {
120
122
  className: 'edrv-pill edrv-pill-ghost edrv-diff-details-toggle',
121
123
  title: detailsOpen ? '收起差异操作' : '展开差异操作',
@@ -226,6 +226,8 @@ export function EditorView(props) {
226
226
  // useMemo 稳定引用:否则 hover 等重渲染会让 view zone effect 反复重建(- 号闪烁)
227
227
  const pendingRegions = React.useMemo(() => regions.filter((r) => r.status === ST.PENDING && !r.stale), [regions])
228
228
  const staleRegions = React.useMemo(() => regions.filter((r) => r.status === ST.PENDING && r.stale), [regions])
229
+ // 单文件 Keep/Undo 的作用域:可定位差异 + 无法定位的冲突差异(后者否则永久留在待处理列表)
230
+ const decideRegions = React.useMemo(() => [...pendingRegions, ...staleRegions], [pendingRegions, staleRegions])
229
231
  hoverRegionsRef.current = pendingRegions
230
232
  // 行 → 差异区域 映射(hover O(1) 命中;每行归属其区域)
231
233
  const lineRegionMap = React.useMemo(() => {
@@ -1591,21 +1593,23 @@ export function EditorView(props) {
1591
1593
  * 单次 setRecords 合并全部结果,避免逐条往返读写整个 sidecar。
1592
1594
  * @author ddj 2026年08月25号
1593
1595
  * @param items 决策项数组(callId/scope/hunkIndex/decision)
1594
- * @returns Promise<{ok:number; fail:number}> 成功/失败计数
1596
+ * @returns Promise<{ok:number; fail:number; stale:number}> 成功/失败/已不存在计数
1595
1597
  */
1596
1598
  const actMany = (items) => {
1597
- if (!items.length) return Promise.resolve({ ok: 0, fail: 0 })
1599
+ if (!items.length) return Promise.resolve({ ok: 0, fail: 0, stale: 0 })
1598
1600
  return rpc('edrv.decideBatch', { sessionId, items }).then((res) => {
1599
1601
  if (!res || !res.ok || !Array.isArray(res.results)) {
1600
1602
  setError(res?.error ? String(res.error) : '批量处理失败')
1601
- return { ok: 0, fail: items.length }
1603
+ return { ok: 0, fail: items.length, stale: 0 }
1602
1604
  }
1603
1605
  let ok = 0
1604
1606
  let fail = 0
1607
+ let stale = 0
1605
1608
  const next = {}
1606
1609
  for (const item of res.results) {
1607
1610
  if (item && item.ok) {
1608
1611
  ok++
1612
+ if (item.stale === true) stale++
1609
1613
  if (item.record) next[item.callId] = item.record
1610
1614
  } else {
1611
1615
  fail++
@@ -1614,13 +1618,25 @@ export function EditorView(props) {
1614
1618
  // 合并结果无变化(批量全部失败/重复点击)时不 setRecords:避免 records 换引用
1615
1619
  // → pendingRegions 换引用 → diff 全量重渲染(内容相同,纯空转)
1616
1620
  if (Object.keys(next).length) setRecords((prev) => Object.assign({}, prev, next))
1617
- return { ok, fail }
1621
+ return { ok, fail, stale }
1618
1622
  }).catch((e) => {
1619
1623
  setError('批量处理异常:' + String(e))
1620
- return { ok: 0, fail: items.length }
1624
+ return { ok: 0, fail: items.length, stale: 0 }
1621
1625
  })
1622
1626
  }
1623
1627
 
1628
+ /**
1629
+ * 批量决策状态文案(含"差异已不存在于文件"提示)。
1630
+ * @author ddj 2026年09月15号
1631
+ * @param prefix 动作前缀(已采纳/已不采纳)
1632
+ * @param result 批量决策计数
1633
+ * @returns 状态栏文案
1634
+ */
1635
+ const decideStatus = (prefix, result) => {
1636
+ const text = prefix + result.ok + ' 处差异' + (result.fail ? ',' + result.fail + ' 处失败' : '')
1637
+ return result.stale ? text + '(其中 ' + result.stale + ' 处已不存在于文件,未改动)' : text
1638
+ }
1639
+
1624
1640
  /**
1625
1641
  * 决策项构造(与单条 actHunk 的 scope 语义一致:create 记录走 call 作用域)。
1626
1642
  * @author ddj 2026年08月25号
@@ -1631,25 +1647,25 @@ export function EditorView(props) {
1631
1647
  const itemOf = (r, reject) => ({ callId: r.callId, scope: r.create ? 'call' : 'hunk', hunkIndex: r.idx, decision: reject ? 'rejected' : 'accepted' })
1632
1648
 
1633
1649
  const acceptFile = () => {
1634
- if (batchBusyRef.current || !pendingRegions.length) return
1650
+ if (batchBusyRef.current || !decideRegions.length) return
1635
1651
  batchBusyRef.current = true
1636
- actMany(pendingRegions.map((r) => itemOf(r, false))).then(({ ok, fail }) => {
1652
+ actMany(decideRegions.map((r) => itemOf(r, false))).then((result) => {
1637
1653
  batchBusyRef.current = false
1638
1654
  reloadFile(true)
1639
1655
  emitRefresh()
1640
- setStatus('已采纳 ' + ok + ' 处差异' + (fail ? ',' + fail + ' 处失败' : ''))
1641
- if (fail) setError(fail + ' 处差异处理失败(可能已被后续修改影响),可刷新后重试')
1656
+ setStatus(decideStatus('已采纳 ', result))
1657
+ if (result.fail) setError(result.fail + ' 处差异处理失败(可能已被后续修改影响),可刷新后重试')
1642
1658
  }).catch(() => { batchBusyRef.current = false })
1643
1659
  }
1644
1660
  const undoFile = () => {
1645
- if (batchBusyRef.current || !pendingRegions.length) return
1661
+ if (batchBusyRef.current || !decideRegions.length) return
1646
1662
  batchBusyRef.current = true
1647
- actMany([...pendingRegions].reverse().map((r) => itemOf(r, true))).then(({ ok, fail }) => {
1663
+ actMany([...decideRegions].reverse().map((r) => itemOf(r, true))).then((result) => {
1648
1664
  batchBusyRef.current = false
1649
1665
  reloadFile(true)
1650
1666
  emitRefresh()
1651
- setStatus('已不采纳 ' + ok + ' 处差异' + (fail ? ',' + fail + ' 处失败' : ''))
1652
- if (fail) setError(fail + ' 处差异处理失败(可能已被后续修改影响),可刷新后重试')
1667
+ setStatus(decideStatus('已不采纳 ', result))
1668
+ if (result.fail) setError(result.fail + ' 处差异处理失败(可能已被后续修改影响),可刷新后重试')
1653
1669
  }).catch(() => { batchBusyRef.current = false })
1654
1670
  }
1655
1671
 
@@ -1677,12 +1693,12 @@ export function EditorView(props) {
1677
1693
  if (batchBusyRef.current || !allPending.length) return
1678
1694
  batchBusyRef.current = true
1679
1695
  const list = reject ? [...allPending].sort((a, b) => (a.at < b.at ? 1 : a.at > b.at ? -1 : b.idx - a.idx)) : allPending
1680
- actMany(list.map((r) => itemOf(r, reject))).then(({ ok, fail }) => {
1696
+ actMany(list.map((r) => itemOf(r, reject))).then((result) => {
1681
1697
  batchBusyRef.current = false
1682
1698
  reloadFile(true)
1683
1699
  emitRefresh()
1684
- setStatus((reject ? '已不采纳 ' : '已采纳 ') + ok + ' 处差异' + (fail ? ',' + fail + ' 处失败' : ''))
1685
- if (fail) setError(fail + ' 处差异处理失败(可能已被后续修改影响),可刷新后重试')
1700
+ setStatus(decideStatus(reject ? '已不采纳 ' : '已采纳 ', result))
1701
+ if (result.fail) setError(result.fail + ' 处差异处理失败(可能已被后续修改影响),可刷新后重试')
1686
1702
  }).catch(() => { batchBusyRef.current = false })
1687
1703
  }
1688
1704
  const acceptAllFiles = () => actAllPending(false)
package/src/revert.ts CHANGED
@@ -8,7 +8,8 @@ import type { Ctx, Session } from './store.js'
8
8
  import { policyOf, resolveTarget } from './store.js'
9
9
  import { applyLocations, locateHunks, preciseHunk } from './shared/diff.js'
10
10
 
11
- export type Result = { ok: true } | { ok: false; error: string }
11
+ /** 回滚结果;stale=true 表示该 hunk 的新文本已不在文件中(无可回滚内容,不算失败)。 */
12
+ export type Result = { ok: true } | { ok: false; error: string; stale?: boolean }
12
13
 
13
14
  /**
14
15
  * 删除新建文件(拒绝创建时):subprocess 删除,路径先经 fs.contains 校验工作区边界。
@@ -81,7 +82,8 @@ export async function revertHunk(ctx: Ctx, session: Session, record: DiffRecord,
81
82
  if ((info.size ?? 0) > 8 * 1024 * 1024) return { ok: false, error: '回滚失败:文件过大,无法安全定位' }
82
83
  const content = await fs.readText(target)
83
84
  const location = locateHunks(content, [precise])[0]
84
- if (!location?.matched) return { ok: false, error: '回滚失败:该区域可能已被后续修改影响' }
85
+ // 新文本已不在文件中(被后续修改覆盖/撤销):不采纳的目标已达成,标记 stale 供调用方按"已回滚"记录决策
86
+ if (!location?.matched) return { ok: false, stale: true, error: '回滚失败:该区域在文件中已不存在(可能已被后续修改覆盖)' }
85
87
  const result = applyLocations(content, [location], true)
86
88
  await fs.writeText(target, result.content, void 0, void 0, policyOf(ctx, session))
87
89
  return { ok: true }
package/src/rpc.ts CHANGED
@@ -137,8 +137,9 @@ async function afterManualSave(
137
137
 
138
138
  /**
139
139
  * 批量决策核心:一次会话/桶解析 + 逐项处理 + 统一落盘(accept/reject 单条与 decideBatch 共用)。
140
- * rejected 项先做回滚,失败记入该项 error 且不改决策;本批次新增"已解决"记录一次性归档,
141
- * 有任何成功项则整桶只写盘一次,避免逐条读写整个 sidecar。
140
+ * rejected 项先做回滚,失败记入该项 error 且不改决策;hunk 的新文本已不在文件中
141
+ * (被后续修改覆盖/撤销)时不算失败:不写文件,按已回滚记录决策并归档(结果项带 stale)。
142
+ * 本批次新增"已解决"记录一次性归档,有任何成功项则整桶只写盘一次,避免逐条读写整个 sidecar。
142
143
  * @author ddj 2026年08月25号
143
144
  * @param items 决策项(按数组顺序处理,rejected 的先后即回滚顺序)
144
145
  * @returns 逐项结果(ok 项含更新后的记录视图)
@@ -159,19 +160,25 @@ async function applyDecisions(
159
160
  results.push({ callId: item.callId, ok: false, error: '记录不存在' })
160
161
  continue
161
162
  }
163
+ let revertedStale = false
162
164
  if (item.decision === 'rejected') {
163
165
  const scope: RpcScope = item.scope ?? 'call'
164
166
  const outcome = scope === 'hunk' ? await revertHunk(ctx, session, record, item.hunkIndex ?? -1) : await revertCall(ctx, session, record)
165
167
  if (!outcome.ok) {
166
- results.push({ callId: item.callId, ok: false, error: outcome.error })
167
- continue
168
+ if (scope !== 'hunk' || outcome.stale !== true) {
169
+ results.push({ callId: item.callId, ok: false, error: outcome.error })
170
+ continue
171
+ }
172
+ revertedStale = true
168
173
  }
169
174
  }
170
175
  const wasResolved = recordResolved(record)
171
176
  markDecision(record, item.scope ?? 'call', item.hunkIndex, item.decision)
172
177
  changed = true
173
178
  if (!wasResolved && recordResolved(record)) resolved.push(record)
174
- results.push({ callId: item.callId, ok: true, record: recView(record) })
179
+ const result: DecideResult = { callId: item.callId, ok: true, record: recView(record) }
180
+ if (revertedStale) result.stale = true
181
+ results.push(result)
175
182
  }
176
183
  if (resolved.length) {
177
184
  const reason = items.some((item) => item.decision === 'rejected') ? '已处理(回滚)' : '已处理'
package/src/shared/rpc.ts CHANGED
@@ -151,6 +151,8 @@ export interface DecideResult {
151
151
  ok: boolean
152
152
  error?: string
153
153
  record?: RecordView
154
+ /** 不采纳时该 hunk 的新文本已不在文件中(未改动文件,仅记录决策)。 */
155
+ stale?: boolean
154
156
  }
155
157
 
156
158
  /** 每个方法的请求参数(sessionId 为公共可选字段)。 */