dsh-vscode-mode 0.20.1 → 0.20.3
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 +239 -49
- package/lib/client.js.map +1 -1
- package/package.json +1 -1
- package/src/client/mcpSessionState.ts +42 -15
- package/src/client/reviewBar.ts +85 -0
- package/src/client/sessionError.ts +12 -0
- package/src/client/styles/editor.css +1 -0
- package/src/client/ui/EditorView.ts +42 -9
- package/src/client/ui/McpSessionStatus.ts +22 -7
- package/src/client/ui/OfficialDiffBar.tsx +14 -10
- package/src/client/ui/ReviewAllConfirm.ts +35 -0
- package/src/client-primitives.d.ts +12 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-vscode-mode",
|
|
3
|
-
"version": "0.20.
|
|
3
|
+
"version": "0.20.3",
|
|
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": [
|
|
@@ -2,11 +2,16 @@
|
|
|
2
2
|
import { rpc } from './rpc.js'
|
|
3
3
|
import type { McpSessionStatus } from '../shared/mcp.js'
|
|
4
4
|
|
|
5
|
-
export interface SessionMcpView {
|
|
5
|
+
export interface SessionMcpView {
|
|
6
|
+
status?: McpSessionStatus
|
|
7
|
+
stale: boolean
|
|
8
|
+
/** Query lifecycle only; never a connection failure classification. */
|
|
9
|
+
reason?: 'pending' | 'paused' | 'unavailable'
|
|
10
|
+
/** Browser receipt time of the last accepted snapshot, in epoch milliseconds. */
|
|
11
|
+
observedAt?: number
|
|
12
|
+
}
|
|
6
13
|
type Snapshot = { sessions: McpSessionStatus[]; sampledAt: number }
|
|
7
14
|
type FetchStatus = (signal: AbortSignal) => Promise<Snapshot>
|
|
8
|
-
const unavailable: SessionMcpView = { stale: true }
|
|
9
|
-
const inactive: SessionMcpView = { stale: false }
|
|
10
15
|
|
|
11
16
|
/** One poller for all mounted row/hover consumers; never retains or activates sessions. */
|
|
12
17
|
export class McpSessionState {
|
|
@@ -15,14 +20,14 @@ export class McpSessionState {
|
|
|
15
20
|
private timer?: ReturnType<typeof setTimeout>
|
|
16
21
|
private request?: AbortController
|
|
17
22
|
private epoch = 0
|
|
18
|
-
private
|
|
23
|
+
private fallback: SessionMcpView = { stale: true, reason: 'pending' }
|
|
19
24
|
private disposed = false
|
|
20
25
|
|
|
21
26
|
/** @public @author ddj 2026年10月20号 @param fetchStatus Authenticated read-only query. @param page Visibility boundary, omitted for server tests. */
|
|
22
27
|
constructor(private readonly fetchStatus: FetchStatus, private readonly page?: Document) {}
|
|
23
28
|
|
|
24
29
|
/** @public @author ddj 2026年10月20号 @param sessionId Row identity. @returns Stable view until the next observation. */
|
|
25
|
-
get(sessionId: string): SessionMcpView { return this.views.get(sessionId) ??
|
|
30
|
+
get(sessionId: string): SessionMcpView { return this.views.get(sessionId) ?? this.fallback }
|
|
26
31
|
|
|
27
32
|
/** @public @author ddj 2026年10月20号 @param sessionId Row identity. @param listener React invalidation. @returns Idempotent unsubscribe; the last consumer stops polling. */
|
|
28
33
|
subscribe(sessionId: string, listener: () => void): () => void {
|
|
@@ -33,7 +38,8 @@ export class McpSessionState {
|
|
|
33
38
|
this.listeners.set(sessionId, listeners)
|
|
34
39
|
if (first) {
|
|
35
40
|
this.page?.addEventListener('visibilitychange', this.visibility)
|
|
36
|
-
|
|
41
|
+
if (this.page?.hidden) this.markStale('paused')
|
|
42
|
+
else void this.poll()
|
|
37
43
|
}
|
|
38
44
|
return () => {
|
|
39
45
|
listeners.delete(listener)
|
|
@@ -54,7 +60,7 @@ export class McpSessionState {
|
|
|
54
60
|
clearTimeout(this.timer)
|
|
55
61
|
this.timer = undefined
|
|
56
62
|
this.request?.abort()
|
|
57
|
-
this.
|
|
63
|
+
this.markStale('paused')
|
|
58
64
|
}
|
|
59
65
|
|
|
60
66
|
/** @private @author ddj 2026年10月20号 Cleans listener lifetime as well as timers. */
|
|
@@ -66,32 +72,53 @@ export class McpSessionState {
|
|
|
66
72
|
/** @public @author ddj 2026年10月20号 Permanently disposes HMR-owned polling. */
|
|
67
73
|
dispose(): void { this.disposed = true; this.pause(); this.listeners.clear(); this.views.clear() }
|
|
68
74
|
|
|
69
|
-
/** @private @author ddj 2026年10月
|
|
70
|
-
private publish(snapshot
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
75
|
+
/** @private @author ddj 2026年10月27号 @param snapshot Fresh ledger. Records browser receipt time without changing the host protocol. */
|
|
76
|
+
private publish(snapshot: Snapshot): void {
|
|
77
|
+
const observedAt = Date.now()
|
|
78
|
+
this.fallback = { stale: false, observedAt }
|
|
79
|
+
this.views = new Map(snapshot.sessions.map((status) => [status.sessionId, { status, stale: false, observedAt }]))
|
|
80
|
+
this.notify()
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** @private @author ddj 2026年10月27号 @param reason Query lifecycle, never a claim about connection health. Preserves stable references for unchanged observations. */
|
|
84
|
+
private markStale(reason: NonNullable<SessionMcpView['reason']>): void {
|
|
85
|
+
if (this.fallback.stale && this.fallback.reason === reason) return
|
|
86
|
+
this.fallback = { ...this.fallback, stale: true, reason }
|
|
87
|
+
for (const [id, view] of this.views) this.views.set(id, { ...view, stale: true, reason })
|
|
88
|
+
this.notify()
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** @private @author ddj 2026年10月27号 Notifies mounted row and hover consumers after an observation changes. */
|
|
92
|
+
private notify(): void {
|
|
74
93
|
for (const listeners of this.listeners.values()) for (const listener of listeners) listener()
|
|
75
94
|
}
|
|
76
95
|
|
|
77
96
|
/** @private @author ddj 2026年10月20号 Queries once, with a 10-second deadline and a 2.5-second gap after settlement. */
|
|
78
97
|
private async poll(): Promise<void> {
|
|
79
98
|
if (this.disposed || this.request || !this.listeners.size || this.page?.hidden) return
|
|
99
|
+
clearTimeout(this.timer)
|
|
100
|
+
this.timer = undefined
|
|
101
|
+
if (this.fallback.reason === 'paused') this.markStale('pending')
|
|
102
|
+
|
|
80
103
|
const request = new AbortController()
|
|
81
104
|
const epoch = this.epoch
|
|
82
105
|
this.request = request
|
|
83
106
|
/** @author ddj 2026年10月20号 Marks overdue responses unusable even if a transport ignores cancellation. */
|
|
84
|
-
const expire = (): void => { request.abort(); if (epoch === this.epoch) this.
|
|
107
|
+
const expire = (): void => { request.abort(); if (epoch === this.epoch) this.markStale('unavailable') }
|
|
85
108
|
const deadline = setTimeout(expire, 10000)
|
|
86
109
|
try {
|
|
87
110
|
const snapshot = await this.fetchStatus(request.signal)
|
|
88
111
|
if (epoch === this.epoch && !request.signal.aborted) this.publish(snapshot)
|
|
89
112
|
} catch {
|
|
90
|
-
if (epoch === this.epoch) this.
|
|
113
|
+
if (epoch === this.epoch) this.markStale('unavailable')
|
|
91
114
|
} finally {
|
|
92
115
|
clearTimeout(deadline)
|
|
93
116
|
this.request = undefined
|
|
94
|
-
|
|
117
|
+
// A visibility/subscription change resumes promptly, but only after the old transport settles.
|
|
118
|
+
if (!this.disposed && this.listeners.size && !this.page?.hidden) {
|
|
119
|
+
const delay = epoch === this.epoch ? 2500 : 0
|
|
120
|
+
this.timer = setTimeout(() => { void this.poll() }, delay)
|
|
121
|
+
}
|
|
95
122
|
}
|
|
96
123
|
}
|
|
97
124
|
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 官方差异栏派生视图:位置、导航提示、失效说明与 Keep 提示的唯一来源。
|
|
3
|
+
* 计数口径分离:待审是「记录数」,可跳转是「块数」,两者不同源,
|
|
4
|
+
* 全部块失效时必须给出可操作叙事,不能只留一句「无法定位」。
|
|
5
|
+
* 作者 ddj 2026年10月27号
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** 官方差异栏输入:全部来自 EditorView dock 快照,不读取磁盘或 RPC。 */
|
|
9
|
+
export interface ReviewBarInput {
|
|
10
|
+
/** 可定位(非 stale)的差异块数 */
|
|
11
|
+
jumps: number
|
|
12
|
+
/** 无法定位的历史差异块数 */
|
|
13
|
+
stale: number
|
|
14
|
+
/** 当前差异块索引(0 基) */
|
|
15
|
+
index: number
|
|
16
|
+
/** 编辑器内容是否已就绪;未就绪时不得断言差异失效 */
|
|
17
|
+
ready: boolean
|
|
18
|
+
/** 当前文件的官方待审记录数 */
|
|
19
|
+
pendingRecords: number
|
|
20
|
+
/** 当前记录是否可 Keep(detail.capability.keep) */
|
|
21
|
+
canKeep: boolean
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** 官方差异栏派生文案;调用方只负责渲染,不再自行拼装计数与提示。 */
|
|
25
|
+
export interface ReviewBarView {
|
|
26
|
+
/** 差异位置文案,如 `2 / 6`、`无可跳转`、`载入中` */
|
|
27
|
+
position: string
|
|
28
|
+
/** 上一个差异按钮提示(兼作禁用原因) */
|
|
29
|
+
navPrevTitle: string
|
|
30
|
+
/** 下一个差异按钮提示(兼作禁用原因) */
|
|
31
|
+
navNextTitle: string
|
|
32
|
+
/** 失效说明,空串表示不显示 */
|
|
33
|
+
notice: string
|
|
34
|
+
/** 说明级别:warn 表示需要用户决策 */
|
|
35
|
+
noticeTone: 'info' | 'warn'
|
|
36
|
+
/** Keep 按钮提示 */
|
|
37
|
+
keepHint: string
|
|
38
|
+
/** 待审计数提示 */
|
|
39
|
+
pendingTitle: string
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** @private @author ddj 2026年10月27号 @param value 任意计数输入 @returns 非负整数,NaN/负数/非数字归零 */
|
|
43
|
+
function safeCount(value: unknown): number {
|
|
44
|
+
const parsed = Number(value)
|
|
45
|
+
return Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : 0
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** @private @author ddj 2026年10月27号 @returns 无差异可跳转时的 Keep 提示 */
|
|
49
|
+
function fullStaleKeepHint(): string {
|
|
50
|
+
return '保留该记录的全部改动并结案:这些改动已不在文件中,记录将被标记为已采纳(不改写磁盘)'
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* 派生差异栏文案。
|
|
55
|
+
* @public
|
|
56
|
+
* @author ddj 2026年10月27号
|
|
57
|
+
* @param input 差异计数、就绪状态与 Keep 能力
|
|
58
|
+
* @returns 栏内位置、导航提示与失效说明
|
|
59
|
+
*/
|
|
60
|
+
export function reviewBarView(input: ReviewBarInput): ReviewBarView {
|
|
61
|
+
const jumps = safeCount(input.jumps)
|
|
62
|
+
const stale = safeCount(input.stale)
|
|
63
|
+
const index = safeCount(input.index)
|
|
64
|
+
const pendingTitle = `${safeCount(input.pendingRecords)} 条官方待审记录` + (input.ready ? `,本文件可跳转 ${jumps} 处` : '')
|
|
65
|
+
const keepHint = jumps === 0 && stale > 0 && input.canKeep ? fullStaleKeepHint() : '保留当前记录的未处理修改'
|
|
66
|
+
|
|
67
|
+
// 载入期所有块天然 stale,此时只能报告进度,不能断言改动已失效。
|
|
68
|
+
if (input.ready !== true) {
|
|
69
|
+
return { position: '载入中', navPrevTitle: '文件载入中', navNextTitle: '文件载入中', notice: '', noticeTone: 'info', keepHint, pendingTitle }
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (jumps > 0) {
|
|
73
|
+
const at = Math.min(index + 1, jumps)
|
|
74
|
+
const notice = stale > 0 ? `${stale} 处历史差异无法定位(其余 ${jumps} 处可跳转),展开详情查看原始增删。` : ''
|
|
75
|
+
return { position: `${at} / ${jumps}`, navPrevTitle: '上一个差异', navNextTitle: '下一个差异', notice, noticeTone: 'info', keepHint, pendingTitle }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (stale > 0) {
|
|
79
|
+
const reason = '该记录的改动已不在当前文件中,无可跳转差异'
|
|
80
|
+
const notice = `本记录 ${stale} 处改动均已不在当前文件中(被后续编辑覆盖或已撤销);可点 Keep 结案(只标记已采纳,不改写磁盘),或展开详情核对原始增删。`
|
|
81
|
+
return { position: '无可跳转', navPrevTitle: reason, navNextTitle: reason, notice, noticeTone: 'warn', keepHint, pendingTitle }
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return { position: '—', navPrevTitle: '当前文件没有可定位差异', navNextTitle: '当前文件没有可定位差异', notice: '', noticeTone: 'info', keepHint, pendingTitle }
|
|
85
|
+
}
|
|
@@ -36,3 +36,15 @@ export function sessionMissingDetail(): string {
|
|
|
36
36
|
return '该会话可能正被其他 DSH 实例(桌面端 / Web 端)占用,或尚未在当前实例激活。' +
|
|
37
37
|
'请在持有该会话的一端查看,或在本端打开该会话后重试。'
|
|
38
38
|
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* 把任意错误文本归一为可展示文案:会话缺失类替换为终态提示,其余原样返回。
|
|
42
|
+
* 差异栏等狭窄区域共用,避免裸「会话不存在」与业务提示混排。
|
|
43
|
+
* @author ddj 2026年10月27号
|
|
44
|
+
* @param text 任意错误文本(host 业务 error、异常 message 等原始形态)
|
|
45
|
+
* @returns 会话缺失为「标题:详情」,其他错误为原文本,空值返回空串
|
|
46
|
+
*/
|
|
47
|
+
export function sessionErrorText(text: unknown): string {
|
|
48
|
+
if (isSessionMissingError(text)) return sessionMissingTitle() + ':' + sessionMissingDetail()
|
|
49
|
+
return String(text ?? '')
|
|
50
|
+
}
|
|
@@ -304,6 +304,7 @@
|
|
|
304
304
|
[data-edrv-diff-dock-shell] .edrv-reviewbar button:focus-visible { outline: 2px solid var(--dsw-alias-brand-primary, #3b63d9); outline-offset: 2px; }
|
|
305
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
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-notice-warn { color: var(--dsw-alias-state-warning-primary, #a86a1f); }
|
|
307
308
|
[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
309
|
@container edrv-review (max-width: 420px) {
|
|
309
310
|
.edrv-reviewbar .edrv-review-heading { gap: 6px; padding-inline: 10px; }
|
|
@@ -24,6 +24,7 @@ import { dbg, isDebug, rpc } from '../rpc.js'
|
|
|
24
24
|
import { refreshFeed, subscribeErrors, subscribeRecords } from '../state/recordsFeed.js'
|
|
25
25
|
import { subscribeReview, refreshReview } from '../state/reviewFeed.js'
|
|
26
26
|
import { runReviewBatch } from '../reviewBatch.js'
|
|
27
|
+
import { ReviewAllConfirm } from './ReviewAllConfirm.js'
|
|
27
28
|
import { reviewRegions } from '../../shared/reviewRegions.js'
|
|
28
29
|
import { diffSummary, officialPath } from '../state/diffSummary.js'
|
|
29
30
|
import { emitFileChanged, emitRefresh } from '../events.js'
|
|
@@ -126,7 +127,10 @@ import { buildBpMenu } from '../dap/bpMenu.js'
|
|
|
126
127
|
import { clampToolbarPosition, loadToolbarPosition, saveToolbarPosition } from '../dap/toolbarDrag.js'
|
|
127
128
|
import { editorModelPathOf } from '../dap/modelPath.js'
|
|
128
129
|
import { dapTrace } from '../dap/trace.js'
|
|
129
|
-
import { isSessionMissingError, sessionMissingDetail, sessionMissingTitle } from '../sessionError.js'
|
|
130
|
+
import { isSessionMissingError, sessionErrorText, sessionMissingDetail, sessionMissingTitle } from '../sessionError.js'
|
|
131
|
+
|
|
132
|
+
/** 官方差异读取失败前缀:仅用于清理同源陈旧错误,不吞并保存类错误。@author ddj 2026年10月27号 */
|
|
133
|
+
const REVIEW_LOAD_ERROR = '官方差异读取失败:'
|
|
130
134
|
|
|
131
135
|
/**
|
|
132
136
|
* 只在 Lua 注解类型槽开启注释自动建议;跨文件/离开类型槽立即恢复默认。
|
|
@@ -445,6 +449,7 @@ export function EditorView(props) {
|
|
|
445
449
|
const hoverPanelRef = React.useRef(false) // 鼠标是否已进入 Keep/Undo 浮层
|
|
446
450
|
const batchBusyRef = React.useRef(false) // 批量 Keep All/Undo All 防重入
|
|
447
451
|
const [reviewBatchBusy, setReviewBatchBusy] = React.useState(false)
|
|
452
|
+
const [reviewAllPrompt, setReviewAllPrompt] = React.useState(null)
|
|
448
453
|
const reviewLifeRef = React.useRef(null)
|
|
449
454
|
React.useEffect(() => {
|
|
450
455
|
const token = { live: true }
|
|
@@ -586,7 +591,9 @@ export function EditorView(props) {
|
|
|
586
591
|
if (!result.ok) throw new Error(result.error)
|
|
587
592
|
setOfficialRecords((previous) => JSON.stringify(previous) === JSON.stringify(page.page.items) ? previous : page.page.items)
|
|
588
593
|
setOfficialDetail((previous) => JSON.stringify(previous) === JSON.stringify(result.detail) ? previous : result.detail)
|
|
589
|
-
|
|
594
|
+
// 读取成功后清除同源陈旧错误,避免会话恢复后仍挂着上一次失败提示。
|
|
595
|
+
setError((previous) => (typeof previous === 'string' && previous.startsWith(REVIEW_LOAD_ERROR) ? null : previous))
|
|
596
|
+
} catch (cause) { if (!controller.signal.aborted) setError(REVIEW_LOAD_ERROR + String(cause)) }
|
|
590
597
|
}
|
|
591
598
|
void loadReview()
|
|
592
599
|
return () => controller.abort()
|
|
@@ -3272,9 +3279,7 @@ export function EditorView(props) {
|
|
|
3272
3279
|
async function actReviewAll(reject) {
|
|
3273
3280
|
if (batchBusyRef.current) return
|
|
3274
3281
|
if (!review.page?.overview) { setError('官方待审列表尚未就绪,未执行批量操作'); return }
|
|
3275
|
-
|
|
3276
|
-
if (!count) return
|
|
3277
|
-
if (!window.confirm(`${reject ? 'Undo All' : 'Keep All'}:处理当前工作区全部 ${count} 个待审差异文件(含其他轮次)?${reject ? '\n撤销会改写磁盘;逐项安全校验,遇到冲突停止,已完成项不会自动恢复。' : '\n只标记待审修改为保留,不改写文件。'}`)) return
|
|
3282
|
+
if (!sum.pendingFiles.length) return
|
|
3278
3283
|
const token = reviewLifeRef.current
|
|
3279
3284
|
const undoPath = activeRef.current
|
|
3280
3285
|
const undoModel = editorRef.current?.getModel?.()
|
|
@@ -3301,8 +3306,28 @@ export function EditorView(props) {
|
|
|
3301
3306
|
} catch (cause) { if (token?.live) setError(String(cause)) }
|
|
3302
3307
|
finally { batchBusyRef.current = false; if (token?.live) setReviewBatchBusy(false) }
|
|
3303
3308
|
}
|
|
3304
|
-
|
|
3305
|
-
|
|
3309
|
+
/** @private @author ddj 2026年10月27号 @param reject Whether to undo pending files. @description Open a DSH confirmation without starting any mutations. */
|
|
3310
|
+
function askReviewAll(reject) {
|
|
3311
|
+
if (batchBusyRef.current || !sum.pendingFiles.length) return
|
|
3312
|
+
if (!review.page?.overview) { setError('官方待审列表尚未就绪,未执行批量操作'); return }
|
|
3313
|
+
setReviewAllPrompt({ reject, count: sum.pendingFiles.length, token: reviewLifeRef.current })
|
|
3314
|
+
}
|
|
3315
|
+
|
|
3316
|
+
/** @private @author ddj 2026年10月27号 @description Dismiss the pending confirmation without changing review state. */
|
|
3317
|
+
function cancelReviewAll() {
|
|
3318
|
+
setReviewAllPrompt(null)
|
|
3319
|
+
}
|
|
3320
|
+
|
|
3321
|
+
/** @private @author ddj 2026年10月27号 @description Confirm only within the session lifetime that opened the modal. @returns The guarded batch operation, if still valid. */
|
|
3322
|
+
function confirmReviewAll() {
|
|
3323
|
+
const prompt = reviewAllPrompt
|
|
3324
|
+
setReviewAllPrompt(null)
|
|
3325
|
+
if (!prompt?.token?.live || prompt.token !== reviewLifeRef.current) return
|
|
3326
|
+
return actReviewAll(prompt.reject)
|
|
3327
|
+
}
|
|
3328
|
+
|
|
3329
|
+
const acceptAllFiles = () => askReviewAll(false)
|
|
3330
|
+
const undoAllFiles = () => askReviewAll(true)
|
|
3306
3331
|
const rollbackFile = () => {
|
|
3307
3332
|
if (!active) return
|
|
3308
3333
|
if (!window.confirm('回滚当前文件到修改前状态?(相关差异将归档)')) return
|
|
@@ -4462,12 +4487,13 @@ export function EditorView(props) {
|
|
|
4462
4487
|
* @returns 含布局及操作回调的差异快照
|
|
4463
4488
|
*/
|
|
4464
4489
|
const officialFile = review.page?.overview?.coveredPaths.find((path) => sameFile(path, active))
|
|
4490
|
+
const rawReviewError = review.error || error
|
|
4465
4491
|
const buildDockSnapshot = () => ({
|
|
4466
4492
|
mode: editorDockMode(active),
|
|
4467
4493
|
official: officialFile ? { sessionId, page: review.page, path: officialFile,
|
|
4468
4494
|
pending: sum.files.find((file) => sameFile(file.path, active))?.pending ?? 0,
|
|
4469
4495
|
detail: officialDetail, records: officialRecords, onSelect: setSelectedReview } : null,
|
|
4470
|
-
reviewError:
|
|
4496
|
+
reviewError: rawReviewError ? sessionErrorText(rawReviewError) : undefined,
|
|
4471
4497
|
layout,
|
|
4472
4498
|
pendingRegions,
|
|
4473
4499
|
staleRegions,
|
|
@@ -4487,6 +4513,7 @@ export function EditorView(props) {
|
|
|
4487
4513
|
onRefresh: reloadFile,
|
|
4488
4514
|
activePath: active,
|
|
4489
4515
|
diffIdx: contentReady ? diffIdx : 0,
|
|
4516
|
+
contentReady,
|
|
4490
4517
|
diffTotal: displayDiffTotal(contentReady, pendingRegions.length, sum.files.find((file) => sameFile(file.path, active))?.pending ?? 0),
|
|
4491
4518
|
fileIdx,
|
|
4492
4519
|
fileTotal: sum.pendingFiles.length,
|
|
@@ -5015,5 +5042,11 @@ export function EditorView(props) {
|
|
|
5015
5042
|
snippetPickerEl,
|
|
5016
5043
|
dapBpMenuEl,
|
|
5017
5044
|
dapBpEditEl)
|
|
5018
|
-
|
|
5045
|
+
const reviewAllEl = reviewAllPrompt?.token?.live && reviewAllPrompt.token === reviewLifeRef.current
|
|
5046
|
+
? React.createElement(ReviewAllConfirm, {
|
|
5047
|
+
reject: reviewAllPrompt.reject, count: reviewAllPrompt.count,
|
|
5048
|
+
onCancel: cancelReviewAll, onConfirm: confirmReviewAll,
|
|
5049
|
+
})
|
|
5050
|
+
: null
|
|
5051
|
+
return React.createElement(React.Fragment, null, rootEl, reviewAllEl)
|
|
5019
5052
|
}
|
|
@@ -20,7 +20,7 @@ function useMcpView({ sessionId, store }: McpRowProps): SessionMcpView {
|
|
|
20
20
|
|
|
21
21
|
/** @public @author ddj 2026年10月20号 @param view Current observation. @returns Accessible row label; steady states deliberately do not occupy the leading slot. */
|
|
22
22
|
export function mcpRowLabel(view: SessionMcpView): string | undefined {
|
|
23
|
-
if (view.stale) return
|
|
23
|
+
if (view.stale) return undefined
|
|
24
24
|
if (!view.status || (!busy.has(view.status.phase) && view.status.phase !== 'error')) return undefined
|
|
25
25
|
return '项目 MCP:' + labels[view.status.phase]
|
|
26
26
|
}
|
|
@@ -30,7 +30,7 @@ export function McpSessionIcon(props: McpRowProps): React.ReactElement | null {
|
|
|
30
30
|
const view = useMcpView(props)
|
|
31
31
|
const label = mcpRowLabel(view)
|
|
32
32
|
if (!label) return null
|
|
33
|
-
const warning = view.
|
|
33
|
+
const warning = view.status?.phase === 'error'
|
|
34
34
|
return React.createElement('span', {
|
|
35
35
|
role: 'img', 'aria-label': label, title: label,
|
|
36
36
|
style: { display: 'inline-flex', width: 16, height: 16, alignItems: 'center', justifyContent: 'center',
|
|
@@ -41,6 +41,13 @@ export function McpSessionIcon(props: McpRowProps): React.ReactElement | null {
|
|
|
41
41
|
/** @private @author ddj 2026年10月20号 @param start Stage start. @param end Stage finish or local clock. @returns Human-readable elapsed time, without claiming a timeout. */
|
|
42
42
|
function elapsed(start: number, end: number): string { return Math.max(0, (end - start) / 1000).toFixed(1) + ' 秒' }
|
|
43
43
|
|
|
44
|
+
/** @private @author ddj 2026年10月27号 @param view Query observation. @returns Query-only explanation, separate from initialization health. */
|
|
45
|
+
function queryMessage(view: SessionMcpView): string {
|
|
46
|
+
if (view.reason === 'paused') return '状态查询已暂停'
|
|
47
|
+
if (view.reason === 'pending') return '正在查询项目 MCP 状态'
|
|
48
|
+
return '状态查询暂不可用,不代表 MCP 初始化失败'
|
|
49
|
+
}
|
|
50
|
+
|
|
44
51
|
/** @public @author ddj 2026年10月20号 @param props Official hover sessionId and poller. @returns Project-only diagnostic detail, never transport credentials. */
|
|
45
52
|
export function McpSessionDetail(props: McpRowProps): React.ReactElement {
|
|
46
53
|
const view = useMcpView(props)
|
|
@@ -53,10 +60,18 @@ export function McpSessionDetail(props: McpRowProps): React.ReactElement {
|
|
|
53
60
|
}, [status, view.stale])
|
|
54
61
|
|
|
55
62
|
const children: React.ReactNode[] = [React.createElement('strong', { key: 'title' }, '项目 MCP')]
|
|
56
|
-
if (view.stale)
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
63
|
+
if (view.stale) {
|
|
64
|
+
children.push(React.createElement('div', { key: 'stale', role: 'status' }, queryMessage(view)))
|
|
65
|
+
if (status) children.push(React.createElement('div', { key: 'history' }, '以下为上次观测结果,不代表当前状态'))
|
|
66
|
+
if (view.observedAt !== undefined) children.push(React.createElement('div', { key: 'observed' },
|
|
67
|
+
'上次观测时间:' + new Date(view.observedAt).toLocaleString()))
|
|
68
|
+
}
|
|
69
|
+
if (!status && !view.stale) children.push(React.createElement('div', { key: 'empty' }, '当前无活跃 MCP 实例'))
|
|
70
|
+
if (status) {
|
|
71
|
+
// Unobserved time must not look like continued initialization, including per-server timing.
|
|
72
|
+
let observedNow = now
|
|
73
|
+
if (view.stale) observedNow = view.observedAt ?? status.phaseAt
|
|
74
|
+
const end = status.finishedAt ?? observedNow
|
|
60
75
|
const completed = status.servers.filter((server) => server.phase === 'ready').length
|
|
61
76
|
const slow = !view.stale && busy.has(status.phase) && now - status.phaseAt > 10000
|
|
62
77
|
children.push(React.createElement('div', { key: 'phase' }, labels[status.phase], slow ? '(耗时较长,不代表失败)' : ''))
|
|
@@ -64,7 +79,7 @@ export function McpSessionDetail(props: McpRowProps): React.ReactElement {
|
|
|
64
79
|
if (status.configMs !== undefined) children.push(React.createElement('div', { key: 'config' }, `配置准备 ${elapsed(0, status.configMs)}`))
|
|
65
80
|
if (status.error) children.push(React.createElement('div', { key: 'error' }, status.error))
|
|
66
81
|
for (const server of status.servers) children.push(React.createElement('div', { key: 'server:' + server.serverName },
|
|
67
|
-
`${server.serverName}:${labels[server.phase]}${server.reused ? ' · 复用已有连接' : ''}${server.phase === 'idle' ? '' : ' · ' + elapsed(server.startedAt, server.finishedAt ??
|
|
82
|
+
`${server.serverName}:${labels[server.phase]}${server.reused ? ' · 复用已有连接' : ''}${server.phase === 'idle' ? '' : ' · ' + elapsed(server.startedAt, server.finishedAt ?? observedNow)}`,
|
|
68
83
|
server.queuedMs === undefined ? '' : ' · 排队 ' + elapsed(0, server.queuedMs),
|
|
69
84
|
server.connectMs === undefined ? '' : ' · 初始化 ' + elapsed(0, server.connectMs),
|
|
70
85
|
server.firstCallMs === undefined ? '' : ' · 首次工具执行 ' + elapsed(0, server.firstCallMs),
|
|
@@ -2,6 +2,7 @@ import React from 'react'
|
|
|
2
2
|
import { Button, Menu } from '@deepseek-ai/dsh-client-ui-primitives'
|
|
3
3
|
import type { ReviewDetail, ReviewItem, ReviewPage } from '../../shared/review.js'
|
|
4
4
|
import { ReviewLedger } from './OfficialReview.js'
|
|
5
|
+
import { reviewBarView } from '../reviewBar.js'
|
|
5
6
|
|
|
6
7
|
export interface OfficialBarProps {
|
|
7
8
|
official: { sessionId: string; path: string; pending: number; detail?: ReviewDetail; records?: ReviewItem[]; page: ReviewPage; onSelect: (id: string) => void }
|
|
@@ -9,6 +10,8 @@ export interface OfficialBarProps {
|
|
|
9
10
|
pendingRegions?: unknown[]
|
|
10
11
|
staleRegions?: unknown[]
|
|
11
12
|
diffIdx?: number
|
|
13
|
+
/** 编辑器内容是否就绪;未就绪时不得断言差异失效 */
|
|
14
|
+
contentReady?: boolean
|
|
12
15
|
fileTotal?: number
|
|
13
16
|
fileIdx?: number
|
|
14
17
|
allPendingCount?: number
|
|
@@ -61,7 +64,8 @@ export function OfficialDiffBar(props: OfficialBarProps): React.ReactElement {
|
|
|
61
64
|
const detail = official.detail
|
|
62
65
|
const records = official.records ?? []
|
|
63
66
|
const count = pendingRegions.length
|
|
64
|
-
const
|
|
67
|
+
const view = reviewBarView({ jumps: count, stale: staleRegions.length, index: diffIdx, ready: props.contentReady === true,
|
|
68
|
+
pendingRecords: official.pending, canKeep: detail?.capability.keep === true })
|
|
65
69
|
|
|
66
70
|
/** @private @author ddj 2026年10月27号 @returns Close current popup without changing selection. */
|
|
67
71
|
function closeMenu(): void { setMenu(null) }
|
|
@@ -69,9 +73,9 @@ export function OfficialDiffBar(props: OfficialBarProps): React.ReactElement {
|
|
|
69
73
|
function selectRecord(id: string): void { official.onSelect(id); closeMenu() }
|
|
70
74
|
/** @private @author ddj 2026年10月27号 @param tab Existing launcher tab. @returns Open existing overview, never create another review entry. */
|
|
71
75
|
function openOverview(tab: string): void { props.onOpenLauncher(tab); closeMenu() }
|
|
72
|
-
/** @private @author ddj 2026年10月27号 @param mark Navigation icon. @param label Accessible action. @param action Existing callback. @param disabled Whether navigation is unavailable. @returns Compact keyboard-focusable navigation control. */
|
|
73
|
-
function navButton(mark: Mark, label: string, action: () => void, disabled: boolean): React.ReactElement {
|
|
74
|
-
return <Button size="sm" variant="ghost" className="edrv-review-icon" aria-label={label} title={label} disabled={disabled} onClick={action}>{barIcon(mark)}</Button>
|
|
76
|
+
/** @private @author ddj 2026年10月27号 @param mark Navigation icon. @param label Accessible action. @param action Existing callback. @param disabled Whether navigation is unavailable. @param title Hover reason, defaults to the label. @returns Compact keyboard-focusable navigation control. */
|
|
77
|
+
function navButton(mark: Mark, label: string, action: () => void, disabled: boolean, title?: string): React.ReactElement {
|
|
78
|
+
return <Button size="sm" variant="ghost" className="edrv-review-icon" aria-label={label} title={title || label} disabled={disabled} onClick={action}>{barIcon(mark)}</Button>
|
|
75
79
|
}
|
|
76
80
|
|
|
77
81
|
const recordTrigger = <Button size="sm" variant="ghost" className="edrv-review-record" aria-label="选择官方差异记录" aria-haspopup="menu" aria-expanded={menu === 'records'} disabled={!records.length} onClick={() => setMenu(menu === 'records' ? null : 'records')}>
|
|
@@ -85,7 +89,7 @@ export function OfficialDiffBar(props: OfficialBarProps): React.ReactElement {
|
|
|
85
89
|
<div className="edrv-review-heading">
|
|
86
90
|
<span className="edrv-review-filemark">{barIcon('file')}</span>
|
|
87
91
|
<span className="edrv-review-filename" title={props.activePath}>{name}</span>
|
|
88
|
-
<span className="edrv-review-pending" title={
|
|
92
|
+
<span className="edrv-review-pending" title={view.pendingTitle}>待审 <strong>{official.pending}</strong></span>
|
|
89
93
|
<Menu open={menu === 'records'} anchor={recordTrigger} items={records.map((item) => ({ id: item.id, label: `轮次 ${item.turn} / 事件 ${item.seq}` }))} selectedId={detail?.item.id} onSelect={selectRecord} onClose={closeMenu} side="top" align="end" portal compact autoFocus />
|
|
90
94
|
</div>
|
|
91
95
|
<div className="edrv-review-tools">
|
|
@@ -94,18 +98,18 @@ export function OfficialDiffBar(props: OfficialBarProps): React.ReactElement {
|
|
|
94
98
|
{navButton('right', '下一个差异文件', props.onNextFile, !props.fileTotal)}
|
|
95
99
|
</div>
|
|
96
100
|
<div className="edrv-review-nav edrv-review-blocknav" role="group" aria-label="差异导航">
|
|
97
|
-
{navButton('up', '上一个差异', props.onPrevDiff, !count)}
|
|
98
|
-
<span className="edrv-review-position" aria-live="polite">{position}</span>
|
|
99
|
-
{navButton('down', '下一个差异', props.onNextDiff, !count)}
|
|
101
|
+
{navButton('up', '上一个差异', props.onPrevDiff, !count, view.navPrevTitle)}
|
|
102
|
+
<span className="edrv-review-position" aria-live="polite">{view.position}</span>
|
|
103
|
+
{navButton('down', '下一个差异', props.onNextDiff, !count, view.navNextTitle)}
|
|
100
104
|
</div>
|
|
101
105
|
<div className="edrv-review-actions">
|
|
102
106
|
<Button size="sm" variant="ghost" className="edrv-review-undo" icon={barIcon('undo')} onClick={props.onUndoFile} disabled={props.reviewBatchBusy || !detail?.capability.undo} title={detail?.capability.reason}>Undo</Button>
|
|
103
|
-
<Button size="sm" variant="primary" className="edrv-review-keep" icon={barIcon('check')} onClick={props.onAcceptFile} disabled={props.reviewBatchBusy || !detail?.capability.keep} title=
|
|
107
|
+
<Button size="sm" variant="primary" className="edrv-review-keep" icon={barIcon('check')} onClick={props.onAcceptFile} disabled={props.reviewBatchBusy || !detail?.capability.keep} title={detail?.capability.keep ? view.keepHint : detail?.capability.reason}>Keep</Button>
|
|
104
108
|
<Button size="sm" variant="ghost" className="edrv-review-details" onClick={() => setExpanded(!expanded)} aria-expanded={expanded} aria-label={expanded ? '收起差异' : '展开差异'} title={expanded ? '收起差异' : '展开差异'}>详情{barIcon(expanded ? 'up' : 'down')}</Button>
|
|
105
109
|
<Menu open={menu === 'more'} anchor={moreTrigger} items={[{ id: 'pending', label: '差异总览' }, { id: 'archive', label: '归档 / 历史' }]} onSelect={openOverview} onClose={closeMenu} side="top" align="end" portal compact autoFocus />
|
|
106
110
|
</div>
|
|
107
111
|
</div>
|
|
108
|
-
{detail &&
|
|
112
|
+
{detail && view.notice ? <div className={view.noticeTone === 'warn' ? 'edrv-review-notice edrv-review-notice-warn' : 'edrv-review-notice'} role="status">{view.notice}</div> : null}
|
|
109
113
|
{props.reviewError ? <div className="edrv-review-notice edrv-review-error" role="alert">{props.reviewError}</div> : null}
|
|
110
114
|
{expanded ? <div className="edrv-review-body"><ReviewLedger key={`${official.sessionId}:${official.path}`} sessionId={official.sessionId} first={official.page} path={official.path} /></div> : null}
|
|
111
115
|
</div>
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { Button, Modal } from '@deepseek-ai/dsh-client-ui-primitives'
|
|
3
|
+
|
|
4
|
+
interface ReviewAllProps {
|
|
5
|
+
reject: boolean
|
|
6
|
+
count: number
|
|
7
|
+
onCancel: () => void
|
|
8
|
+
onConfirm: () => void
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @public
|
|
13
|
+
* @author ddj 2026年10月27号
|
|
14
|
+
* @description Confirm workspace-wide review through the DSH modal; dismissing never applies a decision.
|
|
15
|
+
* @param props Pending file count, action and explicit confirmation callbacks.
|
|
16
|
+
* @returns DSH-themed confirmation with distinct Keep and Undo safety descriptions.
|
|
17
|
+
*/
|
|
18
|
+
export function ReviewAllConfirm(props: ReviewAllProps): React.ReactElement {
|
|
19
|
+
const action = props.reject ? 'Undo All' : 'Keep All'
|
|
20
|
+
const warning = props.reject
|
|
21
|
+
? '撤销会改写磁盘;逐项安全校验,遇到冲突停止,已完成项不会自动恢复。'
|
|
22
|
+
: '只标记待审修改为保留,不改写文件。'
|
|
23
|
+
const footer = React.createElement(React.Fragment, null,
|
|
24
|
+
React.createElement(Button, { variant: 'ghost', onClick: props.onCancel, autoFocus: true }, '取消'),
|
|
25
|
+
React.createElement(Button, { variant: 'primary', onClick: props.onConfirm }, '确认 ' + action))
|
|
26
|
+
|
|
27
|
+
return React.createElement(Modal, {
|
|
28
|
+
open: true,
|
|
29
|
+
title: action + ' 确认',
|
|
30
|
+
closeLabel: '取消',
|
|
31
|
+
onClose: props.onCancel,
|
|
32
|
+
description: `处理当前工作区全部 ${props.count} 个待审差异文件(含其他轮次)?`,
|
|
33
|
+
footer,
|
|
34
|
+
}, React.createElement('p', null, warning))
|
|
35
|
+
}
|
|
@@ -14,6 +14,18 @@ declare module '@deepseek-ai/dsh-client-ui-primitives' {
|
|
|
14
14
|
icon?: import('react').ReactNode
|
|
15
15
|
}>
|
|
16
16
|
|
|
17
|
+
/** Centered body-portaled dialog; mirrors official Modal.d.ts. @author ddj 2026年10月27号 */
|
|
18
|
+
export const Modal: import('react').ComponentType<{
|
|
19
|
+
open: boolean
|
|
20
|
+
onClose: () => void
|
|
21
|
+
title: string
|
|
22
|
+
description?: string
|
|
23
|
+
children?: import('react').ReactNode
|
|
24
|
+
footer?: import('react').ReactNode
|
|
25
|
+
className?: string
|
|
26
|
+
contentClassName?: string
|
|
27
|
+
} & ({ headless: true; closeLabel?: never } | { headless?: false; closeLabel: string })>
|
|
28
|
+
|
|
17
29
|
/** Anchored selectable menu; mirrors the subset used from official Menu.d.ts. @author ddj 2026年10月27号 */
|
|
18
30
|
export const Menu: import('react').ComponentType<{
|
|
19
31
|
open: boolean
|