dsh-vscode-mode 0.19.2 → 0.20.1
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 +26 -15
- package/lib/client.js +4427 -3510
- package/lib/client.js.map +1 -1
- package/lib/index.js +937 -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/monaco/diffRender.ts +2 -2
- package/src/client/reviewBatch.ts +64 -0
- package/src/client/state/diffSummary.ts +30 -0
- package/src/client/state/reviewFeed.ts +64 -0
- package/src/client/styles/editor.css +39 -0
- package/src/client/ui/ConversationDiffDock.ts +9 -10
- package/src/client/ui/DiffBadge.ts +8 -6
- package/src/client/ui/DiffBox.ts +3 -0
- package/src/client/ui/DiffLauncher.ts +21 -5
- package/src/client/ui/EditorView.ts +163 -13
- package/src/client/ui/OfficialDiffBar.tsx +112 -0
- package/src/client/ui/OfficialReview.ts +93 -0
- package/src/client-primitives.d.ts +22 -0
- package/src/index.ts +6 -0
- package/src/model.ts +7 -3
- package/src/officialCapture.ts +203 -0
- package/src/reviewCapture.ts +112 -0
- package/src/reviewDb.ts +285 -0
- package/src/reviewEvidence.ts +70 -0
- package/src/reviewRpc.ts +42 -0
- package/src/reviewUndo.ts +104 -0
- package/src/rpc.ts +14 -18
- package/src/shared/review.ts +95 -0
- package/src/shared/reviewRegions.ts +74 -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.1",
|
|
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
|
|
@@ -5,7 +5,7 @@ import { detectColorScheme, officialThemeOf, themeNameOf } from './theme.js'
|
|
|
5
5
|
// #region 显示数据
|
|
6
6
|
/** @private @author ddj 2026年10月09号 @param regions 待显示区域 @returns 内容敏感且无分隔符碰撞的展示键 */
|
|
7
7
|
function renderKey(regions) {
|
|
8
|
-
return JSON.stringify(regions.map((r) => [r.callId, r.idx, r.start, r.end, r.oldStart, r.create, r.oldLines, r.newLines]))
|
|
8
|
+
return JSON.stringify(regions.map((r) => [r.recordId ?? r.callId, r.id ?? r.idx, r.start, r.end, r.oldStart, r.create, r.oldLines, r.newLines]))
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
/** @private @author ddj 2026年10月09号 @param node 接收样式的节点 @param colors Monaco 颜色 @returns 设置插件局部变量;不覆盖官方令牌 */
|
|
@@ -48,7 +48,7 @@ function marginRow(line) {
|
|
|
48
48
|
function zoneOf(region) {
|
|
49
49
|
const domNode = document.createElement('div')
|
|
50
50
|
domNode.className = 'edrv-del-zone'
|
|
51
|
-
domNode.dataset.edrvHunk = String(region.callId) + ':' + region.idx
|
|
51
|
+
domNode.dataset.edrvHunk = String(region.recordId ?? region.callId) + ':' + (region.id ?? region.idx)
|
|
52
52
|
const marginDomNode = document.createElement('div')
|
|
53
53
|
marginDomNode.className = 'edrv-del-margin'
|
|
54
54
|
const rows = []
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/** Official bulk decisions use persisted metadata, never the visible history page. @author ddj 2026年10月27号 */
|
|
2
|
+
import type { ReviewDetail, ReviewItem, ReviewPage, ReviewResult } from '../shared/review.js'
|
|
3
|
+
|
|
4
|
+
export interface BatchPort {
|
|
5
|
+
list: (cursor?: number) => Promise<ReviewPage>
|
|
6
|
+
detail: (id: string) => Promise<ReviewDetail>
|
|
7
|
+
decide: (detail: ReviewDetail, reject: boolean, operationId: string) => Promise<ReviewResult<{ revision: number }>>
|
|
8
|
+
guard: () => void
|
|
9
|
+
operation: (detail: ReviewDetail, reject: boolean) => string
|
|
10
|
+
}
|
|
11
|
+
export interface BatchResult { completed: number; total: number; paths: string[]; error: string }
|
|
12
|
+
|
|
13
|
+
/** @public @author ddj 2026年10月27号 @param port Authenticated RPC adapter. @returns Frozen complete pending set; aborts before any decision if pagination shifts. @throws Incomplete or changing pages. */
|
|
14
|
+
export async function loadPending(port: Pick<BatchPort, 'list'>): Promise<ReviewItem[]> {
|
|
15
|
+
const items: ReviewItem[] = []
|
|
16
|
+
const seen = new Set<string>()
|
|
17
|
+
const cursors = new Set<number>()
|
|
18
|
+
let cursor: number | undefined
|
|
19
|
+
let expected: number | undefined
|
|
20
|
+
do {
|
|
21
|
+
const page = await port.list(cursor)
|
|
22
|
+
expected ??= page.pending
|
|
23
|
+
if (page.pending !== expected) throw new Error('待审记录在分页期间变化,请刷新后重试')
|
|
24
|
+
for (const item of page.items) {
|
|
25
|
+
if (seen.has(item.id) || !item.current || item.decision !== 'pending') throw new Error('待审分页发生重复或版本变化,请刷新后重试')
|
|
26
|
+
seen.add(item.id); items.push(item)
|
|
27
|
+
}
|
|
28
|
+
if (page.nextCursor === null) break
|
|
29
|
+
if (cursors.has(page.nextCursor) || page.items.length === 0) throw new Error('待审分页未完整返回,请刷新后重试')
|
|
30
|
+
cursor = page.nextCursor; cursors.add(cursor)
|
|
31
|
+
} while (true)
|
|
32
|
+
if (items.length !== expected) throw new Error('待审记录数量不完整,未执行批量操作')
|
|
33
|
+
return items
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** @public @author ddj 2026年10月27号 @param port Guarded existing per-file decision adapter. @param reject Undo instead of Keep. @returns Precise partial result; Undo stops at first conflict, never claims atomic multi-file rollback. */
|
|
37
|
+
export async function runReviewBatch(port: BatchPort, reject: boolean): Promise<BatchResult> {
|
|
38
|
+
const result: BatchResult = { completed: 0, total: 0, paths: [], error: '' }
|
|
39
|
+
try {
|
|
40
|
+
port.guard()
|
|
41
|
+
const items = await loadPending(port)
|
|
42
|
+
result.total = items.length
|
|
43
|
+
const details: ReviewDetail[] = []
|
|
44
|
+
// Preflight the entire frozen set before the first mutation. Disk CAS remains authoritative at execution.
|
|
45
|
+
for (const item of items) {
|
|
46
|
+
port.guard()
|
|
47
|
+
const detail = await port.detail(item.id)
|
|
48
|
+
if (!detail.item.current || detail.item.seq !== item.seq || detail.item.revision !== item.revision
|
|
49
|
+
|| !detail.capability.keep) throw new Error(`${item.display}:审查版本变化,请刷新后重试`)
|
|
50
|
+
if (reject && !detail.capability.undo) throw new Error(`${item.display}:${detail.capability.reason}`)
|
|
51
|
+
details.push(detail)
|
|
52
|
+
}
|
|
53
|
+
// Reverse chronological snapshots allow a newer round's result to expose the previous round's endpoint.
|
|
54
|
+
details.sort((left, right) => right.capturedAt.localeCompare(left.capturedAt) || right.item.seq - left.item.seq)
|
|
55
|
+
for (const detail of details) {
|
|
56
|
+
port.guard()
|
|
57
|
+
const reply = await port.decide(detail, reject, port.operation(detail, reject))
|
|
58
|
+
if (!reply.ok) throw new Error(`${detail.item.display}:${reply.error}`)
|
|
59
|
+
result.completed++
|
|
60
|
+
result.paths.push(detail.item.currentPath)
|
|
61
|
+
}
|
|
62
|
+
} catch (cause) { result.error = cause instanceof Error ? cause.message : String(cause) }
|
|
63
|
+
return result
|
|
64
|
+
}
|
|
@@ -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,64 @@
|
|
|
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, force = false): 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
|
+
const previous = entry.value
|
|
21
|
+
try {
|
|
22
|
+
const result = await rpc('edrv.review.list', { sessionId }, entry.controller.signal)
|
|
23
|
+
if (!entry.controller.signal.aborted) entry.value = result.ok
|
|
24
|
+
? { page: result.page, error: result.page.overview ? '' : 'Host 尚未提供官方文件导航,请重启加载新版插件;当前仅可浏览已返回的官方历史,角标不含官方记录。' }
|
|
25
|
+
: { ...entry.value, error: result.error }
|
|
26
|
+
} catch (error) { if (!entry.controller.signal.aborted) entry.value = { ...entry.value, error: String(error) } }
|
|
27
|
+
finally {
|
|
28
|
+
entry.loading = false
|
|
29
|
+
if (!entry.controller.signal.aborted) {
|
|
30
|
+
// Polling returns fresh JSON objects even when nothing changed. Preserve identity so
|
|
31
|
+
// editors do not tear down Monaco zones and reload file details every five seconds.
|
|
32
|
+
if (!force && JSON.stringify(previous) === JSON.stringify(entry.value)) entry.value = previous
|
|
33
|
+
else notify(entry)
|
|
34
|
+
if (entry.again) void refresh(sessionId, entry, true)
|
|
35
|
+
else entry.timer = setTimeout(() => { void refresh(sessionId, entry) }, 5000)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** @public @author ddj 2026年10月26号 @param sessionId Live scope. @param listener Mounted observer. @returns Cleanup cancelling requests and timers when unused. */
|
|
41
|
+
export function subscribeReview(sessionId: string, listener: Listener): () => void {
|
|
42
|
+
let entry = feeds.get(sessionId)
|
|
43
|
+
if (!entry) {
|
|
44
|
+
/** @private @author ddj 2026年10月26号 Refresh the shared official metadata after edits/decisions. */
|
|
45
|
+
const onRefresh = (): void => refreshReview(sessionId)
|
|
46
|
+
entry = { listeners: new Set(), value: { page: null, error: '' }, controller: new AbortController(), loading: false, again: false, onRefresh }
|
|
47
|
+
feeds.set(sessionId, entry)
|
|
48
|
+
if (typeof window !== 'undefined') window.addEventListener('edrv:refresh', onRefresh)
|
|
49
|
+
void refresh(sessionId, entry)
|
|
50
|
+
}
|
|
51
|
+
entry.listeners.add(listener); listener(entry.value)
|
|
52
|
+
return () => {
|
|
53
|
+
entry.listeners.delete(listener)
|
|
54
|
+
if (entry.listeners.size) return
|
|
55
|
+
entry.controller.abort(); clearTimeout(entry.timer); feeds.delete(sessionId)
|
|
56
|
+
if (typeof window !== 'undefined') window.removeEventListener('edrv:refresh', entry.onRefresh)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** @public @author ddj 2026年10月26号 @param sessionId Refresh metadata after a decision; does not alter legacy or disk. */
|
|
61
|
+
export function refreshReview(sessionId: string): void {
|
|
62
|
+
const entry = feeds.get(sessionId)
|
|
63
|
+
if (entry) { clearTimeout(entry.timer); void refresh(sessionId, entry, true) }
|
|
64
|
+
}
|
|
@@ -271,6 +271,45 @@
|
|
|
271
271
|
[data-edrv-view] .edrv-side-diff-dock .edrv-diffbar-body { max-height: min(180px, 25vh); max-width: 100%; }
|
|
272
272
|
[data-edrv-view] .edrv-side-diff-dock .edrv-diff-details { max-height: min(260px, 35vh); overflow-y: auto; }
|
|
273
273
|
|
|
274
|
+
/* Official review: file context above, navigation/actions below. Theme-owned surfaces; no neon status outlines. @author ddj 2026年10月27号 */
|
|
275
|
+
[data-edrv-diff-dock-shell] .edrv-reviewbar {
|
|
276
|
+
width: 520px; max-width: 100%; min-width: 0; box-sizing: border-box;
|
|
277
|
+
color: var(--dsw-alias-label-primary, #26313d); font-family: var(--dsw-font-family, sans-serif);
|
|
278
|
+
font-size: 12px; line-height: 1.4; container: edrv-review / inline-size;
|
|
279
|
+
}
|
|
280
|
+
[data-edrv-diff-dock-shell] .edrv-review-global { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 4px; padding: 6px 8px; border-bottom: 1px solid var(--dsw-alias-border-l1, #e5e9ef); }
|
|
281
|
+
[data-edrv-diff-dock-shell] .edrv-review-all-actions { display: flex; align-items: center; gap: 4px; }
|
|
282
|
+
[data-edrv-diff-dock-shell] .edrv-review-total strong { font-variant-numeric: tabular-nums; }
|
|
283
|
+
[data-edrv-diff-dock-shell] .edrv-review-heading {
|
|
284
|
+
display: flex; align-items: center; gap: 8px; min-width: 0; padding: 10px 12px 8px;
|
|
285
|
+
border-bottom: 1px solid var(--dsw-alias-border-l1, #e5e9ef);
|
|
286
|
+
}
|
|
287
|
+
[data-edrv-diff-dock-shell] .edrv-review-filemark { display: inline-flex; flex: none; color: var(--dsw-alias-label-tertiary, #697586); }
|
|
288
|
+
[data-edrv-diff-dock-shell] .edrv-review-filename { min-width: 0; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 13px; font-weight: 600; }
|
|
289
|
+
[data-edrv-diff-dock-shell] .edrv-review-pending { flex: none; display: inline-flex; gap: 5px; align-items: center; color: var(--dsw-alias-label-secondary, #697586); font-size: 11px; white-space: nowrap; }
|
|
290
|
+
[data-edrv-diff-dock-shell] .edrv-review-pending strong { font-size: 12px; font-weight: 600; font-variant-numeric: tabular-nums; color: var(--dsw-alias-label-primary, #26313d); }
|
|
291
|
+
[data-edrv-diff-dock-shell] .edrv-review-tools { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; padding: 7px 8px; }
|
|
292
|
+
[data-edrv-diff-dock-shell] .edrv-review-nav { display: flex; align-items: center; flex: none; gap: 1px; }
|
|
293
|
+
[data-edrv-diff-dock-shell] .edrv-review-blocknav { border-left: 1px solid var(--dsw-alias-border-l1, #e5e9ef); padding-left: 6px; }
|
|
294
|
+
[data-edrv-diff-dock-shell] .edrv-review-position { min-width: 32px; text-align: center; font-size: 11px; font-variant-numeric: tabular-nums; color: var(--dsw-alias-label-secondary, #697586); }
|
|
295
|
+
[data-edrv-diff-dock-shell] .edrv-review-actions { display: flex; align-items: center; flex: none; gap: 4px; margin-left: auto; }
|
|
296
|
+
[data-edrv-diff-dock-shell] .edrv-reviewbar button { height: 28px; min-height: 28px; padding: 0 8px; border-radius: 6px; font-family: inherit; font-size: 12px; font-weight: 500; white-space: nowrap; gap: 5px; }
|
|
297
|
+
[data-edrv-diff-dock-shell] .edrv-reviewbar button.edrv-review-icon { width: 28px; min-width: 28px; padding: 0; }
|
|
298
|
+
[data-edrv-diff-dock-shell] .edrv-reviewbar button.edrv-review-record { padding: 0 6px; font-size: 11px; color: var(--dsw-alias-label-secondary, #697586); }
|
|
299
|
+
[data-edrv-diff-dock-shell] .edrv-review-record svg { width: 12px; height: 12px; }
|
|
300
|
+
[data-edrv-diff-dock-shell] .edrv-reviewbar button.edrv-review-keep { background: var(--dsw-alias-brand-primary, #3b63d9); color: var(--dsw-alias-label-primary-inverted, #fff); border: 1px solid transparent; }
|
|
301
|
+
[data-edrv-diff-dock-shell] .edrv-reviewbar button.edrv-review-keep:hover:not(:disabled) { filter: brightness(.94); }
|
|
302
|
+
[data-edrv-diff-dock-shell] .edrv-reviewbar button.edrv-review-undo:hover:not(:disabled) { color: var(--dsw-alias-state-error-primary, #b64043); }
|
|
303
|
+
[data-edrv-diff-dock-shell] .edrv-reviewbar button:disabled { opacity: .4; cursor: not-allowed; }
|
|
304
|
+
[data-edrv-diff-dock-shell] .edrv-reviewbar button:focus-visible { outline: 2px solid var(--dsw-alias-brand-primary, #3b63d9); outline-offset: 2px; }
|
|
305
|
+
[data-edrv-diff-dock-shell] .edrv-review-notice { padding: 7px 12px; border-top: 1px solid var(--dsw-alias-border-l1, #e5e9ef); color: var(--dsw-alias-label-secondary, #697586); font-size: 11px; overflow-wrap: anywhere; }
|
|
306
|
+
[data-edrv-diff-dock-shell] .edrv-review-error { color: var(--dsw-alias-state-error-primary, #b64043); }
|
|
307
|
+
[data-edrv-diff-dock-shell] .edrv-review-body { border-top: 1px solid var(--dsw-alias-border-l1, #e5e9ef); max-height: min(240px, 30vh); overflow: auto; padding: 8px 12px; }
|
|
308
|
+
@container edrv-review (max-width: 420px) {
|
|
309
|
+
.edrv-reviewbar .edrv-review-heading { gap: 6px; padding-inline: 10px; }
|
|
310
|
+
.edrv-reviewbar .edrv-review-actions { justify-content: flex-end; flex: 1 0 100%; border-top: 1px solid var(--dsw-alias-border-l1, #e5e9ef); padding-top: 6px; }
|
|
311
|
+
}
|
|
312
|
+
|
|
274
313
|
/* ===== 侧边栏(活动栏 + 面板区 + 文件树) ===== */
|
|
275
314
|
[data-edrv-view] .edrv-editor-row { display: flex; flex: 1 1 auto; min-width: 0; min-height: 0; overflow: hidden; }
|
|
276
315
|
[data-edrv-view] .edrv-main-col { box-sizing: border-box; min-width: 0; min-height: 0; overflow: hidden; }
|
|
@@ -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 { OfficialDiffBar } from './OfficialDiffBar.js'
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* 折叠控制使用任务栏同尺寸的 chevron 轮廓,避免依赖未声明的运行时图标包。
|
|
@@ -65,6 +66,8 @@ export function DiffBox(props) {
|
|
|
65
66
|
}, diffDockText(fileTotal))))
|
|
66
67
|
}
|
|
67
68
|
|
|
69
|
+
if (props.official) return React.createElement(OfficialDiffBar, props)
|
|
70
|
+
|
|
68
71
|
let bodyEl = null
|
|
69
72
|
if (expanded) {
|
|
70
73
|
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) }, '✕ 关闭')),
|