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.
Files changed (43) hide show
  1. package/README.md +52 -4
  2. package/lib/client.js +3442 -183
  3. package/lib/client.js.map +2 -2
  4. package/lib/index.js +3510 -222
  5. package/lib/index.js.map +1 -1
  6. package/package.json +1 -1
  7. package/src/client/index.ts +166 -0
  8. package/src/client/quickMenuHost.ts +46 -0
  9. package/src/client/quickMenuStore.ts +260 -0
  10. package/src/client/state/diffSummary.ts +52 -8
  11. package/src/client/styles/quickmenu.css +1049 -0
  12. package/src/client/ui/DiffLauncher.ts +1 -1
  13. package/src/client/ui/EditorView.ts +10 -2
  14. package/src/client/ui/OfficialDiffBar.tsx +1 -12
  15. package/src/client/ui/OfficialReview.ts +13 -3
  16. package/src/client/ui/QuickMenuDialog.tsx +835 -0
  17. package/src/client/ui/QuickMenuLauncher.tsx +792 -0
  18. package/src/client/ui/QuickMenuPickers.ts +577 -0
  19. package/src/client/ui/SidecarDiffBar.tsx +0 -22
  20. package/src/index.ts +9 -0
  21. package/src/missingReview.ts +36 -14
  22. package/src/paths.ts +10 -0
  23. package/src/pending/capture.ts +165 -0
  24. package/src/pending/decide.ts +213 -0
  25. package/src/pending/hunks.ts +241 -0
  26. package/src/pending/kernel.ts +233 -0
  27. package/src/pending/persist.ts +329 -0
  28. package/src/pending/store.ts +472 -0
  29. package/src/pending/vcs.ts +444 -0
  30. package/src/pendingCompat.ts +184 -0
  31. package/src/pendingRpc.ts +210 -0
  32. package/src/quickMenu.ts +269 -0
  33. package/src/reviewDb.ts +3 -0
  34. package/src/reviewRpc.ts +52 -1
  35. package/src/rpc.ts +66 -2
  36. package/src/shared/pathKey.ts +54 -0
  37. package/src/shared/pendingReview.ts +105 -0
  38. package/src/shared/quickMenu.ts +1009 -0
  39. package/src/shared/quickPick.ts +67 -0
  40. package/src/shared/rpc.ts +28 -0
  41. package/src/shared/skillCatalog.ts +60 -0
  42. package/src/skillCatalog.ts +138 -0
  43. 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 { fileAbsent, missingReview, missingDecide } from './missingReview.js'
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 results = await applyDecisions(ctx, sc.session, sc.cwd, bucket, Array.isArray(args.items) ? args.items : [])
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:'