dsh-vscode-mode 0.25.0 → 0.26.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 +11 -5
- package/lib/client.js +1613 -1317
- package/lib/client.js.map +2 -2
- package/lib/index.js +7028 -6516
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
- package/src/client/downloadFile.ts +36 -6
- package/src/client/downloadSink.ts +162 -0
- package/src/client/reviewBar.ts +3 -0
- package/src/client/reviewBatch.ts +9 -1
- package/src/client/reviewKeepAll.ts +72 -0
- package/src/client/state/diffSummary.ts +34 -7
- package/src/client/ui/DownloadPicker.ts +2 -1
- package/src/client/ui/EditorView.ts +117 -29
- package/src/client/ui/ReviewAllConfirm.ts +1 -1
- package/src/index.ts +1 -1
- package/src/pending/persist.ts +94 -5
- package/src/pending/store.ts +76 -1
- package/src/pendingCompat.ts +80 -9
- package/src/pendingRpc.ts +7 -6
- package/src/reviewDb.ts +119 -8
- package/src/reviewRpc.ts +110 -3
- package/src/rpc.ts +14 -10
- package/src/shared/review.ts +21 -0
- package/src/shared/rpc.ts +3 -1
- package/src/store.ts +44 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-vscode-mode",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.26.0",
|
|
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": [
|
|
@@ -16,6 +16,8 @@
|
|
|
16
16
|
import { readBinaryPreview } from '../shared/rpc.js'
|
|
17
17
|
import { baseNameOf } from '../shared/fsNames.js'
|
|
18
18
|
import { rpc } from './rpc.js'
|
|
19
|
+
import type { SaveFallback, SaveSink } from './downloadSink.js'
|
|
20
|
+
import { pickSaveSink, writeFileToDir } from './downloadSink.js'
|
|
19
21
|
import type { TreeMenuTarget } from './sidebar/contextMenu.js'
|
|
20
22
|
import type { SidebarCtx } from './sidebar/types.js'
|
|
21
23
|
import { openDownloader } from './ui/DownloadPicker.js'
|
|
@@ -41,6 +43,8 @@ export interface DownloadOpts {
|
|
|
41
43
|
signal?: AbortSignal
|
|
42
44
|
/** 每完成一个文件回调一次(done 含成功与失败)。 */
|
|
43
45
|
onProgress?: (done: number, total: number, path: string) => void
|
|
46
|
+
/** 落盘去处;缺省按浏览器默认下载目录(`<a download>`)。 */
|
|
47
|
+
sink?: SaveSink
|
|
44
48
|
}
|
|
45
49
|
|
|
46
50
|
// --endregion
|
|
@@ -85,16 +89,20 @@ export async function fetchDownload(path: string, sessionId?: string): Promise<{
|
|
|
85
89
|
}
|
|
86
90
|
|
|
87
91
|
/**
|
|
88
|
-
*
|
|
92
|
+
* 下载单个文件并落盘(所选目录则按相对路径写入,否则交给浏览器 `<a download>`)。
|
|
89
93
|
* @author ddj 2026年11月03号
|
|
90
94
|
* @param path 工作区相对路径
|
|
91
|
-
* @param opts 会话 /
|
|
95
|
+
* @param opts 会话 / 取消 / 落盘选项
|
|
92
96
|
* @returns 路径与字节数
|
|
93
|
-
* @throws
|
|
97
|
+
* @throws 读取失败透传主机错误;目录写入失败透传底层错误
|
|
94
98
|
*/
|
|
95
99
|
export async function downloadOne(path: string, opts: DownloadOpts = {}): Promise<DownloadedFile> {
|
|
96
100
|
const file = await fetchDownload(path, opts.sessionId)
|
|
97
|
-
|
|
101
|
+
if (opts.sink?.kind === 'dir') {
|
|
102
|
+
await writeFileToDir(opts.sink.dir, path, file.bytes)
|
|
103
|
+
} else {
|
|
104
|
+
downloadBytes(file.bytes, baseNameOf(path) || 'download', file.mime)
|
|
105
|
+
}
|
|
98
106
|
return { path, size: file.size }
|
|
99
107
|
}
|
|
100
108
|
|
|
@@ -148,22 +156,44 @@ export async function downloadTarget(target: TreeMenuTarget, ctx?: SidebarCtx):
|
|
|
148
156
|
}
|
|
149
157
|
|
|
150
158
|
/**
|
|
151
|
-
*
|
|
159
|
+
* 批量下载并统一反馈(先选保存目录,再逐文件落盘;开始一条、结束一条,避免逐文件刷屏)。
|
|
152
160
|
* @author ddj 2026年11月03号
|
|
153
161
|
* @param paths 勾选路径
|
|
154
162
|
* @param ctx 面板上下文
|
|
163
|
+
* @sideEffects 弹出目录选择器;用户取消则整批中止
|
|
155
164
|
*/
|
|
156
165
|
async function runBatch(paths: string[], ctx?: SidebarCtx): Promise<void> {
|
|
157
166
|
if (!paths.length) return
|
|
167
|
+
|
|
168
|
+
// 选择器必须在用户手势的同步栈里触发,故放在第一个 await 之前。
|
|
169
|
+
const sink = await pickSaveSink()
|
|
170
|
+
if (sink === 'cancelled') {
|
|
171
|
+
ctx?.notify?.('已取消下载')
|
|
172
|
+
return
|
|
173
|
+
}
|
|
174
|
+
if (sink.kind === 'browser') ctx?.notify?.(fallbackText(sink.reason))
|
|
158
175
|
if (paths.length > 1) ctx?.notify?.('正在下载 ' + paths.length + ' 个文件…')
|
|
176
|
+
|
|
159
177
|
try {
|
|
160
|
-
const out = await downloadMany(paths, { sessionId: ctx?.sessionId })
|
|
178
|
+
const out = await downloadMany(paths, { sessionId: ctx?.sessionId, sink })
|
|
161
179
|
ctx?.notify?.(batchText(out))
|
|
162
180
|
} catch (error) {
|
|
163
181
|
ctx?.notify?.('下载失败:' + textOfError(error))
|
|
164
182
|
}
|
|
165
183
|
}
|
|
166
184
|
|
|
185
|
+
/**
|
|
186
|
+
* 目录选择不可用时的提示文案(说明文件去了浏览器默认下载目录)。
|
|
187
|
+
* @author ddj 2026年11月03号
|
|
188
|
+
* @param reason 回落原因
|
|
189
|
+
* @returns 单行提示文案
|
|
190
|
+
*/
|
|
191
|
+
function fallbackText(reason: SaveFallback): string {
|
|
192
|
+
if (reason === 'unsupported') return '浏览器不支持选择保存目录,已保存到默认下载目录'
|
|
193
|
+
if (reason === 'insecure') return '当前页面不是安全上下文(需 https 或 localhost),已保存到默认下载目录'
|
|
194
|
+
return '未获得目录写入权限,已保存到默认下载目录'
|
|
195
|
+
}
|
|
196
|
+
|
|
167
197
|
/**
|
|
168
198
|
* 批量结果口播文案。
|
|
169
199
|
* @author ddj 2026年11月03号
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-vscode-mode client — 下载落盘位置(目录选择器)。
|
|
3
|
+
*
|
|
4
|
+
* 浏览器原生 `<a download>` 由浏览器决定保存位置,页面拿不到路径;本模块用
|
|
5
|
+
* File System Access API 的 `showDirectoryPicker` 让用户在点「下载」时选一次目标文件夹,
|
|
6
|
+
* 之后把文件按工作区相对路径逐个写进去(文件夹下载自动建子目录,仍不打包)。
|
|
7
|
+
*
|
|
8
|
+
* 约束:
|
|
9
|
+
* - 选择器必须在用户手势的同步栈里调用,故入口函数都把 `pickSaveSink()` 放在第一个 await;
|
|
10
|
+
* - 需要安全上下文(https / localhost)与 Chromium 系浏览器,Firefox / Safari / 局域网 http
|
|
11
|
+
* 一律回落到浏览器默认下载目录,并在界面上说明原因,不静默改语义;
|
|
12
|
+
* - 用户取消选择(AbortError)与「不可用」区分开:前者终止整批,后者继续下载。
|
|
13
|
+
* 作者 ddj 2026年11月03号
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
// --region 类型
|
|
17
|
+
|
|
18
|
+
/** 落盘去处:写入用户所选目录,或回落到浏览器默认下载目录。 */
|
|
19
|
+
export type SaveSink =
|
|
20
|
+
| { kind: 'dir'; dir: SinkDir }
|
|
21
|
+
| { kind: 'browser'; reason: SaveFallback }
|
|
22
|
+
|
|
23
|
+
/** 回落到浏览器下载的原因(调用方据此给出提示文案)。 */
|
|
24
|
+
export type SaveFallback = 'unsupported' | 'insecure' | 'denied'
|
|
25
|
+
|
|
26
|
+
/** 可写文件的句柄(File System Access API 的子集)。 */
|
|
27
|
+
export interface SinkWritable {
|
|
28
|
+
write(data: Uint8Array): Promise<void>
|
|
29
|
+
close(): Promise<void>
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** 文件句柄(仅用到创建可写流)。 */
|
|
33
|
+
export interface SinkFileHandle {
|
|
34
|
+
createWritable(): Promise<SinkWritable>
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** 目录句柄(仅用到列目录与建子目录/文件)。 */
|
|
38
|
+
export interface SinkDir {
|
|
39
|
+
name: string
|
|
40
|
+
getDirectoryHandle(name: string, opts?: { create?: boolean }): Promise<SinkDir>
|
|
41
|
+
getFileHandle(name: string, opts?: { create?: boolean }): Promise<SinkFileHandle>
|
|
42
|
+
queryPermission?(opts: { mode: 'readwrite' }): Promise<string>
|
|
43
|
+
requestPermission?(opts: { mode: 'readwrite' }): Promise<string>
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** `window.showDirectoryPicker` 的最小签名。 */
|
|
47
|
+
type DirPicker = (opts?: { id?: string; mode?: string; startIn?: string }) => Promise<SinkDir>
|
|
48
|
+
|
|
49
|
+
// --endregion
|
|
50
|
+
|
|
51
|
+
// --region 选择器
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* 当前浏览器/上下文是否可能弹出目录选择器(安全上下文 + API 存在)。
|
|
55
|
+
* @author ddj 2026年11月03号
|
|
56
|
+
* @returns 可用为 true;不安全上下文或缺 API 为 false
|
|
57
|
+
*/
|
|
58
|
+
export function canPickDirectory(): boolean {
|
|
59
|
+
return pickerOf() !== undefined
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* 让用户选择保存目录。
|
|
64
|
+
* @author ddj 2026年11月03号
|
|
65
|
+
* @returns 所选目录 / 回落原因 / 'cancelled'(用户取消,整批终止)
|
|
66
|
+
* @sideEffects 弹出系统目录选择器,成功后可能触发一次写权限请求
|
|
67
|
+
*/
|
|
68
|
+
export async function pickSaveSink(): Promise<SaveSink | 'cancelled'> {
|
|
69
|
+
const win = globalThis as { isSecureContext?: boolean; showDirectoryPicker?: DirPicker }
|
|
70
|
+
if (win.isSecureContext === false) return { kind: 'browser', reason: 'insecure' }
|
|
71
|
+
|
|
72
|
+
const picker = pickerOf()
|
|
73
|
+
if (!picker) return { kind: 'browser', reason: 'unsupported' }
|
|
74
|
+
|
|
75
|
+
let dir: SinkDir
|
|
76
|
+
try {
|
|
77
|
+
dir = await picker({ id: 'vsm-download', mode: 'readwrite', startIn: 'downloads' })
|
|
78
|
+
} catch (error) {
|
|
79
|
+
return isAbort(error) ? 'cancelled' : { kind: 'browser', reason: 'denied' }
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return (await ensureWritable(dir)) ? { kind: 'dir', dir } : { kind: 'browser', reason: 'denied' }
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* 取回 `showDirectoryPicker`(不安全上下文视为没有)。
|
|
87
|
+
* @author ddj 2026年11月03号
|
|
88
|
+
* @returns 绑定到 globalThis 的选目录函数;不可用为 undefined
|
|
89
|
+
*/
|
|
90
|
+
function pickerOf(): DirPicker | undefined {
|
|
91
|
+
const win = globalThis as { isSecureContext?: boolean; showDirectoryPicker?: DirPicker }
|
|
92
|
+
if (win.isSecureContext === false) return undefined
|
|
93
|
+
return typeof win.showDirectoryPicker === 'function' ? win.showDirectoryPicker.bind(win) : undefined
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* 确认目录可写(部分浏览器选择后仍需显式请求一次 readwrite 权限)。
|
|
98
|
+
* @author ddj 2026年11月03号
|
|
99
|
+
* @param dir 目录句柄
|
|
100
|
+
* @returns 已获得写权限为 true
|
|
101
|
+
* @throws 权限查询本身抛错时向上透传(由调用方按回落处理)
|
|
102
|
+
*/
|
|
103
|
+
async function ensureWritable(dir: SinkDir): Promise<boolean> {
|
|
104
|
+
if (!dir.queryPermission) return true
|
|
105
|
+
if ((await dir.queryPermission({ mode: 'readwrite' })) === 'granted') return true
|
|
106
|
+
if (!dir.requestPermission) return false
|
|
107
|
+
return (await dir.requestPermission({ mode: 'readwrite' })) === 'granted'
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// --endregion
|
|
111
|
+
|
|
112
|
+
// --region 写入
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* 把字节写到所选目录下的相对路径处(自动逐级建子目录)。
|
|
116
|
+
* @author ddj 2026年11月03号
|
|
117
|
+
* @param dir 目标目录句柄
|
|
118
|
+
* @param relPath 工作区相对路径(分隔符 `/`;越界段 `..` 会被丢弃)
|
|
119
|
+
* @param bytes 原始字节
|
|
120
|
+
* @sideEffects 在用户所选目录内创建目录与文件并覆盖同名文件
|
|
121
|
+
* @throws 磁盘写入失败或文件名非法时透传底层错误
|
|
122
|
+
*/
|
|
123
|
+
export async function writeFileToDir(dir: SinkDir, relPath: string, bytes: Uint8Array): Promise<void> {
|
|
124
|
+
const parts = pathSegments(relPath)
|
|
125
|
+
const fileName = parts.pop() ?? 'download'
|
|
126
|
+
|
|
127
|
+
let parent = dir
|
|
128
|
+
for (const part of parts) parent = await parent.getDirectoryHandle(part, { create: true })
|
|
129
|
+
|
|
130
|
+
const handle = await parent.getFileHandle(fileName, { create: true })
|
|
131
|
+
const writable = await handle.createWritable()
|
|
132
|
+
try {
|
|
133
|
+
await writable.write(bytes)
|
|
134
|
+
} finally {
|
|
135
|
+
await writable.close()
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* 相对路径切段(过滤空段、`.` 与越界 `..`,兼容反斜杠)。
|
|
141
|
+
* @author ddj 2026年11月03号
|
|
142
|
+
* @param relPath 工作区相对路径
|
|
143
|
+
* @returns 安全的路径段(末段为文件名)
|
|
144
|
+
*/
|
|
145
|
+
function pathSegments(relPath: string): string[] {
|
|
146
|
+
return relPath
|
|
147
|
+
.split(/[\\/]+/)
|
|
148
|
+
.map((part) => part.trim())
|
|
149
|
+
.filter((part) => part !== '' && part !== '.' && part !== '..')
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* 判断是否用户取消(AbortError),不依赖 DOMException 构造函数的存在。
|
|
154
|
+
* @author ddj 2026年11月03号
|
|
155
|
+
* @param error 捕获值
|
|
156
|
+
* @returns 取消为 true
|
|
157
|
+
*/
|
|
158
|
+
function isAbort(error: unknown): boolean {
|
|
159
|
+
return typeof error === 'object' && error !== null && (error as { name?: string }).name === 'AbortError'
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// --endregion
|
package/src/client/reviewBar.ts
CHANGED
|
@@ -126,6 +126,9 @@ export const SIDECAR_KEEP_TITLE = '标记这些历史改动为已看过:只改
|
|
|
126
126
|
/** 非官方 Undo 的作用域说明:走原有回滚校验写回磁盘,但不影响官方记录。 */
|
|
127
127
|
export const SIDECAR_UNDO_TITLE = '撤销这些历史改动(写回受回滚校验保护):只影响这份历史记录,不影响官方审查。'
|
|
128
128
|
|
|
129
|
+
/** 非官方 Keep 之后的完成态:该通道 Keep 不写盘,不能沿用「已采纳」这种会让人以为改过文件的措辞。 */
|
|
130
|
+
export const SIDECAR_KEEP_DONE = '已标记为看过(未改动文件)'
|
|
131
|
+
|
|
129
132
|
/** 当前端没有该会话时,差异栏里给中性说明而不是红色报错。 */
|
|
130
133
|
export const SESSION_DOWN_NOTICE = '这个会话没在当前端激活,官方审查数据暂时读不到;下面的历史改动仍可查看、Keep、Undo。'
|
|
131
134
|
|
|
@@ -30,7 +30,15 @@ export async function loadPending(port: Pick<BatchPort, 'snapshot'>): Promise<Re
|
|
|
30
30
|
return page.items
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
/**
|
|
33
|
+
/**
|
|
34
|
+
* @public
|
|
35
|
+
* @author ddj 2026年10月27号
|
|
36
|
+
* @description: 逐条决策引擎,现仅供 Undo All 使用(Keep 已改走 `reviewKeepAll.ts` 的宿主批量通道);
|
|
37
|
+
* 保留 reject 形态以便对照既有契约与测试。
|
|
38
|
+
* @param port Guarded existing per-file decision adapter.
|
|
39
|
+
* @param reject Undo instead of Keep.
|
|
40
|
+
* @returns Precise partial result; Undo stops at first conflict, never claims atomic multi-file rollback.
|
|
41
|
+
*/
|
|
34
42
|
export async function runReviewBatch(port: BatchPort, reject: boolean): Promise<BatchResult> {
|
|
35
43
|
const result: BatchResult = { completed: 0, total: 0, paths: [], error: '' }
|
|
36
44
|
try {
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 批量 Keep:一次完整快照 + 一次批量 RPC,单条基线问题只跳过自己,不再整批中止。
|
|
3
|
+
* Undo 仍走 reviewBatch.ts 的逐条通道(写盘要逐项校验并在冲突处停止)。
|
|
4
|
+
* @author ddj 2026年11月03号
|
|
5
|
+
*/
|
|
6
|
+
import type { KeepAllItem, KeepAllResult, ReviewItem, ReviewPage, ReviewResult } from '../shared/review.js'
|
|
7
|
+
import { loadPending } from './reviewBatch.js'
|
|
8
|
+
|
|
9
|
+
/** 批量 Keep 的 RPC 适配器;guard 在取快照前后各校验一次会话寿命。 */
|
|
10
|
+
export interface KeepAllPort {
|
|
11
|
+
snapshot: () => Promise<ReviewPage>
|
|
12
|
+
keepAll: (items: KeepAllItem[], batchId: string) => Promise<ReviewResult<{ result: KeepAllResult }>>
|
|
13
|
+
guard: () => void
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @private
|
|
18
|
+
* @author ddj 2026年11月03号
|
|
19
|
+
* @description: 取一次完整待审快照;完整性校验失败时重取一次,避免瞬时漂移被当成不可恢复错误。
|
|
20
|
+
* @param port 已鉴权的 RPC 适配器
|
|
21
|
+
* @returns 冻结的批量目标
|
|
22
|
+
* @throws 两次都取不到一致快照时抛出原始原因
|
|
23
|
+
*/
|
|
24
|
+
async function snapshotOnce(port: KeepAllPort): Promise<KeepAllItem[]> {
|
|
25
|
+
try {
|
|
26
|
+
return toItems(await loadPending(port))
|
|
27
|
+
} catch {
|
|
28
|
+
// 分页期间数量漂移多为瞬时:guard 仍有效就重新取一次,仍不一致才放弃
|
|
29
|
+
port.guard()
|
|
30
|
+
return toItems(await loadPending(port))
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* @private
|
|
36
|
+
* @author ddj 2026年11月03号
|
|
37
|
+
* @description: 把快照条目裁成批量目标,只带 host 需要的身份与版本线索。
|
|
38
|
+
* @param items 完整待审集
|
|
39
|
+
* @returns 批量目标
|
|
40
|
+
*/
|
|
41
|
+
function toItems(items: ReviewItem[]): KeepAllItem[] {
|
|
42
|
+
return items.map((item) => ({ id: item.id, display: item.display, seq: item.seq, revision: item.revision }))
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* @public
|
|
47
|
+
* @author ddj 2026年11月03号
|
|
48
|
+
* @description: 批量保留全部待审记录(Keep 不改写磁盘)。一次往返完成,跳过项只进 skipped,
|
|
49
|
+
* 段级异常才写 error;guard 失效时保留已完成计数交给调用方呈现。
|
|
50
|
+
* @param port 已鉴权的 RPC 适配器
|
|
51
|
+
* @returns 与 BatchResult 同构的批量结果(含跳过明细)
|
|
52
|
+
*/
|
|
53
|
+
export async function runKeepAll(port: KeepAllPort): Promise<KeepAllResult> {
|
|
54
|
+
const result: KeepAllResult = { completed: 0, total: 0, paths: [], skipped: [], error: '' }
|
|
55
|
+
try {
|
|
56
|
+
port.guard()
|
|
57
|
+
const items = await snapshotOnce(port)
|
|
58
|
+
result.total = items.length
|
|
59
|
+
if (!items.length) return result
|
|
60
|
+
|
|
61
|
+
port.guard()
|
|
62
|
+
const reply = await port.keepAll(items, crypto.randomUUID())
|
|
63
|
+
if (!reply.ok) {
|
|
64
|
+
result.error = reply.error
|
|
65
|
+
return result
|
|
66
|
+
}
|
|
67
|
+
return reply.result
|
|
68
|
+
} catch (cause) {
|
|
69
|
+
result.error = cause instanceof Error ? cause.message : String(cause)
|
|
70
|
+
return result
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -32,6 +32,39 @@ export function officialPath(path: string, page: ReviewPage | null): boolean {
|
|
|
32
32
|
return !!page?.overview?.coveredPaths.some((candidate) => diffPathKey(candidate) === key)
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
/**
|
|
36
|
+
* @author ddj 2026年11月3号
|
|
37
|
+
* @description: 本页待审条目占用的路径键:路径本身与其身份各一份。
|
|
38
|
+
* @param page 审查页
|
|
39
|
+
* @returns 待审覆盖的路径键集合
|
|
40
|
+
*/
|
|
41
|
+
function pendingCoverKeys(page: ReviewPage | null): Set<string> {
|
|
42
|
+
const keys = new Set<string>()
|
|
43
|
+
for (const item of page?.items ?? []) {
|
|
44
|
+
if (!item.id.startsWith(PENDING_PREFIX)) continue
|
|
45
|
+
// 待审条目的 path 取自写入时的原始写法(agent 常用工作区相对路径),官方导航用的却是绝对身份:
|
|
46
|
+
// 只按 path 建覆盖集时,同一文件的官方汇总行不会被压掉,差异栏把 3 个文件算成 6 条。
|
|
47
|
+
// 身份(id 去掉待审前缀)与官方 identity 同源,必须一起入集。
|
|
48
|
+
keys.add(diffPathKey(item.currentPath || item.path))
|
|
49
|
+
keys.add(diffPathKey(item.id.slice(PENDING_PREFIX.length)))
|
|
50
|
+
}
|
|
51
|
+
return keys
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* @public
|
|
56
|
+
* @author ddj 2026年11月3号
|
|
57
|
+
* @description: 取本页"已有权威通道在管"的路径键:官方汇总覆盖集 + 待审条目。
|
|
58
|
+
* 编辑器与差异汇总必须同口径,否则同一改动在列表里被压掉、在编辑器里却仍然画得出来并可以点。
|
|
59
|
+
* @param page 审查页
|
|
60
|
+
* @returns 路径比较键集合
|
|
61
|
+
*/
|
|
62
|
+
export function coveredPathKeys(page: ReviewPage | null): Set<string> {
|
|
63
|
+
const keys = pendingCoverKeys(page)
|
|
64
|
+
for (const path of page?.overview?.coveredPaths ?? []) keys.add(diffPathKey(path))
|
|
65
|
+
return keys
|
|
66
|
+
}
|
|
67
|
+
|
|
35
68
|
/**
|
|
36
69
|
* @author ddj 2026年10月30号
|
|
37
70
|
* @description: 取出本页待审条目(id 带待审前缀)并折算成文件行。
|
|
@@ -40,15 +73,9 @@ export function officialPath(path: string, page: ReviewPage | null): boolean {
|
|
|
40
73
|
*/
|
|
41
74
|
function pendingRowsOf(page: ReviewPage | null): { rows: DiffFile[]; covered: Set<string> } {
|
|
42
75
|
const rows: DiffFile[] = []
|
|
43
|
-
const covered = new Set<string>()
|
|
44
76
|
for (const item of page?.items ?? []) {
|
|
45
77
|
if (!item.id.startsWith(PENDING_PREFIX)) continue
|
|
46
78
|
const path = item.currentPath || item.path
|
|
47
|
-
// 待审条目的 path 取自写入时的原始写法(agent 常用工作区相对路径),官方导航用的却是绝对身份:
|
|
48
|
-
// 只按 path 建覆盖集时,同一文件的官方汇总行不会被压掉,差异栏把 3 个文件算成 6 条。
|
|
49
|
-
// 身份(id 去掉待审前缀)与官方 identity 同源,必须一起入集。
|
|
50
|
-
covered.add(diffPathKey(path))
|
|
51
|
-
covered.add(diffPathKey(item.id.slice(PENDING_PREFIX.length)))
|
|
52
79
|
rows.push({
|
|
53
80
|
path,
|
|
54
81
|
label: item.display || path,
|
|
@@ -59,7 +86,7 @@ function pendingRowsOf(page: ReviewPage | null): { rows: DiffFile[]; covered: Se
|
|
|
59
86
|
sourceKind: 'pending',
|
|
60
87
|
})
|
|
61
88
|
}
|
|
62
|
-
return { rows, covered }
|
|
89
|
+
return { rows, covered: pendingCoverKeys(page) }
|
|
63
90
|
}
|
|
64
91
|
|
|
65
92
|
/** @public @author ddj 2026年10月26号 @param records Tool-call history. @param page Official file navigation, never only its first 50 rows. @returns Pending first, then official; legacy paths remain explicitly labelled. */
|
|
@@ -114,6 +114,7 @@ export function DownloadPicker(props: DownloadPickerProps): React.ReactElement {
|
|
|
114
114
|
},
|
|
115
115
|
React.createElement('h3', null, '下载到本机'),
|
|
116
116
|
React.createElement('div', { className: 'edrv-dl-sub' }, root ? '/' + root : '工作区根目录'),
|
|
117
|
+
React.createElement('div', { className: 'edrv-dl-hint' }, '点下载后会先选择保存文件夹,文件按相对路径写入'),
|
|
117
118
|
renderTree({ state, root, rows, empty, loadDir, renderRow, onToggleDir, onToggleFile }),
|
|
118
119
|
renderFoot({
|
|
119
120
|
picked,
|
|
@@ -182,7 +183,7 @@ function renderFoot(opts: FootOpts): React.ReactElement {
|
|
|
182
183
|
className: 'edrv-dl-go',
|
|
183
184
|
disabled: !picked.length,
|
|
184
185
|
onClick: () => onConfirm(picked),
|
|
185
|
-
}, '
|
|
186
|
+
}, '选择位置并下载 ' + picked.length + ' 个文件'),
|
|
186
187
|
)
|
|
187
188
|
}
|
|
188
189
|
|