dsh-vscode-mode 0.19.2 → 0.20.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 +6 -4
- package/lib/client.js +3868 -3486
- package/lib/client.js.map +1 -1
- package/lib/index.js +535 -42
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
- package/src/archiveDb.ts +8 -8
- package/src/capture.ts +2 -2
- package/src/client/state/diffSummary.ts +30 -0
- package/src/client/state/reviewFeed.ts +60 -0
- package/src/client/ui/ConversationDiffDock.ts +9 -10
- package/src/client/ui/DiffBadge.ts +8 -6
- package/src/client/ui/DiffBox.ts +18 -0
- package/src/client/ui/DiffLauncher.ts +21 -5
- package/src/client/ui/EditorView.ts +17 -5
- package/src/client/ui/OfficialReview.ts +93 -0
- package/src/index.ts +3 -0
- package/src/model.ts +7 -3
- package/src/officialCapture.ts +199 -0
- package/src/reviewDb.ts +214 -0
- package/src/reviewRpc.ts +33 -0
- package/src/rpc.ts +14 -18
- package/src/shared/review.ts +91 -0
- package/src/shared/rpc.ts +7 -0
- package/src/store.ts +2 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-vscode-mode",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.20.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": [
|
package/src/archiveDb.ts
CHANGED
|
@@ -33,7 +33,7 @@ type Row = Record<string, string | number | null>
|
|
|
33
33
|
* @param cwd 工作区绝对路径
|
|
34
34
|
* @returns 已缓存连接
|
|
35
35
|
*/
|
|
36
|
-
function dbFor(cwd: string): DatabaseSync {
|
|
36
|
+
export function dbFor(cwd: string): DatabaseSync {
|
|
37
37
|
const existing = connections.get(cwd)
|
|
38
38
|
if (existing) return existing
|
|
39
39
|
const file = archiveDbFile(cwd)
|
|
@@ -44,7 +44,7 @@ function dbFor(cwd: string): DatabaseSync {
|
|
|
44
44
|
db.exec('CREATE TABLE IF NOT EXISTS metadata (name TEXT PRIMARY KEY, value TEXT NOT NULL)')
|
|
45
45
|
const version = db.prepare("SELECT value FROM metadata WHERE name='schema'").get() as Row | undefined
|
|
46
46
|
const workspace = db.prepare("SELECT value FROM metadata WHERE name='cwd'").get() as Row | undefined
|
|
47
|
-
if ((version && version.value !== '2' && version.value !== '3') || (workspace && workspace.value !== cwd)) throw new Error('归档数据库 schema/工作区不匹配')
|
|
47
|
+
if ((version && version.value !== '2' && version.value !== '3' && version.value !== '4') || (workspace && workspace.value !== cwd)) throw new Error('归档数据库 schema/工作区不匹配')
|
|
48
48
|
if (!!version !== !!workspace) throw new Error('归档数据库元数据不完整')
|
|
49
49
|
if (!version) {
|
|
50
50
|
const other = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' AND name<>'metadata' LIMIT 1").get()
|
|
@@ -81,7 +81,7 @@ function dbFor(cwd: string): DatabaseSync {
|
|
|
81
81
|
throw error
|
|
82
82
|
}
|
|
83
83
|
}
|
|
84
|
-
if (version
|
|
84
|
+
if (version && (version.value === '3' || version.value === '4')) {
|
|
85
85
|
const active = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='active_records'").get()
|
|
86
86
|
const source = db.prepare("SELECT value FROM metadata WHERE name='active_source'").get()
|
|
87
87
|
if (!active || !source) throw new Error('活跃记录数据库元数据不完整')
|
|
@@ -105,7 +105,7 @@ function dbFor(cwd: string): DatabaseSync {
|
|
|
105
105
|
export async function migrateActive(cwd: string, source: string | null, confirm: () => Promise<string | null>): Promise<Map<string, DiffRecord>> {
|
|
106
106
|
const db = dbFor(cwd)
|
|
107
107
|
const version = String((db.prepare("SELECT value FROM metadata WHERE name='schema'").get() as Row).value)
|
|
108
|
-
if (version === '3') return readActive(cwd)
|
|
108
|
+
if (version === '3' || version === '4') return readActive(cwd)
|
|
109
109
|
const digest = source === null ? 'absent' : createHash('sha256').update(source).digest('hex')
|
|
110
110
|
let recs: Record<string, DiffRecord> = {}
|
|
111
111
|
if (source !== null) {
|
|
@@ -148,7 +148,7 @@ export async function migrateActive(cwd: string, source: string | null, confirm:
|
|
|
148
148
|
* @returns 迁移完成返回 true
|
|
149
149
|
*/
|
|
150
150
|
export function activeReady(cwd: string): boolean {
|
|
151
|
-
return (dbFor(cwd).prepare("SELECT value FROM metadata WHERE name='schema'").get() as Row).value
|
|
151
|
+
return ['3', '4'].includes(String((dbFor(cwd).prepare("SELECT value FROM metadata WHERE name='schema'").get() as Row).value))
|
|
152
152
|
}
|
|
153
153
|
|
|
154
154
|
/**
|
|
@@ -173,7 +173,7 @@ export function checkActiveSource(cwd: string, source: string | null): void {
|
|
|
173
173
|
*/
|
|
174
174
|
export function readActive(cwd: string): Map<string, DiffRecord> {
|
|
175
175
|
const db = dbFor(cwd)
|
|
176
|
-
if ((
|
|
176
|
+
if (!activeReady(cwd)) throw new Error('工作区活跃记录未迁移')
|
|
177
177
|
const rows = db.prepare('SELECT payload FROM active_records ORDER BY rowid').all() as Row[]
|
|
178
178
|
const recs = rows.map((row) => JSON.parse(String(row.payload)) as DiffRecord)
|
|
179
179
|
return new Map(recs.map((rec) => [rec.callId, rec]))
|
|
@@ -189,7 +189,7 @@ export function readActive(cwd: string): Map<string, DiffRecord> {
|
|
|
189
189
|
*/
|
|
190
190
|
export function commitActive(cwd: string, changes: ActiveChange, entries: ArchiveBatch[] = []): number {
|
|
191
191
|
const db = dbFor(cwd)
|
|
192
|
-
if ((
|
|
192
|
+
if (!activeReady(cwd)) throw new Error('工作区活跃记录未迁移')
|
|
193
193
|
const upsert = db.prepare(`INSERT INTO active_records(call_id,payload,path,archived,pending,at) VALUES(?,?,?,?,?,?)
|
|
194
194
|
ON CONFLICT(call_id) DO UPDATE SET payload=excluded.payload,path=excluded.path,
|
|
195
195
|
archived=excluded.archived,pending=excluded.pending,at=excluded.at`)
|
|
@@ -218,7 +218,7 @@ export function commitActive(cwd: string, changes: ActiveChange, entries: Archiv
|
|
|
218
218
|
*/
|
|
219
219
|
export function activeSummary(cwd: string): { active: number; pendingByFile: { path: string; pending: number }[] } {
|
|
220
220
|
const db = dbFor(cwd)
|
|
221
|
-
if ((
|
|
221
|
+
if (!activeReady(cwd)) throw new Error('工作区活跃记录未迁移')
|
|
222
222
|
const active = Number((db.prepare('SELECT count(*) AS n FROM active_records WHERE archived=0').get() as Row).n)
|
|
223
223
|
const rows = db.prepare('SELECT path, sum(pending) AS pending FROM active_records WHERE archived=0 AND pending>0 GROUP BY path ORDER BY pending DESC').all() as Row[]
|
|
224
224
|
return { active, pendingByFile: rows.map((row) => ({ path: String(row.path), pending: Number(row.pending) })) }
|
package/src/capture.ts
CHANGED
|
@@ -30,7 +30,7 @@ interface CaptureExec {
|
|
|
30
30
|
/** 捕获所需的工具结果最小形状(tools/result 的结构子集)。 */
|
|
31
31
|
interface CaptureResult {
|
|
32
32
|
isError?: unknown
|
|
33
|
-
value?: { path?: unknown; before?: unknown; after?: unknown } | null
|
|
33
|
+
value?: { path?: unknown; before?: unknown; after?: unknown; operation?: unknown } | null
|
|
34
34
|
meta?: { diffs?: unknown }
|
|
35
35
|
}
|
|
36
36
|
|
|
@@ -90,7 +90,7 @@ function buildLocalRecord(exec: CaptureExec, args: Record<string, unknown>, resu
|
|
|
90
90
|
if (typeof path !== 'string' || !path) return null
|
|
91
91
|
const before = value && typeof value.before === 'string' ? value.before : null
|
|
92
92
|
const after = value && typeof value.after === 'string' ? value.after : (typeof args.content === 'string' ? args.content : null)
|
|
93
|
-
const create = exec.name === 'write' &&
|
|
93
|
+
const create = exec.name === 'write' && value?.operation === 'create'
|
|
94
94
|
const callHunk = exec.name === 'edit' && typeof args.old_string === 'string' && typeof args.new_string === 'string'
|
|
95
95
|
? { oldText: args.old_string, newText: args.new_string }
|
|
96
96
|
: null
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/** Source-aware metadata projection; official patches never become legacy hunks. @author ddj 2026年10月26号 */
|
|
2
|
+
import type { ReviewPage } from '../../shared/review.js'
|
|
3
|
+
import type { RecordView } from '../../shared/types.js'
|
|
4
|
+
import type { FileSummary, Summary } from './records.js'
|
|
5
|
+
import { summarize } from './records.js'
|
|
6
|
+
|
|
7
|
+
export interface DiffFile extends FileSummary { sourceKind: 'official' | 'legacy' }
|
|
8
|
+
export interface DiffSummary extends Summary { files: DiffFile[]; pendingFiles: DiffFile[] }
|
|
9
|
+
|
|
10
|
+
/** @public @author ddj 2026年10月26号 @param path Functional local or remote identity. @returns Comparison key without rewriting remote case. */
|
|
11
|
+
export function diffPathKey(path: string): string {
|
|
12
|
+
if (/^[a-z]+:\/\//i.test(path)) return path
|
|
13
|
+
const normalized = path.replace(/\\/g, '/')
|
|
14
|
+
return /^[a-z]:\//i.test(normalized) ? normalized.toLowerCase() : normalized
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** @public @author ddj 2026年10月26号 @param path Current editor file. @param page Authoritative metadata, including decided paths. @returns Whether legacy actions must be hidden for this file. */
|
|
18
|
+
export function officialPath(path: string, page: ReviewPage | null): boolean {
|
|
19
|
+
const key = diffPathKey(path)
|
|
20
|
+
return !!page?.overview?.coveredPaths.some((candidate) => diffPathKey(candidate) === key)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** @public @author ddj 2026年10月26号 @param records Tool-call history. @param page Official file navigation, never only its first 50 rows. @returns Official-first file counts; legacy paths remain explicitly labelled. */
|
|
24
|
+
export function diffSummary(records: RecordView[], page: ReviewPage | null): DiffSummary {
|
|
25
|
+
const legacy = summarize(records.filter((record) => !officialPath(record.path, page)))
|
|
26
|
+
const files: DiffFile[] = (page?.overview?.files ?? []).map((file) => ({ ...file, recs: [] }))
|
|
27
|
+
for (const file of legacy.files) files.push({ ...file, sourceKind: 'legacy' })
|
|
28
|
+
const pendingFiles = files.filter((file) => file.pending > 0)
|
|
29
|
+
return { files, pendingFiles, totalFiles: pendingFiles.length }
|
|
30
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/** One official metadata request loop per viewed session, lazy historical patches. @author ddj 2026年10月26号 */
|
|
2
|
+
import { rpc } from '../rpc.js'
|
|
3
|
+
import type { ReviewPage } from '../../shared/review.js'
|
|
4
|
+
|
|
5
|
+
export interface ReviewFeed { page: ReviewPage | null; error: string }
|
|
6
|
+
type Listener = (feed: ReviewFeed) => void
|
|
7
|
+
interface Entry { listeners: Set<Listener>; value: ReviewFeed; controller: AbortController; timer?: ReturnType<typeof setTimeout>; loading: boolean; again: boolean; onRefresh: () => void }
|
|
8
|
+
const feeds = new Map<string, Entry>()
|
|
9
|
+
|
|
10
|
+
/** @private @author ddj 2026年10月26号 @param entry Shared feed. Notify mounted observers only. */
|
|
11
|
+
function notify(entry: Entry): void { for (const listener of entry.listeners) listener(entry.value) }
|
|
12
|
+
|
|
13
|
+
/** @private @author ddj 2026年10月26号 @param sessionId Scope. @param entry Current generation. @returns First-page metadata update; no overlap or late publication. */
|
|
14
|
+
async function refresh(sessionId: string, entry: Entry): Promise<void> {
|
|
15
|
+
if (entry.controller.signal.aborted) return
|
|
16
|
+
if (entry.loading) { entry.again = true; return }
|
|
17
|
+
clearTimeout(entry.timer)
|
|
18
|
+
entry.loading = true
|
|
19
|
+
entry.again = false
|
|
20
|
+
try {
|
|
21
|
+
const result = await rpc('edrv.review.list', { sessionId }, entry.controller.signal)
|
|
22
|
+
if (!entry.controller.signal.aborted) entry.value = result.ok
|
|
23
|
+
? { page: result.page, error: result.page.overview ? '' : 'Host 尚未提供官方文件导航,请重启加载新版插件;当前仅可浏览已返回的官方历史,角标不含官方记录。' }
|
|
24
|
+
: { ...entry.value, error: result.error }
|
|
25
|
+
} catch (error) { if (!entry.controller.signal.aborted) entry.value = { ...entry.value, error: String(error) } }
|
|
26
|
+
finally {
|
|
27
|
+
entry.loading = false
|
|
28
|
+
if (!entry.controller.signal.aborted) {
|
|
29
|
+
notify(entry)
|
|
30
|
+
if (entry.again) void refresh(sessionId, entry)
|
|
31
|
+
else entry.timer = setTimeout(() => { void refresh(sessionId, entry) }, 5000)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** @public @author ddj 2026年10月26号 @param sessionId Live scope. @param listener Mounted observer. @returns Cleanup cancelling requests and timers when unused. */
|
|
37
|
+
export function subscribeReview(sessionId: string, listener: Listener): () => void {
|
|
38
|
+
let entry = feeds.get(sessionId)
|
|
39
|
+
if (!entry) {
|
|
40
|
+
/** @private @author ddj 2026年10月26号 Refresh the shared official metadata after edits/decisions. */
|
|
41
|
+
const onRefresh = (): void => refreshReview(sessionId)
|
|
42
|
+
entry = { listeners: new Set(), value: { page: null, error: '' }, controller: new AbortController(), loading: false, again: false, onRefresh }
|
|
43
|
+
feeds.set(sessionId, entry)
|
|
44
|
+
if (typeof window !== 'undefined') window.addEventListener('edrv:refresh', onRefresh)
|
|
45
|
+
void refresh(sessionId, entry)
|
|
46
|
+
}
|
|
47
|
+
entry.listeners.add(listener); listener(entry.value)
|
|
48
|
+
return () => {
|
|
49
|
+
entry.listeners.delete(listener)
|
|
50
|
+
if (entry.listeners.size) return
|
|
51
|
+
entry.controller.abort(); clearTimeout(entry.timer); feeds.delete(sessionId)
|
|
52
|
+
if (typeof window !== 'undefined') window.removeEventListener('edrv:refresh', entry.onRefresh)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** @public @author ddj 2026年10月26号 @param sessionId Refresh metadata after a decision; does not alter legacy or disk. */
|
|
57
|
+
export function refreshReview(sessionId: string): void {
|
|
58
|
+
const entry = feeds.get(sessionId)
|
|
59
|
+
if (entry) { clearTimeout(entry.timer); void refresh(sessionId, entry) }
|
|
60
|
+
}
|
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import React from 'react'
|
|
9
9
|
import { openDiffView } from '../events.js'
|
|
10
|
-
import {
|
|
10
|
+
import { diffSummary } from '../state/diffSummary.js'
|
|
11
|
+
import { subscribeReview } from '../state/reviewFeed.js'
|
|
11
12
|
import { subscribeRecords } from '../state/recordsFeed.js'
|
|
12
13
|
import { nextDiffPath } from '../diffDock.js'
|
|
13
14
|
import { dockInLayout, readDiffDock, subscribeDiffDock } from '../diffDockStore.js'
|
|
@@ -33,9 +34,10 @@ export function ConversationDiffDock(props) {
|
|
|
33
34
|
return undefined
|
|
34
35
|
}
|
|
35
36
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
})
|
|
37
|
+
let records = [], page = null
|
|
38
|
+
const offRecords = subscribeRecords(sessionId, (next) => { records = next; setSummary(diffSummary(records, page)) })
|
|
39
|
+
const offReview = subscribeReview(sessionId, (feed) => { page = feed.page; setSummary(diffSummary(records, page)) })
|
|
40
|
+
return () => { offRecords(); offReview() }
|
|
39
41
|
}, [sessionId])
|
|
40
42
|
|
|
41
43
|
React.useEffect(() => {
|
|
@@ -73,11 +75,8 @@ export function ConversationDiffDock(props) {
|
|
|
73
75
|
}
|
|
74
76
|
|
|
75
77
|
return React.createElement(React.Fragment, null,
|
|
78
|
+
palette,
|
|
76
79
|
renderDock(React.createElement(DiffBox, {
|
|
77
|
-
mode: 'chat',
|
|
78
|
-
|
|
79
|
-
fileTotal: paths.length,
|
|
80
|
-
onOpenNextFile: click,
|
|
81
|
-
}), sessionId || 'chat'),
|
|
82
|
-
palette)
|
|
80
|
+
mode: 'chat', dock: true, fileTotal: paths.length, onOpenNextFile: click,
|
|
81
|
+
}), sessionId || 'chat'))
|
|
83
82
|
}
|
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
import React from 'react'
|
|
8
8
|
import { appUrl } from '../../shared/appUrl.js'
|
|
9
9
|
import { openEditorView, emitShowLauncher } from '../events.js'
|
|
10
|
-
import {
|
|
10
|
+
import { diffSummary } from '../state/diffSummary.js'
|
|
11
|
+
import { subscribeReview } from '../state/reviewFeed.js'
|
|
11
12
|
import { subscribeRecords } from '../state/recordsFeed.js'
|
|
12
13
|
|
|
13
14
|
/**
|
|
@@ -23,13 +24,14 @@ export function DiffBadge(props) {
|
|
|
23
24
|
return undefined
|
|
24
25
|
}
|
|
25
26
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
})
|
|
27
|
+
let records = [], page = null
|
|
28
|
+
const offRecords = subscribeRecords(sessionId, (next) => { records = next; setSummary(diffSummary(records, page)) })
|
|
29
|
+
const offReview = subscribeReview(sessionId, (feed) => { page = feed.page; setSummary(diffSummary(records, page)) })
|
|
30
|
+
return () => { offRecords(); offReview() }
|
|
29
31
|
}, [sessionId])
|
|
30
32
|
|
|
31
33
|
const n = summary?.totalFiles ?? 0
|
|
32
|
-
if (!
|
|
34
|
+
if (!sessionId) return null
|
|
33
35
|
|
|
34
36
|
const click = () => {
|
|
35
37
|
// 侧栏形态:带首个待处理文件路径(内容打开自动展开面板);旧形态行为不变
|
|
@@ -39,7 +41,7 @@ export function DiffBadge(props) {
|
|
|
39
41
|
|
|
40
42
|
return React.createElement('button', {
|
|
41
43
|
onClick: click,
|
|
42
|
-
title: n + '
|
|
44
|
+
title: '差异审查:' + n + ' 个待处理文件;点击查看总览与历史',
|
|
43
45
|
style: { position: 'relative', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', width: 30, height: 30, padding: 0, background: 'transparent', border: 'none', borderRadius: 6, cursor: 'pointer' },
|
|
44
46
|
},
|
|
45
47
|
React.createElement('img', { src: appUrl('/edrv/assets/compare-select.png'), alt: '差异', style: { width: 22, height: 22, display: 'block' } }),
|
package/src/client/ui/DiffBox.ts
CHANGED
|
@@ -8,6 +8,7 @@ import React from 'react'
|
|
|
8
8
|
import { callIdAttr } from '../state/records.js'
|
|
9
9
|
import { canDecideFile, diffDockText } from '../diffDock.js'
|
|
10
10
|
import { badgeOf } from './shared.js'
|
|
11
|
+
import { ReviewLedger } from './OfficialReview.js'
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* 折叠控制使用任务栏同尺寸的 chevron 轮廓,避免依赖未声明的运行时图标包。
|
|
@@ -65,6 +66,23 @@ export function DiffBox(props) {
|
|
|
65
66
|
}, diffDockText(fileTotal))))
|
|
66
67
|
}
|
|
67
68
|
|
|
69
|
+
if (props.official) {
|
|
70
|
+
const official = props.official
|
|
71
|
+
return React.createElement('div', { className: 'edrv-diffbar edrv-diffbar-dock', 'data-edrv-diffbox': '1', 'data-edrv-diff-source': 'official' },
|
|
72
|
+
React.createElement('div', { className: 'edrv-diffbar-row' },
|
|
73
|
+
React.createElement('button', { className: 'edrv-pill edrv-pill-ghost', onClick: onPrevFile, disabled: !fileTotal }, '←'),
|
|
74
|
+
React.createElement('span', null, base + ' · ' + official.pending + ' 条官方待审记录'),
|
|
75
|
+
React.createElement('button', { className: 'edrv-pill edrv-pill-ghost', onClick: onNextFile, disabled: !fileTotal }, '→'),
|
|
76
|
+
React.createElement('button', { className: 'edrv-pill edrv-pill-ghost', onClick: () => setExpanded(!expanded), 'aria-expanded': expanded }, expanded ? '收起差异' : '查看差异 / 标记已审'),
|
|
77
|
+
React.createElement('button', { className: 'edrv-pill edrv-pill-ghost', disabled: true, title: '官方仅提供差异块,缺少可验证全文,不能安全回滚。' }, 'Undo 不可用'),
|
|
78
|
+
React.createElement('button', { className: 'edrv-pill edrv-pill-ghost', onClick: () => onOpenLauncher('pending') }, '差异总览'),
|
|
79
|
+
React.createElement('button', { className: 'edrv-pill edrv-pill-ghost', onClick: () => onOpenLauncher('archive') }, '归档 / 历史')),
|
|
80
|
+
props.reviewError ? React.createElement('div', { role: 'alert' }, props.reviewError) : null,
|
|
81
|
+
expanded ? React.createElement('div', { className: 'edrv-diffbar-body' },
|
|
82
|
+
React.createElement(ReviewLedger, { key: official.sessionId + ':' + official.path, sessionId: official.sessionId,
|
|
83
|
+
first: official.page, path: official.path })) : null)
|
|
84
|
+
}
|
|
85
|
+
|
|
68
86
|
let bodyEl = null
|
|
69
87
|
if (expanded) {
|
|
70
88
|
const rows = pendingRegions.map((r) => {
|
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
import React from 'react'
|
|
8
8
|
import { rpc } from '../rpc.js'
|
|
9
9
|
import { MONO, btn } from './shared.js'
|
|
10
|
+
import { ReviewLedger } from './OfficialReview.js'
|
|
11
|
+
import { subscribeReview } from '../state/reviewFeed.js'
|
|
10
12
|
|
|
11
13
|
/**
|
|
12
14
|
* 文件与批次共同标识归档卡片,避免同文件多批次共用展开状态。
|
|
@@ -57,6 +59,12 @@ export function DiffLauncher(props) {
|
|
|
57
59
|
const detailRef = React.useRef(0)
|
|
58
60
|
const moreRef = React.useRef(false)
|
|
59
61
|
const [error, setError] = React.useState(null)
|
|
62
|
+
const [source, setSource] = React.useState('official')
|
|
63
|
+
const [review, setReview] = React.useState({ page: null, error: '' })
|
|
64
|
+
React.useEffect(() => {
|
|
65
|
+
setReview({ page: null, error: '' })
|
|
66
|
+
return subscribeReview(sessionId, setReview)
|
|
67
|
+
}, [sessionId])
|
|
60
68
|
|
|
61
69
|
/** @author ddj 2026年09月30号 @description 仅加载一个归档摘要页并丢弃失效响应。 */
|
|
62
70
|
const loadArchives = (cursor = null) => {
|
|
@@ -92,16 +100,22 @@ export function DiffLauncher(props) {
|
|
|
92
100
|
moreRef.current = false
|
|
93
101
|
setMoreBusy(false)
|
|
94
102
|
setArchiveDetail(null); setArchivePath(null); setArchiveErr(null)
|
|
95
|
-
if (tab === 'archive') loadArchives()
|
|
103
|
+
if (tab === 'archive' && source === 'legacy') loadArchives()
|
|
96
104
|
else { setArchives(null); setNextCursor(null) }
|
|
97
105
|
return () => { requestRef.current++; detailRef.current++ }
|
|
98
|
-
}, [tab, sessionId])
|
|
106
|
+
}, [tab, sessionId, source])
|
|
99
107
|
|
|
100
108
|
let body
|
|
101
|
-
|
|
102
|
-
|
|
109
|
+
const legacyFiles = sum.pendingFiles.filter((file) => file.sourceKind !== 'official')
|
|
110
|
+
if (source === 'official') {
|
|
111
|
+
body = React.createElement(React.Fragment, null,
|
|
112
|
+
React.createElement('div', { className: 'edrv-search-empty' }, '官方轮次净差异 · 原始历史只读;编辑按钮打开当前文件,保存不代表已审。'),
|
|
113
|
+
review.error ? React.createElement('div', { role: 'alert' }, review.error) : null,
|
|
114
|
+
React.createElement(ReviewLedger, { key: sessionId + ':' + tab, sessionId, first: review.page, history: tab === 'archive' }))
|
|
115
|
+
} else if (tab === 'pending') {
|
|
116
|
+
body = (legacyFiles.length === 0
|
|
103
117
|
? React.createElement('div', { className: 'edrv-search-empty' }, '暂无待处理差异(已全部处理)')
|
|
104
|
-
:
|
|
118
|
+
: legacyFiles.map((f) => React.createElement('div', { key: f.path, className: 'edrv-launch-row', title: '打开并跳转到差异 ' + (f.label ?? f.path), onClick: () => onOpenFile(f.path) },
|
|
105
119
|
React.createElement('span', { className: 'edrv-launch-path' }, f.label ?? f.path),
|
|
106
120
|
React.createElement('span', { className: 'edrv-launch-cnt' }, '待处理 ' + f.pending + ' 处'),
|
|
107
121
|
React.createElement('button', { className: 'edrv-btn-mini edrv-btn-keep', onClick: (e) => { e.stopPropagation(); onOpenFile(f.path) } }, '打开'))))
|
|
@@ -178,6 +192,8 @@ export function DiffLauncher(props) {
|
|
|
178
192
|
React.createElement('span', { style: { fontSize: 12, fontWeight: 600, color: 'var(--dsw-alias-label-primary,#eee)', marginRight: 4 } }, '差异'),
|
|
179
193
|
React.createElement('button', { className: 'edrv-launch-tab' + (tab === 'pending' ? ' on' : ''), onClick: () => setTab('pending') }, '待处理' + (sum.totalFiles ? ' (' + sum.totalFiles + ')' : '')),
|
|
180
194
|
React.createElement('button', { className: 'edrv-launch-tab' + (tab === 'archive' ? ' on' : ''), onClick: () => setTab('archive') }, '归档'),
|
|
195
|
+
React.createElement('button', { className: 'edrv-launch-tab' + (source === 'official' ? ' on' : ''), onClick: () => setSource('official') }, '官方净差异'),
|
|
196
|
+
React.createElement('button', { className: 'edrv-launch-tab' + (source === 'legacy' ? ' on' : ''), onClick: () => setSource('legacy') }, '旧工具记录'),
|
|
181
197
|
React.createElement('span', { style: { flex: 1 } }),
|
|
182
198
|
(error ? React.createElement('span', { style: { fontSize: 11, color: 'var(--dsw-alias-state-error-primary,#d9534f)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', maxWidth: '40%' } }, '错误:' + String(error)) : null),
|
|
183
199
|
React.createElement('button', { onClick: onClose, style: btn(false) }, '✕ 关闭')),
|
|
@@ -22,6 +22,8 @@ import { acceptsLoad, receiptErrorFor, receiptPath, receiptReadyFor, receiptTabP
|
|
|
22
22
|
import { bindPanelCommand } from '../panelCommands.js'
|
|
23
23
|
import { dbg, isDebug, rpc } from '../rpc.js'
|
|
24
24
|
import { refreshFeed, subscribeErrors, subscribeRecords } from '../state/recordsFeed.js'
|
|
25
|
+
import { subscribeReview } from '../state/reviewFeed.js'
|
|
26
|
+
import { diffSummary, officialPath } from '../state/diffSummary.js'
|
|
25
27
|
import { emitFileChanged, emitRefresh } from '../events.js'
|
|
26
28
|
import { langOf, loadMonaco, snippetLanguageOf } from '../monaco/loader.js'
|
|
27
29
|
import { bindEditActions, runEditAction } from '../monaco/editActions.js'
|
|
@@ -256,6 +258,12 @@ export function EditorView(props) {
|
|
|
256
258
|
const [monaco, setMonaco] = React.useState(null)
|
|
257
259
|
const [monacoErr, setMonacoErr] = React.useState(null)
|
|
258
260
|
const [records, setRecords] = React.useState({})
|
|
261
|
+
const [review, setReview] = React.useState({ page: null, error: '' })
|
|
262
|
+
React.useEffect(() => {
|
|
263
|
+
setReview({ page: null, error: '' })
|
|
264
|
+
if (!sessionId) return undefined
|
|
265
|
+
return subscribeReview(sessionId, setReview)
|
|
266
|
+
}, [sessionId])
|
|
259
267
|
const [tabs, setTabs] = React.useState([])
|
|
260
268
|
const [active, setDiskActive] = React.useState(null)
|
|
261
269
|
const [planTabs, setPlanTabs] = React.useState([])
|
|
@@ -531,11 +539,11 @@ export function EditorView(props) {
|
|
|
531
539
|
const currentRecords = React.useMemo(() => {
|
|
532
540
|
const list = []
|
|
533
541
|
for (const rec of Object.values(records)) {
|
|
534
|
-
if (rec.archived === true) continue
|
|
542
|
+
if (rec.archived === true || officialPath(rec.path, review.page)) continue
|
|
535
543
|
if (sameFile(rec.path, active)) list.push(rec)
|
|
536
544
|
}
|
|
537
545
|
return list
|
|
538
|
-
}, [records, active, cwd])
|
|
546
|
+
}, [records, active, cwd, review.page])
|
|
539
547
|
|
|
540
548
|
const contentReady = content !== null && contentPath === active
|
|
541
549
|
// 当前 tab 是否处于图片预览模式(与 loadContent 的分派条件同源;SVG 文本模式时为 false)
|
|
@@ -575,7 +583,7 @@ export function EditorView(props) {
|
|
|
575
583
|
return map
|
|
576
584
|
}, [pendingRegions])
|
|
577
585
|
lineRegionMapRef.current = lineRegionMap
|
|
578
|
-
const sum = React.useMemo(() =>
|
|
586
|
+
const sum = React.useMemo(() => diffSummary(Object.values(records), review.page), [records, review.page])
|
|
579
587
|
|
|
580
588
|
/** @private @author ddj 2026年10月08号 @param path Disk path. @param select Select tab. @param preview Optional explicit Markdown mode. */
|
|
581
589
|
function addTab(path, select, preview) {
|
|
@@ -3132,7 +3140,7 @@ export function EditorView(props) {
|
|
|
3132
3140
|
const allPending = React.useMemo(() => {
|
|
3133
3141
|
const out = []
|
|
3134
3142
|
for (const rec of Object.values(records)) {
|
|
3135
|
-
if (rec.superseded === true) continue
|
|
3143
|
+
if (rec.superseded === true || officialPath(rec.path, review.page)) continue
|
|
3136
3144
|
const perHunk = Array.isArray(rec.decisions?.perHunk) ? rec.decisions.perHunk : []
|
|
3137
3145
|
const hunks = Array.isArray(rec.hunks) ? rec.hunks : []
|
|
3138
3146
|
for (let i = 0; i < hunks.length; i++) {
|
|
@@ -3141,7 +3149,7 @@ export function EditorView(props) {
|
|
|
3141
3149
|
}
|
|
3142
3150
|
}
|
|
3143
3151
|
return out
|
|
3144
|
-
}, [records])
|
|
3152
|
+
}, [records, review.page])
|
|
3145
3153
|
|
|
3146
3154
|
/**
|
|
3147
3155
|
* 批量处理所有差异文件:采纳全部 / 不采纳全部。不采纳按 at 降序(新改先回滚)排序后一次 RPC。
|
|
@@ -4318,8 +4326,12 @@ export function EditorView(props) {
|
|
|
4318
4326
|
* @author ddj 2026年09月29号
|
|
4319
4327
|
* @returns 含布局及操作回调的差异快照
|
|
4320
4328
|
*/
|
|
4329
|
+
const officialFile = review.page?.overview?.coveredPaths.find((path) => sameFile(path, active))
|
|
4321
4330
|
const buildDockSnapshot = () => ({
|
|
4322
4331
|
mode: editorDockMode(active),
|
|
4332
|
+
official: officialFile ? { sessionId, page: review.page, path: officialFile,
|
|
4333
|
+
pending: sum.files.find((file) => sameFile(file.path, active))?.pending ?? 0 } : null,
|
|
4334
|
+
reviewError: review.error,
|
|
4323
4335
|
layout,
|
|
4324
4336
|
pendingRegions,
|
|
4325
4337
|
staleRegions,
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/** Official immutable patch ledger; current files use the existing editor. @author ddj 2026年10月26号 */
|
|
2
|
+
import React from 'react'
|
|
3
|
+
import { rpc } from '../rpc.js'
|
|
4
|
+
import { openEditorView } from '../events.js'
|
|
5
|
+
import { refreshReview } from '../state/reviewFeed.js'
|
|
6
|
+
import type { ReviewDetail, ReviewItem, ReviewPage } from '../../shared/review.js'
|
|
7
|
+
const h = React.createElement
|
|
8
|
+
/** @private @author ddj 2026年10月26号 @param props Latest first-page metadata. @returns Paginated rows independent of expanded historical patch. */
|
|
9
|
+
export function ReviewLedger(props: { sessionId: string; first: ReviewPage | null; history?: boolean; path?: string }) {
|
|
10
|
+
const [page, setPage] = React.useState<ReviewPage | null>(props.first)
|
|
11
|
+
const [history, setHistory] = React.useState(props.history ?? false)
|
|
12
|
+
const [cursor, setCursor] = React.useState(0)
|
|
13
|
+
const [detail, setDetail] = React.useState<ReviewDetail | null>(null)
|
|
14
|
+
const [error, setError] = React.useState('')
|
|
15
|
+
const selected = React.useRef<AbortController | null>(null)
|
|
16
|
+
React.useEffect(() => () => selected.current?.abort(), [])
|
|
17
|
+
React.useEffect(() => {
|
|
18
|
+
const controller = new AbortController()
|
|
19
|
+
if (!history && !props.path && cursor === 0) { setPage(props.first); return () => controller.abort() }
|
|
20
|
+
void rpc('edrv.review.list', { sessionId: props.sessionId, history, cursor, path: props.path }, controller.signal).then((result) => {
|
|
21
|
+
if (controller.signal.aborted) return
|
|
22
|
+
if (result.ok) { setPage(result.page); setError('') }
|
|
23
|
+
else setError(result.error)
|
|
24
|
+
}).catch((cause) => { if (!controller.signal.aborted) setError(String(cause)) })
|
|
25
|
+
return () => controller.abort()
|
|
26
|
+
}, [props.sessionId, props.first, props.path, cursor, history])
|
|
27
|
+
|
|
28
|
+
/** @private @author ddj 2026年10月26号 @param item Frozen version identity; patch is never built from disk. */
|
|
29
|
+
async function select(item: ReviewItem): Promise<void> {
|
|
30
|
+
selected.current?.abort()
|
|
31
|
+
const controller = new AbortController()
|
|
32
|
+
selected.current = controller
|
|
33
|
+
try {
|
|
34
|
+
const result = await rpc('edrv.review.detail', { sessionId: props.sessionId, id: item.id }, controller.signal)
|
|
35
|
+
if (controller.signal.aborted) return
|
|
36
|
+
if (result.ok) { setDetail(result.detail); setError('') }
|
|
37
|
+
else setError(result.error)
|
|
38
|
+
} catch (cause) { if (!controller.signal.aborted) setError(String(cause)) }
|
|
39
|
+
}
|
|
40
|
+
return h('div', { style: { marginTop: 8 } },
|
|
41
|
+
h('label', null, h('input', { type: 'checkbox', checked: history, onChange: () => { setHistory(!history); setCursor(0) } }), '显示已处理及被替换的历史'),
|
|
42
|
+
page?.gaps.map((gap) => h('div', { key: `${gap.sessionId}:${gap.seq}`, role: 'status' }, `轮次 ${gap.turn} 采集不完整:${gap.error}`)),
|
|
43
|
+
error ? h('div', { role: 'alert' }, error) : null,
|
|
44
|
+
!page ? h('p', { role: 'status' }, '正在读取官方审查记录…') : null,
|
|
45
|
+
page && !page.items.length ? h('p', null, history ? '暂无已落盘历史。' : '没有待处理的官方记录;可切换查看历史。') : null,
|
|
46
|
+
h('div', { style: { maxHeight: 210, overflow: 'auto' } }, page?.items.map((item) => h('button', {
|
|
47
|
+
key: item.id, type: 'button', onClick: () => { void select(item) },
|
|
48
|
+
style: { display: 'block', width: '100%', textAlign: 'left', padding: '6px 4px' },
|
|
49
|
+
}, `轮次 ${item.turn} · ${item.display} +${item.added} −${item.deleted} · ${item.decision}${item.current ? '' : '(已被同轮新版本替换)'}`))),
|
|
50
|
+
cursor > 0 ? h('button', { type: 'button', onClick: () => setCursor(Math.max(0, cursor - 50)) }, '上一页') : null,
|
|
51
|
+
page?.nextCursor !== null && page?.nextCursor !== undefined ? h('button', { type: 'button', onClick: () => setCursor(page.nextCursor!) }, '下一页') : null,
|
|
52
|
+
detail ? h(PatchDetail, { key: detail.item.id, sessionId: props.sessionId, detail, onChanged: () => { setDetail(null); refreshReview(props.sessionId) } }) : null)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** @private @author ddj 2026年10月26号 @param props Immutable patch and current review revision. @returns Read-only source with explicit capability gates. */
|
|
56
|
+
function PatchDetail(props: { sessionId: string; detail: ReviewDetail; onChanged(): void }) {
|
|
57
|
+
const [error, setError] = React.useState('')
|
|
58
|
+
const [busy, setBusy] = React.useState(false)
|
|
59
|
+
const [operationId] = React.useState(() => crypto.randomUUID())
|
|
60
|
+
const lifetime = React.useRef<AbortController | null>(null)
|
|
61
|
+
React.useEffect(() => {
|
|
62
|
+
lifetime.current = new AbortController()
|
|
63
|
+
return () => lifetime.current?.abort()
|
|
64
|
+
}, [])
|
|
65
|
+
const { item, diff, capability } = props.detail
|
|
66
|
+
/** @private @author ddj 2026年10月26号 Marks reviewed only; never writes disk. Stable operation id survives retries while mounted. */
|
|
67
|
+
async function keep(): Promise<void> {
|
|
68
|
+
setBusy(true)
|
|
69
|
+
try {
|
|
70
|
+
const result = await rpc('edrv.review.decide', { sessionId: props.sessionId, id: item.id, seq: item.seq, revision: item.revision,
|
|
71
|
+
operationId, decision: 'accepted' }, lifetime.current?.signal)
|
|
72
|
+
if (lifetime.current?.signal.aborted) return
|
|
73
|
+
if (result.ok) props.onChanged()
|
|
74
|
+
else setError(result.error)
|
|
75
|
+
} catch (cause) { if (!lifetime.current?.signal.aborted) setError(String(cause)) }
|
|
76
|
+
finally { if (!lifetime.current?.signal.aborted) setBusy(false) }
|
|
77
|
+
}
|
|
78
|
+
return h('article', { style: { marginTop: 10, borderTop: '1px solid var(--border-color, #d9dee7)', paddingTop: 8 } },
|
|
79
|
+
h('strong', null, `${item.display} · 轮次 ${item.turn} / 事件 ${item.seq}`),
|
|
80
|
+
h('div', null, `来源会话 ${item.sessionId} · 采集于 ${props.detail.capturedAt}`),
|
|
81
|
+
h('p', null, capability.reason),
|
|
82
|
+
diff.kind === 'text' ? h('div', null,
|
|
83
|
+
h('div', null, `${diff.before ? '原文件存在' : '原文件不存在'} / ${diff.after ? '结果文件存在' : '结果文件不存在'}${diff.coarse ? ' · 粗粒度比较' : ''}`),
|
|
84
|
+
h('pre', { style: { overflow: 'auto', maxHeight: 400, fontSize: 11, lineHeight: 1.6 }, 'aria-label': '官方历史差异(只读)' },
|
|
85
|
+
diff.hunks.map((chunk, index) => h('div', { key: index },
|
|
86
|
+
h('div', null, `@@ -${chunk.oldStart},${chunk.oldLines} +${chunk.newStart},${chunk.newLines} @@`),
|
|
87
|
+
chunk.lines.map((line, position) => h('div', { key: position, style: { background: line.startsWith('+') ? 'light-dark(#e5f5e8, #173522)' : line.startsWith('-') ? 'light-dark(#fce9e9, #452323)' : undefined } }, line)))))
|
|
88
|
+
) : h('p', null, diff.kind === 'binary' ? '二进制文件:官方不提供文本差异。' : '文件超过官方大小限制:没有差异内容。'),
|
|
89
|
+
error ? h('div', { role: 'alert' }, error) : null,
|
|
90
|
+
h('button', { type: 'button', disabled: busy || !capability.keep, onClick: () => { void keep() } }, '标记已审(不改文件)'),
|
|
91
|
+
h('button', { type: 'button', disabled: true, title: capability.reason }, '拒绝写回不可用'),
|
|
92
|
+
h('button', { type: 'button', onClick: () => openEditorView(item.currentPath) }, '编辑当前文件'))
|
|
93
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/** dsh-vscode-mode Host composition. @author ddj 2026年09月28号 */
|
|
2
2
|
import { registerRoutes } from './routes.js'
|
|
3
3
|
import { captureToolResult } from './capture.js'
|
|
4
|
+
import { installCapture } from './officialCapture.js'
|
|
4
5
|
import { closeArchiveDbs } from './archiveDb.js'
|
|
5
6
|
import { handleRpc, integrationBaseUrlOf } from './rpc.js'
|
|
6
7
|
import { newSearcher } from './search/orchestrator.js'
|
|
@@ -67,6 +68,7 @@ function createRuntime(ctx: Ctx, config: unknown) {
|
|
|
67
68
|
* @param runtime Managers created by this instance.
|
|
68
69
|
*/
|
|
69
70
|
function attachRuntime(ctx: Ctx, runtime: Runtime): void {
|
|
71
|
+
const stopCapture = installCapture(ctx)
|
|
70
72
|
/** @author ddj 2026年09月28号 @param exec Tool execution. @param result Tool result. */
|
|
71
73
|
function onResult(exec: unknown, result: unknown): void {
|
|
72
74
|
void captureToolResult(ctx, runtime.registry, exec, result)
|
|
@@ -84,6 +86,7 @@ function attachRuntime(ctx: Ctx, runtime: Runtime): void {
|
|
|
84
86
|
}
|
|
85
87
|
/** @author ddj 2026年09月28号 Release every manager even if another cleanup rejects. */
|
|
86
88
|
async function cleanup(): Promise<void> {
|
|
89
|
+
await stopCapture()
|
|
87
90
|
runtime.fileVersions.dispose()
|
|
88
91
|
closeArchiveDbs()
|
|
89
92
|
disposeAllServers()
|
package/src/model.ts
CHANGED
|
@@ -277,11 +277,15 @@ export function fileMaxBatch(records: Map<string, DiffRecord>, path: string): nu
|
|
|
277
277
|
return m
|
|
278
278
|
}
|
|
279
279
|
|
|
280
|
-
/**
|
|
280
|
+
/** @public @author ddj 2026年10月26号 @param map Legacy records. @param max Cache target, never a pending retention limit. */
|
|
281
281
|
export function prune(map: Map<string, DiffRecord>, max = MAX_RECORDS): void {
|
|
282
282
|
if (map.size <= max) return
|
|
283
|
-
const sorted = [...map.values()].
|
|
284
|
-
|
|
283
|
+
const sorted = [...map.values()].filter((record) => record.archived && recordResolved(record))
|
|
284
|
+
.sort((a, b) => (a.at < b.at ? -1 : 1))
|
|
285
|
+
for (const record of sorted) {
|
|
286
|
+
if (map.size <= max) break
|
|
287
|
+
map.delete(record.callId)
|
|
288
|
+
}
|
|
285
289
|
}
|
|
286
290
|
|
|
287
291
|
/** 按批次分组记录。 */
|