dsh-vscode-mode 0.14.2 → 0.14.4
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 +12 -9
- package/lib/client.js +906 -395
- package/lib/client.js.map +1 -1
- package/lib/index.js +1594 -607
- package/lib/index.js.map +1 -1
- package/package.json +7 -7
- package/skills/dsh-vscodemode-mcp/SKILL.md +9 -3
- package/src/archiveDb.ts +476 -0
- package/src/capture.ts +8 -1
- package/src/client/diffDockStore.ts +18 -0
- package/src/client/mcpForm.ts +79 -0
- package/src/client/mcpState.ts +179 -0
- package/src/client/monaco/diffRender.ts +3 -0
- package/src/client/state/regions.ts +21 -1
- package/src/client/styles/editor.css +12 -0
- package/src/client/styles/mcp.css +6 -3
- package/src/client/ui/ConversationDiffDock.ts +2 -2
- package/src/client/ui/DiffLauncher.ts +76 -17
- package/src/client/ui/EditorView.ts +90 -7
- package/src/client/ui/McpSettings.ts +174 -137
- package/src/dshVersion.ts +4 -3
- package/src/index.ts +2 -0
- package/src/mcp.ts +131 -23
- package/src/mcpIsolation.ts +2 -1
- package/src/mcpProject.ts +74 -14
- package/src/mcpRuntime.ts +18 -3
- package/src/paths.ts +11 -0
- package/src/perf.ts +17 -24
- package/src/revert.ts +93 -1
- package/src/rpc.ts +243 -68
- package/src/shared/diff.ts +69 -20
- package/src/shared/mcpStatus.ts +30 -0
- package/src/shared/rpc.ts +10 -1
- package/src/store.ts +154 -55
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { MpcConfig } from '../shared/mcp.js'
|
|
2
|
+
|
|
3
|
+
export interface McpDraft {
|
|
4
|
+
serverName: string
|
|
5
|
+
transport: 'stdio' | 'streamable-http'
|
|
6
|
+
command: string
|
|
7
|
+
args: string
|
|
8
|
+
cwd: string
|
|
9
|
+
url: string
|
|
10
|
+
headers: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @public
|
|
15
|
+
* @author ddj 2026年10月08号
|
|
16
|
+
* @description Parse lossless JSON string[] or one literal argument per nonempty line; no shell splitting.
|
|
17
|
+
* @param text Argument field.
|
|
18
|
+
* @returns Literal argument vector.
|
|
19
|
+
* @throws Invalid JSON or a non-string array element.
|
|
20
|
+
*/
|
|
21
|
+
export function parseArgs(text: string): string[] {
|
|
22
|
+
if (text.trimStart().startsWith('[')) {
|
|
23
|
+
let value: unknown
|
|
24
|
+
try { value = JSON.parse(text) } catch { throw new Error('参数 JSON 无效,请输入 string[] 或每行一个参数') }
|
|
25
|
+
if (!Array.isArray(value) || !value.every((item) => typeof item === 'string')) {
|
|
26
|
+
throw new Error('参数 JSON 必须是 string[]')
|
|
27
|
+
}
|
|
28
|
+
return value
|
|
29
|
+
}
|
|
30
|
+
return text.split(/\r?\n/).filter((line) => line.length > 0)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* @public
|
|
35
|
+
* @author ddj 2026年10月08号
|
|
36
|
+
* @description Validate HTTP headers; duplicates are case-insensitive and values retain embedded equals.
|
|
37
|
+
* @param text One key=value header per line.
|
|
38
|
+
* @returns Validated header dictionary.
|
|
39
|
+
* @throws Missing separator, invalid/empty key, duplicate key, or illegal control characters.
|
|
40
|
+
*/
|
|
41
|
+
export function parsePairs(text: string): Record<string, string> {
|
|
42
|
+
const pairs: Array<[string, string]> = []
|
|
43
|
+
const keys = new Set<string>()
|
|
44
|
+
for (const [index, raw] of text.split(/\r?\n/).entries()) {
|
|
45
|
+
if (!raw.trim()) continue
|
|
46
|
+
const separator = raw.indexOf('=')
|
|
47
|
+
const key = raw.slice(0, separator).trim()
|
|
48
|
+
const value = raw.slice(separator + 1).trim()
|
|
49
|
+
if (separator < 1 || !/^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/.test(key) || /[\x00-\x08\x0a-\x1f\x7f]/.test(value)) {
|
|
50
|
+
throw new Error('请求头第 ' + (index + 1) + ' 行格式错误,应为有效 key=value')
|
|
51
|
+
}
|
|
52
|
+
const normalized = key.toLowerCase()
|
|
53
|
+
if (keys.has(normalized)) throw new Error('请求头重复:' + key)
|
|
54
|
+
keys.add(normalized)
|
|
55
|
+
pairs.push([key, value])
|
|
56
|
+
}
|
|
57
|
+
return Object.fromEntries(pairs)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* @public
|
|
62
|
+
* @author ddj 2026年10月08号
|
|
63
|
+
* @description Convert a form draft without losing argument boundaries or silently accepting malformed headers.
|
|
64
|
+
* @param draft Current editable fields.
|
|
65
|
+
* @returns Host MCP configuration.
|
|
66
|
+
* @throws Argument/header validation errors; callers must keep the form open.
|
|
67
|
+
*/
|
|
68
|
+
export function configOf(draft: McpDraft): MpcConfig {
|
|
69
|
+
const config: MpcConfig = { serverName: draft.serverName.trim(), transport: draft.transport }
|
|
70
|
+
if (draft.transport === 'stdio') {
|
|
71
|
+
config.command = draft.command.trim()
|
|
72
|
+
config.args = parseArgs(draft.args)
|
|
73
|
+
if (draft.cwd.trim()) config.cwd = draft.cwd.trim()
|
|
74
|
+
} else {
|
|
75
|
+
config.url = draft.url.trim()
|
|
76
|
+
config.headers = parsePairs(draft.headers)
|
|
77
|
+
}
|
|
78
|
+
return config
|
|
79
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import type { MpcProject, MpcServer } from '../shared/mcp.js'
|
|
2
|
+
import type { rpc } from './rpc.js'
|
|
3
|
+
|
|
4
|
+
export interface McpView {
|
|
5
|
+
servers: MpcServer[]
|
|
6
|
+
projects: MpcProject[]
|
|
7
|
+
loading: boolean
|
|
8
|
+
busy: string
|
|
9
|
+
error: string
|
|
10
|
+
}
|
|
11
|
+
interface McpTiming { interval?: number; timeout?: number }
|
|
12
|
+
interface ReadToken { controller: AbortController; version: number; timer: ReturnType<typeof setTimeout> }
|
|
13
|
+
type Result = { ok: boolean; error?: string }
|
|
14
|
+
|
|
15
|
+
/** Owns MCP read lifetimes and one synchronous mutation lock for the entire management page. */
|
|
16
|
+
export class McpState {
|
|
17
|
+
readonly view: McpView = { servers: [], projects: [], loading: true, busy: '', error: '' }
|
|
18
|
+
private active = false
|
|
19
|
+
private visible = true
|
|
20
|
+
private disposed = false
|
|
21
|
+
private version = 0
|
|
22
|
+
private reading: ReadToken | null = null
|
|
23
|
+
private timer: ReturnType<typeof setTimeout> | null = null
|
|
24
|
+
private readonly interval: number
|
|
25
|
+
private readonly timeout: number
|
|
26
|
+
|
|
27
|
+
/** @public @author ddj 2026年10月08号 @param request Typed RPC transport. @param changed View notification. @param timing Bounded read and polling timings. */
|
|
28
|
+
constructor(private readonly request: typeof rpc, private readonly changed: (view: McpView) => void, timing: McpTiming = {}) {
|
|
29
|
+
this.interval = Math.max(100, timing.interval ?? 2500)
|
|
30
|
+
this.timeout = Math.max(50, timing.timeout ?? 10000)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
//region Read lifecycle
|
|
34
|
+
/** @private @author ddj 2026年10月08号 @description Publish a copied view only while mounted. */
|
|
35
|
+
private emit(): void {
|
|
36
|
+
if (!this.disposed) this.changed({ ...this.view })
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** @private @author ddj 2026年10月08号 @description Cancel scheduled reads and invalidate every older response. */
|
|
40
|
+
private invalidate(): void {
|
|
41
|
+
this.version++
|
|
42
|
+
if (this.timer !== null) clearTimeout(this.timer)
|
|
43
|
+
this.timer = null
|
|
44
|
+
this.reading?.controller.abort()
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** @private @author ddj 2026年10月08号 @description Schedule a single read only after the preceding transport settled. */
|
|
48
|
+
private schedule(): void {
|
|
49
|
+
if (this.disposed || !this.active || !this.visible || this.reading || this.view.loading || this.view.busy || this.timer !== null) return
|
|
50
|
+
this.timer = setTimeout(this.poll, this.interval)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** @private @author ddj 2026年10月08号 @description Timer entry, with failures handled in read. */
|
|
54
|
+
private poll = (): void => {
|
|
55
|
+
this.timer = null
|
|
56
|
+
void this.read(false)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** @private @author ddj 2026年10月08号 @returns Versioned read token with an abort deadline. */
|
|
60
|
+
private beginRead(): ReadToken {
|
|
61
|
+
const controller = new AbortController()
|
|
62
|
+
const version = this.version
|
|
63
|
+
/** @author ddj 2026年10月08号 @description Abort timed-out transport and invalidate its eventual response. */
|
|
64
|
+
const expire = (): void => {
|
|
65
|
+
controller.abort()
|
|
66
|
+
if (this.disposed || version !== this.version) return
|
|
67
|
+
this.version++
|
|
68
|
+
this.view.loading = false
|
|
69
|
+
this.view.error = '读取 MCP 状态超时'
|
|
70
|
+
this.emit()
|
|
71
|
+
}
|
|
72
|
+
const token = { controller, version, timer: setTimeout(expire, this.timeout) }
|
|
73
|
+
this.reading = token
|
|
74
|
+
return token
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** @private @author ddj 2026年10月08号 @param initial Initialization versus read-only polling. @param signal Abort deadline. @returns Complete settled data, never an early half-settled initial read. @throws Transport or host failures. */
|
|
78
|
+
private async fetchData(initial: boolean, signal: AbortSignal): Promise<Pick<McpView, 'servers' | 'projects'>> {
|
|
79
|
+
if (!initial) {
|
|
80
|
+
const snapshot = await this.request('mcp.snapshot', {}, signal)
|
|
81
|
+
if (!snapshot.ok) throw new Error(snapshot.error)
|
|
82
|
+
return snapshot
|
|
83
|
+
}
|
|
84
|
+
const [list, groups] = await Promise.allSettled([this.request('mcp.list', {}, signal), this.request('mcp.projects', {}, signal)])
|
|
85
|
+
if (list.status === 'rejected') throw list.reason
|
|
86
|
+
if (groups.status === 'rejected') throw groups.reason
|
|
87
|
+
if (!list.value.ok) throw new Error(list.value.error)
|
|
88
|
+
if (!groups.value.ok) throw new Error(groups.value.error)
|
|
89
|
+
return { servers: list.value.servers, projects: groups.value.projects }
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** @private @author ddj 2026年10月08号 @param initial Use legacy initialization RPCs once; later polls are read-only snapshots. @returns Completion after transport settles; aborted results never publish. */
|
|
93
|
+
private async read(initial: boolean): Promise<void> {
|
|
94
|
+
if (this.disposed || this.reading || this.view.busy) return
|
|
95
|
+
if (!initial && (!this.active || !this.visible)) return
|
|
96
|
+
const token = this.beginRead()
|
|
97
|
+
try {
|
|
98
|
+
const signal = token.controller.signal
|
|
99
|
+
const { servers, projects } = await this.fetchData(initial, signal)
|
|
100
|
+
if (!this.disposed && token.version === this.version && !signal.aborted) {
|
|
101
|
+
this.view.servers = servers
|
|
102
|
+
this.view.projects = projects
|
|
103
|
+
this.view.error = ''
|
|
104
|
+
}
|
|
105
|
+
} catch (error) {
|
|
106
|
+
if (!this.disposed && token.version === this.version && !token.controller.signal.aborted) this.view.error = String(error)
|
|
107
|
+
} finally {
|
|
108
|
+
clearTimeout(token.timer)
|
|
109
|
+
this.reading = null
|
|
110
|
+
if (!this.disposed) {
|
|
111
|
+
this.view.loading = false
|
|
112
|
+
this.emit()
|
|
113
|
+
this.schedule()
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** @public @author ddj 2026年10月08号 @description Initialize list/projects once per mounted page. @returns Load completion. */
|
|
119
|
+
load(): Promise<void> { return this.read(true) }
|
|
120
|
+
|
|
121
|
+
/** @public @author ddj 2026年10月08号 @param active Whether the top-level MCP tab is selected. */
|
|
122
|
+
setActive(active: boolean): void {
|
|
123
|
+
if (this.active === active) return
|
|
124
|
+
this.active = active
|
|
125
|
+
if (!active) this.invalidate()
|
|
126
|
+
this.schedule()
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** @public @author ddj 2026年10月08号 @param visible Whether document visibility permits background requests. */
|
|
130
|
+
setVisible(visible: boolean): void {
|
|
131
|
+
if (this.visible === visible) return
|
|
132
|
+
this.visible = visible
|
|
133
|
+
if (!visible) this.invalidate()
|
|
134
|
+
this.schedule()
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** @public @author ddj 2026年10月08号 @description Stop timers and invalidate all responses on unmount. */
|
|
138
|
+
dispose(): void {
|
|
139
|
+
this.disposed = true
|
|
140
|
+
this.invalidate()
|
|
141
|
+
if (this.reading) clearTimeout(this.reading.timer)
|
|
142
|
+
}
|
|
143
|
+
//endregion
|
|
144
|
+
|
|
145
|
+
//region Serialized mutations
|
|
146
|
+
/**
|
|
147
|
+
* @public
|
|
148
|
+
* @author ddj 2026年10月08号
|
|
149
|
+
* @description Acquire a synchronous page-wide lock before validation/RPC. Duplicate actions are rejected; failed saves never call commit.
|
|
150
|
+
* @param label Busy indicator key.
|
|
151
|
+
* @param task Validation and RPC work, including synchronous configuration errors.
|
|
152
|
+
* @param commit Apply successful result; may reset and close a save form.
|
|
153
|
+
* @returns Whether this mutation committed successfully while mounted.
|
|
154
|
+
*/
|
|
155
|
+
async mutate<T extends Result>(label: string, task: () => Promise<T>, commit: (result: T) => void): Promise<boolean> {
|
|
156
|
+
if (this.disposed || this.view.busy) return false
|
|
157
|
+
this.invalidate()
|
|
158
|
+
this.view.busy = label
|
|
159
|
+
this.view.error = ''
|
|
160
|
+
this.emit()
|
|
161
|
+
try {
|
|
162
|
+
const result = await task()
|
|
163
|
+
if (!result.ok) throw new Error(result.error)
|
|
164
|
+
if (this.disposed) return false
|
|
165
|
+
commit(result)
|
|
166
|
+
return true
|
|
167
|
+
} catch (error) {
|
|
168
|
+
if (!this.disposed) this.view.error = String(error)
|
|
169
|
+
return false
|
|
170
|
+
} finally {
|
|
171
|
+
if (!this.disposed) {
|
|
172
|
+
this.view.busy = ''
|
|
173
|
+
this.emit()
|
|
174
|
+
this.schedule()
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
//endregion
|
|
179
|
+
}
|
|
@@ -175,6 +175,9 @@ export function createDiffRenderer(log) {
|
|
|
175
175
|
const overlay = root && root.querySelector('.edrv-minus-overlay')
|
|
176
176
|
if (overlay) overlay.innerHTML = ''
|
|
177
177
|
}
|
|
178
|
+
// #region debug log
|
|
179
|
+
try { log(sessionId, '[DEBUG diffRender] regions=' + pendingRegions.length + ' zones=' + createdZones.length + ' ms=' + (Date.now() - renderT0)) } catch (e) { /* 诊断不可影响渲染 */ }
|
|
180
|
+
// #endregion
|
|
178
181
|
}
|
|
179
182
|
|
|
180
183
|
/** 卸载清理(EditorView unmount 时调用)。 */
|
|
@@ -29,6 +29,24 @@ export function countLinesBefore(text: string, index: number): number {
|
|
|
29
29
|
return lineBefore(text, index)
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
/**
|
|
33
|
+
* 在预计算的换行位置中找出 offset 前的行数。
|
|
34
|
+
* @author ddj 2026年09月29号
|
|
35
|
+
* @param breaks 换行符位置(递增)
|
|
36
|
+
* @param offset 目标位置
|
|
37
|
+
* @returns 目标位置前的换行数
|
|
38
|
+
*/
|
|
39
|
+
function lineAt(breaks: number[], offset: number): number {
|
|
40
|
+
let left = 0
|
|
41
|
+
let right = breaks.length
|
|
42
|
+
while (left < right) {
|
|
43
|
+
const middle = (left + right) >>> 1
|
|
44
|
+
if (breaks[middle] < offset) left = middle + 1
|
|
45
|
+
else right = middle
|
|
46
|
+
}
|
|
47
|
+
return left
|
|
48
|
+
}
|
|
49
|
+
|
|
32
50
|
/**
|
|
33
51
|
* 行级公共前缀/后缀裁剪:old/new 首尾相同的行视为未变化(上下文),只保留真正变更的中间段。
|
|
34
52
|
* @author ddj 2026年08月20号
|
|
@@ -58,6 +76,8 @@ export function diffRegions(records: RecordView[], content: string | null): Regi
|
|
|
58
76
|
if (content === null) return regions
|
|
59
77
|
const normalized = normalizeForCompare(content)
|
|
60
78
|
const lines = splitLines(normalized)
|
|
79
|
+
const breaks: number[] = []
|
|
80
|
+
for (let index = normalized.indexOf('\n'); index >= 0; index = normalized.indexOf('\n', index + 1)) breaks.push(index)
|
|
61
81
|
for (const rec of records) {
|
|
62
82
|
if (rec.create) {
|
|
63
83
|
for (let i = 0; i < rec.hunks.length; i++) {
|
|
@@ -81,7 +101,7 @@ export function diffRegions(records: RecordView[], content: string | null): Regi
|
|
|
81
101
|
regions.push({ callId: rec.callId, idx: entry.idx, stale: true, status, create: false, oldLines: entry.hunk.oldText === null ? [] : entry.hunk.oldText.split('\n'), newLines: entry.hunk.newText ? entry.hunk.newText.split('\n') : [], rec, superseded: rec.superseded === true })
|
|
82
102
|
continue
|
|
83
103
|
}
|
|
84
|
-
const start =
|
|
104
|
+
const start = lineAt(breaks, location.start) + 1
|
|
85
105
|
const oldLines = entry.hunk.oldText === null ? [] : entry.hunk.oldText.split('\n')
|
|
86
106
|
const newLines = entry.hunk.newText.split('\n')
|
|
87
107
|
const trimmed = trimCommonLines(oldLines, newLines)
|
|
@@ -248,6 +248,18 @@
|
|
|
248
248
|
[data-edrv-diff-dock-shell] .edrv-diffbar-editor-content { width: max-content; }
|
|
249
249
|
[data-edrv-diff-dock-shell] .edrv-diffbar-files .edrv-diffbar-file { max-width: 160px; }
|
|
250
250
|
}
|
|
251
|
+
/* 侧栏差异栏在编辑内容底部居中悬浮,按内容宽度收缩;窄栏只在工具条内横滚。@author ddj 2026年09月29号 */
|
|
252
|
+
[data-edrv-view] .edrv-side-diff-dock[data-edrv-diff-dock-shell] { position: absolute; left: 50%; bottom: 28px; z-index: 25; width: max-content; max-width: calc(100% - 20px); max-height: min(300px, calc(100% - 38px)); margin: 0; transform: translateX(-50%); border-radius: 12px; background: var(--dsw-alias-bg-overlay, #fff); box-shadow: 0 6px 20px rgba(0,0,0,.18); overflow-x: hidden; overflow-y: auto; }
|
|
253
|
+
[data-edrv-view] .edrv-side-diff-dock .edrv-diffbar-editor-row { padding-inline: 8px; }
|
|
254
|
+
[data-edrv-view] .edrv-side-diff-dock .edrv-diffbar-editor-content { gap: 5px; width: max-content; }
|
|
255
|
+
[data-edrv-view] .edrv-side-diff-dock .edrv-diffbar-nav,
|
|
256
|
+
[data-edrv-view] .edrv-side-diff-dock .edrv-diffbar-files,
|
|
257
|
+
[data-edrv-view] .edrv-side-diff-dock .edrv-diffbar-actions { gap: 3px; }
|
|
258
|
+
[data-edrv-view] .edrv-side-diff-dock .edrv-pill { padding-inline: 5px; }
|
|
259
|
+
[data-edrv-view] .edrv-side-diff-dock .edrv-diffbar-files .edrv-diffbar-file { min-width: 0; max-width: 72px; }
|
|
260
|
+
[data-edrv-view] .edrv-side-diff-dock .edrv-diffbar-details-toggle { width: 26px; min-width: 26px; }
|
|
261
|
+
[data-edrv-view] .edrv-side-diff-dock .edrv-diffbar-body { max-height: min(180px, 25vh); max-width: 100%; }
|
|
262
|
+
[data-edrv-view] .edrv-side-diff-dock .edrv-diff-details { max-height: min(260px, 35vh); overflow-y: auto; }
|
|
251
263
|
|
|
252
264
|
/* ===== 侧边栏(活动栏 + 面板区 + 文件树) ===== */
|
|
253
265
|
[data-edrv-view] .edrv-editor-row { display: flex; flex: 1 1 auto; min-width: 0; min-height: 0; overflow: hidden; }
|
|
@@ -49,16 +49,19 @@
|
|
|
49
49
|
.vsm-mcp-card-head{display:flex;justify-content:space-between;align-items:center;gap:12px}
|
|
50
50
|
.vsm-mcp-title{display:flex;align-items:center;gap:9px;font-weight:600;font-size:14px;color:var(--dsw-alias-label-primary,#e6e6e6);min-width:0}
|
|
51
51
|
.vsm-mcp-source{font-size:11px;font-weight:400;padding:2px 7px;border-radius:4px;background:var(--dsw-alias-bg-mask-2,#ffffff22);color:var(--dsw-alias-label-tertiary,#9aa5b1);flex-shrink:0}
|
|
52
|
-
.vsm-mcp-dot{width:8px;height:8px;border-radius:50%;flex-shrink:0;background:var(--dsw-alias-
|
|
52
|
+
.vsm-mcp-dot{width:8px;height:8px;border-radius:50%;flex-shrink:0;background:var(--dsw-alias-label-tertiary,#9aa5b1)}
|
|
53
53
|
.vsm-mcp-dot.connected{background:var(--dsw-alias-state-success-primary,#38bd75)}
|
|
54
54
|
.vsm-mcp-dot.connecting{background:var(--dsw-alias-state-warn-primary,#e5b93f)}
|
|
55
|
-
.vsm-mcp-dot.
|
|
55
|
+
.vsm-mcp-dot.error{background:var(--dsw-alias-state-error-primary,#e05252)}
|
|
56
|
+
.vsm-mcp-dot.disabled,.vsm-mcp-dot.neutral,.vsm-mcp-dot.configured,.vsm-mcp-dot.unverified{background:var(--dsw-alias-label-tertiary,#9aa5b1)}
|
|
56
57
|
.vsm-mcp-actions{display:flex;align-items:center;gap:4px;flex-shrink:0}
|
|
57
58
|
.vsm-mcp-actions button{border:0;background:none;color:var(--dsw-alias-label-tertiary,#9aa5b1);cursor:pointer;font-size:15px;width:28px;height:28px;border-radius:6px;line-height:1}
|
|
58
59
|
.vsm-mcp-actions button:hover{color:var(--dsw-alias-label-primary,#fff);background:var(--dsw-alias-interactive-bg-hover,#ffffff14)}
|
|
59
60
|
.vsm-mcp-actions button.vsm-danger:hover{color:var(--dsw-alias-state-error-primary,#f07878);background:var(--dsw-alias-interactive-bg-hover-danger,#ff000014)}
|
|
60
61
|
.vsm-switch{font-size:14px !important}
|
|
61
|
-
|
|
62
|
+
/* 配置启用使用品牌色,不冒充在线状态;在线证据仅由 connected 圆点表达。 */
|
|
63
|
+
.vsm-switch.on{color:var(--dsw-alias-brand-primary,#6d55ff)}
|
|
64
|
+
.vsm-mcp-actions button:disabled,.vsm-mcp-dialog-actions button:disabled{opacity:.55;cursor:default}
|
|
62
65
|
.vsm-mcp-meta{font-size:12px;color:var(--dsw-alias-label-tertiary,#9aa5b1);margin:9px 0}
|
|
63
66
|
.vsm-mcp-error{color:var(--dsw-alias-state-error-primary,#f07878);font-size:12px;margin:8px 0}
|
|
64
67
|
.vsm-mcp-banner{padding:9px 12px;border:1px solid var(--dsw-alias-state-error-secondary,#d9534f55);border-radius:6px;background:var(--dsw-alias-state-error-secondary,#d9534f1a);margin-bottom:10px}
|
|
@@ -9,7 +9,7 @@ import { rpc } from '../rpc.js'
|
|
|
9
9
|
import { openDiffView } from '../events.js'
|
|
10
10
|
import { summarize } from '../state/records.js'
|
|
11
11
|
import { nextDiffPath } from '../diffDock.js'
|
|
12
|
-
import { readDiffDock, subscribeDiffDock } from '../diffDockStore.js'
|
|
12
|
+
import { dockInLayout, readDiffDock, subscribeDiffDock } from '../diffDockStore.js'
|
|
13
13
|
import { DiffBox } from './DiffBox.js'
|
|
14
14
|
import { CommandPalette } from './CommandPalette.js'
|
|
15
15
|
|
|
@@ -65,7 +65,7 @@ export function ConversationDiffDock(props) {
|
|
|
65
65
|
// 与 EditorView 同时挂载时由 commandPaletteStore 的单实例认领保证只渲染一份。
|
|
66
66
|
const palette = React.createElement(CommandPalette, { key: 'edrv-palette', sessionId })
|
|
67
67
|
if (editorSnapshot) {
|
|
68
|
-
if (editorSnapshot
|
|
68
|
+
if (!dockInLayout(editorSnapshot, 'tab')) return palette
|
|
69
69
|
return React.createElement(React.Fragment, null,
|
|
70
70
|
renderDock(React.createElement(DiffBox, Object.assign({}, editorSnapshot, { dock: true })), sessionId),
|
|
71
71
|
palette)
|
|
@@ -8,6 +8,35 @@ import React from 'react'
|
|
|
8
8
|
import { rpc } from '../rpc.js'
|
|
9
9
|
import { MONO, btn } from './shared.js'
|
|
10
10
|
|
|
11
|
+
/**
|
|
12
|
+
* 文件与批次共同标识归档卡片,避免同文件多批次共用展开状态。
|
|
13
|
+
* @author ddj 2026年09月30号
|
|
14
|
+
* @param entry 归档摘要或展开详情
|
|
15
|
+
* @returns 稳定卡片键
|
|
16
|
+
*/
|
|
17
|
+
export function archiveKey(entry) {
|
|
18
|
+
return JSON.stringify([entry.path, entry.batch])
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* 追加一页摘要并按文件/批次去重,保留已有卡片的顺序。
|
|
23
|
+
* @author ddj 2026年09月30号
|
|
24
|
+
* @param current 已加载页面
|
|
25
|
+
* @param next 下一页
|
|
26
|
+
* @returns 合并后的摘要列表
|
|
27
|
+
*/
|
|
28
|
+
export function mergeArchives(current, next) {
|
|
29
|
+
const merged = [...(current ?? [])]
|
|
30
|
+
const keys = new Set(merged.map(archiveKey))
|
|
31
|
+
for (const entry of next) {
|
|
32
|
+
const key = archiveKey(entry)
|
|
33
|
+
if (keys.has(key)) continue
|
|
34
|
+
keys.add(key)
|
|
35
|
+
merged.push(entry)
|
|
36
|
+
}
|
|
37
|
+
return merged
|
|
38
|
+
}
|
|
39
|
+
|
|
11
40
|
/**
|
|
12
41
|
* 全局差异面板:待处理文件列表(点击打开并跳转)+ 归档浏览/批次回滚。
|
|
13
42
|
* 由状态栏差异 chip / header 角标触发。
|
|
@@ -22,27 +51,50 @@ export function DiffLauncher(props) {
|
|
|
22
51
|
const [archiveDetail, setArchiveDetail] = React.useState(null)
|
|
23
52
|
const [archivePath, setArchivePath] = React.useState(null)
|
|
24
53
|
const [archiveErr, setArchiveErr] = React.useState(null)
|
|
54
|
+
const [nextCursor, setNextCursor] = React.useState(null)
|
|
55
|
+
const [moreBusy, setMoreBusy] = React.useState(false)
|
|
56
|
+
const requestRef = React.useRef(0)
|
|
57
|
+
const detailRef = React.useRef(0)
|
|
58
|
+
const moreRef = React.useRef(false)
|
|
25
59
|
const [error, setError] = React.useState(null)
|
|
26
60
|
|
|
27
|
-
|
|
61
|
+
/** @author ddj 2026年09月30号 @description 仅加载一个归档摘要页并丢弃失效响应。 */
|
|
62
|
+
const loadArchives = (cursor = null) => {
|
|
28
63
|
if (!sessionId) { setArchiveErr('会话不存在'); return }
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
64
|
+
if (cursor && moreRef.current) return
|
|
65
|
+
if (cursor) { moreRef.current = true; setMoreBusy(true); setArchiveErr(null) }
|
|
66
|
+
else { requestRef.current++; setArchives(null); setNextCursor(null); setArchiveErr(null) }
|
|
67
|
+
const generation = requestRef.current
|
|
68
|
+
rpc('edrv.archivePage', { sessionId, ...(cursor ? { cursor } : {}) }).then((res) => {
|
|
69
|
+
if (generation !== requestRef.current) return
|
|
70
|
+
if (res && res.ok && Array.isArray(res.entries)) {
|
|
71
|
+
setArchives((current) => cursor ? mergeArchives(current, res.entries) : res.entries)
|
|
72
|
+
setNextCursor(res.nextCursor ?? null)
|
|
73
|
+
setArchiveErr(null)
|
|
74
|
+
} else setArchiveErr(res?.error ? String(res.error) : '归档分页读取失败')
|
|
75
|
+
}).catch((e) => { if (generation === requestRef.current) setArchiveErr('归档异常:' + String(e)) })
|
|
76
|
+
.finally(() => { if (generation === requestRef.current) { moreRef.current = false; setMoreBusy(false) } })
|
|
33
77
|
}
|
|
34
|
-
|
|
78
|
+
/** @author ddj 2026年09月30号 @description 仅加载目标文件和批次详情。 */
|
|
79
|
+
const loadArchiveDetail = (path, batch) => {
|
|
35
80
|
if (!sessionId) return
|
|
81
|
+
const generation = ++detailRef.current
|
|
36
82
|
setArchiveDetail(null)
|
|
37
|
-
rpc('edrv.archiveRead', { sessionId, path }).then((res) => {
|
|
83
|
+
rpc('edrv.archiveRead', { sessionId, path, batch }).then((res) => {
|
|
84
|
+
if (generation !== detailRef.current) return
|
|
38
85
|
if (res && res.ok && Array.isArray(res.batches)) setArchiveDetail(res.batches)
|
|
39
86
|
else setArchiveErr(res?.error ? String(res.error) : '归档详情读取失败')
|
|
40
|
-
}).catch((e) => setArchiveErr('归档异常:' + String(e)))
|
|
87
|
+
}).catch((e) => { if (generation === detailRef.current) setArchiveErr('归档异常:' + String(e)) })
|
|
41
88
|
}
|
|
42
89
|
React.useEffect(() => {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
90
|
+
requestRef.current++
|
|
91
|
+
detailRef.current++
|
|
92
|
+
moreRef.current = false
|
|
93
|
+
setMoreBusy(false)
|
|
94
|
+
setArchiveDetail(null); setArchivePath(null); setArchiveErr(null)
|
|
95
|
+
if (tab === 'archive') loadArchives()
|
|
96
|
+
else { setArchives(null); setNextCursor(null) }
|
|
97
|
+
return () => { requestRef.current++; detailRef.current++ }
|
|
46
98
|
}, [tab, sessionId])
|
|
47
99
|
|
|
48
100
|
let body
|
|
@@ -54,17 +106,18 @@ export function DiffLauncher(props) {
|
|
|
54
106
|
React.createElement('span', { className: 'edrv-launch-cnt' }, '待处理 ' + f.pending + ' 处'),
|
|
55
107
|
React.createElement('button', { className: 'edrv-btn-mini edrv-btn-keep', onClick: (e) => { e.stopPropagation(); onOpenFile(f.path) } }, '打开'))))
|
|
56
108
|
} else {
|
|
57
|
-
if (archiveErr) body = React.createElement('div', { className: 'edrv-search-empty', style: { color: 'var(--dsw-alias-state-error-primary,#d9534f)' } }, '错误:' + String(archiveErr))
|
|
109
|
+
if (archiveErr && archives === null) body = React.createElement('div', { className: 'edrv-search-empty', style: { color: 'var(--dsw-alias-state-error-primary,#d9534f)' } }, '错误:' + String(archiveErr), React.createElement('button', { onClick: () => loadArchives(), style: btn(false) }, '重试'))
|
|
58
110
|
else if (archives === null) body = React.createElement('div', { className: 'edrv-search-empty' }, '加载中…')
|
|
59
111
|
else if (archives.length === 0) body = React.createElement('div', { className: 'edrv-search-empty' }, '暂无归档记录')
|
|
60
112
|
else {
|
|
61
113
|
const inner = archives.map((e) => {
|
|
62
|
-
const
|
|
114
|
+
const entryKey = archiveKey(e)
|
|
115
|
+
const open = archivePath === entryKey
|
|
63
116
|
let detailEl = null
|
|
64
117
|
if (open) {
|
|
65
118
|
if (archiveDetail === null) detailEl = React.createElement('div', { style: { padding: 8, fontSize: 11, color: 'var(--dsw-alias-label-tertiary,#888)' } }, '加载中…')
|
|
66
119
|
else {
|
|
67
|
-
const batches = archiveDetail.filter((b) => b.path === e.path)
|
|
120
|
+
const batches = archiveDetail.filter((b) => b.path === e.path && b.batch === e.batch)
|
|
68
121
|
detailEl = batches.map((b) => (Array.isArray(b.records) ? b.records : []).map((rec) => {
|
|
69
122
|
const lines = []
|
|
70
123
|
const hunks = Array.isArray(rec.hunks) ? rec.hunks : []
|
|
@@ -90,8 +143,8 @@ export function DiffLauncher(props) {
|
|
|
90
143
|
}))
|
|
91
144
|
}
|
|
92
145
|
}
|
|
93
|
-
return React.createElement('div', { key:
|
|
94
|
-
React.createElement('div', { style: { display: 'flex', gap: 8, alignItems: 'center', padding: '6px 8px', background: 'var(--dsw-alias-bg-layer-1,transparent)', cursor: 'pointer', flexWrap: 'wrap' }, onClick: () => { if (open) { setArchivePath(null); setArchiveDetail(null) } else { setArchivePath(
|
|
146
|
+
return React.createElement('div', { key: entryKey, style: { border: '1px solid var(--dsw-alias-border-l1,#333)', borderRadius: 8, overflow: 'hidden' } },
|
|
147
|
+
React.createElement('div', { style: { display: 'flex', gap: 8, alignItems: 'center', padding: '6px 8px', background: 'var(--dsw-alias-bg-layer-1,transparent)', cursor: 'pointer', flexWrap: 'wrap' }, onClick: () => { if (open) { detailRef.current++; setArchivePath(null); setArchiveDetail(null) } else { setArchivePath(entryKey); loadArchiveDetail(e.path, e.batch) } } },
|
|
95
148
|
React.createElement('span', { style: { flex: 1, fontSize: 12, color: 'var(--dsw-alias-label-primary,#ddd)', wordBreak: 'break-all' } }, e.displayPath ?? e.path),
|
|
96
149
|
(e.batch != null ? React.createElement('span', { style: { fontSize: 11, color: 'var(--dsw-alias-label-tertiary,#888)' } }, '批次 ' + e.batch) : null),
|
|
97
150
|
(e.reason ? React.createElement('span', { style: { fontSize: 11, color: 'var(--dsw-alias-state-warn-primary,#b7791f)' } }, e.reason) : null),
|
|
@@ -110,7 +163,13 @@ export function DiffLauncher(props) {
|
|
|
110
163
|
}, style: btn(false) }, '回滚')),
|
|
111
164
|
detailEl)
|
|
112
165
|
})
|
|
113
|
-
body = React.createElement('div', { className: 'edrv-launch-arch' }, inner
|
|
166
|
+
body = React.createElement('div', { className: 'edrv-launch-arch' }, inner,
|
|
167
|
+
archiveErr ? React.createElement('div', { style: { color: 'var(--dsw-alias-state-error-primary,#d9534f)' } }, '归档异常:' + archiveErr,
|
|
168
|
+
React.createElement('button', { onClick: () => { if (nextCursor) loadArchives(nextCursor); else loadArchives() }, style: btn(false) }, '重试')) : null,
|
|
169
|
+
nextCursor || moreBusy ? React.createElement('button', {
|
|
170
|
+
className: 'edrv-btn-mini', disabled: moreBusy || !!archiveErr,
|
|
171
|
+
onClick: () => { if (nextCursor) loadArchives(nextCursor) },
|
|
172
|
+
}, moreBusy ? '加载中…' : '加载更多') : null)
|
|
114
173
|
}
|
|
115
174
|
}
|
|
116
175
|
|