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/README.md +4 -0
- package/lib/client.js +48 -18
- package/lib/client.js.map +1 -1
- package/lib/index.js +21 -12
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
- package/src/client/diffDock.ts +12 -0
- package/src/client/ui/DiffBox.ts +5 -3
- package/src/client/ui/EditorView.ts +32 -16
- package/src/revert.ts +4 -2
- package/src/rpc.ts +12 -5
- package/src/shared/rpc.ts +2 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-vscode-mode",
|
|
3
|
-
"version": "0.4.
|
|
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",
|
package/src/client/diffDock.ts
CHANGED
|
@@ -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号
|
package/src/client/ui/DiffBox.ts
CHANGED
|
@@ -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: !
|
|
118
|
-
React.createElement('button', { className: 'edrv-pill edrv-pill-undo', title: '不采纳当前文件的全部差异(回滚)', disabled: !
|
|
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 || !
|
|
1650
|
+
if (batchBusyRef.current || !decideRegions.length) return
|
|
1635
1651
|
batchBusyRef.current = true
|
|
1636
|
-
actMany(
|
|
1652
|
+
actMany(decideRegions.map((r) => itemOf(r, false))).then((result) => {
|
|
1637
1653
|
batchBusyRef.current = false
|
|
1638
1654
|
reloadFile(true)
|
|
1639
1655
|
emitRefresh()
|
|
1640
|
-
setStatus('已采纳 '
|
|
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 || !
|
|
1661
|
+
if (batchBusyRef.current || !decideRegions.length) return
|
|
1646
1662
|
batchBusyRef.current = true
|
|
1647
|
-
actMany([...
|
|
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('已不采纳 '
|
|
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((
|
|
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 ? '已不采纳 ' : '已采纳 '
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
167
|
-
|
|
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
|
-
|
|
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') ? '已处理(回滚)' : '已处理'
|