dsh-vscode-mode 0.21.4 → 0.23.0
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 +52 -4
- package/lib/client.js +3442 -183
- package/lib/client.js.map +2 -2
- package/lib/index.js +3510 -222
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
- package/src/client/index.ts +166 -0
- package/src/client/quickMenuHost.ts +46 -0
- package/src/client/quickMenuStore.ts +260 -0
- package/src/client/state/diffSummary.ts +52 -8
- package/src/client/styles/quickmenu.css +1049 -0
- package/src/client/ui/DiffLauncher.ts +1 -1
- package/src/client/ui/EditorView.ts +10 -2
- package/src/client/ui/OfficialDiffBar.tsx +1 -12
- package/src/client/ui/OfficialReview.ts +13 -3
- package/src/client/ui/QuickMenuDialog.tsx +835 -0
- package/src/client/ui/QuickMenuLauncher.tsx +792 -0
- package/src/client/ui/QuickMenuPickers.ts +577 -0
- package/src/client/ui/SidecarDiffBar.tsx +0 -22
- package/src/index.ts +9 -0
- package/src/missingReview.ts +36 -14
- package/src/paths.ts +10 -0
- package/src/pending/capture.ts +165 -0
- package/src/pending/decide.ts +213 -0
- package/src/pending/hunks.ts +241 -0
- package/src/pending/kernel.ts +233 -0
- package/src/pending/persist.ts +329 -0
- package/src/pending/store.ts +472 -0
- package/src/pending/vcs.ts +444 -0
- package/src/pendingCompat.ts +184 -0
- package/src/pendingRpc.ts +210 -0
- package/src/quickMenu.ts +269 -0
- package/src/reviewDb.ts +3 -0
- package/src/reviewRpc.ts +52 -1
- package/src/rpc.ts +66 -2
- package/src/shared/pathKey.ts +54 -0
- package/src/shared/pendingReview.ts +105 -0
- package/src/shared/quickMenu.ts +1009 -0
- package/src/shared/quickPick.ts +67 -0
- package/src/shared/rpc.ts +28 -0
- package/src/shared/skillCatalog.ts +60 -0
- package/src/skillCatalog.ts +138 -0
- package/src/store.ts +19 -0
package/src/rpc.ts
CHANGED
|
@@ -34,7 +34,9 @@ import type { ContentSearcher } from './search/content.js'
|
|
|
34
34
|
import { newContentSearcher } from './search/content.js'
|
|
35
35
|
import { restoreFile, revertCall, revertHunk, revertHunks, type Result } from './revert.js'
|
|
36
36
|
import { rulesList, rulesRead, rulesRemove, rulesSave, rulesToggle } from './rules.js'
|
|
37
|
+
import { listSkillCatalog } from './skillCatalog.js'
|
|
37
38
|
import { isSnippetFilePath, snippetsEntries, snippetsList, snippetsRead, snippetsRemove, snippetsSave } from './snippets.js'
|
|
39
|
+
import { patchLauncher, readQuickMenu, saveQuickMenu } from './quickMenu.js'
|
|
38
40
|
import { runtimeOf } from './mcpRuntime.js'
|
|
39
41
|
import { listMcp, refreshMcp, removeMcp, saveMcp, toggleMcp } from './mcp.js'
|
|
40
42
|
import { listProjects, projectRefresh, projectRemove, projectSave, projectToggle, mcpSnapshot } from './mcpProject.js'
|
|
@@ -60,7 +62,9 @@ import type { FileVersions } from './fileVersions.js'
|
|
|
60
62
|
import { settingsDocPath } from './settingsDocPath.js'
|
|
61
63
|
import { createTextPages } from './textPages.js'
|
|
62
64
|
import { reviewHandlers } from './reviewRpc.js'
|
|
63
|
-
import {
|
|
65
|
+
import { pendingHandlers } from './pendingRpc.js'
|
|
66
|
+
import { fileAbsent, missingReview, missingDecide, missingPath } from './missingReview.js'
|
|
67
|
+
import { pendingCovers } from './pendingCompat.js'
|
|
64
68
|
|
|
65
69
|
/** Process-local cursor signer; reads must stay on the Host filesystem capability. */
|
|
66
70
|
const readTextPage = createTextPages()
|
|
@@ -103,6 +107,19 @@ function recView(record: DiffRecord, ws: RemoteWs | null = null): RecordView {
|
|
|
103
107
|
}
|
|
104
108
|
}
|
|
105
109
|
|
|
110
|
+
/**
|
|
111
|
+
* 旧写入口守卫:路径已由待审累计基线接管时一律拒绝,避免两套权威同时写同一文件。
|
|
112
|
+
* @author ddj 2026年10月29号
|
|
113
|
+
* @param cwd 工作区
|
|
114
|
+
* @param path 请求涉及的路径(可为空表示无法定位)
|
|
115
|
+
* @returns 拒绝原因,未接管时为 null
|
|
116
|
+
*/
|
|
117
|
+
function legacyBlocked(cwd: string, path: string | undefined): string | null {
|
|
118
|
+
if (!path) return null
|
|
119
|
+
if (!pendingCovers(cwd, path)) return null
|
|
120
|
+
return '该文件已由待审累计基线接管,请使用面板 Keep/Revert(edrv.review.decide)'
|
|
121
|
+
}
|
|
122
|
+
|
|
106
123
|
/** 会话/工作区公共前置:返回 {session,cwd} 或错误文案。 */
|
|
107
124
|
async function requireSession(ctx: Ctx, sessionId: string | undefined): Promise<
|
|
108
125
|
{ session: Session; cwd: string } | { err: string }
|
|
@@ -600,6 +617,7 @@ export function buildHandlers(
|
|
|
600
617
|
// edrv.dap.* 由 createDapRpc 提供(调试会话单例),这里并入。
|
|
601
618
|
...((dapHandlers ?? {}) as RpcHandlerMap),
|
|
602
619
|
...(reviewHandlers(ctx, registry) as RpcHandlerMap),
|
|
620
|
+
...(pendingHandlers(ctx, registry) as RpcHandlerMap),
|
|
603
621
|
'edrv.list': async (args) => {
|
|
604
622
|
// #region debug log
|
|
605
623
|
const start = performance.now()
|
|
@@ -688,6 +706,8 @@ export function buildHandlers(
|
|
|
688
706
|
const sc = await requireSession(ctx, args.sessionId)
|
|
689
707
|
if ('err' in sc) return { ok: false, error: sc.err }
|
|
690
708
|
const bucket = await bucketOf(registry, ctx, sc.cwd)
|
|
709
|
+
const blocked = legacyBlocked(sc.cwd, bucket.get(args.callId)?.path)
|
|
710
|
+
if (blocked) return { ok: false, error: blocked }
|
|
691
711
|
const results = await applyDecisions(ctx, sc.session, sc.cwd, bucket, [{ callId: args.callId, scope: args.scope, hunkIndex: args.hunkIndex, decision: 'accepted' }])
|
|
692
712
|
const item = results[0]
|
|
693
713
|
if (!item?.ok) return { ok: false, error: item?.error ?? '操作失败' }
|
|
@@ -697,6 +717,8 @@ export function buildHandlers(
|
|
|
697
717
|
const sc = await requireSession(ctx, args.sessionId)
|
|
698
718
|
if ('err' in sc) return { ok: false, error: sc.err }
|
|
699
719
|
const bucket = await bucketOf(registry, ctx, sc.cwd)
|
|
720
|
+
const blocked = legacyBlocked(sc.cwd, bucket.get(args.callId)?.path)
|
|
721
|
+
if (blocked) return { ok: false, error: blocked }
|
|
700
722
|
const results = await applyDecisions(ctx, sc.session, sc.cwd, bucket, [{ callId: args.callId, scope: args.scope, hunkIndex: args.hunkIndex, decision: 'rejected' }])
|
|
701
723
|
const item = results[0]
|
|
702
724
|
if (!item?.ok) return { ok: false, error: item?.error ?? '操作失败' }
|
|
@@ -705,6 +727,8 @@ export function buildHandlers(
|
|
|
705
727
|
'edrv.missingReview': async (args) => {
|
|
706
728
|
const sc = await requireSession(ctx, args.sessionId)
|
|
707
729
|
if ('err' in sc) return { ok: false, error: sc.err }
|
|
730
|
+
const blocked = legacyBlocked(sc.cwd, args.path)
|
|
731
|
+
if (blocked) return { ok: false, error: blocked }
|
|
708
732
|
try {
|
|
709
733
|
const bucket = await bucketOf(registry, ctx, sc.cwd)
|
|
710
734
|
if (!await fileAbsent(ctx, sc.cwd, args.path)) return { ok: true, review: null }
|
|
@@ -714,6 +738,8 @@ export function buildHandlers(
|
|
|
714
738
|
'edrv.missingDecide': async (args) => {
|
|
715
739
|
const sc = await requireSession(ctx, args.sessionId)
|
|
716
740
|
if ('err' in sc) return { ok: false, error: sc.err }
|
|
741
|
+
const blocked = legacyBlocked(sc.cwd, missingPath(args.token))
|
|
742
|
+
if (blocked) return { ok: false, error: blocked }
|
|
717
743
|
try {
|
|
718
744
|
const bucket = await bucketOf(registry, ctx, sc.cwd)
|
|
719
745
|
return { ok: true, callIds: await missingDecide(ctx, sc.session, sc.cwd, bucket, args.token, args.operationId, args.undo === true) }
|
|
@@ -723,7 +749,10 @@ export function buildHandlers(
|
|
|
723
749
|
const sc = await requireSession(ctx, args.sessionId)
|
|
724
750
|
if ('err' in sc) return { ok: false, error: sc.err }
|
|
725
751
|
const bucket = await bucketOf(registry, ctx, sc.cwd)
|
|
726
|
-
const
|
|
752
|
+
const items = Array.isArray(args.items) ? args.items : []
|
|
753
|
+
const blocked = items.map((item) => legacyBlocked(sc.cwd, bucket.get(item.callId)?.path)).find((reason) => reason !== null)
|
|
754
|
+
if (blocked) return { ok: false, error: blocked }
|
|
755
|
+
const results = await applyDecisions(ctx, sc.session, sc.cwd, bucket, items)
|
|
727
756
|
return { ok: true, results }
|
|
728
757
|
},
|
|
729
758
|
/** @author ddj 2026年10月22号 @param args Read-only request. @returns Bounded official range read, or unsupported on older providers; never bypasses Host IO. */
|
|
@@ -1538,6 +1567,14 @@ export function buildHandlers(
|
|
|
1538
1567
|
return { ok: false, error: '切换规则失败:' + String(error) }
|
|
1539
1568
|
}
|
|
1540
1569
|
},
|
|
1570
|
+
'skills.list': async (args) => {
|
|
1571
|
+
try {
|
|
1572
|
+
// 只读枚举:不做 workspace 注册校验(读盘无副作用),cwd 由客户端按会话作用域传入
|
|
1573
|
+
return { ok: true, ...(await listSkillCatalog(args?.cwd)) }
|
|
1574
|
+
} catch (error) {
|
|
1575
|
+
return { ok: false, error: '读取技能候选失败:' + String(error) }
|
|
1576
|
+
}
|
|
1577
|
+
},
|
|
1541
1578
|
'snippets.list': async () => {
|
|
1542
1579
|
try {
|
|
1543
1580
|
return { ok: true, ...(await snippetsList(ctx)) }
|
|
@@ -1577,6 +1614,33 @@ export function buildHandlers(
|
|
|
1577
1614
|
return { ok: false, error: '读取代码片段条目失败:' + String(error) }
|
|
1578
1615
|
}
|
|
1579
1616
|
},
|
|
1617
|
+
'quickMenu.get': async () => {
|
|
1618
|
+
try {
|
|
1619
|
+
const state = await readQuickMenu()
|
|
1620
|
+
return { ok: true, doc: state.doc, warning: state.warning }
|
|
1621
|
+
} catch (error) {
|
|
1622
|
+
return { ok: false, error: '读取快捷菜单失败:' + String(error) }
|
|
1623
|
+
}
|
|
1624
|
+
},
|
|
1625
|
+
'quickMenu.save': async (args) => {
|
|
1626
|
+
try {
|
|
1627
|
+
const result = await saveQuickMenu(args.doc, args.baseRevision)
|
|
1628
|
+
// 冲突时只回错误文案:客户端收到后重拉最新文档,避免用旧版本覆盖别人的写入
|
|
1629
|
+
if (!result.ok) return { ok: false, error: result.error }
|
|
1630
|
+
return { ok: true, doc: result.doc }
|
|
1631
|
+
} catch (error) {
|
|
1632
|
+
return { ok: false, error: '保存快捷菜单失败:' + String(error) }
|
|
1633
|
+
}
|
|
1634
|
+
},
|
|
1635
|
+
'quickMenu.launcher': async (args) => {
|
|
1636
|
+
try {
|
|
1637
|
+
const result = await patchLauncher({ x: args.x, y: args.y })
|
|
1638
|
+
if (!result.ok) return { ok: false, error: result.error }
|
|
1639
|
+
return { ok: true, doc: result.doc }
|
|
1640
|
+
} catch (error) {
|
|
1641
|
+
return { ok: false, error: '保存悬浮按钮位置失败:' + String(error) }
|
|
1642
|
+
}
|
|
1643
|
+
},
|
|
1580
1644
|
}
|
|
1581
1645
|
}
|
|
1582
1646
|
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 路径比较口径:Host 与客户端共用同一套「远端 / 盘符 / 相对路径」判定,只在平台折叠上分工。
|
|
3
|
+
* @author ddj 2026年10月30号
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/** 远端仓库身份:`scheme://...` 区分大小写,绝不能折叠。 */
|
|
7
|
+
const REMOTE = /^[a-z]+:\/\//i
|
|
8
|
+
/** 盘符绝对路径:大小写不敏感与宿主平台无关,两边都必须折叠。 */
|
|
9
|
+
const DRIVE = /^[a-z]:\//i
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @author ddj 2026年10月30号
|
|
13
|
+
* @description: 统一分隔符为 `/`,其余字符原样保留。
|
|
14
|
+
* @param path 原始路径
|
|
15
|
+
* @returns 反斜杠已归一化的路径
|
|
16
|
+
*/
|
|
17
|
+
export function unifiedPath(path: string): string {
|
|
18
|
+
return path.replace(/\\/g, '/')
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @author ddj 2026年10月30号
|
|
23
|
+
* @description: 是否为远端仓库身份。
|
|
24
|
+
* @param path 原始路径
|
|
25
|
+
* @returns 远端身份返回 true
|
|
26
|
+
*/
|
|
27
|
+
export function isRemotePath(path: string): boolean {
|
|
28
|
+
return REMOTE.test(unifiedPath(path))
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @author ddj 2026年10月30号
|
|
33
|
+
* @description: 是否为盘符绝对路径。
|
|
34
|
+
* @param path 原始路径
|
|
35
|
+
* @returns Windows 盘符路径返回 true
|
|
36
|
+
*/
|
|
37
|
+
export function isDrivePath(path: string): boolean {
|
|
38
|
+
return DRIVE.test(unifiedPath(path))
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* @author ddj 2026年10月30号
|
|
43
|
+
* @description: 生成路径比较键。盘符路径恒折叠大小写;其余路径是否折叠由调用方按运行平台决定
|
|
44
|
+
* (Host 在 Windows 上折叠,客户端不在宿主进程里,取 false)。
|
|
45
|
+
* @param path 原始路径
|
|
46
|
+
* @param foldCase 非盘符路径是否折叠大小写
|
|
47
|
+
* @returns 比较键
|
|
48
|
+
*/
|
|
49
|
+
export function pathKeyFor(path: string, foldCase: boolean): string {
|
|
50
|
+
const unified = unifiedPath(path)
|
|
51
|
+
if (REMOTE.test(unified)) return unified
|
|
52
|
+
if (DRIVE.test(unified)) return unified.toLowerCase()
|
|
53
|
+
return foldCase ? unified.toLowerCase() : unified
|
|
54
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-vscode-mode 待审投影(pending review)的共享契约:按文件累计的净差异视图。
|
|
3
|
+
* 官方快照表是不可变事实来源,本类型只是可由事实重建的投影,任何决定都不等于磁盘版本。
|
|
4
|
+
* @author ddj 2026年10月29号
|
|
5
|
+
*/
|
|
6
|
+
import type { OfficialDiff } from './review.js'
|
|
7
|
+
import type { ReviewBlockState } from './reviewRegions.js'
|
|
8
|
+
|
|
9
|
+
/** 基线形态:`file` 写回内容,`none` 说明基线没有该路径(Revert 语义是删除文件)。 */
|
|
10
|
+
export type PendingEarlier = 'file' | 'none'
|
|
11
|
+
|
|
12
|
+
/** 会话血缘,用于子代理/派生会话归属到同一待审视图。 */
|
|
13
|
+
export interface PendingLineage {
|
|
14
|
+
parentSessionId?: string
|
|
15
|
+
origin?: string
|
|
16
|
+
delegationDepth?: number
|
|
17
|
+
cwd?: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** 待审条目的展示投影。 */
|
|
21
|
+
export interface PendingItem {
|
|
22
|
+
/** 稳定身份键(后端 + 目标键),客户端按它发送动作。 */
|
|
23
|
+
id: string
|
|
24
|
+
/** 展示路径。 */
|
|
25
|
+
path: string
|
|
26
|
+
earlierVersion: PendingEarlier
|
|
27
|
+
added: number
|
|
28
|
+
deleted: number
|
|
29
|
+
/** oldText 与 newText 严格相等:已解决但仍留在列表。 */
|
|
30
|
+
settled: boolean
|
|
31
|
+
/** 读者还没看过的最新变化。 */
|
|
32
|
+
unseen: boolean
|
|
33
|
+
/** 文件已不存在(Revert 可恢复)。 */
|
|
34
|
+
missing: boolean
|
|
35
|
+
/** 当前文件内容已被外部改动替换过。 */
|
|
36
|
+
diverged: boolean
|
|
37
|
+
/** 文本超出存储/展示上限,差异与决定能力受限。 */
|
|
38
|
+
oversized: boolean
|
|
39
|
+
revision: number
|
|
40
|
+
updatedAt: number
|
|
41
|
+
sessionId: string
|
|
42
|
+
sessionIds: string[]
|
|
43
|
+
lineage?: PendingLineage
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** 一页待审条目。 */
|
|
47
|
+
export interface PendingPage {
|
|
48
|
+
items: PendingItem[]
|
|
49
|
+
pending: number
|
|
50
|
+
nextCursor: number | null
|
|
51
|
+
/** 采集/重建过程中的缺口说明,绝不静默吞掉。 */
|
|
52
|
+
gaps: string[]
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** 单条待审详情。 */
|
|
56
|
+
export interface PendingDetail {
|
|
57
|
+
item: PendingItem
|
|
58
|
+
diff: OfficialDiff
|
|
59
|
+
blocks: ReviewBlockState[]
|
|
60
|
+
capability: { keep: boolean; revert: boolean; reason: string }
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** 全部请求都带会话作用域,工作区由 host 解析,永不采信客户端 cwd。 */
|
|
64
|
+
export interface PendingRequest {
|
|
65
|
+
sessionId: string
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** 决定粒度。 */
|
|
69
|
+
export interface PendingScope {
|
|
70
|
+
scope: 'file' | 'block' | 'range' | 'all'
|
|
71
|
+
blockId?: string
|
|
72
|
+
startLine?: number
|
|
73
|
+
endLine?: number
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Keep/Revert 请求;operationId 提供幂等,revision 提供并发校验。 */
|
|
77
|
+
export interface PendingDecide extends PendingRequest, PendingScope {
|
|
78
|
+
id: string
|
|
79
|
+
revision: number
|
|
80
|
+
operationId: string
|
|
81
|
+
decision: 'accepted' | 'rejected'
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** 从列表移除已解决条目(不触碰磁盘)。 */
|
|
85
|
+
export interface PendingRemove extends PendingRequest {
|
|
86
|
+
id: string
|
|
87
|
+
revision: number
|
|
88
|
+
operationId: string
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** 撤销/重做一次决定。 */
|
|
92
|
+
export interface PendingHistoryMove extends PendingRequest {
|
|
93
|
+
direction: 'undo' | 'redo'
|
|
94
|
+
operationId: string
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** 从版本控制导入本地改动。 */
|
|
98
|
+
export interface PendingImport extends PendingRequest {
|
|
99
|
+
includeUntracked?: boolean
|
|
100
|
+
/** 只刷新一个文件,便于大仓单文件重扫。 */
|
|
101
|
+
path?: string
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** 待审条目在面板列表里的 id 前缀;客户端与 Host 共用这一份口径。 */
|
|
105
|
+
export const PENDING_PREFIX = 'pending:'
|